Part 1: Foliage in AMD FidelityFX Brixelizer GI
This article will describe Brixelizer GI’s functionality to better highlight the problem with foliage. It will also provide a naive, low-quality solution to the problem and compare the result to the default Brixelizer GI.
AMD FidelityFX Brixelizer
To understand why foliage is a problem in Brixelizer GI, we must first understand Brixelizer itself. Brixelizer is a sparse distance field builder and part of the AMD FidelityFX SDK. Brixelizer GI leverages this backend to compute global illumination.
The Brixelizer API is relatively simple. The programmer feeds geometry to the library, which produces a distance field acceleration structure that can be traced using the provided shader trace operations.
Under the hood, the acceleration structure is sparse. This sparse distance field distributes local distance fields in a sparse grid. In Brixelizer, we refer to these local distance fields as Bricks. To trace this structure, Brixelizer first traverses an AABB tree of the scene geometry, and when the ray enters a leaf node, it switches to ray marching incident bricks.
Distance fields provide an effective way of tracing scenes. However, they are implicit in nature in contrast to triangle-mesh parametric surfaces. This difference makes it difficult to reuse the material bindings of the original triangle surfaces, which poses a significant problem for any GI that uses Brixelizer.
Radiance cache
Brixelizer does not provide an API for accessing a surface’s material properties. This means that Brixelizer GI needs to provide its own solution to sample the outgoing radiance at surface points. The chosen solution implements a secondary cache structure called the radiance cache.
The radiance cache stores the outgoing radiance of surfaces in a sparse grid. This grid leverages the Brick ID provided by Brixelizer to store radiance only on surfaces. Whenever a Brixelizer trace hits a surface, the Brick ID of said surface can then be referenced in the radiance cache to access the outgoing radiance.
Of course, it’s not enough to access the cache structure; we must also fill it out. To do this, Brixelizer GI uses a sort of screen-space voxelization. The developer passes the previously lit frame to Brixelizer GI, this frame is successively split into 4×4 tiles. The algorithm chooses a random fragment from each of these 4×4 tiles in every frame. World-space coordinates of these fragments are then reconstructed using the depth buffer and the view-screen projection. Finally, the color value of each fragment is written to the radiance cache.
Probes
Brixelizer GI uses screen-space probes to sample the radiance cache. The screen probes are spawned on the world-space coordinates of screen-space fragments. The probes trace the distance field and access the radiance cache to approximate the incoming radiance. The details here are not especially relevant to this blog post. To find out more, refer to the Brixelizer GI GDC presentation and AMD GI-1.0.
Before we move on to the problem of foliage, let’s examine what the probes see in a scene like Sponza.
In the left image, we can see the distance field constructed by Brixelizer. On the right, we see the same distance field, but with the radiance cache consulted at each hit point. This is, in essence, what the probes will see.
The astute reader might notice that some radiance bleeds onto the pillars from the curtains. Before storing fragments in the cache, we apply a slight jitter to the world-space coordinates. This jitter is necessary because the distance field is generally not precise, meaning the surfaces will not coincide with the original triangle meshes.
Foliage
Now we have a grasp of how Brixelizer GI traces the scene. To illustrate the problem with foliage, let us now look at how Brixelizer GI performs in a scene full of alpha-tested geometry.
In this scene, the grass is over-darkening. To understand why this is happening, let’s look at the scene’s distance field and radiance cache.
As observed, Brixelizer builds the distance field on the entire geometry of the alpha-clipped cards, resulting in an inaccurate distance field representation. The screen-space probes placed on the grass will trace this distance field and erroneously sample the radiance at the transparent parts of the geometry; these parts never have any associated fragments, resulting in the cache stored there being pure darkness. The image below demonstrates a screen-space probe placed on a strand of grass that is unable to escape the enclosing distance field.
Naive solution
A naive solution to the over-darkening issue is avoiding passing alpha-tested geometry into Brixelizer. This solution removes erroneously built cards from the distance field. Screen probes placed on the grass can now trace through any alpha-tested geometry, thus allowing them to sample the sky and opaque parts of the scene.
Unfortunately, this also means that the leaves will no longer be able to provide indirect shadowing for the grass. In this situation, we will instead have a case of over-brightening. Additionally, since no bricks are created around the grass, we experience problems with other cache structures in Brixelizer GI, causing flickering and artifacts.
In Part 2 of this blog series, we will modify the Brixelizer GI algorithm to try to provide a solution that fixes the over-darkening and brightening while still avoiding artifacts.