Water rendering

Our goal was to make the rendering better by making the water surfaces that may exist on a terrain look like real. As for terrain texturing, we used Cg shaders to achieve it.

Detection and materialization of the lakes

When we talk about water surfaces, mostly on our planet, we think about oceans and seas that are the lowest elevated zones and, therefore, easily detectable. However, in this project (the Alpes), we mainly had to work on lakes (not all at the same altitude) instead of seas and oceans. Thus, it was important for us to detect that kind of zones in order to use shaders on them.

Using a mask to detect lakes

We created, from the heightmap and an atlas, a black and white image (with the size of the heightmap) and we painted the lakes in black, the land in white. Then, we used an edge detection algorithm. You can see below the results: on the left, the initial mask and on the right the edges detected.

masqueLacs
Mask of the lake and edge detection

Creating a Lake format

Once the edge detection was done, we just have to get back the black points in the right order and store them in a file with the .lac extension. Every lake has its own file. This file will allow to make seaground and the polygon of the lake itself.

Bringing the terrain down for the seaground

The .lac file contains points that are rightly located if you follow the heightmap order. To bring down the seaground, we just have to change the altitude of every single point in the mesh. We get back the lowest elevation of all the points. This elevation will become the altitude of the lake. This is how the newly brought down seaground looks like:

abaissementLacs
Bringing down the seaground

Creating the polygon of the lake

As for the seaground, we used the .lac file to create the polygon of the lake. Points are given line after line. We handled some peculiar lakes: we forced the file to contain only two points per line so we can directly create a triangle strip from the list of points of the file. We might have worked more on the edge detection and the creation of the polygon if we had had normal lakes. You can see below the result of the creation of the polygon and its incorporation in the terrain that has already been brought down:

lac
Adding the polygon of the lake

Reflecting and animating lakes

Reflection

Visual properties of water surfaces

Now that our mesh know where the lakes are, the next step is to make these blue uniform lakes look like real, that is to say like a water surface. But what does a water surface look like? First of all, the environment is reflected in it if you're close to the surface. Then, the reflection is distorted by the waves. For instance:

Eau Miroir
Example of reflecting water surface

Modeling water

Let's see how this natural phenomena is modeled with graphic computers. To model water with shaders, there are two widespread approaches:

  1. the first one uses particle volumes that interact so it seems to be a fluid volume quite close to the natural model. Its expensiveness is a drawback and makes it hard to render wide water surfaces (even if we use bigger particles to reduce the calculations). It could have fit if the main goal of this project had been to render water. Unfortunately, in our project, rendering water is one part among many others so we couldn't afford such a cost.
  2. the second approach, based on visual properties of water, uses a reflecting plane that gives the illusion of water. We chose that one in order to keep a low framerate. Of course, it implies having a few drawbacks: a low-angled view will clearly show that the water is planar and has no depth.

Hence, we have worked on two different parts:

  1. making the reflecting plane;
  2. animating the surface with specular reflections that move.

Making the reflecting plane - Plan A

The idea (in the OpenGL part of our program) is divided in two steps:

First pass

We make a render to texture under water. This means that all that can be watched from that position is kept in a texture.

planA
plan A
Second pass

From the normal point of view of the camera, we make a rendering of the scene that we previously stored. This gives us the reflection of the environment on the water surface.

But this technique works for wide surfaces such as seas and oceans (where there is no seaground modelled), not for lakes (that do have seaground). Our mesh prevents us from creating the reflected beam.

planA rate
The reason why plan A didn't work.

Making the reflecting plane - Plan B

Thus, we had to find new ways to have a reflecting plane. This is what we eventually did. From the center of the lake, we make six views -six textures- (up, down, left, right, front, back) in order to create a new cubemap. This is what a view looks like:

Vue
One view of our environment

This is what our views look like all together:

Vues deroulees
5 views (one is missing)

From these six views stored as a cubemap, we will be able to calculate the reflection (and perhaps refraction if needed one day). This last technique is widespread in shaders programming.

Animation

In reality, water doesn't just reflect its environment, it also waves. That's why if we just do reflection, we will have a mirror-like rendering instead of having a water-like one. So, to have a realistic rendering, we must simulate the waves on the surface. For this purpose, we disposed of several methods and, we finally chose the one that was the best compromise between complexity and rendering quality.

In this part, we will briefly talk about the methods we had planned to use. Then, we will detail more precisely the one we chose.

Existing methods

The first method we thought of was changing the elevation of the vertices and calculating again their normals in order to create a waving movement on the whole mesh. This would be followed by the calculation of the reflection thanks to a cubemap. That method is divided in three passes into the shaders. During the first pass, the force applied to a vertex is calculated from its elevation (declared as a Cg texture parameter). The result is saved as a color. Eventually, the framebuffer (depicting the forces applied to every vertex) that is rendered in a texture, is recovered. The second pass allows to calculate the speed of every vertex from the texture recovered before. The result is got back as before (through the framebuffer and saved in a texture).

Finally, during the last pass, the new elevation of the vertices is calculated thanks to speed texture. Then, the normals of every vertex have also to be re-calculated. The reflection can now be calculated thanks to a cubemap. However, in that moment, we were thinking of implementing the reflection by moving the camera, as thought in plan A. That technique didn't fit with plan A.

The second method is quite similar to the first one (even if it's older: this one is extracted from an article dated 1999 whereas the first one is dated 2004). It also implies changing the elevation of the vertices to create a waving movement (though the source site implements the mesh animation in C++, we would have done it as in the preceding method). But instead of calculating the reflection with the cubemap, pixels are coloured according to their neighbours' position in order to create an artificial lighting. The result can be mixed with any background texture, in our case, the reflection texture.

The advantage of these techniques is that they're realistic, especially the first one. But we considered that the mesh animation had two drawbacks. It is more complex because it needs to pass pieces of information on a vertex's neighbour (elevation...). It is hard to do when programming in Cg, so these kind of information need to be cleverly passed as textures or colours. What is more, it is very expensive because heights and normals have to be re-computed at every rendering.

Implemented method

The method that we eventually implemented simulates waves on a planar surface. The way we handle colours will make the surface look like moving. For this purpose, we move a noise texture that we mix with reflection. All this processing is done in the fragment shader.

We use a noise texture (called Noise in the fragment shader), in grayscale as below. It is important that this texture might be divided (we will explain that later).

bruit
Noise texture

To create the waving effect, the noise texture has to be duplicated. The two resulting textures are then moved independently. All this is done in the fragment shader, that is to say applied to pixels. We modify once the coordinates of the current pixel (first variable). We repeat that but the moving is slightly different (so the two textures don't move in the same way) and we store the colour in a second variable. To modify the coordinates, we use periodic functions, here sine and cosine, that we make change with a parameter modified in the .cpp at every rendering, and that we pass to the fragment shader.

Then we mix the two colours seen before with the color from the reflection in order to distort it. This makes the whole look like moving but we still need specular effects to make it more realistic. For this purpose, we use a gradient as on the figure below, which allows to make the clear colours more pale (to simulate specular stains) and to add some little blue to the reflection colour (so it seems more aquatic). See the diagram below:

animEau
Summary diagram

This processing is not applied straight away on the whole surface, it is repeated many times. That's why the noise texture has to be divisible: it prevents form artefacts between the different textured pieces. See below the final rendering:

renduEau
Final rendering