FoliMesh
Блог

Как сделать лес в Godot 4, который не убивает FPS: MultiMesh, LOD и диапазоны видимости

Практическое руководство по быстрой растительности в Godot 4: бюджет треугольников, автоматический LOD против диапазонов видимости, MultiMesh по чанкам, прозрачность листвы, тени, отсечение перекрытых объектов и замеры.

·9 мин чтения#godot#optimization#lod#tutorial

Первый лес в проекте на Godot обычно выглядит отлично — пока в него не зайдёшь. Двести деревьев спустя частота кадров падает вдвое, профайлер показывает тысячи вызовов отрисовки, и непонятно, какую настройку крутить. На форуме Godot один разработчик описал ровно это: самодельная система LOD для травы дала около 10 FPS и больше 3000 вызовов отрисовки на маленьком участке земли.

Хорошая новость: в Godot 4 есть всё, что нужно для большого и быстрого леса. Инструменты просто надо правильно сложить вместе. В этом руководстве они идут в том порядке, в каком важны, — бюджет, LOD, инстансинг, листва, тени, отсечение, — а в конце есть чек-лист.

Почему лес тормозит

Дерево — один из самых дорогих объектов в игре. Оно стоит дорого сразу в четырёх местах:

  1. Вызовы отрисовки (draw calls). Каждое дерево — минимум две сетки (кора и листва), часто с разными материалами. Сотня отдельных деревьев — это сотни вызовов отрисовки, а вы ещё ничего другого не нарисовали.
  2. Треугольники и вершины. У детального дерева легко 40 000+ треугольников. Для одного дерева крупным планом — нормально, для целого склона — смертельно.
  3. Перерисовка (overdraw). Листья — карточки с прозрачными пикселями. Видеокарта всё равно обсчитывает невидимые части, слой за слоем, для каждой карточки в кроне.
  4. Тени. Каждое дерево рисуется ещё раз — в карту теней, иногда несколько раз, по разу на каждый каскад.

Каждый приём ниже бьёт по одной или нескольким из этих статей расходов.

Шаг 1. Решите бюджет до того, как что-то строить

Самая полезная привычка — заранее решить, сколько треугольников дереву можно стоить на каждой дистанции, ещё до того, как вы его смоделируете или сгенерируете. Вот настоящий дуб из FoliMesh — пример того, как выглядит игровая цепочка:

Уровень Дистанция (пример) Треугольников
LOD0 0–30 м 14 490
LOD1 30–70 м 3 984
LOD2 70–150 м 1 154
Импостор 150 м и дальше 2

Важны не сами числа, а форма кривой: каждый уровень примерно в 3–4 раза легче предыдущего, а самые дальние деревья почти ничего не стоят. На мобильных делите всё на два-три. Большая часть леса далеко — значит, большая часть леса должна быть дешёвой.

Один и тот же дуб на уровнях LOD0, LOD1, LOD2 и LOD3 Одно дерево на четырёх уровнях детализации: силуэт тот же, треугольников всё меньше.

Шаг 2. Выберите механизм LOD: автоматический или диапазоны видимости

В Godot 4 две разные системы LOD, и неправильный выбор для деревьев — частый источник проблем.

Автоматический LOD сеток. При импорте сцен glTF, .blend или FBX Godot сам строит упрощённые версии каждой сетки через meshoptimizer и сам переключает их. По официальной документации, выбор зависит от ближайшей к камере точки ограничивающего объёма узла и от того, сколько пикселей займёт ошибка (Rendering → Mesh LOD → LOD Change → Threshold Pixels, по умолчанию 1 пиксель). У каждого узла есть ещё LOD Bias.

Это хорошо работает для камней, зданий и пропсов — сплошных поверхностей. Крона дерева устроена иначе: это сотни отдельных карточек листвы. Обычное упрощение на такое не рассчитано, и крона может поредеть или исказиться. Сама документация для LOD, сделанных вручную, советует другой инструмент.

Диапазоны видимости (HLOD). Здесь вы сами даёте сетки LOD и говорите каждой, на какой дистанции она видна. У каждого GeometryInstance3D есть:

  • visibility_range_begin / visibility_range_end — полоса дистанций, в которой узел рисуется (считается до центра его ограничивающего объёма);
  • visibility_range_begin_margin / visibility_range_end_margin — запас, чтобы уровень не мигал, когда камера стоит ровно на границе;
  • visibility_range_fade_mode — Disabled (мгновенная смена, быстрее всего), Self или Dependencies (плавное растворение; на время перехода включает прозрачную отрисовку, а это стоит производительности).

Для деревьев диапазоны видимости — надёжный выбор: уровни сделаны специально под листву, дистанции под вашим контролем, а на последнем уровне можно выключить тени или поставить импостор. Минимальная настройка для одного дерева:

# 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 рисует сколько угодно копий сетки за один вызов отрисовки.

Есть подвох, который удивляет почти всех. Документация говорит, что узлы, которые рисуют много сеток сразу, — MultiMeshInstance3D и частицы, — рисуют все экземпляры на одном уровне LOD, а отсечение может убрать только узел целиком, не отдельные экземпляры. Разработчик Godot подтвердил это в issue #76436: один MultiMesh на весь уровень — значит, ни LOD по каждому дереву, ни отсечения по пирамиде видимости и перекрытию.

Решение — чанки. Разбейте мир на сетку и дайте каждой клетке свой MultiMesh на каждый уровень LOD со своим диапазоном видимости. Дальние чанки показывают лёгкий LOD, ближние — детальный, а чанки за спиной камеры отсекаются целиком.

# ForestChunks.gd — строит MultiMesh по чанкам с диапазонами видимости
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 м.
  • Но и не слишком мелко. Каждый чанк добавляет вызовы отрисовки (по одному на уровень LOD и материал). Замеряйте: обычно несколько сотен чанков в кадре на ПК ещё нормально, на мобильных — намного меньше.

У MultiMesh нет коллизии. Добавляйте простые формы только там, куда может дойти игрок, — капсулу или цилиндр на ствол, никогда не крону.

Лес биома в редакторе FoliMesh Сотни деревьев-экземпляров: при правильном устройстве число вызовов отрисовки почти не растёт вместе с лесом.

Шаг 4. Листва: Alpha Scissor, а не смешивание

Именно на листве большинство лесов теряет производительность — и красоту тоже, если прозрачность настроена неправильно.

  • Alpha Scissor (вырез), а не Alpha. В материале поставьте Transparency → Alpha Scissor. Смешиваемая прозрачность требует сортировки, ломает глубину и тени и намного дороже на кроне из сотен перекрывающихся карточек.
  • Сгладьте края через alpha to coverage. С включённым MSAA поставьте Alpha Antialiasing → Alpha Edge Clip (или Blend) — края листьев станут мягкими без настоящей прозрачности.
  • Боритесь с перерисовкой формой карточки. Квадратная карточка вокруг круглой веточки тратит много пикселей впустую. Карточки, обрезанные по форме пучка листьев, обсчитывают куда меньше невидимых пикселей. Проверяйте режимом вьюпорта Display Overdraw: ярко-красные кроны — слишком много слоёв.
  • Уберите то, чего никто не видит. Пучки глубоко внутри кроны снаружи не видны; дальние LOD должны убирать их первыми.

Шаг 5. Тени: скрытый второй лес

Каждое дерево, которое отбрасывает тень, рисуется ещё раз — в карту теней. Три настройки дают большую часть выигрыша:

  • Ограничьте дальность теней. DirectionalLight3D → Directional Shadow → Max Distance. Дальше 100–200 м мало кто заметит, что у отдельных деревьев нет теней.
  • Выключите тени у дальних LOD. Поставьте Cast Shadow → Off у LOD2 и импосторов (для MultiMesh — у MultiMeshInstance3D этого уровня).
  • Меньше каскадов на слабом железе. Два каскада вместо четырёх — часто незаметная разница и ощутимая экономия.

Шаг 6. Отсечение перекрытых объектов и ветер

Occlusion culling помогает, когда лес закрывают крупные объекты: холмы, скалы, здания, сам рельеф. Включите его в Project Settings → Rendering → Occlusion Culling, добавьте OccluderInstance3D и запеките. Сами деревья — плохие заслоны, они все в дырах, поэтому пусть работу делает рельеф. Больше всего выигрывают MultiMesh по чанкам: целый скрытый чанк просто пропускается.

Ветер должен жить в вершинном шейдере и читать заранее посчитанные веса из сетки: ствол почти не двигается, толстые ветки качаются медленно, кончики листвы дрожат быстро. Считать это с нуля в шейдере дорого, прочитать два числа на вершину — почти бесплатно.

Шаг 7. Замеряйте на целевом железе

Никогда не оптимизируйте вслепую. В Godot есть всё нужное:

  • Debugger → Monitors: вызовы отрисовки, объекты и примитивы (треугольники) в кадре.
  • Debugger → Visual Profiler: сколько процессор и видеокарта тратят на каждую часть кадра.
  • Вьюпорт → Display Overdraw / Wireframe: куда уходят пиксели и треугольники.

Замеряйте до и после каждого изменения — на самом слабом устройстве, которое хотите поддерживать, а не только на своём рабочем ПК.

Чек-лист

  • У каждого дерева есть бюджет треугольников по дистанциям, каждый уровень в 3–4 раза легче.
  • Деревья используют диапазоны видимости со своими LOD; автоматический LOD — только для камней и пропсов.
  • Тысячи деревьев разбиты на MultiMesh по чанкам размером около дистанции LOD0.
  • Листва — Alpha Scissor плюс alpha to coverage; карточки повторяют форму пучков.
  • Дальность теней ограничена; дальние LOD и импосторы теней не отбрасывают.
  • Occlusion culling запечён по рельефу и крупным объектам.
  • Ветер — дешёвый вершинный шейдер, который читает веса из вершин.
  • Вызовы отрисовки и время кадра замерены на целевом устройстве.

Как здесь помогает FoliMesh

Мы сделали FoliMesh для своей большой игры, поэтому он выдаёт деревья, которые уже следуют этим правилам. Пакет для Godot — папка, которую достаточно перетащить в проект: GLB с уровнями LOD0, LOD1, LOD2…, построенными под ваши бюджеты треугольников, и готовая сцена .tscn, скрипт которой сам выставляет диапазоны видимости каждому уровню, шейдер ветра (изгиб ствола и качание веток хранятся во втором UV-канале сетки) и StaticBody3D с капсулой по стволу для коллизии. Карты нормалей уже в формате OpenGL, как ждёт Godot.

Любую модель перед импортом можно проверить нашими бесплатными инструментами в браузере: просмотр LOD, счётчик полигонов и валидатор glTF. Общую картину по инструментам для деревьев — в нашем сравнении генераторов 3D-деревьев.

Частые вопросы

Godot 4 делает LOD автоматически? Да — для импортированных сцен glTF, .blend, Collada и FBX он строит LOD сеток при импорте. Лучше всего это работает на сплошных поверхностях; для деревьев ручные или сгенерированные LOD с диапазонами видимости дают более предсказуемый результат.

Почему LOD не работает с моим MultiMesh? Godot выбирает один уровень LOD на весь MultiMeshInstance3D. Разбейте экземпляры на чанки — у каждого свой узел и свой диапазон видимости.

Сколько деревьев может нарисовать Godot? Это зависит от бюджета на дерево, а не от движка. С MultiMesh по чанкам, дешёвыми дальними LOD и импосторами десятки тысяч деревьев в кадре реальны на ПК; замеряйте на своём целевом устройстве.

Alpha Scissor или Alpha Hash для листвы? Обычный выбор — Alpha Scissor с alpha to coverage. Alpha Hash даёт мягкие края без MSAA, но выглядит шумно и лучше всего работает вместе с TAA.