Water in OpenGL and GLES 2.0: Part3 - Reflection

Continuing from the previous part of this article on creating a water effect in Gameplay3D, in this part we'll cover creating reflections on the surface of the water. It is important that you have read and completed part two, and that you have the refraction buffer drawing, previewed, and projected on to the water plane. This is because before we can continue we need to replicate the refraction buffer with a new member *m_reflectionBuffer, as well a new sprite batch *m_reflectBatch to draw the preview. Add these to the project, initialise them in the initialise() function, release and delete them in the finalise() function, and update the render() function so that the scene is drawn to the new reflection buffer, and the reflection buffer preview is drawn next to the preview window of the refraction buffer - all in the same way as the refraction buffer.

Reflecting the View
Once you have the scene set up we can start to modify the process, so that instead of getting a duplicate of the refraction buffer, we actually get a reflection. Firstly modify the clip plane settings in the render function right before drawing the reflection buffer:

m_clipPlane.y = 1.f;
m_clipPlane.w = -m_waterHeight;

By inverting the normal direction and the plane height the plane now faces the opposite direction. When you compile and load the scene you should see in the preview window that the grass is kept, and that the bottom of the pond is clipped away instead. This is because we want to reflect the scene as it appears above the water. Next we need to consider how to invert the image vertically, as a reflection would appear in the water. A reflection isn't just the image as seen from the camera, only upside down, however. What we see is, in fact, what would be seen by a camera below the water plane, targeted at the same point as the scene's camera:


If the scene's main camera is camera A, then the reflection it sees is the same as if the scene were viewed from camera B. If you've been reading the reference articles linked at the bottom of these posts, you'll see each one offers its own implementation of this camera set up. If we were using raw OpenGL the preferable way would be to use a reflection matrix but, as this article is based around the Gameplay framework, the option is not particularly viable. An alternative would be to scale the entire scene in the Y axis by -1 during the reflection pass, which is possible, but has the drawback of not easily being able to store the WorldViewProjection matrix (more on this shortly). Finally we could create a second camera in place of camera B on the diagram, by taking the forward and right vectors of the scene camera, computing the cross product of the two vectors to find the up vector, and using them to compute a new LookAt matrix each frame, to orient camera B in the right direction. The latter seems a little heavy to do each frame so I settled on (perhaps controversially) creating a second camera in the scene, and having it follow the movements of the main camera, only mirrored about the water plane.

Creating the Second Camera
In the initialise() function directly after creating the scene camera:

//add a second camera do draw the reflections
m_reflectCamNode = gp::Node::create("reflectCamNode");
m_reflectCamNode->setTranslation(camStartPosition.x, -camStartPosition.y, camStartPosition.z);
 

camPitchNode = gp::Node::create();
gp::Matrix::createLookAt(m_reflectCamNode->getTranslation(), gp::Vector3::zero(), gp::Vector3::unitY(), &m);
camPitchNode->rotate(m);
m_reflectCamNode->addChild(camPitchNode);
 

camera = gp::Camera::createPerspective(45.f, gp::Game::getInstance()->getAspectRatio(), 0.1f, 150.f);
camPitchNode->setCamera(camera);
SAFE_RELEASE(camera);
SAFE_RELEASE(camPitchNode);

This is pretty much a duplicate of the scene camera creation code although, crucially, the Y component of the start point vector is negated, so the the initial LookAt matrix is a reflection of that of the scene camera. Next we need to modify the mouse move event, so that the new camera's pitch movement is inverse to that of the scene's main camera, while the yaw remains the same.

m_reflectCamNode->rotateY(xMovement);
m_reflectCamNode->getFirstChild()->rotateX(-yMovement);

And, of course, we need to make sure that it follows the translation of the main camera in the update() function

auto position = m_cameraNode->getTranslation();
position.y = -position.y + m_waterHeight * 2.f;
m_reflectCamNode->setTranslation(position);

while making sure the Y position is reflected about the water plane by negating it, and adding the plane height multiplied by two. Now when drawing the scene to the reflection buffer we can switch cameras by making a copy of the active scene camera, making the reflection camera the new active scene camera, rendering the reflected scene and then restoring the original camera before drawing the final pass.
   The preview window for the reflection buffer displays the edges of the pond, as seen from below from the view of the reflection camera, and is ready to be projected onto the water plane. In part two of the article the last step was to project the refraction buffer on to the plane via the water shader. We need to do the same thing again, only this time we are using a different camera, so we need to use the corresponding WorldViewProjection matrix to generate the texture coordinates. While the reflection camera is active we can store the plane's WorldViewProjection matrix in a member variable

m_worldViewProjectionReflection = m_scene->findNode("Water")->getWorldViewProjectionMatrix();

This is important that we do this here because *the matrix is only valid while the reflection camera is active*, and is why we store it in a member variable. Adding a private function which returns a const reference to m_worldViewProjectionReflection will then allow us to bind it to the water shader in the same way as the other shader-bound variables which, hopefully, you should now be familiar with. All that's left to do, then, is modify watersample.vert and watersample.frag with uniforms for the new projection matrix and the reflection buffer sampler, in the same way in which we added the refraction buffer previously.

Updating the Shader
In the vertex shader:

uniform mat4 u_worldViewProjectionReflectionMatrix;
varying vec4 v_vertexReflectionPosition;

and

v_vertexReflectionPosition = u_worldViewProjectionReflectionMatrix * a_position;

and in the fragment shader we sample the reflection texture with the new coordinates

textureCoord = fromClipSpace(v_vertexReflectionPosition);    
vec4 reflectionColour = texture2D(u_reflectionTexture, textureCoord);

To see the result we can assign reflectionColour directly to gl_FragColor. Notice how, because we projected the texture as if it were from the reflection camera, the image is automatically flipped! You should have something which looks like a flat, glossy mirror, albeit with some slight artifacting due to the lower resolution render buffer.


Now we are most of the way there. The only things left to do are to blend the reflection and refraction maps in the watersample fragment shader, and add some animated waves to make the scene look a bit more natural. I will cover that in the next, and final, part of this article.

Part Four

References:
Eric Pacelli
Lauris Kaplinski
Riemer's XNA page

Source Code:
Github page

Previous Parts:
Part One
Part Two

Comments

Popular Posts