FoliMesh
Blog

Alberi e foreste in three.js: LOD, instancing, KTX2 e vento — una guida pratica

Come inserire una vera foresta in un gioco web con three.js senza far crollare il frame rate né gonfiare il download: GLB pronto per il gioco, compressione, THREE.LOD, InstancedMesh e BatchedMesh, foglie in cutout, ombre e uno shader del vento economico.

·9 min di lettura#threejs#webgl#optimization#lod#tutorial

Sempre più giochi nascono per il browser — e una parte sempre maggiore del loro codice viene scritta insieme ad assistenti di IA, che per default scelgono three.js. La Vibe Jam 2026, per esempio, chiedeva giochi per browser che partissero senza download e consigliava three.js. Prima o poi ognuno di questi giochi vuole una foresta. E una foresta è di solito la prima cosa che trasforma 60 FPS in 15 e una pagina da 5 MB in una da 150 MB.

Il web è la piattaforma più severa di tutte: portatili e telefoni poco potenti, una GPU condivisa con il browser, e ogni megabyte va scaricato prima che il giocatore veda qualcosa. Questa guida mostra come costruire una vegetazione che sopravviva a questi limiti — passo dopo passo, con codice per la versione attuale di three.js.

Il budget del web

Prima di scrivere codice, fissa dei limiti. Sono punti di partenza per un portatile o un telefono di fascia media, non leggi — misura la tua scena:

Cosa Obiettivo comodo
Draw call per frame al massimo qualche centinaio
Triangoli per frame 0,5–2 milioni
Un albero, livello vicino 5.000–15.000 triangoli
Un albero, livello lontano meno di 1.500 triangoli; impostor — 2
Download dell'intera foresta pochi MB, non decine

L'idea chiave è la stessa di qualsiasi motore: la maggior parte della foresta è lontana, quindi la maggior parte della foresta deve essere economica. Ogni livello di dettaglio dovrebbe essere circa 3–4 volte più leggero del precedente.

La stessa quercia in LOD0, LOD1, LOD2 e LOD3 Un albero, quattro livelli di dettaglio: la silhouette resta, il costo scende.

Passo 1. Parti da un GLB pronto per il gioco

Un albero per un gioco non è solo una bella mesh. Prima di scrivere anche una sola riga di codice di rendering, verifica che il file abbia:

  • una catena di LOD con la stessa silhouette (e idealmente un impostor per il livello più lontano);
  • foglie come card raggruppate in cluster, non migliaia di foglie separate né una massa compatta a "broccolo";
  • il pivot alla base del tronco e scala reale in metri;
  • una collisione semplice — una capsula per il tronco, non per la chioma;
  • pesi del vento memorizzati per vertice.

Puoi controllare tutto questo nel browser con strumenti gratuiti: il contapoligoni, il visualizzatore di LOD e il validatore glTF.

Passo 2. Rendi il file leggero: meshopt e KTX2

Due compressioni riducono un albero tipico di diverse volte:

  • Geometria — compressione meshopt (o Draco).
  • Texture — KTX2 (Basis Universal). A differenza di PNG o JPEG, KTX2 resta compresso in memoria video, quindi fa risparmiare sia sul download sia sulla VRAM.

Puoi farlo nel browser con il nostro compressore glTF e il convertitore KTX2, oppure da riga di comando con glTF Transform:

npx @gltf-transform/cli optimize tree.glb tree.min.glb --compress meshopt --texture-compress ktx2

(La compressione KTX2 nella CLI richiede KTX-Software installato; --texture-compress webp funziona anche senza.)

Caricare un file compresso in three.js:

import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';

const ktx2 = new KTX2Loader().setTranscoderPath('/basis/').detectSupport(renderer);
const loader = new GLTFLoader().setKTX2Loader(ktx2).setMeshoptDecoder(MeshoptDecoder);

const gltf = await loader.loadAsync('/models/oak.min.glb');

Passo 3. Pochi alberi: THREE.LOD

Per una manciata di alberi vicino al giocatore — la piazza di un villaggio, un albero in primo piano — basta il THREE.LOD integrato. Mostra un figlio in base alla distanza dalla telecamera; il terzo argomento è un'isteresi che evita lo sfarfallio sul confine:

const lod = new THREE.LOD();
lod.addLevel(lod0, 0);
lod.addLevel(lod1, 30, 0.1);
lod.addLevel(lod2, 70, 0.1);
lod.addLevel(impostor, 150, 0.1);
lod.position.set(x, 0, z);
scene.add(lod);

THREE.LOD si aggiorna automaticamente quando il renderer disegna la scena. Il suo limite è che ogni albero resta un oggetto separato — draw call separate per corteccia e foglie. Oltre qualche decina di alberi ti serve l'instancing.

Passo 4. Tanti alberi: instancing, a chunk

InstancedMesh disegna migliaia di copie di una mesh in una sola draw call. Ma three.js la tratta come un unico oggetto: viene scartata dal culling nel suo insieme e non ha LOD — tutte le istanze vengono disegnate con lo stesso dettaglio, anche quelle a 500 m o dietro la telecamera.

La soluzione è la stessa di Godot e Unity: dividi il mondo in chunk e crea una InstancedMesh per chunk, per livello di LOD e per materiale. Così ogni chunk sceglie il suo livello in base alla distanza:

const CHUNK = 32; // metri — vicino alla distanza del LOD0
const STARTS = [0, 35, 80, 160]; // dove inizia ogni LOD
const CULL = 400;

function buildChunk(trees, lodMeshes) {
  // lodMeshes[i] = [barkMesh, leavesMesh] presi dal GLB
  const levels = lodMeshes.map((parts) => parts.map((src) => {
    const im = new THREE.InstancedMesh(src.geometry, src.material, trees.length);
    trees.forEach((m, k) => im.setMatrixAt(k, m));
    im.computeBoundingSphere();
    return im;
  }));
  const group = new THREE.Group();
  levels.flat().forEach((im) => group.add(im));
  group.userData.levels = levels;
  group.userData.center = centerOf(trees); // un THREE.Vector3
  return group;
}

function updateChunk(group, camera) {
  const d = camera.position.distanceTo(group.userData.center);
  group.userData.levels.forEach((parts, i) => {
    const visible = d >= STARTS[i] && d < (STARTS[i + 1] ?? CULL);
    parts.forEach((im) => { im.visible = visible; });
  });
}

Chiama updateChunk per ogni chunk qualche volta al secondo invece che a ogni frame — le distanze cambiano lentamente. La dimensione del chunk conta: la distanza si misura fino al centro del chunk, quindi chunk molto più grandi della fascia del LOD0 mostrerebbero alberi grossolani proprio accanto al giocatore.

BatchedMesh è l'alternativa moderna. Disegna molte geometrie diverse con un solo materiale, fa il culling di ogni oggetto separatamente (perObjectFrustumCulled è attivo di default) e permette di cambiare la geometria di qualsiasi istanza con setGeometryIdAt — che è esattamente ciò che fa un cambio di LOD:

const bark = new THREE.BatchedMesh(maxTrees, maxVerts, maxIndices, barkMaterial);
const lodIds = barkLods.map((g) => bark.addGeometry(g));

for (const t of trees) {
  t.id = bark.addInstance(lodIds[0]);
  bark.setMatrixAt(t.id, t.matrix);
}

// qualche volta al secondo:
for (const t of trees) {
  const d = camera.position.distanceTo(t.position);
  const lod = STARTS.findLastIndex((s) => d >= s);
  bark.setGeometryIdAt(t.id, lodIds[lod]);
}

Usa una BatchedMesh per materiale (corteccia, foglie). Se preferisci una soluzione pronta, le librerie della community @three.ez/instanced-mesh (culling e LOD per istanza con l'instancing) e @three.ez/batched-mesh-extensions (LOD per BatchedMesh) fanno questo lavoro al posto tuo.

Un'altra scorciatoia: se il tuo file usa l'estensione glTF EXT_mesh_gpu_instancing, GLTFLoader crea gli oggetti InstancedMesh per te — un'intera foresta in un solo file.

Una foresta nell'editor di FoliMesh Una foresta di alberi in instancing: il numero di draw call cresce appena all'aumentare degli alberi.

Passo 5. Foglie: cutout, non trasparenza

La maggior parte delle foreste lente sul web è lenta a causa delle foglie.

  • Usa alphaTest, non transparent: true. I materiali trasparenti vengono ordinati e miscelati, non scrivono la profondità e moltiplicano l'overdraw. Un cutout è molto più economico:
leaves.material.transparent = false;
leaves.material.alphaTest = 0.5;
leaves.material.side = THREE.DoubleSide;
leaves.material.alphaToCoverage = true; // bordi morbidi quando il renderer ha antialias: true
  • Attenzione alla forma delle card. Una card quadrata attorno a un cluster di foglie tondo calcola lo shading di molti pixel invisibili. Le card ritagliate sulla silhouette del cluster fanno risparmiare molto fill rate — e sui telefoni il fill rate è spesso il vero limite.
  • Abbassa la risoluzione sui dispositivi deboli. renderer.setPixelRatio(Math.min(devicePixelRatio, 1.5)) è spesso il singolo guadagno più grande per il fogliame sui telefoni ad alta densità di pixel.

Passo 6. Ombre e vento

Le ombre disegnano la foresta una seconda volta. Tieni la shadow camera stretta attorno al giocatore, usa uno shadow.mapSize moderato e lascia proiettare ombre solo ai livelli vicini:

lod2Leaves.castShadow = false;
impostor.castShadow = false;

Il vento va nel vertex shader. Se il modello memorizza i pesi del vento per vertice — per esempio la flessione del tronco in x e l'oscillazione dei rami in y del secondo canale UV — puoi aggiungere movimento a un materiale standard con onBeforeCompile:

const time = { value: 0 };
geometry.setAttribute('wind', geometry.getAttribute('uv1')); // TEXCOORD_1 dal GLB

material.onBeforeCompile = (shader) => {
  shader.uniforms.uTime = time;
  shader.vertexShader = shader.vertexShader
    .replace('#include <common>', '#include <common>\nattribute vec2 wind;\nuniform float uTime;')
    .replace('#include <begin_vertex>', `#include <begin_vertex>
      float sway = sin(uTime * 1.3 + position.y * 0.2);
      transformed.x += (sway * wind.x * 0.25 + sin(uTime * 2.7 + position.x) * wind.y * 0.1);
      transformed.z += sway * wind.x * 0.1;`);
};

// nel render loop:
time.value = clock.getElapsedTime();

Copiare l'attributo con un nome tutto suo evita il conflitto con three.js, che dichiara uv1 da sé quando un materiale usa un secondo set di UV.

Passo 7. Misura

  • renderer.info.render.calls e renderer.info.render.triangles — draw call e triangoli per frame;
  • il pannello Performance del browser — per capire se il limite è la CPU o la GPU;
  • un vero telefono di fascia media — non solo la tua macchina di sviluppo.

Misura prima e dopo ogni modifica. Spesso un solo intervento (foglie in cutout, pixel ratio, distanza delle ombre) rende più di tutti gli altri messi insieme.

Checklist

  • GLB con catena di LOD, card di foglie in cluster, pivot alla radice, collisione e pesi del vento.
  • Geometria compressa con meshopt, texture con KTX2.
  • THREE.LOD per pochi alberi vicini; InstancedMesh a chunk o BatchedMesh per le foreste.
  • Foglie con alphaTest e alphaToCoverage, non transparent.
  • Ombre solo sui livelli vicini; shadow camera stretta.
  • Pixel ratio limitato sui telefoni.
  • Draw call, triangoli e tempo di frame misurati su un dispositivo reale.

Come si inserisce FoliMesh

FoliMesh è un generatore di alberi e natura che funziona nel browser ed è costruito attorno ai budget dei giochi. Imposti un budget di triangoli per ogni livello e lui esporta un GLB con LOD0–LOD3 e un billboard incrociato per la distanza massima, card di foglie ritagliate sulla forma dei loro cluster, pivot alla radice, pesi del vento in TEXCOORD_1 (three.js lo legge come uv1) e distanze di LOD e capsule di collisione negli extras del nodo (in three.js, object.userData). Intere foreste possono essere esportate come un unico GLB con EXT_mesh_gpu_instancing, che GLTFLoader trasforma in mesh in instancing.

Per vedere come FoliMesh si confronta con altri strumenti, leggi il nostro confronto tra generatori di alberi 3D. Lavori invece con Godot? Scopri come creare una foresta veloce in Godot 4.

FAQ

three.js ha i LOD? Sì, THREE.LOD passa da un oggetto figlio all'altro in base alla distanza. Non funziona per singola istanza dentro una InstancedMesh — per quello usa i chunk, BatchedMesh con setGeometryIdAt o una libreria come @three.ez/instanced-mesh.

InstancedMesh o BatchedMesh? InstancedMesh è la più semplice e la più veloce per tante copie di una sola mesh. BatchedMesh è migliore quando vuoi culling e LOD per oggetto, oppure molte mesh diverse con un solo materiale.

Perché le foglie dei miei alberi sono nere o ordinate male? Di solito perché il materiale è transparent: true. Passa ad alphaTest con DoubleSide: i problemi di ordinamento spariscono e le prestazioni migliorano.

Vale la pena usare KTX2 per gli alberi? Sì. Le texture di corteccia e foglie sono spesso la parte più grande del download e della memoria video, e KTX2 riduce entrambe.