Memory Optimization Algorithm for Landscape Grass Based on Visibility Culling in UE5

Jabriel,·6 min read

Landscape Grass in UE5ImageLandscape Grass in UE5

This article presents a method to reduce the memory footprint of Landscape Grass. By analyzing Landscape Grass as an excellent scene vegetation rendering solution, we can modify its planting pipeline to further optimize memory usage. This optimization has particularly significant effects on mobile devices, where it can save around 40% to 50% of Landscape Grass memory usage.

For a detailed analysis of the Landscape Grass principles, please refer to the previous blog: UE5 Landscape Grass Source Analysis: A Look Under the Hood

Optimization Approach: Visibility Culling#

After analyzing the source code, we found that the generation of Landscape Grass is related to the position of the camera in the scene, but not to the camera's orientation. Therefore, all grass within a certain radius around the camera will be generated and stored in memory. On performance-constrained mobile devices, we can apply the concept of GPU visibility culling to optimize Landscape Grass memory usage. The fundamental principle of this optimization is to eliminate what is not needed.

Top-down View of Grass RenderingImageTop-down View of Grass Rendering

In the above image, the red area represents the frustum region. Grass outside this region will be culled by the GPU, but the corresponding number of Grass Instances is still allocated in memory, leading to some waste.

Without affecting the gameplay, we can consider the following approach:

  • All grass in front of the camera's field of view should be generated because the player may move the camera, and grass in the distance continuously comes into view during forward movement.
  • Considering that grass is not updated every frame (as mentioned in the theoretical part regarding TickInterval and maximum AsyncTasks), we should avoid the sudden appearance or disappearance of grass in the distance while the player is moving forward.
  • We do not need to generate all the grass behind the camera's field of view. However, to avoid vegetation loss, we introduce a small Patch Zone (the yellow area in the image) behind the camera as a buffer to ensure that the grass behind the player is correctly loaded.
  • No grass should be planted in the remaining areas.
  • This approach does not consider the height in 3D space and is a 2D optimization solution.

Filling the Gap#

Grass Variety is generated based on Grass Subsections, and we need to consider some boundary cases.

A Grass Subsection is a quadrangle (not a Landscape Component Subsection), so it is natural to use the center of the subsection's projection in the zz direction as our starting point for calculations.

We have the Camera Forward vector VcamV_{cam}, and let's assume that the vector from the starting point of VcamV_{cam} to the center PP of the subsection is VsubV_{sub} .

Therefore, the determination of the land blocks in front of the field of view is:

VcamVsub>0V_{cam} \cdot V_{sub} > 0

If the Camera Vector VcamV_{cam} is in front of the subsection, but some grass within the FOV is exposed, that portion of grass will disappear.

How grass disappears in the field of viewImageHow grass disappears in the field of view

Dealing with this situation is relatively simple. We only need to check if the Camera is within the Bounding Box (AABB)1 of this subsection. When the Camera is within this subsection, forcefully load the grass on this block. However, considering the boundary cases where the camera is at the edge of the subsection, the grass on the neighboring blocks also disappears. We can modify the determination formula as follows:

VcamVsub0V_{cam} \cdot V_{sub} \geq 0

However, when VcamVsub=0V_{cam} \cdot V_{sub} = 0, an entire row of grass will also be loaded, which is obviously not the optimal solution we want. At the same time, the grass on the land blocks VsubV_{sub} where VcamVsub0V_{cam} \cdot V_{sub} \to 0- will also disappear:

Boundary case of orthogonal land block with the cameraImageBoundary case of orthogonal land block with the camera

We can make additional judgments based on the (shortest) distance from the Camera to the subsection, that is, the ratio of the distance from the tail of VcamV_{cam} to the AABB of the subsection and the size of the subsection's side length. This can control the loading of nearby grass on the land blocks.

However, such a complex algorithm may not be very pleasant to work with.

Generalized Algorithm#

Instead of considering whether the Camera is inside the subsection, we can use the following generalized algorithm:

Assuming the Distance Vector from the Camera to any vertex of the subsection is VdV_d, when VdVcam>0V_d \cdot V_{cam} > 0, we can identify all the subsections located in the Camera's orthogonal direction and in front of it. Then, we check if the distance from the Camera to the AABB of the subsection is less than the side length of the subsection in order to load the nearby grass on the land blocks.

Final Generalized AlgorithmImageFinal Generalized Algorithm

Controlling Culling Granularity#

If you think this is the end of the optimization solution, then you are too young too simple. Through the source code, we find that we have another variable called GMaxInstancesPerComponent, which is used to control the maximum number of instances per subsection (not semantically the landscape component...). The engine's default value is 65536 (2162^{16}), with a lower limit of 1024 (2102^{10}), and no upper limit.

See previous article: UE5 Landscape Grass Source Analysis: A Look Under the Hood

Therefore, the smaller the GMaxInstancesPerComponent, the more subsections there will be, and the grass density in the scene will appear slightly sparser. This also means that the granularity of culling is smaller, which benefits memory optimization.

By setting different values for GMaxInstancesPerComponent and comparing the number of built tasks before and after enabling the optimization algorithm, we can observe how it affects the grass generation process:

In the demo scene, based on the improved generation algorithm:

GMaxInstancesPerComponentBuilt Tasks Before OptimizationBuilt Tasks After OptimizationOptimization Ratio
210=10242^{10}=10241518742.4%
211=20482^{11}=20481116640.5%
216=655362^{16}=65536 (Engine Default)121016.7%

We can see that GMaxInstancesPerComponent has a significant impact on the grass generation tasks and should not be set too large.

Algorithm Implementation#

You can borrow my ideas, but do you really want to copy my code?

Pseudocode for the core algorithm:

// Iterate over subsection vertices
for (Vector Vertex: Subsections)
{
	IsCameraBehind = IsCameraBehind || (
		(Vertex.X - CameraLocation.X) * CameraForward.X +
		(Vertex.Y - CameraLocation.Y) * CameraForward.Y) > 0);
}
 
if (!IsCameraBehind && DistanceFromCameraToSubsection) > Threshold)
{
	// Don't spawn grass
}
else
{
	// Spawn grass
}

Memory Testing#

Based on theoretical analysis, our Landscape Grass memory usage can be reduced to approximately half of the original.

The author conducted ablation experiments on a demo scene and analyzed the number of specified vegetation StaticMesh instances to determine the memory size. The following are the experimental results:

GMaxInstancesPerComponentNumInstances Before OptimizationNumInstances After OptimizationOptimization Ratio
210=10242^{10}=1024875954864244.5%
211=20482^{11}=2048992395829041.3%

In another simple demo scene with less grass, a quick memory analysis was performed, and the optimization ratio was approximately 46.4%:

ItemMemory [MB]Built Tasks
Map without grass582N/A
Default algorithm61012
Culling optimization59510

The results are quite significant.

Reference#

  1. UE5 Landscape Grass Source Analysis: A Look Under the Hood
  2. 《InsideUE4》GamePlay 架构(十一)Subsystems (open in a new tab)
  3. UE4 Mobile Landscape 总览及源码解析 (open in a new tab)
  4. UE4 中的植被工具 (open in a new tab)
  5. LearnOpenGL - Instancing (open in a new tab)
  6. UE4 材质系统 (open in a new tab)
  7. Halton Sequence (open in a new tab)

Citation#

Footnotes#

  1. AABB stands for Axis-Aligned Bounding Box.

CC BY-NC-SA 4.02017 - 2023 © Jabriel