From dfbe765baac5f7151a6b346e2d88ca4d8a3cd784 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 21 May 2026 16:15:18 +0900 Subject: [PATCH] ObjectContainer --- .../frontend/src/pages/rooms/room.core.vue | 4 +- packages/frontend/src/world/EngineBase.ts | 2 +- .../src/world/room/ObjectContainer.ts | 262 +++++++++++ packages/frontend/src/world/room/engine.ts | 411 +++--------------- .../frontend/src/world/room/object-defs.ts | 5 +- packages/frontend/src/world/room/object.ts | 15 +- .../src/world/room/objects/ironFrameShelf.ts | 6 +- .../src/world/room/objects/steelRack.ts | 28 +- .../frontend/src/world/room/previewEngine.ts | 16 +- 9 files changed, 370 insertions(+), 379 deletions(-) create mode 100644 packages/frontend/src/world/room/ObjectContainer.ts diff --git a/packages/frontend/src/pages/rooms/room.core.vue b/packages/frontend/src/pages/rooms/room.core.vue index 6b902c3a38..a69f7e26c2 100644 --- a/packages/frontend/src/pages/rooms/room.core.vue +++ b/packages/frontend/src/pages/rooms/room.core.vue @@ -617,8 +617,8 @@ function showOtherMenu(ev: PointerEvent) { title: i18n.ts.areYouSure, }).then(({ canceled }) => { if (canceled) return; - localStorage.removeItem('roomData'); - window.location.reload(); + + // TODO }); }, }, { diff --git a/packages/frontend/src/world/EngineBase.ts b/packages/frontend/src/world/EngineBase.ts index b9f6ae4db8..39093b71e2 100644 --- a/packages/frontend/src/world/EngineBase.ts +++ b/packages/frontend/src/world/EngineBase.ts @@ -43,7 +43,7 @@ export abstract class EngineBase extends EventEmit private currentRafId: number | null = null; - private startRenderLoop() { + protected startRenderLoop() { if (this.fps == null) { this.engine.runRenderLoop(() => { this.scene.render(); diff --git a/packages/frontend/src/world/room/ObjectContainer.ts b/packages/frontend/src/world/room/ObjectContainer.ts new file mode 100644 index 0000000000..aa6517ed76 --- /dev/null +++ b/packages/frontend/src/world/room/ObjectContainer.ts @@ -0,0 +1,262 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as BABYLON from '@babylonjs/core'; +import { scaleMorph, camelToKebab, cm, WORLD_SCALE, Timer } from '../utility.js'; +import { getObjectDef } from './object-defs.js'; +import { ModelManager, SYSTEM_MESH_NAMES } from './utility.js'; +import type { ConvertedOptions, RoomObjectInstance } from './object.js'; + +function mergeMeshes(meshes: BABYLON.Mesh[], root: BABYLON.Mesh, hasTexture: boolean) { + const excludeMeshes = root.getChildMeshes().filter(m => SYSTEM_MESH_NAMES.some(s => m.name.includes(s))); + + const childMeshes = root.getChildMeshes().filter(m => !excludeMeshes.some(x => x === m) && m.isVisible && !m.isDisposed()); + + const toMerge = [] as BABYLON.Mesh[]; + for (const mesh of childMeshes) { + if (mesh instanceof BABYLON.InstancedMesh) { + continue; + } + + if (mesh.hasInstances) continue; + + if (mesh instanceof BABYLON.Mesh) { + toMerge.push(mesh); + } + } + + if (toMerge.length <= 1) { // マージ対象が一つしかない状態でマージするのは単純に無駄なのと、babylonのバグが知らないけどなぜか法線が反転する + return null; + } + + for (const mesh of toMerge) { + if (hasTexture) { + if (mesh.getVerticesData(BABYLON.VertexBuffer.UVKind) == null) { + const vertexCount = mesh.getTotalVertices(); + const uvs = new Array(vertexCount * 2).fill(0); + mesh.setVerticesData(BABYLON.VertexBuffer.UVKind, uvs, false, 2); + } + if (mesh.getVerticesData(BABYLON.VertexBuffer.UV2Kind) == null) { + const vertexCount = mesh.getTotalVertices(); + const uvs = new Array(vertexCount * 2).fill(0); + mesh.setVerticesData(BABYLON.VertexBuffer.UV2Kind, uvs, false, 2); + } + } + } + + const merged = BABYLON.Mesh.MergeMeshes(toMerge, true, false, undefined, false, true); + + return merged; +} + +export class ObjectContainer { + public id: string; + public type: string; + public options: ConvertedOptions; + public root: BABYLON.TransformNode; + private subRoot: BABYLON.TransformNode | null = null; + private metadata: any; + public instance: RoomObjectInstance | null = null; + public model: ModelManager | null = null; + private scene: BABYLON.Scene; + public onMeshesUpdated: (meshes: BABYLON.Mesh[]) => void = () => {}; + private sr: BABYLON.SnapshotRenderingHelper; + private getIsSrReady: () => boolean; + private lightContainer: BABYLON.ClusteredLightContainer; + private graphicsQuality: number; + private timer: Timer = new Timer(); + private sitChair: () => void = () => {}; + + constructor(args: { + id: string; + type: string; + options: ConvertedOptions; + position: BABYLON.Vector3; + rotation: BABYLON.Vector3; + metadata: any; + sr: BABYLON.SnapshotRenderingHelper; + getIsSrReady: () => boolean; + lightContainer: BABYLON.ClusteredLightContainer; + scene: BABYLON.Scene; + graphicsQuality: number; + sitChair?: () => void; + }) { + this.id = args.id; + this.type = args.type; + this.options = args.options; + this.sr = args.sr; + this.getIsSrReady = args.getIsSrReady; + this.lightContainer = args.lightContainer; + this.scene = args.scene; + this.metadata = args.metadata; + this.graphicsQuality = args.graphicsQuality; + this.root = new BABYLON.TransformNode(`object_${args.id}_${args.type}`, this.scene); + this.root.position = args.position; + this.root.rotation = args.rotation; + this.root.metadata = this.metadata; + if (args.sitChair != null) this.sitChair = args.sitChair; + } + + public async load() { + const def = getObjectDef(this.type); + + const filePath = def.path != null ? `/client-assets/room/objects/${def.path(this.options)}.glb` : `/client-assets/room/objects/${camelToKebab(this.type)}/${camelToKebab(this.type)}.glb`; + const loaderResult = await BABYLON.LoadAssetContainerAsync(filePath, this.scene); + + // babylonによって自動で追加される右手系変換用ノード + const subRootMesh = loaderResult.meshes[0] as BABYLON.Mesh; + + // 不要なUVを掃除 + if (!def.hasTexture) { + for (const m of loaderResult.meshes) { + if (m.geometry != null) { + m.geometry.removeVerticesData(BABYLON.VertexBuffer.UVKind); + m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV2Kind); + m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV3Kind); + m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV4Kind); + m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV5Kind); + m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV6Kind); + } + } + } + + if (def.canPreMeshesMerging) { + const merged = mergeMeshes(loaderResult.meshes, subRootMesh, def.hasTexture); + if (merged != null) { + merged.setParent(subRootMesh); + merged.name = 'preMerged'; + + merged.material.freeze(); + if (merged.material instanceof BABYLON.MultiMaterial) { + for (const subMat of merged.material.subMaterials) { + subMat.freeze(); + } + } + + // TODO: 再帰的にする + for (const m of loaderResult.transformNodes) { + if (m.getChildren().length === 0) { + m.dispose(); + } + } + } + } + + // meshじゃなくtransform nodeにしてパフォーマンス向上 + this.subRoot = new BABYLON.TransformNode('__root__', this.scene); + console.log(this.root.position); + this.subRoot.parent = this.root; + this.subRoot.scaling.x = -1; + this.subRoot.scaling = this.subRoot.scaling.scale(WORLD_SCALE);// cmをmに + + for (const m of subRootMesh.getChildren()) { + if (m.parent === subRootMesh) { + m.parent = this.subRoot; + } + } + + subRootMesh.dispose(); + + def.treatLoaderResult?.(loaderResult); + + this.model = new ModelManager(this.subRoot, loaderResult.meshes.filter(m => !m.isDisposed() && m.name !== '__root__'), def.hasTexture, (meshes) => { + this.onMeshesUpdated(meshes); + }); + + this.instance = await def.createInstance({ + scene: this.scene, + sr: { + updateMesh: (mesh) => { + if (!this.getIsSrReady()) return; + this.sr.updateMesh(mesh); + }, + reset: () => { + if (!this.getIsSrReady()) return; + this.sr.disableSnapshotRendering(); + this.sr.enableSnapshotRendering(); + }, + fixParticleSystem: (ps) => this.sr.fixParticleSystem(ps), + }, + lc: this.lightContainer, + root: this.root, + options: this.options, + model: this.model!, + id: this.id, + timer: this.timer, + graphicsQuality: this.graphicsQuality, + reloadModel: () => { + this.reload(); + }, + sitChair: () => { + this.sitChair(); + }, + stickyMarkerMeshUpdated: (mesh) => { + // TODO + //// stickyな子の位置を更新 + //if (mesh.name.includes('__TOP__')) { + // mesh.unfreezeWorldMatrix(); + // mesh.computeWorldMatrix(true); + // const updateChildStickyObjectPosition = (objectId: string) => { + // const stickyObjectIds = Array.from(this.roomState.installedObjects.filter(o => o.sticky === objectId)).map(o => o.id); + // for (const soid of stickyObjectIds) { + // const soMesh = this.objectEntities.get(soid)!.rootMesh; + // soMesh.unfreezeWorldMatrix(); + // for (const m of soMesh.getChildMeshes()) { + // m.unfreezeWorldMatrix(); + // } + // console.log(mesh.getAbsolutePosition().y); + // soMesh.position.y = mesh.getAbsolutePosition().y; + // updateChildStickyObjectPosition(soid); + // } + // }; + // updateChildStickyObjectPosition(args.id); + //} + }, + }); + + this.instance.onInited?.(); + + this.model.bakeMesh(); + } + + public interact(iid: string | null = null) { + if (this.instance == null) return; + if (iid == null) { + if (this.instance.primaryInteraction != null) { + this.instance.interactions[this.instance.primaryInteraction].fn(); + } + } else { + this.instance.interactions[iid].fn(); + } + } + + public async reload() { + this.timer.dispose(); + this.instance?.dispose(); + this.instance = null; + this.model = null; + this.subRoot?.dispose(); + this.root.removeChild(this.subRoot); + + this.timer = new Timer(); + + await this.load(); + + this.sr.disableSnapshotRendering(); + this.sr.enableSnapshotRendering(); + } + + public optionsUpdated(key: string, value: any) { + if (this.instance == null) return; + this.instance.onOptionsUpdated?.([key, value]); + } + + public destroy() { + this.timer.dispose(); + this.instance?.dispose(); + this.subRoot?.dispose(); + this.root.dispose(true); + } +} diff --git a/packages/frontend/src/world/room/engine.ts b/packages/frontend/src/world/room/engine.ts index 73a5ddce5a..9206dcad22 100644 --- a/packages/frontend/src/world/room/engine.ts +++ b/packages/frontend/src/world/room/engine.ts @@ -20,6 +20,7 @@ import { getObjectDef } from './object-defs.js'; import { findMaterial, GRAPHICS_QUALITY, ModelManager, SYSTEM_HEYA_MESH_NAMES, SYSTEM_MESH_NAMES } from './utility.js'; import { JapaneseEnvManager, MuseumEnvManager, SimpleEnvManager } from './env.js'; import { convertRawOptions } from './object.js'; +import { ObjectContainer } from './objectContainer.js'; import type { RoomAttachments } from './utility.js'; import type { ConvertedOptions, ObjectDef, RawOptions, RoomObjectInstance, RoomStateObject } from './object.js'; import type { GridMaterial } from '@babylonjs/materials'; @@ -60,48 +61,6 @@ export function collectReferencedDriveFileIds(roomState: RoomState) { return fileIds; } -function mergeMeshes(meshes: BABYLON.Mesh[], root: BABYLON.Mesh, hasTexture: boolean) { - const excludeMeshes = root.getChildMeshes().filter(m => SYSTEM_MESH_NAMES.some(s => m.name.includes(s))); - - const childMeshes = root.getChildMeshes().filter(m => !excludeMeshes.some(x => x === m) && m.isVisible && !m.isDisposed()); - - const toMerge = [] as BABYLON.Mesh[]; - for (const mesh of childMeshes) { - if (mesh instanceof BABYLON.InstancedMesh) { - continue; - } - - if (mesh.hasInstances) continue; - - if (mesh instanceof BABYLON.Mesh) { - toMerge.push(mesh); - } - } - - if (toMerge.length <= 1) { // マージ対象が一つしかない状態でマージするのは単純に無駄なのと、babylonのバグが知らないけどなぜか法線が反転する - return null; - } - - for (const mesh of toMerge) { - if (hasTexture) { - if (mesh.getVerticesData(BABYLON.VertexBuffer.UVKind) == null) { - const vertexCount = mesh.getTotalVertices(); - const uvs = new Array(vertexCount * 2).fill(0); - mesh.setVerticesData(BABYLON.VertexBuffer.UVKind, uvs, false, 2); - } - if (mesh.getVerticesData(BABYLON.VertexBuffer.UV2Kind) == null) { - const vertexCount = mesh.getTotalVertices(); - const uvs = new Array(vertexCount * 2).fill(0); - mesh.setVerticesData(BABYLON.VertexBuffer.UV2Kind, uvs, false, 2); - } - } - } - - const merged = BABYLON.Mesh.MergeMeshes(toMerge, true, false, undefined, false, true); - - return merged; -} - function enableObjectCollision(meshes: BABYLON.Mesh[]) { for (const mesh of meshes) { if (mesh.name.includes('__COLLISION__')) { @@ -142,12 +101,7 @@ export class RoomEngine extends EngineBase<{ private useGlow: boolean; public camera: BABYLON.UniversalCamera; private fixedCamera: BABYLON.FreeCamera; - public objectEntities: Map = new Map(); + public objectContainers: Map = new Map(); private envManager: EnvManager | null = null; // TODO: たぶんオブジェクト内の値のmutateはsetで検知できないので、そのような操作を実際に行うようになった & それを検知する必要性が出てきたら専用の設定関数などを新設してそれを使わせる @@ -177,7 +131,7 @@ export class RoomEngine extends EngineBase<{ // TODO: たぶんオブジェクト内の値のmutateはsetで検知できないので、そのような操作を実際に行うようになった & それを検知する必要性が出てきたら専用の設定関数などを新設してそれを使わせる private _selected: { objectId: string; - objectEntity: RoomEngine['objectEntities'] extends Map ? V : never; + objectContainer: RoomEngine['objectContainers'] extends Map ? V : never; objectState: RoomStateObject; objectDef: ObjectDef; } | null = null; @@ -193,10 +147,10 @@ export class RoomEngine extends EngineBase<{ this.ev('changeSelectedState', { selected: { objectId: v.objectId, objectState: v.objectState, - interacions: Object.entries(v.objectEntity.instance.interactions).map(([interactionId, interactionInfo]) => ({ + interacions: Object.entries(v.objectContainer.instance.interactions).map(([interactionId, interactionInfo]) => ({ id: interactionId, label: interactionInfo.label, - isPrimary: v.objectEntity.instance.primaryInteraction === interactionId, + isPrimary: v.objectContainer.instance.primaryInteraction === interactionId, })), } }); } @@ -519,13 +473,13 @@ export class RoomEngine extends EngineBase<{ // TODO: GPUPickerを使いたいが、なぜか一部のメッシュが反応しない const pickingInfo = this.scene.pick(ev.x, ev.y, - (m) => m.name.includes('__PICK__') || (m.isVisible && m.isEnabled() && m.metadata?.objectId != null && this.objectEntities.has(m.metadata.objectId))); + (m) => m.name.includes('__PICK__') || (m.isVisible && m.isEnabled() && m.metadata?.objectId != null && this.objectContainers.has(m.metadata.objectId))); if (pickingInfo.pickedMesh != null) { const oid = pickingInfo.pickedMesh.metadata.objectId; - if (oid != null && this.objectEntities.has(oid)) { - const o = this.objectEntities.get(oid)!; - const boundingInfo = getMeshesBoundingBox(o.rootMesh.getChildMeshes().filter(m => m.isEnabled() && m.isVisible && !m.isDisposed()), true); + if (oid != null && this.objectContainers.has(oid)) { + const o = this.objectContainers.get(oid)!; + const boundingInfo = getMeshesBoundingBox(o.root.getChildMeshes().filter(m => m.isEnabled() && m.isVisible && !m.isDisposed()), true); this.selectObject(oid); { // camera animation @@ -651,193 +605,7 @@ export class RoomEngine extends EngineBase<{ options: RawOptions; }) { const def = getObjectDef(args.type); - - const root = new BABYLON.TransformNode(`object_${args.id}_${args.type}`, this.scene); - - const filePath = def.path != null ? `/client-assets/room/objects/${def.path}.glb` : `/client-assets/room/objects/${camelToKebab(args.type)}/${camelToKebab(args.type)}.glb`; - const loaderResult = await BABYLON.LoadAssetContainerAsync(filePath, this.scene); - - // babylonによって自動で追加される右手系変換用ノード - let subRoot = loaderResult.meshes[0] as BABYLON.TransformNode; - - // 不要なUVを掃除 - if (!def.hasTexture) { - for (const m of loaderResult.meshes) { - if (m.geometry != null) { - m.geometry.removeVerticesData(BABYLON.VertexBuffer.UVKind); - m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV2Kind); - m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV3Kind); - m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV4Kind); - m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV5Kind); - m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV6Kind); - } - } - } - - if (def.canPreMeshesMerging) { - const merged = mergeMeshes(loaderResult.meshes, subRoot, def.hasTexture); - if (merged != null) { - merged.setParent(subRoot); - merged.name = 'preMerged'; - - merged.material.freeze(); - if (merged.material instanceof BABYLON.MultiMaterial) { - for (const subMat of merged.material.subMaterials) { - subMat.freeze(); - } - } - - // TODO: 再帰的にする - for (const m of loaderResult.transformNodes) { - if (m.getChildren().length === 0) { - m.dispose(); - } - } - } - } - - if (BAKE_TRANSFORM) { - subRoot.scaling = new BABYLON.Vector3(1, 1, 1); - subRoot.rotationQuaternion = null; - subRoot.rotation = new BABYLON.Vector3(0, 0, 0); - //subRoot.scaling = subRoot.scaling.scale(WORLD_SCALE);// cmをmに - //subRoot.bakeCurrentTransformIntoVertices(); - //subRoot.bakeTransformIntoVertices(BABYLON.Matrix.Scaling(WORLD_SCALE, WORLD_SCALE, WORLD_SCALE)); - - for (const m of loaderResult.transformNodes) { - if (m.name === '__root__') continue; - if (m.parent === subRoot) { - m.setParent(root); - //m.parent = root; - } - } - for (const m of loaderResult.meshes) { - if (m.name === '__root__') continue; - if (m.parent === subRoot) { - m.setParent(root); - //m.parent = root; - } - } - - const bakeTransformNode = (m: BABYLON.TransformNode) => { - m.position.x *= -WORLD_SCALE; - m.position.y *= WORLD_SCALE; - m.position.z *= WORLD_SCALE; - m.rotation = m.rotationQuaternion.toEulerAngles(); - m.rotationQuaternion = null; - //m.rotation.x = -m.rotation.x; - m.rotation.y = -m.rotation.y; - m.rotation.z = -m.rotation.z; - for (const child of m.getChildren()) { - if (child instanceof BABYLON.Mesh) { - //child.scaling = child.scaling.scale(WORLD_SCALE);// cmをmに - //child.position = child.position.scale(WORLD_SCALE); - - const pos = child.position.clone(); - const scaling = child.scaling.clone(); - child.scaling.x = -WORLD_SCALE; - child.scaling.y = WORLD_SCALE; - child.scaling.z = WORLD_SCALE; - - const rotation = child.rotationQuaternion ? child.rotationQuaternion.toEulerAngles() : child.rotation.clone(); - child.rotationQuaternion = null; - child.position = new BABYLON.Vector3(0, 0, 0); - - child.parent = root; - child.bakeCurrentTransformIntoVertices(); - child.parent = m; - child.scaling = scaling; - child.position.x = pos.x * -WORLD_SCALE; - child.position.y = pos.y * WORLD_SCALE; - child.position.z = pos.z * WORLD_SCALE; - - child.rotation = rotation; - - scaleMorph(child, [-WORLD_SCALE, WORLD_SCALE, WORLD_SCALE]); - - //const indices = child.getIndices(); - //const positions = child.getVerticesData(BABYLON.VertexBuffer.PositionKind); - //const normals = child.getVerticesData(BABYLON.VertexBuffer.NormalKind); - //BABYLON.VertexData.ComputeNormals(positions, indices, normals); - //child.updateVerticesData(BABYLON.VertexBuffer.NormalKind, normals, false, false); - } else if (child instanceof BABYLON.InstancedMesh) { - const pos = child.position.clone(); - child.position.x = pos.x * -WORLD_SCALE; - child.position.y = pos.y * WORLD_SCALE; - child.position.z = pos.z * WORLD_SCALE; - } else if (child instanceof BABYLON.TransformNode) { - bakeTransformNode(child); - } - } - }; - - const bakeChildren = (node: BABYLON.Node) => { - for (const m of node.getChildren(undefined, true)) { - if (m instanceof BABYLON.Mesh) { - const scaling = m.scaling.clone(); - m.scaling.x = -WORLD_SCALE; - m.scaling.y = WORLD_SCALE; - m.scaling.z = WORLD_SCALE; - //m.position.x *= -WORLD_SCALE; - //m.position.y *= WORLD_SCALE; - //m.position.z *= WORLD_SCALE; - const pos = m.position.clone(); - const rotation = m.rotationQuaternion.toEulerAngles(); - m.rotationQuaternion = null; - m.rotation = new BABYLON.Vector3(0, 0, 0); - m.position = new BABYLON.Vector3(0, 0, 0); - m.bakeCurrentTransformIntoVertices(); - m.scaling.x = scaling.x; - m.scaling.y = scaling.y; - m.scaling.z = scaling.z; - m.position.x = pos.x * -WORLD_SCALE; - m.position.y = pos.y * WORLD_SCALE; - m.position.z = pos.z * WORLD_SCALE; - m.rotation = rotation; - //m.rotation.x = -m.rotation.x; - m.rotation.y = -m.rotation.y; - m.rotation.z = -m.rotation.z; - - scaleMorph(m, [-WORLD_SCALE, WORLD_SCALE, WORLD_SCALE]); - } else if (m instanceof BABYLON.InstancedMesh) { - //const pos = m.position.clone(); - //m.position.x = pos.x * -WORLD_SCALE; - //m.position.y = pos.y * WORLD_SCALE; - //m.position.z = pos.z * WORLD_SCALE; - m.position.x *= -WORLD_SCALE; - m.position.y *= WORLD_SCALE; - m.position.z *= WORLD_SCALE; - m.rotation = m.rotationQuaternion.toEulerAngles(); - m.rotationQuaternion = null; - m.rotation.x = -m.rotation.x; - m.rotation.y = -m.rotation.y; - m.rotation.z = -m.rotation.z; - } else if (m instanceof BABYLON.TransformNode) { - bakeTransformNode(m); - } - } - }; - - bakeChildren(root); - - subRoot.dispose(); - } else { - // meshじゃなくtransform nodeにしてパフォーマンス向上 - const _subRoot = new BABYLON.TransformNode('__root__', this.scene); - _subRoot.scaling.x = -1; - _subRoot.scaling = _subRoot.scaling.scale(WORLD_SCALE);// cmをmに - - for (const m of subRoot.getChildren()) { - if (m.parent === subRoot) { - m.parent = _subRoot; - } - } - - subRoot.dispose(); - subRoot = _subRoot; - } - - def.treatLoaderResult?.(loaderResult); + const convertedOptions = convertRawOptions(def.options.schema, args.options, this.roomAttachments); const metadata = { isObject: true, @@ -845,15 +613,23 @@ export class RoomEngine extends EngineBase<{ objectType: args.type, }; - if (!BAKE_TRANSFORM) { - root.addChild(subRoot); - } - - root.position = args.position.clone(); - root.rotation = args.rotation.clone(); - root.metadata = metadata; - - const model = new ModelManager(BAKE_TRANSFORM ? root : subRoot, loaderResult.meshes.filter(m => !m.isDisposed() && m.name !== '__root__'), def.hasTexture, (meshes) => { + const container = new ObjectContainer({ + id: args.id, + type: args.type, + position: args.position.clone(), + rotation: args.rotation.clone(), + options: convertedOptions, + metadata, + sr: this.sr, + getIsSrReady: () => this.inited, + lightContainer: this.lightContainer, + graphicsQuality: this.graphicsQuality, + scene: this.scene, + sitChair: () => { + this.sitChair(args.id); + }, + }); + container.onMeshesUpdated = (meshes) => { if (this.selected?.objectId === args.id) { this.highlightMeshes(meshes); } @@ -917,69 +693,17 @@ export class RoomEngine extends EngineBase<{ mesh.geometry.clearCachedData(); } */ - }); + }; - const convertedOptions = convertRawOptions(def.options.schema, args.options, this.roomAttachments); - - const objectInstance = await def.createInstance({ - scene: this.scene, - sr: { - updateMesh: (mesh) => { - if (!this.inited) return; - this.sr.updateMesh(mesh); - }, - reset: () => { - if (!this.inited) return; - this.sr.disableSnapshotRendering(); - this.sr.enableSnapshotRendering(); - }, - fixParticleSystem: (ps) => this.sr.fixParticleSystem(ps), - }, - lc: this.lightContainer, - root, - options: convertedOptions, - model, - id: args.id, - timer: this.timer, // TODO: 家具が撤去された後も動作し続けるのをどうにかする - graphicsQuality: this.graphicsQuality, - sitChair: () => { - this.sitChair(args.id); - }, - stickyMarkerMeshUpdated: (mesh) => { - // TODO - //// stickyな子の位置を更新 - //if (mesh.name.includes('__TOP__')) { - // mesh.unfreezeWorldMatrix(); - // mesh.computeWorldMatrix(true); - // const updateChildStickyObjectPosition = (objectId: string) => { - // const stickyObjectIds = Array.from(this.roomState.installedObjects.filter(o => o.sticky === objectId)).map(o => o.id); - // for (const soid of stickyObjectIds) { - // const soMesh = this.objectEntities.get(soid)!.rootMesh; - // soMesh.unfreezeWorldMatrix(); - // for (const m of soMesh.getChildMeshes()) { - // m.unfreezeWorldMatrix(); - // } - // console.log(mesh.getAbsolutePosition().y); - // soMesh.position.y = mesh.getAbsolutePosition().y; - // updateChildStickyObjectPosition(soid); - // } - // }; - // updateChildStickyObjectPosition(args.id); - //} - }, - }); - - objectInstance.onInited?.(); - - model.bakeMesh(); + await container.load(); if (def.hasCollisions) { - enableObjectCollision(root.getChildMeshes()); + enableObjectCollision(container.root.getChildMeshes()); } - this.objectEntities.set(args.id, { convertedOptions, instance: objectInstance, rootMesh: root, model }); + this.objectContainers.set(args.id, container); - return { root, objectInstance }; + return container; } public cameraMove(vector: { x: number; y: number; }, dash: boolean) { @@ -1001,18 +725,18 @@ export class RoomEngine extends EngineBase<{ if (currentSelected != null) { this.selected = null; this.clearHighlight(); - currentSelected.objectEntity.model.bakeMesh(); + currentSelected.objectContainer.model.bakeMesh(); } if (objectId != null) { - const entity = this.objectEntities.get(objectId); - if (entity != null) { - entity.model.unbakeMesh(); - this.highlightMeshes(entity.rootMesh.getChildMeshes()); + const container = this.objectContainers.get(objectId); + if (container != null) { + container.model.unbakeMesh(); + this.highlightMeshes(container.root.getChildMeshes()); const state = this.roomState.installedObjects.find(o => o.id === objectId)!; this.selected = { objectId, - objectEntity: entity, + objectContainer: container, objectState: state, objectDef: getObjectDef(state.type), }; @@ -1230,7 +954,7 @@ export class RoomEngine extends EngineBase<{ this.sr.disableSnapshotRendering(); - const selectedObject = this.selected.objectEntity.rootMesh; + const selectedObject = this.selected.objectContainer.root; this.clearHighlight(); const initialPosition = selectedObject.position.clone(); @@ -1240,7 +964,7 @@ export class RoomEngine extends EngineBase<{ const setStickyParentRecursively = (mesh: BABYLON.AbstractMesh) => { const stickyObjectIds = Array.from(this.roomState.installedObjects.filter(o => o.sticky === mesh.metadata.objectId)).map(o => o.id); for (const soid of stickyObjectIds) { - const soMesh = this.objectEntities.get(soid)!.rootMesh; + const soMesh = this.objectContainers.get(soid)!.root; setStickyParentRecursively(soMesh); soMesh.setParent(mesh); soMesh.unfreezeWorldMatrix(); @@ -1300,7 +1024,7 @@ export class RoomEngine extends EngineBase<{ const removeStickyParentRecursively = (mesh: BABYLON.Mesh) => { const stickyObjectIds = Array.from(this.roomState.installedObjects.filter(o => o.sticky === mesh.metadata.objectId)).map(o => o.id); for (const soid of stickyObjectIds) { - const soMesh = this.objectEntities.get(soid)!.rootMesh; + const soMesh = this.objectContainers.get(soid)!.root; soMesh.setParent(null); removeStickyParentRecursively(soMesh); @@ -1338,7 +1062,7 @@ export class RoomEngine extends EngineBase<{ const removeStickyParentRecursively = (mesh: BABYLON.Mesh) => { const stickyObjectIds = Array.from(this.roomState.installedObjects.filter(o => o.sticky === mesh.metadata.objectId)).map(o => o.id); for (const soid of stickyObjectIds) { - const soMesh = this.objectEntities.get(soid)!.rootMesh; + const soMesh = this.objectContainers.get(soid)!.root; soMesh.setParent(null); const pos = soMesh.position.clone(); @@ -1403,7 +1127,7 @@ export class RoomEngine extends EngineBase<{ public interact(oid: string, iid: string | null = null) { const o = this.roomState.installedObjects.find(o => o.id === oid)!; - const entity = this.objectEntities.get(o.id)!; + const entity = this.objectContainers.get(o.id)!; if (iid == null) { if (entity.instance.primaryInteraction != null) { @@ -1416,7 +1140,7 @@ export class RoomEngine extends EngineBase<{ public sitChair(objectId: string) { this.isSitting = true; - this.fixedCamera.parent = this.objectEntities.get(objectId)!.rootMesh; + this.fixedCamera.parent = this.objectContainers.get(objectId)!.root; this.fixedCamera.position = new BABYLON.Vector3(0, cm(120), 0); this.fixedCamera.rotation = new BABYLON.Vector3(0, 0, 0); this.scene.activeCamera = this.fixedCamera; @@ -1475,22 +1199,14 @@ export class RoomEngine extends EngineBase<{ if (!this.isEditMode) return; if (this.grabbingCtx != null) return; - if (attachments != null) { - this.roomAttachments = attachments; - } - + if (attachments != null) this.roomAttachments = attachments; this.selectObject(null); - const dir = this.camera.getDirection(BABYLON.Axis.Z).scale(this.scene.useRightHandedSystem ? -1 : 1); - const distance = cm(50); - const id = genId(); - const def = getObjectDef(type); - const options = _options != null ? deepClone(_options) : deepClone(def.options.default); - const { root } = await this.loadObject({ + const container = await this.loadObject({ id: id, type, position: new BABYLON.Vector3(0, 0, 0), @@ -1498,13 +1214,14 @@ export class RoomEngine extends EngineBase<{ options, }); - root.unfreezeWorldMatrix(); - for (const m of root.getChildMeshes()) { + container.root.unfreezeWorldMatrix(); + for (const m of container.root.getChildMeshes()) { m.unfreezeWorldMatrix(); m.checkCollisions = false; } - const ghost = this.createGhost(root); + const ghost = this.createGhost(container.root); + const distance = cm(50); let sticky: string | null; let grabbingEnded = false; @@ -1513,7 +1230,7 @@ export class RoomEngine extends EngineBase<{ objectId: id, objectType: type, forInstall: true, - mesh: root, + mesh: container.root, originalDiffOfPosition: new BABYLON.Vector3(0, 0, 0), originalDiffOfRotation: new BABYLON.Vector3(0, Math.PI, 0), distance: distance, @@ -1531,11 +1248,11 @@ export class RoomEngine extends EngineBase<{ grabbingEnded = true; if (def.hasCollisions) { - enableObjectCollision(root.getChildMeshes()); + enableObjectCollision(container.root.getChildMeshes()); } - const pos = root.position.clone(); - const rotation = root.rotation.clone(); + const pos = container.root.position.clone(); + const rotation = container.root.rotation.clone(); // 場合によってはなぜかSRが効かなくなる //const putParticleSystem = this.getPutParticleSystem(); @@ -1548,11 +1265,11 @@ export class RoomEngine extends EngineBase<{ }); // put animation - root.animations.push(def.placement === 'side' || def.placement === 'wall' ? this.putAnimH : this.putAnimV); + container.root.animations.push(def.placement === 'side' || def.placement === 'wall' ? this.putAnimH : this.putAnimV); const animationObserver = this.scene.onAfterAnimationsObservable.add(() => { - this.sr.updateMesh(root.getChildMeshes(), true); + this.sr.updateMesh(container.root.getChildMeshes(), true); }); - this.scene.beginAnimation(root, 0, 60, false, 3, () => { + this.scene.beginAnimation(container.root, 0, 60, false, 3, () => { this.scene.onAfterAnimationsObservable.remove(animationObserver); }); @@ -1590,7 +1307,7 @@ export class RoomEngine extends EngineBase<{ public enterEditMode() { this.isEditMode = true; - for (const entity of this.objectEntities.values()) { + for (const entity of this.objectContainers.values()) { entity.instance.resetTemporaryState?.(); } @@ -1680,8 +1397,8 @@ export class RoomEngine extends EngineBase<{ const objectId = this.selected.objectId; - this.objectEntities.get(objectId)?.rootMesh.dispose(); - this.objectEntities.delete(objectId); + this.objectContainers.get(objectId)?.destroy(); + this.objectContainers.delete(objectId); this.roomState.installedObjects = this.roomState.installedObjects.filter(o => o.id !== objectId); for (const o of this.roomState.installedObjects.filter(o => o.sticky === objectId)) { o.sticky = null; @@ -1719,14 +1436,14 @@ export class RoomEngine extends EngineBase<{ this.ev('changeRoomState', { roomState: this.roomState }); - const entity = this.objectEntities.get(objectId); - if (entity == null) return; + const container = this.objectContainers.get(objectId); + if (container == null) return; const converted = convertRawOptions(def.options.schema, o.options, this.roomAttachments); - entity.convertedOptions[key] = converted[key]; + container.options[key] = converted[key]; this.sr.disableSnapshotRendering(); - entity.instance.onOptionsUpdated?.([key, converted[key]]); + container.optionsUpdated(key, converted[key]); this.sr.enableSnapshotRendering(); } diff --git a/packages/frontend/src/world/room/object-defs.ts b/packages/frontend/src/world/room/object-defs.ts index 61cddb3858..1b1e965c83 100644 --- a/packages/frontend/src/world/room/object-defs.ts +++ b/packages/frontend/src/world/room/object-defs.ts @@ -88,7 +88,7 @@ import { speakerStand } from './objects/speakerStand.js'; import { spotLight } from './objects/spotLight.js'; import { sprayer } from './objects/sprayer.js'; import { stanchionPole } from './objects/stanchionPole.js'; -import { steelRack60x35, steelRack90x35 } from './objects/steelRack.js'; +import { steelRack } from './objects/steelRack.js'; import { stormGlass } from './objects/stormGlass.js'; import { tableSalt } from './objects/tableSalt.js'; import { tabletopCalendar } from './objects/tabletopCalendar.js'; @@ -193,8 +193,7 @@ export const OBJECT_DEFS = [ speaker, speakerStand, sprayer, - steelRack60x35, - steelRack90x35, + steelRack, stormGlass, tableSalt, tabletopCalendar, diff --git a/packages/frontend/src/world/room/object.ts b/packages/frontend/src/world/room/object.ts index 9b7a4dcb92..22628932f9 100644 --- a/packages/frontend/src/world/room/object.ts +++ b/packages/frontend/src/world/room/object.ts @@ -115,10 +115,15 @@ type GetConvertedOptionsSchemaValues = { never; }; +export type SnapshotRenderingHelperWrapper = { + updateMesh: (meshes: BABYLON.Mesh[]) => void; + reset: () => void; + fixParticleSystem: (ps: BABYLON.ParticleSystem) => void; +}; + export type ObjectDef = { id: string; name: string; - path?: string; options: { schema: OpSc extends undefined ? OptionsSchema : NonNullable; default: OpSc extends undefined ? RawOptions : GetRawOptionsSchemaValues>; @@ -130,15 +135,12 @@ export type ObjectDef = { //groupingMeshes: string[]; // multi-materialなメッシュは複数のメッシュに分割されるが、それだと不便な場合に追加の親メッシュでグルーピングするための指定 isChair?: boolean; treatLoaderResult?: (loaderResult: BABYLON.AssetContainer) => void; + path?: (options: OpSc extends undefined ? ConvertedOptions : Readonly>>) => string; createInstance: (args: { scene: BABYLON.Scene; // TODO: snapshot renderingの関心を隠蔽した方が綺麗かもしれない // 例えばmaterialUpdatedというメソッドを用意して内部的にresetを呼ぶなど - sr: { - updateMesh: (meshes: BABYLON.Mesh[]) => void; - reset: () => void; - fixParticleSystem: (ps: BABYLON.ParticleSystem) => void; - }; + sr: SnapshotRenderingHelperWrapper; lc: BABYLON.ClusteredLightContainer | null; root: BABYLON.TransformNode; options: OpSc extends undefined ? ConvertedOptions : Readonly>>; @@ -148,6 +150,7 @@ export type ObjectDef = { graphicsQuality: number; stickyMarkerMeshUpdated?: (mesh: BABYLON.Mesh) => void; sitChair?: () => void; + reloadModel: () => void; }) => RoomObjectInstance>> | Promise>>>; // TODO: createInstanceをasyncにするのではなく、別にreadyみたいなものを返させる }; diff --git a/packages/frontend/src/world/room/objects/ironFrameShelf.ts b/packages/frontend/src/world/room/objects/ironFrameShelf.ts index fe03eeefab..5eef862f8b 100644 --- a/packages/frontend/src/world/room/objects/ironFrameShelf.ts +++ b/packages/frontend/src/world/room/objects/ironFrameShelf.ts @@ -78,17 +78,17 @@ const base = defineObjectClass({ export const ironFrameShelf5 = base.extend({ id: 'ironFrameShelf5', name: 'ironFrameShelf 5', - path: 'iron-frame-shelf/iron-frame-shelf-5', + path: () => 'iron-frame-shelf/iron-frame-shelf-5', }); export const ironFrameShelf4 = base.extend({ id: 'ironFrameShelf4', name: 'ironFrameShelf 4', - path: 'iron-frame-shelf/iron-frame-shelf-4', + path: () => 'iron-frame-shelf/iron-frame-shelf-4', }); export const ironFrameShelf3 = base.extend({ id: 'ironFrameShelf3', name: 'ironFrameShelf 3', - path: 'iron-frame-shelf/iron-frame-shelf-3', + path: () => 'iron-frame-shelf/iron-frame-shelf-3', }); diff --git a/packages/frontend/src/world/room/objects/steelRack.ts b/packages/frontend/src/world/room/objects/steelRack.ts index b5467f3761..8eb87d94d3 100644 --- a/packages/frontend/src/world/room/objects/steelRack.ts +++ b/packages/frontend/src/world/room/objects/steelRack.ts @@ -4,10 +4,12 @@ */ import * as BABYLON from '@babylonjs/core'; -import { defineObjectClass } from '../object.js'; +import { defineObject } from '../object.js'; import { cm, remap } from '@/world/utility.js'; -const base = defineObjectClass({ +export const steelRack = defineObject({ + id: 'steelRack', + name: 'steelRack', options: { schema: { shelfColor: { @@ -18,6 +20,11 @@ const base = defineObjectClass({ type: 'color', label: 'Pole color', }, + widthAndDepthVariation: { + type: 'enum', + label: 'W x D', + enum: ['60-35', '90-35'], + }, height: { type: 'range', label: 'Height', @@ -106,6 +113,7 @@ const base = defineObjectClass({ default: { shelfColor: [0.8, 0.8, 0.8], poleColor: [0.8, 0.8, 0.8], + widthAndDepthVariation: '60-35', height: 5, numberOfShelfs: 5, shelf1Position: 0.0, @@ -123,7 +131,8 @@ const base = defineObjectClass({ placement: 'floor', hasCollisions: true, hasTexture: true, - createInstance: ({ options, model }) => { + path: (options) => `steel-rack/${options.widthAndDepthVariation}`, + createInstance: ({ options, model, reloadModel }) => { const matrix = model.root.getWorldMatrix(true); const scale = new BABYLON.Vector3(); matrix.decompose(scale); @@ -207,6 +216,7 @@ const base = defineObjectClass({ switch (k) { case 'shelfColor': applyShelfColor(); break; case 'poleColor': applyPoleColor(); break; + case 'widthAndDepthVariation': reloadModel(); break; case 'height': applyHeight(); break; case 'numberOfShelfs': applyNumberOfShelfs(); break; case 'shelf1Position': @@ -225,15 +235,3 @@ const base = defineObjectClass({ }; }, }); - -export const steelRack60x35 = base.extend({ - id: 'steelRack60x35', - name: 'steelRack60x35', - path: 'steel-rack/60-35', -}); - -export const steelRack90x35 = base.extend({ - id: 'steelRack90x35', - name: 'steelRack90x35', - path: 'steel-rack/90-35', -}); diff --git a/packages/frontend/src/world/room/previewEngine.ts b/packages/frontend/src/world/room/previewEngine.ts index efc377ec16..d14b247184 100644 --- a/packages/frontend/src/world/room/previewEngine.ts +++ b/packages/frontend/src/world/room/previewEngine.ts @@ -243,7 +243,7 @@ export class RoomObjectPreviewEngine extends EngineBase<{ const root = new BABYLON.Mesh(`object_${args.type}`, this.scene); - const filePath = def.path != null ? `/client-assets/room/objects/${def.path}.glb` : `/client-assets/room/objects/${camelToKebab(args.type)}/${camelToKebab(args.type)}.glb`; + const filePath = def.path != null ? `/client-assets/room/objects/${def.path(this.convertedObjectOptions!)}.glb` : `/client-assets/room/objects/${camelToKebab(args.type)}/${camelToKebab(args.type)}.glb`; const loaderResult = await BABYLON.LoadAssetContainerAsync(filePath, this.scene); // babylonによって自動で追加される右手系変換用ノード @@ -312,11 +312,14 @@ export class RoomObjectPreviewEngine extends EngineBase<{ fixParticleSystem: (ps) => this.sr.fixParticleSystem(ps), }, root, - options: this.convertedObjectOptions, + options: this.convertedObjectOptions!, model, id: args.id, timer: this.timerForEachObject, graphicsQuality: GRAPHICS_QUALITY.MEDIUM, + reloadModel: () => { + this.reloadModel(); + }, }); objectInstance.onInited?.(); @@ -354,6 +357,15 @@ export class RoomObjectPreviewEngine extends EngineBase<{ this.sr.enableSnapshotRendering(); } + private async reloadModel() { + if (this.objectType == null) return; + this.clearObject(); + await this.loadObject_({ + type, + id, + }); + } + public cameraRotate(vector: { x: number; y: number; }) { (this.camera.inputs.attached.manual as ArcRotateCameraManualInput).setRotationVector(vector); }