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)
506 lines
22 KiB
JavaScript
506 lines
22 KiB
JavaScript
/**
|
||
* GdGroundSkin — 3D-травинки, 3D-цветы и неоновая полоса для GD-уровней.
|
||
*
|
||
* Этап G2 плана RUBLOX_GD_GRAPHICS_PLAN.md (v2 — настоящие 3D-меши).
|
||
*
|
||
* 1. Grass tufts — пучки травы из 2-3 пересекающихся текстурированных
|
||
* плоскостей. Через thin instances ~600 пучков вдоль трассы.
|
||
* Анимация: лёгкое колебание sin как от ветра.
|
||
* 2. Flowers — 3D-цветы (стебель + 5-лепестковая шапка) ~80 шт.
|
||
* 3. Neon edge — тонкая светящаяся полоса вдоль края пола.
|
||
*
|
||
* Использование:
|
||
* const gs = new GdGroundSkin();
|
||
* gs.attach(scene, levelWidth);
|
||
* gs.dispose();
|
||
*/
|
||
import {
|
||
MeshBuilder, StandardMaterial, Color3, Color4, Vector3, Matrix, Quaternion,
|
||
DynamicTexture, Mesh, VertexData,
|
||
} from '@babylonjs/core';
|
||
|
||
const NEON_EDGE_Y = 0.55;
|
||
const NEON_EDGE_Z = -1.51;
|
||
const GROUND_TOP_Y = 1.0; // верх grass-блока (блоки лежат от y=0 до y=1)
|
||
const TUFT_DENSITY = 1.2; // ~1.2 пучка на метр X
|
||
const FLOWER_DENSITY = 0.15;
|
||
|
||
export class GdGroundSkin {
|
||
constructor() {
|
||
this.scene = null;
|
||
this._grassTuftsProto = null; // прототип меша пучка
|
||
this._grassMatrices = null; // Float32Array с матрицами всех инстансов
|
||
this._grassCount = 0;
|
||
this._grassBaseRotations = null; // Float32Array — базовый угол колебания для каждого инстанса
|
||
this._grassBasePos = null; // [{x,y,z,scale,baseRotY}] для пересчёта матриц при ветре
|
||
this._flowersProto = null;
|
||
this._neonEdge = null;
|
||
this._neonEdgeBack = null;
|
||
this._onBeforeRender = null;
|
||
this._windT = 0;
|
||
}
|
||
|
||
attach(scene, levelWidth = 1000, shadowGenerator = null, scene3d = null) {
|
||
if (!scene) return;
|
||
this.scene = scene;
|
||
this._scene3d = scene3d;
|
||
this._createGrassTufts(levelWidth);
|
||
this._createFlowers(levelWidth);
|
||
this._createFakeShadows(levelWidth);
|
||
this._createPits(levelWidth);
|
||
this._setupWind();
|
||
}
|
||
|
||
/** Сегменты bottom-cover ПОД grass-блоками + НЕТ ничего над ямами.
|
||
* Так в ямах ничего не загораживает — видна голубое небо/skybox (бездна). */
|
||
_createPits(levelWidth) {
|
||
const floorXs = this._collectFloorXs();
|
||
if (floorXs.size === 0) return;
|
||
const minX = Math.min(...floorXs);
|
||
const maxX = Math.max(...floorXs);
|
||
// Найти непрерывные сегменты пола (где блоки ЕСТЬ)
|
||
const segments = [];
|
||
let segStart = null;
|
||
for (let x = minX; x <= maxX + 1; x++) {
|
||
if (floorXs.has(x)) {
|
||
if (segStart == null) segStart = x;
|
||
} else {
|
||
if (segStart != null) {
|
||
segments.push({ start: segStart, end: x - 1 });
|
||
segStart = null;
|
||
}
|
||
}
|
||
}
|
||
if (segments.length === 0) return;
|
||
|
||
// Тёмный материал «земля под полом»
|
||
const mat = new StandardMaterial('gd_undersole_mat', this.scene);
|
||
mat.diffuseColor = new Color3(0.30, 0.20, 0.12);
|
||
mat.emissiveColor = new Color3(0.25, 0.17, 0.10);
|
||
mat.specularColor = new Color3(0, 0, 0);
|
||
mat.disableLighting = true;
|
||
this._pitMat = mat;
|
||
this._pits = [];
|
||
|
||
const COVER_DEPTH = 200; // от y=0 далеко вниз
|
||
for (const s of segments) {
|
||
const w = s.end - s.start + 1;
|
||
const cx = (s.start + s.end) / 2;
|
||
// Сегмент = вертикальная стенка под полом этого сегмента.
|
||
// Низ — далеко вниз (закрывает низ экрана), верх — y=0 (низ grass).
|
||
const mesh = MeshBuilder.CreateBox(`gd_undersole_${s.start}_${s.end}`, {
|
||
width: w, height: COVER_DEPTH, depth: 0.2,
|
||
}, this.scene);
|
||
mesh.position.set(cx, -COVER_DEPTH / 2, -1.55);
|
||
mesh.material = mat;
|
||
mesh.isPickable = false;
|
||
mesh.applyFog = false;
|
||
this._pits.push(mesh);
|
||
}
|
||
console.log(`[GdGroundSkin] floor segments: ${segments.length}, gaps: ${segments.length - 1}`);
|
||
}
|
||
|
||
/** Синтетические тени:
|
||
* - один follow-кружок под игроком (двигается каждый кадр).
|
||
* - статические кружки под каждым шипом/cone primitive.
|
||
* Это плоские круги тёмного цвета чуть выше пола (y=1.01). */
|
||
_createFakeShadows(levelWidth) {
|
||
const dt = this._makeShadowTexture();
|
||
const mat = new StandardMaterial('gd_shadow_mat', this.scene);
|
||
mat.diffuseTexture = dt;
|
||
mat.diffuseTexture.hasAlpha = true;
|
||
mat.emissiveTexture = dt;
|
||
mat.emissiveColor = new Color3(0, 0, 0);
|
||
mat.useAlphaFromDiffuseTexture = true;
|
||
mat.backFaceCulling = false;
|
||
mat.specularColor = new Color3(0, 0, 0);
|
||
mat.disableLighting = true;
|
||
mat.alphaMode = 2; // BABYLON.Engine.ALPHA_COMBINE
|
||
this._shadowMat = mat;
|
||
|
||
// 1. Тень игрока — следует за ним
|
||
const playerShadow = MeshBuilder.CreateGround('gd_player_shadow', {
|
||
width: 1.4, height: 1.4,
|
||
}, this.scene);
|
||
playerShadow.position.y = 1.01;
|
||
playerShadow.material = mat;
|
||
playerShadow.isPickable = false;
|
||
playerShadow.renderingGroupId = 0;
|
||
this._playerShadow = playerShadow;
|
||
|
||
// 2. Тени под примитивами (cone/sphere/box)
|
||
const pm = this._scene3d?.primitiveManager;
|
||
const staticShadows = [];
|
||
// ID куба игрока — он двигается, тень делается через _playerShadow.
|
||
// В GD-уровнях скрипт использует REF_BODY = 'primitive:10001'.
|
||
const PLAYER_CUBE_ID = 10001;
|
||
if (pm) {
|
||
for (const data of pm.instances.values()) {
|
||
if (typeof data.x !== 'number') continue;
|
||
if (data.id === PLAYER_CUBE_ID) continue; // не дублируем тень игрока
|
||
const type = String(data.type || '');
|
||
let size = 1.2;
|
||
if (type === 'cone') size = 1.1;
|
||
else if (type === 'sphere') size = 1.0;
|
||
else if (type === 'cube') size = 1.4;
|
||
else continue;
|
||
// Пропускаем кубы лежащие на y=0 (это декоративный пол)
|
||
if (type === 'cube' && data.y === 0) continue;
|
||
const sh = MeshBuilder.CreateGround(`gd_shadow_${data.id || Math.random()}`, {
|
||
width: size, height: size,
|
||
}, this.scene);
|
||
sh.position.set(data.x, 1.01, data.z || 0);
|
||
sh.material = mat;
|
||
sh.isPickable = false;
|
||
staticShadows.push(sh);
|
||
}
|
||
}
|
||
this._staticShadows = staticShadows;
|
||
console.log(`[GdGroundSkin] fake shadows: 1 player + ${staticShadows.length} static`);
|
||
}
|
||
|
||
/** Canvas с радиальным градиентом — мягкий тёмный круг. */
|
||
_makeShadowTexture() {
|
||
const S = 128;
|
||
const dt = new DynamicTexture('gd_shadow_tex', { width: S, height: S }, this.scene, true);
|
||
const ctx = dt.getContext();
|
||
ctx.clearRect(0, 0, S, S);
|
||
const grad = ctx.createRadialGradient(S / 2, S / 2, 0, S / 2, S / 2, S / 2);
|
||
grad.addColorStop(0.0, 'rgba(0,0,0,0.55)');
|
||
grad.addColorStop(0.6, 'rgba(0,0,0,0.25)');
|
||
grad.addColorStop(1.0, 'rgba(0,0,0,0)');
|
||
ctx.fillStyle = grad;
|
||
ctx.beginPath();
|
||
ctx.arc(S / 2, S / 2, S / 2, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
dt.hasAlpha = true;
|
||
dt.update();
|
||
return dt;
|
||
}
|
||
|
||
/** 3D-пучки травы вдоль уровня через thin instances. */
|
||
_createGrassTufts(levelWidth) {
|
||
const proto = this._buildTuftProto();
|
||
this._grassTuftsProto = proto;
|
||
|
||
// Собираем множество x-координат где есть пол (y=0). Трава ставится
|
||
// только там — над пропастями (дырами) её не будет.
|
||
const floorXs = this._collectFloorXs();
|
||
const hasFloorAt = (xWorld) => {
|
||
// По текущим z пол узкий (-1..1). Проверяем x округлённо.
|
||
const x = Math.round(xWorld);
|
||
return floorXs.has(x);
|
||
};
|
||
|
||
const count = Math.floor(levelWidth * TUFT_DENSITY);
|
||
const matrices = new Float32Array(count * 16);
|
||
const basePos = new Array(count);
|
||
let n = 0;
|
||
for (let i = 0; i < count; i++) {
|
||
const xRand = Math.abs((Math.sin(i * 12.9898) * 43758)) % 1;
|
||
const zRand = Math.abs((Math.sin(i * 78.233 + 1.1) * 43758)) % 1;
|
||
const sRand = Math.abs((Math.sin(i * 41.21 + 0.5) * 43758)) % 1;
|
||
const yawRand = Math.abs((Math.sin(i * 23.45 + 2.7) * 43758)) % 1;
|
||
const x = (i / count) * levelWidth + (xRand - 0.5) * (1 / TUFT_DENSITY);
|
||
const z = (zRand - 0.5) * 2.4;
|
||
if (!hasFloorAt(x)) continue; // нет пола → пропуск (дыра/пропасть)
|
||
const scale = 0.55 + sRand * 0.6;
|
||
const baseRotY = yawRand * Math.PI * 2;
|
||
basePos[n] = { x, z, scale, baseRotY, phaseSeed: i };
|
||
n++;
|
||
}
|
||
this._grassBasePos = basePos.slice(0, n);
|
||
// re-allocate matrices под реальное n (могло быть меньше count)
|
||
this._grassMatrices = new Float32Array(n * 16);
|
||
this._grassCount = n;
|
||
|
||
this._refreshGrassMatrices(0);
|
||
proto.thinInstanceSetBuffer('matrix', this._grassMatrices, 16, false);
|
||
proto.thinInstanceCount = n;
|
||
}
|
||
|
||
/** Собрать множество x-координат, где есть блок на y=0 (пол).
|
||
* Используется и для травы, и для цветов — не сеем над пропастями. */
|
||
_collectFloorXs() {
|
||
const set = new Set();
|
||
const bm = this._scene3d?.blockManager;
|
||
if (!bm || !bm.blocks) return set;
|
||
for (const key of bm.blocks.keys()) {
|
||
const parts = String(key).split(',');
|
||
const x = parseInt(parts[0], 10);
|
||
const y = parseInt(parts[1], 10);
|
||
if (Number.isFinite(x) && y === 0) set.add(x);
|
||
}
|
||
return set;
|
||
}
|
||
|
||
/** Построить mesh-пучок — 3 пересекающиеся текстурированные плоскости. */
|
||
_buildTuftProto() {
|
||
// Текстура одного «листа» травы: тонкий вертикальный градиент с прозрачностью
|
||
const TW = 64, TH = 64;
|
||
const dt = new DynamicTexture('gd_tuft_tex', { width: TW, height: TH }, this.scene, true);
|
||
const ctx = dt.getContext();
|
||
ctx.clearRect(0, 0, TW, TH);
|
||
// Несколько «травинок» в одном тайле
|
||
const blades = [
|
||
{ x: 12, color: '#3a8a3a' },
|
||
{ x: 22, color: '#5acc5a' },
|
||
{ x: 32, color: '#88dd55' },
|
||
{ x: 42, color: '#5acc5a' },
|
||
{ x: 52, color: '#3a8a3a' },
|
||
];
|
||
for (const b of blades) {
|
||
const grad = ctx.createLinearGradient(b.x, TH, b.x, 0);
|
||
grad.addColorStop(0, b.color);
|
||
grad.addColorStop(1, '#88dd55');
|
||
ctx.strokeStyle = grad;
|
||
ctx.lineWidth = 4;
|
||
ctx.lineCap = 'round';
|
||
ctx.beginPath();
|
||
ctx.moveTo(b.x, TH - 2);
|
||
ctx.quadraticCurveTo(b.x + 3, TH * 0.4, b.x + (b.x % 2 ? 4 : -4), 6);
|
||
ctx.stroke();
|
||
}
|
||
dt.hasAlpha = true;
|
||
dt.update();
|
||
|
||
const mat = new StandardMaterial('gd_tuft_mat', this.scene);
|
||
mat.diffuseTexture = dt;
|
||
mat.diffuseTexture.hasAlpha = true;
|
||
mat.emissiveTexture = dt;
|
||
mat.emissiveColor = new Color3(0.55, 0.55, 0.55);
|
||
mat.useAlphaFromDiffuseTexture = true;
|
||
mat.backFaceCulling = false;
|
||
mat.specularColor = new Color3(0, 0, 0);
|
||
mat.transparencyMode = 1; // ALPHATEST — нет issue с порядком сортировки
|
||
|
||
// Меш — 3 квадрата, повёрнутые на 0°, 60°, 120° вокруг Y, склеенные в один
|
||
const proto = this._makeCrossPlanes('gd_tuft_proto', 0.32, 0.35);
|
||
proto.material = mat;
|
||
proto.isPickable = false;
|
||
return proto;
|
||
}
|
||
|
||
/** Создать меш из 3-х пересекающихся плоскостей (cross-billboard). */
|
||
_makeCrossPlanes(name, halfWidth, height) {
|
||
const positions = [];
|
||
const indices = [];
|
||
const uvs = [];
|
||
const normals = [];
|
||
const blades = 3;
|
||
for (let i = 0; i < blades; i++) {
|
||
const ang = (i / blades) * Math.PI; // 0°, 60°, 120°
|
||
const cs = Math.cos(ang), sn = Math.sin(ang);
|
||
const baseIdx = positions.length / 3;
|
||
// 4 угла: bottom-left, bottom-right, top-right, top-left
|
||
const corners = [
|
||
[-halfWidth, 0, 0],
|
||
[ halfWidth, 0, 0],
|
||
[ halfWidth, height, 0],
|
||
[-halfWidth, height, 0],
|
||
];
|
||
for (const c of corners) {
|
||
const x = c[0] * cs;
|
||
const z = c[0] * sn;
|
||
positions.push(x, c[1], z);
|
||
normals.push(-sn, 0, cs);
|
||
}
|
||
uvs.push(0, 0, 1, 0, 1, 1, 0, 1);
|
||
indices.push(baseIdx, baseIdx + 1, baseIdx + 2);
|
||
indices.push(baseIdx, baseIdx + 2, baseIdx + 3);
|
||
// Обратная сторона (для двусторонних листьев)
|
||
indices.push(baseIdx, baseIdx + 2, baseIdx + 1);
|
||
indices.push(baseIdx, baseIdx + 3, baseIdx + 2);
|
||
}
|
||
const mesh = new Mesh(name, this.scene);
|
||
const vd = new VertexData();
|
||
vd.positions = positions;
|
||
vd.indices = indices;
|
||
vd.uvs = uvs;
|
||
vd.normals = normals;
|
||
vd.applyToMesh(mesh, false);
|
||
return mesh;
|
||
}
|
||
|
||
/** Пересчитать матрицы инстансов травы с учётом ветра (windT). */
|
||
_refreshGrassMatrices(windT) {
|
||
const m = this._grassMatrices;
|
||
const items = this._grassBasePos;
|
||
if (!m || !items) return;
|
||
const tmp = new Matrix();
|
||
for (let i = 0; i < items.length; i++) {
|
||
const it = items[i];
|
||
// Лёгкое колебание: tilt + yaw чуть-чуть
|
||
const phase = it.phaseSeed * 0.13;
|
||
const tilt = Math.sin(windT * 1.6 + phase) * 0.12;
|
||
const yaw = it.baseRotY + Math.sin(windT * 0.7 + phase * 1.3) * 0.03;
|
||
const q = Quaternion.RotationYawPitchRoll(yaw, 0, tilt);
|
||
const s = new Vector3(it.scale, it.scale, it.scale);
|
||
const p = new Vector3(it.x, GROUND_TOP_Y, it.z);
|
||
Matrix.ComposeToRef(s, q, p, tmp);
|
||
tmp.copyToArray(m, i * 16);
|
||
}
|
||
}
|
||
|
||
/** 3D-цветы — стебель + 5-лепестковая шляпка. */
|
||
_createFlowers(levelWidth) {
|
||
const proto = this._buildFlowerProto();
|
||
this._flowersProto = proto;
|
||
const floorXs = this._collectFloorXs();
|
||
const hasFloorAt = (xWorld) => floorXs.has(Math.round(xWorld));
|
||
const want = Math.floor(levelWidth * FLOWER_DENSITY);
|
||
const items = [];
|
||
for (let i = 0; i < want; i++) {
|
||
const xRand = Math.abs((Math.sin(i * 12.9898 + 100) * 43758)) % 1;
|
||
const zRand = Math.abs((Math.sin(i * 78.233 + 200) * 43758)) % 1;
|
||
const sRand = Math.abs((Math.sin(i * 41.21 + 300) * 43758)) % 1;
|
||
const yawRand = Math.abs((Math.sin(i * 23.45 + 400) * 43758)) % 1;
|
||
const x = (i / want) * levelWidth + (xRand - 0.5) * (1 / FLOWER_DENSITY);
|
||
const z = (zRand - 0.5) * 2.4;
|
||
if (!hasFloorAt(x)) continue;
|
||
items.push({ x, z, scale: 0.45 + sRand * 0.45, yaw: yawRand * Math.PI * 2 });
|
||
}
|
||
const matrices = new Float32Array(items.length * 16);
|
||
const tmp = new Matrix();
|
||
for (let i = 0; i < items.length; i++) {
|
||
const it = items[i];
|
||
const q = Quaternion.RotationYawPitchRoll(it.yaw, 0, 0);
|
||
const s = new Vector3(it.scale, it.scale, it.scale);
|
||
const p = new Vector3(it.x, GROUND_TOP_Y, it.z);
|
||
Matrix.ComposeToRef(s, q, p, tmp);
|
||
tmp.copyToArray(matrices, i * 16);
|
||
}
|
||
proto.thinInstanceSetBuffer('matrix', matrices, 16, true);
|
||
proto.thinInstanceCount = items.length;
|
||
}
|
||
|
||
_buildFlowerProto() {
|
||
// Текстура: 5-лепестковый цветок + желтый центр
|
||
const T = 64;
|
||
const dt = new DynamicTexture('gd_flower_tex', { width: T, height: T }, this.scene, true);
|
||
const ctx = dt.getContext();
|
||
ctx.clearRect(0, 0, T, T);
|
||
// Случайно выберем один из цветов — но раз нужны разные цветы, нарисуем сразу несколько
|
||
// и выберем через UV. Простой путь: один тип цветка (розовый) — позже можно расширить.
|
||
const cx = T / 2, cy = T / 2;
|
||
const petalR = 18, centerR = 7;
|
||
ctx.fillStyle = '#ff6b9b';
|
||
for (let p = 0; p < 5; p++) {
|
||
const ang = (p / 5) * Math.PI * 2 - Math.PI / 2;
|
||
const px = cx + Math.cos(ang) * 13;
|
||
const py = cy + Math.sin(ang) * 13;
|
||
ctx.beginPath();
|
||
ctx.arc(px, py, petalR / 2, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
ctx.fillStyle = '#ffe44a';
|
||
ctx.beginPath();
|
||
ctx.arc(cx, cy, centerR, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
dt.hasAlpha = true;
|
||
dt.update();
|
||
|
||
const mat = new StandardMaterial('gd_flower_mat', this.scene);
|
||
mat.diffuseTexture = dt;
|
||
mat.diffuseTexture.hasAlpha = true;
|
||
mat.emissiveTexture = dt;
|
||
mat.emissiveColor = new Color3(0.7, 0.7, 0.7);
|
||
mat.useAlphaFromDiffuseTexture = true;
|
||
mat.backFaceCulling = false;
|
||
mat.specularColor = new Color3(0, 0, 0);
|
||
mat.transparencyMode = 1;
|
||
|
||
// Цветок = 2 крест-плоскости (поменьше, повыше)
|
||
const proto = this._makeCrossPlanes('gd_flower_proto', 0.25, 0.35);
|
||
proto.material = mat;
|
||
proto.isPickable = false;
|
||
return proto;
|
||
}
|
||
|
||
/** Анимация ветра + follow-тень игрока. */
|
||
_setupWind() {
|
||
let frame = 0;
|
||
this._onBeforeRender = () => {
|
||
frame++;
|
||
// Тень игрока — каждый кадр (player._pos живой, в отличие от .position)
|
||
const pp = this._scene3d?.player?._pos;
|
||
if (this._playerShadow && pp) {
|
||
this._playerShadow.position.x = pp.x;
|
||
this._playerShadow.position.z = pp.z || 0;
|
||
const h = Math.max(0, pp.y - 1);
|
||
const visScale = Math.max(0.6, 1 - h * 0.12);
|
||
this._playerShadow.scaling.x = visScale;
|
||
this._playerShadow.scaling.z = visScale;
|
||
}
|
||
// Ветер — раз в 3 кадра
|
||
if (frame % 3 !== 0) return;
|
||
this._windT += 0.05;
|
||
this._refreshGrassMatrices(this._windT);
|
||
if (this._grassTuftsProto && this._grassMatrices) {
|
||
this._grassTuftsProto.thinInstanceBufferUpdated('matrix');
|
||
}
|
||
};
|
||
this.scene.onBeforeRenderObservable.add(this._onBeforeRender);
|
||
}
|
||
|
||
/** Светящаяся зелёная полоса по краю пола (передний и задний). */
|
||
_createNeonEdge(levelWidth) {
|
||
const W = levelWidth + 40;
|
||
const H = 0.12; // тонкая
|
||
// Передний край (z = -1.51)
|
||
const front = MeshBuilder.CreateBox('gd_neon_edge_front', {
|
||
width: W,
|
||
height: H,
|
||
depth: 0.05,
|
||
}, this.scene);
|
||
front.position = new Vector3(levelWidth / 2 - 10, NEON_EDGE_Y + 0.45, NEON_EDGE_Z);
|
||
const matF = new StandardMaterial('gd_neon_edge_mat', this.scene);
|
||
matF.diffuseColor = new Color3(0.13, 1, 0.4);
|
||
matF.emissiveColor = new Color3(0.13, 1, 0.4);
|
||
matF.disableLighting = true;
|
||
front.material = matF;
|
||
front.isPickable = false;
|
||
front.applyFog = false;
|
||
this._neonEdge = front;
|
||
|
||
// Задний край (z = 1.51) — на всякий случай если камера сверху/сбоку
|
||
const back = MeshBuilder.CreateBox('gd_neon_edge_back', {
|
||
width: W,
|
||
height: H,
|
||
depth: 0.05,
|
||
}, this.scene);
|
||
back.position = new Vector3(levelWidth / 2 - 10, NEON_EDGE_Y + 0.45, 1.51);
|
||
back.material = matF;
|
||
back.isPickable = false;
|
||
back.applyFog = false;
|
||
this._neonEdgeBack = back;
|
||
}
|
||
|
||
dispose() {
|
||
if (!this.scene) return;
|
||
try {
|
||
if (this._onBeforeRender) {
|
||
this.scene.onBeforeRenderObservable.removeCallback(this._onBeforeRender);
|
||
this._onBeforeRender = null;
|
||
}
|
||
} catch (e) {}
|
||
const all = [
|
||
this._grassTuftsProto, this._flowersProto, this._neonEdge, this._neonEdgeBack,
|
||
this._playerShadow, ...(this._staticShadows || []), ...(this._pits || []),
|
||
];
|
||
for (const m of all) {
|
||
if (m) {
|
||
try { m.material?.dispose(true, true); } catch (e) {}
|
||
try { m.dispose(); } catch (e) {}
|
||
}
|
||
}
|
||
this._grassTuftsProto = this._flowersProto = this._neonEdge = this._neonEdgeBack = null;
|
||
this._playerShadow = null;
|
||
this._staticShadows = null;
|
||
this._scene3d = null;
|
||
this._grassMatrices = null;
|
||
this._grassBasePos = null;
|
||
this.scene = null;
|
||
}
|
||
}
|