-
Notifications
You must be signed in to change notification settings - Fork 94
Destructible Terrain
I thought I’d take some time to explain the concept of destructible terrain. I’ve needed this for Gorillas, and it took me quite some time to figure out a fast and flexible way of implementing it.
The main problem is that circles take quite a lot of vertices to draw. You really don’t want to just take a slab of terrain, and cut circles out of it, and then use that data to draw a texture on or calculate intersection against.
Square shapes are far less expensive; and that’s how my method got born. It’s a little tricky, so you might need to read over this a few times to get it straight.
We want to be able to use any kind of background. Be it a solid color, a gradient, or as advanced as a moving image. That means that we can’t just paint a background, paint a terrain, and fill up holes in the terrain with a background color.
We start simply by drawing whatever background we need.
In the case of Gorillas, that’s a simple gradient for the sky and a few dots for stars. When you disable the special effects in Gorillas, it just draws a plain solid color.
Plain background:
GLubyte *colorBytes = (GLubyte *) &skyColor;
glClearColor(colorBytes[3] / (float)0xff, colorBytes[2] / (float)0xff, colorBytes[1] / (float)0xff, colorBytes[0] / (float)0xff);
glClear(GL_COLOR_BUFFER_BIT);
Gradient background:
glVertexPointer(2, GL_FLOAT, 0, vertices);
const GLubyte *fromColorBytes = (GLubyte *)&fromColor;
const GLubyte *toColorBytes = (GLubyte *)&toColor;
const GLubyte colors[4 * 4] = {
fromColorBytes3, fromColorBytes2, fromColorBytes1, fromColorBytes0,
fromColorBytes3, fromColorBytes2, fromColorBytes1, fromColorBytes0,
toColorBytes3, toColorBytes2, toColorBytes1, toColorBytes0,
toColorBytes3, toColorBytes2, toColorBytes1, toColorBytes0,
};
glColorPointer(4, GL_UNSIGNED_BYTE, 0, colors);
glEnableClientState(GL_COLOR_ARRAY);
// Draw.
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// Untoggle state.
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);
(This code assumes ‘skyColor’, ‘fromColor’ and ‘toColor’ are ’long’s that contain your color codes, such as 0×0000FFFF for bright blue, and ‘from’ and ‘to’ are structs with two floats which describe the background sky box)
See:
http://gorillas.lyndir.com/trac/browser/Classes/SkyLayer.m
Say we’ve already got five explosion holes that should carve chunks out of our terrain. As explained above, the problem is that we need to see the background we just drew through those chunks.
The magic to make this happen is clever blending and toggling of OpenGL state.
Note: Make sure that your render buffer context is created with a color space that supports alpha color values. For instance, RGB8888, but not RGB565.
We basically just draw a hole, which is a texture that is a white square with a transparent circle in the middle. Before we draw it, we tell OpenGL to NOT change any of the RGB components of our render buffer, but only touch the Opacity component. Then, we tell it to blend the destination (existing) components with the alpha values of the texture’s components. As a result, we get a destination in our render buffer that has identical RGB components as before (we didn’t touch those) but Opacity components as they are in the texture we drew. Which is to say, a transparent circle in the center and opaqueness around it. The render buffer doesn’t look any different, but where we want a hole, the pixels’ Opacity components are no longer opaque.
// Blend our transarent white with DST. If SRC, make DST transparent, hide original DST.
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_TRUE);
glBlendFunc(GL_ZERO, GL_SRC_ALPHA);
[super draw];
// Reset blend & data source.
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
(The [super draw] call just draws our hole texture where we need it to be)
See:
http://gorillas.lyndir.com/trac/browser/Classes/SkyLayer.m
Now that we have a background where some of the pixels’ Opacity component have been altered, it’s time to draw our terrain on top of it.
In Gorillas, the terrain are buildings that gorillas are perched on top of. That basically means several squares of different shades. What your terrain looks like and how you draw it doesn’t really matter that much; the important part, once again, is the way we blend it into the existing render buffer.
What we’re going to do now is draw every pixel of the terrain on top of the background pixels, except for where the background pixels aren’t fully opaque. Where the background isn’t fully opaque, we want to draw our buildings with the Opacity of the background pixels. So basically we switch the Opacity values of the terrain and background pixels around to compose the resulting pixel. That will make sure that where the background is opaque, we draw an opaque terrain pixel on top, but where the background is not opaque, we blend a terrain pixel into it, where fully transparent background pixels result in no terrain pixel data being added to the mix.
In the code for Gorillas I actually draw the buildings twice with a different blend. The first time, I draw it as explained above, the second time I draw them using a different blend, which causes the second draw to only apply pixels where the render buffer contains non-opaque pixels. That causes the second render to draw pixels where there are holes. I use this to draw the back of the building, to illustrate having blown the front off of a building. I also use some blending trickery when drawing the front of the buildings to set the render buffer’s Opacity to 1 where I have windows in the building without drawing the windows where there are holes. That means, when I draw the back of the building later, I’m not drawing any back pixels where I set the opacity of the background to 1 (for the windows), meaning the back of the building will have square holes in it where there used to be windows (blown out windows).
// == DRAW BUILDING ==
// Blend with DST_ALPHA (DST_ALPHA of 1 means draw SRC, hide DST; DST_ALPHA of 0 means hide SRC, leave DST).
glBlendFunc(GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA);
[self drawBuildingFront];
// == DRAW BUILDING BACK ==
// Draw back of building where DST opacity is < 1.
glBlendFunc(GL_ONE_MINUS_DST_ALPHA, GL_DST_ALPHA);
[self drawBuildingBack];
// Reset the blend.
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
See:
http://gorillas.lyndir.com/trac/browser/Classes/BuildingLayer.m
After all that’s done we basically have a nice background that we can do whatever with (use whatever content and animate it if we like), draw invisible holes on top of that which change the opacity of the background pixels, draw a terrain on top of that using the opacity of the existing background pixels, and optionally drawing an alternative destroyed terrain on top of that using the opposite of the opacity of the existing render buffer pixels.
We now have destructible terrain; but we should probably add some finishing touches to it. In Gorillas I use a particle emitter to draw flames and explosions.
For collision detection we really just need to implement a way of checking whether a certain point collides with the terrain and whether it collides with an explosion.
Then, we can just check whether the point collides with our terrain and does not collide with any of our holes. It’s a simple loop of checks and shouldn’t be too expensive at all. Your hole collision detection will probably just check the distance between your hole’s center and the point, and your terrain detection’s complexity will depend on what your terrain looks like. Gorillas just uses rectangles, so it’s pretty simple.