FoliMesh
Blog

Come creare una foresta in Godot 4 senza far crollare gli FPS: MultiMesh, LOD e visibility range

Una guida pratica alla vegetazione veloce in Godot 4: budget di triangoli, LOD automatico delle mesh vs visibility range, MultiMesh a chunk, trasparenza delle foglie, ombre, occlusion culling e come misurare il risultato.

·11 min di lettura#godot#optimization#lod#tutorial

La prima foresta di un progetto Godot di solito è bellissima — finché non ci entri dentro. Duecento alberi dopo il frame rate si dimezza, il profiler mostra migliaia di draw call e nessuno sa bene quale impostazione toccare. Un utente del forum di Godot ha descritto esattamente questo: un sistema di LOD per l'erba fatto in casa è finito a circa 10 FPS e più di 3.000 draw call su un piccolo appezzamento di terreno.

La buona notizia: Godot 4 ha tutti gli strumenti necessari per una foresta grande e veloce. Basta combinarli nel modo giusto. Questa guida li affronta nell'ordine che conta — budget, LOD, instancing, foglie, ombre, culling — e si chiude con una checklist.

Perché le foreste sono lente

Un albero è uno degli oggetti più costosi di un gioco. Pesa in quattro punti contemporaneamente:

  1. Draw call. Ogni albero è composto da almeno due mesh (corteccia e foglie), spesso con materiali diversi. Cento alberi separati significano centinaia di draw call prima ancora di aver disegnato qualsiasi altra cosa.
  2. Triangoli e vertici. Un albero dettagliato supera facilmente i 40.000 triangoli. Va bene per un singolo albero in primo piano, è fatale per un'intera collina.
  3. Overdraw. Le foglie sono card con pixel trasparenti. La GPU calcola comunque lo shading delle parti invisibili, strato dopo strato, per ogni card della chioma.
  4. Ombre. Ogni albero viene disegnato di nuovo nella shadow map — a volte più volte, una per ogni cascata d'ombra.

Ognuna delle tecniche seguenti attacca uno o più di questi costi.

Passo 1. Decidi il budget prima di costruire qualsiasi cosa

L'abitudine più utile in assoluto è stabilire quanti triangoli può costare un albero a ogni distanza prima di modellarlo o generarlo. Ecco una vera quercia di FoliMesh, come esempio di catena pronta per il gioco:

Livello Distanza (esempio) Triangoli
LOD0 0–30 m 14.490
LOD1 30–70 m 3.984
LOD2 70–150 m 1.154
Impostor 150 m + 2

Non contano tanto i numeri esatti quanto la forma della curva: ogni livello è circa 3–4 volte più leggero del precedente, e gli alberi più lontani non costano quasi nulla. Su mobile, dividi tutto per due o tre. La maggior parte della foresta è lontana, quindi la maggior parte della foresta deve essere economica.

La stessa quercia in LOD0, LOD1, LOD2 e LOD3 Lo stesso albero su quattro livelli di dettaglio: la silhouette resta, il numero di triangoli scende.

Passo 2. Scegli il meccanismo di LOD: LOD automatico delle mesh o visibility range

Godot 4 offre due sistemi di LOD diversi, e scegliere quello sbagliato per gli alberi è una fonte di problemi molto comune.

LOD automatico delle mesh. Quando importi una scena glTF, .blend o FBX, Godot genera versioni semplificate di ogni mesh con meshoptimizer e passa dall'una all'altra in autonomia. Secondo la documentazione ufficiale, la scelta si basa sul punto della bounding box del nodo più vicino alla telecamera e su quanti pixel coprirebbe l'errore (Rendering → Mesh LOD → LOD Change → Threshold Pixels, 1 pixel di default). Ogni nodo ha anche un LOD Bias.

Funziona bene per rocce, edifici e prop — superfici continue. La chioma di un albero è diversa: sono centinaia di card di foglie separate. La semplificazione generica non è pensata per questo, e il risultato può essere una chioma diradata e deformata. La documentazione stessa consiglia un altro strumento quando hai LOD fatti a mano.

Visibility range (HLOD). Qui le mesh dei LOD le fornisci tu e indichi per ciascuna a quale distanza è visibile. Ogni GeometryInstance3D ha:

  • visibility_range_begin / visibility_range_end — la fascia di distanza in cui il nodo viene disegnato (misurata fino al centro della sua bounding box);
  • visibility_range_begin_margin / visibility_range_end_margin — una zona cuscinetto perché il livello non sfarfalli quando la telecamera si trova proprio sul confine;
  • visibility_range_fade_mode — Disabled (cambio istantaneo, il più veloce), Self o Dependencies (dissolvenza in alpha; durante la transizione forza il rendering trasparente, che costa prestazioni).

Per gli alberi, le visibility range sono la scelta affidabile: i LOD sono progettati per il fogliame, le distanze sono sotto il tuo controllo e sull'ultimo livello puoi eliminare le ombre o passare a un impostor. Una configurazione minima per un albero:

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

Se il cambio si nota, prima di tutto assicurati che i LOD mantengano la stessa silhouette — conta più di qualsiasi impostazione di dissolvenza.

Passo 3. Tanti alberi: MultiMesh, ma a chunk

I nodi separati vanno bene per qualche decina di alberi. Per migliaia ti serve MultiMesh: un solo MultiMeshInstance3D disegna un numero qualsiasi di copie di una mesh in una sola draw call.

C'è un tranello che sorprende quasi tutti. La documentazione afferma che i nodi che disegnano molte mesh insieme — MultiMeshInstance3D e le particelle — disegnano tutte le istanze allo stesso livello di LOD, e il culling può eliminare solo il nodo nel suo insieme, non le singole istanze. Un maintainer di Godot lo ha confermato nella issue #76436: un unico MultiMesh per tutto il livello significa niente LOD per singolo albero e niente frustum né occlusion culling.

La soluzione sono i chunk. Dividi il mondo in una griglia e assegna a ogni cella un proprio MultiMesh per ogni livello di LOD, ciascuno con la sua visibility range. I chunk lontani mostrano il LOD leggero, quelli vicini quello dettagliato, e quelli dietro la telecamera vengono scartati del tutto.

# ForestChunks.gd — costruisce MultiMesh a chunk con visibility range
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)

Due regole pratiche:

  • Dimensione del chunk vicina alla distanza del LOD0. La distanza si misura fino al centro del chunk, quindi un chunk da 100 m mostrerebbe il LOD1 per un albero proprio accanto al giocatore. Con il LOD0 fino a 35 m, chunk da 24–32 m funzionano bene.
  • Ma nemmeno troppo piccoli. Ogni chunk aggiunge draw call (una per LOD per materiale). Misura: di solito qualche centinaio di chunk a schermo va ancora bene su desktop, molti meno su mobile.

MultiMesh non ha collisioni. Aggiungi forme semplici solo dove il giocatore può arrivare — una capsula o un cilindro per tronco, mai la chioma.

Una foresta di bioma nell'editor di FoliMesh Centinaia di alberi in instancing: con la struttura giusta le draw call restano quasi invariate mentre la foresta cresce.

Passo 4. Foglie: alpha scissor, non alpha blending

È sulle foglie che la maggior parte delle foreste perde le prestazioni — e gran parte della bellezza, se la trasparenza è impostata male.

  • Usa Alpha Scissor (cutout), non Alpha. Nel materiale imposta Transparency → Alpha Scissor. L'alpha in blending richiede l'ordinamento, rompe la profondità e le ombre ed è molto più costoso su una chioma con centinaia di card sovrapposte.
  • Ammorbidisci i bordi con l'alpha to coverage. Con l'MSAA attivo, imposta Alpha Antialiasing → Alpha Edge Clip (o Blend) per avere bordi delle foglie morbidi senza vera trasparenza.
  • Combatti l'overdraw con la forma della card. Una card quadrata attorno a un rametto tondo spreca un sacco di pixel. Le card ritagliate sulla forma del cluster di foglie calcolano lo shading di molti meno pixel invisibili. Controlla con la modalità Display Overdraw della viewport: chiome rosso acceso significano troppi strati.
  • Elimina ciò che nessuno vede. I cluster in profondità nella chioma sono invisibili dall'esterno; i LOD lontani dovrebbero eliminarli per primi.

Passo 5. Ombre: la seconda foresta nascosta

Ogni albero che proietta ombre viene renderizzato di nuovo per la shadow map. Tre impostazioni danno la maggior parte del risparmio:

  • Limita la distanza delle ombre. DirectionalLight3D → Directional Shadow → Max Distance. Oltre i 100–200 m pochi giocatori notano la mancanza delle ombre dei singoli alberi.
  • Disattiva le ombre sui LOD lontani. Imposta Cast Shadow → Off sul LOD2 e sugli impostor (per MultiMesh, sul MultiMeshInstance3D di quel livello).
  • Usa meno cascate sull'hardware debole. Due cascate invece di quattro sono spesso un cambiamento invisibile e un risparmio notevole.

Passo 6. Occlusion culling e vento

L'occlusion culling aiuta quando oggetti grandi nascondono la foresta: colline, scogliere, edifici, il terreno stesso. Attivalo in Project Settings → Rendering → Occlusion Culling, aggiungi un OccluderInstance3D ed esegui il bake. Gli alberi in sé sono pessimi occluder — sono pieni di buchi — quindi lascia lavorare il terreno. I MultiMesh a chunk ne beneficiano di più: un intero chunk nascosto viene saltato.

Il vento deve stare nel vertex shader e leggere pesi precalcolati dalla mesh: il tronco si muove appena, i rami grandi ondeggiano lentamente, le punte delle foglie vibrano veloci. Calcolare tutto questo da zero nello shader è costoso; leggere due numeri per vertice è quasi gratis.

Passo 7. Misura sull'hardware di destinazione

Non ottimizzare mai alla cieca. Godot ti dà tutto ciò che serve:

  • Debugger → Monitors: draw call nel frame, oggetti, primitive (triangoli) nel frame.
  • Debugger → Visual Profiler: quanto tempo CPU e GPU spendono su ogni parte del frame.
  • Viewport → Display Overdraw / Wireframe: dove finiscono pixel e triangoli.

Misura prima e dopo ogni modifica, sul dispositivo più debole che vuoi supportare — non solo sul tuo PC di sviluppo.

Checklist

  • Ogni albero ha un budget di triangoli per distanza, con ogni livello 3–4 volte più leggero.
  • Gli alberi usano visibility range con LOD propri; il LOD automatico delle mesh solo per rocce e prop.
  • Migliaia di alberi sono suddivisi in MultiMesh a chunk, grandi circa quanto la distanza del LOD0.
  • Le foglie usano Alpha Scissor più alpha to coverage; le card seguono la forma dei cluster.
  • La distanza delle ombre è limitata; LOD lontani e impostor non proiettano ombre.
  • L'occlusion culling è bakato a partire dal terreno e dagli oggetti grandi.
  • Il vento è un vertex shader economico che legge pesi per vertice.
  • Draw call e tempo di frame sono misurati sul dispositivo di destinazione.

Come si inserisce FoliMesh

Abbiamo creato FoliMesh per il nostro gioco su larga scala, quindi produce alberi che seguono già queste regole. Il pacchetto per Godot è una cartella da trascinare nel progetto: un GLB con LOD0, LOD1, LOD2… generati secondo i tuoi budget di triangoli, e una scena .tscn pronta il cui script imposta le visibility range per ogni livello, uno shader del vento (la flessione del tronco e l'oscillazione dei rami sono memorizzate nel secondo canale UV della mesh) e uno StaticBody3D con una capsula attorno al tronco per le collisioni. Le normal map sono già nel formato OpenGL che Godot si aspetta.

Puoi controllare qualsiasi modello prima di importarlo con i nostri strumenti gratuiti nel browser: il visualizzatore di LOD, il contapoligoni e il validatore glTF. Per una panoramica più ampia degli strumenti per alberi, leggi il nostro confronto tra generatori di alberi 3D.

FAQ

Godot 4 crea i LOD automaticamente? Sì — per le scene glTF, .blend, Collada e FBX importate genera i LOD delle mesh all'importazione. Funziona meglio sulle superfici continue; per gli alberi, LOD fatti a mano o generati, con le visibility range, danno risultati più prevedibili.

Perché il LOD non funziona con il mio MultiMesh? Godot seleziona un solo livello di LOD per l'intero MultiMeshInstance3D. Suddividi le istanze in chunk, ciascuno con il proprio nodo e la propria visibility range.

Quanti alberi può renderizzare Godot? Dipende dal budget per albero, non dal motore. Con MultiMesh a chunk, LOD lontani economici e impostor, decine di migliaia di alberi a schermo sono realistici su hardware desktop; misura sul tuo dispositivo di destinazione.

Alpha Scissor o Alpha Hash per le foglie? Alpha Scissor con alpha to coverage è la scelta abituale. Alpha Hash dà bordi più morbidi senza MSAA, ma ha un aspetto rumoroso e rende meglio con il TAA.