Since my last post about my water shader, I’ve been making continual tweaks. The next big thing I did was to investigate water optics. I want to have control over a small set of parameters that let me get the kind of water I want: open ocean, swamps, lakes, etc…

## Fake reflections revisited

Before I get into the water optics, I’ll just talk a bit more about my water reflections (mentioned in my previous post).

### Fewer instructions

The first thing to note is that, with the help of someone on the gamedev.net forums, I’ve reduced the shader instruction count from around 65 instructions to about 22 instructions. I got rid of all the trigonometric functions (as I suspected might be possible).

It turns out it is fairly straightforward to express the values for cos(nθ) and sin(nθ) as a function of the x and y variables whose arctangent gave you θ in the first place. As a result, evaluating the Fourier series now looks like this:

float EvaluateFourier(float yIn, float xIn, float4 coefs1, float4 coefs2) { float2 yx = normalize(float2(yIn, xIn)); float y = yx.x; float x = yx.y; float value = coefs1.r; // a0 // Precalcing these saves a couple of instructions. float xx = x * x; float yy = y * y; // cos(a) value += coefs1.g * x; // sin(a) value += coefs1.b * y; // cos(2a) value += coefs1.a * (xx - yy); // sin(2a) value += coefs2.r * (2 * x * y); // cos(3a) value += coefs2.g * (x * xx - 3 * x * yy); // sin(3a) value += coefs2.b * (3 * xx * y - yy * y); // cos(4a) value += coefs2.a * ((xx * xx) - (6 * xx * yy) + (yy * yy)); return value; }

### Even fewer instructions

If you recall, the evaluation of the Fourier series gives us the angle above the horizon at which the sky begins – thus letting us fake a reflection by making it dark for reflected rays below that angle. The problem with this is that it involves computing an atan2 in the shader (about 20 instructions). However, I don’t need an actual arctangent; I simply need values that let me make a greater than/less than comparison. So instead, I can simply express the value as a ratio between the two components – a couple of instructions instead of 20. Basically I’m comparing the tangent of the angles instead of the angles.

### Compressing the Fourier coefficients

I did some experiments and found that the reflections tend to suffer if I go much below 8 coefficients. However, I’ve been able to compress each coefficient down to 1 byte, for a total of 8 bytes per vertex. The higher order coefficients tend to be rather close to zero, so I apply some bias such that the largest of them fit in the (0, 1) range. I reverse the bias in the vertex shader to avoid doing it in the pixel shader.

## Water color and optics

After being satisfied with the reflections, my next order of business was to get the water color to look correct.

I spent a while with techniques in the Procedural Ocean Effects from Shader X6, and also from this excellent article. The latter technique unfortunately seemed to require too many parameters to tweak (RGB extinction, deep color, shallow color, visibility, fade speed). It was too easy to get something that just looked wrong, with conflicting colors. The images that accompany that article look very artificial.

I dove into a famous older technical paper on the subject, but I could not get anything that looked reasonable – the numbers they provide in that paper just don’t seem to add up, and there are missing pieces. Apparently the color of the ocean floor is not taken into account, so it makes it only useful for deep water.

In the end I decided to hack together my own system. I wanted as few parameters as possible. I was hoping to get away with only an extinction color and a “scattering” coefficient, but I had to add RGB values for the scattering to get something that looked good. That sort of makes sense – the colors that are scattered are only somewhat related to those that get absorbed. So basically the parameters I have are:

**Extinction**(RGB)**Scattering coefficient**(how murky the water is)- An
**RGB scatter color** - An overall scale for all these parameters, WorldUnitsToMeters. This really just needs to be set once for your game world, and depends on its scale. I scale the Extinction and Scattering coefficient by this value prior to sending them to the shader. The screenshots in this article use 6 for this value.

So there are roughly 7 values to tweak if you count individual color components.

I’ll now describe how it works. You can refer to the following diagram:

A certain amount of light (sunlight + ambient light + whatever you’re using) hits the surface of the water (top right in the image above). It travels downward while being extinguished at the rate described by **Extinction**. It hits the bottom and lights up the diffuse sample of the lake/ocean/river bottom. From them it travels back up towards the surface towards the water pixel we’re rendering, all the while being extinguished along the ” accumulated depth”. Accumulated depth is the distance from the water pixel being rendered to the point we’re sampling from on the water bottom.

We calculate the **scattering** amount also based on accumulated depth. This is basically the actual color of the water. It’s nearly zero for clear bodies of water, and much higher for turbid water where lots of light is scattered. The end result is a value from 0 to 1. We use this to interpolated between the refracted value described in the previous paragraph and the **RGB scatter** color. This provides our final refracted pixel color (which is then combined with the reflected value based on the Fresnel factor).

The equations are:

(note, sometimes the formulas above appear corrupted in the browser. In that case, try this).

where

- c_sample is the refracted sample (bottom of the water body)
- I is the incident light as described in the previous paragraphs
- K is the
**Extinction**value (RGB)*((0.46, 0.09, 0. 06) for tropical ocean)* - b is the
**scattering coefficient***((0.11) for tropical ocean)* - c_scatter is the
**RGB scatter color***((0.05, 0.05, 0.10) for tropical ocean)* - d is the depth and d_acc is the accumulated depth

In the shader, this looks like the following.

float3 GetWaterColor(float accumulatedWater, float depth, float3 refractionValue, float3 incidentLight) { // This tracks the incident light coming down, lighting up the ocean bed and then travelling back up to the surface. float3 refractionAmountAtSurface = refractionValue * exp(-Extinction * (depth + accumulatedWater)); // This tracks the scattering that occurs. // Scattering should quickly max out with depth (since the amount of scatter added gets weaker and weaker the deewpr your go.) float inverseScatterAmount = exp(-ScatteringCoeff * accumulatedWater); return lerp(ScatterColor, refractionAmountAtSurface, inverseScatterAmount) * incidentLight; }

## Future work

So I now have flowing water, decent optical qualities, and some “faked” reflections.

I also have some refractive effects that are implemented by offsetting my refraction sample value by the planar components of the normal. There can be artifacts along water edges though, and I’m not sure how necessary this will be given that my world will generally be viewed from high above.

I would still like to have some physical waves (and interactions) and foam, but I think the next step is to get this working in my actual game engine. I’m sure to hit some snags here, either with shadows, my fog implementation, or performance, or some such thing.

I also need to figure out to implement the map that describes the water optical qualities. Ideally I set up predefined profiles (muddy river, open ocean, dark lake, etc…) and there is just an index associated with each world unit. I could calculate the actual scattering colors and such based on this in the vertex shader, and pass these values to the pixel shader.

[…] issues I encountered when trying to fit my water shader (discussed previously in part one and part two) into the engine for my game, which uses deferred rendering. Not all of the discussion is relevant […]