From f03af71dc03e1a9a61d5cf1a8cd595e65b0efefc Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 3 May 2026 16:54:11 +0900 Subject: [PATCH] wip --- .../frontend/src/pages/room.env-options.vue | 1 + packages/frontend/src/world/room/engine.ts | 70 ++++++++++--------- packages/frontend/src/world/room/env.ts | 36 +++++++++- 3 files changed, 74 insertions(+), 33 deletions(-) diff --git a/packages/frontend/src/pages/room.env-options.vue b/packages/frontend/src/pages/room.env-options.vue index ef55aaabda..1b23c1af3d 100644 --- a/packages/frontend/src/pages/room.env-options.vue +++ b/packages/frontend/src/pages/room.env-options.vue @@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/world/room/engine.ts b/packages/frontend/src/world/room/engine.ts index 4edecbdb1f..62e9c29f04 100644 --- a/packages/frontend/src/world/room/engine.ts +++ b/packages/frontend/src/world/room/engine.ts @@ -27,7 +27,6 @@ import { deepClone } from '@/utility/clone.js'; const BAKE_TRANSFORM = false; // 実験的 const IGNORE_OBJECTS: string[] = ['aquarium']; // for debug -const RENDER_OUTDOOR_ENV = false; const IN_WEB_WORKER = typeof window === 'undefined'; export type RoomState = { @@ -237,6 +236,7 @@ export class RoomEngine extends EventEmitter { this.fps = options.fps; this.useGlow = options.graphicsQuality >= GRAPHICS_QUALITY_MEDIUM; + this.time = TIME_MAP[new Date().getHours() as keyof typeof TIME_MAP]; registerBuiltInLoaders(); @@ -250,32 +250,14 @@ export class RoomEngine extends EventEmitter { this.scene.skipPointerMovePicking = true; this.scene.skipFrustumClipping = true; // snapshot renderingでは全てのメッシュがアクティブになっている必要があるため this.scene.gravity = new BABYLON.Vector3(0, -0.1, 0).scale(WORLD_SCALE); + this.scene.ambientColor = new BABYLON.Color3(1.0, 0.9, 0.8); + this.scene.collisionsEnabled = true; this.sr = new BABYLON.SnapshotRenderingHelper(this.scene); - const skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(1000) }, this.scene); - const skyboxMat = new BABYLON.StandardMaterial('skyboxMat', this.scene); - skyboxMat.backFaceCulling = false; - skyboxMat.disableLighting = true; - skybox.material = skyboxMat; - skybox.infiniteDistance = true; - - this.time = TIME_MAP[new Date().getHours() as keyof typeof TIME_MAP]; - - if (this.time === 0) { - skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.9, 1.0); - } else if (this.time === 1) { - skyboxMat.emissiveColor = new BABYLON.Color3(0.8, 0.5, 0.3); - } else { - skyboxMat.emissiveColor = new BABYLON.Color3(0.05, 0.05, 0.2); - } - this.scene.ambientColor = new BABYLON.Color3(1.0, 0.9, 0.8); - - this.scene.collisionsEnabled = true; - this.camera = new BABYLON.FreeCamera('camera', new BABYLON.Vector3(0, cm(130), cm(0)), this.scene); this.camera.minZ = cm(1); - this.camera.maxZ = RENDER_OUTDOOR_ENV ? cm(10000) : cm(1000); + this.camera.maxZ = cm(1000); this.camera.fov = 1; this.camera.ellipsoid = new BABYLON.Vector3(cm(15), cm(65), cm(15)); this.camera.checkCollisions = true; @@ -580,7 +562,7 @@ export class RoomEngine extends EventEmitter { } } - public pauseRender() { + public pauseRender() { // TODO: srと同じく参照カウント方式にした方が便利そう this.engine.stopRenderLoop(); if (this.currentRafId != null) { // workerで実行される可能性がある @@ -828,9 +810,8 @@ export class RoomEngine extends EventEmitter { public async changeEnvType(type: RoomState['env']['type'], forInit = false) { this.roomState.env.type = type; - if (this.envManager != null) { - this.envManager.dispose(); - } + this.sr.disableSnapshotRendering(); + this.pauseRender(); const onMeshUpdatedCallback = (meshes: BABYLON.AbstractMesh[]) => { for (const m of meshes) { @@ -856,18 +837,41 @@ export class RoomEngine extends EventEmitter { } }; + let envManager: EnvManager; + if (this.roomState.env.type === 'simple') { - const envManager = new SimpleEnvManager(onMeshUpdatedCallback); - await envManager.load(this.roomState.env.options, this.scene); - this.envManager = envManager; + envManager = new SimpleEnvManager(onMeshUpdatedCallback); } else if (this.roomState.env.type === 'japanese') { // TODO } else if (this.roomState.env.type === 'museum') { - const envManager = new MuseumEnvManager(onMeshUpdatedCallback); - await envManager.load(this.roomState.env.options, this.scene); - this.envManager = envManager; + envManager = new MuseumEnvManager(onMeshUpdatedCallback); } + await envManager.load(this.roomState.env.options, this.scene); + envManager.setTime(this.time); + + for (const mat of this.scene.materials) { + mat.unfreeze(); + if (mat instanceof BABYLON.MultiMaterial) { + for (const subMat of mat.subMaterials) { + if (subMat.metadata.useEnvMapAsObjectMaterial) subMat.reflectionTexture = envManager.envMapIndoor; + } + } else { + if (mat.metadata?.useEnvMapAsObjectMaterial) mat.reflectionTexture = envManager.envMapIndoor; + } + } + + if (this.envManager != null) { + this.envManager.dispose(); + } + + this.envManager = envManager; + + this.camera.maxZ = this.envManager.maxCameraZ; + + this.resumeRender(); + this.sr.enableSnapshotRendering(); + if (!forInit) { this.ev('changeRoomState', { roomState: this.roomState }); } @@ -1118,6 +1122,7 @@ export class RoomEngine extends EventEmitter { (subMat as BABYLON.PBRMaterial).transparencyMode = BABYLON.PBRMaterial.PBRMATERIAL_ALPHABLEND; } (subMat as BABYLON.PBRMaterial).reflectionTexture = this.envManager?.envMapIndoor; + (subMat as BABYLON.PBRMaterial).metadata.useEnvMapAsObjectMaterial = true; (subMat 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 (subMat as BABYLON.PBRMaterial).anisotropy.isEnabled = false; // なんかきれいにレンダリングされないため } @@ -1127,6 +1132,7 @@ export class RoomEngine extends EventEmitter { (mesh.material as BABYLON.PBRMaterial).transparencyMode = BABYLON.PBRMaterial.PBRMATERIAL_ALPHABLEND; } (mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.envManager?.envMapIndoor; + (mesh.material as BABYLON.PBRMaterial).metadata.useEnvMapAsObjectMaterial = true; (mesh.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 (mesh.material as BABYLON.PBRMaterial).anisotropy.isEnabled = false; // なんかきれいにレンダリングされないため } diff --git a/packages/frontend/src/world/room/env.ts b/packages/frontend/src/world/room/env.ts index 099c054a25..d7979e2c56 100644 --- a/packages/frontend/src/world/room/env.ts +++ b/packages/frontend/src/world/room/env.ts @@ -17,7 +17,8 @@ import { findMaterial } from './utility.js'; export abstract class EnvManager { protected onMeshUpdatedCallback: ((meshes: BABYLON.AbstractMesh[]) => void) | null = null; - abstract envMapIndoor: BABYLON.CubeTexture | null; + public abstract envMapIndoor: BABYLON.CubeTexture | null; + public abstract maxCameraZ: number; constructor(onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null) { this.onMeshUpdatedCallback = onMeshUpdatedCallback ?? null; @@ -25,6 +26,7 @@ export abstract class EnvManager { abstract load(options: T, scene: BABYLON.Scene): Promise; abstract applyOptions(options: T): void; + abstract setTime(time: number): void; abstract dispose(): void; } @@ -70,13 +72,23 @@ export class SimpleEnvManager extends EnvManager { private pillarMaterials: Record<'nw' | 'ne' | 'sw' | 'se', BABYLON.PBRMaterial> | null = null; private ceilingMaterial: BABYLON.PBRMaterial | null = null; private floorMaterial: BABYLON.PBRMaterial | null = null; + private skybox: BABYLON.Mesh | null = null; + private skyboxMat: BABYLON.StandardMaterial | null = null; public envMapIndoor: BABYLON.CubeTexture | null = null; + public maxCameraZ = cm(1000); constructor(onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null) { super(onMeshUpdatedCallback); } public async load(options: SimpleEnvOptions, scene: BABYLON.Scene) { + this.skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(1000) }, scene); + this.skyboxMat = new BABYLON.StandardMaterial('skyboxMat', scene); + this.skyboxMat.backFaceCulling = false; + this.skyboxMat.disableLighting = true; + this.skybox.material = this.skyboxMat; + this.skybox.infiniteDistance = true; + this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/envs/default/300.glb', scene); this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', scene); @@ -173,6 +185,16 @@ export class SimpleEnvManager extends EnvManager { await this.applyOptions(options); } + public setTime(time: number) { + if (time === 0) { + this.skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.9, 1.0); + } else if (time === 1) { + this.skyboxMat.emissiveColor = new BABYLON.Color3(0.8, 0.5, 0.3); + } else { + this.skyboxMat.emissiveColor = new BABYLON.Color3(0.05, 0.05, 0.2); + } + } + public applyOptions(options: SimpleEnvOptions) { // TODO: 返り値をpromiseにしてちゃんとテクスチャが読み終わってからresolveする @@ -337,6 +359,9 @@ export class SimpleEnvManager extends EnvManager { for (const m of Object.values(this.pillarMaterials ?? {})) { m.dispose(); } + this.skybox?.dispose(); + this.skyboxMat?.dispose(); + this.envMapIndoor?.dispose(); } } @@ -345,6 +370,8 @@ export type MuseumEnvOptions = any; export class MuseumEnvManager extends EnvManager { private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null; private meshes: BABYLON.Mesh[] = []; + public envMapIndoor: BABYLON.CubeTexture | null = null; + public maxCameraZ = cm(5000); constructor(onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null) { super(onMeshUpdatedCallback); @@ -353,6 +380,9 @@ export class MuseumEnvManager extends EnvManager { public async load(options: MuseumEnvOptions, scene: BABYLON.Scene) { this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/envs/museum/museum.glb', scene); + this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', scene); + this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(500), cm(500), cm(500)); + this.meshes = this.loaderResult.meshes.filter(m => m instanceof BABYLON.Mesh); this.meshes[0].scaling = this.meshes[0].scaling.scale(WORLD_SCALE); this.meshes[0].rotationQuaternion = null; @@ -381,6 +411,9 @@ export class MuseumEnvManager extends EnvManager { await this.applyOptions(options); } + public setTime(time: number) { + } + public applyOptions(options: MuseumEnvOptions) { this.onMeshUpdatedCallback?.(this.meshes); } @@ -397,5 +430,6 @@ export class MuseumEnvManager extends EnvManager { for (const m of this.meshes) { m.dispose(false, true); } + this.envMapIndoor?.dispose(); } }