/* eslint-disable */ /** * TerrainRegionWorker — фоновый воркер сборки mesh-геометрии региона. * * Основной поток отдаёт массив voxel'ов одного материала и желаемый * detail-уровень (1 = full, 2 = half, 4 = quarter). Воркер собирает: * - positions: Float32Array вершин * - normals: Float32Array нормалей * - uvs: Float32Array UV * - indices: Uint32Array индексов * * Главный поток создаёт Mesh + vertexData.applyToMesh. * * Передача через Transferable — zero-copy. * * Зачем это нужно: на больших картах (1М+ voxel'ов) сборка mesh'ей в * main thread лагает рендер. Worker делает это асинхронно в фоне. * * Формат сообщения {type: 'build', regionId, voxels, voxelSize, lod}: * - voxels: Int16Array, шаг 4: x, y, z, matIdx (т.к. матриалы упакованы) * - matKeys: string[] — материалы по индексу (или 'matId:top'/':side'/':bottom' * для MultiCube). Их main thread разворачивает обратно. * * Сообщение ответа {type: 'built', regionId, byMaterial:{[matKey]: {positions, normals, uvs, indices}}}. */ // ============================================================================ // Конфигурация граней — копия с ChunkMesher // ============================================================================ const FACES = [ { faceIdx: 0, dx: 0, dy: 0, dz: -1, nx: 0, ny: 0, nz: -1, corners: [[0,0,0],[0,1,0],[1,1,0],[1,0,0]] }, { faceIdx: 1, dx: 1, dy: 0, dz: 0, nx: 1, ny: 0, nz: 0, corners: [[1,0,0],[1,1,0],[1,1,1],[1,0,1]] }, { faceIdx: 2, dx: 0, dy: 0, dz: 1, nx: 0, ny: 0, nz: 1, corners: [[1,0,1],[1,1,1],[0,1,1],[0,0,1]] }, { faceIdx: 3, dx: -1, dy: 0, dz: 0, nx: -1, ny: 0, nz: 0, corners: [[0,0,1],[0,1,1],[0,1,0],[0,0,0]] }, { faceIdx: 4, dx: 0, dy: -1, dz: 0, nx: 0, ny: -1, nz: 0, corners: [[0,0,1],[0,0,0],[1,0,0],[1,0,1]] }, { faceIdx: 5, dx: 0, dy: 1, dz: 0, nx: 0, ny: 1, nz: 0, corners: [[0,1,0],[0,1,1],[1,1,1],[1,1,0]] }, ]; // MultiCube материалы — для каждого создаём отдельный ключ матерала // для top/side/bottom. main thread знает какие текстуры применить. const MULTICUBE = new Set(['grass', 'trunk', 'trunk_white']); const TRANSPARENT = new Set([ 'water', 'glacier', 'leaves', 'leaves_orange', 'flower_red', 'flower_blue', 'flower_yellow', 'mushroom_red', 'tall_grass', ]); function materialKey(matId, faceIdx) { if (!MULTICUBE.has(matId)) return matId; if (faceIdx === 5) return matId + ':top'; if (faceIdx === 4) return matId + ':bottom'; return matId + ':side'; } function isFaceVisible(ownMat, neighborMat) { if (!neighborMat) return true; const ownT = TRANSPARENT.has(ownMat); const nbT = TRANSPARENT.has(neighborMat); if (!ownT && !nbT) return false; if (!ownT && nbT) return true; if (ownT && !nbT) return false; return ownMat !== neighborMat; } // ============================================================================ // Основная функция сборки геометрии региона // ============================================================================ /** * @param {Object} msg - {voxels: Array<{x,y,z,m}>, voxelSize, lod} * lod 1 = полное разрешение (каждый voxel) * lod 2 = каждый 2-й (×8 меньше треугольников, для дальних регионов) * lod 4 = каждый 4-й (×64 меньше) * @returns {Object} byMaterial: {[matKey]: {positions, normals, uvs, indices}} */ function buildRegionGeometry(msg) { const { voxels, voxelSize, lod = 1 } = msg; // 1. Построить hash voxels: "x,y,z" → matId (для проверки соседей) const voxMap = new Map(); for (const v of voxels) { if (lod > 1) { // LOD: фильтруем — берём только voxel'ы кратные lod if (v.x % lod !== 0 || v.y % lod !== 0 || v.z % lod !== 0) continue; } voxMap.set(v.x + ',' + v.y + ',' + v.z, v.m); } // 2. Для каждого voxel — проверяем 6 граней // accumulators: matKey → {positions:[], normals:[], uvs:[], indices:[]} const accumulators = new Map(); function getAccum(matKey) { let a = accumulators.get(matKey); if (!a) { a = { positions: [], normals: [], uvs: [], indices: [] }; accumulators.set(matKey, a); } return a; } const step = lod; // шаг при LOD — увеличенный размер voxel'а const visualSize = voxelSize * step; for (const [key, ownMat] of voxMap) { const parts = key.split(','); const x = parseInt(parts[0], 10); const y = parseInt(parts[1], 10); const z = parseInt(parts[2], 10); const wx0 = x * voxelSize; const wy0 = y * voxelSize; const wz0 = z * voxelSize; for (let f = 0; f < 6; f++) { const face = FACES[f]; // Сосед на расстоянии step (учитываем LOD) const nx = x + face.dx * step; const ny = y + face.dy * step; const nz = z + face.dz * step; const nbMat = voxMap.get(nx + ',' + ny + ',' + nz); if (!isFaceVisible(ownMat, nbMat)) continue; const matKey = materialKey(ownMat, face.faceIdx); const accum = getAccum(matKey); const baseV = accum.positions.length / 3; const uvCorners = [[0, 1], [0, 0], [1, 0], [1, 1]]; for (let i = 0; i < 4; i++) { const c = face.corners[i]; accum.positions.push( wx0 + c[0] * visualSize, wy0 + c[1] * visualSize, wz0 + c[2] * visualSize, ); accum.normals.push(face.nx, face.ny, face.nz); const uv = uvCorners[i]; accum.uvs.push(uv[0], uv[1]); } accum.indices.push(baseV, baseV + 1, baseV + 2); accum.indices.push(baseV, baseV + 2, baseV + 3); } } // 3. Конвертируем в Typed Arrays (для Transferable) const result = {}; const transferables = []; for (const [matKey, accum] of accumulators) { const positions = new Float32Array(accum.positions); const normals = new Float32Array(accum.normals); const uvs = new Float32Array(accum.uvs); const indices = new Uint32Array(accum.indices); result[matKey] = { positions, normals, uvs, indices }; transferables.push(positions.buffer, normals.buffer, uvs.buffer, indices.buffer); } return { result, transferables }; } // ============================================================================ // Worker message handler // ============================================================================ self.onmessage = (e) => { const msg = e.data; if (msg.type === 'build') { try { const t0 = performance.now(); const { result, transferables } = buildRegionGeometry(msg); const dt = performance.now() - t0; self.postMessage( { type: 'built', regionId: msg.regionId, byMaterial: result, timeMs: dt, }, transferables, ); } catch (err) { self.postMessage({ type: 'error', regionId: msg.regionId, error: err.message, }); } } };