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; }