diff --git a/packages/frontend-misskey-world-engine/src/room/engine.ts b/packages/frontend-misskey-world-engine/src/room/engine.ts index 20038f5bd5..0277da8bba 100644 --- a/packages/frontend-misskey-world-engine/src/room/engine.ts +++ b/packages/frontend-misskey-world-engine/src/room/engine.ts @@ -543,35 +543,14 @@ export class RoomEngine extends EngineBase<{ this.pauseRender(); } - 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; - if (m.name.includes('__COLLISION__')) { - m.checkCollisions = true; - } - continue; - } - - m.isPickable = false; - m.checkCollisions = false; - if (m.material != null) { - (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 - } - } - }; - let envManager: EnvManager; if (this.roomState.env.type === 'simple') { - envManager = new SimpleEnvManager(this, onMeshUpdatedCallback); + envManager = new SimpleEnvManager(this); } else if (this.roomState.env.type === 'japanese') { - envManager = new JapaneseEnvManager(this, onMeshUpdatedCallback); + envManager = new JapaneseEnvManager(this); } else if (this.roomState.env.type === 'museum') { - envManager = new MuseumEnvManager(this, onMeshUpdatedCallback); + envManager = new MuseumEnvManager(this); } await envManager.load(this.roomState.env.options); diff --git a/packages/frontend-misskey-world-engine/src/room/env.ts b/packages/frontend-misskey-world-engine/src/room/env.ts index 716b31d3f8..1cc340f187 100644 --- a/packages/frontend-misskey-world-engine/src/room/env.ts +++ b/packages/frontend-misskey-world-engine/src/room/env.ts @@ -9,25 +9,44 @@ import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js'; import { findMaterial, GRAPHICS_QUALITY } from '../utility.js'; import { SYSTEM_HEYA_MESH_NAMES } from './utility.js'; import type { RoomEngine } from './engine.js'; -import type { SimpleEnvOptions, JapaneseEnvOptions, MuseumEnvOptions } from 'misskey-world/src/room/type.js'; +import type { SimpleEnvOptions, JapaneseEnvOptions, MuseumEnvOptions, CustomMadoriEnvOptions } from 'misskey-world/src/room/type.js'; //export interface EnvManager { -// constructor(onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null): void; +// constructor(registerMeshes?: ((meshes: BABYLON.AbstractMesh[]) => void) | null): void; // load: (options: T, scene: BABYLON.Scene) => Promise; // applyOptions: (options: T) => void; // dispose: () => void; //} +const registerMeshes = (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; + if (m.name.includes('__COLLISION__')) { + m.checkCollisions = true; + } + continue; + } + + m.isPickable = false; + m.checkCollisions = false; + if (m.material != null) { + (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 + } + } +}; + 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) { + constructor(engine: RoomEngine) { this.engine = engine; - this.onMeshUpdatedCallback = onMeshUpdatedCallback ?? null; } abstract load(options: T, scene: BABYLON.Scene, engine: RoomEngine): Promise; @@ -75,8 +94,8 @@ export class SimpleEnvManager extends EnvManager { public envMapIndoor: BABYLON.CubeTexture | null = null; public maxCameraZ = cm(1000); - constructor(engine: RoomEngine, onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null) { - super(engine, onMeshUpdatedCallback); + constructor(engine: RoomEngine) { + super(engine); } public async load(options: SimpleEnvOptions) { @@ -87,7 +106,7 @@ export class SimpleEnvManager extends EnvManager { 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 = new BABYLON.SpotLight('env: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); @@ -108,7 +127,7 @@ export class SimpleEnvManager extends EnvManager { } 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 = new BABYLON.DirectionalLight('env: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); @@ -412,7 +431,7 @@ export class SimpleEnvManager extends EnvManager { this.floorMaterial.freeze(); } - this.onMeshUpdatedCallback?.(this.meshes); + registerMeshes(this.meshes); } public dispose() { @@ -455,8 +474,8 @@ export class JapaneseEnvManager extends EnvManager { public envMapIndoor: BABYLON.CubeTexture | null = null; public maxCameraZ = cm(1000); - constructor(engine: RoomEngine, onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null) { - super(engine, onMeshUpdatedCallback); + constructor(engine: RoomEngine) { + super(engine); } public async load(options: JapaneseEnvOptions) { @@ -467,7 +486,7 @@ export class JapaneseEnvManager extends EnvManager { 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 = new BABYLON.SpotLight('env: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); @@ -488,7 +507,7 @@ export class JapaneseEnvManager extends EnvManager { } 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 = new BABYLON.DirectionalLight('env: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); @@ -598,7 +617,7 @@ export class JapaneseEnvManager extends EnvManager { } public applyOptions(options: SimpleEnvOptions) { - this.onMeshUpdatedCallback?.(this.meshes); + registerMeshes(this.meshes); } public dispose() { @@ -630,8 +649,8 @@ export class MuseumEnvManager extends EnvManager { public envMapIndoor: BABYLON.CubeTexture | null = null; public maxCameraZ = cm(3000); - constructor(engine: RoomEngine, onMeshUpdatedCallback?: ((meshes: BABYLON.AbstractMesh[]) => void) | null) { - super(engine, onMeshUpdatedCallback); + constructor(engine: RoomEngine) { + super(engine); } public async load(options: MuseumEnvOptions) { @@ -665,7 +684,7 @@ export class MuseumEnvManager extends EnvManager { } } - this.roomLight = new BABYLON.DirectionalLight('museumEnv:RoomLight', new BABYLON.Vector3(0, -1, 0), this.engine.scene); + this.roomLight = new BABYLON.DirectionalLight('env: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); @@ -687,7 +706,7 @@ export class MuseumEnvManager extends EnvManager { } for (const node of this.meshes.filter(mesh => mesh.name.includes('__LIGHT__'))) { - const light = new BABYLON.SpotLight('museumEnv:SubRoomLight', node.position, new BABYLON.Vector3(0, -1, 0), 16, 8, this.engine.scene, true); + const light = new BABYLON.SpotLight('env: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); @@ -754,7 +773,7 @@ export class MuseumEnvManager extends EnvManager { } public applyOptions(options: MuseumEnvOptions) { - this.onMeshUpdatedCallback?.(this.meshes); + registerMeshes(this.meshes); } public dispose() { @@ -777,3 +796,385 @@ export class MuseumEnvManager extends EnvManager { super.dispose(); } } + +export class CustomMadoriEnvManager extends EnvManager { + private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null; + private meshes: BABYLON.Mesh[] = []; + private floorRootNode: BABYLON.TransformNode | 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) { + super(engine); + } + + public async load(options: CustomMadoriEnvOptions) { + 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('env: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; // 効いてなさそう babylonのバグ? + } + //shadowGeneratorForRoomLight.useContactHardeningShadow = true; + //shadowGeneratorForRoomLight.contactHardeningLightSizeUVRatio = 0.01; + this.shadowGenerators.push(shadowGeneratorForRoomLight); + } + + if (this.engine.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM) { + this.sunLight = new BABYLON.DirectionalLight('env: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; // 効いてなさそう babylonのバグ? + } + this.shadowGenerators.push(shadowGeneratorForSunLight); + } + + this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/room/envs/custom-madori/units.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.floorRootNode = this.loaderResult.transformNodes.find(t => t.name.includes('__FLOOR__')); + + 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); + } + + private createUnit(options: CustomMadoriEnvOptions, x: number, z: number) { + const defIndex = x + (options.dimension[0] * z); + const unitDef = options.units[defIndex]; + if (unitDef == null) return; + + const unitRoot = new BABYLON.TransformNode(`unit_${x}_${z}`, this.engine.scene); + unitRoot.position = new BABYLON.Vector3(cm(100 * x), 0, cm(100 * z)); + + this.floorRootNode.clone(`floor_${x}_${z}`, unitRoot); + } + + 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: CustomMadoriEnvOptions) { + // 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(); + } + + registerMeshes(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(); + } +} diff --git a/packages/misskey-world/src/room/type.ts b/packages/misskey-world/src/room/type.ts index 6a81a98db0..33f7206873 100644 --- a/packages/misskey-world/src/room/type.ts +++ b/packages/misskey-world/src/room/type.ts @@ -55,3 +55,31 @@ export type SimpleEnvOptions = { }; export type MuseumEnvOptions = any; + +export type CustomMadoriEnvOptions = { + dimension: [number, number]; + units: ({ + type: 'floor'; + 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]; + }; + } | null)[]; +};