FoliMesh
Blog

Créer une forêt dans Godot 4 sans tuer vos FPS : MultiMesh, LOD et visibility ranges

Un guide pratique pour une végétation rapide dans Godot 4 : budgets de triangles, LOD de mesh automatique ou visibility ranges, MultiMesh par chunks, transparence des feuilles, ombres, occlusion culling et mesure des résultats.

·12 min de lecture#godot#optimization#lod#tutorial

La première forêt d'un projet Godot est généralement superbe… jusqu'à ce qu'on marche dedans. Deux cents arbres plus tard, le framerate est divisé par deux, le profiler affiche des milliers de draw calls et personne ne sait vraiment quel paramètre changer. Un utilisateur du forum Godot a décrit exactement ce cas : un système de LOD maison pour l'herbe a fini à environ 10 FPS et plus de 3 000 draw calls sur un petit bout de terrain.

La bonne nouvelle : Godot 4 dispose de tous les outils nécessaires pour une forêt grande et rapide. Il faut simplement les combiner correctement. Ce guide les passe en revue dans l'ordre qui compte — budget, LOD, instancing, feuilles, ombres, culling — et se termine par une checklist.

Pourquoi les forêts sont lentes

Un arbre est l'un des objets les plus coûteux d'un jeu. Il coûte à quatre endroits à la fois :

  1. Draw calls. Chaque arbre compte au moins deux meshes (écorce et feuillage), souvent avec des matériaux différents. Cent arbres séparés, c'est des centaines de draw calls avant même d'avoir dessiné quoi que ce soit d'autre.
  2. Triangles et vertices. Un arbre détaillé dépasse facilement 40 000 triangles. Acceptable pour un arbre « héros », fatal pour toute une colline.
  3. Overdraw. Les feuilles sont des cartes avec des pixels transparents. Le GPU calcule quand même les parties invisibles, couche après couche, pour chaque carte de la couronne.
  4. Ombres. Chaque arbre est redessiné dans la shadow map — parfois plusieurs fois, une par cascade d'ombres.

Chaque technique ci-dessous s'attaque à un ou plusieurs de ces coûts.

Étape 1. Fixer le budget avant de construire quoi que ce soit

L'habitude la plus utile consiste à décider combien de triangles un arbre peut coûter à chaque distance avant de le modéliser ou de le générer. Voici un vrai chêne issu de FoliMesh, à titre d'exemple de chaîne prête pour le jeu :

Niveau Distance (exemple) Triangles
LOD0 0–30 m 14 490
LOD1 30–70 m 3 984
LOD2 70–150 m 1 154
Impostor 150 m + 2

L'important n'est pas la valeur exacte des chiffres, mais la forme de la courbe : chaque niveau est environ 3 à 4 fois plus léger que le précédent, et les arbres les plus lointains ne coûtent presque rien. Sur mobile, divisez tout par deux ou trois. La majeure partie de votre forêt est loin, donc la majeure partie de votre forêt doit être bon marché.

Le même chêne en LOD0, LOD1, LOD2 et LOD3 Le même arbre sur quatre niveaux de détail : la silhouette reste, le nombre de triangles baisse.

Étape 2. Choisir le mécanisme de LOD : LOD de mesh automatique ou visibility ranges

Godot 4 propose deux systèmes de LOD différents, et choisir le mauvais pour les arbres est une source fréquente de problèmes.

LOD de mesh automatique. Lorsque vous importez une scène glTF, .blend ou FBX, Godot génère des versions simplifiées de chaque mesh avec meshoptimizer et bascule entre elles tout seul. D'après la documentation officielle, le choix dépend du point de la bounding box du nœud le plus proche de la caméra et du nombre de pixels que couvrirait l'erreur (Rendering → Mesh LOD → LOD Change → Threshold Pixels, 1 pixel par défaut). Chaque nœud dispose aussi d'un LOD Bias.

Cela fonctionne bien pour les rochers, les bâtiments et les props — des surfaces continues. Une couronne d'arbre, c'est autre chose : des centaines de cartes de feuilles séparées. La simplification générique n'a pas été conçue pour ça, et le résultat peut être une couronne clairsemée et déformée. La documentation elle-même recommande un autre outil si vous avez des LOD faits à la main.

Visibility ranges (HLOD). Ici, vous fournissez vous-même les meshes de LOD et indiquez à quelle distance chacun est visible. Chaque GeometryInstance3D possède :

  • visibility_range_begin / visibility_range_end — la plage de distance dans laquelle le nœud est dessiné (mesurée jusqu'au centre de sa bounding box) ;
  • visibility_range_begin_margin / visibility_range_end_margin — une zone tampon pour que le niveau ne scintille pas quand la caméra se trouve pile sur la limite ;
  • visibility_range_fade_mode — Disabled (bascule instantanée, la plus rapide), Self ou Dependencies (fondu alpha ; il force un rendu transparent pendant la transition, ce qui coûte en performances).

Pour les arbres, les visibility ranges sont le choix fiable : les LOD sont conçus pour le feuillage, les distances sont sous votre contrôle, et vous pouvez couper les ombres ou passer à un impostor au dernier niveau. Une configuration minimale pour un arbre :

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

Si la transition se voit, vérifiez d'abord que les LOD conservent la même silhouette — c'est plus important que n'importe quel réglage de fondu.

Étape 3. Beaucoup d'arbres : MultiMesh, mais par chunks

Des nœuds séparés suffisent pour quelques dizaines d'arbres. Pour des milliers, il vous faut MultiMesh : un seul MultiMeshInstance3D dessine autant de copies d'un mesh que vous voulez en un seul draw call.

Il y a un piège qui surprend presque tout le monde. La documentation indique que les nœuds qui dessinent beaucoup de meshes à la fois — MultiMeshInstance3D et les particules — dessinent toutes les instances au même niveau de LOD, et que le culling ne peut retirer que le nœud entier, pas des instances individuelles. Un mainteneur de Godot l'a confirmé dans l'issue #76436 : un seul MultiMesh pour tout le niveau, c'est pas de LOD par arbre et pas de frustum ni d'occlusion culling.

La solution, ce sont les chunks. Découpez le monde en grille et donnez à chaque cellule son propre MultiMesh pour chaque niveau de LOD, chacun avec sa propre visibility range. Les chunks lointains affichent le LOD léger, les chunks proches le LOD détaillé, et les chunks derrière la caméra sont entièrement cullés.

# ForestChunks.gd — construit des MultiMesh par chunks avec 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)

Deux règles pratiques :

  • Une taille de chunk proche de votre distance de LOD0. La distance est mesurée jusqu'au centre du chunk, donc un chunk de 100 m afficherait le LOD1 pour un arbre juste à côté du joueur. Avec un LOD0 jusqu'à 35 m, des chunks de 24–32 m fonctionnent bien.
  • Pas trop petits non plus. Chaque chunk ajoute des draw calls (un par LOD et par matériau). Mesurez : en général, quelques centaines de chunks à l'écran passent encore bien sur desktop, beaucoup moins sur mobile.

MultiMesh n'a pas de collision. Ajoutez des formes simples uniquement là où le joueur peut aller — une capsule ou un cylindre par tronc, jamais pour la couronne.

Une forêt de biome dans l'éditeur FoliMesh Des centaines d'arbres instanciés : avec la bonne structure, le nombre de draw calls reste presque stable à mesure que la forêt grandit.

Étape 4. Les feuilles : alpha scissor, pas alpha blending

C'est sur les feuilles que la plupart des forêts perdent leurs performances — et l'essentiel de leur beauté, si la transparence est mal configurée.

  • Utilisez Alpha Scissor (cutout), pas Alpha. Dans le matériau, réglez Transparency → Alpha Scissor. L'alpha mélangé impose un tri, casse la profondeur et les ombres, et coûte bien plus cher sur une couronne aux centaines de cartes superposées.
  • Adoucissez les bords avec l'alpha to coverage. Avec le MSAA activé, réglez Alpha Antialiasing → Alpha Edge Clip (ou Blend) pour obtenir des bords de feuilles doux sans vraie transparence.
  • Combattez l'overdraw par la forme des cartes. Une carte carrée autour d'un rameau arrondi gaspille beaucoup de pixels. Des cartes découpées selon la forme de la grappe de feuilles calculent bien moins de pixels invisibles. Vérifiez avec le mode Display Overdraw du viewport : des couronnes rouge vif signifient trop de couches.
  • Supprimez ce que personne ne voit. Les grappes situées au cœur de la couronne sont invisibles de l'extérieur ; les LOD lointains doivent les retirer en premier.

Étape 5. Les ombres : la seconde forêt cachée

Chaque arbre qui projette une ombre est rendu une nouvelle fois pour la shadow map. Trois réglages apportent l'essentiel des gains :

  • Limitez la distance des ombres. DirectionalLight3D → Directional Shadow → Max Distance. Au-delà de 100–200 m, peu de joueurs remarquent l'absence d'ombre d'un arbre isolé.
  • Coupez les ombres sur les LOD lointains. Réglez Cast Shadow → Off sur le LOD2 et sur les impostors (pour MultiMesh — sur le MultiMeshInstance3D du niveau concerné).
  • Utilisez moins de cascades sur le matériel modeste. Deux cascades au lieu de quatre, c'est souvent un changement invisible et un gain notable.

Étape 6. Occlusion culling et vent

L'occlusion culling aide quand de gros objets cachent la forêt : collines, falaises, bâtiments, le terrain lui-même. Activez-le dans Project Settings → Rendering → Occlusion Culling, ajoutez un OccluderInstance3D et lancez le bake. Les arbres eux-mêmes sont de mauvais occluders — ils sont pleins de trous —, alors laissez le terrain faire le travail. Ce sont les MultiMesh par chunks qui en profitent le plus : un chunk entièrement caché est ignoré.

Le vent doit vivre dans le vertex shader et lire des poids précalculés stockés dans le mesh : le tronc bouge à peine, les grosses branches oscillent lentement, les pointes des feuilles frémissent vite. Calculer tout cela de zéro dans le shader coûte cher ; lire deux nombres par vertex est quasiment gratuit.

Étape 7. Mesurer sur le matériel cible

N'optimisez jamais à l'aveugle. Godot vous donne tout ce qu'il faut :

  • Debugger → Monitors : draw calls dans la frame, objets, primitives (triangles) dans la frame.
  • Debugger → Visual Profiler : le temps que le CPU et le GPU passent sur chaque partie de la frame.
  • Viewport → Display Overdraw / Wireframe : où partent les pixels et les triangles.

Mesurez avant et après chaque changement, sur l'appareil le plus faible que vous voulez prendre en charge — pas seulement sur votre PC de développement.

Checklist

  • Chaque arbre a un budget de triangles par distance, chaque niveau étant 3 à 4 fois plus léger.
  • Les arbres utilisent des visibility ranges avec leurs propres LOD ; le LOD de mesh automatique est réservé aux rochers et aux props.
  • Les milliers d'arbres sont répartis dans des MultiMesh par chunks, d'une taille proche de la distance de LOD0.
  • Les feuilles utilisent Alpha Scissor et l'alpha to coverage ; les cartes suivent la forme des grappes.
  • La distance des ombres est limitée ; les LOD lointains et les impostors ne projettent pas d'ombre.
  • L'occlusion culling est baké à partir du terrain et des grands objets.
  • Le vent est un vertex shader léger qui lit des poids par vertex.
  • Les draw calls et le temps de frame sont mesurés sur l'appareil cible.

Où FoliMesh intervient

Nous avons créé FoliMesh pour notre propre jeu à grande échelle ; il produit donc des arbres qui respectent déjà ces règles. Le package Godot est un dossier à déposer dans le projet : un GLB avec LOD0, LOD1, LOD2… généré selon vos budgets de triangles, une scène .tscn prête à l'emploi dont le script règle les visibility ranges de chaque niveau, un shader de vent (la flexion du tronc et le balancement des branches sont stockés dans le second canal UV du mesh) et un StaticBody3D avec une capsule autour du tronc pour la collision. Les normal maps sont déjà au format OpenGL qu'attend Godot.

Vous pouvez vérifier n'importe quel modèle avant de l'importer grâce à nos outils gratuits dans le navigateur : le visualiseur de LOD, le compteur de polygones et le validateur glTF. Pour une vue d'ensemble des outils de création d'arbres, consultez notre comparatif des générateurs d'arbres 3D.

FAQ

Godot 4 crée-t-il les LOD automatiquement ? Oui — pour les scènes glTF, .blend, Collada et FBX importées, il génère des LOD de mesh à l'import. Cela fonctionne au mieux sur des surfaces continues ; pour les arbres, des LOD faits main ou générés, combinés aux visibility ranges, donnent des résultats plus prévisibles.

Pourquoi le LOD ne fonctionne-t-il pas avec mon MultiMesh ? Godot choisit un seul niveau de LOD pour tout le MultiMeshInstance3D. Répartissez les instances en chunks, chacun avec son propre nœud et sa propre visibility range.

Combien d'arbres Godot peut-il afficher ? Cela dépend du budget par arbre, pas du moteur. Avec des MultiMesh par chunks, des LOD lointains légers et des impostors, des dizaines de milliers d'arbres à l'écran sont réalistes sur une machine de bureau ; mesurez sur votre appareil cible.

Alpha Scissor ou Alpha Hash pour les feuilles ? Alpha Scissor avec l'alpha to coverage est le choix habituel. Alpha Hash donne des bords plus doux sans MSAA, mais paraît bruité et fonctionne mieux avec le TAA.