FoliMesh
Blog

Ein Wald in Godot 4, der nicht die FPS killt: MultiMesh, LOD und Visibility Ranges

Ein Praxisleitfaden für schnelle Vegetation in Godot 4: Dreiecksbudgets, automatisches Mesh-LOD vs. Visibility Ranges, MultiMesh in Chunks, Blatttransparenz, Schatten, Occlusion Culling und wie man das Ergebnis misst.

·10 Min. Lesezeit#godot#optimization#lod#tutorial

Der erste Wald in einem Godot-Projekt sieht meistens großartig aus – bis man hineinläuft. Zweihundert Bäume später ist die Framerate halbiert, der Profiler zeigt Tausende Draw Calls, und niemand weiß genau, welche Einstellung man ändern soll. Ein Nutzer im Godot-Forum hat genau das beschrieben: Sein selbst gebautes Gras-LOD-System landete auf einem kleinen Stück Gelände bei etwa 10 FPS und mehr als 3.000 Draw Calls.

Die gute Nachricht: Godot 4 bringt alle Werkzeuge mit, die man für einen großen, schnellen Wald braucht. Man muss sie nur richtig kombinieren. Dieser Leitfaden geht sie in der Reihenfolge durch, auf die es ankommt – Budget, LOD, Instancing, Blätter, Schatten, Culling – und endet mit einer Checkliste.

Warum Wälder langsam sind

Ein Baum gehört zu den teuersten Objekten in einem Spiel. Er kostet an vier Stellen gleichzeitig:

  1. Draw Calls. Jeder Baum besteht aus mindestens zwei Meshes (Rinde und Blätter), oft mit unterschiedlichen Materialien. Hundert einzelne Bäume bedeuten Hunderte Draw Calls, bevor überhaupt etwas anderes gezeichnet wurde.
  2. Dreiecke und Vertices. Ein detaillierter Baum hat schnell 40.000+ Dreiecke. Für einen einzelnen Hero-Baum in Ordnung, für einen ganzen Hang tödlich.
  3. Overdraw. Blätter sind Karten mit transparenten Pixeln. Die GPU schattiert trotzdem auch die unsichtbaren Teile, Schicht für Schicht, für jede Karte in der Krone.
  4. Schatten. Jeder Baum wird noch einmal in die Shadow Map gezeichnet – manchmal mehrfach, einmal pro Schattenkaskade.

Jede der folgenden Techniken setzt an einem oder mehreren dieser Kostenpunkte an.

Schritt 1. Das Budget festlegen, bevor man irgendetwas baut

Die nützlichste Gewohnheit überhaupt: Lege fest, wie viele Dreiecke ein Baum auf welcher Distanz kosten darf, bevor du ihn modellierst oder generierst. Hier eine echte Eiche aus FoliMesh als Beispiel dafür, wie eine spielfertige LOD-Kette aussieht:

Stufe Distanz (Beispiel) Dreiecke
LOD0 0–30 m 14.490
LOD1 30–70 m 3.984
LOD2 70–150 m 1.154
Impostor 150 m + 2

Entscheidend sind nicht die genauen Zahlen, sondern die Form der Kurve: Jede Stufe ist ungefähr 3- bis 4-mal leichter als die vorherige, und die entferntesten Bäume kosten fast nichts. Auf Mobilgeräten teilst du alles durch zwei oder drei. Der größte Teil deines Waldes ist weit weg – also sollte der größte Teil deines Waldes billig sein.

Dieselbe Eiche in LOD0, LOD1, LOD2 und LOD3 Derselbe Baum in vier Detailstufen: Die Silhouette bleibt, die Zahl der Dreiecke sinkt.

Schritt 2. Den LOD-Mechanismus wählen: automatisches Mesh-LOD oder Visibility Ranges

Godot 4 bietet zwei verschiedene LOD-Systeme, und die falsche Wahl für Bäume ist eine häufige Quelle von Problemen.

Automatisches Mesh-LOD. Beim Import einer glTF-, .blend- oder FBX-Szene erzeugt Godot mit meshoptimizer vereinfachte Versionen jedes Meshes und schaltet selbstständig zwischen ihnen um. Laut offizieller Dokumentation richtet sich die Auswahl nach dem Punkt der Bounding Box des Nodes, der der Kamera am nächsten liegt, und danach, wie viele Pixel der Fehler abdecken würde (Rendering → Mesh LOD → LOD Change → Threshold Pixels, standardmäßig 1 Pixel). Zusätzlich hat jeder Node einen LOD Bias.

Das funktioniert gut für Felsen, Gebäude und Props – also durchgehende Oberflächen. Eine Baumkrone ist etwas anderes: Sie besteht aus Hunderten einzelner Blattkarten. Generische Vereinfachung ist dafür nicht gemacht, und das Ergebnis kann eine ausgedünnte, verzerrte Krone sein. Die Dokumentation selbst empfiehlt ein anderes Werkzeug, wenn man handgemachte LODs hat.

Visibility Ranges (HLOD). Hier lieferst du die LOD-Meshes selbst und legst für jedes fest, auf welcher Distanz es sichtbar ist. Jeder GeometryInstance3D hat:

  • visibility_range_begin / visibility_range_end – den Distanzbereich, in dem der Node gezeichnet wird (gemessen zum Mittelpunkt seiner Bounding Box);
  • visibility_range_begin_margin / visibility_range_end_margin – eine Pufferzone, damit die Stufe nicht flackert, wenn die Kamera genau auf der Grenze steht;
  • visibility_range_fade_mode – Disabled (sofortiger Wechsel, am schnellsten), Self oder Dependencies (Alpha-Überblendung; erzwingt während des Übergangs transparentes Rendering, was Performance kostet).

Für Bäume sind Visibility Ranges die verlässliche Wahl: Die LODs sind für Laub ausgelegt, die Distanzen hast du selbst in der Hand, und auf der letzten Stufe kannst du die Schatten abschalten oder auf einen Impostor wechseln. Ein minimales Setup für einen Baum:

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

Wenn der Wechsel sichtbar ist, stell zuerst sicher, dass die LODs dieselbe Silhouette behalten – das ist wichtiger als jede Fade-Einstellung.

Schritt 3. Viele Bäume: MultiMesh, aber in Chunks

Einzelne Nodes reichen für ein paar Dutzend Bäume. Für Tausende brauchst du MultiMesh: Ein einziger MultiMeshInstance3D zeichnet beliebig viele Kopien eines Meshes in einem Draw Call.

Es gibt einen Haken, der fast alle überrascht. Laut Dokumentation zeichnen Nodes, die viele Meshes auf einmal rendern – MultiMeshInstance3D und Partikel –, alle Instanzen in derselben LOD-Stufe, und Culling kann nur den Node als Ganzes entfernen, nicht einzelne Instanzen. Ein Godot-Maintainer hat das in Issue #76436 bestätigt: Ein MultiMesh für das ganze Level bedeutet kein LOD pro Baum und kein Frustum- oder Occlusion Culling.

Die Lösung sind Chunks. Teile die Welt in ein Raster und gib jeder Zelle für jede LOD-Stufe ein eigenes MultiMesh mit eigener Visibility Range. Ferne Chunks zeigen das leichte LOD, nahe Chunks das detaillierte, und Chunks hinter der Kamera werden komplett weggecullt.

# ForestChunks.gd — baut MultiMeshes in Chunks mit 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)

Zwei Praxisregeln:

  • Chunk-Größe nahe an deiner LOD0-Distanz. Die Distanz wird zum Mittelpunkt des Chunks gemessen, ein 100-m-Chunk würde also für einen Baum direkt neben dem Spieler LOD1 anzeigen. Bei LOD0 bis 35 m funktionieren Chunks von 24–32 m gut.
  • Aber auch nicht zu klein. Jeder Chunk bringt zusätzliche Draw Calls (einen pro LOD und Material). Miss nach: Ein paar Hundert Chunks auf dem Bildschirm sind auf dem Desktop meist noch in Ordnung, auf Mobilgeräten deutlich weniger.

MultiMesh hat keine Kollision. Füge einfache Shapes nur dort hinzu, wo der Spieler hinkommt – eine Kapsel oder einen Zylinder pro Stamm, niemals für die Krone.

Ein Biom-Wald im FoliMesh-Editor Hunderte instanzierte Bäume: Mit der richtigen Struktur bleiben die Draw Calls nahezu konstant, während der Wald wächst.

Schritt 4. Blätter: Alpha Scissor statt Alpha Blending

Bei den Blättern verlieren die meisten Wälder ihre Performance – und, wenn die Transparenz falsch eingestellt ist, auch den Großteil ihrer Schönheit.

  • Alpha Scissor (Cutout) statt Alpha verwenden. Setze im Material Transparency → Alpha Scissor. Geblendetes Alpha erzwingt Sortierung, bricht Tiefe und Schatten und ist bei einer Krone mit Hunderten überlappender Karten deutlich teurer.
  • Kanten mit Alpha to Coverage glätten. Mit aktiviertem MSAA setzt du Alpha Antialiasing → Alpha Edge Clip (oder Blend) und bekommst weiche Blattkanten ohne echte Transparenz.
  • Overdraw über die Kartenform bekämpfen. Eine quadratische Karte um einen runden Zweig verschwendet viele Pixel. Karten, die auf die Form des Blattclusters zugeschnitten sind, schattieren weit weniger unsichtbare Pixel. Prüfe das mit dem Viewport-Modus Display Overdraw: Knallrote Kronen bedeuten zu viele Schichten.
  • Entferne, was niemand sieht. Cluster tief im Inneren der Krone sind von außen unsichtbar; ferne LODs sollten sie als Erstes weglassen.

Schritt 5. Schatten: der versteckte zweite Wald

Jeder schattenwerfende Baum wird für die Shadow Map erneut gerendert. Drei Einstellungen bringen den Großteil der Einsparung:

  • Schattendistanz begrenzen. DirectionalLight3D → Directional Shadow → Max Distance. Jenseits von 100–200 m bemerken nur wenige Spieler fehlende Schatten einzelner Bäume.
  • Schatten auf fernen LODs abschalten. Setze Cast Shadow → Off auf LOD2 und auf Impostors (bei MultiMesh auf dem MultiMeshInstance3D der jeweiligen Stufe).
  • Auf schwacher Hardware weniger Kaskaden verwenden. Zwei statt vier Kaskaden sind oft eine unsichtbare Änderung und eine spürbare Einsparung.

Schritt 6. Occlusion Culling und Wind

Occlusion Culling hilft, wenn große Objekte den Wald verdecken: Hügel, Klippen, Gebäude, das Terrain selbst. Aktiviere es unter Project Settings → Rendering → Occlusion Culling, füge einen OccluderInstance3D hinzu und bake. Bäume selbst sind schlechte Occluder – sie sind voller Löcher –, also überlass die Arbeit dem Terrain. Am meisten profitieren MultiMeshes in Chunks: Ein ganzer verdeckter Chunk wird übersprungen.

Wind gehört in den Vertex-Shader und sollte vorberechnete Gewichte aus dem Mesh lesen: Der Stamm bewegt sich kaum, große Äste schwingen langsam, Blattspitzen flattern schnell. Das im Shader von Grund auf zu berechnen ist teuer; zwei Zahlen pro Vertex zu lesen ist praktisch kostenlos.

Schritt 7. Auf der Zielhardware messen

Optimiere nie blind. Godot liefert alles, was du brauchst:

  • Debugger → Monitors: Draw Calls im Frame, Objekte, Primitive (Dreiecke) im Frame.
  • Debugger → Visual Profiler: wie lange CPU und GPU für jeden Teil des Frames brauchen.
  • Viewport → Display Overdraw / Wireframe: wohin die Pixel und Dreiecke gehen.

Miss vor und nach jeder Änderung, und zwar auf dem schwächsten Gerät, das du unterstützen willst – nicht nur auf deinem Entwicklungsrechner.

Checkliste

  • Jeder Baum hat ein Dreiecksbudget pro Distanz, jede Stufe ist 3- bis 4-mal leichter.
  • Bäume nutzen Visibility Ranges mit eigenen LODs; automatisches Mesh-LOD nur für Felsen und Props.
  • Tausende Bäume sind auf MultiMeshes in Chunks verteilt, etwa so groß wie die LOD0-Distanz.
  • Blätter nutzen Alpha Scissor plus Alpha to Coverage; die Karten folgen der Form der Cluster.
  • Die Schattendistanz ist begrenzt; ferne LODs und Impostors werfen keine Schatten.
  • Occlusion Culling ist aus Terrain und großen Objekten gebakt.
  • Wind ist ein günstiger Vertex-Shader, der Gewichte pro Vertex liest.
  • Draw Calls und Frame-Zeit sind auf dem Zielgerät gemessen.

Wie FoliMesh hier hineinpasst

Wir haben FoliMesh für unser eigenes groß angelegtes Spiel gebaut, deshalb erzeugt es Bäume, die diese Regeln bereits befolgen. Das Godot-Paket ist ein Ordner, den du einfach ins Projekt ziehst: eine GLB mit LOD0, LOD1, LOD2…, generiert nach deinen Dreiecksbudgets, eine fertige .tscn-Szene, deren Skript Visibility Ranges für jede Stufe setzt, ein Wind-Shader (Stammbiegung und Astschwingen sind im zweiten UV-Kanal des Meshes gespeichert) und ein StaticBody3D mit Kapsel um den Stamm als Kollision. Die Normal Maps liegen bereits im OpenGL-Format vor, das Godot erwartet.

Mit unseren kostenlosen Browser-Tools kannst du jedes Modell vor dem Import prüfen: dem LOD-Viewer, dem Polygonzähler und dem glTF-Validator. Einen Überblick über Baum-Tools gibt unser Vergleich von 3D-Baumgeneratoren.

FAQ

Erstellt Godot 4 LODs automatisch? Ja – für importierte glTF-, .blend-, Collada- und FBX-Szenen erzeugt es beim Import Mesh-LODs. Das funktioniert am besten bei durchgehenden Oberflächen; bei Bäumen liefern handgemachte oder generierte LODs mit Visibility Ranges vorhersehbarere Ergebnisse.

Warum funktioniert LOD mit meinem MultiMesh nicht? Godot wählt eine einzige LOD-Stufe für den gesamten MultiMeshInstance3D. Teile die Instanzen in Chunks auf, jeder mit eigenem Node und eigener Visibility Range.

Wie viele Bäume kann Godot rendern? Das hängt vom Budget pro Baum ab, nicht von der Engine. Mit MultiMeshes in Chunks, günstigen fernen LODs und Impostors sind Zehntausende Bäume auf dem Bildschirm auf Desktop-Hardware realistisch; miss auf deinem Zielgerät.

Alpha Scissor oder Alpha Hash für Blätter? Alpha Scissor mit Alpha to Coverage ist die übliche Wahl. Alpha Hash liefert ohne MSAA weichere Kanten, wirkt aber verrauscht und funktioniert am besten mit TAA.