Як зробити ліс у Godot 4, який не вбиває FPS: MultiMesh, LOD і Visibility Range
Практичний посібник зі швидкої рослинності в Godot 4: бюджет трикутників, автоматичний LOD мешів проти visibility range, MultiMesh по чанках, прозорість листя, тіні, occlusion culling і як виміряти результат.

Перший ліс у проєкті на Godot зазвичай виглядає чудово — доки ви в нього не зайдете. Двісті дерев — і частота кадрів падає вдвічі, профайлер показує тисячі draw call'ів, а ніхто не знає, яке налаштування змінювати. Один користувач форуму Godot описав саме таку ситуацію: саморобна система LOD для трави на невеликій ділянці дала близько 10 FPS і понад 3 000 draw call'ів.
Добра новина: у Godot 4 є всі інструменти для великого й швидкого лісу. Їх лише треба правильно поєднати. У цьому посібнику ми пройдемо їх у порядку важливості — бюджет, LOD, інстансинг, листя, тіні, culling — а наприкінці буде чекліст.
Чому ліс гальмує
Дерево — один із найдорожчих об'єктів у грі. Воно коштує вам одразу в чотирьох місцях:
- Draw call'и. Кожне дерево — це щонайменше два меші (кора й листя), часто з різними матеріалами. Сотня окремих дерев — це сотні draw call'ів, ще до того як ви намалювали щось інше.
- Трикутники й вершини. Детальне дерево легко має 40 000+ трикутників. Для одного «геройського» дерева це нормально, для цілого схилу — смертельно.
- Overdraw. Листя — це картки з прозорими пікселями. GPU однаково обраховує невидимі частини, шар за шаром, для кожної картки в кроні.
- Тіні. Кожне дерево малюється ще раз у карту тіней — іноді кілька разів, по одному на кожен каскад тіней.
Кожна техніка нижче б'є по одній або кількох із цих статей витрат.
Крок 1. Визначте бюджет, перш ніж щось будувати
Найкорисніша звичка — заздалегідь вирішити, скільки трикутників може коштувати дерево на кожній дистанції, ще до моделювання чи генерації. Ось справжній дуб із FoliMesh як приклад того, як виглядає готовий до гри ланцюжок:
| Рівень | Дистанція (приклад) | Трикутники |
|---|---|---|
| LOD0 | 0–30 м | 14 490 |
| LOD1 | 30–70 м | 3 984 |
| LOD2 | 70–150 м | 1 154 |
| Імпостер | 150 м + | 2 |
Важливі не точні цифри, а форма кривої: кожен рівень приблизно у 3–4 рази легший за попередній, а найдальші дерева майже нічого не коштують. На мобільних пристроях поділіть усе на два-три. Більша частина вашого лісу далеко, тож більша частина лісу має бути дешевою.
Те саме дерево на чотирьох рівнях деталізації: силует зберігається, кількість трикутників падає.
Крок 2. Оберіть механізм LOD: автоматичний LOD мешів чи visibility range
Godot 4 пропонує дві різні системи LOD, і неправильний вибір для дерев — часте джерело проблем.
Автоматичний LOD мешів. Під час імпорту сцени glTF, .blend або FBX Godot генерує спрощені версії кожного мешу за допомогою meshoptimizer і сам перемикається між ними. Згідно з офіційною документацією, вибір залежить від найближчої до камери точки обмежувального об'єму вузла та від того, скільки пікселів займе похибка (Rendering → Mesh LOD → LOD Change → Threshold Pixels, за замовчуванням 1 піксель). Кожен вузол також має LOD Bias.
Це добре працює для каміння, будівель і пропсів — суцільних поверхонь. Крона дерева — інша річ: це сотні окремих карток листя. Універсальне спрощення не розраховане на таке, і в результаті крона може порідшати й спотворитися. Сама документація радить інший інструмент, якщо у вас є власноруч зроблені LOD.
Visibility range (HLOD). Тут ви самі надаєте меші LOD і вказуєте кожному, на якій дистанції він видимий. Кожен GeometryInstance3D має:
visibility_range_begin/visibility_range_end— діапазон дистанцій, у якому вузол малюється (вимірюється до центру його обмежувального об'єму);visibility_range_begin_margin/visibility_range_end_margin— буферна зона, щоб рівень не мерехтів, коли камера стоїть точно на межі;visibility_range_fade_mode— Disabled (миттєве перемикання, найшвидше), Self або Dependencies (плавне згасання через альфу; під час переходу воно змушує рендерити об'єкт як прозорий, що знижує продуктивність).
Для дерев visibility range — надійний вибір: LOD спроєктовані саме для листя, дистанції під вашим контролем, а на останньому рівні можна вимкнути тіні або перейти на імпостер. Мінімальне налаштування для одного дерева:
# Tree.gd — дочірні вузли: LOD0, LOD1, LOD2 (MeshInstance3D)
extends Node3D
@export var lod_starts := PackedFloat32Array([0.0, 30.0, 70.0])
@export var cull_distance := 150.0
func _ready() -> void:
for i in get_child_count():
var mi := get_child(i) as MeshInstance3D
if mi == null:
continue
mi.visibility_range_begin = lod_starts[i]
mi.visibility_range_end = lod_starts[i + 1] if i + 1 < lod_starts.size() else cull_distance
mi.visibility_range_end_margin = 2.0
mi.visibility_range_fade_mode = GeometryInstance3D.VISIBILITY_RANGE_FADE_DISABLED
Якщо перемикання помітне, спершу переконайтеся, що LOD зберігають однаковий силует, — це важливіше за будь-які налаштування згасання.
Крок 3. Багато дерев: MultiMesh, але по чанках
Окремі вузли підходять для кількох десятків дерев. Для тисяч потрібен MultiMesh: один MultiMeshInstance3D малює будь-яку кількість копій мешу за один draw call.
Тут є підступ, який дивує майже всіх. У документації сказано, що вузли, які малюють багато мешів одночасно, — MultiMeshInstance3D і частинки, — малюють усі інстанси на одному рівні LOD, а culling може прибрати лише вузол цілком, а не окремі інстанси. Один із мейнтейнерів Godot підтвердив це в issue #76436: один MultiMesh на весь рівень означає відсутність LOD для кожного дерева і жодного frustum чи occlusion culling.
Рішення — чанки. Розбийте світ на сітку й дайте кожній клітинці власний MultiMesh для кожного рівня LOD, кожен зі своїм visibility range. Далекі чанки показують легкий LOD, близькі — детальний, а чанки позаду камери відсікаються повністю.
# ForestChunks.gd — будує MultiMesh по чанках із visibility range
extends Node3D
@export var lod_meshes: Array[Mesh] = [] # LOD0, LOD1, LOD2, імпостер
@export var lod_starts := PackedFloat32Array([0.0, 35.0, 80.0, 160.0])
@export var cull_distance := 400.0
@export var chunk_size := 32.0
func build(transforms: Array[Transform3D]) -> void:
var chunks := {}
for t in transforms:
var key := Vector2i(floori(t.origin.x / chunk_size), floori(t.origin.z / chunk_size))
if not chunks.has(key):
chunks[key] = []
chunks[key].append(t)
for key in chunks:
_build_chunk(chunks[key])
func _build_chunk(items: Array) -> void:
for i in lod_meshes.size():
var mm := MultiMesh.new()
mm.transform_format = MultiMesh.TRANSFORM_3D
mm.mesh = lod_meshes[i]
mm.instance_count = items.size()
for k in items.size():
mm.set_instance_transform(k, items[k])
var node := MultiMeshInstance3D.new()
node.multimesh = mm
node.visibility_range_begin = lod_starts[i]
node.visibility_range_end = lod_starts[i + 1] if i + 1 < lod_starts.size() else cull_distance
node.visibility_range_end_margin = 4.0
add_child(node)
Два практичні правила:
- Розмір чанка — близько до дистанції LOD0. Дистанція вимірюється до центру чанка, тож у чанку на 100 м дерево поруч із гравцем показувалося б на рівні LOD1. Якщо LOD0 діє до 35 м, добре працюють чанки по 24–32 м.
- Але й не надто дрібні. Кожен чанк додає draw call'и (по одному на кожен LOD і кожен матеріал). Вимірюйте: зазвичай кілька сотень чанків на екрані на десктопі ще прийнятні, на мобільних — значно менше.
У MultiMesh немає колізій. Додавайте прості форми лише там, куди може дістатися гравець, — капсулу чи циліндр на кожен стовбур, але ніколи на крону.
Сотні інстансованих дерев: правильна структура тримає кількість draw call'ів майже незмінною, хоч ліс і росте.
Крок 4. Листя: alpha scissor, а не alpha blending
Саме на листі більшість лісів втрачає продуктивність — і значну частину краси, якщо прозорість налаштована неправильно.
- Використовуйте Alpha Scissor (cutout), а не Alpha. У матеріалі встановіть Transparency → Alpha Scissor. Змішувана альфа вимагає сортування, ламає глибину й тіні і значно дорожча для крони із сотнями карток, що перекривають одна одну.
- Згладжуйте краї через alpha to coverage. З увімкненим MSAA встановіть Alpha Antialiasing → Alpha Edge Clip (або Blend), щоб отримати м'які краї листя без справжньої прозорості.
- Боріться з overdraw формою картки. Квадратна картка навколо круглої гілочки марнує багато пікселів. Картки, вирізані за формою кластера листя, обраховують набагато менше невидимих пікселів. Перевірте це режимом в'юпорту Display Overdraw: яскраво-червоні крони означають забагато шарів.
- Приберіть те, чого ніхто не бачить. Кластери глибоко всередині крони ззовні невидимі; далекі LOD мають позбуватися їх насамперед.
Крок 5. Тіні: прихований другий ліс
Кожне дерево, що відкидає тінь, рендериться ще раз для карти тіней. Три налаштування дають більшу частину економії:
- Обмежте дальність тіней.
DirectionalLight3D→ Directional Shadow → Max Distance. Далі 100–200 м мало хто з гравців помічає відсутність тіней від окремих дерев. - Вимкніть тіні на далеких LOD. Встановіть Cast Shadow → Off на LOD2 та імпостерах (для MultiMesh — на
MultiMeshInstance3Dвідповідного рівня). - Використовуйте менше каскадів на слабкому залізі. Два каскади замість чотирьох — часто непомітна зміна і відчутна економія.
Крок 6. Occlusion culling і вітер
Occlusion culling допомагає, коли ліс закривають великі об'єкти: пагорби, скелі, будівлі, сам рельєф. Увімкніть його в Project Settings → Rendering → Occlusion Culling, додайте OccluderInstance3D і запечіть. Самі дерева — погані оклюдери, у них повно дірок, — тож нехай цю роботу виконує рельєф. Найбільше виграють MultiMesh по чанках: цілий прихований чанк просто пропускається.
Вітер має жити у вершинному шейдері й читати заздалегідь обчислені ваги з мешу: стовбур ледь рухається, великі гілки повільно гойдаються, кінчики листя швидко тремтять. Обчислювати це з нуля в шейдері дорого; прочитати два числа на вершину — майже безкоштовно.
Крок 7. Вимірюйте на цільовому залізі
Ніколи не оптимізуйте наосліп. Godot дає все необхідне:
- Debugger → Monitors: draw call'и в кадрі, об'єкти, примітиви (трикутники) в кадрі.
- Debugger → Visual Profiler: скільки часу CPU і GPU витрачають на кожну частину кадру.
- Viewport → Display Overdraw / Wireframe: куди йдуть пікселі й трикутники.
Вимірюйте до і після кожної зміни, на найслабшому пристрої, який хочете підтримувати, — а не лише на своєму робочому ПК.
Чекліст
- Кожне дерево має бюджет трикутників для кожної дистанції, кожен рівень у 3–4 рази легший.
- Дерева використовують visibility range із власними LOD; автоматичний LOD мешів — лише для каміння й пропсів.
- Тисячі дерев розбито на MultiMesh по чанках розміром приблизно з дистанцію LOD0.
- Листя використовує Alpha Scissor разом з alpha to coverage; картки повторюють форму кластерів.
- Дальність тіней обмежено; далекі LOD та імпостери не відкидають тіней.
- Occlusion culling запечено з рельєфу й великих об'єктів.
- Вітер — дешевий вершинний шейдер, що читає ваги з вершин.
- Draw call'и та час кадру виміряно на цільовому пристрої.
Де тут FoliMesh
Ми створили FoliMesh для власної масштабної гри, тому він генерує дерева, які вже відповідають цим правилам. Пакет для Godot — це папка, яку достатньо закинути в проєкт: GLB з LOD0, LOD1, LOD2…, згенерованими під ваші бюджети трикутників, і готова сцена .tscn, чий скрипт задає visibility range для кожного рівня, шейдер вітру (вигин стовбура й гойдання гілок зберігаються в другому UV-каналі мешу) та StaticBody3D з капсулою навколо стовбура для колізій. Карти нормалей уже у форматі OpenGL, якого очікує Godot.
Будь-яку модель можна перевірити перед імпортом нашими безкоштовними браузерними інструментами: переглядачем LOD, лічильником полігонів і валідатором glTF. Ширшу картину інструментів для дерев дає наше порівняння 3D-генераторів дерев.
FAQ
Чи створює Godot 4 LOD автоматично? Так — для імпортованих сцен glTF, .blend, Collada і FBX він генерує LOD мешів під час імпорту. Найкраще це працює на суцільних поверхнях; для дерев передбачуваніший результат дають власноруч зроблені або згенеровані LOD із visibility range.
Чому LOD не працює з моїм MultiMesh?
Godot обирає один рівень LOD для всього MultiMeshInstance3D. Розбийте інстанси на чанки, кожен зі своїм вузлом і visibility range.
Скільки дерев може відрендерити Godot? Це залежить від бюджету на дерево, а не від рушія. З MultiMesh по чанках, дешевими далекими LOD та імпостерами десятки тисяч дерев на екрані цілком реальні на десктопному залізі; вимірюйте на своєму цільовому пристрої.
Alpha Scissor чи Alpha Hash для листя? Зазвичай обирають Alpha Scissor з alpha to coverage. Alpha Hash дає м'якші краї без MSAA, але виглядає шумно й найкраще працює з TAA.

