August 15, 2016

Water for Games - Cheap, Simple, Effective

71% of the Earth's surface is water. Yet, of all the materials I've ever made, water seems to be the most difficult to get right. Even in UDK with planar reflections and Phong shading, water was still incredibly complex. Once you crack the code to good water, your game can turn from a nice scene to a gorgeous, luscious environment. For the sake of this blog post, I will avoid delving into vertex displacement and dive right into translucent rendering. This kind of water is good for a calm ocean, a shimmering lake, the famous Carribbean seas, inter-coastal waterways, and pool water, with more emphasis on translucency and reflective effects. Raging oceans and rivers will be very different.




I first really got excited about realtime water with Super Mario Sunshine. For years I crowned its water as a technical and artistic achievement of the highest order. Keep in mind games like Goldeneye 007 released just 4 years prior to this game, and this was before we got a chance to experience the water from Half Life 2, Assassin's Creed, Uncharted, and Crysis. Before all these games, there was Super Mario Sunshine. The water was just so blue and seemed to look so beautiful without ever being inappropriate. But as it turns out, the water in Sunshine was, for the most part, a hoax. The water surface itself was just a texture that bobbed up and down with the waves. At close distance the water was completely translucent, while the "shimmering" texture would be emphasized further away. The water itself was clear: objects underneath the water were vertex painted to a clear blue color giving the water that strong Caribbean blue saturation. Sunshine used custom mipmaps to push the shimmering effect away from the player, but we can use other means to simulate this.

In real life, water reflects, refracts, and scatters light. You can make the shader much easier to render by mimicking the results instead of the effects. The combination of reflections and refractions looks like turbulence that bends around the bulges in the water's surface. Specularity can help to define actual reflections on the surface. The scattering can be simplified to a depth-based color.

Pros to my water technique (inspired by a combination of Super Mario Sunshine and Super Mario Galaxy's techniques):
  • Cheap! 45 instructions for texture highlights, 72 for Phong specular, 74 for GGX specular, 76 for texture+GGX specular.
  • Fluid! Looks like flowing, fluid water.
  • Translucent! You can see objects underneath. And those objects have proper depth.
  • Distance opacity! The depth method inherently makes the water more "reflective" in the distance and more translucent up close.
  • Tilable, but doesn't look it! 2048 and different tiling factors between the distortion asset and texture/normal asset reduces tiling in the long range.
  • Memory and texture sampler efficient! While the 2048 texture can seem like a lot, you can hide this in a channel of a compressed 2k mask, effectively getting this endless ocean for less than 1MB of texture data. And if you use 1k maps, you can cut that size by 1/4. And the distortion normal map only needs to be 256x256 pixels. Two textures for beautiful water!
  • Flexible. Since the lighting is handled using forward-rendering techniques, you can get benefits out of this system that you can't with UE4's deferred renderer, like changing the color of the specularity, coloring by sphere maps, iridescent effects.
  • Supports day/night cycle. By updating the sun's rotation in blueprints, the specularity on the water can change according to the sun's position.
  • Supports color gradient by depth. Expanding the depth fade and lerping the result to different colors, you can make deeper waters a much darker blue than shallow areas.
Limitations of my water technique:
  • No shadows. While you can multiply the specularity by a custom texture map or through vertex painting, there is no real process to get shadows baked on here.
  • Only supports one light. The sunlight. This method will not work for reflections from lights on a pier. But it can provide general water shading to support such methods.
  • Some manual effort required to provide light direction. 




All you need to create this effect is a simple distortion texture (a tiling bubbly normal map), a shimmery, circular noise grayscale texture, and that's it! For a more advanced version with GGX specular highlights from the sun, you'll also need a normal map of that circular noise grayscale texture.




This material only costs 46 pixel shader instructions and 45 vertex shader instructions despite being a translucent material with depth fading. If you have a sand texture underneath, this water will be cheaper to render than the sky. Making the material unlit and removing vertex fog will eliminate a ton of instructions.

In the picture above, the Roughness GGX Specular is not even used, so the distortion is actually doing most of the heavy lifting for this effect. Pan the bubbly normals, mask RG, and scale down by 0.05 to 0.1, or however strong you wish the distortion to be. Add that to the UV coordinates of your water texture (tile it as many times as you need). If the texture is imported as an uncompressed linear grayscale, multiplying it by itself will give you a cheap gamma-corrected version to add on. Multiply it by the brightness you wish the highlights to be, and add it to your ocean color. A deep saturated cerulean blue with some extra intensity works great. The depth fade in opacity will make your water more translucent with objects closer to the surface and more colored when objects beneath the water are further away. For an ocean and most water surfaces in general, this means you will get a more opaque appearance in the distance.

To get the right texture you need, start with a 256x256 pixel grayscale noise. Enlarge this to 2048x2048. Use the gradient map tool to make white circular bands according to the noise texture. Two bands through the gradient map adjustment is enough to get a good effect. Convert this "heightmap" to a normal map, and you can use the normal map instead (or both).




The GGX specular method calculates a specular highlight using the same principled GGX specular code that UE4 uses for calculating specular highlights for stationary and dynamic lights. The HLSL GGX code in the custom node is:

float a = Roughness * Roughness;
float a2 = a * a;
float d = ( NoH * a2 - NoH ) * NoH + 1;
return a2 / ( PI*d*d );

And the two inputs are Roughness and NoH, where NoH is the Blinn model. I added a Lambertian diffuse and multiplied it by itself to get a more focused, gamma-corrected result. The light vector is pulled from a Blueprint actor that is placed in the level and automatically grabs the directional light/sunlight's rotation.




This code will then feed to a Material Parameter Collection that communicates with the material to set up a specular highlight on the water.




This version of the water shader replaces the texture highlights with the GGX specular highlights calculated from the normals, so there is no texture 74 pixel shader instructions, 2 textures.




And this version adds both the texture and the normal specular for highlight rendering. The texture is not multiplied, only added. 76 instructions, 3 textures.


No comments:

Post a Comment