LOADING 0%
// nav_menu.exe
Home Resume Blog Contact
English فارسی
~/blog / game-dev / game-rendering-optimization

HOW GAMES RENDER THOUSANDS OF TREES WITHOUT FPS DROP

Imagine you're in an open-world game. You're driving down a highway at 180 km/h. To your right, there's a massive forest — thousands of trees, bushes, rocks, and power lines. Everything runs smoothly. But does your GPU really render thousands of trees every single frame?

# A Mistake Many People Might Make at First

Suppose you're just starting to build a game engine. The first solution that probably comes to mind looks something like this:
naive_render.cpp — C++
for (const Tree& tree : trees)
{
    tree.Draw();
}
Logically, this code is perfectly correct. But if you have just 10,000 trees, this loop runs 10,000 times per frame. Now imagine the game runs at 60 frames per second:
calc.cpp
// Draw calls per second:
10,000 × 60 = 600,000 Draw Call
And that's just for trees — we haven't counted buildings, cars, NPCs, and other objects. That's why almost no modern game engine uses this approach.

# Trick #1: Only Draw What's Visible

If there's a forest behind your character, can the player see it? No. So why should the GPU waste time rendering it?
Almost all game engines perform this check before rendering any object:
frustum_culling.cpp — C++
for (const Tree& tree : trees)
{
    if (!camera.IsVisible(tree.GetBoundingBox()))
        continue;
 
    tree.Draw();
}
If a tree is outside the camera's field of view, the loop doesn't even enter the Draw() function. That's why, out of 50,000 trees in the map, only about 400 might actually be rendered.
Technical note — Frustum Culling: This technique checks whether an object is inside the camera's field of view (Frustum). The Frustum is a truncated pyramid representing the portion of the 3D world that the camera sees. Any object outside this region is not rendered.

# Trick #2: The Further Away, The Simpler

Now suppose a tree is exactly one kilometer away from you. Do you really need to process all the details of its leaves and branches? Definitely not.
That's why game engines keep multiple versions of each model:
lod.cpp — C++
float distance = glm::distance(camera.Position(), tree.Position());
 
if (distance < 30.0f)
{
    renderer.Draw(tree.HighLOD());
}
else if (distance < 100.0f)
{
    renderer.Draw(tree.MediumLOD());
}
else
{
    renderer.Draw(tree.LowLOD());
}
When you're close to a tree, the high-quality version is displayed. As the distance increases, simpler models take its place. The interesting thing is that most players don't even notice this change.
Technical note — Level of Detail (LOD): This technique refers to using different versions of a model with varying levels of detail. HighLOD typically has tens of thousands of polygons, while LowLOD might only have a few hundred. This technique dramatically reduces processing load.

# Trick #3: One Command Instead of Ten Thousand

One of the most expensive operations in graphics is sending multiple commands from CPU to GPU. If we want to render each tree separately, this happens:
individual_draw.cpp — C++
for (const Tree& tree : trees)
{
    renderer.Draw(tree.Mesh(), tree.Transform());
}
But modern engines use a feature called GPU Instancing:
instancing.cpp — C++
renderer.DrawInstanced(
    treeMesh,
    treeTransforms.data(),
    static_cast<uint32_t>(treeTransforms.size())
);
In this method, the tree model is sent to the GPU only once, and then a list of all tree positions is sent. Now the GPU itself renders thousands of copies of the same model. As a result, both the CPU is less involved and rendering speed increases significantly.
Technical note — GPU Instancing: This technique refers to rendering multiple instances of a model with a single Draw Call. Instead of sending separate commands for each tree, all positions are sent in one buffer and the Shader on the GPU is responsible for rendering all of them. This technique can improve performance by up to 100x.

# And Those Very Distant Trees...

If you look at the horizon, you probably see thousands of trees. But you might be surprised to learn that many of them aren't even 3D models.
When the distance exceeds a certain threshold, the game engine decides to display just a 2D image instead of the actual model:
billboard.cpp — C++
if (distance > 300.0f)
{
    renderer.DrawBillboard(
        tree.BillboardTexture(),
        tree.Position()
    );
}
From that distance, the human eye perceives almost no difference between a 2D image and the actual model, but the load on the GPU drops dramatically.
Technical note — Billboard: A Billboard is a 2D image that always faces the camera. This technique is used for distant objects like trees, clouds, and stars. Since these objects look roughly the same from a distance, using Billboards dramatically reduces processing load.

# What Decision Does the Game Engine Make Each Frame?

If we want to show the engine's logic simply, it would look something like this:
render_loop.cpp — C++
for (const Tree& tree : world.Trees())
{
    // 1) Frustum Culling
    if (!camera.IsVisible(tree.GetBoundingBox()))
        continue;
 
    // 2) Calculate distance
    float distance = glm::distance(
        camera.Position(),
        tree.Position()
    );
 
    // 3) LOD Selection
    if (distance < 30.0f)
    {
        renderer.Draw(tree.HighLOD());
    }
    else if (distance < 100.0f)
    {
        renderer.Draw(tree.MediumLOD());
    }
    else if (distance < 300.0f)
    {
        renderer.Draw(tree.LowLOD());
    }
    else
    {
        // 4) Billboard for very distant objects
        renderer.DrawBillboard(
            tree.BillboardTexture(),
            tree.Position()
        );
    }
}
This is a simple example, but the idea used by major engines like Unreal Engine or proprietary open-world game engines is based on the same principle — with the difference that they also use more advanced structures like Octree, BVH, Occlusion Culling, GPU Culling, and Streaming.

# Summary

Every time you look at a forest in an open-world game, remember that what you see is the result of a series of intelligent decisions:
Frustum Culling: Only objects inside the field of view are rendered.
LOD: The further away, the simpler.
GPU Instancing: Thousands of instances with one command.
Billboard: Very distant objects are just 2D images.
takeaway.txt
Game engines don't render everything; they only render what's necessary.