ObjectContainer

This commit is contained in:
syuilo 2026-05-21 16:15:18 +09:00
commit dfbe765baa
9 changed files with 370 additions and 379 deletions

View file

@ -617,8 +617,8 @@ function showOtherMenu(ev: PointerEvent) {
title: i18n.ts.areYouSure,
}).then(({ canceled }) => {
if (canceled) return;
localStorage.removeItem('roomData');
window.location.reload();
// TODO
});
},
}, {

View file

@ -43,7 +43,7 @@ export abstract class EngineBase<EVs extends EngineBaseEvents> extends EventEmit
private currentRafId: number | null = null;
private startRenderLoop() {
protected startRenderLoop() {
if (this.fps == null) {
this.engine.runRenderLoop(() => {
this.scene.render();

View file

@ -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);
}
}

View file

@ -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<string, {
rootMesh: BABYLON.TransformNode;
convertedOptions: ConvertedOptions;
instance: RoomObjectInstance;
model: ModelManager;
}> = new Map();
public objectContainers: Map<string, ObjectContainer> = 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<string, infer V> ? V : never;
objectContainer: RoomEngine['objectContainers'] extends Map<string, infer V> ? 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();
}

View file

@ -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,

View file

@ -115,10 +115,15 @@ type GetConvertedOptionsSchemaValues<T extends OptionsSchema> = {
never;
};
export type SnapshotRenderingHelperWrapper = {
updateMesh: (meshes: BABYLON.Mesh[]) => void;
reset: () => void;
fixParticleSystem: (ps: BABYLON.ParticleSystem) => void;
};
export type ObjectDef<OpSc extends OptionsSchema | undefined = undefined> = {
id: string;
name: string;
path?: string;
options: {
schema: OpSc extends undefined ? OptionsSchema : NonNullable<OpSc>;
default: OpSc extends undefined ? RawOptions : GetRawOptionsSchemaValues<NonNullable<OpSc>>;
@ -130,15 +135,12 @@ export type ObjectDef<OpSc extends OptionsSchema | undefined = undefined> = {
//groupingMeshes: string[]; // multi-materialなメッシュは複数のメッシュに分割されるが、それだと不便な場合に追加の親メッシュでグルーピングするための指定
isChair?: boolean;
treatLoaderResult?: (loaderResult: BABYLON.AssetContainer) => void;
path?: (options: OpSc extends undefined ? ConvertedOptions : Readonly<GetConvertedOptionsSchemaValues<NonNullable<OpSc>>>) => 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<GetConvertedOptionsSchemaValues<NonNullable<OpSc>>>;
@ -148,6 +150,7 @@ export type ObjectDef<OpSc extends OptionsSchema | undefined = undefined> = {
graphicsQuality: number;
stickyMarkerMeshUpdated?: (mesh: BABYLON.Mesh) => void;
sitChair?: () => void;
reloadModel: () => void;
}) => RoomObjectInstance<OpSc extends undefined ? ConvertedOptions : GetConvertedOptionsSchemaValues<NonNullable<OpSc>>> | Promise<RoomObjectInstance<OpSc extends undefined ? ConvertedOptions : GetConvertedOptionsSchemaValues<NonNullable<OpSc>>>>; // TODO: createInstanceをasyncにするのではなく、別にreadyみたいなものを返させる
};

View file

@ -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',
});

View file

@ -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',
});

View file

@ -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);
}