“Terrain” water has existed in ROBLOX for the last year and a half and, while the physics have always behaved in satisfyingly realistic fashion, the appearance left something to be desired. Last week, we turned on a new look for our water, bringing waves, reflections, and blue underwater “fog” to ROBLOXian H2O. The feature is primarily eye candy, but it fits in our grand rendering overhaul that started with improved performance and lead to dynamic lighting, part outlines, and in the near future new building material textures.
Real-world water is a very complex optical phenomenon. It comes in a staggering number of variations, from a tiny glass of water to an ocean, from a murky swamp to a crystal-clear lake, from a tiny creek to a mighty waterfall. And all of it is governed by the same laws of physics.
We’ll start with reflections and refractions.
We know that water is a weak conductor. It stands apart from both strong conductors (e.g., metals) and strong insulators (e.g., plastic). From electromagnetic theory, we know that the major difference between conductors and insulators is how they react to incoming electromagnetic radiation (light).
Metals have free electrons within their crystal lattice; so, when a light wave hits the metal, free electrons (shared across the entire volume, if you will) relocate to create an electric field that compensates the (external) electric component of the incoming wave. This leads to the wave being instantly reflected off the surface without any penetration. The reflection is fairly sharp; if the metal has mathematically ideal flat surface, the light will be ideally reflected according to this equation:
Where i is incident ray directed into the surface, n is surface normal, and r is the reflected ray. (Keep these variables in mind.)
For ideally flat metal, all light from the light source is reflected in a single direction, and we call this specular reflection.
The insulators, on the other hand have all of their molecules locked in place with all the electrons intact. That means there is no significant resistance for the light wave to penetrate the surface. The light, however, being energy interacts with the molecules and is gradually reflected back (in unpredictable directions).
Thus, the light emerging from the surface is scattered (diffused) randomly by these reflections. That means when we observe such surface, our (mathematically ideal) eye collects light from many points on and below its surface, and that is exactly why uniform plastic is dull. We call this diffuse reflection. Again, in optically dense insulators (e.g. plastic), the penetration depth is still fairly small (millimeters?). A simple Lambert’s law is a good approximation for those:
Where e0 is the amount of energy (outgoing direction makes no sense here) the substance receives, Kd is the substance absorption coefficient, i is the incident ray and n is the surface normal.
Note that Kd actually depends on the wavelength. Certain materials absorb more light only at certain wavelengths, and that is exactly how we perceive colors. What’s more, the combination of (Kdr, Kdg, Kdb)measured for monochromatic red, green and blue light wavelengths is actually the surface color for the standard RGB model! We usually call this diffuse color (a very widespread term, and hence that d subscript), although the more correct term would be albedo.
Enter the water
Being a weak conductor, water exhibits both properties: if you stare at a still body of water at a grazing angle (that is, toward the horizon), you’ll see it reflects the sky and the sun. However, if you look directly down into a clear pond, you’ll naturally see the bottom, but also a very faint reflection of yourself. Most light rays punch though the water surface with almost no resistance, reflect off the bottom and dart back. If the water is sufficiently deep (e.g., an ocean), the rays will be gradually scattered by the water — that’s what you see as that “deep water.” The physics laws regarding sub-surface scattering are fairly complex. The easiest way to describe them is probably Beer-Lambert’s law (basically, an exponent of depth), but we (over-) simplify it into a single solid color for performance reasons. (We might consider simulating this phenomenon as a PC-only feature in the future.)
So how do these properties get along with one another? When a light ray hits the surface of water, it actually splits into two.
- The reflected ray simply bounces off the surface.
- The refracted ray punches through the surface of water at a slightly different angle. Its wavefront is compressed in the direction parallel to the boundary due to the fact that the speed of light in water is less than in air (quantum physics disagrees with this, but we won’t bother for the sake of simplicity). That’s why the ray looks “fractured.”
Our water does not currently simulate refraction, so we can disregard that component. A widely used simplification of the equations that determine the reflection of a light source in real-time graphics is Schlick’s approximation:
Again, we’ve omitted refraction. This equation makes very little sense; however, we’ve taken further steps to simplify it to:
With a and b having completely nonsensical values — others might call those “artistic variables.” We use this equation to knock off a few pixel shader instructions. The original Schlick’s equation is still present in our shader code, for those of you who enjoy playing with shaders.
Putting it all together
Each point on the water surface receives light from many sources at once. The primary source is the sun, that produces those bright specular highlights on the surface. The secondary (environment) light source is the blue sky, which is merely a reflection and scattering of the sunlight by the atmosphere (that’s why the sky is not plain black during the day). Another secondary source is pretty much anything — objects, terrain, effects, etc. — however, we don’t use it (at the moment) for — you’ve guessed it — performance reasons.
For rendering, the environment light comes “pre-collected” in the form of a small static cubemap with an image of blurry sky in it. We cross-fade it with the water diffuse color (that approximates a portion of energy that the water surface refracts towards the bottom and that is being reflected back by the thickness of water) using an approximation of Fresnel’s coefficient.
The specular component is being treated in a different way. Despite the fact that direct sunlight behaves the same way as anything else, it is so bright that even Fresnel’s equation cannot properly dim it. So, we simply add it up on top of our lighting equation and “hope for the best.”
Arseny’s magnificent global lighting affects water. We properly shadow the specular highlights where sunlight/moonlight is blocked by level geometry. We dim the water and environment a little bit where there’s significant amount of ambient occlusion. Local lights affect only the water color, though; they do not produce specular highlights.We’ve also made a basic underwater effect: a simple global fog that’s being turned on when underwater.
The water waves consist of two elements:
- Low-frequency component: per-vertex wave-like animation computed in vertex shader
- High-frequency component: animated normal map (sequence of 24 normal maps with linear interpolation between frames)
Normal maps play a very important role. Still water reflects incident light as a perfect mirror would. In reality, however, the water is rarely still. There are waves and ripples, and those, being a kind of surface normal perturbation, have an effect on the way light interacts with water. As a result, we see distorted reflected and refracted light coming from some random directions. And that’s why we see things like a path of light going towards the setting sun on the ocean surface.
There’s still a ton of things we could do with the new water: proper reflections, transparency and refracted/scattered light, caustics, splashes and physically-based ripples, foam and surf. We could also add controls (APIs) to adjust the water conditions: colors, waves (from still to stormy).
Performance-wise, the water is approximately in line with other shaders we have. The water works fine on third-generation iPads. We’ve also got a fallback for lower-end PCs and second-generation iPads. This fallback is a further simplification of the water shaders:
- We remove the specular component (saves a lot of shader instructions)
- We replace environment lookup with yet another solid color (saves a dependent cubemap lookup and some vector reflection equation)
- We do not interpolate normals between two normal maps (saves another texture lookup)
- We remove the vertex animation for the water geometry (knocks off about half of the vertex shader instruction count)
For ancient graphics cards with no support for Shader Model 3.0 (i.e., 8+ years old), we’ve retained the old water.
The dramatic effect
This technology has had a dramatic effect on existing places that were built using water, as you can see in the video at the top of this article and the following gallery of screenshots.
If you want to build using the new water, you can do so by placing high-scalability (i.e., terrain) blocks using the Stamper tool in ROBLOX Studio or “Build” mode/Personal Build Server tools. Get started by heading to the Develop page, then clicking “Edit” on an active place to build in Studio or “Build” to use in-game tools.