studio/src/editor/engine/TerrainRegionWorker.js
МИН 31adbf151b Initial public release: Студия Рублокса v1.0
Open-source веб-студия для создания игр Рублокса, двойная лицензия
AGPL-3.0 + Коммерческая.

Главное:
- Vite 5 + React 18 + Babylon 7.54.3 + Monaco Editor + Colyseus 0.16
- Самодостаточный движок ~28к строк (66 файлов): BlockManager,
  TerrainVoxelBuilder, ModelManager, DecoManager, PlayerController,
  ScriptSandboxWorker, MultiplayerSync, 30+ GD-гейммодов
- Главный редактор KubikonEditor (~37к строк) + панели, ScriptEditor (Monaco)
- Витрина игр (KubikonFeed, KubikonStudio, KubikonDocs, KubikonLearn)
- Geometry Dash sub-app (GdMenu, GdShop, GdRules, GdCoverArt)
- 10 admin-preview каталогов для дизайнеров (скины, музыка, порталы и т.д.)
- Конфигурируемый бэкенд через VITE_API_BASE — работает со staging
  (dev-api.rublox.pro) без настройки
- Standalone-режим (VITE_STANDALONE=true) — открыть пустой редактор без бэка
- Полная документация (на русском): README, ARCHITECTURE, CONTRIBUTING,
  SECURITY, CHANGELOG
- ESLint + Prettier + EditorConfig
- Legal: LICENSE (AGPL-3.0), LICENSE-COMMERCIAL.md, CLA.md, COPYRIGHT.md
- Issue templates: bug_report, feature_request, security_disclosure

Перед публикацией:
- Все импорты из minecraftia заменены на локальные
- Все хардкоды URL (minecraftia-school.ru) и внутренних IP убраны → env
- Admin-эндпоинты Kubikon3DService вырезаны (остаются в приватном репо)
- AdminKubikonModeration не публикуется (модерация — в team.rublox.pro)
- 93 МБ ассетов public/kubikon-assets вынесены в .gitignore
  (раздаются через release artifact)
2026-05-27 23:41:10 +03:00

188 lines
7.8 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.

/* 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,
});
}
}
};