Memory Optimization Algorithm for Landscape Grass Based on Visibility Culling in UE5
ImageLandscape 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.
ImageTop-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 direction as our starting point for calculations.
We have the Camera Forward vector , and let's assume that the vector from the starting point of to the center of the subsection is .
Therefore, the determination of the land blocks in front of the field of view is:
If the Camera Vector is in front of the subsection, but some grass within the FOV is exposed, that portion of grass will disappear.
ImageHow 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:
However, when , 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 where will also disappear:
ImageBoundary 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 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 , when , 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.
ImageFinal 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 (), with a lower limit of 1024 (), 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:
GMaxInstancesPerComponent | Built Tasks Before Optimization | Built Tasks After Optimization | Optimization Ratio |
---|---|---|---|
151 | 87 | 42.4% | |
111 | 66 | 40.5% | |
(Engine Default) | 12 | 10 | 16.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:
GMaxInstancesPerComponent | NumInstances Before Optimization | NumInstances After Optimization | Optimization Ratio |
---|---|---|---|
87595 | 48642 | 44.5% | |
99239 | 58290 | 41.3% |
In another simple demo scene with less grass, a quick memory analysis was performed, and the optimization ratio was approximately 46.4%:
Item | Memory [MB] | Built Tasks |
---|---|---|
Map without grass | 582 | N/A |
Default algorithm | 610 | 12 |
Culling optimization | 595 | 10 |
The results are quite significant.
Reference#
- UE5 Landscape Grass Source Analysis: A Look Under the Hood
- 《InsideUE4》GamePlay 架构(十一)Subsystems (open in a new tab)
- UE4 Mobile Landscape 总览及源码解析 (open in a new tab)
- UE4 中的植被工具 (open in a new tab)
- LearnOpenGL - Instancing (open in a new tab)
- UE4 材质系统 (open in a new tab)
- Halton Sequence (open in a new tab)
Citation#
Footnotes#
-
AABB stands for Axis-Aligned Bounding Box. ↩