From 6aa741f8d47313b1622822af39b9cae2f6646158 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Mon, 20 Apr 2026 14:32:46 +0900 Subject: [PATCH] wip --- packages/frontend/src/pages/room.vue | 21 +++ .../frontend/src/world/room/controller.ts | 16 ++ packages/frontend/src/world/room/engine.ts | 150 ++++++------------ packages/frontend/src/world/room/heya.ts | 128 +++++++++++++++ packages/frontend/src/world/room/utility.ts | 1 + 5 files changed, 218 insertions(+), 98 deletions(-) create mode 100644 packages/frontend/src/world/room/heya.ts diff --git a/packages/frontend/src/pages/room.vue b/packages/frontend/src/pages/room.vue index dbd40aff3c..54da48d1ef 100644 --- a/packages/frontend/src/pages/room.vue +++ b/packages/frontend/src/pages/room.vue @@ -64,6 +64,23 @@ SPDX-License-Identifier: AGPL-3.0-only + +
+
+ Room options + + + + + + +
+
@@ -75,6 +92,7 @@ SPDX-License-Identifier: AGPL-3.0-only Exit edit mode Edit mode addObject + roomSettings Export Import @@ -109,6 +127,7 @@ function resize() { } const isZenMode = ref(false); +const isRoomSettingsOpen = ref(false); const data = localStorage.getItem('roomData') != null ? { ...JSON.parse(localStorage.getItem('roomData')!), ...{ heya: { @@ -177,6 +196,8 @@ const data = localStorage.getItem('roomData') != null ? { ...JSON.parse(localSto installedObjects: [], }; +console.log(data); + const controller = new RoomController(data); onMounted(async () => { diff --git a/packages/frontend/src/world/room/controller.ts b/packages/frontend/src/world/room/controller.ts index c052d9abbd..16da5bf687 100644 --- a/packages/frontend/src/world/room/controller.ts +++ b/packages/frontend/src/world/room/controller.ts @@ -182,6 +182,22 @@ export class RoomController { } } + public changeHeyaType(type: RoomState['heya']['type']) { + if (this.worker != null) { + this.worker.postMessage({ type: 'changeHeyaType', heyaType: type }); + } else if (this.engine != null) { + this.engine.changeHeyaType(type); + } + } + + public updateHeyaOptions(options: RoomState['heya']['options']) { + if (this.worker != null) { + this.worker.postMessage({ type: 'updateHeyaOptions', heyaOptions: options }); + } else if (this.engine != null) { + this.engine.updateHeyaOptions(options); + } + } + public beginSelectedInstalledObjectGrabbing() { if (this.worker != null) { this.worker.postMessage({ type: 'beginSelectedInstalledObjectGrabbing' }); diff --git a/packages/frontend/src/world/room/engine.ts b/packages/frontend/src/world/room/engine.ts index 12635bec31..489fef0450 100644 --- a/packages/frontend/src/world/room/engine.ts +++ b/packages/frontend/src/world/room/engine.ts @@ -16,7 +16,9 @@ import { GridMaterial } from '@babylonjs/materials'; import { EventEmitter } from 'eventemitter3'; import { TIME_MAP, scaleMorph, HorizontalCameraKeyboardMoveInput, camelToKebab, cm, WORLD_SCALE, getMeshesBoundingBox, Timer } from '../utility.js'; import { getObjectDef } from './object-defs.js'; -import { findMaterial, ModelManager, SYSTEM_MESH_NAMES } from './utility.js'; +import { findMaterial, ModelManager, SYSTEM_HEYA_MESH_NAMES, SYSTEM_MESH_NAMES } from './utility.js'; +import { SimpleHeyaManager } from './heya.js'; +import type { HeyaManager, JapaneseHeyaOptions, SimpleHeyaOptions } from './heya.js'; import type { ObjectDef, RoomObjectInstance, RoomStateObject } from './object.js'; import { genId } from '@/utility/id.js'; import { deepClone } from '@/utility/clone.js'; @@ -27,35 +29,14 @@ const IGNORE_OBJECTS: string[] = []; // for debug const USE_GLOW = true; // ドローコールが増えて重い const IN_WEB_WORKER = typeof window === 'undefined'; -type SimpleHeyaWallBase = { - material: null | 'wood' | 'concrete'; - color: [number, number, number]; -}; - -type Heya = { - type: 'simple'; - options: { - dimension: [number, number]; - window: 'none' | 'kosidakamado' | 'demado' | 'hakidasimado'; - wallN: SimpleHeyaWallBase; - wallE: SimpleHeyaWallBase; - wallS: SimpleHeyaWallBase; - wallW: SimpleHeyaWallBase; - flooring: { - material: null | 'wood' | 'concrete'; - color: [number, number, number]; - }; - ceiling: { - material: null | 'wood' | 'concrete'; - color: [number, number, number]; - }; - }; -} | { - type: 'japanese'; -}; - export type RoomState = { - heya: Heya; + heya: { + type: 'simple'; + options: SimpleHeyaOptions; + } | { + type: 'japanese'; + options: JapaneseHeyaOptions; + }; installedObjects: RoomStateObject[]; }; @@ -142,6 +123,7 @@ export class RoomEngine extends EventEmitter { instance: RoomObjectInstance; model: ModelManager; }> = new Map(); + private heyaManager: HeyaManager | null = null; // TODO: たぶんオブジェクト内の値のmutateはsetで検知できないので、そのような操作を実際に行うようになった & それを検知する必要性が出てきたら専用の設定関数などを新設してそれを使わせる private _grabbingCtx: { @@ -425,7 +407,7 @@ export class RoomEngine extends EventEmitter { } public async init() { - await this.loadRoomModel(); + await this.loadHeya(); //await this.loadEnvModel(); const objects = this.roomState.installedObjects.filter(o => !IGNORE_OBJECTS.includes(o.type)); @@ -742,77 +724,49 @@ export class RoomEngine extends EventEmitter { } } - private async loadRoomModel() { - //await BABYLON.InitializeCSG2Async(); + public async changeHeyaType(type: RoomState['heya']['type']) { + this.roomState.heya.type = type; - //const box = BABYLON.MeshBuilder.CreateBox('box', { size: cm(50) }, this.scene); - //const boxCsg = BABYLON.CSG2.FromMesh(box); + if (this.heyaManager != null) { + this.heyaManager.dispose(); + } - const meshes: BABYLON.Mesh[] = []; + const onMeshUpdatedCallback = (meshes: BABYLON.AbstractMesh[]) => { + for (const m of meshes) { + if (SYSTEM_HEYA_MESH_NAMES.some(name => m.name.includes(name))) { + m.isPickable = false; + m.receiveShadows = false; + m.isVisible = false; + m.checkCollisions = false; + continue; + } + + m.isPickable = false; + m.checkCollisions = false; + m.receiveShadows = true; + this.shadowGeneratorForRoomLight.addShadowCaster(m); + this.shadowGeneratorForSunLight.addShadowCaster(m); + //if (m.material) (m.material as BABYLON.PBRMaterial).ambientColor = new BABYLON.Color3(1, 1, 1); + if (m.material) { + (m.material as BABYLON.PBRMaterial).reflectionTexture = this.envMapIndoor; + (m.material as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts + } + } + }; if (this.roomState.heya.type === 'simple') { - const loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/rooms/default/300.glb', this.scene); - loaderResult.meshes[0].scaling = loaderResult.meshes[0].scaling.scale(WORLD_SCALE); - loaderResult.meshes[0].rotationQuaternion = null; - loaderResult.meshes[0].rotation = new BABYLON.Vector3(0, 0, 0); - - const wallNRoot = loaderResult.transformNodes.find(t => t.name.includes('__WALL_N__')); - const wallSRoot = loaderResult.transformNodes.find(t => t.name.includes('__WALL_S__')); - const wallWRoot = loaderResult.transformNodes.find(t => t.name.includes('__WALL_W__')); - const wallERoot = loaderResult.transformNodes.find(t => t.name.includes('__WALL_E__')); - - const wallMaterial = findMaterial(loaderResult.meshes[0], '__X_WALL__'); - - const wallNMaterial = wallMaterial.clone('wallNMaterial'); - wallNMaterial.albedoColor = new BABYLON.Color3(...this.roomState.heya.options.wallN.color); - - const wallSMaterial = wallMaterial.clone('wallSMaterial'); - wallSMaterial.albedoColor = new BABYLON.Color3(...this.roomState.heya.options.wallS.color); - - const wallWMaterial = wallMaterial.clone('wallWMaterial'); - wallWMaterial.albedoColor = new BABYLON.Color3(...this.roomState.heya.options.wallW.color); - - const wallEMaterial = wallMaterial.clone('wallEMaterial'); - wallEMaterial.albedoColor = new BABYLON.Color3(...this.roomState.heya.options.wallE.color); - - // TODO: wall meshが一部instanced meshになっているせいでマテリアルが共有されるのを直す - - for (const m of wallNRoot.getChildMeshes().filter(m => m.material === wallMaterial)) { - m.material = wallNMaterial; - } - for (const m of wallSRoot.getChildMeshes().filter(m => m.material === wallMaterial)) { - m.material = wallSMaterial; - } - for (const m of wallWRoot.getChildMeshes().filter(m => m.material === wallMaterial)) { - m.material = wallWMaterial; - } - for (const m of wallERoot.getChildMeshes().filter(m => m.material === wallMaterial)) { - m.material = wallEMaterial; - } - - meshes.push(...loaderResult.meshes); + const heyaManager = new SimpleHeyaManager(onMeshUpdatedCallback); + await heyaManager.load(this.roomState.heya.options, this.scene); + this.heyaManager = heyaManager; + } else if (this.roomState.heya.type === 'japanese') { + // TODO } - for (const m of meshes) { - if (m.name.includes('__ROOM_WALL__') || m.name.includes('__ROOM_SIDE__') || m.name.includes('__ROOM_FLOOR__') || m.name.includes('__ROOM_CEILING__') || m.name.includes('__ROOM_TOP__') || m.name.includes('__ROOM_BOTTOM__')) { - m.isPickable = false; - m.receiveShadows = false; - m.isVisible = false; - m.checkCollisions = false; - continue; - } + this.emit('changeRoomState', { roomState: this.roomState }); + } - m.isPickable = false; - m.checkCollisions = false; - m.receiveShadows = true; - this.shadowGeneratorForRoomLight.addShadowCaster(m); - this.shadowGeneratorForSunLight.addShadowCaster(m); - //if (m.material) (m.material as BABYLON.PBRMaterial).ambientColor = new BABYLON.Color3(1, 1, 1); - if (m.material) { - (m.material as BABYLON.PBRMaterial).reflectionTexture = this.envMapIndoor; - (m.material as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts - } - } + private async loadHeya() { + await this.changeHeyaType(this.roomState.heya.type); } private async loadObject(args: { @@ -1592,11 +1546,11 @@ export class RoomEngine extends EventEmitter { const entity = this.objectEntities.get(objectId); if (entity == null) return; entity.instance.onOptionsUpdated?.([key, value]); + } - if (this.selected?.objectId === objectId) { - // TODO - //triggerRef(this.selected); - } + public updateHeyaOptions(options: RoomState['heya']['options']) { + this.heyaManager.applyOptions(options); + this.emit('changeRoomState', { roomState: this.roomState }); } private playSfxUrl(url: string, options: { volume: number; playbackRate: number }) { diff --git a/packages/frontend/src/world/room/heya.ts b/packages/frontend/src/world/room/heya.ts new file mode 100644 index 0000000000..e41987e876 --- /dev/null +++ b/packages/frontend/src/world/room/heya.ts @@ -0,0 +1,128 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as BABYLON from '@babylonjs/core'; +import { WORLD_SCALE } from '../utility.js'; +import { findMaterial } from './utility.js'; + +//export interface HeyaManager { +// constructor(onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null): void; +// load: (options: T, scene: BABYLON.Scene) => Promise; +// applyOptions: (options: T) => void; +// dispose: () => void; +//} + +export abstract class HeyaManager { + protected onMeshUpdatedCallback: ((meshes: BABYLON.AbstractMesh[]) => void) | null = null; + + constructor(onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null) { + this.onMeshUpdatedCallback = onMeshUpdatedCallback ?? null; + } + + abstract load(options: T, scene: BABYLON.Scene): Promise; + abstract applyOptions(options: T): void; + abstract dispose(): void; +} + +type SimpleHeyaWallBase = { + material: null | 'wood' | 'concrete'; + color: [number, number, number]; +}; + +export type SimpleHeyaOptions = { + dimension: [number, number]; + window: 'none' | 'kosidakamado' | 'demado' | 'hakidasimado'; + wallN: SimpleHeyaWallBase; + wallE: SimpleHeyaWallBase; + wallS: SimpleHeyaWallBase; + wallW: SimpleHeyaWallBase; + flooring: { + material: null | 'wood' | 'concrete'; + color: [number, number, number]; + }; + ceiling: { + material: null | 'wood' | 'concrete'; + color: [number, number, number]; + }; +}; + +export type JapaneseHeyaOptions = { + window: 'none' | 'kosidakamado' | 'demado' | 'hakidasimado'; +}; + +export class SimpleHeyaManager extends HeyaManager { + private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null; + private wallNRoot: BABYLON.TransformNode | null = null; + private wallSRoot: BABYLON.TransformNode | null = null; + private wallWRoot: BABYLON.TransformNode | null = null; + private wallERoot: BABYLON.TransformNode | null = null; + private wallNMaterial: BABYLON.Material | null = null; + private wallSMaterial: BABYLON.Material | null = null; + private wallWMaterial: BABYLON.Material | null = null; + private wallEMaterial: BABYLON.Material | null = null; + + constructor(onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null) { + super(onMeshUpdatedCallback); + } + + public async load(options: SimpleHeyaOptions, scene: BABYLON.Scene) { + this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/rooms/default/300.glb', scene); + this.loaderResult.meshes[0].scaling = this.loaderResult.meshes[0].scaling.scale(WORLD_SCALE); + this.loaderResult.meshes[0].rotationQuaternion = null; + this.loaderResult.meshes[0].rotation = new BABYLON.Vector3(0, 0, 0); + + this.wallNRoot = this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_N__'))!; + this.wallSRoot = this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_S__'))!; + this.wallWRoot = this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_W__'))!; + this.wallERoot = this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_E__'))!; + + const wallMaterial = findMaterial(this.loaderResult.meshes[0], '__X_WALL__'); + this.wallNMaterial = wallMaterial.clone('wallNMaterial'); + this.wallSMaterial = wallMaterial.clone('wallSMaterial'); + this.wallWMaterial = wallMaterial.clone('wallWMaterial'); + this.wallEMaterial = wallMaterial.clone('wallEMaterial'); + + // TODO: wall meshが一部instanced meshになっているせいでマテリアルが共有されるのを直す + + for (const m of this.wallNRoot.getChildMeshes().filter(m => m.material === wallMaterial)) { + m.material = this.wallNMaterial; + } + for (const m of this.wallSRoot.getChildMeshes().filter(m => m.material === wallMaterial)) { + m.material = this.wallSMaterial; + } + for (const m of this.wallWRoot.getChildMeshes().filter(m => m.material === wallMaterial)) { + m.material = this.wallWMaterial; + } + for (const m of this.wallERoot.getChildMeshes().filter(m => m.material === wallMaterial)) { + m.material = this.wallEMaterial; + } + + this.applyOptions(options); + } + + public applyOptions(options: SimpleHeyaOptions) { + this.wallNMaterial.albedoColor = new BABYLON.Color3(...options.wallN.color); + this.wallSMaterial.albedoColor = new BABYLON.Color3(...options.wallS.color); + this.wallWMaterial.albedoColor = new BABYLON.Color3(...options.wallW.color); + this.wallEMaterial.albedoColor = new BABYLON.Color3(...options.wallE.color); + + this.onMeshUpdatedCallback?.(this.loaderResult.meshes); + } + + public dispose() { + if (this.loaderResult != null) { + for (const m of this.loaderResult.meshes) { + m.dispose(); + } + for (const t of this.loaderResult.transformNodes) { + t.dispose(); + } + } + this.wallNMaterial?.dispose(); + this.wallSMaterial?.dispose(); + this.wallWMaterial?.dispose(); + this.wallEMaterial?.dispose(); + } +} diff --git a/packages/frontend/src/world/room/utility.ts b/packages/frontend/src/world/room/utility.ts index 652a60789d..39274c68eb 100644 --- a/packages/frontend/src/world/room/utility.ts +++ b/packages/frontend/src/world/room/utility.ts @@ -8,6 +8,7 @@ import { applyMorphTargetsToMesh, cm, getPlaneUvIndexes } from '../utility.js'; import type { RoomEngine } from './engine.js'; export const SYSTEM_MESH_NAMES = ['__TOP__', '__SIDE__', '__PICK__', '__COLLISION__']; +export const SYSTEM_HEYA_MESH_NAMES = ['__ROOM_WALL__', '__ROOM_SIDE__', '__ROOM_FLOOR__', '__ROOM_CEILING__', '__ROOM_TOP__', '__ROOM_BOTTOM__']; export function yuge(scene: BABYLON.Scene, mesh: BABYLON.Mesh, offset: BABYLON.Vector3) { const emitter = new BABYLON.TransformNode('emitter', scene);