Dynamic destruction in Radio Viscera

July 21, 2021
protect

Introduction 

This is an overview of all the elements which contribute to the dynamic damage system created for Radio Viscera.

I've written this article with a focus on the concepts used rather than a specific language or environment or as a step-by-step guide. The implementation described here was built very specifically for the game I was making, so including the technical minutiae here did not seem helpful. Hopefully some of this information will help you in the development of your own project using whichever engine or environment suits you best.

I've tried to insert as many gifs, screenshots and sounds as I could to keep it from being too dry. The last few sections focus on the (less technical) secondary systems and effects that interact with the destruction and should be a bit more digestible.

This design was built in a custom C++ based game engine which uses an OpenGL renderer and the Bullet Physics SDK.

Index

1. Overview

timeline0.jpg

Figure 1. The central concepts behind the system


The destruction system depends on three elements that work together to present the illusion that a hole is being smashed in a wall:

    Parametric geometry
    Used to render the damaged walls.

    Physics simulation
    Controls the collision shapes which make the wall solid, performs raycast queries and helps                  generate triangulated collision shapes when the wall is damaged.

    Render textures
    Stores damage data for each wall, which is fed into the geometry shader and also used as the              source for generating collision shapes.

2. Initial face generation

Each destructible face is built of three pieces. The first piece is the display mesh which is a parametrically generated plane with position, normal and UV coordinate data. This is used to render the face in-game.

The second piece is the collision shape. This is a static collideable rigid body which is added to the dynamics world of the physics simulation. The shape itself is an identical match for the display mesh and is positioned to sit perfectly over top. In addition to providing the collision that you would expect from a solid wall it also detects damaging raycast queries (see Detecting raycasts).

initial_face.jpg

Figure 2. Initial un-damaged face components (display mesh, collision shape, damage buffer)
 

The third piece is the damage buffer. This is a 2D render texture used to store the damage state for the face. The dimensions of this texture are calculated to match the size of the face in world space multiplied by a "pixels-per-meter" factor which determines the resolution at which the damage will be stored. In my case I use 48 pixels-per-meter so a wall 4 x 2 meters in worldspace would have a 192 x 96px sized damage buffer. This texture is cleared to RGBA [0,0,0,0].

Until the wall is first damaged these three pieces do not change. The simple planar display mesh allows me to use a standard shader and render the wall like any other entity in the scene until the damage shader is required.

3. Detecting raycasts and applying damage

Damage is applied using raycasts. When a weapon is fired a raycast test is performed against all collision bodies in the dynamics world, with the ray originating at the weapon muzzle and extending toward the current aim direction. If the raycast intersects with the collision shape of a damageable face then a hit is registered. The world position and normal direction of the intersection are recorded and forwarded to the damageable face entity so it can record the hit to its damage buffer.

timeline1.jpg

Figure 3. Surface normal of the hit is used for directional effects
 

To actually apply damage a sprite needs to be drawn to the damage buffer. A black pixel on the damage buffer indicates no damage to that area and a fully white pixel indicates complete destruction of the area. Before the sprite can be drawn I need to calculate where to actually draw onto the damage buffer so it will match the hit location in world space. Barycentric coordinates will be used for this process so I first need to figure out which of the two triangles (that constitute the un-damaged plane collision shape) received the hit.

timeline3.jpg

Figure 4. Steps required to draw the damage onto the buffer

Note: the barycentric method is a leftover from when the system was designed to work with arbitrary triangle meshes. If you're only dealing with flat, rectangular faces this conversion could be simplified.

Finding the correct triangle is done with a ray-triangle intersection test, using the data from the raycast hit. Since there are only two triangles this happens quickly. The destruction world position is then transformed into face-relative local space by multiplying it against the inverse transform of the wall entity. The distances between the hit position and the triangle vertices are calculated and used to generate a UV coordinate which aligns with the damage position on the wall.

invertSplatSprite.png

Figure 5. An enlarged and brightened version of the damage sprite
 

The last step here is to draw the damage sprite to the damage buffer render texture like a stamp. The design of the sprite itself is the result of lots of trial and error to see what kind of shape worked well and had the intended effect (both visually and physically). The rotation of the sprite is randomized and the scale and brightness of the sprite are based on the magnitude of the damage. This allows more powerful hits to make larger holes. The sprite is drawn with additive blending so that successive damage sprites can "build" on each other if applied in the same area.

4. Drawing a damaged wall

tess0.jpg

Figure 6. Mesh wireframe after tessellation. No damage has been applied.
 

When an untouched face is damaged for the first time a few things happen. First the display mesh, which up until this point was a two triangle plane, is tessellated. Similar to how the resolution of the damage buffer is calculated, there is a mesh density factor which describes "faces-per-meter". I use a value of 6, so my 4 x 2m wall results in a mesh that measures 24 x 12 square sections (576 triangles). The blue channel of the vertex color is used as a boolean value to mark which vertices are on the outermost edges of the plane mesh. This allows me to extrude those vertices in the geometry shader to create the wall thickness you see along the top edge.

Once damage has been applied, the face is flagged and starts to draw using a special damage shader. This shader samples from the damage buffer and uses a geometry stage to cull any faces that are fully damaged and construct a solid tapered edge at the borderline of the damaged area.

UVs.jpg

Figure 7. Visual representation of the two UV sets (lower) with the textures they sample (upper)
 

The wall geometry needs to have tiling UVs so the square wall display texture can repeat on long sections without stretching. However, the damage buffer requires unique UVs that map 1-to-1 with the wall geometry so a second set of UVs are generated and stored in the red and green channels of the vertex color. This way each vertex of the wall is mapped to a unique space on the damage buffer.

This next bit is the part where I try to describe a shader using words. If you prefer to just read the pseudocode, scroll down a bit further.

 

In the geometry shader each triangle of the mesh is processed independently. The UVs from the three vertices of each triangle are sampled from the damage buffer to determine if any of them should be fully discarded. If all three vertices are damaged beyond the threshold value then the entire triangle is discarded and the shader returns.

If none of the triangle vertices have any damage then the triangle should just be drawn normally. These triangles actually need to be drawn twice: once regularly and then again extruded inwards with the normal and winding order inverted. This makes the wall look double-sided and gives it its thickness. Before finishing, the shader will also check if any of these vertices are edge vertices via the vertex color attribute we used earlier. If it is an edge vertex the shader will generate flat geometry to cover the top edge which masks the hollow area between the two wall faces.

The last case is the most involved. In this case one or more of the three vertices has a damage value greater than 0 but below the threshold of "fully damaged". The position of the damaged vertex is transformed so the more damage a vertex has the more it will be pushed inward, both into the wall and toward it's neighbour vertices. This transform is applied to the original vertices as well as the inverted "inner wall" vertices. The resulting effect is a polygonal edge that shrinks and grows closer together as more damage is applied, making the wall geometry thinner and thinner until

JikGuard.com, a high-tech security service provider focusing on game protection and anti-cheat, is committed to helping game companies solve the problem of cheats and hacks, and providing deeply integrated encryption protection solutions for games.

Read More>>