Merge pull request 'feat(09): Studs материал + окрашиваемые блоки + лего-сет' (#13) from feat/studs-material-09 into main
This commit is contained in:
commit
cec58412dc
@ -94,6 +94,10 @@ export class BlockManager {
|
||||
this._lavaSurfaceBaseY = null;
|
||||
this._lavaDirty = false;
|
||||
this._animTime = 0;
|
||||
// Окрашиваемые блоки (studs-block, задача 09): per-instance color через
|
||||
// ThinInstance color buffer. blockTypeId → Float32Array(maxBlocks*4 RGBA).
|
||||
this._colorsByProto = new Map();
|
||||
this._STUDS_MAX = 20000; // макс блоков одного окрашиваемого типа
|
||||
}
|
||||
|
||||
/** Вызывать каждый кадр для анимации воды/лавы. */
|
||||
@ -359,6 +363,23 @@ export class BlockManager {
|
||||
const mat = new StandardMaterial(name, this.scene);
|
||||
mat.specularColor = new Color3(0, 0, 0);
|
||||
|
||||
// Окрашиваемый блок (studs-block): цвет берётся per-instance из vertex
|
||||
// color буфера ThinInstance и умножается на серую текстуру. Включаем
|
||||
// useVertexColors, normal map (выпуклость кружков), мягкий спекуляр.
|
||||
if (blockType.colorable) {
|
||||
const tex = new Texture(texturePath, this.scene);
|
||||
mat.diffuseTexture = tex;
|
||||
mat.diffuseColor = new Color3(1, 1, 1); // нейтраль — цвет идёт из vertex color
|
||||
if (blockType.normal) {
|
||||
try { mat.bumpTexture = new Texture(blockType.normal, this.scene); } catch (e) {}
|
||||
}
|
||||
// Сочность (Roblox-look): почти-белая текстура × яркий vertex color,
|
||||
// specular убран (он белит/тускнит цвет).
|
||||
mat.specularColor = new Color3(0, 0, 0);
|
||||
mat.useVertexColors = true;
|
||||
return mat;
|
||||
}
|
||||
|
||||
if (texturePath) {
|
||||
const tex = new Texture(texturePath, this.scene);
|
||||
tex.updateSamplingMode(Texture.NEAREST_SAMPLINGMODE);
|
||||
@ -439,7 +460,7 @@ export class BlockManager {
|
||||
* один meshes-prototype на тип блока, тысячи позиций в одном draw call.
|
||||
* Жидкости (water/lava) идут по старому пути — у них свой single-surface.
|
||||
*/
|
||||
addBlock(x, y, z, blockTypeId) {
|
||||
addBlock(x, y, z, blockTypeId, color) {
|
||||
const ix = Math.round(x);
|
||||
const iy = Math.round(y);
|
||||
const iz = Math.round(z);
|
||||
@ -449,6 +470,9 @@ export class BlockManager {
|
||||
const typeDef = getBlockType(blockTypeId);
|
||||
const isWater = !!typeDef?.isWater;
|
||||
const isLava = !!typeDef?.isLava;
|
||||
// Окрашиваемый блок: цвет инстанса (из аргумента или дефолт типа).
|
||||
const colorable = !!typeDef?.colorable;
|
||||
const instColor = colorable ? (color || typeDef.defaultColor || '#cccccc') : null;
|
||||
|
||||
// Для жидкостей оставляем старую логику: невидимый куб + единый surface
|
||||
if (isWater || isLava) {
|
||||
@ -496,6 +520,9 @@ export class BlockManager {
|
||||
keysArr[idx] = key;
|
||||
this._cellToInst.set(key, { typeId: blockTypeId, idx });
|
||||
|
||||
// Окрашиваемый блок — пишем цвет инстанса в color buffer.
|
||||
if (colorable) this._setBlockColorAt(blockTypeId, idx, instColor);
|
||||
|
||||
// Логический «meshProxy» — объект, имитирующий API mesh для совместимости.
|
||||
// НЕ создаёт реального меша. Используется selection / removeBlockByMesh.
|
||||
const meshProxy = {
|
||||
@ -511,6 +538,7 @@ export class BlockManager {
|
||||
mass: 1,
|
||||
folderId: null,
|
||||
_thinIdx: idx,
|
||||
color: instColor, // per-instance цвет окрашиваемого блока
|
||||
},
|
||||
// Минимальные методы, которые ожидает остальной код
|
||||
position: new Vector3(ix, iy + 0.5, iz),
|
||||
@ -538,6 +566,18 @@ export class BlockManager {
|
||||
proto.material = material;
|
||||
if (isMulti) this._setupSubmeshes(proto);
|
||||
|
||||
// Окрашиваемый блок — включаем per-instance color buffer (vertex colors).
|
||||
const _bt = getBlockType(blockTypeId);
|
||||
if (_bt && _bt.colorable) {
|
||||
proto.useVertexColors = true;
|
||||
proto.hasVertexAlpha = false;
|
||||
const buf = new Float32Array(this._STUDS_MAX * 4);
|
||||
// Дефолт-цвет (белый) для всех слотов — иначе невыставленные = чёрные.
|
||||
buf.fill(1);
|
||||
proto.thinInstanceSetBuffer('color', buf, 4, false);
|
||||
this._colorsByProto.set(blockTypeId, buf);
|
||||
}
|
||||
|
||||
proto.checkCollisions = false; // коллизии через thin-instances не работают штатно;
|
||||
// PlayerController в Play-режиме читает blocks Map напрямую (см. PhysicsAABB).
|
||||
proto.isPickable = true;
|
||||
@ -571,6 +611,44 @@ export class BlockManager {
|
||||
/** Подписка для уведомлений о создании prototype-меша (для shadow setup). */
|
||||
setOnProtoCreated(cb) { this._onProtoCreated = cb; }
|
||||
|
||||
/**
|
||||
* Записать цвет инстанса окрашиваемого блока в color buffer (RGBA float).
|
||||
* idx — индекс thin-instance. hex — '#rrggbb'. В batch-режиме обновление
|
||||
* GPU откладывается (флаг dirty), иначе сразу thinInstanceBufferUpdated.
|
||||
*/
|
||||
_setBlockColorAt(blockTypeId, idx, hex) {
|
||||
const buf = this._colorsByProto.get(blockTypeId);
|
||||
if (!buf) return;
|
||||
const c = Color3.FromHexString(hex || '#cccccc');
|
||||
const o = idx * 4;
|
||||
buf[o] = c.r; buf[o + 1] = c.g; buf[o + 2] = c.b; buf[o + 3] = 1;
|
||||
const proto = this._protoMeshes.get(blockTypeId);
|
||||
if (!proto) return;
|
||||
if (this._batchMode) {
|
||||
if (!this._colorDirtyProtos) this._colorDirtyProtos = new Set();
|
||||
this._colorDirtyProtos.add(blockTypeId);
|
||||
} else {
|
||||
try { proto.thinInstanceBufferUpdated('color'); } catch (e) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Сменить цвет окрашиваемого блока в (x,y,z) на лету (для scene.setColor /
|
||||
* color-пикера). Возвращает true если блок окрашиваемый и цвет применён.
|
||||
*/
|
||||
setBlockColor(x, y, z, hex) {
|
||||
const key = this._key(Math.round(x), Math.round(y), Math.round(z));
|
||||
const inst = this._cellToInst.get(key);
|
||||
if (!inst) return false;
|
||||
const bt = getBlockType(inst.typeId);
|
||||
if (!bt || !bt.colorable) return false;
|
||||
this._setBlockColorAt(inst.typeId, inst.idx, hex);
|
||||
const mp = this.blocks.get(key);
|
||||
if (mp && mp.metadata) mp.metadata.color = hex;
|
||||
this._notifyChange();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Установить флаг anchored у блока. */
|
||||
setBlockAnchored(x, y, z, anchored) {
|
||||
const mesh = this.blocks.get(this._key(x, y, z));
|
||||
@ -767,6 +845,8 @@ export class BlockManager {
|
||||
canCollide: m.canCollide !== false,
|
||||
visible: m.visible !== false,
|
||||
mass: m.mass ?? 1,
|
||||
// per-instance цвет окрашиваемого блока (studs-block); иначе пропускаем
|
||||
...(m.color ? { color: m.color } : {}),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
@ -778,7 +858,7 @@ export class BlockManager {
|
||||
this._batchMode = true;
|
||||
try {
|
||||
for (const b of arr) {
|
||||
const mesh = this.addBlock(b.x, b.y, b.z, b.type);
|
||||
const mesh = this.addBlock(b.x, b.y, b.z, b.type, b.color);
|
||||
if (!mesh) continue;
|
||||
if (b.anchored === false) {
|
||||
mesh.metadata.anchored = false;
|
||||
@ -802,6 +882,14 @@ export class BlockManager {
|
||||
proto.thinInstanceRefreshBoundingInfo(true);
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
// Финальный refresh color-буферов окрашиваемых блоков (batch).
|
||||
if (this._colorDirtyProtos) {
|
||||
for (const typeId of this._colorDirtyProtos) {
|
||||
const proto = this._protoMeshes.get(typeId);
|
||||
try { proto?.thinInstanceBufferUpdated('color'); } catch (e) { /* ignore */ }
|
||||
}
|
||||
this._colorDirtyProtos.clear();
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
|
||||
@ -105,6 +105,11 @@ export const BLOCK_TYPES = [
|
||||
// top = stone, side = oven (4 стороны с дверцей — лучше чем раньше),
|
||||
// bottom = stone. В будущем для одной двери понадобится 6-face формат.
|
||||
multiCube('oven', 'Печь', 'Особые', `${TEX}/stone.png`, `${TEX}/oven.png`, `${TEX}/stone.png`),
|
||||
|
||||
// === ОКРАШИВАЕМЫЕ (задача 09) — паритет со студией ===
|
||||
cube('studs-block', 'Лего-кирпич', 'Окрашиваемые',
|
||||
'/kubikon-assets/materials/studs_v4_diffuse.png',
|
||||
{ colorable: true, normal: '/kubikon-assets/materials/studs_v4_normal.png', defaultColor: '#3a7aff' }),
|
||||
];
|
||||
|
||||
/** Все доступные категории в порядке появления. */
|
||||
@ -120,6 +125,7 @@ export const CATEGORY_COLORS = {
|
||||
'Кирпич': '#9d4a3a',
|
||||
'Особые': '#9966ff',
|
||||
'Природа': '#5a8c3e',
|
||||
'Окрашиваемые': '#3a7aff',
|
||||
};
|
||||
|
||||
/** Найти описание блока по id. */
|
||||
|
||||
@ -2385,6 +2385,15 @@ export class GameRuntime {
|
||||
try {
|
||||
const color = payload?.color;
|
||||
if (typeof color !== 'string') return;
|
||||
// Окрашиваемый блок (studs-block): ref 'block:x,y,z' → BlockManager.
|
||||
const ref = payload?.id;
|
||||
if (typeof ref === 'string' && ref.startsWith('block:')) {
|
||||
const parts = ref.slice(6).split(',').map(Number);
|
||||
if (parts.length === 3 && parts.every(Number.isFinite)) {
|
||||
this.scene3d?.blockManager?.setBlockColor?.(parts[0], parts[1], parts[2], color);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const pm = this.scene3d?.primitiveManager;
|
||||
if (!pm) return;
|
||||
const rid = this._resolvePrimitiveId(payload?.id);
|
||||
@ -2398,6 +2407,9 @@ export class GameRuntime {
|
||||
if (data.material === 'neon') {
|
||||
data.mesh.material.emissiveColor = c;
|
||||
}
|
||||
if (data.material === 'studs') {
|
||||
data.mesh.material.emissiveColor = new Color3(c.r * 0.45, c.g * 0.45, c.b * 0.45);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@ -3199,7 +3211,8 @@ export class GameRuntime {
|
||||
if (!this._localToReal) this._localToReal = new Map();
|
||||
try {
|
||||
if (kind === 'block') {
|
||||
this.scene3d?.blockManager?.addBlock(payload.x, payload.y, payload.z, subType);
|
||||
// color — для окрашиваемых блоков (studs-block); иначе игнорируется.
|
||||
this.scene3d?.blockManager?.addBlock(payload.x, payload.y, payload.z, subType, payload.color);
|
||||
// Для блоков ref детерминированный, но запоминаем — чтобы при
|
||||
// Stop удалить заспавненные скриптом блоки (см. stop()).
|
||||
if (ref) this._localToReal.set(ref, ref);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -19,7 +19,7 @@
|
||||
* При касании игроком обновляет spawnPoint сцены.
|
||||
*/
|
||||
import {
|
||||
MeshBuilder, StandardMaterial, Color3, Vector3, PointLight,
|
||||
MeshBuilder, StandardMaterial, Color3, Vector3, Vector4, PointLight,
|
||||
Mesh, VertexData,
|
||||
} from '@babylonjs/core';
|
||||
// CRA→Vite адаптация: оригинальный код в setTexture()/_applyMaterial()/
|
||||
@ -33,6 +33,57 @@ import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTextur
|
||||
import { Texture } from '@babylonjs/core/Materials/Textures/texture';
|
||||
import { getPrimitiveType } from './PrimitiveTypes';
|
||||
|
||||
// === Материал «studs» (лего-кружки, задача 09) — паритет со студией ===
|
||||
const STUDS_DIFFUSE_URL = '/kubikon-assets/materials/studs_v4_diffuse.png';
|
||||
const STUDS_NORMAL_URL = '/kubikon-assets/materials/studs_v4_normal.png';
|
||||
const STUD_UNIT = 1;
|
||||
const STUDS_GRID = 4;
|
||||
const _studsTexCache = new WeakMap();
|
||||
function _getStudsTextures(scene) {
|
||||
let c = _studsTexCache.get(scene);
|
||||
if (!c) {
|
||||
c = { diffuse: new Texture(STUDS_DIFFUSE_URL, scene), normal: new Texture(STUDS_NORMAL_URL, scene) };
|
||||
_studsTexCache.set(scene, c);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
function _studsTiling(type, sx, sy, sz, density) {
|
||||
// density — множитель плотности кружков (1=стандарт, 2=вдвое мельче/чаще).
|
||||
const d = density && density > 0 ? density : 1;
|
||||
const f = (STUD_UNIT * STUDS_GRID) / d;
|
||||
let u = Math.max(sx, sz) / f;
|
||||
let v = sy / f;
|
||||
if (type === 'cylinder') { u = (Math.PI * sx) / f; v = sy / f; }
|
||||
else if (type === 'sphere') { u = (Math.PI * sx) / f; v = (Math.PI * sy) / f; }
|
||||
else if (type === 'plane') { u = sx / f; v = sz / f; }
|
||||
return { u: Math.max(0.25, u), v: Math.max(0.25, v) };
|
||||
}
|
||||
/**
|
||||
* faceUV для куба со studs — КАЖДАЯ грань тайлится по СВОИМ реальным размерам,
|
||||
* чтобы кружки были одного размера на всех гранях (не растягивались на длинных).
|
||||
* Грани CreateBox: 0=front(z-) 1=back(z+) 2=right(x+) 3=left(x-) 4=top(y+) 5=bottom(y-).
|
||||
* front/back → ширина=sx, высота=sy
|
||||
* left/right → ширина=sz, высота=sy
|
||||
* top/bottom → ширина=sx, высота=sz
|
||||
* UV-диапазон грани = (0,0)..(кол-во_studs_по_ширине, кол-во_по_высоте).
|
||||
*/
|
||||
function _studsCubeFaceUV(sx, sy, sz, density) {
|
||||
const d = density && density > 0 ? density : 1;
|
||||
const f = (STUD_UNIT * STUDS_GRID) / d;
|
||||
const nx = Math.max(0.25, sx / f); // studs вдоль X
|
||||
const ny = Math.max(0.25, sy / f); // studs вдоль Y
|
||||
const nz = Math.max(0.25, sz / f); // studs вдоль Z
|
||||
// Vector4(u0, v0, u1, v1)
|
||||
return [
|
||||
new Vector4(0, 0, nx, ny), // front (z-): X×Y
|
||||
new Vector4(0, 0, nx, ny), // back (z+): X×Y
|
||||
new Vector4(0, 0, nz, ny), // right (x+): Z×Y
|
||||
new Vector4(0, 0, nz, ny), // left (x-): Z×Y
|
||||
new Vector4(0, 0, nx, nz), // top (y+): X×Z
|
||||
new Vector4(0, 0, nx, nz), // bottom (y-): X×Z
|
||||
];
|
||||
}
|
||||
|
||||
export class PrimitiveManager {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
@ -73,6 +124,8 @@ export class PrimitiveManager {
|
||||
const isGlowingGd = isGdKind;
|
||||
const isGdSpike = typeDef.kind === 'gd_spike';
|
||||
const material = opts.material ?? (isGlowingGd ? 'neon' : 'matte');
|
||||
// studDensity — плотность кружков studs (1=стандарт, >1 мельче/чаще).
|
||||
const studDensity = Number.isFinite(opts.studDensity) ? opts.studDensity : 1;
|
||||
// canCollide: для всех GD-сущностей и шипов — false (игрок проходит сквозь, логика по дистанции)
|
||||
const canCollide = opts.canCollide !== false && !isGdKind && !isGdSpike;
|
||||
const visible = opts.visible !== false;
|
||||
@ -90,7 +143,7 @@ export class PrimitiveManager {
|
||||
const rotationY = opts.rotationY ?? 0;
|
||||
const rotationZ = opts.rotationZ ?? 0;
|
||||
|
||||
const mesh = this._createMeshForType(typeDef, id, sx, sy, sz);
|
||||
const mesh = this._createMeshForType(typeDef, id, sx, sy, sz, material, studDensity);
|
||||
mesh.position = new Vector3(x, y, z);
|
||||
mesh.rotation = new Vector3(rotationX, rotationY, rotationZ);
|
||||
mesh.isPickable = true;
|
||||
@ -114,13 +167,15 @@ export class PrimitiveManager {
|
||||
id, mesh, type, x, y, z, sx, sy, sz,
|
||||
rotationX, rotationY, rotationZ,
|
||||
color, material, canCollide, visible, anchored, mass,
|
||||
textureAsset,
|
||||
textureAsset, studDensity,
|
||||
// locked — объект защищён от выделения/перемещения в редакторе
|
||||
// (Фаза 5.11). На геймплей не влияет.
|
||||
locked: opts.locked === true,
|
||||
name: opts.name || null,
|
||||
folderId: opts.folderId ?? null,
|
||||
};
|
||||
// Размеры для тайлинга studs-материала (читается в _applyMaterial).
|
||||
mesh._studsDims = { type, sx, sy, sz, density: studDensity };
|
||||
this._applyMaterial(mesh, typeDef, color, material);
|
||||
this._applyVisible(mesh, visible, typeDef);
|
||||
// Пользовательская текстура — поверх базового материала.
|
||||
@ -185,13 +240,17 @@ export class PrimitiveManager {
|
||||
}
|
||||
|
||||
/** Создать базовый mesh нужной формы (без материала). */
|
||||
_createMeshForType(typeDef, id, sx, sy, sz) {
|
||||
_createMeshForType(typeDef, id, sx, sy, sz, material, studDensity) {
|
||||
const name = `prim_${typeDef.id}_${id}`;
|
||||
switch (typeDef.id) {
|
||||
case 'cube':
|
||||
case 'trigger':
|
||||
return MeshBuilder.CreateBox(name,
|
||||
{ width: sx, height: sy, depth: sz }, this.scene);
|
||||
case 'trigger': {
|
||||
const boxOpts = { width: sx, height: sy, depth: sz };
|
||||
// studs — per-face UV, чтобы кружки были одного размера на всех
|
||||
// гранях (не растягивались на длинной стороне).
|
||||
if (material === 'studs') boxOpts.faceUV = _studsCubeFaceUV(sx, sy, sz, studDensity);
|
||||
return MeshBuilder.CreateBox(name, boxOpts, this.scene);
|
||||
}
|
||||
case 'sphere':
|
||||
return MeshBuilder.CreateSphere(name,
|
||||
{ diameterX: sx, diameterY: sy, diameterZ: sz, segments: 24 }, this.scene);
|
||||
@ -431,6 +490,31 @@ export class PrimitiveManager {
|
||||
mat.emissiveColor = Color3.FromHexString(color || '#888888');
|
||||
mat.specularColor = new Color3(0, 0, 0);
|
||||
break;
|
||||
case 'studs': {
|
||||
// Лего-материал (паритет со студией): почти-белая diffuse × цвет
|
||||
// меша + normal map. emissive = доля цвета → сочность (Roblox-look).
|
||||
const tex = _getStudsTextures(this.scene);
|
||||
const dt = tex.diffuse.clone();
|
||||
const nt = tex.normal.clone();
|
||||
const dims = mesh._studsDims || { type: 'cube', sx: 1, sy: 1, sz: 1 };
|
||||
// Куб/триггер тайлятся через faceUV геометрии (uScale=1) — кружки
|
||||
// одного размера на всех гранях. Остальные формы — через uScale.
|
||||
if (dims.type === 'cube' || dims.type === 'trigger') {
|
||||
dt.uScale = nt.uScale = 1;
|
||||
dt.vScale = nt.vScale = 1;
|
||||
} else {
|
||||
const tile = _studsTiling(dims.type, dims.sx, dims.sy, dims.sz, dims.density);
|
||||
dt.uScale = nt.uScale = tile.u;
|
||||
dt.vScale = nt.vScale = tile.v;
|
||||
}
|
||||
mat.diffuseTexture = dt;
|
||||
mat.bumpTexture = nt;
|
||||
const sc = Color3.FromHexString(color || '#cccccc');
|
||||
mat.diffuseColor = sc;
|
||||
mat.emissiveColor = new Color3(sc.r * 0.45, sc.g * 0.45, sc.b * 0.45);
|
||||
mat.specularColor = new Color3(0, 0, 0);
|
||||
break;
|
||||
}
|
||||
case 'matte':
|
||||
default:
|
||||
mat.specularColor = new Color3(0, 0, 0);
|
||||
@ -576,6 +660,12 @@ export class PrimitiveManager {
|
||||
if (patch.sx !== undefined) { data.sx = patch.sx; scaleChanged = true; }
|
||||
if (patch.sy !== undefined) { data.sy = patch.sy; scaleChanged = true; }
|
||||
if (patch.sz !== undefined) { data.sz = patch.sz; scaleChanged = true; }
|
||||
// Плотность studs (мелкие/крупные кружки) — требует пересоздания меша
|
||||
// (faceUV для куба зашит в геометрию).
|
||||
if (patch.studDensity !== undefined) {
|
||||
data.studDensity = Number.isFinite(patch.studDensity) ? patch.studDensity : 1;
|
||||
scaleChanged = true;
|
||||
}
|
||||
if (scaleChanged) {
|
||||
// Поскольку MeshBuilder уже создал mesh с базовыми размерами,
|
||||
// изменения через scaling кажутся правильными. Простой способ —
|
||||
@ -601,6 +691,7 @@ export class PrimitiveManager {
|
||||
if (data.mesh.material) {
|
||||
try { data.mesh.material.dispose(); } catch (e) { /* ignore */ }
|
||||
}
|
||||
data.mesh._studsDims = { type: data.type, sx: data.sx, sy: data.sy, sz: data.sz };
|
||||
this._applyMaterial(data.mesh, typeDef, data.color, data.material);
|
||||
}
|
||||
// Текстуру переприменяем если: сменили саму текстуру, или
|
||||
@ -614,6 +705,7 @@ export class PrimitiveManager {
|
||||
if (data.mesh.material) {
|
||||
try { data.mesh.material.dispose(); } catch (e) { /* ignore */ }
|
||||
}
|
||||
data.mesh._studsDims = { type: data.type, sx: data.sx, sy: data.sy, sz: data.sz };
|
||||
this._applyMaterial(data.mesh, typeDef, data.color, data.material);
|
||||
}
|
||||
|
||||
@ -704,10 +796,17 @@ export class PrimitiveManager {
|
||||
const oldMat = oldMesh.material;
|
||||
|
||||
const typeDef = getPrimitiveType(data.type);
|
||||
const newMesh = this._createMeshForType(typeDef, data.id, data.sx, data.sy, data.sz);
|
||||
const newMesh = this._createMeshForType(typeDef, data.id, data.sx, data.sy, data.sz, data.material, data.studDensity);
|
||||
newMesh.position = oldPos;
|
||||
if (oldRot) newMesh.rotation = oldRot;
|
||||
newMesh.material = oldMat;
|
||||
// studs — материал пересоздаём заново (свежий faceUV/тайлинг). Иначе перенос.
|
||||
if (data.material === 'studs') {
|
||||
newMesh._studsDims = { type: data.type, sx: data.sx, sy: data.sy, sz: data.sz, density: data.studDensity };
|
||||
this._applyMaterial(newMesh, typeDef, data.color, data.material);
|
||||
try { oldMat?.dispose(); } catch (e) { /* ignore */ }
|
||||
} else {
|
||||
newMesh.material = oldMat;
|
||||
}
|
||||
newMesh.isPickable = true;
|
||||
newMesh.metadata = { ...oldMesh.metadata };
|
||||
newMesh.setEnabled(data.visible);
|
||||
@ -717,6 +816,7 @@ export class PrimitiveManager {
|
||||
catch (e) { /* ignore */ }
|
||||
|
||||
data.mesh = newMesh;
|
||||
// _studsDims и материал studs уже выставлены выше.
|
||||
}
|
||||
|
||||
/** Удалить инстанс. */
|
||||
@ -765,6 +865,8 @@ export class PrimitiveManager {
|
||||
...(d.locked ? { locked: true } : {}),
|
||||
// id пользовательской текстуры (картинка из AssetManager).
|
||||
...(d.textureAsset ? { textureAsset: d.textureAsset } : {}),
|
||||
// Плотность studs (если не 1) — мелкие/крупные кружки.
|
||||
...(d.studDensity && d.studDensity !== 1 ? { studDensity: d.studDensity } : {}),
|
||||
// Параметры лампы (только для type='light', иначе undefined)
|
||||
...(d.light ? { brightness: d.brightness, range: d.range } : {}),
|
||||
// Параметр эмиттера (только для type='emitter')
|
||||
|
||||
@ -1410,7 +1410,8 @@ const game = {
|
||||
if (kind === 'block') {
|
||||
const ix = Math.round(x), iy = Math.round(y), iz = Math.round(z);
|
||||
const ref = 'block:' + ix + ',' + iy + ',' + iz;
|
||||
_send('scene.spawn', { kind: 'block', subType, x: ix, y: iy, z: iz, ref });
|
||||
// color — для окрашиваемых блоков (studs-block, задача 09).
|
||||
_send('scene.spawn', { kind: 'block', subType, x: ix, y: iy, z: iz, ref, color: opts.color });
|
||||
if (opts.lifetime != null) _scheduleDelete(ref, opts.lifetime);
|
||||
return ref;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user