Урок · Середній рівень · ~60 хв
Three.js · L-системи · InstancedMesh

Дерева на L-системах із прискоренням на GPU

L-системи (системи Ліндемаєра) описують складні структури галуження простими правилами переписування. Цей урок генерує реалістичний 3D-дуб, березу чи фрактальну папороть зі рядка-аксіоми, а потім рендерить 100 000+ екземплярів гілок при 60 FPS за допомогою THREE.InstancedMesh — один виклик відмалювання на весь ліс.

1Граматика L-системи та генерація рядка

L-система складається з аксіоми (початкового рядка) та набору правил породження. На кожному поколінні кожен символ одночасно замінюється правою частиною свого правила:

// Класичне 3D-дерево з галуженням (варіант L-системи дерева Хонди) const lsystem = { axiom: 'A', rules: { 'A': 'F[+A][-A][&A][^A]', // стовбур → 4 повернуті гілки 'F': 'FF', // сегмент росте }, angle: 25.7 // градусів }; function expand(lsystem, generations) { let str = lsystem.axiom; for (let i = 0; i < generations; i++) { let next = ''; for (const c of str) next += lsystem.rules[c] ?? c; // замінюємо, якщо правило існує, інакше лишаємо str = next; } return str; } // На поколінні 5 просте дерево дає ~20 000 символів const treeString = expand(lsystem, 5); console.log('String length:', treeString.length); // ~21,764
Значення символів: F = малювати вперед, +/- = рискання ±кут, &/^ = тангаж ±кут, \// = крен ±кут, [ = покласти у стек, ] = зняти зі стеку.

2Інтерпретація через черепашачу графіку у 3D

Інтерпретуйте рядок, рухаючи «черепашку» крізь 3D-простір. Підтримуйте стек матриць для вкладеності гілок:

import { Matrix4, Vector3, Quaternion, Euler } from 'three'; function interpretLSystem(str, segLength, angle) { const branches = []; // [{matrix, level}] let matrix = new Matrix4(); // поточна поза черепашки const stack = []; const DEG = Math.PI / 180; for (const c of str) { switch (c) { case 'F': { // Записуємо сегмент гілки як матрицю branches.push(matrix.clone()); // Рухаємо черепашку вперед уздовж локальної +Y matrix = matrix.clone().multiply( new Matrix4().makeTranslation(0, segLength, 0)); break; } case '+': matrix.multiply(new Matrix4().makeRotationZ( angle * DEG)); break; case '-': matrix.multiply(new Matrix4().makeRotationZ(-angle * DEG)); break; case '&': matrix.multiply(new Matrix4().makeRotationX( angle * DEG)); break; case '^': matrix.multiply(new Matrix4().makeRotationX(-angle * DEG)); break; case '\\': matrix.multiply(new Matrix4().makeRotationY( angle * DEG)); break; case '/': matrix.multiply(new Matrix4().makeRotationY(-angle * DEG)); break; case '[': stack.push(matrix.clone()); break; case ']': matrix = stack.pop(); break; } } return branches; }

3Стохастичні L-системи

Детерміновані L-системи дають ідеально регулярні фрактали. Додайте випадковість, задавши для одного символу кілька правил із ймовірностями:

const stochasticTree = { axiom: 'A', rules: { 'A': [ { p: 0.33, s: 'F[+A][-A]' }, { p: 0.33, s: 'F[+A][&A]' }, { p: 0.34, s: 'F[-A][^A][+A]' }, ], 'F': [{ p: 1.0, s: 'FF' }] }, angle: 22.5 }; function expandStochastic(lsystem, gens, seed = 42) { let rng = seed; const rand = () => { rng = (rng * 1664525 + 1013904223) & 0xffffffff; return (rng >>> 0) / 0xffffffff; }; let str = lsystem.axiom; for (let g = 0; g < gens; g++) { let next = ''; for (const c of str) { const candidates = lsystem.rules[c]; if (!candidates) { next += c; continue; } let r = rand(), cumulative = 0; for (const { p, s } of candidates) { cumulative += p; if (r <= cumulative) { next += s; break; } } } str = next; } return str; }

4InstancedMesh — один виклик відмалювання

THREE.InstancedMesh рендерить N копій однієї й тієї ж геометрії за один виклик відмалювання, надсилаючи всі матриці перетворення на GPU як буфер. Одна циліндрична гілка, повторена 20 000 разів, працює тривіально швидко:

const SEG_LENGTH = 0.2, SEG_RADIUS = 0.015; const branches = interpretLSystem(treeString, SEG_LENGTH, 25.7); const branchGeo = new THREE.CylinderGeometry(SEG_RADIUS, SEG_RADIUS * 1.4, SEG_LENGTH, 5, 1); const branchMat = new THREE.MeshLambertMaterial({ color: 0x5c3b1e }); const iMesh = new THREE.InstancedMesh(branchGeo, branchMat, branches.length); iMesh.castShadow = true; const tmpMatrix = new THREE.Matrix4(); branches.forEach((mat, i) => { // Обертаємо циліндр (за замовчуванням уздовж Y), щоб вирівняти з напрямком гілки iMesh.setMatrixAt(i, mat); }); iMesh.instanceMatrix.needsUpdate = true; scene.add(iMesh);
Для лісу зі 100 дерев по 20 000 гілок кожне (загалом 2 млн) використайте один InstancedMesh місткістю 2 000 000. Це малює весь ліс за один виклик GPU. На Mac з M1 це працює при 60 FPS у роздільній здатності 1440p.

5Білборди листя

Додайте листя як спрайти, обернені до камери, на кінцях гілок. Позначте спеціальний символ листка L в L-системі та збирайте їхні позиції під час інтерпретації:

// У interpretLSystem також відстежуємо позиції листя case 'L': { const pos = new Vector3().applyMatrix4(matrix); leafPositions.push(pos); break; } // Рендеримо як InstancedMesh з плоских PlaneGeometry const leafGeo = new THREE.PlaneGeometry(0.3, 0.3); const leafTex = new THREE.TextureLoader().load('leaf.png'); const leafMat = new THREE.MeshBasicMaterial({ map: leafTex, side: THREE.DoubleSide, alphaTest: 0.5, transparent: true }); const leafMesh = new THREE.InstancedMesh(leafGeo, leafMat, leafPositions.length); // У циклі анімації: обертаємо кожен листок до камери const cameraDir = new Vector3(); function updateLeaves() { camera.getWorldDirection(cameraDir); const q = new THREE.Quaternion().setFromUnitVectors( new THREE.Vector3(0, 0, 1), cameraDir.negate()); const r = new THREE.Matrix4().makeRotationFromQuaternion(q); leafPositions.forEach((pos, i) => { leafMesh.setMatrixAt(i, r.clone().setPosition(pos)); }); leafMesh.instanceMatrix.needsUpdate = true; }