FoliMesh
Blog

Jak zbudować las w Godot 4, który nie zabije FPS: MultiMesh, LOD i visibility ranges

Praktyczny poradnik szybkiej roślinności w Godot 4: budżety trójkątów, automatyczny LOD siatek kontra visibility ranges, MultiMesh w chunkach, przezroczystość liści, cienie, occlusion culling i pomiar efektów.

·9 min czytania#godot#optimization#lod#tutorial

Pierwszy las w projekcie Godot zwykle wygląda świetnie — dopóki do niego nie wejdziesz. Dwieście drzew później liczba klatek spada o połowę, profiler pokazuje tysiące draw calli i nikt nie wie, które ustawienie zmienić. Jeden z użytkowników forum Godota opisał dokładnie taką sytuację: domowej roboty system LOD dla trawy skończył na około 10 FPS i ponad 3000 draw calli na niewielkim kawałku terenu.

Dobra wiadomość: Godot 4 ma wszystkie narzędzia potrzebne do dużego i szybkiego lasu. Trzeba je tylko odpowiednio połączyć. Ten poradnik omawia je w kolejności, która ma znaczenie — budżet, LOD, instancing, liście, cienie, culling — i kończy się checklistą.

Dlaczego lasy są wolne

Drzewo to jeden z najdroższych obiektów w grze. Kosztuje w czterech miejscach jednocześnie:

  1. Draw calle. Każde drzewo to co najmniej dwie siatki (kora i liście), często z różnymi materiałami. Sto osobnych drzew to setki draw calli, zanim narysujesz cokolwiek innego.
  2. Trójkąty i wierzchołki. Szczegółowe drzewo bez trudu przekracza 40 000 trójkątów. W porządku dla jednego drzewa na pierwszym planie, zabójcze dla całego zbocza.
  3. Overdraw. Liście to karty z przezroczystymi pikselami. GPU i tak cieniuje niewidoczne fragmenty, warstwa po warstwie, dla każdej karty w koronie.
  4. Cienie. Każde drzewo jest rysowane ponownie do shadow mapy — czasem kilka razy, raz na każdą kaskadę cieni.

Każda z poniższych technik atakuje jeden lub kilka z tych kosztów.

Krok 1. Ustal budżet, zanim cokolwiek zbudujesz

Najbardziej przydatny nawyk to ustalenie, ile trójkątów może kosztować drzewo na każdym dystansie, zanim je wymodelujesz lub wygenerujesz. Oto prawdziwy dąb z FoliMesh jako przykład łańcucha gotowego do gry:

Poziom Dystans (przykład) Trójkąty
LOD0 0–30 m 14 490
LOD1 30–70 m 3984
LOD2 70–150 m 1154
Impostor 150 m + 2

Ważne są nie konkretne liczby, lecz kształt krzywej: każdy poziom jest mniej więcej 3–4 razy lżejszy od poprzedniego, a najdalsze drzewa nie kosztują prawie nic. Na urządzeniach mobilnych podziel wszystko przez dwa lub trzy. Większość lasu jest daleko, więc większość lasu musi być tania.

Ten sam dąb w LOD0, LOD1, LOD2 i LOD3 To samo drzewo na czterech poziomach szczegółowości: sylwetka zostaje, liczba trójkątów spada.

Krok 2. Wybierz mechanizm LOD: automatyczny LOD siatek czy visibility ranges

Godot 4 oferuje dwa różne systemy LOD, a wybór niewłaściwego dla drzew to częste źródło problemów.

Automatyczny LOD siatek. Gdy importujesz scenę glTF, .blend lub FBX, Godot generuje uproszczone wersje każdej siatki za pomocą meshoptimizera i sam się między nimi przełącza. Według oficjalnej dokumentacji wybór zależy od punktu bounding boxa węzła najbliższego kamerze oraz od tego, ile pikseli zająłby błąd (Rendering → Mesh LOD → LOD Change → Threshold Pixels, domyślnie 1 piksel). Każdy węzeł ma też LOD Bias.

To dobrze działa dla skał, budynków i propów — powierzchni ciągłych. Korona drzewa jest inna: to setki osobnych kart z liśćmi. Ogólne upraszczanie nie było do tego projektowane, a efektem może być przerzedzona, zniekształcona korona. Sama dokumentacja zaleca inne narzędzie, gdy masz LOD-y wykonane ręcznie.

Visibility ranges (HLOD). Tutaj sam dostarczasz siatki LOD i określasz dla każdej, na jakim dystansie jest widoczna. Każdy GeometryInstance3D ma:

  • visibility_range_begin / visibility_range_end — przedział dystansu, w którym węzeł jest rysowany (mierzony do środka jego bounding boxa);
  • visibility_range_begin_margin / visibility_range_end_margin — strefę buforową, żeby poziom nie migotał, gdy kamera stoi dokładnie na granicy;
  • visibility_range_fade_mode — Disabled (natychmiastowe przełączenie, najszybsze), Self lub Dependencies (przenikanie przez alfę; na czas przejścia wymusza renderowanie przezroczyste, co kosztuje wydajność).

Dla drzew visibility ranges to pewny wybór: LOD-y są projektowane pod listowie, dystanse masz pod kontrolą, a na ostatnim poziomie możesz wyłączyć cienie albo przełączyć się na impostor. Minimalna konfiguracja dla jednego drzewa:

# Tree.gd — dzieci: 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

Jeśli przełączenie jest widoczne, najpierw upewnij się, że LOD-y zachowują tę samą sylwetkę — to ważniejsze niż jakiekolwiek ustawienia przenikania.

Krok 3. Dużo drzew: MultiMesh, ale w chunkach

Osobne węzły wystarczą dla kilkudziesięciu drzew. Dla tysięcy potrzebujesz MultiMesh: jeden MultiMeshInstance3D rysuje dowolną liczbę kopii siatki w jednym draw callu.

Jest tu haczyk, który zaskakuje prawie wszystkich. Dokumentacja mówi, że węzły rysujące wiele siatek naraz — MultiMeshInstance3D i cząsteczki — rysują wszystkie instancje na tym samym poziomie LOD, a culling może usunąć tylko cały węzeł, a nie pojedyncze instancje. Jeden z maintainerów Godota potwierdził to w issue #76436: jeden MultiMesh na cały poziom oznacza brak LOD dla poszczególnych drzew i brak frustum oraz occlusion cullingu.

Rozwiązaniem są chunki. Podziel świat na siatkę i daj każdej komórce własny MultiMesh dla każdego poziomu LOD, każdy z własnym visibility range. Dalekie chunki pokazują lekki LOD, bliskie — szczegółowy, a chunki za kamerą są całkowicie odrzucane.

# ForestChunks.gd — buduje MultiMeshe w chunkach z visibility ranges
extends Node3D

@export var lod_meshes: Array[Mesh] = []   # LOD0, LOD1, LOD2, impostor
@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)

Dwie praktyczne zasady:

  • Rozmiar chunka zbliżony do dystansu LOD0. Dystans jest mierzony do środka chunka, więc chunk o boku 100 m pokazałby LOD1 dla drzewa tuż obok gracza. Przy LOD0 do 35 m dobrze sprawdzają się chunki 24–32 m.
  • Ale nie za małe. Każdy chunk dodaje draw calle (jeden na LOD na materiał). Mierz: zwykle kilkaset chunków na ekranie to wciąż nic strasznego na desktopie, na urządzeniach mobilnych — dużo mniej.

MultiMesh nie ma kolizji. Dodawaj proste kształty tylko tam, gdzie gracz może dotrzeć — kapsułę lub walec na pień, nigdy na koronę.

Las biomu w edytorze FoliMesh Setki instancjonowanych drzew: właściwa struktura sprawia, że liczba draw calli prawie nie rośnie wraz z lasem.

Krok 4. Liście: alpha scissor, nie alpha blending

To na liściach większość lasów traci wydajność — i sporo urody, jeśli przezroczystość jest źle ustawiona.

  • Używaj Alpha Scissor (cutout), a nie Alpha. W materiale ustaw Transparency → Alpha Scissor. Alfa z blendingiem wymusza sortowanie, psuje głębię i cienie i jest dużo droższa w koronie z setkami nakładających się kart.
  • Wygładź krawędzie przez alpha to coverage. Przy włączonym MSAA ustaw Alpha Antialiasing → Alpha Edge Clip (lub Blend), aby uzyskać miękkie krawędzie liści bez prawdziwej przezroczystości.
  • Walcz z overdraw kształtem karty. Kwadratowa karta wokół okrągłej gałązki marnuje mnóstwo pikseli. Karty przycięte do kształtu kępy liści cieniują znacznie mniej niewidocznych pikseli. Sprawdź to trybem Display Overdraw w viewporcie: jaskrawoczerwone korony oznaczają zbyt wiele warstw.
  • Usuń to, czego nikt nie widzi. Kępy głęboko w koronie są niewidoczne z zewnątrz; dalekie LOD-y powinny pozbywać się ich w pierwszej kolejności.

Krok 5. Cienie: ukryty drugi las

Każde drzewo rzucające cień jest renderowane ponownie do shadow mapy. Trzy ustawienia dają większość oszczędności:

  • Ogranicz zasięg cieni. DirectionalLight3D → Directional Shadow → Max Distance. Powyżej 100–200 m niewielu graczy zauważy brak cieni pojedynczych drzew.
  • Wyłącz cienie na dalekich LOD-ach. Ustaw Cast Shadow → Off na LOD2 i na impostorach (w przypadku MultiMesh — na MultiMeshInstance3D danego poziomu).
  • Używaj mniej kaskad na słabszym sprzęcie. Dwie kaskady zamiast czterech to często niewidoczna zmiana i odczuwalna oszczędność.

Krok 6. Occlusion culling i wiatr

Occlusion culling pomaga, gdy las zasłaniają duże obiekty: wzgórza, klify, budynki, sam teren. Włącz go w Project Settings → Rendering → Occlusion Culling, dodaj OccluderInstance3D i zrób bake. Same drzewa są słabymi okluderami — są pełne dziur — więc niech robotę wykona teren. Najwięcej zyskują MultiMeshe w chunkach: cały zasłonięty chunk jest pomijany.

Wiatr powinien działać w vertex shaderze i odczytywać z siatki wstępnie obliczone wagi: pień prawie się nie rusza, duże gałęzie kołyszą się powoli, końcówki liści drgają szybko. Liczenie tego od zera w shaderze jest drogie; odczyt dwóch liczb na wierzchołek jest niemal darmowy.

Krok 7. Mierz na docelowym sprzęcie

Nigdy nie optymalizuj na ślepo. Godot daje ci wszystko, czego potrzebujesz:

  • Debugger → Monitors: draw calle w klatce, obiekty, prymitywy (trójkąty) w klatce.
  • Debugger → Visual Profiler: ile czasu CPU i GPU spędzają na każdej części klatki.
  • Viewport → Display Overdraw / Wireframe: gdzie idą piksele i trójkąty.

Mierz przed każdą zmianą i po niej, na najsłabszym urządzeniu, które chcesz wspierać — nie tylko na swoim komputerze deweloperskim.

Checklista

  • Każde drzewo ma budżet trójkątów na dystans, a każdy poziom jest 3–4 razy lżejszy.
  • Drzewa używają visibility ranges z własnymi LOD-ami; automatyczny LOD siatek tylko dla skał i propów.
  • Tysiące drzew podzielone są na MultiMeshe w chunkach, mniej więcej wielkości dystansu LOD0.
  • Liście używają Alpha Scissor plus alpha to coverage; karty podążają za kształtem kęp.
  • Zasięg cieni jest ograniczony; dalekie LOD-y i impostory nie rzucają cieni.
  • Occlusion culling jest wypiekany z terenu i dużych obiektów.
  • Wiatr to tani vertex shader odczytujący wagi dla każdego wierzchołka.
  • Draw calle i czas klatki są zmierzone na urządzeniu docelowym.

Gdzie w tym wszystkim jest FoliMesh

Stworzyliśmy FoliMesh dla naszej własnej gry o dużej skali, więc generuje drzewa, które już przestrzegają tych zasad. Paczka dla Godota to folder, który wrzucasz do projektu: GLB z LOD0, LOD1, LOD2… wygenerowanymi według twoich budżetów trójkątów oraz gotowa scena .tscn, której skrypt ustawia visibility ranges dla każdego poziomu, shader wiatru (zgięcie pnia i kołysanie gałęzi zapisane są w drugim kanale UV siatki) i StaticBody3D z kapsułą wokół pnia do kolizji. Normal mapy są już w formacie OpenGL, którego oczekuje Godot.

Każdy model możesz sprawdzić przed importem naszymi darmowymi narzędziami w przeglądarce: przeglądarką LOD, licznikiem polygonów i walidatorem glTF. Szerszy obraz narzędzi do drzew znajdziesz w naszym porównaniu generatorów drzew 3D.

FAQ

Czy Godot 4 tworzy LOD-y automatycznie? Tak — dla importowanych scen glTF, .blend, Collada i FBX generuje LOD-y siatek przy imporcie. Najlepiej działa to na powierzchniach ciągłych; dla drzew przewidywalniejsze wyniki dają LOD-y wykonane ręcznie lub wygenerowane, w połączeniu z visibility ranges.

Dlaczego LOD nie działa z moim MultiMeshem? Godot wybiera jeden poziom LOD dla całego MultiMeshInstance3D. Podziel instancje na chunki, każdy z własnym węzłem i własnym visibility range.

Ile drzew Godot jest w stanie wyrenderować? To zależy od budżetu na drzewo, a nie od silnika. Z MultiMeshami w chunkach, tanimi dalekimi LOD-ami i impostorami dziesiątki tysięcy drzew na ekranie są realne na sprzęcie desktopowym; zmierz na swoim urządzeniu docelowym.

Alpha Scissor czy Alpha Hash dla liści? Standardowym wyborem jest Alpha Scissor z alpha to coverage. Alpha Hash daje miękkie krawędzie bez MSAA, ale wygląda na zaszumiony i najlepiej sprawdza się z TAA.