Users of the Triton Ocean SDK sometimes want to have direct control over the transparency of Triton’s water.
Doing this properly would involve maintaining a refraction texture map for underwater objects and terrain, similar to the reflection map we support for above-water reflections. But, it is possible to approximate the effect by just adjusting the transparency of the water, with a result like this image. It doesn’t properly model the distortion of underwater features you’d see in real life, but it does model how areas of the water will either refract or reflect light depending on the angles involved.
OpenGL users may do this by using Triton’s extensible shader framework. We’ll be working with the user-functions.glsl file found in the Resources subdirectory of Triton; so open that with a text editor.
We’ll start by adding the following at the top of the file:
#define WATER_TRANSPARENCY 0.8
If you wanted to make the transparency adjustable at runtime, you could instead make WATER_TRANSPARENCY a uniform float value. Triton::Ocean::GetShaderObject() can then be used to retrieve the OpenGL shader program ID’s needed to retrieve the uniform locations you need to set this value from your own code.
We are extending Triton’s fragment shaders here. For each fragment, some light will be reflected from above, and some will be refracted through the water surface – those areas of refraction are the ones we’ll make transparent. Computing the balance of reflected and refracted light involves the Fresnel equations, but we can quickly approximate this value by using the vectors passed into the user_lighting hook inside user-functions.glsl. Replace the user_lighting function with the following:
// Light, view, and normal vectors are all in world space.
// This function may be used to modify the ambient, diffuse, and specular light computed by Triton's fragment shaders.
void user_lighting(in vec3 L
, in vec3 vVertex_World_Space, in vec3 vNormal_World_Space
, in vec4 vVertex_Projection_Space
, inout vec3 ambient, inout vec3 diffuse, inout vec3 specular)
vec3 vNorm = normalize(vVertex_World_Space);
vec3 nNorm = normalize(vNormal_World_Space);
vec3 P = reflect(vNorm, nNorm);
reflectivity = max(0.0,min(1.0,r+(1.0-r)*pow((1.0-dot(nNorm,P)), 4.0)));
This computes the value of “reflectivity,” which we now must apply when modifying the final ocean fragment’s alpha value. To do that, we can replace the user_tonemap function with this:
// The final computed color is normally just clamped to (0,1), but you may override this behavior here.
void user_tonemap(in vec4 preToneMapColor, inout vec4 postToneMapColor)
postToneMapColor = mix(vec4(postToneMapColor.xyz, postToneMapColor.a * WATER_TRANSPARENCY), postToneMapColor, reflectivity);
A limitation of this technique is that artifacts may be seen at higher sea states from certain angles. Transparent objects should be drawn back-to-front, but we can’t do that given that the water is just one big mesh. Having good depth buffer resolution can help, so make sure your near clip plane is as far out, and your far clip plane as far in, as you can get away with.
The ability to extend the shaders in Triton and SilverLining opens up a lot of possibilities, and we’ll cover some more examples in future posts.