Tutorial 6: Water ReflectionThis tutorial shows how we can implement a real time water surface reflection. The most important part of the tutorial is to render the reflection texture based on the camera position. We use a frame buffer object to peform an offscreen rendering of the mirrored scene into a texture, this texture is passed to a shader to simualte water surfrace.
To find the Mirrored camera position, from the graph above we have:
Zc = Zb + 2 * x ( eq 1)
where x = H - Zb
Substiting x in the eq 1, the final height of the mirrored position is equal to:
Zb = 2H - Zc
In the CWaterEntity, we setup an offscreen render target with a color texture and a depth render buffer. The function PrepareReflectionPass activate the frame buffer object for offscreen rendering, and we then find the mirrored camera position and target as explaned before:
//enable Fbo for offscreen rendering
//send water height to the shader and enable discard, this is to clip all under water objects.
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
vec3f camPos = camera->GetPosition(); // camera position
vec3f camTarget = camPos + camera->GetForwardVector();//camera target
//if the camera is above the water, invert its position
if(camPos.z >= m_fWaterHeight)
//the mirrored camera position
camPos.z = 2.0f * m_fWaterHeight - camPos.z;
camTarget.z = 2.0f * m_fWaterHeight - camTarget.z;
vec3f forwardVector = camTarget - camPos; // find forward vector from the new positions
vec3f sideVector = camera->GetRigthVector(); // get right vector
vec3f reflectionCamUp = sideVector.crossProduct(forwardVector); //find the up vector
//setup the mirrored view matrix from the new vectors. now the scene will be rendered upside down.
m = mat4f::createLookAt(camPos, camTarget, reflectionCamUp);
m_mLookAt = m_mMirror * m;
Now that we have the reflection texture. Next, we use this texture when rendering the water surface. The water surface is composed from a simple quad rectangle where the reflection texture is mapped on it. A normal texture is used to disturb the reflection texture on different u,v direction.
In the pixel shader, we use two texture coordinates with a tile factor to fetch the normal texture. The resulting texture coordinate is combined with the fragment coordinate to fetch the mirrored texture:
//this define the number to water surface tile ( repeat texture)
const float tile_factor = 20.0;
const float noise_factor = 0.03;
vec2 texCoordNormal0 = v_texCoord * tile_factor;
texCoordNormal0 += time ;
vec2 texCoordNormal1 = v_texCoord * tile_factor;
texCoordNormal1.s -= time ;
texCoordNormal1.t += time ;
//the tex coordinate used to disturb texCoordReflection vector
vec3 normal0 = texture2D(normal, texCoordNormal0).rgb * 2.0 - 1.0;
vec3 normal1 = texture2D(normal, texCoordNormal1).rgb * 2.0 - 1.0;
vec3 normal = normalize(normal0 + normal1);
//adjust texture coordinate based on screen size
vec2 texCoordReflection = vec2(gl_FragCoord.x * screenWidth, gl_FragCoord.y * screenHeight);
//the final texture cooridnate is disturbed using the normal multiplied by the noise factor
vec2 texCoordFinal = texCoordReflection.xy + noise_factor * normal.xy;
gl_FragColor = texture2D( texture0, texCoordFinal);
In this tutorial we dont handle the case where the camera is under the water surface, in this situation we need an addition render pass to capture a refraction texture. In reality under water we can still see the refracted world, this additional texture is used to simulate refraction effect, fresnel term is used to blend the final color between the reflection and the refraction texture.
The aim of the tutorial was to introduce a simple and fast water reflection simulation on mobile. More advanced water surface simulation can be found on the net.
Some optimization that can done is to reduce the size of the reflection texture, this can be done in the CWaterEntity by reducing the size of the frame buffer object output, the current size is set to 256, try different values to see the impact on quality/performance.
m_pOffscreenRT->Initialize(kCONTEXT_COLOR | kCONTEXT_DEPTH, 256);
Another optimization is to replace the discard operation from the pixel shader when rendering terrain, the discard operation is used to clip all pixels that are above water when capturing the reflection texture. this could be done by adjusting the clip planes of the projection matrix. Check Imagination technologies SDK (water reflection sample).