This blog post will break down Optimization section of the Procedural Monadnock project. You will be able to learn how to generate a forest containing hundreds of thousands of trees while maintaining performance. I’ll also talk about the process and some problems I encountered throughout this section of the project. Link to Portfolio Post below.
I do want to include a disclaimer acknowledging that there are likely aspects of this project that could benefit from further optimization. My primary goal was to achieve a stable 60 FPS, and once that target was met, I prioritized other aspects of the project over additional asset refinements.
Computer Specs
While working on this project I used a computer with these specifications:
– Intel(R) Core(TM) i7-9700 CPU
– NVIDIA RTX 2070 Super
– 32 GB of RAM
Nanite vs Imposters
Nanite excels at optimizing meshes in the near and medium foreground, but its performance benefits diminish over long distances. For example, a 500k+ triangle tree won’t be rendered as efficiently as an imposter when viewed from afar. Since Nanite lacks a way to force pre-assigned LOD transitions, I developed a custom solution that combines the strengths of both Nanite and traditional imposters for optimal performance and visual fidelity.
In practice, this involves spawning both an Imposter and a Hi-Res Nanite mesh through the PCG system. The Nanite meshes are culled at a designated distance, while the imposters are smoothly faded in. Since the Static Mesh Spawner node in PCG doesn’t allow for a culling bias to be directly applied to the imposters, the transition had to be handled using a fade-in material effect.
This hybrid approach leverages the strengths of both Nanite meshes and imposters. By combining these techniques, I was able to strike a balance between high-quality visuals and performance for large dense environments.
Shadow Optimizations
The Green represents cached (rigid) shadows, the Blue are (dynamic) shadows that have to be recalculated every frame.
One of the most significant performance improvements came from optimizing Virtual Shadow Maps (VSM). While dynamic shadows offer great visual quality, they can heavily impact performance. By setting the Shadow Cache Invalidation Behavior to Rigid for all spawned meshes, I converted their shadows to being static. This drastically reduced the frequency of VSM Shadow Cache updates, resulting in a 5-8ms decrease in frame render time.
Another substantial performance boost came from disabling dynamic shadows on imposters and utilizing contact shadows instead. This approach minimized the frequency of VSM Shadow Cache invalidations caused by camera movement, further improving overall performance.
Minor Changes
Adjusting the scalability settings for the engine changed the performance quite noticeably. For the final output I had all settings on Epic, except Shadows, Global Illumination and Reflections which were all set on High. This allowed me to average around 60 fps above the forest, and around 50 fps near the ground.
By limiting the World Position Offset (WPO) of meshes, I was able to preserve the dynamic vertex animations for close-up details while significantly reducing the computational overhead of materials rendered at a distance. Each mesh was assigned a WPO cutoff distance through the layer’s Data Asset.
Profiling Tools
Achieving the level of performance required using several tools to identify and isolate problems with the rendering process. For this I used a handful of tools within Unreal, including, but not limited to: Trace, GPU Visualizer, Statistics, Render Resource Viewer, VSM/Nanite/Shader Optimization view modes.
I also found these console commands to be very helpful: stat rhi, stat GPU, stat FPS, stat UnitGraph
Resources
These were some of the online resources that I found very helpful during the optimization stage:
– Performance Optimization for Environments | Inside Unreal
– Optimizing UE5: Advanced Rendering, Graphics Performance, and Memory Management | Unreal Fest 2024
– 5 tips to optimize your UE5 game!
– Nanite Forest Optimization in Unreal Engine 5
– Unreal Engine 5.3 Preview 1 – Nanite Foliage: The End of the Story
– UE5 Console Variables Dictionary