# Infinity Terrain in C++ using Perlin Noise and OpenGL

Tuesday, January 7th 2020, 2:50:54 am I just finished another semester of grad school and this is the first of hopefully a few blog posts where I share some of the assignments I did this semester in my classes. This one is from a Computer Graphics course.

### 1.0 - Introduction

When I started this project I set out to make a simple interactive graphics program (I’m heistant to call it a game because of its simplicity) using OpenGL that renders an infinite mesh of procedurally generated terrain using: Perlin noise

This mesh would represent a terrain stretching out in all directions that a player avatar could move on:

It would have certain regions that would be water and somehow impenetrable (the player character does not know how to swim perhaps.)

From this concept, I set out to write a simple program without any frameworks and in as vanilla of C++ code as possible (though it is based on a fork of some boilerplate code provided by one of my professors that you can find here.)

### 1.1 - Terrain Mesh Generation

First I generated my basic geometry.

A single, larget terrain mesh is generated as a series of NxM vertices in the `x` and `z` directions in world space with the `y` component generated as a call to a perlin noise function `noise_callback()`.

``````void Mesh::generateVertexes(unsigned int w, unsigned int h) {
// Create vertices
vertices.reserve(w * h);
float spacing = 1.0;
// Rows
for (int r = 0; r < h; r++) {
// Cols
for (int c = 0; c < w; c++) {
// NOTE: origin is not at center of mesh
float x = c; // col
float z = r; // row
float y =
noise_callback( // perlin or other noise func
(float)c / w, // x
(float)r / h // y
);
glm::vec3 v(x * spacing, y * spacing, z); //
vertices.emplace_back(v);

// Vertex colors not supported!
vertex_colors.push_back(glm::vec3(0.0, 0.0, 0.0));

// Create a vector in the mapping
vector<int> triangles;
vertex_to_triangles_map.emplace(vertices.size() - 1, triangles);
}
}

// Generate faces

// Rows (-1 for last)
for (int r = 0; r < h - 1; r++) {
// Cols (-1 for last)
for (int c = 0; c < w - 1; c++) {
// Upper triangle
/*

v0 -- v2
|    /
|  /
v1
*/
int f0_0 = (r * w) + c;
int f0_1 = ((r + 1) * w) + c;
int f0_2 = (r * w) + c + 1;
glm::vec3 f0(f0_0, f0_1, f0_2);
faces.push_back(f0); // TODO: emplace

// Lower triangle
/*

v2
/ |
/   |
v0 --- v1
*/
int f1_0 = ((r + 1) * w) + c;
;
int f1_1 = ((r + 1) * w) + c + 1;
int f1_2 = (r * w) + c + 1;

glm::vec3 f1(f1_0, f1_1, f1_2);
faces.push_back(f1); // TODO: emplace
}
}

// Post-processing code
}``````

### 1.2 - Terrain Mesh Instancing & Mirroring

The generated terrain mesh is then used to create 9 instances of a `SceneObject` representing the entire surface with a single set of vertexes and reducing the need to transfer geometry back and forth from GPU. Each instance would have different orientation according to their relative position in the grid as follows:

`(x,y)` -1 0 1
-1 `MIRROR_3D_XY` `MIRROR_3D_Y` `MIRROR_3D_XY`
0 `MIRROR_3D_X` `MIRROR_NONE` `MIRROR_3D_X`
1 `MIRROR_3D_XY` `MIRROR_3D_Y` `MIRROR_3D_XY`

This mirroring can be generalized across the entire grid of the world coordinates using the following algorithm (pseudocode)

``````
float get_mirroring(int x, int y):
# Set mirroring
# Case 1: both are odd
if x % 2 != 0 and y % 2 != 0:
return MIRROR_3D_XY
# Case 2: y is odd, x is even
elif y % 2 != 0 and x % 2 == 0:
return MIRROR_3D_Y
# Case 3: x is odd, y is even
elif x % 2 != 0 and y % 2 == 0:
return MIRROR_3D_X
# Default Case: no mirroring, both x & y are odd
else:
return MIRROR_3D_NONE``````

Using a simple asynchrnous update coroutine in the player movement function, we can keep the terrain self-updating and mirroring as the player moves. That only requires translation of mesh tiles that are more than 2 tiles away and leaves current and adjacent tiles untouched.

In context:

``````void updateTerrain() {
if (!terrain_update_mutex.try_lock()) {
return;
}

for (int i = 0; i < terrain_objects.size(); i++) {
SceneObject *so = scene_objects.at(terrain_objects[i]);
glm::vec2 so_loc = so->getWorldGridPos(XMAX - 1, YMAX - 1);
glm::vec2 p_loc = player->getWorldGridPos(XMAX - 1, YMAX - 1);
glm::vec2 dir = p_loc - so_loc;
float x_dist = dir.x;
float y_dist = dir.y;
glm::vec3 t(0.0f, 0.0f, 0.0f);

if (abs(x_dist) >= 2.0) {
float x = (glm::sign(x_dist) * 3.0f) * (XMAX - 1);
t.x = x;
}

if (abs(y_dist) >= 2.0) {
float y = (glm::sign(y_dist) * 3.0f) * (YMAX - 1);
t.z = y;
}
if (abs(x_dist) > 0.001 || abs(y_dist) > 0.001) {
so->translate(t);
}

glm::vec2 so_next_loc = so->getWorldGridPos(XMAX - 1, YMAX - 1);

/*

Mirroring is applied to whichever the odd-numbered
grid coordinate is.

*/

// Case 0: No mirroring
glm::mat4 mirroring_mat = MIRROR_3D_NONE; // No mirroring by default

// Set mirroring
int so_x = (int)so_next_loc.x;
int so_y = (int)so_next_loc.y;
// Case 1: both are odd
if (so_x % 2 != 0 && so_y % 2 != 0) {
mirroring_mat = MIRROR_3D_XY;
// Case 2: y is odd, x is even
} else if (so_y % 2 != 0 && so_x % 2 == 0) {
mirroring_mat = MIRROR_3D_Y;
// Case 3: x is odd, y is even
} else if (so_x % 2 != 0 && so_y % 2 == 0) {
mirroring_mat = MIRROR_3D_X;
}

so->setMirroring(mirroring_mat);
}

terrain_update_mutex.unlock();
}``````

This results in a seamless transitioning of tiles as the player moves:  ### 2.1 - OBJ File Parsing

Using guidance from an OpenGL Tutorial I added a basic OBJ file parser.

Although I did not add full `.mtl` file support, I did add support for a custom directive called `usergb`. This directive allows me to define RGB colors for groups of faces as follows:

``````f 1780/10026/3342 1781/10025/3342 1773/10024/3342
f 1779/10029/3343 1780/10028/3343 1773/10027/3343
f 1778/10032/3344 1779/10031/3344 1773/10030/3344
f 1777/10035/3345 1778/10034/3345 1773/10033/3345
f 1776/10038/3346 1777/10037/3346 1773/10036/3346
f 1775/10041/3347 1776/10040/3347 1773/10039/3347
f 1774/10044/3348 1775/10043/3348 1773/10042/3348
usergb 1.00 0.92 0.23        # <---- updates rgb of all subsequent faces
f 292/1593/531 291/1592/531 290/1591/531
f 293/1596/532 292/1595/532 290/1594/532
f 290/1599/533 295/1598/533 294/1597/533
f 293/1602/534 290/1601/534 294/1600/534
f 291/1605/535 296/1604/535 295/1603/535
f 290/1608/536 291/1607/536 295/1606/536
f 292/1611/537 297/1610/537 296/1609/53``````

Using this parser and the custom directive, I modified all calls to the .mtl material definitions with my custom `usergb` directives and I parsed the following 3D model of a Gundam RX78 Robot from `poly.google.com`: ### 2.2 - OBJ Face Coloring

I then added a VBO object that stored the color values from the OBJ file and made the accessible in the shader and weighted based on the `vertexColorBlendAmount` instance variable on `SceneObjects`.

``````#version 150 core
in vec3 vertexColor;
uniform vec3 ModelColor;
uniform float vertexColorBlendAmount;

out vec3 v_color;
out int;

void main() {
// Other code...

// This is how colors are computed
v_color = ModelColor + (vertexColor * vertexColorBlendAmount);

// Other code ...
}``````

### 2.3 - The Player `SceneObject`

Using the Gundam model, I instantiate a `SceneObject` for the player and position them in the world at the start of the program execution:

``````createModelInstance(0);       // Add robot
SceneObject *player = scene_objects.at(9); // Store ref to robot
player->setColor(8);
player->rotation_axis_idx = 1;
player->rotate(0.75f);
player->vertex_color_blend_amount = 1.0f;

// Move player to middle of center tile at start of game
translateSelectedModelInstance(glm::vec3(
XMAX / 2, player->mesh->mesh_radius * player->scale.y, YMAX / 2));``````

### 2.4 - Player Camera Movements

The camera is designed to follow the player and assumes the current player position is its origin. This gives the controls the standard WASD + Mouse camera controls

#### Keyboard Only #### Keyboard + Mouse #### 2.5 - Collisions

Strictly speaking, there is no collision handling. However, the `noise(x, y)` function that was used to generate the mesh, is accessible to the player movement function and the player’s `y` value factors in the terrain elevation at position `x,y` in grid space (or really `x,z` in world space) and considers that a lower bound on the player’s elevation. This allows the player to “collide” with the mountains and water.

### 3.1 - Pixelation Camera Filter FX

I added a secondary set of shaders to support filtering FX as well. These work by conditionally rendering to a texture instead of the primary frame buffer and then using two simple co-shaders to sample from the texture in the secondary shader program, which renders simply a screen-sized quad polygon:

``````#version 330 core

// Input vertex data, different for all executions of this shader.
layout(location = 0) in vec3 vertexPosition_modelspace;

uniform vec2 iResolution;

// Output data ; will be interpolated for each fragment.
out vec2 UV;
out vec2 res;

void main() {
vec3 vIN = vertexPosition_modelspace.xyz;
gl_Position = vec4(vIN, 1);

UV = (vertexPosition_modelspace.xy + vec2(1, 1)) / 2.0;

res = iResolution.xy;
}``````

``````#version 330 core

in vec2 UV;
in vec2 res;

layout(location = 0) out vec3 color;

uniform sampler2D renderedTexture;
uniform float time;
uniform float pixelWidth;

void main() {
float Pixels = pixelWidth;
float Intensity = 5.0;
float dx = Intensity * (1.0 / Pixels);
float dy = Intensity * (1.0 / Pixels);
vec2 uv = vec2(dx * floor(UV.x / dx),  // derive x
dy * floor(UV.y / dy)); // derive y
vec4 f_color = texture(renderedTexture, uv);

color = f_color.xyz;
}`````` ### 3.2 - Dynamic Sky Coloring + Pixelation Shader

Finally, the sky color can be changed using the `-` and `=` keys from black to blue to white: This can also be controlled in tandem with the pixelation shader.

### Code, etc

You can check out the full source code of the project on my Github at:

omardelarosa/infinity-terrain-3d