MudRunner is the ultimate version of Spintires, released on PC, and for the first time on consoles.
Like Spintires before it, MudRunner will put players in the driver’s seat of a variety of incredible all-terrain vehicles, venturing across extreme Siberian landscapes with only a map and compass as their guides.
In this blog, we'll be discussing how we render and simulate water in MudRunner. Previously, we discussed how MudRunner renders mud here.
Let’s take an in-game screenshot and deconstruct it:
Disable SSAO, FXAA, DOF, Sharpen Effect, Color Correction, Motion Blur and Bloom Effect:
Taking away the layers in the reverse order they are applied:
Probably any game has that kind of effect. Particles in MudRunner can be of two types:
- “billboard” particles: a quad with spray texture, oriented towards the camera
- “subparticles”: small bits of substance that collide with water, terrain and vehicle wheels. Spawned in large quantities (about 14000 on that screenshot!) and are heavily optimized (spawned and deleted in chunks of 16)
LUA scripts are used to spawn the particles. Given wheel and chassis positions, velocities, sizes, current water penetration depth and other parameters, scripts compute initial position and velocity for each particle. The advantage of this approach is that it is very customizable – you can tune particles dynamics in any way you want on the fly. The disadvantage is that this process is very technical.
2. Geometric Water Waves
This layer actually consists of two different effects, have a look at their wireframes:
Both meshes are generated on the CPU, with a lot of empirical constants involved, and have a very vague connection with the real-world physics of the water.
3. Terrain Wet Decals (will get back to it later)
And now we are left with the water surface itself:
Have a look at its wireframe:
Water in MudRunner can flow in different directions, with varying speed, at any angle, it can have different colour and transparency, and it can get foamy (when vehicles strike it or simply due to its flow intensity). So how does it work?
Any water shader in any game needs to simulate the water waves. There are many different ways to do this. In MudRunner, as you’ve seen above, the water mesh has relatively high tessellation, so our waves are actually geometric. Most simple way (but far from the most realistic) to render the water waves is to mix several instances of texture like this (MudRunner mixes 5 layers while applying different texture coordinates scale and offset):
This texture is actually a procedural noise that you can generate in Photoshop. But different character of that texture yields different character of water surface in a game.
But as will be explained later, if you want your water to flow in any direction at any speed, you will actually need to do the above-mentioned arithmetic 4 times. And if you want your water to be foamy, you will need to do the whole thing with the foam texture too! And that sums down to a lot of shader arithmetic that just won’t work in a real-world scenario. MudRunner’s solution is to pre-mix water waves and water foam texture:
Animated water texture. The only small trick is to make sure those textures can be seamlessly tiled which is easily achieved with some shader math. In the same way, we generate ANIMATED CAUSTICS TEXTURE which we will reference later.
So how do you make your water actually flow? Simple – you scroll the texture coordinates over time. But in which direction, and at what pace? Obviously, that depends on the water direction and water flow speed. In MudRunner, to define water direction and flow speed, map author places “water rivers” in the Level Editor:
Each “river” is a curve with varying width.
When building a level (preparing it for the game), Level Editor generates continuous water surface by mixing all “water rivers” together:
Vertices that define water surface. “Rivers” are not used by the game itself.
Map author uses a brush to paint water flow intensity. So in the end, additionally to the water surface, Level Editor generates that A8R8G8B8 texture for the game to use when it renders water:
Water data per terrain block (terrain blocks are described in the MUD OF MUDRUNNER paper).
So water shader knows water direction and flow intensity (speed), which is actually merely a 2d vector, let’s call it “flowDir”. They key concept to understanding next step is discretization. It means that we pick one of let’s say 16 possible water directions, a direction that is closest to “flowDir” (let’s call it “flowDir1”), and its “neighbor” direction (“flowDir2”), so that
(here and later HLSL code is used)
flowDir = lerp(flowDir1, flowDir2, flowT);
Where “flowT“ is the interpolation parameter of the two directions.
Having “worldPos” as a world position, water shader can now compute texture coordinates for each direction:
float2 angTC1 = float2(
dot(float2(+flowDir1.x, +flowDir1.y), worldPos.xz),
dot(float2(-flowDir1.y, +flowDir1.x), worldPos.xz));
float2 angTC2 = float2(
dot(float2(+flowDir2.x, +flowDir2.y), worldPos.xz),
dot(float2(-flowDir2.y, +flowDir2.x), worldPos.xz));
Which can actually be used to sample animated water texture (having “g_fTime” as animation time, “tcScale” and “tcScrollSpeed” – arbitrary constants):
float2 tcScroll = float2(g_fTime, 0) * tcScrollSpeed;
float4 waves = lerp(
tex2D(g_samWaves, (angTC1 + tcScroll) * tcScale),
tex2D(g_samWaves, (angTC2 + tcScroll) * tcScale), flowT);
We have omitted the water flow speed discretization for simplicity here, but it follows the same idea, and thus you need the 4 samples to the animated water texture (“g_samWaves” sampler in the code above) which were mentioned earlier.
But the water waves need to be shaded. The recipe for that is pretty common, and it basically involves two components: Reflections and Refractions.
Refractions: objects seen through water. Notice caustics effect that we will get back to later.
Reflections: objects that are mirrored by the water surface. To render reflections, you can put the camera below the water surface and point it upwards (“reflect the camera”), then use a technique called “oblique clipping plane”. But that only works well if your water surface is “planar” – which is not the case for MudRunner. MudRunner uses a technique called “Screen Space Reflections” (SSR - this technique is well documented in a multitude of sources). MudRunner uses SSR only for water reflections, so its version is highly optimized and very lightweight. One of the optimizations is, we render the “water reflections mesh” (see picture) instead of full-screen quad, so we know the position of the shaded fragment from vertex shader instead of having to do z-unprojection, and don’t need to do SSR pixel shader for the entire screen.
MudRunner uses a very simple algorithm to compute water simulation, it involves two A8R8G8B8 textures. In the same fashion as mud simulation, we build and draw special primitives into a render target texture:
With a knowledge of wheels and chassis positions, their size and velocities, their current water penetration depth, and water speed and direction, CPU forms various “primitives” and draws them into render target textures, which is then read back by GPU. That is pure empirics, and have very vague connection to real-world physics of the water!
The first texture with simulation data looks like this:
After the primitives are drawn into that texture, MudRunner performs a “GPU simulation shader” that does a simple propagation of foam, water, height and mud parameters to neighbour texture samples. So two instances of each texture are involved: one for read-back, one for output, and they are swapped each frame.
The second texture only stores “WATER MUD”. Note – there are algorithms that simulate much more realistic water dynamics, but they all require floating point textures for the data, which makes algorithm somewhat more resources-heavy.
Let’s have a look at another peculiar effect. For the water render pass, we generate special low-res texture, which we call “water domain texture”:
Water domain texture uses a concept, similar to parallax effect, and allows mud (not seen at above picture) and foam to be seen “through” the water. It also stores “water flow speed” that we use to pick the mip level of the “animated caustics texture”. The caustics themselves are simply z-projected when water is drawn. Note that water caustics in MudRunner cannot be seen above the water – drawing the caustics above the water would be a nice improvement in the future!
As referenced in the “MUD OF MUDRUNNER”, there are 4 types of decals in MudRunner:
- Mud decals, produced by “mud” particles and applied to the terrain.
- Wet decals, produced by “water” particles and applied to the terrain AND water.
- Oil slick decals – produced by working vehicle engine and emulates slick on the water surface, applied to water only.
- Dry oil decals – same as oil decals, but emulates oil marks on dry surfaces, so applied to terrain only.
Let’s have a look at oil decals:
The render process is similar to rendering mud decals:
Water needs to output a mask when it’s rendered, so decals can distinguish the surface type they are applied to. Water also writes screen normals that decals use for shading.
Thank you for reading!