<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Eviston</title>
    <description>The latest articles on DEV Community by Eviston (@eviston).</description>
    <link>https://dev.to/eviston</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4215363%2Ffea52eec-63da-4681-99cc-05e6a3532bcf.png</url>
      <title>DEV Community: Eviston</title>
      <link>https://dev.to/eviston</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eviston"/>
    <language>en</language>
    <item>
      <title>How to Build a Forest in Godot 4 That Doesn't Kill Your FPS: MultiMesh, LOD and Visibility Ranges</title>
      <dc:creator>Eviston</dc:creator>
      <pubDate>Fri, 30 Oct 2026 17:14:06 +0000</pubDate>
      <link>https://dev.to/eviston/how-to-build-a-forest-in-godot-4-that-doesnt-kill-your-fps-multimesh-lod-and-visibility-ranges-1oi6</link>
      <guid>https://dev.to/eviston/how-to-build-a-forest-in-godot-4-that-doesnt-kill-your-fps-multimesh-lod-and-visibility-ranges-1oi6</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I'm part of the team behind FoliMesh, a tree generator. This guide is engine-only — everything here works with any trees.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The first forest in a Godot project usually looks great — until you walk into it. Two hundred trees later the frame rate halves and the profiler shows thousands of draw calls.&lt;/p&gt;

&lt;p&gt;Godot 4 has every tool you need for a big, fast forest. They just have to be combined in the right order: &lt;strong&gt;budget → LOD → instancing → leaves → shadows → culling&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why forests are slow
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37q28svk9815vrn9fgzz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F37q28svk9815vrn9fgzz.png" alt=" " width="800" height="383"&gt;&lt;/a&gt;&lt;br&gt;
A tree costs you in four places at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Draw calls&lt;/strong&gt; — bark and leaves are separate meshes/materials. 100 trees = hundreds of draw calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triangles&lt;/strong&gt; — a detailed tree easily has 40k+ triangles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Overdraw&lt;/strong&gt; — leaf cards are mostly transparent pixels, and the GPU still shades them, layer after layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shadows&lt;/strong&gt; — every tree is drawn again into the shadow map, sometimes once per cascade.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Step 1. Set a triangle budget first
&lt;/h2&gt;

&lt;p&gt;Decide how much a tree may cost at each distance &lt;em&gt;before&lt;/em&gt; you model or generate it. Example chain for one oak:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Level&lt;/th&gt;
&lt;th&gt;Distance&lt;/th&gt;
&lt;th&gt;Triangles&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;LOD0&lt;/td&gt;
&lt;td&gt;0–30 m&lt;/td&gt;
&lt;td&gt;14,490&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LOD1&lt;/td&gt;
&lt;td&gt;30–70 m&lt;/td&gt;
&lt;td&gt;3,984&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LOD2&lt;/td&gt;
&lt;td&gt;70–150 m&lt;/td&gt;
&lt;td&gt;1,154&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Impostor&lt;/td&gt;
&lt;td&gt;150 m+&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The shape of the curve matters more than the numbers: each level ~3–4× lighter. On mobile, divide by 2–3. Most of your forest is far away, so most of it should be cheap.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2. Automatic mesh LOD vs visibility ranges
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Automatic mesh LOD&lt;/strong&gt; (generated on import with meshoptimizer) is great for rocks and buildings. A tree crown, though, is hundreds of separate leaf cards — generic simplification can thin and distort it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Visibility ranges&lt;/strong&gt; let you supply your own LODs and set the distance band for each. Every &lt;code&gt;GeometryInstance3D&lt;/code&gt; has &lt;code&gt;visibility_range_begin/end&lt;/code&gt;, margins to avoid flicker at the border, and &lt;code&gt;visibility_range_fade_mode&lt;/code&gt; (Disabled is fastest; fades force transparent rendering during the transition).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Tree.gd — children: LOD0, LOD1, LOD2 (MeshInstance3D)&lt;/span&gt;
&lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Node3D&lt;/span&gt;

&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lod_starts&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PackedFloat32Array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;30.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;70.0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cull_distance&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;150.0&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_ready&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;get_child_count&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mi&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_child&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;MeshInstance3D&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;mi&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kt"&gt;null&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;mi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibility_range_begin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lod_starts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;mi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibility_range_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lod_starts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;lod_starts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;cull_distance&lt;/span&gt;
        &lt;span class="n"&gt;mi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibility_range_end_margin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.0&lt;/span&gt;
        &lt;span class="n"&gt;mi&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibility_range_fade_mode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GeometryInstance3D&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VISIBILITY_RANGE_FADE_DISABLED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the switch is visible, check that LODs keep the same silhouette first — it matters more than any fade setting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. Thousands of trees: MultiMesh, in chunks
&lt;/h2&gt;

&lt;p&gt;One &lt;code&gt;MultiMeshInstance3D&lt;/code&gt; draws any number of copies in one draw call. The catch: &lt;strong&gt;all instances share one LOD level, and culling only removes the node as a whole&lt;/strong&gt; (see Godot issue #76436). One MultiMesh for the whole level = no per-tree LOD, no culling.&lt;/p&gt;

&lt;p&gt;The fix is a grid of chunks, each with its own MultiMesh per LOD level and its own visibility range:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight gdscript"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ForestChunks.gd&lt;/span&gt;
&lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="n"&gt;Node3D&lt;/span&gt;

&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lod_meshes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Mesh&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="c1"&gt;# LOD0, LOD1, LOD2, impostor&lt;/span&gt;
&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;lod_starts&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PackedFloat32Array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;35.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;80.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;160.0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cull_distance&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;400.0&lt;/span&gt;
&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;32.0&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transforms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Transform3D&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;transforms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Vector2i&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;floori&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;floori&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;origin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;chunk_size&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_build_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;_build_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;void&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lod_meshes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;mm&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MultiMesh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;mm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transform_format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MultiMesh&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TRANSFORM_3D&lt;/span&gt;
        &lt;span class="n"&gt;mm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mesh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lod_meshes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;mm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instance_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;mm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_instance_transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MultiMeshInstance3D&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;multimesh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mm&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibility_range_begin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lod_starts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibility_range_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lod_starts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;lod_starts&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;cull_distance&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;visibility_range_end_margin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;
        &lt;span class="n"&gt;add_child&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chunk size ≈ LOD0 distance.&lt;/strong&gt; Distance is measured to the chunk centre, so huge chunks show LOD1 right next to the player. With LOD0 up to 35 m, 24–32 m chunks work well.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not too small either&lt;/strong&gt; — every chunk adds draw calls. Measure.&lt;/li&gt;
&lt;li&gt;MultiMesh has no collision: add a capsule per trunk only where the player can reach.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 4. Leaves: Alpha Scissor, not Alpha
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Set &lt;strong&gt;Transparency → Alpha Scissor&lt;/strong&gt;. Blended alpha needs sorting, breaks depth and shadows, and is far more expensive.&lt;/li&gt;
&lt;li&gt;With MSAA, use &lt;strong&gt;Alpha Antialiasing → Alpha Edge Clip/Blend&lt;/strong&gt; for soft edges.&lt;/li&gt;
&lt;li&gt;Cut cards to the shape of the leaf cluster to reduce overdraw. Check with &lt;strong&gt;Display Overdraw&lt;/strong&gt; — bright red crowns mean too many layers.&lt;/li&gt;
&lt;li&gt;Drop inner clusters nobody sees, starting with far LODs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 5. Shadows: the hidden second forest
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Limit &lt;strong&gt;DirectionalLight3D → Directional Shadow → Max Distance&lt;/strong&gt; (100–200 m is usually enough).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cast Shadow → Off&lt;/strong&gt; on LOD2 and impostors.&lt;/li&gt;
&lt;li&gt;Two cascades instead of four on weak hardware.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 6. Occlusion culling and wind
&lt;/h2&gt;

&lt;p&gt;Trees are full of holes and make poor occluders — let terrain, hills and buildings do the work. Enable occlusion culling, add an &lt;code&gt;OccluderInstance3D&lt;/code&gt; and bake. Chunked MultiMeshes benefit most: a hidden chunk is skipped entirely.&lt;/p&gt;

&lt;p&gt;Wind belongs in the vertex shader, reading precomputed per-vertex weights (trunk barely moves, branches sway, leaf tips flutter). Reading two numbers per vertex is almost free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7. Measure on the target hardware
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Debugger → Monitors&lt;/strong&gt;: draw calls, objects, primitives in frame.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Debugger → Visual Profiler&lt;/strong&gt;: CPU vs GPU time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Viewport → Display Overdraw / Wireframe&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Measure before and after each change, on the weakest device you support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Triangle budget per distance, each LOD 3–4× lighter&lt;/li&gt;
&lt;li&gt;[ ] Visibility ranges with own LODs for trees; auto mesh LOD for rocks/props&lt;/li&gt;
&lt;li&gt;[ ] Chunked MultiMeshes, chunk ≈ LOD0 distance&lt;/li&gt;
&lt;li&gt;[ ] Alpha Scissor + alpha to coverage; cards follow cluster shape&lt;/li&gt;
&lt;li&gt;[ ] Limited shadow distance; far LODs don't cast shadows&lt;/li&gt;
&lt;li&gt;[ ] Occlusion baked from terrain and large objects&lt;/li&gt;
&lt;li&gt;[ ] Wind as a cheap vertex shader&lt;/li&gt;
&lt;li&gt;[ ] Draw calls and frame time measured on target device&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you'd rather not build the LOD chain by hand: we made &lt;a href="https://folimesh.com/godot?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=seeding" rel="noopener noreferrer"&gt;FoliMesh's Godot package&lt;/a&gt; for our own large-scale game — a GLB with LODs at your triangle budgets plus a ready &lt;code&gt;.tscn&lt;/code&gt; that sets visibility ranges, a wind shader and trunk collision.&lt;/p&gt;

&lt;p&gt;What's your biggest forest performance pain in Godot? Happy to dig into it in the comments.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>godot</category>
      <category>threejs</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
