Dev Notes: Seamless LODs, pt. 1

In my previous articles, I’ve often mentioned Levels of Detail (LOD). In a nutshell, it means that things that are further away get rendered with a lower amount of detail. Since there is only limited memory and computational power available, it makes sense to put more effort into rendering things that are closer to the camera.

When rendering terrain, the typical approach is to divide the total area into multiple layers or rings that are centered on the camera position. Each layer will be rendered with about the same detail “count”. That means that they all have the same amount of detail relative to their area, but the bigger, outer layers will have a lower absolute amount of detail. These rings are called Levels of Detail.

This allows to render landscapes with far view distances and high local visual fidelity. However, there is the big downside of visual artifacts in the areas where two different levels of detail meet. This isn’t as apparent in still images, but when the camera moves, areas and objects will suddenly switch to a different level of detail. The result is an effect called “LOD popping“.

Humans do, in fact, perceive the world with different levels of detail (things that are farther away appear to be less detailed, thanks to the non-infinite resolution of our eyes). However, there is no discrimination between distinctive levels, which is to say, we perceive each individual bit of visual information with its own level of detail.

To achieve an ideal representation of reality, a terrain LOD model would require each visual fragment to have its own LOD. This is an approach that raytracing-based renderers take, for example Terragen. Current hardware is too slow to do this in realtime (as in, less than the 16ms that are available per frame when rendering with 60fps).

What this series is about

For my current terrain rendering project, I decided to go with a standard low-count LOD model. But in order to hide the transitions between individual LODs, I implemented seamless blending from one LOD to the next. To achieve this, I combined three separate techniques: geometry, normal and texture LOD blending. This (upcoming) series of articles will explain each step of the way.

  • Geometry LOD blending
  • Normal/lighting LOD blending
  • Texture/normalmap LOD blending
  • Lessons learned, how it turned out
  • Getting started: Geometry blending

    This is based on the simple idea that a subdivided (tessellated) rectangle that is perfectly flat looks about the same as a solid rectangle. In order to blend from high-detailed subdivided rectangles to low-detailed undivided rectangles, I’m making the outer subdivided rectangles look like undivided ones by flattening them.

    Then I can blend from completely flat to detailed subdivisions, depending on the distance to the camera, by adjusting the height values of the subdivision points from “looks completely flat” to “looks detailed”.

    First, I start out with nested LOD layers. Each layer is a rectangle. Each layer except for the innermost layer has a hole cut in the middle, in which the next layer sits. Every layer has all of its quads subdivided for now.

    Next, the algorithm decides if each quad of a layer should be subdivided, depending on the camera distance. This isn’t strictly necessary but it saves some gpu cycles. These quads are supposed to look like they aren’t subdivided, so we might as well skip the subdivision.

    You can still tell which quads belong to the outer and which belong to the inner LOD layer because the outer ones are triangulated in a fancy pattern, while the inner ones are all triangulated in the same direction.

    Now for the important step. Take a look at the next graphic to see what’s going on.

    [1] is a quad that wasn’t subdivided. It’s on the outer side of the LOD layer. Or maybe it’s already on the inner side of the outer LOD layer. The point is that it doesn’t matter; they’re supposed to look the same.

    [2] is a quad that has already been subdivided. It’s part of the inner LOD layer. However, it is perfectly flat and it has the same geometrical features as one of the quads that aren’t subdivided. This, too, is supposed to look the same as [1].

    [3] is a quad that is not flat. You can clearly tell that it has a detailed geometry.

    The quad between [2] and [3] has been partly flattened. There’s a slight irregularity in the height values of the subdivision points.

    So in order to blend a quad from detailed to flat, we need two height values for each subdivision point – the detailed height and the flattened height. Then we can blend between the two, depending on the distance to the camera.

    The detailed height value is simply the height value that was calculated for this point. In my current implementation, I’m reading the height values from a texture map.

    The “flattened” height value is the height value the point needs to have to make it look like it was just one of many points on a flat quad – as in, it’s simply the height of the undivided quad at this point. Which means that this height value is the interpolated value between the four corners of the undivided quad. Since I’m using a tessellation shader to compute my subdivisions, this is simply the “Tesscoord.xy mix()” of the four corners.

    Summary of this technique

    Each LOD-level is a mesh of quads. If the quads are close to the outer border of the layer, they won’t be subdivided. Otherwise, they will be subdivided.

    If a quad is subdivided, each of its subdivision points will have a height that is a mix of two height values. One is the “flat” value that is obtained by interpolating the corners of the quad relative to the subdivision coordinate. The other value is the “detailed” value and comes directly from the height function or heightmap.

    If the subdivision point is far away from the camera, the flat value will be used, and the closer it is to the camera, the flat value will seamlessly blend into the detailed value.

    The non-obvious

    This process blends the outer side of a LOD layer into the less detailed inner side of the layer surrounding it. It can be repeated for each individual LOD layer, thus blending the entire range of LODs seamlessly into each other. Each LOD needs its own distance threshold to blend between flat and detailed for this specific LOD.

    You might have noticed that the outer tessellation level of each quad is always the same as the inner tessellation level. Normally, putting a tessellated quad next to an untessellated one would result in nasty gaps in the terrain (see the image below) that are then corrected by using a lower outer tessellation level that matches the outer resolution of the untessellated quad.


    In this case, this isn’t necessary because the tessellated quad is supposed to be flat and look exactly like an untessellated one. If there actually were any terrain gaps showing, this would be a clear indicator that the algorithm didn’t flatten the quad correctly. Therefore, it makes sense not to bother with matching the tessellation edges.

    Next part coming soon: Normal/lighting LOD blending