Cards in the Lumen

Why Cards

As discussed in the previous chapter, the calculation process for ray tracing under normal circumstances is as follows:

image

We hope to cache the last step to save computation time.

image

Of course, there are many forms of caching, and Lumen has chosen Card as the form of caching.

Data Structure

ℹ️
\Engine\Source\Runtime\Renderer\Private\Lumen\LumenSceneData.h

This image shows the relationship between key data structures. FLumenMeshCards and FLumenCard are the most central data structures.

On the left, it shows how these two are related to the data structure we previously discussed, such as FPrimitiveSceneProxy.

On the right, it shows the data structure that serves as the data hub for the entire LumenScene: FLumenSceneData, and the relationship between this data structure and FLumenMeshCards and FLumenCard.

image

OBB

Please note that the representation of FLumenCard is a rotated bounding box (OBB). The card is not positioned in the world space in the form of AABB. Please note that in the following image, as the cube rotates, the card also rotates simultaneously.

An example of the card OBB
An example of the card OBB

Struct-of-Array

FLumenCard is stored continuously in LumenSceneData in SOA style.

image

On the other hand, FLumenMeshCardssimply holds the index pointing to the array and the number of Cards.

ℹ️
Please note that not all Meshes have exactly six Cards. In our case, the plane used as the floor only generates one Card because it is very thin.

In addition, FLumenMeshCards itself is also stored continuously in LumenSceneData.

In order to easily query the information of belonging to FLumenMeshCards in reverse, FLumenCard itself also holds the index MeshCardsIndex pointing to the MeshCards array.

Card Pre-build

ℹ️
\Engine\Source\Developer\MeshUtilities\Private\MeshCardRepresentationUtilities.cpp: GenerateSurfelsForDirection
Does Lumen's Card generation simply involve projecting the bounding box 6 times and then calculating the OBB Card?

No. Complex meshes have more than 6 cards, check this example

image
image
image

Obviously, Lumen realizes that wrapping the Card only on the outside is not enough, and more Cards need to be generated for the surface inside the bounding box of this complex Mesh.

Tips

If you want to debug this part yourself, remember to set this console command first. Otherwise, importing the same mesh will not trigger the card building again:

r.MeshCardRepresentation.Debug 1

Heightfield-based Card Capture

The simplest way I can think of to understand the process of generating a Card is to see it as Ray Marching through height fields along 6 axes, clustering the resulting Surfels, and finally saving the bounding boxes for each cluster.

Let me use a sphere as an example. A cube is too simple.

image

We will capture the height field of the sphere from six directions, as indicated by the directional arrows in the figure.

image

What is "capture"? This means initializing a projection area on the corresponding axis, which is slightly larger than the bounding box of the sphere in that direction.

image

And the Z-axis in this area corresponds to the height direction of the captured height field. Therefore, the axis is as shown in the figure.

image

Next is the important sampling process.

image

Firstly, the projection plane will be divided into a series of cells in the X-Y direction. The size of the cell is specified by the VoxelSize parameter, which is set to 20 in my test.

Each cell will perform 32 ray-tracing Ray Marching operations to sample the height field. It is important to note that:

  • Ray Marching is used here, so when the first surface is encountered, the ray-tracing will not stop, but continue to sample multiple times inside. This is necessary to capture the surfaces inside the Mesh.
  • image
  • 32 samples are not evenly distributed, please do not be misled by the neat lines drawn in the graph for ease of understanding.

After sorting and organizing the results obtained from sampling, they will be used to generate surfels.

At the same time, visibility check will also be performed on Surfel here. Specifically, it performs multiple ray samples on the upper hemisphere space of the surfel to determine if it is visible.

ℹ️
\Engine\Source\Developer\MeshUtilities\Private\MeshCardRepresentationUtilities.cpp: ComputeSurfelVisibility

Surfels can be understood as small surface fragments. These fragments will form the basic units for clustering in the following step.

image

If it's easier for you to imagine, you can treat this as a "height-field".

Clustering

Then, clustering will be performed on the generated Surfel. The result of clustering will be an oriented bounding box (OBB). This is the Card that we cached.

image

The same process will be repeated for each axis, so in the end we will obtain Card OBBs in 6 directions.

Please note that this does not mean we will only obtain 6 Cards. I will explain the reason in the following.

How to treat internal surfaces

As the image shown earlier proves, Lumen does not only generate a card for each of the six axes. Otherwise, for meshes with complex topological structures, cards would not be able to capture a large amount of internal structures, which means that normal global illumination cannot be generated.

Lumen supports capturing internal surfaces through several levels of schemes.

Surfel

During the Cell sampling stage in Surfel, if it is found that different Surfel Samples within the same Cell are at different heights, they will be allocated to multiple Surfels.

image

It achieves this by sorting the discrete SurfelSamples by height, and then merging SurfelSamples with the same height into the same Surfel.

image

Cluster

ℹ️
\Engine\Source\Developer\MeshUtilities\Private\MeshCardRepresentationUtilities.cpp: BuildSurfelClusters

During the Cluster stage, pushing the merging near plane of the Cluster gradually from near to far can capture the surface inside the Mesh.

image

This image shows an example of how the near clipping plane is pushed forward to capture surfaces inside. Interestingly, the bounding box of the outermost Card actually wraps around the inner Card surfaces.

image
image
ℹ️
To check like me, use the console command r.Lumen.Visualize.CardPlacementIndex 1 to display only one card with index 1.

After Build

If we ignore the Debug-related data, then actually the only information that is truly stored is the bounding box information of the Card. The intermediate results of the calculations, including SurfelSample and Surfel, will be discarded and not truly stored in the game data (after packaging).

Material data for Card will not be stored during the preprocessing stage, nor can it be stored at this stage. This is because the material node may contain dynamically calculated results (such as the Time node), and the precalculated results will be invalid.