3 Texture and Illumination

Normal mapping can have better effects when dealing with the details.
Then we use Normal Mapping.
This tutorial has a very clear structure. We will add it to our previous mesh.

We get a texture and its normal map from the Internet.

texture.png
texture normal.png

There are some additional attributes: normal, tangent and bitangent. Previously, we have only 5 attributes and now we have 14 attributes.
We have to add those values and we need a new array to store all the values.

static float quadVertices3[(SIZE_WATER-1)*(SIZE_WATER-1)*3*2*14] = {0.0f};

It is clearly demonstrated how to get tangent and bitangent in the tutorial.
Now we have to reuse the wdata array. And we need to load these data according to its indices which is a little troublesome.

// positions
glm::vec3 npos1(wdata[data_index],  wdata[data_index+1], wdata[data_index+2]);
glm::vec3 npos2(wdata[data_index+5],  wdata[data_index+5+1], wdata[data_index+5+2]);
glm::vec3 npos3(wdata[data_index+SIZE_WATER*5],  wdata[data_index+SIZE_WATER*5+1], wdata[data_index+SIZE_WATER*5+2]);
glm::vec3 npos4(wdata[data_index+(SIZE_WATER+1)*5],  wdata[data_index+(SIZE_WATER+1)*5+1], wdata[data_index+(SIZE_WATER+1)*5+2]);

The texture coordinates are easy to handle.

 // texture coordinates
glm::vec2 nuv1(0.0f, 0.0f);
glm::vec2 nuv2(1.0f, 0.0f);
glm::vec2 nuv3(0.0f, 1.0f);
glm::vec2 nuv4(1.0f, 1.0f);

Normal here is the same.

glm::vec3 nm2(0.0f, 0.0f, 1.0f);

How to compute tangent and bitangent are also well demonstrated in the tutorial which are
the cross products.

for(long i = 0; i < (SIZE_WATER-1)*(SIZE_WATER-1)*3*2*14; i = i +14*6)
    {
        if((data_index/5) % SIZE_WATER == SIZE_WATER-1)
            data_index += 5;
        
        
        /* -------------------- tangent ---------------------------------------*/
        // positions
        glm::vec3 npos1(wdata[data_index],  wdata[data_index+1], wdata[data_index+2]);
        glm::vec3 npos2(wdata[data_index+5],  wdata[data_index+5+1], wdata[data_index+5+2]);
        glm::vec3 npos3(wdata[data_index+SIZE_WATER*5],  wdata[data_index+SIZE_WATER*5+1], wdata[data_index+SIZE_WATER*5+2]);
        glm::vec3 npos4(wdata[data_index+(SIZE_WATER+1)*5],  wdata[data_index+(SIZE_WATER+1)*5+1], wdata[data_index+(SIZE_WATER+1)*5+2]);
        // texture coordinates
        glm::vec2 nuv1(0.0f, 0.0f);
        glm::vec2 nuv2(1.0f, 0.0f);
        glm::vec2 nuv3(0.0f, 1.0f);
        glm::vec2 nuv4(1.0f, 1.0f);
        // calculate tangent/bitangent vectors of both triangles
        glm::vec3 ntangent1, nbitangent1;
        glm::vec3 ntangent2, nbitangent2;
        // new tri 1
        glm::vec3 nedge1 = npos2 - npos1;
        glm::vec3 nedge2 = npos2 - npos4;
        glm::vec2 ndeltaUV1 = nuv2 - nuv1;
        glm::vec2 ndeltaUV2 = nuv2 - nuv4;
        
        GLfloat nf = 1.0f / (ndeltaUV1.x * ndeltaUV2.y - ndeltaUV2.x * ndeltaUV1.y);
        
        ntangent1.x = nf * (ndeltaUV2.y * nedge1.x - ndeltaUV1.y * nedge2.x);
        ntangent1.y = nf * (ndeltaUV2.y * nedge1.y - ndeltaUV1.y * nedge2.y);
        ntangent1.z = nf * (ndeltaUV2.y * nedge1.z - ndeltaUV1.y * nedge2.z);
        ntangent1 = glm::normalize(ntangent1);
        
        nbitangent1.x = nf * (-ndeltaUV2.x * nedge1.x + ndeltaUV1.x * nedge2.x);
        nbitangent1.y = nf * (-ndeltaUV2.x * nedge1.y + ndeltaUV1.x * nedge2.y);
        nbitangent1.z = nf * (-ndeltaUV2.x * nedge1.z + ndeltaUV1.x * nedge2.z);
        nbitangent1 = glm::normalize(nbitangent1);
        
        // new tri 2
        nedge1 = npos3 - npos1;
        nedge2 = npos3 - npos4;
        ndeltaUV1 = nuv3 - nuv1;
        ndeltaUV2 = nuv3 - nuv4;
        
        nf = 1.0f / (ndeltaUV1.x * ndeltaUV2.y - ndeltaUV2.x * ndeltaUV1.y);
        
        ntangent2.x = nf * (ndeltaUV2.y * nedge1.x - ndeltaUV1.y * nedge2.x);
        ntangent2.y = nf * (ndeltaUV2.y * nedge1.y - ndeltaUV1.y * nedge2.y);
        ntangent2.z = nf * (ndeltaUV2.y * nedge1.z - ndeltaUV1.y * nedge2.z);
        ntangent2 = glm::normalize(ntangent2);
        
        nbitangent2.x = nf * (-ndeltaUV2.x * nedge1.x + ndeltaUV1.x * nedge2.x);
        nbitangent2.y = nf * (-ndeltaUV2.x * nedge1.y + ndeltaUV1.x * nedge2.y);
        nbitangent2.z = nf * (-ndeltaUV2.x * nedge1.z + ndeltaUV1.x * nedge2.z);
        nbitangent2 = glm::normalize(nbitangent2);
        
        /* -----------------------------------------------------------*/
        update2(data_index, i, ntangent1, nbitangent1);
        update2(data_index+(SIZE_WATER+1)*5, i+14, ntangent1, nbitangent1);
        update2(data_index+1*5, i+14*2, ntangent1, nbitangent1);
        
        update2(data_index, i+14*3, ntangent2, nbitangent2);
        update2(data_index+(SIZE_WATER+1)*5, i+14*4, ntangent2, nbitangent2);
        update2(data_index+SIZE_WATER*5, i+14*5, ntangent2, nbitangent2);

Vertex shader used in tutorial.

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in vec3 aTangent;
layout (location = 4) in vec3 aBitangent;

out VS_OUT {
    vec3 FragPos;
    vec2 TexCoords;
    vec3 TangentLightPos;
    vec3 TangentViewPos;
    vec3 TangentFragPos;
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

uniform vec3 lightPos;
uniform vec3 viewPos;

void main()
{
    vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
    vs_out.TexCoords = aTexCoords;
    
    mat3 normalMatrix = transpose(inverse(mat3(model)));
    vec3 T = normalize(normalMatrix * aTangent);
    vec3 N = normalize(normalMatrix * aNormal);
    T = normalize(T - dot(T, N) * N);
    vec3 B = cross(N, T);
    
    mat3 TBN = transpose(mat3(T, B, N));
    vs_out.TangentLightPos = TBN * lightPos;
    vs_out.TangentViewPos  = TBN * viewPos;
    vs_out.TangentFragPos  = TBN * vs_out.FragPos;
    
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}

Fragment shader used in the tutorial. Blinn-Phong Illumination model.

Code:

#version 330 core
out vec4 FragColor;

in VS_OUT {
    vec3 FragPos;
    vec2 TexCoords;
    vec3 TangentLightPos;
    vec3 TangentViewPos;
    vec3 TangentFragPos;
} fs_in;

uniform sampler2D diffuseMap;
uniform sampler2D normalMap;

uniform vec3 lightPos;
uniform vec3 viewPos;

void main()
{
    // obtain normal from normal map in range [0,1]
    vec3 normal = texture(normalMap, fs_in.TexCoords).rgb;
    // transform normal vector to range [-1,1]
    normal = normalize(normal * 2.0 - 1.0);  // this normal is in tangent space
    
    // get diffuse color
    vec3 color = texture(diffuseMap, fs_in.TexCoords).rgb;
    // ambient
    vec3 ambient = 0.1 * color;
    // diffuse
    vec3 lightDir = normalize(fs_in.TangentLightPos - fs_in.TangentFragPos);
    float diff = max(dot(lightDir, normal), 0.0);
    vec3 diffuse = diff * color;
    // specular
    vec3 viewDir = normalize(fs_in.TangentViewPos - fs_in.TangentFragPos);
    vec3 reflectDir = reflect(-lightDir, normal);
    vec3 halfwayDir = normalize(lightDir + viewDir);
    float spec = pow(max(dot(normal, halfwayDir), 0.0), 32.0);
    
    vec3 specular = vec3(0.2) * spec;
    FragColor = vec4(ambient + diffuse + specular, 1.0);
}

2 Water Ripple Simulation

1 Water Ripple Generation

Without external forces, the water surface would stay calm. In order to have water ripples, vibration sources must be added to the water. The style of the water ripple is different with different ways of initialization. The most common way is to initialize a circle area to a specific value.

Simulation Algorithms of Water Wave Caused by Moving Body:

According to wave theory, when the oscillation of water ripples happens, 
the vibration would move along the negative z-direction and this vibration would spread to other directions in xy surface 
which means a negative impulse should be added to the mesh to cause the vibration.

A(x,y) is a round area with the center (x,y) and radius r. And use A(x,y) to initialize the mesh(Adding a negative impulse to the mesh, a positive one has no differences).If r=1 , then we just change one value of the node in the mesh. It is not bad to simulate the raindrop effect.

void init(int x, int y, float energy)
{
     nwater2[i][j] = energy;
}
2 Water Ripple Propagation

The article has the derivation process of the water ripple and it is not difficult.
In wave mechanics, small amplitude wave is a common and basic model to describe water wave. According to the small amplitude wave theory, the water height can be described as follow:

\eta = -\frac{\omega}{g}D\cdot coth(kh)\cdot sin(\omega t+k_{1}x+k_{2}z+\phi)

Generally, it is a complex sine function where parameters are not important here.

Let

\hat t = \frac{t_i+t_{i+1}}{2}
\Delta t = t_{i+1} -t_i (i\ge0)

This function can be deduced:

\eta(x,z,t_i) + \eta(x,z,t_{i+1}) = 2\eta(x,z,\hat t) cos\frac{\omega\Delta t}{2}

Ideally, water wave propagation is isotropic. And the height of a cell is affected by the heights of its adjacent cells in the previous moment.

\eta(x,z,\hat t)

Can be estimated by the heights of its neighbor points.
Suppose the mesh is like this:

mesh03.png
\eta(x,z,\hat t)= \frac{1}{r}\sum\limits_{j=1}^{r}\eta(x_j,z_j,t_i)

Here r is 4, let

2cos\frac{\omega\Delta t}{2} = 1

and the law of energy conservation should be taken into consideration.

If r is 8, then

cos\frac{\omega\Delta t}{2} = 1

The article only mentions when r=8.

Then the evolving formula is

\eta(x_k,z_k,t_{i+1}) = \frac{1}{4}\sum\limits_{j=1}^{4}\eta(x_j,z_j,t_i)- \eta(x_k,z_k,t_i)
3 Fading Process

Ideally, it has to follow the law of energy conservation. But in reality, the energy can have the loss. It the energy increased, then the effect of the water surface would be a mess. We just to multiple a variable which has a value between 0 and 1 to make the ripple fade away.

void spread()
{
    for(int i = 1;i < SIZE_WATER-1; i++)
    {
        for(int j = 1; j < SIZE_WATER-1; j++)
        {
            float y = nwater1[i-1][j] + nwater1[i+1][j] + nwater1[i][j-1] + nwater1[i][j+1];
            nwater2[i][j] = y * 0.5f - nwater2[i][j];
            nwater2[i][j] -= nwater2[i][j] / 32.0f;
        }
    }
    for(int i = 1;i < SIZE_WATER-1; i++)
    {
        for(int j = 1; j < SIZE_WATER-1; j++)
        {
            ntmp[i][j] = nwater1[i][j];
            nwater1[i][j] = nwater2[i][j];
            nwater2[i][j] = ntmp[i][j];
        }
    }
}

The water ripple effect:

mesh04.png
mesh05.png
mesh06.png
mesh07.png
mesh08.png
mesh09.png
mesh10.png

This algorithm is very awesome that its simple rules can easily handle the interaction of many waves and the obstacles. Adding a loss can simulate the fading process well.

1 Mesh Generation

There are many tutorials about OpenGL that use different frameworks. It is very unfriendly to the beginners. The first tutorial I read is using GLUT which is outdated. Later I knew glfw and glad are the most popular frameworks used today. I wasted a lot of time on it. The details of the codes about OpenGL are recorded here, we edit the codes from the tutorial.

In the tutorial, it shows how to draw a rectangle that is formed by 2 triangles. So, drawing a water surface mesh we have to draw many rectangles. And the height of the mesh(Z) is the height of the water surface.
The size of the mesh is 100 and we have 10000 nodes/cells. The range of nodes position is from -5 to 5 in the XY plane. The height is initialized as 0 which we would update them later. Besides, we need two arrays to store the heights of the water surface.

static GLfloat nwater1[SIZE_WATER][SIZE_WATER] = {0.0f};
static GLfloat nwater2[SIZE_WATER][SIZE_WATER] = {0.0f};

An array to store all the information(5 attributes: the XYZ coordinates and UV coordinates). The size of it is 50000.

static GLfloat wdata[SIZE_WATER*SIZE_WATER*5] = {0.0f};

According to that array, we can bind the VBO and VAO. And we would update the data of the arrayvertices.

glBindVertexArray(VAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 5, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

We try to add texture to each rectangle first. The correctness of UV coordinates of the nodes are extremely important, or we would fail to see the result we want. And the XYZ coordinates too. The following steps would be based on this mesh we now define. It could be much more convenient to use the class to encapsulate it. Or we have to compute the location in the array to get the data we want to modify.
Our indices of the mesh start from the left bottom corner: 1 2 … 100. The line above is 101 102 … 200. The rectangle is divided like this. It would be used in shape assemble step.

9900    ...  10000
.              .
.              .
.              .
101-102-103...200
 | / | / |     |
 1 - 2 - 3 ...100
for(int i = 0; i < SIZE_WATER*SIZE_WATER*5; i++)
    {
        int ii = i % 5;
        int xi = (i / 5) % SIZE_WATER;
        int yi = i / (SIZE_WATER*5);
        if(ii == 0)
        {
            wdata[i] = x + xi * det;
            
        }
        else if(ii == 1)
        {
            wdata[i] = y + yi * det;
        }
        else if(ii == 2)
        {
            int x = xi;
            int y = yi;
            if(x == 0 || y==0 || x == SIZE_WATER-1 || y == SIZE_WATER-1)
                continue;
            
            wdata[i] = nwater2[x][y];
        }
        
        else if(ii == 3)
        {
            if(xi%2==0)
                wdata[i] = 0.0f;
            else
                wdata[i] = 1.0f;
        }
        
        else if(ii == 4)
        {
            if(yi%2==0)
                wdata[i] = 0.0f;
            else
                wdata[i] = 1.0f;
        }
    }

The mesh:

mesh01.png

0

DH2323 CG Project
Reference paper:
A New Algorithm for Water Wave Animation
This paper presented a new algorithm of water wave animation and the deriving process is not difficult to understand. So we decide to implement this algorithm presented in this paper.
There are many ways of water wave animation. The simplest is the sin function and some complex are physical models which have more realistic effects but more workloads. It is an old paper that currently the algorithm is mostly used in the mobile app to simulate the raindrop effects.



We decide to implement it by OpenGL, but we have no experience of any OpenGL. We learned it from learnopengl.com.
For this project: Firstly, generate a water surface, then apply this algorithm. Finally, add texture and illumination.

通过 WordPress.com 设计一个这样的站点
从这里开始