Assuming that you have read Working with noise

Here is a WebGL experiment showing what’s taking place in Sketch.h and Sketch.cpp from the Desert project’s source folder.

This example is the most advanced so far, as it optimizes the usage of Perlin noise as well as the rendering of the terrain.

Sketch.h

We can see 2 new classes: NoiseSurface and NoisePatch.

NoiseSurface

Have a look at NoiseSurface.h and NoiseSurface.cpp. This class is holding a Noise object internally and provides a getHeight() method to return a value at some x,y position. But instead of using the Noise object to produce this value, it is using a precomputed table and operates bilinear interpolation on it.

The advantage of this method is that it is faster than working with Noise directly. The disadvantage is that it requires to define a finite table of values. So the table is providing perfect tiling but the values are repeating themselves.

NoisePatch

A NoisePatch is a rectangular area which uses a NoiseSurface to provide values for generating geometry in an optimized fashion: instead of operating on batches, we operate on their underlying buffers directly.

Let’s examine NoisePatch.h. We can see two batches: gridBatch and fillBatch. The first will be used to draw the (optional) horizontal and vertical lines and the second will be used to draw the terrain mesh. But the truly interesting part is:

chr::gl::Buffer<chr::gl::Vertex<chr::gl::XYZ>> vertexBuffer;
chr::gl::Buffer<GLuint> gridIndexBuffer;
chr::gl::Buffer<GLuint> fillIndexBuffer;

3 buffer are defined. One for the vertices, one for the grid indices and one for the fill indices. An IndexedVertexBatch is made of 2 internal buffers: one for the vertices and one for the indices. In this particular case, vertexBuffer will be shared by gridBatch and fillBatch.

Now have a look at NoisePatch.cpp. Let’s start with the constructor:

NoisePatch::NoisePatch(NoiseSurface *surface)
:
surface(surface),
gridBatch(GL_LINES, vertexBuffer, gridIndexBuffer),
fillBatch(GL_TRIANGLES, vertexBuffer, fillIndexBuffer)
{}

We create gridBatch and fillBatch by passing a primitive type, a vertex buffer and an index buffer.

Now let’s look a the method that fills the vertices:

void NoisePatch::update(float height, const glm::vec2 &offset)
{
  auto &vertices = vertexBuffer->storage;
  vertices.clear();
  vertices.reserve(nx * ny);
  vertexBuffer.requestUpload();

  for (int iy = 0; iy < ny; iy++)
  {
    float y = (oy1 + iy) * surface->gridSize;
    for (int ix = 0; ix < nx; ix++)
    {
      float x = (ox1 + ix) * surface->gridSize;
      vertices.emplace_back(x, y, height * surface->getHeight(x + offset.x, y + offset.y));
    }
  }
}

First, we access the buffer’s internal storage, which is a vector<Vertex<XYZ>>. Then we clear it, and request that its capacity corresponds to the number of vertices that are going to be added. Then, we call requestUpload() which signifies that the buffer will have to be uploaded to the GPU during the next batch flush.

Then comes a loop, which fills the vertices x,y,z values using emplace_back(). There is no faster way of working with vertices than what’s described here.

Sketch.cpp

Sketch::Sketch()
:
fogColorShader(InputSource::resource("FogColorShader.vert"), InputSource::resource("FogColorShader.frag")),
surface(800, 4, 0.01f, 3, 123456789),
patch(&surface)
{}

In the sketch’s constuctor, we initialize our custom shader, then we create our NoiseSurface with a width of 800, a grid-size of 4, some noise-scale of 0.01, 3 for the noise octaves and some seed number. Then, we create our NoisePatch by assigning it a pointer to the surface.

Inside setup()

surface.generateBase();
patch.setFrontFace(CCW);

We precompute the internal table discussed previously. It will have a size of 200x200 (we take the width, divide it by the grid-size, and use it for both dimensions.)

Then, we define that the mesh in our patch will be drawn in CCW mode.

Inside update()

We increment indefinitely the viewOX variable, which controls horizontal scrolling.

Inside draw()

CinderCamera camera;
camera.setEyePoint(glm::vec3(-1.1223f, -185.783f, 48.6359f));
camera.setOrientation(glm::quat(0.7915398f, 0.6111088f, -0.003264602f, -5.330436E-4f));
camera.setPerspective(60, windowInfo.aspectRatio(), 1, 2000);
camera.calcMatrices();

This experiment has been previously coded with Cinder, so in order to keep the original camera properties, there’s a need to use a CinderCamera with some hard-coded values.

Matrix viewMatrix = camera.getViewMatrix();
viewMatrix.translate(-viewOX, -viewOY, -(surface.noiseMin + (surface.noiseMax - surface.noiseMin) * 0.5f) * TERRAIN_H);

The camera we use has the effect of trading the y axis for the z axis, and vice-versa. So the translation taking place here is:

  • scrolling horizontally according to viewOX.
  • not doing anything regarding depth (viewOY is equal to 0.)
  • shifting vertically, based on the height of the terrain combined with the surface’s minimum and maximum values.
State()
  .setShader(fogColorShader)
  .setShaderMatrix<MVP>(viewMatrix * camera.getProjectionMatrix())
  .setShaderUniform("u_fogDensity", 0.005f)
  .setShaderUniform("u_fogColor", COLOR_BACK)
  .apply();

Here, we define that our custom shader will be used. It’s a shader that simulates fog in EXP2 mode. It’s a cheap way for us to add depth to the scene. Then, we pass 2 parameters to the shader: u_fogDensity and u_fogColor.

Then we take care of the patch:

patch.setup(viewOX - BORDER_HM, viewOY - BORDER_VM, BORDER_HM + BORDER_HP, BORDER_VM + BORDER_VP, NoisePatch::MODE_FILL | NoisePatch::MODE_GRID_H);
patch.update(TERRAIN_H, glm::vec2(0));

First, we call setup() with the center position and the size of our patch. We also state that we want to fill the mesh and draw only the horizontal lines of the grid. This method is filling the internal buffers with the proper vertex indices.

Then we call update() with the terrain height and a null offset. This method is filling the internal buffers with the proper vertices.

Finally, we proceed to rendering:

glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(4.0f, 1.0f);

patch.drawFill(COLOR_SOIL);

glDisable(GL_POLYGON_OFFSET_FILL);
glDepthMask(GL_FALSE);

patch.drawGrid(COLOR_SOIL_WIRE);

First, we would like to render our fill. But since we need to render the grid lines on top of it, we must use some polygon offset, otherwise we will suffer from z-fighting. Then, in order to render the grid lines properly: we cancel the polygon offset and we set the depth mask to false.