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.
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:
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:
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.)
So 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:
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...
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.
Here’s how each vertex needs to move in order to match the next ring:
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:
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!