player/src/engine/TerrainGenWorker.js
МИН 87444ee2c8 Initial public release: Rublox Player v1.0
Open-source web player for Rublox games, dual-licensed under
AGPL-3.0 + Commercial.

Highlights:
- Babylon.js 7 + React 18 + Vite 5 stack
- Self-contained engine (~46k lines): BlockManager, ModelManager,
  PlayerController, ScriptSandboxWorker, MultiplayerSync, 30+ GD
  gamemodes
- Configurable backend via VITE_API_BASE and friends — works against
  staging (dev-api.rublox.pro) out of the box
- Standalone mode (VITE_STANDALONE=true) loads a bundled sample game
  for first-run without any backend
- Full docs: README, ARCHITECTURE, CONTRIBUTING, SECURITY, CHANGELOG
- Lint + format scaffolding (ESLint + Prettier + EditorConfig)
- Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md
- Issue templates: bug_report, feature_request, security_disclosure

Removed before public release:
- frontend_deploy.py (contained production SSH credentials)
- ~27 admin endpoints (kept in private repo)
- Hard-coded internal URLs and IPs
- All previous git history (clean repo init)
2026-05-27 23:04:04 +03:00

542 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* TerrainGenWorker.js — Web Worker для процедурной генерации ландшафта.
*
* Архитектура (Этап C оптимизации):
* - main thread → отправляет params + bbox → worker
* - worker (фоновый поток) → сэмплит heightmap, surface extraction,
* деревья, decorations
* - worker → отправляет результат: Array<{x,y,z,m}> + decorations[]
* - main thread → передаёт в terrainManager.loadFromArray + decoManager
*
* Worker устраняет 5-15 секунд блокировки UI на больших картах.
* UI остаётся отзывчивым — progress-bar анимируется плавно.
*
* Worker код — self-contained, без импортов. Загружается через blob URL.
*
* НЕ импортируется напрямую. Используй getTerrainGenWorkerUrl().
*/
const SOURCE = `
"use strict";
// ============================================================================
// SimplexNoise (inline копия из engine/voxel/SimplexNoise.js)
// ============================================================================
function mulberry32(seed) {
let t = seed | 0;
return function () {
t = (t + 0x6D2B79F5) | 0;
let r = Math.imul(t ^ (t >>> 15), 1 | t);
r = (r + Math.imul(r ^ (r >>> 7), 61 | r)) ^ r;
return ((r ^ (r >>> 14)) >>> 0) / 4294967296;
};
}
const GRAD2 = [
1, 1, -1, 1, 1, -1, -1, -1,
1, 0, -1, 0, 1, 0, -1, 0,
0, 1, 0, -1, 0, 1, 0, -1,
1, 1, 0, 0, -1, 1, 0, 0,
];
const F2 = 0.5 * (Math.sqrt(3) - 1);
const G2 = (3 - Math.sqrt(3)) / 6;
class SimplexNoise {
constructor(seed) {
this.seed = seed;
const rand = mulberry32(seed);
const perm = new Uint8Array(512);
const p = new Uint8Array(256);
for (let i = 0; i < 256; i++) p[i] = i;
for (let i = 255; i > 0; i--) {
const j = Math.floor(rand() * (i + 1));
const tmp = p[i]; p[i] = p[j]; p[j] = tmp;
}
for (let i = 0; i < 512; i++) perm[i] = p[i & 255];
this.perm = perm;
}
noise2(xin, yin) {
const perm = this.perm;
const s = (xin + yin) * F2;
const i = Math.floor(xin + s);
const j = Math.floor(yin + s);
const t = (i + j) * G2;
const X0 = i - t, Y0 = j - t;
const x0 = xin - X0, y0 = yin - Y0;
let i1, j1;
if (x0 > y0) { i1 = 1; j1 = 0; } else { i1 = 0; j1 = 1; }
const x1 = x0 - i1 + G2, y1 = y0 - j1 + G2;
const x2 = x0 - 1 + 2 * G2, y2 = y0 - 1 + 2 * G2;
const ii = i & 255, jj = j & 255;
const gi0 = perm[ii + perm[jj]] & 15;
const gi1 = perm[ii + i1 + perm[jj + j1]] & 15;
const gi2 = perm[ii + 1 + perm[jj + 1]] & 15;
let n0 = 0, n1 = 0, n2 = 0;
let t0 = 0.5 - x0 * x0 - y0 * y0;
if (t0 >= 0) { t0 *= t0; n0 = t0 * t0 * (GRAD2[gi0*2]*x0 + GRAD2[gi0*2+1]*y0); }
let t1m = 0.5 - x1 * x1 - y1 * y1;
if (t1m >= 0) { t1m *= t1m; n1 = t1m * t1m * (GRAD2[gi1*2]*x1 + GRAD2[gi1*2+1]*y1); }
let t2m = 0.5 - x2 * x2 - y2 * y2;
if (t2m >= 0) { t2m *= t2m; n2 = t2m * t2m * (GRAD2[gi2*2]*x2 + GRAD2[gi2*2+1]*y2); }
return 70 * (n0 + n1 + n2);
}
fbm2(x, y, octaves, persistence, lacunarity) {
let total = 0, amp = 1, freq = 1, norm = 0;
for (let i = 0; i < octaves; i++) {
total += this.noise2(x * freq, y * freq) * amp;
norm += amp;
amp *= persistence;
freq *= lacunarity;
}
return total / norm;
}
}
// ============================================================================
// Хэш для детерминированной расстановки структур
// ============================================================================
function hash2(x, y, seed) {
let h = (x * 374761393 + y * 668265263 + seed * 1442695040) | 0;
h = Math.imul(h ^ (h >>> 13), 1274126177);
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
}
// ============================================================================
// WorldGenerator (упрощённая копия — sample + bbox)
// ============================================================================
let G_PARAMS = null;
let G_HEIGHT_NOISE = null;
let G_BIOME_NOISE = null;
function setupGenerator(params) {
G_PARAMS = params;
G_HEIGHT_NOISE = new SimplexNoise(params.seed);
G_BIOME_NOISE = new SimplexNoise(params.seed + (params.biomeMap?.seedOffset ?? 100));
}
function rawHeight(x, z) {
const hp = G_PARAMS.heightmap;
let wx = x, wz = z;
if (G_PARAMS.domainWarp?.enabled) {
const ws = G_PARAMS.domainWarp.scale;
const wstr = G_PARAMS.domainWarp.strength;
wx = x + G_HEIGHT_NOISE.noise2(x * ws, z * ws) * wstr;
wz = z + G_HEIGHT_NOISE.noise2(x * ws + 100, z * ws + 100) * wstr;
}
return G_HEIGHT_NOISE.fbm2(wx * hp.scale, wz * hp.scale, hp.octaves, hp.persistence, hp.lacunarity);
}
function sampleBiome(x, z) {
const bm = G_PARAMS.biomeMap;
const n = (G_BIOME_NOISE.fbm2(x * bm.scale, z * bm.scale, 3, 0.5, 2.0) + 1) * 0.5;
const biomes = G_PARAMS.biomes;
for (let i = 0; i < biomes.length; i++) {
const b = biomes[i];
if (n >= b.threshold[0] && n < b.threshold[1]) return b;
}
return biomes[biomes.length - 1];
}
function smoothBiomeBonus(x, z) {
const BLEND_WIDTH = 0.08;
const bm = G_PARAMS.biomeMap;
const n = (G_BIOME_NOISE.fbm2(x * bm.scale, z * bm.scale, 3, 0.5, 2.0) + 1) * 0.5;
const biomes = G_PARAMS.biomes;
let currentIdx = -1;
for (let i = 0; i < biomes.length; i++) {
const b = biomes[i];
if (n >= b.threshold[0] && n < b.threshold[1]) { currentIdx = i; break; }
}
if (currentIdx === -1) currentIdx = biomes.length - 1;
const cur = biomes[currentIdx];
const curBonus = cur.heightBonus ?? 1;
const t0 = cur.threshold[0];
const t1 = cur.threshold[1];
const distFromStart = n - t0;
const distFromEnd = t1 - n;
if (distFromStart < BLEND_WIDTH && currentIdx > 0) {
const prev = biomes[currentIdx - 1];
const prevBonus = prev.heightBonus ?? 1;
const tt = distFromStart / BLEND_WIDTH;
const ts = tt * tt * (3 - 2 * tt);
return prevBonus * (1 - ts) + curBonus * ts;
}
if (distFromEnd < BLEND_WIDTH && currentIdx < biomes.length - 1) {
const next = biomes[currentIdx + 1];
const nextBonus = next.heightBonus ?? 1;
const tt = distFromEnd / BLEND_WIDTH;
const ts = tt * tt * (3 - 2 * tt);
return nextBonus * (1 - ts) + curBonus * ts;
}
return curBonus;
}
function sampleHeight(x, z) {
const hp = G_PARAMS.heightmap;
let n = rawHeight(x, z);
n = (n + 1) * 0.5;
n = Math.pow(Math.max(0, Math.min(1, n)), hp.exponent);
const bonus = smoothBiomeBonus(x, z);
return Math.round(hp.baseHeight + n * hp.amplitude * bonus);
}
function sampleTreeAt(x, z) {
const s = G_PARAMS.structures?.trees;
if (!s || !s.enabled) return null;
const biome = sampleBiome(x, z);
const treeProb = (biome.features?.trees ?? 0) * s.density;
if (treeProb <= 0) return null;
const grid = Math.max(2, s.minDistance);
const gx = Math.floor(x / grid);
const gz = Math.floor(z / grid);
const r = hash2(gx, gz, G_PARAMS.seed + 999);
if (r >= treeProb) return null;
const ox = Math.floor(hash2(gx, gz, G_PARAMS.seed + 1001) * grid);
const oz = Math.floor(hash2(gx, gz, G_PARAMS.seed + 1002) * grid);
if (gx * grid + ox !== x || gz * grid + oz !== z) return null;
const treeTypes = biome.features?.treeTypes ?? ['oak'];
const tt = Math.floor(hash2(gx, gz, G_PARAMS.seed + 1003) * treeTypes.length);
return { type: treeTypes[tt % treeTypes.length] };
}
// ============================================================================
// Полная генерация: возвращает voxels[] + decorations[]
// ============================================================================
function generateWorld(bbox, progressCb) {
const { x0, z0, x1, z1 } = bbox;
const width = x1 - x0;
const depth = z1 - z0;
const total = width * depth;
let done = 0;
const totalW = total * 2 + total * 0.3;
// === Шаг 1: heightmap ===
const heightmap = new Map();
for (let z = z0; z < z1; z++) {
for (let x = x0; x < x1; x++) {
const h = sampleHeight(x, z);
const biome = sampleBiome(x, z);
heightmap.set(x + ',' + z, { h, biome });
}
done += width;
if ((z - z0) % 16 === 0 && progressCb) progressCb(done, totalW, 'heightmap');
}
// === Шаг 2: surface extraction ===
const voxelMap = new Map(); // "x,y,z" → matId
const setVoxel = (x, y, z, m) => voxelMap.set(x + ',' + y + ',' + z, m);
const getVoxel = (x, y, z) => voxelMap.get(x + ',' + y + ',' + z);
for (let z = z0; z < z1; z++) {
for (let x = x0; x < x1; x++) {
const info = heightmap.get(x + ',' + z);
const h = info.h;
const biome = info.biome;
const nb = [];
for (const [dx, dz] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
const nbInfo = heightmap.get((x + dx) + ',' + (z + dz));
nb.push(nbInfo ? nbInfo.h : 0);
}
const minNbH = Math.min(...nb);
if (h <= 0) {
setVoxel(x, 0, z, biome.softMaterial);
done++;
continue;
}
setVoxel(x, h - 1, z, biome.topMaterial);
if (h >= 2 && (h - 2) >= minNbH) setVoxel(x, h - 2, z, biome.softMaterial);
if (h >= 3 && (h - 3) >= minNbH) setVoxel(x, h - 3, z, biome.softMaterial);
const bottomVisible = Math.max(0, minNbH);
const topVisible = h - 3;
for (let y = bottomVisible; y < topVisible; y++) {
setVoxel(x, y, z, biome.hardMaterial);
}
if (minNbH <= 0 && !getVoxel(x, 0, z)) {
setVoxel(x, 0, z, biome.hardMaterial);
}
done++;
}
if ((z - z0) % 16 === 0 && progressCb) progressCb(done, totalW, 'surface');
}
// === Шаг 3: деревья ===
let treesPlaced = 0;
if (G_PARAMS.structures?.trees?.enabled) {
for (let z = z0; z < z1; z++) {
for (let x = x0; x < x1; x++) {
const tree = sampleTreeAt(x, z);
if (!tree) continue;
const info = heightmap.get(x + ',' + z);
if (!info || info.h < 1) continue;
if (info.biome.topMaterial !== 'grass') continue;
placeTree(x, info.h - 1, z, tree.type, setVoxel);
treesPlaced++;
}
done += width * 0.3;
if ((z - z0) % 32 === 0 && progressCb) progressCb(done, totalW, 'trees');
}
}
// === Шаг 4: decorations ===
const decorations = [];
const decoParams = G_PARAMS.structures?.decorations ?? {};
const flowersDensity = decoParams.flowersDensity ?? 0.015;
const grassDensity = decoParams.grassDensity ?? 0.05;
if ((flowersDensity > 0 || grassDensity > 0) && decoParams.enabled !== false) {
const FLOWERS_BY_BIOME = {
grass: ['daisy', 'cornflower', 'poppy', 'dandelion'],
forest: ['daisy', 'fly_mushroom', 'brown_mushroom', 'fly_mushroom'],
plain: ['dandelion', 'cornflower', 'daisy'],
};
const GRASS_POOL = [
'grass_short', 'grass_short', 'grass_short', 'grass_short',
'grass_tuft', 'grass_tuft', 'grass_tuft',
'grass_blade', 'grass_blade',
'grass_wide', 'grass_wide',
'grass_tall', 'grass_clump', 'grass_dry',
];
for (let z = z0; z < z1; z++) {
for (let x = x0; x < x1; x++) {
const info = heightmap.get(x + ',' + z);
if (!info || info.h < 1) continue;
if (info.biome.topMaterial !== 'grass') continue;
const topY = info.h - 1;
const aboveMat = getVoxel(x, topY + 1, z);
if (aboveMat) continue;
const wx = (x + 0.5) * 0.25;
const wy = info.h * 0.25;
const wz = (z + 0.5) * 0.25;
const rFlower = hash2(x, z, G_PARAMS.seed + 5001);
if (rFlower < flowersDensity) {
const biomeId = info.biome.id;
const pool = FLOWERS_BY_BIOME[biomeId] || FLOWERS_BY_BIOME.plain;
if (pool && pool.length > 0) {
const modelIdx = Math.floor(hash2(x, z, G_PARAMS.seed + 5002) * pool.length);
const modelId = pool[modelIdx];
const rotation = Math.floor(hash2(x, z, G_PARAMS.seed + 5003) * 4);
decorations.push({ x: wx, y: wy, z: wz, modelId, rotation });
continue;
}
}
const rGrass = hash2(x, z, G_PARAMS.seed + 5004);
if (rGrass < grassDensity) {
const modelIdx = Math.floor(hash2(x, z, G_PARAMS.seed + 5005) * GRASS_POOL.length);
const modelId = GRASS_POOL[modelIdx];
const rotation = Math.floor(hash2(x, z, G_PARAMS.seed + 5006) * 4);
const scale = 0.7 + hash2(x, z, G_PARAMS.seed + 5007) * 0.7;
decorations.push({ x: wx, y: wy, z: wz, modelId, rotation, scale });
}
}
}
}
// Конвертируем Map в массив для возврата
const voxels = [];
for (const [key, m] of voxelMap) {
const lastComma = key.lastIndexOf(',');
const midComma = key.lastIndexOf(',', lastComma - 1);
voxels.push({
x: parseInt(key.slice(0, midComma), 10),
y: parseInt(key.slice(midComma + 1, lastComma), 10),
z: parseInt(key.slice(lastComma + 1), 10),
m,
});
}
return { voxels, decorations, treesPlaced };
}
// ============================================================================
// _placeTree — копия из engine/voxel/WorldGenerator.js (упрощённая)
// ============================================================================
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(cx, cy, cz, r, squish, placeLeaf) {
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, G_PARAMS.seed + 3000 + dy) > 0.5) continue;
}
if (dx === 0 && dz === 0 && dy < 0) continue;
const colorNoise = hash2(cx + dx, cz + dz + dy * 11, G_PARAMS.seed + 3500);
placeLeaf(cx + dx, cy + dy, cz + dz, colorNoise);
}
}
}
}
function placeTree(tx, topY, tz, type, setVoxel) {
const r1 = hash2(tx, tz, G_PARAMS.seed + 2001);
const r2 = hash2(tx, tz, G_PARAMS.seed + 2002);
const r3 = hash2(tx, tz, G_PARAMS.seed + 2003);
const sizeScale = Math.max(0.3, Math.min(5.0, G_PARAMS.structures?.trees?.sizeScale ?? 1.0));
let primaryTrunk, secondaryTrunk, primaryLeaf, secondaryLeaf;
let secondaryLeafChance, trunkH, crownR, crownSquish, trunkThickness;
if (type === 'birch') {
primaryTrunk = 'trunk_white'; secondaryTrunk = 'trunk_white';
primaryLeaf = 'leaves'; secondaryLeaf = 'leaves_orange';
secondaryLeafChance = 0.10;
trunkH = Math.round((10 + r1 * 4) * sizeScale);
crownR = Math.round(3 * sizeScale);
crownSquish = 1.5;
trunkThickness = sizeScale < 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) * sizeScale);
crownR = Math.round((4 + r2 * 2) * sizeScale);
crownSquish = 0.85;
trunkThickness = sizeScale < 0.8 ? 0 : sizeScale < 1.5 ? 1 : sizeScale < 2.5 ? 2 : 3;
} else {
primaryTrunk = 'trunk'; secondaryTrunk = 'wood';
primaryLeaf = 'leaves'; secondaryLeaf = 'leaves_orange';
secondaryLeafChance = 0.07;
trunkH = Math.round((8 + r1 * 4) * sizeScale);
crownR = Math.round((3 + r2 * 2) * sizeScale);
crownSquish = 1.0;
trunkThickness = sizeScale < 0.8 ? 0 : sizeScale < 1.5 ? 1 : sizeScale < 2.5 ? 2 : 3;
}
if (trunkH < 3) trunkH = 3;
if (crownR < 2) crownR = 2;
const placeLeaf = (px, py, pz, noise) => {
const isSecondary = noise < secondaryLeafChance;
setVoxel(px, py, pz, isSecondary ? secondaryLeaf : primaryLeaf);
};
const placeTrunk = (px, py, pz, noise) => {
const isSecondary = (type !== 'birch') && noise < 0.05;
setVoxel(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, G_PARAMS.seed + 2100);
placeTrunk(tx + ox, topY + dy, tz + oz, noise);
}
}
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, G_PARAMS.seed + 2200) > 0.55) continue;
placeTrunk(tx + ox, topY + dy, tz + oz, 0.5);
}
}
}
const branchTips = [];
const numBranches = type === 'birch'
? (sizeScale < 1.5 ? 2 : 3)
: Math.max(2, Math.min(8, Math.round((3 + r3 * 2) * Math.sqrt(sizeScale))));
const branchLenBase = Math.max(2, Math.floor(crownR * 0.8 + sizeScale));
for (let b = 0; b < numBranches; b++) {
const baseAngle = (b / numBranches) * Math.PI * 2;
const angleJitter = (hash2(tx + b * 31, tz + b * 53, G_PARAMS.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, G_PARAMS.seed + 2400) * 3);
let bx = tx, by = topY + branchStartY, bz = tz;
const dirX = Math.cos(angle), 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, G_PARAMS.seed + 2500 + step) - 0.3) * 1.5;
by = topY + branchStartY + Math.floor(upBias + jitterY);
const noise = hash2(bx, bz + step * 3, G_PARAMS.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(tx, crownCy, tz, mainCrownR, crownSquish, placeLeaf);
const subCrownRBase = Math.max(2, Math.floor(crownR * 0.6));
for (const tip of branchTips) {
const sizeJitter = hash2(tip.x, tip.z, G_PARAMS.seed + 2600) * 0.4 - 0.2;
const subR = Math.max(2, Math.round(subCrownRBase * (1.0 + sizeJitter)));
placeCrown(tip.x, tip.y + 1, tip.z, subR, crownSquish, placeLeaf);
}
}
// ============================================================================
// Worker message handler
// ============================================================================
self.onmessage = function(e) {
const msg = e.data;
if (msg.type === 'generate') {
try {
setupGenerator(msg.params);
const t0 = performance.now();
const result = generateWorld(msg.bbox, (done, total, phase) => {
self.postMessage({ type: 'progress', done, total, phase });
});
const dt = performance.now() - t0;
// Передаём voxels — может быть большой массив, лучше JSON через
// postMessage (не Transferable т.к. это plain objects).
self.postMessage({
type: 'done',
voxels: result.voxels,
decorations: result.decorations,
treesPlaced: result.treesPlaced,
timeMs: Math.round(dt),
});
} catch (err) {
self.postMessage({ type: 'error', message: err.message, stack: err.stack });
}
}
};
`;
/**
* Создаёт Blob URL Worker'а. Использовать в new Worker(url).
*/
export function getTerrainGenWorkerUrl() {
const blob = new Blob([SOURCE], { type: 'application/javascript' });
return URL.createObjectURL(blob);
}