I missed last weeks post because I was writing a research paper about the graduation work and didn’t get around to updating my blog since. You can read the paper here, I go over a lot of the topics I discussed in previous posts, but also discuss some new things. Aside from that I made the terrain sample a height map in the vertex shader to offset the height of the triangles appropriately. I also made sure that the backface culling doesn’t cull terrain with potential peaks that loom over the horizon. This video shows the results quite nicely:
The more interesting change was added last week:
I decided to work with a modified version of CDLOD for my terrain algorithm. Among the reasons for this decision are:
- It is a modern algorithm designed to leverage the GPUs potential
- The morphing means that no neighbours need to be determined for crack fixing
- Morphing allows for smooth transitions in terrain detail instead of popping
The idea behind CDLOD is that there is a quadtree that is subdivided based on camera distance, and at every leaf a patch is drawn. Every patch is a grid of vertices of a predefined resolution. In the vertex shader vertices are transformed to fit into the leaf of the quadtree and with the height of the terrain. Every other vertex is also morphed to linearly merge with its neighbour vertex if it is close to the border of a lower subdivision level based on the camera distance. This way cracks can be avoided at the edges of subdivision levels.
The advantage of using these patches is that they only need to be sent to the GPU once, which alleviates the CPU from tons of geometry calculations.
Using CDLOD for planet rendering of course has the typical problem of the algorithm being designed for quadratic terrain patches. Under normal circumstances this would mean that the base geometry of the planet needs to consist of squares, like a cube, and thus loose all the advantageous look up tables generated for equilateral triangles in an icosphere. Therefore, I looked into the possibility of achieving the same morphing effect with triangular patchs, and found one:
Here, smaller triangles get twisted into the larger triangles until the vertices are at the same position. A small complication is that neighbouring triangles need to twist into the opposite direction, in other words every second triangle twists counter clockwise.
In my implementation every patch holds the vertex positions and morph vectors in a normalised two dimensional form (PatchVertex: vec2 pos; vec2 morph;). These two dimensional coordinates can be treated like barycentric coordinates. Every patch is a leaf node of the triangle tree, where a triangle holds the position A, the vectors R and S and an integer with its subdivision level for morphing. R and S can be constructed from a triangle ABC, where R = B-A and S = C-A.
When transforming a vertex from the patch data using such a triangle its position can be determined as follows:
vec3 triPos = A + R*pos.x + S*pos.y;
Using the distance lookup table described in an earlier post, the camera position and the subdivision level of the patch, a morphing factor can be determined. Once that is the case the position can be modified again:
triPos += R*morph.x + S*morph.y;
In order to make sure the winding is inverted every other triangle the R and S vector can be swapped during the triangle tree calculation on the CPU.
Patches are drawn instanced in order to avoid hundreds of drawcalls per terrain, so a vertex buffer object is created to hold the triangle tree information and updated every frame (PatchInstanc: int level; vec3 A; vec3 R; vec3 S;).
The following video shows how the effect looks like quite nicely:
Using this implementation allowed me to achieve solid framerates at ridiculously high vertex counts, in some cases 120 fps at nearly 3 million vertices, which is not a resolution needed by a long shot to achieve convincing terrain:
I have not profiled the performance in depth yet, but I seem to be getting very decent results at less than 250k triangles:
Note that the performance is usually better than the screenshots indicate because the terrain is drawn twice here, solid and wireframe on top.
One problem is that the subdivision always assumes that triangles are at sea level, which can cause problems with distributions on taller mountains, which is a problem I am currently looking into.
Apart from this, I started researching atmospheric scattering now in order to make sure I get all required topics for my graduation work together in a timely manner, and I can always come back to the terrain LOD to refine it later.
As usual, you can find the full implementation on the GitHub repository.