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)
188 lines
7.8 KiB
JavaScript
188 lines
7.8 KiB
JavaScript
/* 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,
|
||
});
|
||
}
|
||
}
|
||
};
|