Caustics Simulation in Unity

Part 4 in a series about creating a realistic water simulation and rendering

Caustics are ripples of light projected onto surfaces behind the refracting surface when light shines through. Caustics have the effect of concentrating light in  very distinctive bands because when the refracting surface deforms it acts like a complex lens dynamically bending and focusing the light. Like the refraction, caustics are relatively straightforward to implement in ray-tracing though very computationally expensive.

I could find no implementations of real caustics in Unity. The approaches I found use some kind of animation with static baked caustic textures that are warped in a wave-like fashion. That approach is very computationally cheap to implement and looks ok at a distance. I wanted to find a way to use my simulated water surface to create REAL caustics. Apart from making caustics more realistic, using the simulated water to drive them opens the door to caustics that follow the intensity, frequency, and location of the perturbations of the water.

The technique I found is quite complex but not that computationally expensive. It consists of 3 steps:

  1. Convert the surface mesh of the water into a high resolution normal map texture. I use linear interpolation of the surface normals, the higher resolution is important as we will see in the next steps. We perform this computation using a compute shader driven off the normal buffer generated by the mesh compute.
  2. The next step is another compute shader which performs the refraction, it shines a light beam through each point on the normal map and projects it onto a flat surface at a fixed distance below.  The light is accumulated at each point on the surface below, like a camera sensor accumulates photons. This accumulation buffer is then written to a texture. Some interesting trickery is needed to allow the GPU to concurrently access the shared buffer safely across all it’s cores in a thread-safe way. It uses the HLSL (High Level Shader Language) instruction InterlockedAdd - see Caustics.compute for more details. Sub-pixel positioning is used accumulating light rays to create a smoother result in the generated texture. This is also why the normal map needs to be high resolution: it allows us to accumulate more points of light.
  3. Finally we use the generated texture to apply a cookie to a spotlight. Light cookies are not like browser cookies, they act like a mask placed over a light that allows it to project a texture.  By using the cookie we have a light source that shines the light just as though it had been refracted through the liquid surface.

A repo containing the full Unity project we are using in this tutorial is available on GitLab. The Scene with the setup described in this section is CausticsRig.unity.