There's something hypnotic about the way water interacts with light: the subtle reflections and refractions, the way light bends to form dancing light patterns on the bottom of the sea, and the infinitely-varied look of the ocean surface. These phenomena and their complexity have attracted many researchers from the physics field and, in recent years, the computer graphics arena. Simulating and rendering realistic water is, like simulating fire, a fascinating task: it is not easy to achieve good results at interactive frame rates, and thus creative thinking must often be used.
This article explains an aesthetics-driven method for rendering the underwater lighting effects known as "caustics" in real-time. We believe the technique is fully original, and has very low computational cost. It is a purely aesthetics-driven approach, and thus realism is simply out of consideration: we try to create something that looks good, not something that is right from a simulation standpoint. As we will soon see, results look remarkably realistic, and the method can easily be implemented in most graphics hardware. This simplified approach has proven very successful in many fractal-related disciplines, such as mountain and cloud rendering or tree modeling.
The purpose of this article is twofold. First, it tries to expose a new technique, from its physical foundations to the implementation details. As the technique is procedural, it yields elegantly to a shader-based implementation. Thus, a second objective is to showcase the conversion from regular OpenGL code to an implementation using the Cg programming language. This article should then satisfy both those interested in water rendering algorithms, as well as those wanting an introduction to pixel shader programming in Cg.
Computing underwater caustics accurately is a complex process: millions of individual photons are involved, with many interactions taking place. To simulate it properly, we must begin by shooting photons from the light source (for a sea scene, the sun). A fraction of these photons eventually collide with the ocean surface, either reflecting or refracting themselves. Let's forget about reflection for a second, and see how refracted photons bend their trajectory according to Snell's Law, which states that:
sin (incoming)/sin (refracted) = IOR
The formulation above makes only one restriction, and thus is not very convenient as a way to compute the refracted ray. Assuming the incident, transmitted and surface normal rays must be co-planar, a variety of coder-friendly formulas can be used, such as the one explained in Foley & VanDam's book:
Here T is the transmitted ray, N is the surface normal, E is the incident ray, and na, nb are the incides of the refraction.
Once bent, photons advance through the water, their intensity attenuating as they reach deeper. Eventually, part of those photons will strike the ocean floor, lighting it. As the ocean's surface was wavy, photons following different paths can end up lighting the same area. Whenever this happens, we see a bright spot which is a caustic created by the light concentrating, in a similar way that a lens can burn a piece of paper.
From a simulation standpoint, caustics are usually computed by either forward or backwards ray tracing. In forward ray tracing, photons are sent from light sources and followed through the scene, accumulating their contribution over discrete areas of the ground. The problem with this approach is that many photons do not even collide with the ocean surface and, from those that actually collide with it, very few actually contribute to caustic formation. Thus, it is a brute-force method, even with some speed-ups thanks to spatial subdivision.
Backwards ray-tracing works on the opposite direction. It begins at the ocean floor, and traces rays backwards in reverse chronological order, trying to compute the sum of all incoming lighting for a given point. Ideally, this would be achieved by solving the semi-spherical integral of all light coming from above the point being lit. Still, for practical reasons the result of the integral is resolved via Monte Carlo sampling. Thus, a beam of candidate rays is sent in all directions over the hemisphere centered at the sampling point. Those that hit other objects (such as a whale, ship or stone) are discarded. On the other hand, rays that hit the ocean surface definitely came from the outside, making them good candidate. Thus, they must be refracted, using the inverse of Snell's Law. These remaining rays must be propagated in the air, to test whether that hypothetic ray actually emanated from a light source, or was simply a false hypothesis. Again, only those rays that actually end up hitting a light source do contribute to the caustic, and the rest of the rays are just discarded as false hypothesis.
Both approaches are thus very costly: only a tiny portion of the computation time actually contributes to the end result. In commercial caustic processors it is common to see ratios of useful vs. total rays between 1 and 5%.
Real-time caustics were first explored by and Jos Stam . Their approach involved computing an animated caustic texture using wave theory, so it can be used to light the ground floor. This texture was additively blended with the object's base textures, giving a nice convincing look.
Figure 1: picture showing the caustics create using Stam's textures
Another interesting approach was explored by Lasse Staff Jensen and Robert Golias in their excellent Gamasutra paper . The paper covers not only caustics, but a complete water animation and rendering framework. The platform is based upon Fast Fourier Transforms (FFTs) for wave function modeling. On top of that, it handles reflection, refraction, and caustics attempting to reach physically-accurate models for each one. It is unsurprising, then, that their approach to caustics tries to model the actual process: rays are traced from the Sun to each vertex in the wave mesh. Those rays are refracted using Snell's Law, and thus new rays are created.
The algorithm we use to simulate underwater caustics is just a simplification of the backwards Monte Carlo ray tracing idea explained above. We make some aggressive assumptions on good candidates for caustics, and thus compute only a subset of the arriving rays. Thus, the method has very low computational cost, and produces something that, while being totally incorrect from a physical standpoint, very closely resembles a caustic's look and behavior.
To begin with, we shall assume we are computing caustics at noon in the Equator. This implies the sun is directly above us. For the sake of our algorithm, we will need to compute the angle of the sky covered by the sun disk. The sun is between 147 and 152 million kilometers away from Earth depending on the time of the year, and its diameter is 1,42 million kilometers. Half a page of algebra and trigonometry yield an angle for the Sun disk of 0.53º. Still, the Sun is surrounded by a very bright halo, which can be considered as a secondary light source. The area covered by the halo is about ten times larger than the Sun itself.
The second assumption we shall make is that the ocean floor is located at a constant depth-usually relatively shallow. The transparency of water is between 77 and 80% per linear meter. This means that between 20 and 23% of incident light per meter is absorbed by the medium (heating it up), giving a total visibility range between fifteen and twenty meters. Logically, this means caustics will be formed most easily when light rays travel the shortest distance from the moment they enter the water to the moment they hit the ocean floor. Thus, caustics will be maximal for vertical rays, and will not be so visible for rays entering water sideways. This is an aggressive assumption, but is key to the success of the algorithm.
Then, our algorithm works as follows: we start at the bottom of the sea, right after we have painted the ground plane. A second additive, blended pass is used to render the caustic on top of that. To do so, we create a mesh with the same granularity as the wave mesh, and which will be colored per-vertex with the caustic value: zero means no lighting, one means a beam of very focused light hit the sea bottom. To construct this lighting, backwards ray tracing is used: for each vertex of the said mesh, we project it vertically until we reach the wave point located right on top of it. Then, we compute the normal of the wave at that point using finite differences. With the vector and the normal, and using Snell's Law (remember the IOR for water is 1.33333) we can create secondary rays, which travel from the wave into the air. These rays are potential candidates for bringing illumination onto the ocean floor. To test them, we compute the angle between them and the vertical. As the Sun disk is very far away, we can simply use this angle as a measure of illumination: the closer to the vertical, the more light that comes from that direction into the ocean.
Implementation Using OpenGL
The initial implementation of the algorithm is just plain OpenGL code with the only exception being the use of multipass texturing. A first pass renders the ocean floor as a regular textured quad. Then, the same floor is painted again using a fine mesh, which is lit per-vertex using our caustic generator as can be seen in the figures. For each vertex in the fine mesh, we shoot a ray vertically, collide it with the ocean surface, generate the bent ray using Snell's Law, and use that ray to perform a texture map lookup, such as the one in figure 2. In the end, the operation is not very different from a planar environment mapping pass. The third and final pass renders the ocean waves using our waveform generator. These triangles will be applied a planar environment mapping, so we get a nice sky reflection on them. Other effects such as Fresnel's equation can be implemented as well on top of that. Here is the full pseudo-code for this version:
For each vertex in the fine mesh
Send a vertical ray
Collide the ray with the ocean's mesh
Compute the refracted ray using Snell's law reversed
Use the refracted ray to compute texturing coordinates on the "sun" map
Apply textured coordinates to vertex in finer mesh
Figure 2: OpenGL implementation, showing the wireframe and solid mode