/* eslint-disable id-denylist */ /* * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import * as BABYLON from '@babylonjs/core'; import { cm, WORLD_SCALE } from '../utility.js'; import { findMaterial, GRAPHICS_QUALITY, SYSTEM_HEYA_MESH_NAMES } from './utility.js'; import type { RoomEngine } from './engine.js'; //export interface EnvManager { // constructor(onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null): void; // load: (options: T, scene: BABYLON.Scene) => Promise; // applyOptions: (options: T) => void; // dispose: () => void; //} export abstract class EnvManager { protected engine: RoomEngine; protected onMeshUpdatedCallback: ((meshes: BABYLON.AbstractMesh[]) => void) | null = null; public abstract envMapIndoor: BABYLON.CubeTexture | null; public abstract maxCameraZ: number; protected shadowGenerators: BABYLON.ShadowGenerator[] = []; constructor(engine: RoomEngine, onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null) { this.engine = engine; this.onMeshUpdatedCallback = onMeshUpdatedCallback ?? null; } abstract load(options: T, scene: BABYLON.Scene, engine: RoomEngine): Promise; abstract applyOptions(options: T): void; abstract setTime(time: number): void; abstract updateRoomLightColor(color: BABYLON.Color3): void; abstract turnOnRoomLight(): void; abstract turnOffRoomLight(): void; public addShadowCaster(mesh: BABYLON.AbstractMesh) { for (const shadowGen of this.shadowGenerators) { shadowGen.addShadowCaster(mesh); } } public removeShadowCaster(mesh: BABYLON.AbstractMesh) { for (const shadowGen of this.shadowGenerators) { shadowGen.removeShadowCaster(mesh); } } public dispose() { for (const shadowGen of this.shadowGenerators) { shadowGen.dispose(); } } } export type SimpleEnvOptions = { dimension: [number, number]; window: 'none' | 'kosidakamado' | 'demado' | 'hakidasimado'; walls: Record<'n' | 's' | 'w' | 'e', { material: null | 'wood' | 'concrete'; color: [number, number, number]; withBeam: boolean; beamMaterial: null | 'wood' | 'concrete'; beamColor: [number, number, number]; withBaseboard: boolean; }>; pillars: Record<'nw' | 'ne' | 'sw' | 'se', { material: null | 'wood' | 'concrete'; color: [number, number, number]; show: boolean; }>; flooring: { material: null | 'wood' | 'concrete'; color: [number, number, number]; }; ceiling: { material: null | 'wood' | 'concrete'; color: [number, number, number]; }; }; // TODO: マテリアルは必要になるまで作成しないようにする export class SimpleEnvManager extends EnvManager { private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null; private meshes: BABYLON.Mesh[] = []; private wallRoots: Record<'n' | 's' | 'w' | 'e', BABYLON.TransformNode> = null as any; private wallMaterials: Record<'n' | 's' | 'w' | 'e', BABYLON.PBRMaterial> | null = null; private wallBeamMaterials: Record<'n' | 's' | 'w' | 'e', BABYLON.PBRMaterial> | null = null; private pillarRoots: Record<'nw' | 'ne' | 'sw' | 'se', BABYLON.TransformNode> | null = null; 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; private roomLight: BABYLON.SpotLight | null = null; private sunLight: BABYLON.DirectionalLight | null = null; public envMapIndoor: BABYLON.CubeTexture | null = null; public maxCameraZ = cm(1000); constructor(engine: RoomEngine, onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null) { super(engine, onMeshUpdatedCallback); } public async load(options: SimpleEnvOptions) { this.skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(1000) }, this.engine.scene); this.skyboxMat = new BABYLON.StandardMaterial('skyboxMat', this.engine.scene); this.skyboxMat.backFaceCulling = false; this.skyboxMat.disableLighting = true; this.skybox.material = this.skyboxMat; this.skybox.infiniteDistance = true; this.roomLight = new BABYLON.SpotLight('simpleEnv:RoomLight', new BABYLON.Vector3(0, cm(249), 0), new BABYLON.Vector3(0, -1, 0), 16, 8, this.engine.scene); this.roomLight.diffuse = new BABYLON.Color3(...this.engine.roomState.roomLightColor); this.roomLight.shadowMinZ = cm(10); this.roomLight.shadowMaxZ = cm(300); this.roomLight.radius = cm(30); if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) { const shadowGeneratorForRoomLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.roomLight); shadowGeneratorForRoomLight.forceBackFacesOnly = true; shadowGeneratorForRoomLight.bias = 0.0005; shadowGeneratorForRoomLight.usePercentageCloserFiltering = true; shadowGeneratorForRoomLight.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH; if (this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM) { shadowGeneratorForRoomLight.getShadowMap().refreshRate = 60; } //shadowGeneratorForRoomLight.useContactHardeningShadow = true; //shadowGeneratorForRoomLight.contactHardeningLightSizeUVRatio = 0.01; this.shadowGenerators.push(shadowGeneratorForRoomLight); } if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) { this.sunLight = new BABYLON.DirectionalLight('simpleEnv:SunLight', new BABYLON.Vector3(0.2, -1, -1), this.engine.scene); this.sunLight.position = new BABYLON.Vector3(cm(-20), cm(1000), cm(1000)); this.sunLight.shadowMinZ = cm(1000); this.sunLight.shadowMaxZ = cm(2000); const shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.sunLight); shadowGeneratorForSunLight.forceBackFacesOnly = true; shadowGeneratorForSunLight.bias = 0.00001; shadowGeneratorForSunLight.usePercentageCloserFiltering = true; shadowGeneratorForSunLight.usePoissonSampling = true; if (this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM) { shadowGeneratorForSunLight.getShadowMap().refreshRate = 60; } this.shadowGenerators.push(shadowGeneratorForSunLight); } this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/envs/default/300.glb', this.engine.scene); this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.engine.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; this.meshes[0].rotation = new BABYLON.Vector3(0, 0, 0); // instanced mesh を通常の mesh に変換 (そうしないとマテリアルが共有される) for (const mesh of this.loaderResult.meshes) { if (mesh instanceof BABYLON.InstancedMesh) { const realizedMesh = mesh.sourceMesh.clone(mesh.name, null, true); realizedMesh.position = mesh.position.clone(); if (mesh.rotationQuaternion) { realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone(); } else { realizedMesh.rotation = mesh.rotation.clone(); } realizedMesh.scaling = mesh.scaling.clone(); realizedMesh.parent = mesh.parent; mesh.dispose(); this.engine.scene.removeMesh(mesh); this.meshes.push(realizedMesh); } } this.wallRoots = { n: this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_N__'))!, s: this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_S__'))!, w: this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_W__'))!, e: this.loaderResult.transformNodes.find(t => t.name.includes('__WALL_E__'))!, }; this.pillarRoots = { nw: this.loaderResult.transformNodes.find(t => t.name.includes('__PILLAR_NW__'))!, ne: this.loaderResult.transformNodes.find(t => t.name.includes('__PILLAR_NE__'))!, sw: this.loaderResult.transformNodes.find(t => t.name.includes('__PILLAR_SW__'))!, se: this.loaderResult.transformNodes.find(t => t.name.includes('__PILLAR_SE__'))!, }; const wallMaterial = findMaterial(this.meshes[0], '__WALL__'); //wallMaterial.metadata.disableEnvMap = true; this.wallMaterials = { n: wallMaterial.clone('wallNMaterial'), s: wallMaterial.clone('wallSMaterial'), w: wallMaterial.clone('wallWMaterial'), e: wallMaterial.clone('wallEMaterial'), }; const beamMaterial = findMaterial(this.meshes[0], '__BEAM__'); //beamMaterial.metadata.disableEnvMap = true; this.wallBeamMaterials = { n: beamMaterial.clone('wallNBeamMaterial'), s: beamMaterial.clone('wallSBeamMaterial'), w: beamMaterial.clone('wallWBeamMaterial'), e: beamMaterial.clone('wallEBeamMaterial'), }; const pillarMaterial = findMaterial(this.meshes[0], '__PILLAR__'); //pillarMaterial.metadata.disableEnvMap = true; this.pillarMaterials = { nw: pillarMaterial.clone('pillarNWMaterial'), ne: pillarMaterial.clone('pillarNEMaterial'), sw: pillarMaterial.clone('pillarSWMaterial'), se: pillarMaterial.clone('pillarSEMaterial'), }; for (const [k, v] of Object.entries(this.wallRoots)) { for (const m of v.getChildMeshes().filter(m => m.material === wallMaterial)) { m.material = this.wallMaterials[k]; } for (const m of v.getChildMeshes().filter(m => m.material === beamMaterial)) { m.material = this.wallBeamMaterials[k]; } } for (const [k, v] of Object.entries(this.pillarRoots)) { for (const m of v.getChildMeshes().filter(m => m.material === pillarMaterial)) { m.material = this.pillarMaterials[k]; } } this.ceilingMaterial = findMaterial(this.meshes[0], '__CEILING__'); //this.ceilingMaterial.metadata.disableEnvMap = true; this.floorMaterial = findMaterial(this.meshes[0], '__FLOOR__'); //this.floorMaterial.metadata.disableEnvMap = true; const baseboardMaterial = findMaterial(this.meshes[0], '__BASEBOARD__'); //baseboardMaterial.metadata.disableEnvMap = true; for (const mesh of this.meshes) { if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) continue; mesh.receiveShadows = true; if (mesh.material !== this.floorMaterial) { // 床は他の何にも影を落とさないことが確定している this.addShadowCaster(mesh); } const mat = mesh.material; if (mat instanceof BABYLON.MultiMaterial) { for (const subMat of mat.subMaterials) { subMat.reflectionTexture = this.envMapIndoor; } } else if (mat instanceof BABYLON.PBRMaterial) { mat.reflectionTexture = this.envMapIndoor; } } await this.applyOptions(options); } public setTime(time: number) { if (this.skyboxMat == null) return; 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); } if (this.sunLight != null) { this.sunLight.diffuse = time === 0 ? new BABYLON.Color3(1.0, 0.9, 0.8) : time === 1 ? new BABYLON.Color3(1.0, 0.8, 0.6) : new BABYLON.Color3(0.6, 0.8, 1.0); this.sunLight.intensity = time === 0 ? 3 : time === 1 ? 1 : 0.25; } } public updateRoomLightColor(color: BABYLON.Color3): void { if (this.roomLight == null) return; this.roomLight.diffuse = color; } public turnOnRoomLight(): void { if (this.roomLight == null) return; this.roomLight.intensity = 18 * WORLD_SCALE * WORLD_SCALE; if (this.envMapIndoor != null) this.envMapIndoor.level = 0.6; for (const m of this.engine.scene.materials) { if (m.metadata?.disableEnvMap) { m.ambientColor = new BABYLON.Color3(0.5, 0.5, 0.5); } } } public turnOffRoomLight(): void { if (this.roomLight == null) return; this.roomLight.intensity = 0; if (this.envMapIndoor != null) this.envMapIndoor.level = 0.025; for (const m of this.engine.scene.materials) { if (m.metadata?.disableEnvMap) { m.ambientColor = new BABYLON.Color3(0.025, 0.025, 0.025); } } } public applyOptions(options: SimpleEnvOptions) { // TODO: 返り値をpromiseにしてちゃんとテクスチャが読み終わってからresolveする for (const type of ['n', 's', 'w', 'e'] as const) { const wallRoot = this.wallRoots[type]; const wallOptions = options.walls[type]; for (const mesh of wallRoot.getChildMeshes()) { if (mesh.name.includes('__BEAM__')) { mesh.setEnabled(wallOptions.withBeam); } else if (mesh.name.includes('__BASEBOARD__')) { mesh.setEnabled(wallOptions.withBaseboard); } } { const targetMaterial = this.wallMaterials[type]; targetMaterial.unfreeze(); targetMaterial.albedoColor = new BABYLON.Color3(...wallOptions.color); const texPath = wallOptions.material === 'wood' ? '/client-assets/room/textures/wall-wood2.png' : wallOptions.material === 'concrete' ? '/client-assets/room/textures/concrete1.png' : null; if (texPath != null) { const tex = new BABYLON.Texture(texPath, this.meshes[0].getScene(), false, false); targetMaterial.albedoTexture = tex; } else { targetMaterial.albedoTexture = null; } targetMaterial.freeze(); } { const targetMaterial = this.wallBeamMaterials[type]; targetMaterial.unfreeze(); targetMaterial.albedoColor = new BABYLON.Color3(...wallOptions.beamColor); const texPath = wallOptions.beamMaterial === 'wood' ? '/client-assets/room/textures/wall-wood2.png' : wallOptions.beamMaterial === 'concrete' ? '/client-assets/room/textures/concrete1.png' : null; if (texPath != null) { const tex = new BABYLON.Texture(texPath, this.meshes[0].getScene(), false, false); targetMaterial.albedoTexture = tex; } else { targetMaterial.albedoTexture = null; } targetMaterial.freeze(); } } for (const type of ['nw', 'ne', 'sw', 'se'] as const) { const pillarRoot = this.pillarRoots[type]; const pillarOptions = options.pillars[type]; let isEnabled = pillarOptions.show; if (!isEnabled) { // 梁同士が直交することは許さない(z-fightingが発生する)ので柱を強制追加 if (type === 'nw') { isEnabled = options.walls.n.withBeam && options.walls.w.withBeam; } else if (type === 'ne') { isEnabled = options.walls.n.withBeam && options.walls.e.withBeam; } else if (type === 'sw') { isEnabled = options.walls.s.withBeam && options.walls.w.withBeam; } else if (type === 'se') { isEnabled = options.walls.s.withBeam && options.walls.e.withBeam; } } pillarRoot.setEnabled(isEnabled); const targetMaterial = this.pillarMaterials[type]; targetMaterial.unfreeze(); targetMaterial.albedoColor = new BABYLON.Color3(...pillarOptions.color); const texPath = pillarOptions.material === 'wood' ? '/client-assets/room/textures/wall-wood2.png' : pillarOptions.material === 'concrete' ? '/client-assets/room/textures/concrete1.png' : null; if (texPath != null) { const tex = new BABYLON.Texture(texPath, this.meshes[0].getScene(), false, false); targetMaterial.albedoTexture = tex; } else { targetMaterial.albedoTexture = null; } targetMaterial.freeze(); } { this.ceilingMaterial.unfreeze(); this.ceilingMaterial.albedoColor = new BABYLON.Color3(...options.ceiling.color); const texPath = options.ceiling.material === 'wood' ? '/client-assets/room/textures/ceiling-wood.png' : options.ceiling.material === 'concrete' ? '/client-assets/room/textures/concrete3.png' : null; if (texPath != null) { const tex = new BABYLON.Texture(texPath, this.meshes[0].getScene(), false, false); this.ceilingMaterial.albedoTexture = tex; } else { this.ceilingMaterial.albedoTexture = null; } this.ceilingMaterial.freeze(); } { this.floorMaterial.unfreeze(); this.floorMaterial.albedoColor = new BABYLON.Color3(...options.flooring.color); const texPath = options.flooring.material === 'wood' ? '/client-assets/room/textures/flooring-wood.png' : options.flooring.material === 'concrete' ? '/client-assets/room/textures/concrete3.png' : null; if (texPath != null) { const tex = new BABYLON.Texture(texPath, this.meshes[0].getScene(), false, false); this.floorMaterial.albedoTexture = tex; } else { this.floorMaterial.albedoTexture = null; } this.floorMaterial.freeze(); } this.onMeshUpdatedCallback?.(this.meshes); } public dispose() { for (const m of this.meshes) { m.dispose(false, true); } for (const m of Object.values(this.wallMaterials ?? {})) { m.dispose(); } for (const m of Object.values(this.wallBeamMaterials ?? {})) { m.dispose(); } for (const m of Object.values(this.pillarMaterials ?? {})) { m.dispose(); } this.skybox?.dispose(); this.skyboxMat?.dispose(); this.envMapIndoor?.dispose(); this.roomLight?.dispose(); this.sunLight?.dispose(); if (this.loaderResult != null) { for (const m of this.loaderResult.meshes) { m.dispose(false, true); } for (const t of this.loaderResult.transformNodes) { t.dispose(false, true); } } super.dispose(); } } export type JapaneseEnvOptions = { window: 'none' | 'kosidakamado' | 'demado' | 'hakidasimado'; }; export class JapaneseEnvManager extends EnvManager { private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null; private meshes: BABYLON.Mesh[] = []; private skybox: BABYLON.Mesh | null = null; private skyboxMat: BABYLON.StandardMaterial | null = null; private roomLight: BABYLON.SpotLight | null = null; private sunLight: BABYLON.DirectionalLight | null = null; public envMapIndoor: BABYLON.CubeTexture | null = null; public maxCameraZ = cm(1000); constructor(engine: RoomEngine, onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null) { super(engine, onMeshUpdatedCallback); } public async load(options: JapaneseEnvOptions) { this.skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(1000) }, this.engine.scene); this.skyboxMat = new BABYLON.StandardMaterial('skyboxMat', this.engine.scene); this.skyboxMat.backFaceCulling = false; this.skyboxMat.disableLighting = true; this.skybox.material = this.skyboxMat; this.skybox.infiniteDistance = true; this.roomLight = new BABYLON.SpotLight('simpleEnv:RoomLight', new BABYLON.Vector3(0, cm(249), 0), new BABYLON.Vector3(0, -1, 0), 16, 8, this.engine.scene); this.roomLight.diffuse = new BABYLON.Color3(...this.engine.roomState.roomLightColor); this.roomLight.shadowMinZ = cm(10); this.roomLight.shadowMaxZ = cm(300); this.roomLight.radius = cm(30); if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) { const shadowGeneratorForRoomLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.roomLight); shadowGeneratorForRoomLight.forceBackFacesOnly = true; shadowGeneratorForRoomLight.bias = 0.0005; shadowGeneratorForRoomLight.usePercentageCloserFiltering = true; shadowGeneratorForRoomLight.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH; if (this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM) { shadowGeneratorForRoomLight.getShadowMap().refreshRate = 60; } //shadowGeneratorForRoomLight.useContactHardeningShadow = true; //shadowGeneratorForRoomLight.contactHardeningLightSizeUVRatio = 0.01; this.shadowGenerators.push(shadowGeneratorForRoomLight); } if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) { this.sunLight = new BABYLON.DirectionalLight('simpleEnv:SunLight', new BABYLON.Vector3(0.2, -1, -1), this.engine.scene); this.sunLight.position = new BABYLON.Vector3(cm(-20), cm(1000), cm(1000)); this.sunLight.shadowMinZ = cm(1000); this.sunLight.shadowMaxZ = cm(2000); const shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.sunLight); shadowGeneratorForSunLight.forceBackFacesOnly = true; shadowGeneratorForSunLight.bias = 0.00001; shadowGeneratorForSunLight.usePercentageCloserFiltering = true; shadowGeneratorForSunLight.usePoissonSampling = true; if (this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM) { shadowGeneratorForSunLight.getShadowMap().refreshRate = 60; } this.shadowGenerators.push(shadowGeneratorForSunLight); } this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/envs/japanese/japanese.glb', this.engine.scene); this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.engine.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; this.meshes[0].rotation = new BABYLON.Vector3(0, 0, 0); // instanced mesh を通常の mesh に変換 (そうしないとマテリアルが共有される) for (const mesh of this.loaderResult.meshes) { if (mesh instanceof BABYLON.InstancedMesh) { const realizedMesh = mesh.sourceMesh.clone(mesh.name, null, true); realizedMesh.position = mesh.position.clone(); if (mesh.rotationQuaternion) { realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone(); } else { realizedMesh.rotation = mesh.rotation.clone(); } realizedMesh.scaling = mesh.scaling.clone(); realizedMesh.parent = mesh.parent; mesh.dispose(); this.engine.scene.removeMesh(mesh); this.meshes.push(realizedMesh); } } for (const mesh of this.meshes) { if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) continue; mesh.receiveShadows = true; this.addShadowCaster(mesh); const mat = mesh.material; if (mat instanceof BABYLON.MultiMaterial) { for (const subMat of mat.subMaterials) { subMat.reflectionTexture = this.envMapIndoor; } } else if (mat instanceof BABYLON.PBRMaterial) { mat.reflectionTexture = this.envMapIndoor; } } await this.applyOptions(options); } public setTime(time: number) { if (this.skyboxMat == null) return; 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); } if (this.sunLight != null) { this.sunLight.diffuse = time === 0 ? new BABYLON.Color3(1.0, 0.9, 0.8) : time === 1 ? new BABYLON.Color3(1.0, 0.8, 0.6) : new BABYLON.Color3(0.6, 0.8, 1.0); this.sunLight.intensity = time === 0 ? 3 : time === 1 ? 1 : 0.25; } } public updateRoomLightColor(color: BABYLON.Color3): void { if (this.roomLight == null) return; this.roomLight.diffuse = color; } public turnOnRoomLight(): void { if (this.roomLight == null) return; this.roomLight.intensity = 18 * WORLD_SCALE * WORLD_SCALE; if (this.envMapIndoor != null) this.envMapIndoor.level = 0.6; for (const m of this.engine.scene.materials) { if (m.metadata?.disableEnvMap) { m.ambientColor = new BABYLON.Color3(0.5, 0.5, 0.5); } } } public turnOffRoomLight(): void { if (this.roomLight == null) return; this.roomLight.intensity = 0; if (this.envMapIndoor != null) this.envMapIndoor.level = 0.025; for (const m of this.engine.scene.materials) { if (m.metadata?.disableEnvMap) { m.ambientColor = new BABYLON.Color3(0.025, 0.025, 0.025); } } } public applyOptions(options: SimpleEnvOptions) { this.onMeshUpdatedCallback?.(this.meshes); } public dispose() { for (const m of this.meshes) { m.dispose(false, true); } this.skybox?.dispose(); this.skyboxMat?.dispose(); this.envMapIndoor?.dispose(); this.roomLight?.dispose(); this.sunLight?.dispose(); if (this.loaderResult != null) { for (const m of this.loaderResult.meshes) { m.dispose(false, true); } for (const t of this.loaderResult.transformNodes) { t.dispose(false, true); } } super.dispose(); } } export type MuseumEnvOptions = any; export class MuseumEnvManager extends EnvManager { private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null; private meshes: BABYLON.Mesh[] = []; private roomLight: BABYLON.DirectionalLight | null = null; private subRoomLights: BABYLON.SpotLight[] = []; public envMapIndoor: BABYLON.CubeTexture | null = null; public maxCameraZ = cm(3000); constructor(engine: RoomEngine, onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null) { super(engine, onMeshUpdatedCallback); } public async load(options: MuseumEnvOptions) { this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/envs/museum/museum.glb', this.engine.scene); this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.engine.scene); this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(2000), cm(500), cm(2000)); 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; this.meshes[0].rotation = new BABYLON.Vector3(0, 0, 0); // instanced mesh を通常の mesh に変換 (そうしないとマテリアルが共有される) for (const mesh of this.loaderResult.meshes) { if (mesh instanceof BABYLON.InstancedMesh) { const realizedMesh = mesh.sourceMesh.clone(mesh.name, null, true); realizedMesh.position = mesh.position.clone(); if (mesh.rotationQuaternion) { realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone(); } else { realizedMesh.rotation = mesh.rotation.clone(); } realizedMesh.scaling = mesh.scaling.clone(); realizedMesh.parent = mesh.parent; mesh.dispose(); this.engine.scene.removeMesh(mesh); this.meshes.push(realizedMesh); } } this.roomLight = new BABYLON.DirectionalLight('museumEnv:RoomLight', new BABYLON.Vector3(0, -1, 0), this.engine.scene); this.roomLight.position = new BABYLON.Vector3(0, cm(300), 0); this.roomLight.diffuse = new BABYLON.Color3(...this.engine.roomState.roomLightColor); this.roomLight.shadowMinZ = cm(10); this.roomLight.shadowMaxZ = cm(500); this.roomLight.radius = cm(30); if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) { const shadowGeneratorForRoomLight = new BABYLON.ShadowGenerator(this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM ? 1024 : 2048, this.roomLight); shadowGeneratorForRoomLight.forceBackFacesOnly = true; shadowGeneratorForRoomLight.bias = 0.00001; shadowGeneratorForRoomLight.normalBias = 0.005; shadowGeneratorForRoomLight.usePercentageCloserFiltering = true; shadowGeneratorForRoomLight.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH; if (this.engine.graphicsQuality <= GRAPHICS_QUALITY.MEDIUM) { shadowGeneratorForRoomLight.getShadowMap().refreshRate = 60; } //this.shadowGeneratorForRoomLight.useContactHardeningShadow = true; this.shadowGenerators.push(shadowGeneratorForRoomLight); } for (const node of this.loaderResult.transformNodes.filter(node => node.name.includes('__LIGHT__'))) { const light = new BABYLON.SpotLight('museumEnv:SubRoomLight', node.position, new BABYLON.Vector3(0, -1, 0), 16, 8, this.engine.scene, true); light.diffuse = new BABYLON.Color3(...this.engine.roomState.roomLightColor); light.range = cm(500); light.radius = cm(15); light.parent = this.meshes[0]; this.engine.lightContainer.addLight(light); this.subRoomLights.push(light); } for (const mesh of this.meshes) { if (SYSTEM_HEYA_MESH_NAMES.some(name => mesh.name.includes(name))) continue; mesh.receiveShadows = true; //this.addShadowCaster(mesh); const mat = mesh.material; if (mat instanceof BABYLON.MultiMaterial) { for (const subMat of mat.subMaterials) { subMat.reflectionTexture = this.envMapIndoor; } } else if (mat instanceof BABYLON.PBRMaterial) { mat.reflectionTexture = this.envMapIndoor; } } await this.applyOptions(options); } public setTime(time: number) { } public updateRoomLightColor(color: BABYLON.Color3): void { if (this.roomLight == null) return; this.roomLight.diffuse = color; for (const subLight of this.subRoomLights) { subLight.diffuse = color; } } public turnOnRoomLight(): void { if (this.roomLight == null) return; this.roomLight.intensity = 0.00005 * WORLD_SCALE * WORLD_SCALE; for (const subLight of this.subRoomLights) { subLight.intensity = 20 * WORLD_SCALE * WORLD_SCALE; } if (this.envMapIndoor != null) this.envMapIndoor.level = 0.2; for (const m of this.engine.scene.materials) { if (m.metadata?.disableEnvMap) { m.ambientColor = new BABYLON.Color3(0.5, 0.5, 0.5); } } } public turnOffRoomLight(): void { if (this.roomLight == null) return; this.roomLight.intensity = 0; for (const subLight of this.subRoomLights) { subLight.intensity = 0; } if (this.envMapIndoor != null) this.envMapIndoor.level = 0.025; for (const m of this.engine.scene.materials) { if (m.metadata?.disableEnvMap) { m.ambientColor = new BABYLON.Color3(0.025, 0.025, 0.025); } } } public applyOptions(options: MuseumEnvOptions) { this.onMeshUpdatedCallback?.(this.meshes); } public dispose() { this.envMapIndoor?.dispose(); this.roomLight?.dispose(); for (const subLight of this.subRoomLights) { subLight.dispose(); } if (this.loaderResult != null) { for (const m of this.loaderResult.meshes) { m.dispose(false, true); } for (const t of this.loaderResult.transformNodes) { t.dispose(false, true); } } for (const m of this.meshes) { m.dispose(false, true); } super.dispose(); } }