I’ve been working on adding terrain variety to the game – I wanted nice wide valleys for easy game play, mountainous regions with less usable area for harder game play, and more water features, such as lakes and ponds. When I started on the lakes and ponds, the material I had built for river water just wasn’t looking right for larger bodies of water.
After a few hours of messing about with the pixel shader for the water and not liking any of my results, I decided to examine some pictures of real lakes and ponds, and thinking about how I’d make water work if I was coding a realistic global illumination renderer. It’s very easy for me to get distracted by visuals that don’t fit in, and I really like graphics programming, so I went on a bit of a graphics binge.
So what happens when light enters water? There’s a chance that the light scatters off the water just like it does on the ground. There’s a chance that it will be reflected over the surface normal of the water, and there’s a chance that it will refract into the water and continue into it. There are changes in the amount of refraction vs reflection based on surface normal. Once light enters the water, depending on how far it travels it will probably hit something and scatter.
So I decided to try to do all these things, albeit gross approximations to what actually occurs in nature, since I needed the entire water pixel shader to fit in ~50 assembly instructions so that I could maintain using a mininum of shader model 2.0 and still have instructions left for computing shadows on the water. Here’s what I ended up doing.
This is the terrain I’m starting with. The landscape has had a few lakes carved out of it, and it’s been eroded to give it a slightly more realistic shape. The first thing I decided to deal with is depth scattering. The effect I was going for here is that while water is clear, the deeper it is, the less you can see. There are a few ways to approach this in an accurate way, but I chose a simple fake rather than more GPU work.
I color the terrain to a dark color based upon how deep it is. While not physically accurate, it’s fast and it does the job. Shallow water stays visible, deep water is dark, and there aren’t too many places where it breaks down visually. When the water renders on top of the colored terrain, it will blend with the edges, showing transparency just at the edges of lakes and in shallow streams.
For refraction, reflection, and cubemap reflection sampling, I’m going to be distorting things using a normal map. This texture is generated from several textures that move over time to give the impression of slight waves. Different textures are used for the rivers to simulate ripples moving downstream.
Next I want refraction. Where you can see through the water, there should be an offset and distorted view of what’s below. I copy the entire frame, and by using the normal map as an offset for sampling, the frame is distorted. This effect is hard to see when not in motion and when reduced to a small image, so I’ve increased the effect dramatically for this image.
Now I need reflection. I had a few choices here, and my first was to only reflect the sky. This is easily doable with a cubemap but it lacked a certain realism. So I took a few hours and made changes to my graphics engine to actually render a proper reflection. I then took a few more hours and added an actual sky to the game, because it needs to be there to be properly reflected. This certainly takes a bit more CPU and GPU time, but I think the visual results are worth it. For those with slower GPU’s, players can always cut the reflection back to just the sky and terrain, or just the sky.
Once the reflection is generated it’s distorting using the same normal map as the refraction. You’ll note there are no shadows on the reflected trees in the non-distorted version. This is for performance and once distorted it’s hard to tell there’s anything missing. The renderer can handle this situation, but the visual payoff isn’t worth the extra GPU time.
Now if I had a high dynamic range renderer, the sky would encode lighting values from zero to infinity and you’d get nice effects like over-brightening where the sun is reflected on the water. Since the renderer only works in eight-bit color values, this sort of effect has to be faked. I have a simple cubemap with the sun and bloom in it. The view direction is reflected over the surface normal and the cubemap is sampled. This gives a very nice highlight where the sun is brightest.
The final thing I need is diffuse scattering. This gives the water a bit of an emerald color in some viewing conditions and allows shadows on the water to be visible. Not much of this color is generally visible, but it’s lack of contribution would be noticed if it wasn’t there.
Putting it all together, results in a nice scene with fairly decent looking water.
But unfortunately my work wasn’t yet done. When it rains, or when there is snow on the ground, a bright blue reflection in the water doesn’t look right.
So I needed another sky for when it rains. I encoded the cloudy sky in the same cube map as the main sky, but flipped into the lower half of the cube. When the sky renders, it samples both the upper half and lower half of the sky, and blends between them based on seasons and weather.
So now the river water in the game looks much better, and larger bodies of fit into the scene nicely. Now I’m back to adding more interesting terrain features. While I’m at that, enjoy these other images of the new water.