/** * VoxelTreeBuilder — алгоритм генерации красивого voxel-дерева. * * Перенесён из WorldGenerator._placeTree (там используется для процедурной * генерации мира). Здесь экспортирован как чистая функция, чтобы можно было * звать из brush-инструмента "Деревья" в редакторе. * * Виды деревьев: * - 'oak' — дуб: коричневый ствол, зелёная крона * - 'birch' — берёза: белый тонкий ствол, листва с оранжевыми вкраплениями * - 'autumn' — осеннее: коричневый ствол, ОРАНЖЕВАЯ листва * * Алгоритм: * 1. Толстый ствол (offsets по thickness 0..3) с сужением к верху * 2. Утолщение у основания (корни) для thickness >= 2 * 3. Зигзагообразные ветви — 2-8 шт., расходятся под углом * 4. Главная крона (сфера) + крона на конце каждой ветви * * sizeScale: 0.3..5.0 * 0.3..0.8 → саженец (~3-5 voxel высота, thin ствол) * 1.0 → стандарт (~8-12 voxel, средняя крона) * 2.0 → большое (~20 voxel, толстый ствол 3×3, много ветвей) * 5.0 → гигантское (~50 voxel, ствол 5×5) */ /** Простой 2D-hash, возвращает float ∈ [0..1). Детерминированный. */ function hash2(x, y, seed) { let h = ((x | 0) * 73856093) ^ ((y | 0) * 19349663) ^ ((seed | 0) * 83492791); h = (h ^ (h >>> 13)) * 1274126177; return ((h ^ (h >>> 16)) >>> 0) / 0xffffffff; } /** * Шаблон офсетов ствола. * 0: один voxel * 1: крест 5 voxel'ов (центр + 4 стороны) * 2: полный 3×3 (9 voxel'ов) * 3: 5×5 без 4-х угловых (21 voxel) */ function trunkOffsets(thickness) { if (thickness <= 0) return [[0, 0]]; if (thickness === 1) return [[0, 0], [1, 0], [-1, 0], [0, 1], [0, -1]]; if (thickness === 2) { return [ [0, 0], [1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [-1, -1], [1, -1], [-1, 1], ]; } const out = []; for (let dx = -2; dx <= 2; dx++) { for (let dz = -2; dz <= 2; dz++) { if (Math.abs(dx) === 2 && Math.abs(dz) === 2) continue; out.push([dx, dz]); } } return out; } /** * Сфера кроны с пятнистой раскраской и лохматыми краями. */ function placeCrown(setVoxelFn, cx, cy, cz, r, squish, placeLeaf, seed) { const r2sq = r * r; const innerR2 = r > 2 ? (r - 2) * (r - 2) : 0; const yRange = Math.floor(r * squish) + 2; for (let dx = -r - 1; dx <= r + 1; dx++) { for (let dy = -Math.floor(r / 2); dy <= yRange; dy++) { for (let dz = -r - 1; dz <= r + 1; dz++) { const dyEff = dy / squish; const d2 = dx * dx + dyEff * dyEff + dz * dz; if (d2 > r2sq + 2) continue; if (d2 < innerR2) continue; const edgeThresh = r2sq - 1.5; if (d2 > edgeThresh) { if (hash2(cx + dx, cz + dz, seed + 3000 + dy) > 0.5) continue; } if (dx === 0 && dz === 0 && dy < 0) continue; const colorNoise = hash2(cx + dx, cz + dz + dy * 11, seed + 3500); placeLeaf(cx + dx, cy + dy, cz + dz, colorNoise); } } } } /** * Главная функция — сгенерировать дерево. * * @param {function(x,y,z,matId):void} setVoxelFn — колбэк постановки voxel * @param {number} tx — voxel-x ствола * @param {number} topY — voxel-y поверхности (1-я "земля" под деревом) * @param {number} tz — voxel-z ствола * @param {'oak'|'birch'|'autumn'} type * @param {number} sizeScale — 0.3..5.0 * @param {number} seed — детерминизирует форму. Дай разные значения для разных деревьев. */ export function placeVoxelTree(setVoxelFn, tx, topY, tz, type, sizeScale = 1.0, seed = 12345) { const r1 = hash2(tx, tz, seed + 2001); const r2 = hash2(tx, tz, seed + 2002); const r3 = hash2(tx, tz, seed + 2003); const scale = Math.max(0.3, Math.min(5.0, sizeScale)); let primaryTrunk, secondaryTrunk; let primaryLeaf, secondaryLeaf; let secondaryLeafChance; let trunkH, crownR, crownSquish; let trunkThickness; if (type === 'birch') { primaryTrunk = 'trunk_white'; secondaryTrunk = 'trunk_white'; primaryLeaf = 'leaves'; secondaryLeaf = 'leaves_orange'; secondaryLeafChance = 0.10; trunkH = Math.round((10 + r1 * 4) * scale); crownR = Math.round(3 * scale); crownSquish = 1.5; trunkThickness = scale < 1.5 ? 0 : 1; } else if (type === 'autumn') { primaryTrunk = 'trunk'; secondaryTrunk = 'wood'; primaryLeaf = 'leaves_orange'; secondaryLeaf = 'leaves'; secondaryLeafChance = 0.25; trunkH = Math.round((9 + r1 * 4) * scale); crownR = Math.round((4 + r2 * 2) * scale); crownSquish = 0.85; trunkThickness = scale < 0.8 ? 0 : scale < 1.5 ? 1 : scale < 2.5 ? 2 : 3; } else { // oak (default) primaryTrunk = 'trunk'; secondaryTrunk = 'wood'; primaryLeaf = 'leaves'; secondaryLeaf = 'leaves_orange'; secondaryLeafChance = 0.07; trunkH = Math.round((8 + r1 * 4) * scale); crownR = Math.round((3 + r2 * 2) * scale); crownSquish = 1.0; trunkThickness = scale < 0.8 ? 0 : scale < 1.5 ? 1 : scale < 2.5 ? 2 : 3; } if (trunkH < 3) trunkH = 3; if (crownR < 2) crownR = 2; const placeLeaf = (px, py, pz, noise) => { const isSecondary = noise < secondaryLeafChance; setVoxelFn(px, py, pz, isSecondary ? secondaryLeaf : primaryLeaf); }; const placeTrunk = (px, py, pz, noise) => { const isSecondary = (type !== 'birch') && noise < 0.05; setVoxelFn(px, py, pz, isSecondary ? secondaryTrunk : primaryTrunk); }; // Ствол с сужением const taperStart = Math.floor(trunkH * 0.7); for (let dy = 1; dy <= trunkH; dy++) { const thicknessAtY = dy < taperStart ? trunkThickness : Math.max(0, trunkThickness - Math.floor((dy - taperStart) * 0.5)); const offsets = trunkOffsets(thicknessAtY); for (const [ox, oz] of offsets) { const noise = hash2(tx + ox * 13, tz + oz * 17 + dy * 7, seed + 2100); placeTrunk(tx + ox, topY + dy, tz + oz, noise); } } // Корни у основания (thickness >= 2) if (trunkThickness >= 2) { const baseH = Math.min(3, Math.floor(trunkH / 5)); for (let dy = 1; dy <= baseH; dy++) { for (const [ox, oz] of [[2, 0], [-2, 0], [0, 2], [0, -2], [1, 1], [-1, -1], [1, -1], [-1, 1]]) { if (hash2(tx + ox * 13, tz + oz * 17 + dy, seed + 2200) > 0.55) continue; placeTrunk(tx + ox, topY + dy, tz + oz, 0.5); } } } // Ветви — зигзаг const branchTips = []; const numBranches = type === 'birch' ? (scale < 1.5 ? 2 : 3) : Math.max(2, Math.min(8, Math.round((3 + r3 * 2) * Math.sqrt(scale)))); const branchLenBase = Math.max(2, Math.floor(crownR * 0.8 + scale)); for (let b = 0; b < numBranches; b++) { const baseAngle = (b / numBranches) * Math.PI * 2; const angleJitter = (hash2(tx + b * 31, tz + b * 53, seed + 2300) - 0.5) * 0.6; const angle = baseAngle + angleJitter + r1 * 0.5; const startYFrac = 0.5 + (b / numBranches) * 0.45; const branchStartY = Math.max(2, Math.floor(trunkH * startYFrac)); const branchLen = branchLenBase + Math.floor(hash2(tx + b, tz, seed + 2400) * 3); let bx = tx, by = topY + branchStartY, bz = tz; const dirX = Math.cos(angle); const dirZ = Math.sin(angle); for (let step = 1; step <= branchLen; step++) { bx = Math.round(tx + dirX * step); bz = Math.round(tz + dirZ * step); const upBias = step * 0.4; const jitterY = (hash2(bx * 7, bz * 11, seed + 2500 + step) - 0.3) * 1.5; by = topY + branchStartY + Math.floor(upBias + jitterY); const noise = hash2(bx, bz + step * 3, seed + 2310); placeTrunk(bx, by, bz, noise); if (trunkThickness >= 2 && step <= branchLen / 2) { const sx = Math.round(bx - dirZ * 0.7); const sz = Math.round(bz + dirX * 0.7); if (sx !== bx || sz !== bz) { placeTrunk(sx, by, sz, noise + 0.1); } } } branchTips.push({ x: bx, y: by, z: bz }); } // Главная крона const crownCy = topY + trunkH; const mainCrownR = Math.max(2, Math.floor(crownR * 0.85)); placeCrown(setVoxelFn, tx, crownCy, tz, mainCrownR, crownSquish, placeLeaf, seed); // Кроны на ветвях const subCrownRBase = Math.max(2, Math.floor(crownR * 0.6)); for (const tip of branchTips) { const sizeJitter = hash2(tip.x, tip.z, seed + 2600) * 0.4 - 0.2; const subR = Math.max(2, Math.round(subCrownRBase * (1.0 + sizeJitter))); placeCrown(setVoxelFn, tip.x, tip.y + 1, tip.z, subR, crownSquish, placeLeaf, seed); } } /** Все доступные виды деревьев — для случайного выбора в plant-кисти. */ export const TREE_TYPES = ['oak', 'birch', 'autumn'];