/** * BlockManager — управление voxel-блоками в сцене Babylon.js. * * Поддерживает: * 1. Простые кубы — одна текстура на все 6 граней (поле `texture`). * 2. Мульти-материал кубы — разные текстуры на верх/стороны/низ * (поля `top`, `side`, `bottom`). Используется для травы, бревна, * печки, тыквы. * * Все блоки 1×1×1 на целочисленных координатах: блок (gridX, gridY, gridZ) * имеет центр в (gridX, gridY + 0.5, gridZ) — низ на y=gridY. * * Public API: * addBlock(x, y, z, blockTypeId) — поставить * removeBlock(x, y, z) — удалить * removeBlockByMesh(mesh) * hasBlock(x, y, z) * count() / clear() * serialize() / loadFromArray() * * Материалы и текстуры кешируются по типу блока. */ import { MeshBuilder, StandardMaterial, MultiMaterial, SubMesh, Texture, DynamicTexture, Color3, Color4, Vector3, Matrix, Mesh, VertexData, VertexBuffer, } from '@babylonjs/core'; import { getBlockType } from './BlockTypes'; /** * Какой материал применить к какой грани куба Babylon-MeshBuilder.CreateBox. * * Babylon BoxBuilder делит куб на 12 треугольников = 6 граней по 2 треугольника. * Порядок граней в индексах: * 0: +Z (front) * 1: -Z (back) * 2: +X (right) * 3: -X (left) * 4: +Y (top) * 5: -Y (bottom) * * Каждая грань = 2 треугольника = 6 индексов. На каждую — отдельный submesh. */ const FACE_INDEX_MAP = { front: 0, back: 1, right: 2, left: 3, top: 4, bottom: 5, }; export class BlockManager { constructor(scene) { this.scene = scene; // Логическая запись блоков: "x,y,z" → metadata (без отдельного меша!) // Это позволяет не создавать тысячи individual mesh'ей и использовать // thinInstances. Для совместимости со старым кодом ниже мы создаём // legacy-обёртку (Map "x,y,z" → mesh-proxy) — proxy это переиспользуемый // невидимый pick-mesh, который заполняется при pick/select. this.blocks = new Map(); // Кеш материалов по типу блока — { material, isMulti } this._materials = new Map(); // Prototype-меши для thinInstances: blockTypeId → Mesh (с thinInstances). this._protoMeshes = new Map(); // Для каждого blockTypeId — массив логических ключей по индексу инстанса: // _instanceKeys.get(typeId)[idx] === "x,y,z" (или null для удалённых, см. _freeSlots) this._instanceKeys = new Map(); // Свободные слоты в массиве thin-instances (после удаления) — переиспользуем. this._freeSlots = new Map(); // blockTypeId → [idx, idx, ...] // Обратный индекс: "x,y,z" → { typeId, idx } для быстрого pick/remove this._cellToInst = new Map(); this._onChange = null; // Callback вызывается когда создаётся новый proto-меш — BabylonScene // подписывается чтобы зарегистрировать его как shadow caster. this._onProtoCreated = null; // Жидкости — отдельный single-mesh водной поверхности с волнами this._waterMeshes = new Set(); // меши-«невидимки» блоков воды (для логики) this._lavaMeshes = new Set(); this._waterSurface = null; // единый mesh всей воды this._waterSurfaceBaseY = null; // массив исходных Y верхних вершин (для волн) this._waterDirty = false; // нужна ли пересборка surface-меша this._lavaSurface = null; this._lavaSurfaceBaseY = null; this._lavaDirty = false; this._animTime = 0; } /** Вызывать каждый кадр для анимации воды/лавы. */ tick(dt) { this._animTime += dt; // Пересборка single-mesh при изменениях if (this._waterDirty) { this._rebuildLiquidSurface('water'); this._waterDirty = false; } if (this._lavaDirty) { this._rebuildLiquidSurface('lava'); this._lavaDirty = false; } // Per-block bobbing — одна формула sin на блок, считается каждый кадр. // Это дёшево (≤ 5000 операций для крупного моря) и обеспечивает плавность. // Skip только для гигантских поверхностей > 8000 блоков. this._waterAnimFrame = ((this._waterAnimFrame || 0) + 1) | 0; const waterCount = this._waterSurfaceBaseY?.length || 0; if (waterCount > 0) { const skip = waterCount > 8000 ? 2 : 1; if (this._waterAnimFrame % skip === 0) { this._animateLiquidSurface(this._waterSurface, this._waterSurfaceBaseY, 2.0, 0.10); } } const lavaCount = this._lavaSurfaceBaseY?.length || 0; if (lavaCount > 0) { const skip = lavaCount > 8000 ? 2 : 1; if (this._waterAnimFrame % skip === 0) { // Лава — медленная (freq=0.7) и амплитуда чуть меньше воды (0.07). // Та же per-block bobbing-механика что и у воды. this._animateLiquidSurface(this._lavaSurface, this._lavaSurfaceBaseY, 0.7, 0.07); } } } _animateLiquidSurface(mesh, baseY, freq, amp) { if (!mesh || mesh.isDisposed?.()) return; const positions = mesh.getVerticesData(VertexBuffer.PositionKind); if (!positions || !baseY) return; const t = this._animTime; // Per-block bobbing: одна высота волны на блок (4 вершины двигаются синхронно). // Фаза = пространственная (x*0.6+z*0.4) → волна плавно бежит через поверхность. for (let k = 0; k < baseY.length; k++) { const grp = baseY[k]; const wave = Math.sin(t * freq + grp.phase) * amp; const newY = grp.y + wave; if (Array.isArray(grp.i)) { positions[grp.i[0] * 3 + 1] = newY; positions[grp.i[1] * 3 + 1] = newY; positions[grp.i[2] * 3 + 1] = newY; positions[grp.i[3] * 3 + 1] = newY; } else { positions[grp.i * 3 + 1] = newY; } } mesh.updateVerticesData(VertexBuffer.PositionKind, positions); } /** Пересоздать single-mesh для воды или лавы. */ _rebuildLiquidSurface(kind) { const isWater = kind === 'water'; const oldMesh = isWater ? this._waterSurface : this._lavaSurface; if (oldMesh) { try { oldMesh.dispose(); } catch (e) {} } const blocks = isWater ? this._waterMeshes : this._lavaMeshes; if (blocks.size === 0) { if (isWater) { this._waterSurface = null; this._waterSurfaceBaseY = null; } else { this._lavaSurface = null; this._lavaSurfaceBaseY = null; } return; } // Собираем set координат блоков для проверки соседей const set = new Set(); for (const m of blocks) { const md = m.metadata; set.add(`${md.gridX},${md.gridY},${md.gridZ}`); } const has = (x, y, z) => set.has(`${x},${y},${z}`); const positions = []; const indices = []; const normals = []; const uvs = []; // baseY теперь — массив групп { i: [v0..v3], y: baseY, phase, freq } // Каждая группа = один блок воды с верхней гранью; все 4 вершины // двигаются вместе → блок остаётся плоским квадратом, без сглаживания. const baseY = []; let vIdx = 0; // Для каждого блока — добавляем грани, у которых нет соседа той же жидкости. for (const m of blocks) { const md = m.metadata; const x = md.gridX, y = md.gridY, z = md.gridZ; // ВЕРХ — рисуем если выше нет блока этой же жидкости if (!has(x, y + 1, z)) { this._addQuad(positions, indices, normals, uvs, x - 0.5, y + 1, z - 0.5, x + 0.5, y + 1, z - 0.5, x + 0.5, y + 1, z + 0.5, x - 0.5, y + 1, z + 0.5, 0, 1, 0, vIdx); // Записываем группу из 4 вершин с фазой, плавно зависящей от координат // блока. Главная компонента — `x*0.6 + z*0.4` — даёт волну, которая // непрерывно «бежит» через поверхность (соседи отличаются мало). // Поверх — крошечный рандом, чтобы блоки не были полностью синхронны. const seed = (x * 73856093) ^ (z * 83492791); const r1 = ((seed * 1103515245 + 12345) >>> 16) & 0x7FFF; const jitter = (r1 / 0x7FFF - 0.5) * 0.5; // ±0.25 рад const phase = x * 0.6 + z * 0.4 + jitter; baseY.push({ i: [vIdx, vIdx + 1, vIdx + 2, vIdx + 3], y: y + 1, phase, }); vIdx += 4; } // НИЗ — рисуем если ниже нет блока этой же жидкости (часто пол) if (!has(x, y - 1, z)) { this._addQuad(positions, indices, normals, uvs, x - 0.5, y, z + 0.5, x + 0.5, y, z + 0.5, x + 0.5, y, z - 0.5, x - 0.5, y, z - 0.5, 0, -1, 0, vIdx); vIdx += 4; } // СТОРОНЫ if (!has(x + 1, y, z)) { this._addQuad(positions, indices, normals, uvs, x + 0.5, y, z - 0.5, x + 0.5, y, z + 0.5, x + 0.5, y + 1, z + 0.5, x + 0.5, y + 1, z - 0.5, 1, 0, 0, vIdx); vIdx += 4; } if (!has(x - 1, y, z)) { this._addQuad(positions, indices, normals, uvs, x - 0.5, y, z + 0.5, x - 0.5, y, z - 0.5, x - 0.5, y + 1, z - 0.5, x - 0.5, y + 1, z + 0.5, -1, 0, 0, vIdx); vIdx += 4; } if (!has(x, y, z + 1)) { this._addQuad(positions, indices, normals, uvs, x + 0.5, y, z + 0.5, x - 0.5, y, z + 0.5, x - 0.5, y + 1, z + 0.5, x + 0.5, y + 1, z + 0.5, 0, 0, 1, vIdx); vIdx += 4; } if (!has(x, y, z - 1)) { this._addQuad(positions, indices, normals, uvs, x - 0.5, y, z - 0.5, x + 0.5, y, z - 0.5, x + 0.5, y + 1, z - 0.5, x - 0.5, y + 1, z - 0.5, 0, 0, -1, vIdx); vIdx += 4; } } const mesh = new Mesh(`liquidSurface_${kind}`, this.scene); const vd = new VertexData(); vd.positions = positions; vd.indices = indices; vd.normals = normals; vd.uvs = uvs; vd.applyToMesh(mesh, true); // updatable=true — будем менять positions mesh.isPickable = false; mesh.alphaIndex = 100; // рендерить после непрозрачных mesh.material = isWater ? this._buildWaterMaterial() : this._buildLavaMaterial(); // Метим что это жидкость — игрок не должен по нему «коллидить» mesh.metadata = { isLiquidSurface: true, kind }; if (isWater) { this._waterSurface = mesh; this._waterSurfaceBaseY = baseY; } else { this._lavaSurface = mesh; this._lavaSurfaceBaseY = baseY; } } _addQuad(positions, indices, normals, uvs, ax, ay, az, bx, by, bz, cx, cy, cz, dx, dy, dz, nx, ny, nz, vIdx) { positions.push(ax, ay, az, bx, by, bz, cx, cy, cz, dx, dy, dz); normals.push(nx, ny, nz, nx, ny, nz, nx, ny, nz, nx, ny, nz); // UV — мировые координаты делёные на 1 (плитка совпадает с блоком) // Для верха/низа берём X/Z, для боков — соответствующие. if (Math.abs(ny) > 0.5) { uvs.push(ax, az, bx, bz, cx, cz, dx, dz); } else if (Math.abs(nx) > 0.5) { uvs.push(az, ay, bz, by, cz, cy, dz, dy); } else { uvs.push(ax, ay, bx, by, cx, cy, dx, dy); } indices.push(vIdx, vIdx + 1, vIdx + 2, vIdx, vIdx + 2, vIdx + 3); } _buildWaterMaterial() { // Кешируем — один материал на всю сцену if (this._waterMatCache) return this._waterMatCache; const mat = new StandardMaterial('waterSurfaceMat', this.scene); // Простой материал без текстуры — анимация даётся per-block bobbing. // Чистый цвет без тайлинга = никаких швов и решёток. mat.diffuseColor = new Color3(0.30, 0.62, 0.92); // disableLighting=true — без освещения нет «диагональных» полос от // треугольных триангуляций quad'ов (когда нормали вершин слегка разъехались // после анимации). Вода становится плоского цвета — это и есть Roblox-look. mat.disableLighting = true; mat.emissiveColor = new Color3(0.30, 0.62, 0.92); // дублируем как «светящийся» цвет mat.alpha = 0.78; mat.backFaceCulling = false; // disableDepthWrite=true — устраняет alpha-blend «полоски» mat.disableDepthWrite = true; this._waterMatCache = mat; return mat; } _buildLavaMaterial() { // Кешируем — один материал на сцену if (this._lavaMatCache) return this._lavaMatCache; const mat = new StandardMaterial('lavaSurfaceMat', this.scene); // По аналогии с водой: чистый цвет + disableLighting → ровный flat shading, // никаких швов/диагоналей. Per-block bobbing анимирует поверхность. mat.diffuseColor = new Color3(1.0, 0.45, 0.08); mat.disableLighting = true; mat.emissiveColor = new Color3(1.0, 0.45, 0.08); // лава светится mat.specularColor = new Color3(0, 0, 0); mat.alpha = 0.95; mat.backFaceCulling = false; mat.disableDepthWrite = true; this._lavaMatCache = mat; return mat; } /** Установить колбэк изменения (BabylonScene → KubikonEditor.markDirty). */ setOnChange(cb) { this._onChange = cb; } _notifyChange() { // Инвалидируем кэш высоты поверхности (используется ZombieManager). // Просто инкрементим версию — старые записи в _surfaceCache при // следующем чтении не пройдут проверку cached.v === version. this._surfaceCacheVersion = (this._surfaceCacheVersion || 0) + 1; if (this._onChange) this._onChange(); } _key(x, y, z) { return `${Math.round(x)},${Math.round(y)},${Math.round(z)}`; } /** * Создать StandardMaterial с одной текстурой и Minecraft-параметрами. */ _createSingleMat(blockType, texturePath, name) { const mat = new StandardMaterial(name, this.scene); mat.specularColor = new Color3(0, 0, 0); if (texturePath) { const tex = new Texture(texturePath, this.scene); tex.updateSamplingMode(Texture.NEAREST_SAMPLINGMODE); mat.diffuseTexture = tex; if (blockType.alpha != null && blockType.alpha < 1) { mat.diffuseTexture.hasAlpha = true; mat.useAlphaFromDiffuseTexture = true; mat.alpha = blockType.alpha; } if (Array.isArray(blockType.emissive)) { mat.emissiveColor = new Color3( blockType.emissive[0], blockType.emissive[1], blockType.emissive[2] ); } // Запоминаем материалы жидкостей для анимации (uOffset/vOffset) if (blockType.isWater && !this._waterMat) { this._waterMat = mat; tex.wrapU = Texture.WRAP_ADDRESSMODE; tex.wrapV = Texture.WRAP_ADDRESSMODE; } if (blockType.isLava && !this._lavaMat) { this._lavaMat = mat; tex.wrapU = Texture.WRAP_ADDRESSMODE; tex.wrapV = Texture.WRAP_ADDRESSMODE; } } else { mat.diffuseColor = Color3.FromHexString(blockType.color || '#888'); } return mat; } /** * Получить (создать если нет) материал для типа блока. * Возвращает: { material, isMulti } — где material это StandardMaterial * (для simple куба) или MultiMaterial (для куба с разными гранями). */ _getMaterial(blockTypeId) { if (this._materials.has(blockTypeId)) { return this._materials.get(blockTypeId); } const blockType = getBlockType(blockTypeId); let entry; if (blockType.top || blockType.side || blockType.bottom) { // Мульти-материал куб const matTop = this._createSingleMat(blockType, blockType.top || blockType.side, `mat_${blockTypeId}_top`); const matSide = this._createSingleMat(blockType, blockType.side || blockType.top, `mat_${blockTypeId}_side`); const matBottom = this._createSingleMat(blockType, blockType.bottom || blockType.side, `mat_${blockTypeId}_bot`); const multi = new MultiMaterial(`multi_${blockTypeId}`, this.scene); multi.subMaterials[FACE_INDEX_MAP.front] = matSide; multi.subMaterials[FACE_INDEX_MAP.back] = matSide; multi.subMaterials[FACE_INDEX_MAP.right] = matSide; multi.subMaterials[FACE_INDEX_MAP.left] = matSide; multi.subMaterials[FACE_INDEX_MAP.top] = matTop; multi.subMaterials[FACE_INDEX_MAP.bottom] = matBottom; // Замораживаем sub-материалы — Babylon перестанет проверять // их dirty-флаги каждый кадр (мы их уже не меняем). try { matSide.freeze?.(); matTop.freeze?.(); matBottom.freeze?.(); } catch (e) {} entry = { material: multi, isMulti: true }; } else { // Простой куб с одной текстурой const mat = this._createSingleMat(blockType, blockType.texture, `mat_${blockTypeId}`); try { mat.freeze?.(); } catch (e) {} entry = { material: mat, isMulti: false }; } this._materials.set(blockTypeId, entry); return entry; } /** * Поставить блок в (x, y, z). * Возвращает meshProxy (для совместимости со старым API) или null если занято. * * Производительность: для обычных блоков используем thinInstances — * один meshes-prototype на тип блока, тысячи позиций в одном draw call. * Жидкости (water/lava) идут по старому пути — у них свой single-surface. */ addBlock(x, y, z, blockTypeId) { const ix = Math.round(x); const iy = Math.round(y); const iz = Math.round(z); const key = this._key(ix, iy, iz); if (this.blocks.has(key)) return null; const typeDef = getBlockType(blockTypeId); const isWater = !!typeDef?.isWater; const isLava = !!typeDef?.isLava; // Для жидкостей оставляем старую логику: невидимый куб + единый surface if (isWater || isLava) { const mesh = MeshBuilder.CreateBox(`block_${key}`, { size: 1 }, this.scene); mesh.position = new Vector3(ix, iy + 0.5, iz); mesh.isPickable = false; // surface ловит pick mesh.metadata = { isBlock: true, blockTypeId, gridX: ix, gridY: iy, gridZ: iz, anchored: true, canCollide: false, visible: true, isWater, isLava, mass: 1, folderId: null, _liquidProxy: true, // признак proxy-mesh (не настоящий блок) }; mesh.setEnabled(false); if (isWater) { this._waterMeshes.add(mesh); this._waterDirty = true; } else { this._lavaMeshes.add(mesh); this._lavaDirty = true; } this.blocks.set(key, mesh); this._notifyChange(); return mesh; } // === ОБЫЧНЫЕ БЛОКИ — через thinInstances === const proto = this._getOrCreateProto(blockTypeId); if (!proto) return null; // Берём свободный слот или append let idx; const freeList = this._freeSlots.get(blockTypeId); const matrix = Matrix.Translation(ix, iy + 0.5, iz); // refresh=false и для set, и для add — финальный refresh делаем один раз в loadFromArray // или при первом render-кадре через thinInstanceBufferUpdated. if (freeList && freeList.length > 0) { idx = freeList.pop(); proto.thinInstanceSetMatrixAt(idx, matrix, !this._batchMode); } else { idx = proto.thinInstanceAdd(matrix, !this._batchMode); } // Сохраняем обратный индекс const keysArr = this._instanceKeys.get(blockTypeId); keysArr[idx] = key; this._cellToInst.set(key, { typeId: blockTypeId, idx }); // Логический «meshProxy» — объект, имитирующий API mesh для совместимости. // НЕ создаёт реального меша. Используется selection / removeBlockByMesh. const meshProxy = { _isBlockProxy: true, metadata: { isBlock: true, blockTypeId, gridX: ix, gridY: iy, gridZ: iz, anchored: true, canCollide: true, visible: true, isWater: false, isLava: false, mass: 1, folderId: null, _thinIdx: idx, }, // Минимальные методы, которые ожидает остальной код position: new Vector3(ix, iy + 0.5, iz), isDisposed: () => !this.blocks.has(key), dispose: () => this.removeBlock(ix, iy, iz), setEnabled: () => { /* видимость через thinInstance scaling, упрощённо */ }, getTotalVertices: () => 36, // Pick-helper: возвращает proto-меш и индекс инстанса (для подсветки) _proto: proto, _thinIdx: idx, }; this.blocks.set(key, meshProxy); this._notifyChange(); return meshProxy; } /** Получить или создать prototype-меш для типа блока. */ _getOrCreateProto(blockTypeId) { let proto = this._protoMeshes.get(blockTypeId); if (proto) return proto; proto = MeshBuilder.CreateBox(`proto_${blockTypeId}`, { size: 1 }, this.scene); const { material, isMulti } = this._getMaterial(blockTypeId); proto.material = material; if (isMulti) this._setupSubmeshes(proto); proto.checkCollisions = false; // коллизии через thin-instances не работают штатно; // PlayerController в Play-режиме читает blocks Map напрямую (см. PhysicsAABB). proto.isPickable = true; proto.thinInstanceEnablePicking = true; // alwaysSelectAsActiveMesh = true: на сцене ~500 блоков, прото — это // mesh с гигантским bounding box покрывающим всю карту. Frustum-test // на нём всё равно почти всегда true → пропускаем дорогие проверки. proto.alwaysSelectAsActiveMesh = true; // doNotSyncBoundingInfo = true — не пересчитывать bbox при изменениях // thin-instances (мы не используем bbox для frustum, см. выше). proto.doNotSyncBoundingInfo = true; proto.metadata = { _isBlockProto: true, blockTypeId, }; // Тени: блоки принимают тени от других объектов (персонажа, // деревьев, моделей). Сами блоки автоматически становятся // shadow casters через addShadowCaster в refreshAllShadows. proto.receiveShadows = true; this._protoMeshes.set(blockTypeId, proto); this._instanceKeys.set(blockTypeId, []); this._freeSlots.set(blockTypeId, []); if (typeof this._onProtoCreated === 'function') { try { this._onProtoCreated(proto); } catch (e) { /* ignore */ } } return proto; } /** Подписка для уведомлений о создании prototype-меша (для shadow setup). */ setOnProtoCreated(cb) { this._onProtoCreated = cb; } /** Установить флаг anchored у блока. */ setBlockAnchored(x, y, z, anchored) { const mesh = this.blocks.get(this._key(x, y, z)); if (!mesh) return; mesh.metadata.anchored = !!anchored; // Поддерживаем Set _unanchoredBlocks — для O(1)-доступа в PhysicsAABB. if (!this._unanchoredBlocks) this._unanchoredBlocks = new Set(); if (anchored) this._unanchoredBlocks.delete(mesh); else this._unanchoredBlocks.add(mesh); this._notifyChange(); } /** Установить свойства блока (canCollide / visible / mass). */ setBlockProps(x, y, z, patch) { const key = this._key(x, y, z); const mesh = this.blocks.get(key); if (!mesh) return; if (patch.canCollide !== undefined) mesh.metadata.canCollide = !!patch.canCollide; if (patch.visible !== undefined) { mesh.metadata.visible = !!patch.visible; // Для thin-instance proxy: переключаем матрицу (нормальная ↔ scale=0) if (mesh._isBlockProxy) { const inst = this._cellToInst.get(key); if (inst) { const proto = this._protoMeshes.get(inst.typeId); if (proto) { const m = mesh.metadata.visible ? Matrix.Translation(mesh.metadata.gridX, mesh.metadata.gridY + 0.5, mesh.metadata.gridZ) : Matrix.Scaling(0, 0, 0); proto.thinInstanceSetMatrixAt(inst.idx, m, true); } } } else if (typeof mesh.setEnabled === 'function') { mesh.setEnabled(mesh.metadata.visible); } } if (patch.mass !== undefined) { const m = Number(patch.mass); if (Number.isFinite(m) && m > 0) mesh.metadata.mass = m; } this._notifyChange(); } /** * Разбить cube-mesh на 6 SubMesh (по одной на грань). * Babylon CreateBox использует 36 индексов (12 треугольников = 36 вершин-индексов), * по 6 индексов на грань. Грани идут в порядке +Z, -Z, +X, -X, +Y, -Y. */ _setupSubmeshes(mesh) { const verticesCount = mesh.getTotalVertices(); mesh.subMeshes = []; const indicesPerFace = 6; for (let face = 0; face < 6; face++) { new SubMesh( face, // material index 0, // verticesStart verticesCount, // verticesCount face * indicesPerFace, // indexStart indicesPerFace, // indexCount mesh ); } } removeBlock(x, y, z) { const key = this._key(x, y, z); const mesh = this.blocks.get(key); if (!mesh) return false; // Удаляем из set unanchored если он там был — иначе утечка. if (this._unanchoredBlocks) this._unanchoredBlocks.delete(mesh); // Жидкости — старый путь: dispose + dirty if (mesh.metadata?._liquidProxy) { if (mesh.metadata.isWater) this._waterDirty = true; if (mesh.metadata.isLava) this._lavaDirty = true; this._waterMeshes.delete(mesh); this._lavaMeshes.delete(mesh); try { mesh.dispose(); } catch (e) { /* ignore */ } this.blocks.delete(key); this._notifyChange(); return true; } // Обычный блок — thin instance: «погасить» матрицу (схлопнуть в 0) и пометить слот свободным const inst = this._cellToInst.get(key); if (inst) { const proto = this._protoMeshes.get(inst.typeId); if (proto) { // Прячем экземпляр через scale=0 — его не видно, но остаётся в буфере const zeroMat = Matrix.Scaling(0, 0, 0); proto.thinInstanceSetMatrixAt(inst.idx, zeroMat, true); } const keysArr = this._instanceKeys.get(inst.typeId); if (keysArr) keysArr[inst.idx] = null; const free = this._freeSlots.get(inst.typeId); if (free) free.push(inst.idx); this._cellToInst.delete(key); } this.blocks.delete(key); this._notifyChange(); return true; } hasBlock(x, y, z) { return this.blocks.has(this._key(x, y, z)); } removeBlockByMesh(mesh) { if (!mesh) return false; // Прокси-mesh (water/lava или старый legacy) if (mesh.metadata?.isBlock) { const { gridX, gridY, gridZ } = mesh.metadata; return this.removeBlock(gridX, gridY, gridZ); } return false; } /** * Найти proxy-блок по результату raycast'а. * Если попало в proto-меш с thinInstanceIndex — возвращает соответствующий proxy. */ findProxyByPickInfo(pickInfo) { if (!pickInfo || !pickInfo.pickedMesh) return null; const m = pickInfo.pickedMesh; if (m.metadata?._isBlockProto) { // В разных версиях Babylon индекс инстанса называется по-разному. // Пробуем все известные варианты. const idx = (typeof pickInfo.thinInstanceIndex === 'number' && pickInfo.thinInstanceIndex >= 0) ? pickInfo.thinInstanceIndex : (typeof pickInfo.instanceIndex === 'number' && pickInfo.instanceIndex >= 0) ? pickInfo.instanceIndex : -1; const typeId = m.metadata.blockTypeId; if (idx >= 0) { const keysArr = this._instanceKeys.get(typeId); const key = keysArr ? keysArr[idx] : null; if (key) { const proxy = this.blocks.get(key); if (proxy) return proxy; } } // Fallback: ищем по точке контакта. Учитываем нормаль грани — // точка лежит на поверхности блока, нужно сместиться "внутрь" // на 0.01 по противоположному направлению нормали и взять клетку. const p = pickInfo.pickedPoint; if (!p) return null; // Нормаль грани (если доступна) let nx = 0, ny = 0, nz = 0; try { const n = pickInfo.getNormal && pickInfo.getNormal(true); if (n) { nx = n.x; ny = n.y; nz = n.z; } } catch (e) { /* ignore */ } // Точка чуть «внутрь» блока против направления нормали const innerX = p.x - nx * 0.05; const innerY = p.y - ny * 0.05; const innerZ = p.z - nz * 0.05; // Координаты клетки: блок (gx, gy, gz) занимает Y от gy до gy+1, // X/Z центр на gx (от gx-0.5 до gx+0.5). const gx = Math.round(innerX); const gy = Math.floor(innerY); const gz = Math.round(innerZ); const key = this._key(gx, gy, gz); if (this.blocks.has(key)) return this.blocks.get(key); // Расширенный поиск в окрестности (на случай погрешностей) for (let ddy = -1; ddy <= 1; ddy++) { for (let ddx = -1; ddx <= 1; ddx++) { for (let ddz = -1; ddz <= 1; ddz++) { if (ddx === 0 && ddy === 0 && ddz === 0) continue; const k2 = this._key(gx + ddx, gy + ddy, gz + ddz); if (this.blocks.has(k2)) { const proxy = this.blocks.get(k2); // Только если это блок ИМЕННО того же типа что прото (точно тот меш) if (proxy?.metadata?.blockTypeId === typeId) return proxy; } } } } return null; } if (m.metadata?.isBlock) return m; // legacy/liquid proxy return null; } count() { return this.blocks.size; } serialize() { const out = []; for (const mesh of this.blocks.values()) { const m = mesh.metadata; out.push({ x: m.gridX, y: m.gridY, z: m.gridZ, type: m.blockTypeId, anchored: m.anchored !== false, canCollide: m.canCollide !== false, visible: m.visible !== false, mass: m.mass ?? 1, }); } return out; } loadFromArray(arr) { this.clear(); // Массовый режим — буферы thinInstances обновляются один раз в конце this._batchMode = true; try { for (const b of arr) { const mesh = this.addBlock(b.x, b.y, b.z, b.type); if (!mesh) continue; if (b.anchored === false) { mesh.metadata.anchored = false; if (!this._unanchoredBlocks) this._unanchoredBlocks = new Set(); this._unanchoredBlocks.add(mesh); } if (b.canCollide === false) mesh.metadata.canCollide = false; if (b.visible === false) { mesh.metadata.visible = false; // setBlockProps вызовет setMatrixAt — но мы в batchMode, так что без refresh } if (b.mass != null) mesh.metadata.mass = b.mass; } } finally { this._batchMode = false; } // Финальный refresh всех буферов thinInstances + bounding info for (const proto of this._protoMeshes.values()) { try { if (proto.thinInstanceBufferUpdated) proto.thinInstanceBufferUpdated('matrix'); proto.thinInstanceRefreshBoundingInfo(true); } catch (e) { /* ignore */ } } } clear() { const had = this.blocks.size > 0; // Жидкости-проксы — dispose for (const mesh of this.blocks.values()) { if (mesh && mesh.metadata?._liquidProxy && typeof mesh.dispose === 'function') { try { mesh.dispose(); } catch (e) { /* ignore */ } } } // Прототипы thin-instances — сбрасываем все instances но сами proto оставляем // (можно переиспользовать; материалы тоже остаются) for (const proto of this._protoMeshes.values()) { try { proto.thinInstanceCount = 0; } catch (e) { /* ignore */ } } for (const arr of this._instanceKeys.values()) arr.length = 0; for (const arr of this._freeSlots.values()) arr.length = 0; this._cellToInst.clear(); this.blocks.clear(); this._waterMeshes.clear(); this._lavaMeshes.clear(); this._waterDirty = true; this._lavaDirty = true; if (had) this._notifyChange(); } dispose() { this.clear(); // Дисозим proto-меши блоков for (const proto of this._protoMeshes.values()) { try { proto.dispose(); } catch (e) { /* ignore */ } } this._protoMeshes.clear(); this._instanceKeys.clear(); this._freeSlots.clear(); for (const entry of this._materials.values()) { const mat = entry.material; if (entry.isMulti) { // MultiMaterial — диспозим под-материалы и их текстуры for (const sub of mat.subMaterials) { if (sub) { if (sub.diffuseTexture) sub.diffuseTexture.dispose(); sub.dispose(); } } } else { if (mat.diffuseTexture) mat.diffuseTexture.dispose(); } mat.dispose(); } this._materials.clear(); } }