We recently added an option in Cesium for Unreal for smoother transitions between levels of detail with dithered opacity masking. Enabling this option provides a more seamless viewing experience for end users with minimal rendering overhead.
3D Tiles is an OGC Community Standard that enables seamless streaming and rendering of large datasets while minimizing the network and system costs of doing so. The 3D Tiles hierarchical level-of-detail (HLOD) helps achieve this by enabling different levels-of-detail to be used for different parts of the scene at the same time. Tiles further away from the camera can be streamed and rendered at a lower level-of-detail (LOD) than tiles up close to the camera which are rendered at higher detail. Taken together, these tiles of varying LODs stitch together to cover the entire scene.
This HLOD approach necessitates that tiles often transition between higher and lower levels of detail as users move around the scene. Switching between tiles immediately allows users to see the target detail level as soon as it is loaded and ready to render, but it can feel quite frantic and jarring since the LODs switch so suddenly. This effect is known as “LOD popping.”
In most cases, LOD popping can be greatly minimized by making tile transitions slightly longer and more gradual.
To address LOD popping in Cesium for Unreal, we have recently added the Use LOD Transitions option on the Cesium3DTileset actor. Our approach makes a careful tradeoff between LOD popping, tile load latency, and rendering cost. The goal is to ensure that the dithered transition length does not cause notable latency, but still has enough transition overlap to hide the LOD switching. An additional factor taken into account is to avoid too many tiles fading in or out simultaneously as that could lead to a spike in draw calls during camera movements.
Using a translucency-based effect to blend LODs may be tempting, but it comes with several performance drawbacks. In Unreal Engine, translucent objects are rendered in a separate forward-only render pass after the opaque parts of the scene are drawn with the deferred rendering pipeline. The translucency render pass disables depth testing and composites translucent triangles back to front—this requires that translucent triangles are sorted by distance in each frame. The lack of depth testing causes significant overdraw, and the triangle sorting can get quite computationally expensive. Translucent objects are also not written to the depth buffer, which means they cannot be used to occlude other objects during occlusion culling.
It turns out that transparent rendering is complicated because of partial transparency. If a material has opacity values that are always 0 or 1 (fully transparent or fully opaque respectively), rendering the material becomes much easier. In Unreal Engine, this concept is called “opacity masking.” In opacity masked materials, the opacity value is determined in a fragment shader, usually by sampling from an opacity mask texture. This shader code runs during the depth pass in addition to the base opaque pass. This means that apart from the potential extra texture samples during both passes, opacity masked objects get rendered in the exact same way as opaque objects. They can even act as occluders since they get drawn into the depth buffer.
Opacity masking is a flexible and performant technique that is already used in another part of Cesium for Unreal—we use opacity masks to clip 2.5D regions out of tilesets.
Dithering is a concept borrowed from digital signal processing—it refers to the process of adding noise to a digital signal in order to make it appear analog. In Unreal Engine, dithering refers to a screen-space sampling of white noise that is thresholded at a provided grayscale value per-fragment. The threshold value can be ramped up or down over time to get the dithered effect to dissolve into or out of the screen.
The dither value can be used as an opacity mask to roughly mimic partial translucency for a fraction of the cost. When combined with anti-aliasing, dithered opacity masking becomes a rough monte-carlo approximation of translucency. In Cesium for Unreal, the dithered opacity mask threshold is ramped up throughout the LOD transition to fade from the old tile to the new tile.
As the name suggests, the “Dither Temporal AA” material node works best with temporal anti-aliasing (TAA). If TAA is enabled, the sampled noise changes every frame so that the temporal accumulation ends up blending the foreground and background together over several frames to very closely mimic translucency.
As briefly mentioned already, the LOD fading is implemented by changing the dither threshold over time. Both tiles use the same threshold value and as a result get the same screen-space dither value. The fading-in tile however, inverts the dither value before plugging it into the opacity mask. This allows both fading tiles to stitch together water-tight during the transition.
Dithered opacity masking is significantly more performant than the translucency approach. In comparison to translucency, dithered opacity masking has essentially no overdraw, no depth sorting cost, no extra lighting cost, and no negative effect on occlusion culling. However, the implicit cost of LOD blending remains—both LODs have to be drawn at the same time during the transition period. Transitions also have to be animated with a uniform update every frame. This means that too many tile LOD transitions happening at the same time will lead to a temporary spike in both draw calls and uniform updates. We chose 0.5 seconds as the default transition length to balance transition smoothness, apparent latency, and GPU spikes. Longer transition lengths will lead to bigger spikes and increased latency.
These improvements and others are available as of Cesium for Unreal v1.17.0, available now on Unreal Marketplace and on GitHub. If you already have Cesium for Unreal, update the plugin version from the Epic Games Launcher.
Try out dithered LOD transitions by enabling Use LOD Transitions on any Cesium3DTileset actor. If your tileset uses a custom material, you will have to add the ML_DitherFade material layer to your material.
If you are new to Cesium for Unreal, this is a great time to try it out—check out our Quickstart Tutorial to get started!