I recently made a few performance improvements with my snow rendering system, so I figured it would be a good time to talk about the challenges I’ve faced in getting nice-looking snow cover. I’m still not overly satisfied with the way it looks or performs, but it seems interesting to talk about anyway.
Tracking snow accumulation
The snow is part of my dynamic weather system, which tracks precipitation “fronts” and hot/cold “fronts” across the game world. This isn’t anywhere close to modelling real weather, but it’s sufficient for a game.
The weather simulation is run each frame on the GPU, currently in a 256 x 256 grid for the 1024 x 1024 world. So each 4 x 4 chunk in my world gets its own precipitation and temperature value.
The snow cover simulation is also run on the GPU. It tracks snow accumulation and melting based on the results of the current frame’s weather and temperature maps.
The precipitation and temperature maps themselves are generated from scratch each time by drawing a few “front” blobs. However, the snow accumulation map needs to persist across time. This means that we need to store this information in a reliable place. Unfortunately, the GPU’s memory is not such a place, as the graphics device can be reset at any time, making you lose all your graphics resources (on Windows, anyway – this is not an issue on the Xbox). So in general, you need to be able to recreate all your resources at any time. XNA handles much of this automatically, but it is not possible to do so for render targets.
So that means we need to keep a copy of the data on the CPU. Reading back from the GPU is generally expensive since it will cause a stall. Though that’s the main issue, we still want to limit how much data we need to read back. Originally, I had the snow cover map the same size as the other weather system maps (256 x 256), but I realized I could still get a decent effect with a 32 x 32 map. A small snow cover map is also good for save games. The snow cover is tracked with 16 bits per pixel, so a 256 x 256 map would add 128KB to each save game, while a 32 x 32 map only adds 2KB.
The snow cover map looks like this:
Now, there are some constraints imposed by such a small map for the entire world. While I can use point sampling for actually calculating the snow cover map, linear sampling is absolutely necessary when sampling from the snow cover map while rendering the terrain. This ensures smooth gradients in snow cover. Otherwise, you’d end up with very noticeable boundaries:
Linear sampling means that (in XNA) we are limited to using a non-floating point surface format for our snow accumulation render target, such as Rgba32. However, using a single 8 bit component isn’t really enough to track snow accumulation. We need to be able to “accumulate” a sufficient amount of snow to snow melting when the temperature warms. We also don’t want noticeable “jumps” in the amount of snow at one point. This means we need more discrete values than 8 bits can provide.
For now, I think 16 bits is fine. So I store the snow depth in both the R and G components by scaling the snow value amount so that it lies between 0 and 255, and storing (snowAmount / 255) in R and frac(snowAmount) in G. When sampling snow depth to determine how much snow to draw, I only look at the R component, since combining R and G make no sense when linear sampling.
Rendering the snow
Now that I’ve discussed the format of the snow cover map, I’ll talk about how I render the snow itself.
The snow affects the “diffuse” color of an object. Basically it replaces it with white (or whatever color we decide snow should be). The final color is either the original, white, or somewhere in between. Snow generally isn’t transparent, so the amount “somewhere in between” has to be chosen judiciously, as it doesn’t typically look realistic. Basically, there is a small range of snow depth for which “somewhere in between” is chosen, so hopefully the ground changes from its original color to completely white over a short distance. I also have tried using some Perlin noise to adjust this value – more on that later on in this post.
In addition to the snow depth, I also take into account the world normal of the object. Snow doesn’t accumulate on steep ground as much as it does on flat ground, so this looks very natural. This significantly alleviates the “somewhere in between” problem mentioned in the previous paragraph, and is the main thing that makes snow look (somewhat) realistic.
Here’s a screenshot of the same region with and without snow cover:
Since I’m using deferred rendering, the G Buffer provides a natural way to do this. I have a buffer that contains the normals, so I can sample this when rendering the snow. I can allocate a bit (or 8) in one of the buffers to indicate whether an object accumulates snow (terrain does, but water and animated characters don’t, for instance).
The problem is, rendering the snow essentially changes the diffuse value. This means that all the lighting passes that use the G Buffer need to use an alternate version of the diffuse buffer that was generated by the snow pass. So the logic goes:
- Render scene to G-buffer (3 render targets: Diffuse, Normal and Depth)
- Do a snow pass that renders to a new DiffuseWithSnow render target. For every pixel, it needs to sample Diffuse (to know which color to blend with the snow), Normal (to know much snow to apply here), and Depth (depth allows us to get the original world position), along with the snow cover map.
- Do all the deferred lighting using DiffuseWithSnow, Normal and Depth.
Step 2 is fairly costly. It uses much texture bandwidth (sampling from 3 full screen textures), and involves a render target resolve. This was my initial implementation, and on the Xbox, step 2 cost me nearly 3ms per frame for a 1280 x 720 image.
Unfortunately there isn’t a straightforward way to avoid the extra resolve. Snow could be “blended” into the original Diffuse buffer – but unfortunately the snow amount requires sampling Normal and Depth. In order to resolve Normal and Depth, I also need to resolve Diffuse.
There a few other options: Instead of a distinct snow pass (which is clean architecturally), I could apply the snow value at every light. That means every light needs to sample from the snow-cover map. Another alternative is to render the actual snow amount to the G-Buffer. So the “original” Diffuse buffer will already have the snow applied. That means every object (or every object that has snow) as it is rendered to the G-Buffer needs to sample from the snow-cover map.
The latter seemed like a reasonable alternative, so I tried it out – just on terrain to start with. It results in a not-too-shabby performance improvement: down to an extra per-frame cost of 1.8ms on the Xbox compared to the same scene without snow. Having each shader know about snow is a little hacky, but it wasn’t very difficult to implement since every shader that writes to the G-Buffer reuses the same shader header file.
The edges of the snow pack look different when they are melting vs when they are accumulating. Melting snow tends to have a sharp well-defined edge, and greater local variation (i.e. it’s “patchy”). I can simulate this by sampling from a couple of perlin noise textures (using differently scaled texture coordinates) and using that value to modify the perceived snow accumulation at a particular point.
This lets me define a unique snow edge for nearly any spot in the world – i.e., it looks like there is much more detail than there actually is. The perlin noise texture repeats throughout the world, but almost always with a different base snow cover amount, so you can’t tell.
Here’s a far away screenshot comparing smooth snow edges vs the Perlin-modified sharper edges. Snow depth (from the snow cover map) increases gradually from left to right in this image.
In the end though, I’m not sure I really like the patchy look. It may be more realistic for melting snow, but that would mean I need to track whether a particular spot is melting or accumulating. On top of that, it ignores topography. It looks weird when the patches extend across ridges and such:
So in the end, I’ll probably just use smooth snow.
I’m not completely certain how I’ll handle objects other than terrain.
Most objects will be pretty straightforward. They’ll be tagged as either accumulating snow or not, and the shader constants adjusted accordingly.
Vegetation is the tricky one. It looks unrealistic if it doesn’t accumulate snow (see the above images). On the other hand, it blows in the wind, so it also looks unrealistic if it accumulates too much snow. I also don’t currently support normal maps for the vegetation – normal maps are pretty key to good-looking “speckle-y” snow.
I haven’t done any “sparkle research” yet, but it is possible I could modify the normal and specular power in random ways to achieve some sort of glitter effect on freshly fallen snow.