This is the final project of CIS 560 Interactive Computer Graphics.

In this project I implemented the following features:

  • Effecient Chunking Rendering – Milestone 1
  • Multi-thread Terrain Generation – Milestone 2
  • Shadow Mapping – Milestone 3
  • Distance Fog – Milestone 3
  • Fluid “Simulation” – Milestone 3
  • Water Waves – Milestone 3

Efficient Terrain Rendering and Chunking

Efficient chunk rendering

Interleaved vertex data

To better store and use the vertex attributes, I wrote a struct “BlockFace” which has the direction, position, normal and color data of a face in a block. Since every block’s vertex positions and normals are the same with respect to the block, I hard coded the vertex position and normal data for each face. For the texture/color for milestone 1, I wrote a function “type2Color” to convert the string enum texture type to glm::vec4 colors.

To create the chunk-based VBO data, we have to determine which face of a non-EMPTY block should be rendered. I created an array of BlockFace type to store the 6 faces’ vertex data of a block. When checking each block’s faces to see if it is neighboring to an EMPTY block, I add the offset of this block and the origin of this chunk to the face vertex position data to get its position in this chunk.

Create chunk VBO data

void Chunk::createVBOdata()

After the first implementation of the function, I have an error of array out of range. This is because the neighbor block of blocks at the boundary is not initialized so we cannot get its blocktype. I added a boolean function to check if a block is at the boundary of this chunk and manually set its neighbor blocktype to EMPTY.

In drawable.h, I added the following member variables/functions:

GLuint m_bufAll;
bool m_allGenerated;
void generateAll();
bool bindAll();

Modify draw function in ShaderProgram

void ShaderProgram::drawChunk(Drawable &d)

The stride parameter of glVertexAttribPointer() function is relevant to the type of vertex attributes in the interleaved data type. I set the attributes of them to glm::vec4 so the stride of them are sizeof(glm::vec4) * 4.

Modify drawChunks() Function of Terrain

void Terrain::drawChunks(int minX, int maxX, int minZ, int maxZ, ShaderProgram *shaderProgram)

After my version merged with the Procedural terrain generation branch, the render speed is very slow. I add a new flag in Chunk class to track if its VBO data has been created before pass it to the shader so the program does not need to create every chunk’s VBO data in the terrains everytime paintGL is called.

Add New Terrain Based on Player’s Position

void Terrain::checkAddNewChunks(glm::vec3 pos, Player* player)

Add a function to check the player’s position in order to render the next terrain on screen. I wrote 4 helper functions to check if player is near the boundary of current chunk:

bool isNearNorth(int threshold, int x, int z); // north -> zneg
bool isNearSouth(int threshold, int x, int z); // south -> zpos
bool isNearEast(int threshold, int x, int z); // east -> xpos
bool isNearWest(int threshold, int x, int z); // west -> xneg

And four other helper functions to check if the current chunk’s neighbor in four directions are EMPTY:

bool hasNorthNeighbor();
bool hasSouthNeighbor();
bool hasEastNeighbor();
bool hasWestNeighbor();

In this function, I use the floor() to get the origin of the current terrain. Origin in four directions are different from each other.

Before merging this branch with procedural terrain generation, I wrote a function in Terrain class to generate a floor for testing this feature.

Multi-threading

Multi-thread workers

There are two types of workers to do the multi-threading tasks:

  • BlockType worker:to generate the Block type data for chunks and store them in a shared memory in main thread
  • VBO worker: to compute the VBO data for each chunk and store them in a shared memory in main thread

Terrain expand

In every tick in MyGL, call the terrain expand function to check the need to create or destroy terrain zones based on the player’s movement. In our project, the radius for creating BlockType data for terrain zones is 2 and for creating VBO data is 1.

I check the player’s movement in four directions separately. If the player reaches another terrain zone, the workers will be called to handle the generation of the terrain. After the terrain expand function, the VBO worker managment function will spawn the VBO workers to compute the chunks’ VBO data and bind the VBO data to buffers.

Bugs and fixes

Segment fault in asynchronous threads: When creating VBO data for a chunk in VBO workers, the main thread runs very slowly. It is because the chunk VBO create function is inside the critical section of a mutex. I added a flag isCreated to Chunk class as a member variable. If the chunk’s VBO data has been computed, it will be set to true.

Shadow Mapping

Set up Depth/Shadow Map

Create a new shader to render the whole scene from the perspective of the light direction and store the nearest position to the light as a texture. The texture is stored in one of the OpenGL texture slots and will be passed to shaders that actually render the terrain.

The information shadow map shader needs is basically the transformation matrix from world space to light space. But for the correct lighting of the terrain we also need to pass more light-related info to the shaders later. These are the lighting info stored in MyGL:

// -------------- lighting info --------------- //
    glm::vec3 lightPos = glm::vec3(-10.f, 200.f, -10.f);
    glm::mat4 lightSpaceTrans; // proj * view mat in light space
    glm::vec3 lightDir;
    int DayStatus = 1;

To render the shadow map, I wrote the following functions in MyGL:

// ------------ shadow mapping functions ------------------ //
    void renderDepthTex(); // render depth of scene to texture from light's persp
    void renderDepthQuad(); // For testing: render depth map to the quad to show on screen
    void computeLightInfo(); // compute transform in light space
    void configureDepthMapFBO(); // set up frame buffer for the depth map
    void bindShadowMap(); // bind the shadow map texture to the shader

The pipeline can be briefly described as:

...
bindFrameBuffer()
renderShadowMap();
bindFrameBuffer();
bindShadowMap();
renderterrain();
postProcess();
...

Improvements

Due to the difference between resolution of the shadow map and the final rendered scene, there will be shadow acne on the ground. Use a bias to shift the surface beneath the original ground se the acne will not be seen. To reduce the floating shadow effect, I used the front-face culling when rendering the shadow map.

Combine with Day-light Cycle

After merging with the day-night cycle, the original method of using the light position to render shadow map is not working because in the new lighting computation only uses lighDir. To fix this, I moved the calculation(rotation) of light direction from GPU shaders to CPU, and pass the lighting infos I listed above to GPU for color computing.

Distance Fog

The idea of distance fog is to interpolate color of the actual texture color with the fog color based on the distance to camera/player. To compute the distance, I passed the view matrix to the vertex shader and multiply with world pos so the z component of the result is the distance of this verex to the camera.

// ----- Compute Fog ------ //
    float dist = 0;
    float fogFactor = 0;
    dist = abs(fs_viewPos.z);
    fogFactor = 1.0 /exp((dist * FogDensity));
    fogFactor = clamp(fogFactor * 1.35, 0.0, 1.0 );

    vec4 mixColor = mix(fogColor, blinPhonCol, fogFactor);

Fluid “Simulation”

Move with flow

First set the flow direction. In this project, the direction is positive X direction of the world. In every tick, determine whether the player is in “WATER” blocktype. If the player is in water and not on fly mode, compare the x component of the player’s velocity and the water velocity and set the player’s velocity.x to the one that has the larger magnitude.

Water Fill

When creating VBO data for a chunk, check the current block if it is water. For each water block, check if its neighbours in side directions are EMPTY, and set them to WATER.

Water Waves

Used sin functions to distort vertex position in vertex shader and corresponding normals in fragment shader. In previous implementations, the water has a label “is_animated” passed to GPU. For this feature, if the “is_animated” is labeled, the use the sin function to distort position and normal. The wave function is relative to time and x coordinate.

// vertex shader
// distort water surface
    if(is_animated == 1) {
        pos.y +=  0.1 * sin(u_Time * 0.01 + 30 * pos.x) - 0.1;
    }
// fragment shader
// wave effect
        normal.y +=  0.2 * sin(u_Time * 0.01 + 10 * fs_Pos.x) - 0.2;

Leave a Reply

Your email address will not be published. Required fields are marked *

%d bloggers like this: