Archive for the ‘Graphics’ Category

 

Digital test cards for games

Friday, July 10th, 2009

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".

TV test cards are a rare sight on broadcasts these days, with most digital channels choosing to just have a blank screen when a channel is off air, but pretty much everyone in the uk will have the famous Test Card F burnt into their retina at some point. Test card design is rather facinating, as all the various elements (blocks of colour, diagonal stripes, etc.) are designed to allow particular elements of a broadcast or tv configuration to be tested and tweeked.

Most of them aren't relevent to games though, since we don't have to worry about scan amplitudes and all that mumbo jumbo. Mostly we're concerned about getting binary data off disk and accurately displaying that on the screen (dealing with file formats, endianness, and display surface formats in the process). So while Test Card F is nice and iconic, it's not really suitable for our use.

Here's a couple of my old (crude) test images:

They're not bad, but by todays standards they're a little small, the nicer Tails one is an awkward 48x48 and neither of them are good when you're trying to debug file format, image pitch or similar issues since the borders and colours aren't terribly helpful.

With those issues in mind, here's my new test card:

This breaks down as:
1. Well defined an unique border pixels make it easy to see if you're displaying the whole image or if you've sliced off an edge accidentally.
2. The main circle shows is for judging aspect ratio, and making sure you've not accidentally streched or squashed it.
3. The checkered edge markings are every 8 pixels, so you can line things up easily.
4. Various pixel patterns in the corners for checking 1:1 pixel drawing.
5. Colour and greyscale bars and gradients for general colour/gamma correction and to highlight colour precision issues.
6. Ticks mark the center of each edge for alignment.
7. Tails is always cool. [grin] Replace with your own favorite character as you see fit. The image and the text make sure you're displaying it at the correct orientation and not flipping or mirroring it.
8. The empty box at the bottom is left blank for you own logo or text.

One nice "feature" is the very outer border, here's a close up of the top left corner:

The start of the scanline starts with easily identifiable red, green and blue pixels (which show up as nice round non-zero, non-FF, hex numbers in a debugger's memory window) which are similar to a text file's byte-order mark. Just after that the desaturated colours spell out (again, when viewed in a debugger's memory view) "RedGreenBlue TestCardA 256x256". Of course if you're actually loading from a BGR image format that'll just be a row of junk characters, so directly after that the message is repeated (this time "BlueGreenRed TestCardA 256x256). This allows you to easily identify if you've actually calculated the start position of the image header from your image file and that you're reading it in in the right format and endianness - if you can read "BlueGreenRed" when you were trying to load a RGB image, you know you've messed something up somewhere. :)

The top right has a similar terminating series of characters:

This time it's a slightly different hex pattern for the numbers so you can distinguish the start of one scanline from the next. Similarly the rest of the image has two distinct colours all the way down the edges. These are particularly useful when debugging misaligned image data or pitch issues. The bottom row contains the same encoded messages for those crazy image formats which are stored bottom up rather than top down.

So there you go. Use it, modify it, abuse it however you see fit. If I get chance I think I'm going to produce a few variants for lower resolutions (maybe 128x128 and 64x64) plus RGBA versions for testing alpha channels. And suggestions for improvements or tweeks are welcome.

Licensed under Creative Commons 2.0.

Renderer Optimisations Part 1

Monday, April 27th, 2009

After I upgraded to a 720p display format (rather than just 800x600) 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 512x512 to 1024x1024 and then 2048x2048. 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 512x512.

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 260x260 it meant that only one could fit onto a sheet and would leave lots of wasted space. Trimming them down to 250x250 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...

Rescue Squad 2 Preview Video

Sunday, February 1st, 2009

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.

2D Ambient Shadows

Friday, July 25th, 2008

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):


(all screenshots a quater full size, click to view the full sized version)

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);
 
vec2 samples[numSamples];
 
void main()
{
	// Our generated poisson disc sample offsets
	samples[0] = vec2(0.007937789, 0.73124397);
	samples[1] = vec2(-0.10177308, -0.6509396);
	samples[2] = vec2(-0.9906806, -0.63400936);
	samples[3] = vec2(-0.5583586, -0.3614012);
	samples[4] = vec2(0.7163085, 0.22836149);
	samples[5] = vec2(-0.65210974, 0.37117887);
	samples[6] = vec2(-0.12714535, 0.112056136);
	samples[7] = vec2(0.48898065, -0.66669613);
	samples[8] = vec2(-0.9744036, 0.9155904);
	samples[9] = vec2(0.9274436, -0.9896486);
	samples[10] = vec2(0.9782181, 0.90990245);
	samples[11] = vec2(0.96427417, -0.25506377);
	samples[12] = vec2(-0.5021933, -0.9712455);
	samples[13] = vec2(0.3091557, -0.17652994);
	samples[14] = vec2(0.4665941, 0.96454906);
	samples[15] = 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[0].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.