Sponsored By

WebGL Terrain Rendering in Trigger Rally - Part 2

Rendering large, detailed terrains efficiently is an Interesting Problem in computer graphics and games. Doing it with WebGL makes it even more interesting.

Jasmine Kent, Blogger

September 8, 2013

4 Min Read
Game Developer logo in a gray background | Game Developer

Welcome to this series of posts about WebGL Terrain Rendering in Trigger Rally!

If you haven't yet, you should read Part 1 where I talk about the importance of minimizing CPU-GPU data transfer, and introduce the idea of combining static vertex buffers with height data stored in textures.

In this post, I'll discuss the vertex data format and morphing.

Trigger Rally terrain screenshot 

Rings

Geoclipmap rendering uses a set of square “rings” around the viewpoint, where each ring is twice the size of the previous one, and so has half the spatial resolution. This results in approximately consistent screen space resolution of the terrain at all distances. The innermost (highest resolution) ring has its center filled in, becoming a simple square grid of triangles:

 Geoclipmapping rings

Geometry that repeats itself in a grid pattern has a nice property: we can translate it by exact multiples of the grid size without any visible change to the user, except that the edges appear to have moved:

Translating a ring

We can use this property to move the geometry around, keeping it approximately centered under the camera, but without it being obvious that this movement is occurring.

Each ring has its own grid size, and since the translation distance depends on the geometry size, we will need to move the rings independently of each other. Thus the vertex shader needs to know which layer a vertex belongs to, both for translation and so that it can morph it correctly (we'll come back to morphing in a minute.)

Raw vertex dataSo the vertex attributes we need are:

  • Position X

  • Position Y

  • Layer index 

In Trigger Rally's implementation, we use an [X,Y,Z] 3-vector and encode the layer index as Z, so that in our raw geometry the rings appear to be stacked.

Filling in the gaps

Each ring is drawn at a different scale, and they are also translated by multiples of this scale. So there is a problem: when one ring is translated but its neighbor is not, a gap will appear:

Gaps when rings are translated

One way of fixing this is to extend the edge of the ring with extra geometry, known as a skirt. In the geoclipmapping approached described in this paper, the skirt is carefully assembled from many smaller pieces, using multiple small vertex buffers and careful CPU logic. We don’t want that!

When implementing the terrain in Trigger Rally, I spent hours trying to find a clever way to design the skirt to be both seamless and entirely static, to no avail.

But then I met Florian Bösch at last year’s WebGL Camp Europe, and he suggested just making the rings bigger and letting them overlap.

Now, seasoned graphics programmers will probably be gasping “No! You can’t overlap geometry! It’s wasteful and you’ll get horrible depth fighting artifacts!” But other than a tiny bit of overdraw, it actually turns out to be an excellent solution provided that the geometry matches up exactly. Which brings us to...

Morphing

At the boundary between rings we have geometry at one resolution next to geometry at half that resolution. We need to introduce transition regions at the edge of each ring, where the geometry gradually moves or “morphs” from high resolution to low, so that by the time you reach the edge of the ring it will match up perfectly with the next ring beyond.

Geoclipmapping ring transition regions

Here’s how each vertex needs to move in order to match the next ring:

Vertex movement diagram

We need to perform this translation in the vertex shader. The simplest approach would be to include the morph direction vector as part of the vertex data format, but again Florian had a better suggestion: use modular arithmetic!

To show how this works, let’s tabulate the data:

Vertex coordinate    

0

 1

4

MOD 2

0

 1

0

1

0

MOD 4

0

 1

2

3

0

Morph vector

0

-1 

0

1

0


So we can compute the morph vector from the vertex position with this GLSL code:

vec2 morphVector = mod(position.xy, 2.0) * (mod(position.xy, 4.0) - 2.0);

No extra vertex attributes needed!

Tune in next time

In the next post, I’ll talk about how multi-resolution height data is stored in Trigger Rally, and how it's processed in the vertex shader. After that we’ll look at surface shading in the fragment shader, and how to render scenery meshes efficiently.

Thanks for reading!

@jareiko

Continue to Part 3...

Read more about:

Featured Blogs
Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like