Let’s take a game screenshot and deconstruct it.
How about this one?
Let’s start by disabling post-processing: SSAO (ambient shadows), FXAA (softens object edges), DOF (blurs foreground), Sharpen Effect and Color Correction:
Now let’s take away all the layers in the reverse order they are applied when the game renders a frame.
1. Terrain Mud Decals (will get back to it later)
2. Wheel Tracks (which we will get back to later)
3. Mud Particles
Those objects float on top of the mud and are affected by vehicle wheels (can stick to them even), they only act as decoration and don’t affect the physics. Mud particles are simulated by the CPU in a separate thread. A lockless synchronization technique makes this whole effect very performance-friendly.
4. Mud Chunks
These are bigger chunks of mud, each is a full-fledged rigid body so they interact with the physics world properly and they even break into smaller chunks, when vehicles drive over.
They are maintained by the same systems that simulate and render all other plant types in the game. (A “plant” is an object that is scattered over the terrain in large quantities, like rocks or trees.)
5. Road Overlays with Parallax Effect.
Roads, that map author places within Level Editor, are then baked into a special data that game uses to draw them efficiently. They are not actually rendered as a separate layer and are embedded into terrain shaders.
Now we are left with the mud itself, which is a simple heightfield (essentially a 2d mesh):
Have a look at its wireframe.
Most of the game level terrain is actually rendered at lower resolution. So if vehicles have not deformed terrain, its wireframe would have looked like this:
Now let’s take a closer look at each layer…
Game levels in MudRunner can be up to 1km x 1km in size. They are subdivided into a grid of 16m x 16m blocks. Each block contains a list of plants (trees, rocks, etc.), base terrain data, mud data (optional), and other data. The game only draws blocks that are inside camera “frustum”. Only blocks that are near the truck that player is driving are “active”. The game only performs plant physics simulation, mud simulation and water simulation for “active” blocks.
The mud simulation consists of mud-vehicle physics processing (performed by the CPU) and rendering of the mud (performed by the GPU). For a number of reasons (the CPU and GPU are out of sync on PC, high level of detail required for rendering is not required for physics, etc..), these two tasks operate with completely different sets of data. We won’t go into details of physics simulation here, but in short, there is no rocket science involved, but it’s not something Havok (the physics engine in MudRunner) can do out of the box.
In order to draw the mud, the vertex shader takes in a simple 2D grid of vertices, then fetches two A8R8G8B8 textures that look like this:
Texture 1. 25x25 in size for each block, is used for rendering both low-res base terrain and the high-res mud.
R (red) channel – packed height (elevation). This is unpacked into real-world height with a little math.
G (green) channel – grass/dirt materials mix factor. Currently, each block can only mix 2 materials.
B (blue) channel – “tint” factor. Tint adds some color variation to the environment, and illustrates physics properties (“tinted” terrain is softer).
A (alpha) channel – marks portions of base terrain that are substituted by the mud, it’s the only channel that is dynamically updated as vehicles deform terrain.
Texture 2. 128x128 in size, is only allocated for blocks with mud.
R (red) channel – “mud height”, is relative to elevation of the base terrain. It gets unpacked into a floating point value with simple multiplication.
GB (green, blue) channels – used for “mud offset” effect (the mud slides away from vehicles wheels as they move).
A (alpha) channel – “mud tracks”, a blending factor of the terrain shader into the mud shader, In combination with A channel of Texture 1 that makes mud transition into terrain unnoticeable.
The visual part of mud simulation boils down to properly updating Texture 2:
With knowledge of wheels and chassis positions, their size and velocities, and their current mud penetration depth, the CPU forms various “primitives” and draws them into a “Texture 2” RT (render target), which is then read back by GPU. That is an approximation and has a very vague connection to real world physics!
The mud pixel shader itself is nothing fancy, and it simply blends few diffuse textures based on the normal which it derives from the heightfield data.
Now let’s get to more interesting stuff…
Let’s take another game screenshot and see the steps involved in rendering wheel tracks:
Wheel tracks are rendered after terrain and most of the environment. So by then we already have screen normals and “wheel tracks screen mask” (which masks a portion of the screen, on top of which tracks shouldn’t be rendered), in addition to the Z buffer, of course. The mesh itself is a simple series of trapezes that follow the path of a wheel. But for the parallax effect, that gives tire treads a cool volumetric look, we first need to render wheel track “heights”:
Wheel track heights are required for a parallax effect. The textures are projected onto the terrain using Z-buffer. The tricky part is ordering tracks properly (more recent tracks should cover older tracks). There are several approaches to do this, we use GPU-only method: render tracks with their own z-buffer (so no hardware z-test against scene), and offset more recent tracks towards the camera in vertex shader. Z-test against the scene is performed manually inside pixel shader.
The wheel tracks mesh is then drawn again for a second, final, pass:
Traditional parallax effects take multiple samples from associated “heights” textures and then offsets texture coordinates accordingly (we won’t go into details of the math of it). Problem is, if you are z-projecting your texture onto z-buffer, there is no easy way to make said samples. That is why wheel tracks are rendered in 2 passes. The visual artifacts are neglectable with that approach, you just need to make sure you don’t sample from parts of the texture where you don’t have wheel track height info at (that’s what green mark-up of wheel track heights texture is for).
TERRAIN MUD DECALS
When vehicle’s chassis or wheels move through the mud at high speed, or when a wheel spins in a mud, it launches mud particles. Mud particles affect per-vertex mud data of most of the environmental assets, and they also generate terrain mud decals:
Each terrain mud decal has a mesh of oriented box:
In the same fashion as wheel tracks, after the terrain and most of environment is rendered (it’s called “Color Pass” and it uses MRT – multiple render targets), we have screen normals (used to compute lighting) and terrain decals screen mask (masks out objects that decals shouldn’t be rendered on top of). Color Pass also write occlusion factor for terrain decals so they can be lit more naturally. Decals are z-projected on top of the scene.
Most of the games now feature z-projected decals, so it’s nothing really fancy. But the same system that generates Terrain Mud Decals in MudRunner actually also generates 3 other types of decals: Terrain Wet Decals, Terrain/Water Oil Decals and Terrain/Water Wet decals! But that’s a topic for another day…