Archive for the ‘Graphics’ Category
Don’t you hate it when you’re writing some new image loading or texture drawing code but don’t have any suitable test images? I always waste lots of time looking for a “nice” image to test with, and often end up drawing something with distict pixel values so I can pinpoint where any given image loading bug is. With that in mind I’ve spent a few evenings working on a proper “test card”.
After I upgraded to a 720p display format (rather than just 800×600) the framerate took a little bit of a dip on my slower machine. Understandable really as it’s drawing quite a few more pixels – 921k rather than 480k in the worst case, ignoring overdraw. I’ve spent the last few days optimising the renderer to see how much of the performance I could get back.
Firstly, you’ve got to have some numbers to see what’s working and what isn’t, so I added a debug overlay which displays all kind of renderer stats:
The top four boxes show stats for the four separate sprite groups – main sprites are visible ones like the helicopters and landscape, lightmap sprites contains lights and shadows, sonar sprites are used for sonar drawing, and hud sprites are those that make up the gui and other hud elements like the health and fuel bars. The final box at the bottom shows per-frame stats such as the total number of sprites drawn and the framerate.
Most suprising is the “average batch size” for the whole frame – at only 4.1 that means that I’m only drawing about four sprites per draw call, which is quite bad. (Although I call them sprites there’s actually a whole range of “drawables” in the scene, water ripples for example are made of RingGeometry which is a ring made up of many individual triangles, but it’s easier to call them all sprites).
Individual sprite images (such as a building or a person) are packed into sprite sheets at compile time. In theory that means that you can draw lots of different sprites in the same batch because they’re all from the same texture. If however you’re drawing a building but then need to draw a person and the person is on a different sheet, then the batch is “broken” and it’s got to start a new one.
To test this out I increased the sprite sheet size from 512×512 to 1024×1024 and then 2048×2048. For the main sprites (which is the one I’m focusing on) this pushed the average batch count up from 5.3 to 5.6 and then 16.2. Obviously the texture switching was hurting my batch count – 16 would be a much more respectable figure. Unfortunately not everyone’s graphics card can load textures that big, which is why I’d been sticking to a nice safe 512×512.
However further investigation found that the sprite sheets weren’t being packed terribly efficiently – in fact packing would give up when one sprite wouldn’t fit and a new sheet would be started. This would mean that most sheets were only about half full – fixing the bug means that almost all of the sheet is now used. Below you can see one of the fixed sheets, with almost all the space used up.
Along with this I split my sheets into three groups – one for the landscape sprites (the grass and coast line), one for world sprites (helicopters and people) and one for gui sprites. Since these are drawn in distinct layers it makes sense to keep them all together on the same sheets rather than randomly intermingling them.
One last tweak was to shave off a few dead pixels on some of the larger sprites – since they were 260×260 it meant that only one could fit onto a sheet and would leave lots of wasted space. Trimming them down to 250×250 fits four in a single sheet and is much more efficient.
Overall these upped the batch count for main sprites up to a much more healthy 9.2, reducing the number of batches from ~280 to ~130.
Good, but there’s still optimisations left to be done…
Here’s a video I knocked up a little while back but never got around to posting here, it’s of the work-in-progress Rescue Squad 2.
It’s the first proper level I’ve done now I’ve got my map editor sorted and can properly lay out maps with a landscape to put the buildings and other objects on top of. I’ve also embedded Jython for scripting, so each stage loads a single jython file to control the objectives and tell the player what to do.
For the last few days I’ve taken some time off from the AI work to mess around with some graphical effects, and in particular I’ve been experimenting with a 2d ambient shadows effect. This is inspired by the Screen Space Ambient Occulsion (SSAO) effect which has gotten popular lately, and is largely a translation and adaptation of it into a 2d world.
To start with, here’s my test scene (unrelated to the current platformer/ai work):
That’s a whole bunch of tightly packed parallax layers with some trees and letters interleaved between them. The parallax is quite subtle, so it’s mostly lost in a static shot but it creates a nice 3d effect when scrolling.
The first step is to also generate a depth map from this. Since we’re in 2d and we don’t have a z-buffer, we can fake one with a simple shader to tint the sprites based on their depth.
(I’ve artificially tweeked the colour levels in the above to exagerate the layers, as otherwise you only really see white objects on a black background). It looks a bit jaggier than the base colour because we clamp the alpha values of the sprite textures to either be zero or one in the depth shader as otherwise the semi-transparent pixels introduce errors in the next step.
Next is the real magic, we apply the ambient shader. This accepts the previously generated depth texture as an input. For each fragment it looks up it’s base depth, then samples a number of surrounding texels and finds their depth as well. Surrounding depths which are higher than our base depth (i.e. it’s from a surface in front of us) darken our ambient shadow factor. We also apply a cutoff for this test so that depths which are really far in front get ignored as we decide that their shadow won’t be cast onto our current pixel. Surrounding depths lower (i.e. behind) our base depth are ignored.
Surrounding texels are found using precalculated poisson disc offsets in a similar way to traditional growable blur. We also apply a constant offset to these samples so that the shadows appear dropped slightly down-left of the shadow casters.
This produces the raw ambient map:
You can see how the grass layers are much more clearly defined and that letters both cast shadows onto trees behind them and receive shadows from trees in front as well.
Since this is a little noisy, we apply a simple blur to the raw ambient map:
Then as a final stage we combine the blurred ambient map with the colour map (and any other layers, like a bloom map) to the framebuffer:
A pretty neat effect I think – it’s certainly got a lot more depth than the basic colour version, and the shadows on moving objects really help them feel like they’re part of the world.
And if anyone wants to play around with this, here’s the GLSL shader to generate the raw ambient map:
uniform sampler2D depthMap;
const int numSamples = 16;
const float divisor = 1.0 / float(numSamples);
// Our generated poisson disc sample offsets
samples = vec2(0.007937789, 0.73124397);
samples = vec2(-0.10177308, -0.6509396);
samples = vec2(-0.9906806, -0.63400936);
samples = vec2(-0.5583586, -0.3614012);
samples = vec2(0.7163085, 0.22836149);
samples = vec2(-0.65210974, 0.37117887);
samples = vec2(-0.12714535, 0.112056136);
samples = vec2(0.48898065, -0.66669613);
samples = vec2(-0.9744036, 0.9155904);
samples = vec2(0.9274436, -0.9896486);
samples = vec2(0.9782181, 0.90990245);
samples = vec2(0.96427417, -0.25506377);
samples = vec2(-0.5021933, -0.9712455);
samples = vec2(0.3091557, -0.17652994);
samples = vec2(0.4665941, 0.96454906);
samples = vec2(-0.461774, 0.9360856);
// Sample spread distance
float spread = 0.007;
// Offset to make shadows set slightly down and left from their caster
vec2 depthOffset = vec2(0.001, 0.003);
// Grab the base texture coord
vec2 baseCoord = gl_TexCoord.xy;
float baseDepth = texture2D(depthMap, baseCoord).r;
float ambient = 1.0;
for (int i=0; i<numSamples; i++)
float offsetDepth = texture2D(depthMap, baseCoord + depthOffset +
(samples[i] * spread) ).r;
float diff = offsetDepth - baseDepth; // diff is +itive if offset depth
// is in front of us
float cutoff = 0.08; // limits how far objects can cast a shadow
float threshold = 0.01; // must cross this threshold to cast a shdow
if (diff < cutoff && diff > 0.01)
diff = clamp(diff, 0.0, cutoff);
diff = cutoff - diff;
ambient -= diff;
gl_FragColor = vec4(ambient, ambient, ambient, 1.0);
All of the other shaders are trivial, so I won’t include those. And the above is probably pretty sub-optimal as it was written for clarity rather than speed but it seems to fly along at a nice smooth framerate regardless.
If anyone experiments further with this I’d be interested in hearing about your results and comments. Ta.