diff --git a/packages/frontend-misskey-world-engine/src/engine.ts b/packages/frontend-misskey-world-engine/src/engine.ts index 13c8aac448..4d199b385c 100644 --- a/packages/frontend-misskey-world-engine/src/engine.ts +++ b/packages/frontend-misskey-world-engine/src/engine.ts @@ -5,18 +5,19 @@ import * as BABYLON from '@babylonjs/core/pure.js'; import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic.js'; -import tinycolor from 'tinycolor2'; -import Hls from 'hls.js'; import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js'; -import { RecyvlingTextGrid, Timer, createPlaneUvMapper, randomRange } from './utility.js'; +import { FreeCameraManualInput, GRAPHICS_QUALITY, Timer } from './utility.js'; import { TIME_MAP } from './utility.js'; import { EngineBase } from './EngineBase.js'; +import { LobbyEnvManager } from './envs/lobby.js'; +import type { PlayerContainer, PlayerProfile, PlayerState } from './PlayerContainer.js'; +import type { WorldEnvManager } from './env.js'; -const SNAPSHOT_RENDERING = false; // 実験的 -const USE_GLOW = true; // ドローコールが増えて重い const IN_WEB_WORKER = typeof window === 'undefined'; export class WorldEngine extends EngineBase<{ + 'changeMyPlayerState': (ctx: PlayerState) => void; + 'playerPointed': (ctx: { playerId: string; }) => void; 'playSfxUrl': (ctx: { url: string; options: { @@ -25,29 +26,48 @@ export class WorldEngine extends EngineBase<{ }; }) => void; 'loadingProgress': (ctx: { progress: number }) => void; + 'contextlost': (ctx: { reason: string; message: string; }) => void; }> { - private shadowGeneratorForSunLight: BABYLON.ShadowGenerator; public camera: BABYLON.UniversalCamera; private time: 0 | 1 | 2 = 0; // 0: 昼, 1: 夕, 2: 夜 private envMap: BABYLON.CubeTexture; public lightContainer: BABYLON.ClusteredLightContainer; public sr: BABYLON.SnapshotRenderingHelper; private gl: BABYLON.GlowLayer | null = null; - private textMaterial: BABYLON.StandardMaterial; - private translucentTextMaterial: BABYLON.StandardMaterial; - private reflectionProbe: BABYLON.ReflectionProbe; public timer: Timer = new Timer(); public isSitting = false; + private cameraHeight = cm(130); + private fov: number; + private useGlow: boolean; + public graphicsQuality: number; + private playerProfiles: Record = {}; + private playerContainers: PlayerContainer[] = []; + private showUsernameOnAvatar: boolean; + private show2dAvatarOnAvatar: boolean; + private envManager: WorldEnvManager | null = null; + private inited = false; constructor(options: { - canvas: HTMLCanvasElement; engine: BABYLON.WebGPUEngine; + graphicsQuality: number; + fps: number | null; + antialias: boolean; + fov: number; + useVirtualJoystick?: boolean; + showUsernameOnAvatar: boolean; + show2dAvatarOnAvatar: boolean; }) { super({ engine: options.engine, - fps: null, + fps: options.fps, }); + this.graphicsQuality = options.graphicsQuality; + this.useGlow = this.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM; + this.fov = options.fov; + this.showUsernameOnAvatar = options.showUsernameOnAvatar; + this.show2dAvatarOnAvatar = options.show2dAvatarOnAvatar; + registerBuiltInLoaders(); this.scene.autoClear = false; @@ -55,6 +75,7 @@ export class WorldEngine extends EngineBase<{ 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.collisionsEnabled = true; this.sr = new BABYLON.SnapshotRenderingHelper(this.scene); @@ -81,73 +102,37 @@ export class WorldEngine extends EngineBase<{ //this.envMap.level = 1; this.envMap.level = 0; - this.scene.collisionsEnabled = true; - - this.camera = new BABYLON.UniversalCamera('camera', new BABYLON.Vector3(cm(0), cm(250), cm(3000)), this.scene); - this.camera.attachControl(this.canvas); + this.camera = new BABYLON.FreeCamera('camera', new BABYLON.Vector3(0, this.cameraHeight, cm(0)), this.scene); this.camera.minZ = cm(1); - this.camera.maxZ = cm(100000); - this.camera.fov = 1; + this.camera.maxZ = cm(1000); + this.camera.fov = this.fov; this.camera.ellipsoid = new BABYLON.Vector3(cm(15), cm(65), cm(15)); this.camera.checkCollisions = true; this.camera.applyGravity = true; this.camera.needMoveForGravity = true; - this.camera.keysUp.push(87); // W - this.camera.keysDown.push(83); // S - this.camera.keysLeft.push(65); // A - this.camera.keysRight.push(68); // D - const normalSpeed = 0.03 * WORLD_SCALE; - this.camera.speed = normalSpeed; - this.scene.onKeyboardObservable.add((kbInfo) => { - switch (kbInfo.type) { - case BABYLON.KeyboardEventTypes.KEYDOWN: - if (kbInfo.event.key === 'Shift') { - this.camera.speed = normalSpeed * 4; - } - break; - case BABYLON.KeyboardEventTypes.KEYUP: - if (kbInfo.event.key === 'Shift') { - this.camera.speed = normalSpeed; - } - break; - } - }); + this.camera.inputs.clear(); + if (options.useVirtualJoystick) { + this.camera.inputs.add(new FreeCameraManualInput(this.scene, { + moveSensitivity: 0.015 * WORLD_SCALE, + rotationSensitivity: 0.0007, + })); + this.camera.inertia = 0.75; + } else { + this.camera.inputs.add(new FreeCameraManualInput(this.scene, { + moveSensitivity: 0.002 * WORLD_SCALE, + rotationSensitivity: 0.0003, + })); + } - //this.scene.activeCamera = this.camera; - - //this.reflectionProbe = new BABYLON.ReflectionProbe('rp', 512, this.scene, true, true, false); - //this.reflectionProbe.refreshRate = 60; - - //const mainLight = new BABYLON.PointLight('mainLight', new BABYLON.Vector3(0, cm(300), 0), this.scene); - //mainLight.intensity = 10000000; - //mainLight.radius = cm(1000); - //mainLight.diffuse = new BABYLON.Color3(1, 1, 1); - - const ambientLight1 = new BABYLON.HemisphericLight('ambientLight1', new BABYLON.Vector3(0, 1, 0), this.scene); - ambientLight1.diffuse = new BABYLON.Color3(1.0, 0.9, 0.8); - ambientLight1.intensity = 1; - const ambientLight2 = new BABYLON.HemisphericLight('ambientLight2', new BABYLON.Vector3(0, -1, 0), this.scene); - ambientLight2.diffuse = new BABYLON.Color3(0.8, 0.9, 1.0); - ambientLight2.intensity = 1; - //ambientLight.intensity = 0; - - const sunLight = new BABYLON.DirectionalLight('sunLight', new BABYLON.Vector3(0, -1, 0), this.scene); - sunLight.position = new BABYLON.Vector3(cm(0), cm(10000), cm(0)); - sunLight.diffuse = this.time === 0 ? new BABYLON.Color3(1.0, 1.0, 1.0) : this.time === 1 ? new BABYLON.Color3(1.0, 0.8, 0.6) : new BABYLON.Color3(0.6, 0.8, 1.0); - sunLight.intensity = this.time === 0 ? 2 : this.time === 1 ? 0.5 : 0.25; - sunLight.shadowMinZ = cm(1000); - sunLight.shadowMaxZ = cm(2000); - - this.shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(4096, sunLight); - this.shadowGeneratorForSunLight.forceBackFacesOnly = true; - this.shadowGeneratorForSunLight.bias = 0.0001; - this.shadowGeneratorForSunLight.usePercentageCloserFiltering = true; - this.shadowGeneratorForSunLight.usePoissonSampling = true; - //this.shadowGeneratorForSunLight.getShadowMap().refreshRate = 60; + this.scene.activeCamera = this.camera; this.lightContainer = new BABYLON.ClusteredLightContainer('clustered', [], this.scene); + this.lightContainer.maxRange = cm(1000); + this.lightContainer.verticalTiles = 32; + this.lightContainer.horizontalTiles = 32; + this.lightContainer.depthSlices = 32; - if (USE_GLOW) { + if (this.useGlow) { this.gl = new BABYLON.GlowLayer('glow', this.scene, { //mainTextureFixedSize: 512, blurKernelSize: 64, @@ -155,405 +140,123 @@ export class WorldEngine extends EngineBase<{ this.gl.intensity = 0.5; this.gl.addExcludedMesh(skybox); this.scene.setRenderingAutoClearDepthStencil(this.gl.renderingGroupId, false); - - if (SNAPSHOT_RENDERING) { - this.sr.updateMeshesForEffectLayer(this.gl); - } + this.sr.updateMeshesForEffectLayer(this.gl); } } public async init() { - await this.loadEnvModel(); + await this.loadEnv(); - if (SNAPSHOT_RENDERING) { - this.sr.enableSnapshotRendering(); - } + this.startRenderLoop(); - this.inputs.on('keydown', (ev) => { - }); + await this.scene.whenReadyAsync(); + + // 必ずシーンが少なくとも1フレームレンダリングがされてから呼ばれるように注意すること。そうしないとタイミングによってはエンジンがクラッシュする + this.sr.enableSnapshotRendering(); this.inputs.on('wheel', (ev) => { - this.camera.fov += ev.deltaY * 0.001; - this.camera.fov = Math.max(0.25, Math.min(1, this.camera.fov)); + if (this.scene.activeCamera === this.camera) { + this.camera.fov += ev.deltaY * 0.001; + this.camera.fov = Math.max(0.25, Math.min(this.fov, this.camera.fov)); + } + }); + + this.inputs.on('zoom', (ev) => { + if (this.scene.activeCamera === this.camera) { + this.camera.fov += -ev.delta * 0.003; + this.camera.fov = Math.max(0.25, Math.min(this.fov, this.camera.fov)); + } }); this.inputs.on('click', (ev) => { + // TODO: GPUPickerを使いたいが、なぜか一部のメッシュが反応しない + const pickingInfo = this.scene.pick(ev.x, ev.y, + (m) => m.name.includes('__PICK__') || m.metadata?.isPlayer || (m.isVisible && m.isEnabled() && m.metadata?.furnitureId != null && this.furnitureContainers.has(m.metadata.furnitureId))); + if (pickingInfo.pickedMesh != null) { + const playerId = pickingInfo.pickedMesh.metadata.playerId; + if (playerId != null && this.playerContainers.some(c => c.id === playerId)) { + const c = this.playerContainers.find(c => c.id === playerId)!; + this.look(c.root.position); + this.ev('playerPointed', { playerId }); + return; + } + } }); - } - private async loadEnvModel() { - const envObj = await BABYLON.ImportMeshAsync('/client-assets/world/lobby/default.glb', this.scene); - envObj.meshes[0].scaling = envObj.meshes[0].scaling.scale(WORLD_SCALE); - envObj.meshes[0].bakeCurrentTransformIntoVertices(); - for (const mesh of envObj.meshes) { - if (mesh.name === '__root__') continue; - if (mesh.name.includes('__COLLISION__')) { - mesh.checkCollisions = true; - mesh.isVisible = false; + this.inputs.on('pointer', (ev) => { + if (this.scene.activeCamera === this.camera) { + (this.camera.inputs.attached.manual as FreeCameraManualInput).setRotationVector({ x: ev.x, y: ev.y }); } - if (this.reflectionProbe != null) { - if (mesh.material) (mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.reflectionProbe.cubeTexture; - if (mesh.material) (mesh.material as BABYLON.PBRMaterial).realTimeFiltering = true; - } - } - - this.textMaterial = new BABYLON.StandardMaterial('textMaterial', this.scene); - this.textMaterial.diffuseTexture = new BABYLON.Texture('/client-assets/world/chars.png', this.scene, false, false); - this.textMaterial.diffuseTexture.hasAlpha = true; - this.textMaterial.disableLighting = true; - this.textMaterial.transparencyMode = BABYLON.Material.MATERIAL_ALPHABLEND; - this.textMaterial.useAlphaFromDiffuseTexture = true; - this.textMaterial.freeze(); - - this.translucentTextMaterial = this.textMaterial.clone('translucentTextMaterial'); - this.translucentTextMaterial.alpha = 0.25; - - { - const objet = envObj.meshes.find(m => m.name.includes('__OBJET__')); - objet.rotation = objet.rotationQuaternion.toEulerAngles(); - objet.rotationQuaternion = null; - - const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); - anim.setKeys([ - { frame: 0, value: 0 }, - { frame: 5000, value: -(Math.PI * 2) }, - ]); - objet.animations = [anim]; - this.scene.beginAnimation(objet, 0, 5000, true); - } - - { - const ring = envObj.meshes.find(m => m.name.includes('__LED_RING__')); - ring.rotation = ring.rotationQuaternion.toEulerAngles(); - ring.rotationQuaternion = null; - - const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); - anim.setKeys([ - { frame: 0, value: 0 }, - { frame: 5000, value: -(Math.PI * 2) }, - ]); - ring.animations = [anim]; - this.scene.beginAnimation(ring, 0, 5000, true); - } - - { - const messageRingRoot = new BABYLON.TransformNode('', this.scene); - const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_OUTER_1__')); - messageRing.parent = messageRingRoot; - messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles(); - messageRing.rotationQuaternion = null; - const text = new RecyvlingTextGrid(messageRing, 256, { - meshFlipped: true, - material: this.textMaterial, - }); - - text.write('Wellcome to Misskey World!'); - - //messageRingRoot.rotation.x = Math.PI / 4; - - const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); - anim.setKeys([ - { frame: 0, value: 0 }, - { frame: 10000, value: -(Math.PI * 2) }, - ]); - messageRing.animations = [anim]; - this.scene.beginAnimation(messageRing, 0, 10000, true); - - const texts = [ - 'Wellcome to Misskey World!', - 'Enjoy your stay!', - 'Feel free to look around!', - 'This is a virtual space for Misskey users!', - //'You can chat, play games, and more!', - //'Check out the bulletin board for announcements', - 'Have a nice day with Misskey!', - 'MAINTENANCE will begin at 9:00 A.M.', - ]; - - let currentTextIndex = 1; - - this.timer.setInterval(() => { - const textToShow = texts[currentTextIndex]; - currentTextIndex = (currentTextIndex + 1) % texts.length; - text.writeWithAnimation(textToShow); - }, 10000); - } - - { - const messageRingRoot = new BABYLON.TransformNode('', this.scene); - const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_OUTER_2__')); - messageRing.parent = messageRingRoot; - messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles(); - messageRing.rotationQuaternion = null; - const text = new RecyvlingTextGrid(messageRing, 256, { - meshFlipped: true, - material: this.translucentTextMaterial, - repeatSeparator: ' ', - }); - - messageRingRoot.rotation.x = Math.PI / 2; - - const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); - anim.setKeys([ - { frame: 0, value: 0 }, - { frame: 10000, value: -(Math.PI * 2) }, - ]); - messageRing.animations = [anim]; - this.scene.beginAnimation(messageRing, 0, 10000, true); - - this.timer.setInterval(() => { - text.write(Date.now().toString()); - }, 10); - } - - { - const messageRingRoot = new BABYLON.TransformNode('', this.scene); - const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_INNER_1__')); - messageRing.parent = messageRingRoot; - messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles(); - messageRing.rotationQuaternion = null; - const text = new RecyvlingTextGrid(messageRing, 64, { - material: this.textMaterial, - repeatSeparator: ' ', - }); - - //messageRingRoot.rotation.x = Math.PI / 4; - - const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); - anim.setKeys([ - { frame: 0, value: 0 }, - { frame: 10000, value: (Math.PI * 2) }, - ]); - messageRing.animations = [anim]; - this.scene.beginAnimation(messageRing, 0, 10000, true); - - this.timer.setInterval(() => { - const now = new Date(); - const hours = now.getHours().toString().padStart(2, '0'); - const minutes = now.getMinutes().toString().padStart(2, '0'); - const seconds = now.getSeconds().toString().padStart(2, '0'); - text.write(`${hours}:${minutes}:${seconds}`); - }, 1000); - } - - { - const messageRingRoot = new BABYLON.TransformNode('', this.scene); - const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_INNER_2__')); - messageRing.parent = messageRingRoot; - messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles(); - messageRing.rotationQuaternion = null; - const text = new RecyvlingTextGrid(messageRing, 64, { - material: this.textMaterial, - repeatSeparator: ' ', - }); - - //messageRingRoot.rotation.x = Math.PI / 4; - - const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); - anim.setKeys([ - { frame: 0, value: 0 }, - { frame: 10000, value: -(Math.PI * 2) }, - ]); - messageRing.animations = [anim]; - this.scene.beginAnimation(messageRing, 0, 10000, true); - - this.timer.setInterval(() => { - const now = new Date(); - const years = now.getFullYear().toString(); - const months = (now.getMonth() + 1).toString().padStart(2, '0'); - const days = now.getDate().toString().padStart(2, '0'); - text.write(`${years}/${months}/${days}`); - }, 1000); - } - - for (let i = 0; i < 16; i++) { - const sphereRoot = new BABYLON.TransformNode('', this.scene); - sphereRoot.position = new BABYLON.Vector3(cm(0), cm(1000 + (100 * i)), cm(0)); - const rotation = Math.random() * Math.PI * 2; - const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(randomRange(50, 300)), segments: 16 }, this.scene); - sphere.parent = sphereRoot; - sphere.position = new BABYLON.Vector3(cm(0), cm(0), cm(randomRange(2000, 7000))); - - const mat = new BABYLON.PBRMaterial('', this.scene); - const color = tinycolor({ h: Math.random() * 360, s: 1, l: 0.5 }).toRgb(); - mat.emissiveColor = new BABYLON.Color3(color.r / 255, color.g / 255, color.b / 255); - mat.disableLighting = true; - this.gl?.addExcludedMesh(sphere); - sphere.material = mat; - - const speed = randomRange(5000, 30000); - const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); - anim.setKeys([ - { frame: 0, value: rotation }, - { frame: speed, value: Math.random() < 0.5 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) }, - ]); - sphereRoot.animations = [anim]; - this.scene.beginAnimation(sphereRoot, 0, speed, true); - - if (this.reflectionProbe != null) this.reflectionProbe.renderList.push(sphere); - } - - for (let i = 0; i < 64; i++) { - const sphereRoot = new BABYLON.TransformNode('', this.scene); - sphereRoot.position = new BABYLON.Vector3(cm(0), cm(randomRange(-5000, 5000)), cm(0)); - const rotation = Math.random() * Math.PI * 2; - const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(randomRange(500, 3000)), segments: 16 }, this.scene); - sphere.parent = sphereRoot; - sphere.position = new BABYLON.Vector3(cm(0), cm(0), cm(randomRange(10000, 15000))); - - const mat = new BABYLON.PBRMaterial('', this.scene); - const color = tinycolor({ h: Math.random() * 360, s: randomRange(0, 1), l: randomRange(0.75, 1) }).toRgb(); - mat.emissiveColor = new BABYLON.Color3(color.r / 255, color.g / 255, color.b / 255); - mat.disableLighting = true; - this.gl?.addExcludedMesh(sphere); - sphere.material = mat; - - const speed = randomRange(10000, 100000); - const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); - anim.setKeys([ - { frame: 0, value: rotation }, - { frame: speed, value: Math.random() < 0.5 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) }, - ]); - sphereRoot.animations = [anim]; - this.scene.beginAnimation(sphereRoot, 0, speed, true); - - if (this.reflectionProbe != null) this.reflectionProbe.renderList.push(sphere); - } - - //const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(10) }, this.scene); - - const adsCountCol = 4; - const adsCountRow = 2; - for (let j = 0; j < adsCountRow; j++) { - for (let i = 0; i < adsCountCol; i++) { - const adRoot = new BABYLON.TransformNode(`ad_${j}_${i}_root`, this.scene); - adRoot.position = new BABYLON.Vector3(cm(0), cm(500 + (1000 * j)), cm(0)); - const rotation = (i / adsCountCol) * Math.PI * 2; - const adMesh = BABYLON.MeshBuilder.CreatePlane(`ad_${j}_${i}`, { width: cm(1000), height: cm(700) }, this.scene); - adMesh.parent = adRoot; - adMesh.position = new BABYLON.Vector3(cm(0), cm(0), cm(7500)); - - const tex = new BABYLON.Texture('/client-assets/world/lobby/dummy-ads/angry_ai.png', this.scene); - const adMat = new BABYLON.StandardMaterial(`ad_${j}_${i}_mat`, this.scene); - adMat.emissiveTexture = tex; - adMat.disableLighting = true; - adMesh.material = adMat; - - const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); - anim.setKeys([ - { frame: 0, value: rotation }, - { frame: 15000, value: j % 2 === 0 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) }, - ]); - adRoot.animations = [anim]; - this.scene.beginAnimation(adRoot, 0, 15000, true); - } - } - - const worldRingH = envObj.meshes.find(m => m.name.includes('__WORLD_RING_H__')); - const worldRingM = envObj.meshes.find(m => m.name.includes('__WORLD_RING_M__')); - - worldRingH.material.reflectionTexture = null; - worldRingM.material.reflectionTexture = null; - - if (this.reflectionProbe != null) this.reflectionProbe.renderList.push(worldRingH); - if (this.reflectionProbe != null) this.reflectionProbe.renderList.push(worldRingM); - - worldRingH.rotation = worldRingH.rotationQuaternion.toEulerAngles(); - worldRingM.rotation = worldRingM.rotationQuaternion.toEulerAngles(); - worldRingH.rotationQuaternion = null; - worldRingM.rotationQuaternion = null; - - const _1h = 1000 * 60 * 60; - const _12h = _1h * 12; - const _7days = _1h * 24 * 7; - const _30days = _1h * 24 * 30; + }); this.timer.setInterval(() => { - const time = Date.now(); - worldRingH.rotation.x = ((time % _12h) / _12h) * Math.PI * 2; - worldRingM.rotation.y = -(((time % _1h) / _1h) * Math.PI); + const camera = this.scene.activeCamera!; + const myPos = camera.globalPosition; + const myRotation = camera.absoluteRotation.toEulerAngles(); + this.ev('changeMyPlayerState', { + position: [myPos.x, myPos.y, myPos.z], + rotation: [myRotation.x, myRotation.y, myRotation.z], + }); }, 100); - const screenMeshes = envObj.meshes.filter(m => m.name.includes('__SCREEN__')); - const screenMaterial = screenMeshes[0].material as BABYLON.PBRMaterial; + this.inited = true; + } - const videoEl = document.createElement('video'); - videoEl.crossOrigin = 'anonymous'; + private async loadEnv() { + const envManager = new LobbyEnvManager(this); - const hls = new Hls(); - hls.loadSource('https://tvs.misskey.io/official/hq-beta/ts:abr.m3u8'); - hls.attachMedia(videoEl); + await envManager.load(); + envManager.setTime(this.time); - this.timer.setTimeout(() => { - const tex = new BABYLON.VideoTexture('', videoEl, this.scene, true, true); - tex.level = 0.5; - tex.video.loop = true; - tex.video.volume = 0.25; - tex.video.muted = true; - - screenMaterial.albedoColor = new BABYLON.Color3(0, 0, 0); - screenMaterial.emissiveTexture = tex; - screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1); - - tex.onLoadObservable.addOnce(() => { - tex.video.play(); - for (const mesh of screenMeshes) { - if (mesh instanceof BABYLON.InstancedMesh) continue; - //normalizeUvToSquare(mesh); - const updateUv = createPlaneUvMapper(mesh); - if (tex == null) return; - const srcAspect = tex.getSize().width / tex.getSize().height; - const targetAspect = 16 / 9; - updateUv(srcAspect, targetAspect, 'cover'); + for (const mat of this.scene.materials) { + mat.unfreeze(); + if (mat instanceof BABYLON.MultiMaterial) { + for (const subMat of mat.subMaterials) { + if (subMat.metadata?.useEnvMap) subMat.reflectionTexture = envManager.envMapIndoor; } - }); - }, 3000); + } else { + if (mat.metadata?.useEnvMap) mat.reflectionTexture = envManager.envMapIndoor; + } + } - const emitter = new BABYLON.TransformNode('emitter', this.scene); - emitter.position = new BABYLON.Vector3(0, cm(-1000), 0); - const ps = new BABYLON.ParticleSystem('', 128, this.scene); - ps.particleTexture = new BABYLON.Texture('/client-assets/world/objects/lava-lamp/bubble.png'); - ps.emitter = emitter; - ps.isLocal = true; - ps.minEmitBox = new BABYLON.Vector3(cm(-1000), 0, cm(-1000)); - ps.maxEmitBox = new BABYLON.Vector3(cm(1000), 0, cm(1000)); - ps.minEmitPower = cm(100); - ps.maxEmitPower = cm(500); - ps.minLifeTime = 30; - ps.maxLifeTime = 30; - ps.minSize = cm(30); - ps.maxSize = cm(300); - ps.direction1 = new BABYLON.Vector3(0, 1, 0); - ps.direction2 = new BABYLON.Vector3(0, 1, 0); - ps.emitRate = 1.5; - ps.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD; - ps.color1 = new BABYLON.Color4(1, 1, 1, 0.3); - ps.color2 = new BABYLON.Color4(1, 1, 1, 0.2); - ps.colorDead = new BABYLON.Color4(1, 1, 1, 0); - ps.preWarmCycles = Math.random() * 1000; - ps.start(); + this.envManager = envManager; + + this.camera.maxZ = this.envManager.maxCameraZ; } - public sitChair(furnitureId: string) { - this.isSitting = true; - this.fixedCamera.parent = this.objectMeshs.get(furnitureId); - this.fixedCamera.position = new BABYLON.Vector3(0, cm(120), 0); - this.fixedCamera.rotation = new BABYLON.Vector3(0, 0, 0); - this.scene.activeCamera = this.fixedCamera; - this.selectFurniture(null); + public cameraMove(vector: { x: number; y: number; }, dash: boolean) { + (this.camera.inputs.attached.manual as FreeCameraManualInput).setMoveVector(dash ? { x: vector.x * 3, y: vector.y * 3 } : vector); } - public standUp() { - this.isSitting = false; - this.scene.activeCamera = this.camera; - this.fixedCamera.parent = null; + public cameraJoystickMove(vector: { x: number; y: number; }) { + (this.camera.inputs.attached.manual as FreeCameraManualInput).setMoveVector(vector); } private playSfxUrl(url: string, options: { volume: number; playbackRate: number }) { this.emit('playSfxUrl', { url, options }); } + public clearPlayers() { + this.sr.disableSnapshotRendering(); + for (const playerContainer of this.playerContainers) { + playerContainer.destroy(); + } + this.sr.enableSnapshotRendering(); + this.playerContainers = []; + } + + public updateAvatarDisplayOptions(options: { showUsername: boolean; show2dAvatar: boolean }) { + this.showUsernameOnAvatar = options.showUsername; + this.show2dAvatarOnAvatar = options.show2dAvatar; + + this.sr.disableSnapshotRendering(); + for (const playerContainer of this.playerContainers) { + playerContainer.updateUserInfoDisplayOptions(options); + } + this.sr.enableSnapshotRendering(); + } + public resize() { this.engine.resize(true); } @@ -561,37 +264,6 @@ export class WorldEngine extends EngineBase<{ public destroy() { super.destroy(); this.timer.dispose(); - } -} - -class MessageRing { - constructor(mesh: BABYLON.Mesh, scene: BABYLON.Scene, options: { material: BABYLON.StandardMaterial; repeatSeparator: string; }) { - const messageRingRoot = new BABYLON.TransformNode('', this.scene); - const messageRing = envObj.meshes.find(m => m.name.includes('__MESSAGE_RING_INNER_1__')); - messageRing.parent = messageRingRoot; - messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles(); - messageRing.rotationQuaternion = null; - const text = new RecyvlingTextGrid(messageRing, 64, { - material: this.textMaterial, - repeatSeparator: ' ', - }); - - //messageRingRoot.rotation.x = Math.PI / 4; - - const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); - anim.setKeys([ - { frame: 0, value: 0 }, - { frame: 10000, value: (Math.PI * 2) }, - ]); - messageRing.animations = [anim]; - this.scene.beginAnimation(messageRing, 0, 10000, true); - - this.timer.setInterval(() => { - const now = new Date(); - const hours = now.getHours().toString().padStart(2, '0'); - const minutes = now.getMinutes().toString().padStart(2, '0'); - const seconds = now.getSeconds().toString().padStart(2, '0'); - text.write(`${hours}:${minutes}:${seconds}`); - }, 1000); + this.envManager.dispose(); } } diff --git a/packages/frontend-misskey-world-engine/src/env.ts b/packages/frontend-misskey-world-engine/src/env.ts new file mode 100644 index 0000000000..3aeb801ac4 --- /dev/null +++ b/packages/frontend-misskey-world-engine/src/env.ts @@ -0,0 +1,109 @@ + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as BABYLON from '@babylonjs/core/pure.js'; +import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js'; +import { findMaterial, GRAPHICS_QUALITY } from './utility.js'; +import type { WorldEngine } from './engine.js'; + +export abstract class WorldEnvManager { + protected engine: WorldEngine; + public abstract envMapIndoor: BABYLON.CubeTexture | null; + public abstract maxCameraZ: number; + private shadowGenerators: BABYLON.ShadowGenerator[] = []; + + constructor(engine: WorldEngine) { + this.engine = engine; + } + + abstract load(): Promise; + abstract setTime(time: number): void; + + protected registerShadowGenerator(shadowGenerator: BABYLON.ShadowGenerator) { + this.shadowGenerators.push(shadowGenerator); + + const shadowMap = shadowGenerator.getShadowMap()!; + shadowMap.refreshRate = BABYLON.RenderTargetTexture.REFRESHRATE_RENDER_ONCE; + + // https://forum.babylonjs.com/t/is-it-intentional-that-the-shadow-map-refresh-rate-is-ignored-under-fast-snapshot-rendering/63523 + const objectRenderer = shadowMap._objectRenderer; + const originalShouldRender = objectRenderer.shouldRender.bind(objectRenderer); + objectRenderer.shouldRender = function () { + if (this._engine.snapshotRendering) { + return this.refreshRate !== BABYLON.RenderTargetTexture.REFRESHRATE_RENDER_ONCE; + } + + return originalShouldRender(); + }; + } + + 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 async renderShadow() { + this.engine.sr.disableSnapshotRendering(); + + for (const shadowGen of this.shadowGenerators) { + const shadowMap = shadowGen.getShadowMap()!; + shadowMap.refreshRate = 1; + } + + await new Promise(resolve => setTimeout(resolve, 1)); + + for (const shadowGen of this.shadowGenerators) { + const shadowMap = shadowGen.getShadowMap()!; + shadowMap.refreshRate = BABYLON.RenderTargetTexture.REFRESHRATE_RENDER_ONCE; + } + + this.engine.sr.enableSnapshotRendering(); + } + + protected registerMeshes(meshes: BABYLON.AbstractMesh[]) { + for (const mesh of meshes) { + if (!this.engine.scene.meshes.includes(mesh)) this.engine.scene.addMesh(mesh); + + if (['__COLLISION__'].some(name => mesh.name.includes(name))) { + mesh.isPickable = false; + mesh.receiveShadows = false; + mesh.isVisible = false; + mesh.checkCollisions = false; + if (mesh.name.includes('__COLLISION__')) { + mesh.checkCollisions = true; + } + continue; + } + + mesh.isPickable = false; + mesh.checkCollisions = false; + if (mesh.material != null) { + (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 + + if (mesh.material instanceof BABYLON.MultiMaterial) { + for (const subMat of mesh.material.subMaterials) { + subMat.reflectionTexture = this.envMapIndoor; + } + } else if (mesh.material instanceof BABYLON.PBRMaterial) { + mesh.material.reflectionTexture = this.envMapIndoor; + } + } + } + } + + public dispose() { + for (const shadowGen of this.shadowGenerators) { + shadowGen.dispose(); + } + } +} diff --git a/packages/frontend-misskey-world-engine/src/envs/lobby.ts b/packages/frontend-misskey-world-engine/src/envs/lobby.ts new file mode 100644 index 0000000000..02e1fc9073 --- /dev/null +++ b/packages/frontend-misskey-world-engine/src/envs/lobby.ts @@ -0,0 +1,471 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as BABYLON from '@babylonjs/core/pure.js'; +import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js'; +import tinycolor from 'tinycolor2'; +import Hls from 'hls.js'; +import { findMaterial, GRAPHICS_QUALITY, Timer } from '../utility.js'; +import { WorldEnvManager } from '../env.js'; +import { RecyvlingTextGrid, createPlaneUvMapper, randomRange } from '../utility.js'; +import type { WorldEngine } from '../engine.js'; + +export class LobbyEnvManager extends WorldEnvManager { + private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null; + private meshes: BABYLON.Mesh[] = []; + private skybox: BABYLON.Mesh | null = null; + private skyboxMat: BABYLON.StandardMaterial | null = null; + private sunLight: BABYLON.DirectionalLight | null = null; + public envMapIndoor: BABYLON.CubeTexture | null = null; + public maxCameraZ = cm(100000); + private textMaterial: BABYLON.StandardMaterial | null = null; + private translucentTextMaterial: BABYLON.StandardMaterial | null = null; + private timer: Timer = new Timer(); + + constructor(engine: WorldEngine) { + super(engine); + } + + public async load() { + this.engine.camera.position = new BABYLON.Vector3(cm(0), cm(250), cm(3000)); + + this.skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(30000) }, 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; + + const ambientLight1 = new BABYLON.HemisphericLight('ambientLight1', new BABYLON.Vector3(0, 1, 0), this.engine.scene); + ambientLight1.diffuse = new BABYLON.Color3(1.0, 0.9, 0.8); + ambientLight1.intensity = 1; + const ambientLight2 = new BABYLON.HemisphericLight('ambientLight2', new BABYLON.Vector3(0, -1, 0), this.engine.scene); + ambientLight2.diffuse = new BABYLON.Color3(0.8, 0.9, 1.0); + ambientLight2.intensity = 1; + //ambientLight.intensity = 0; + + this.sunLight = new BABYLON.DirectionalLight('sunLight', new BABYLON.Vector3(0, -1, 0), this.engine.scene); + this.sunLight.position = new BABYLON.Vector3(cm(0), cm(10000), cm(0)); + this.sunLight.shadowMinZ = cm(1000); + this.sunLight.shadowMaxZ = cm(2000); + + this.loaderResult = await BABYLON.ImportMeshAsync('/client-assets/world/envs/lobby/lobby.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; + 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); + + for (const mesh of this.meshes) { + if (['__COLLISION__'].some(name => mesh.name.includes(name))) continue; + mesh.receiveShadows = true; + + this.addShadowCaster(mesh); + } + + this.textMaterial = new BABYLON.StandardMaterial('textMaterial', this.engine.scene); + this.textMaterial.diffuseTexture = new BABYLON.Texture('/client-assets/world/chars.png', this.engine.scene, false, false); + this.textMaterial.diffuseTexture.hasAlpha = true; + this.textMaterial.disableLighting = true; + this.textMaterial.transparencyMode = BABYLON.Material.MATERIAL_ALPHABLEND; + this.textMaterial.useAlphaFromDiffuseTexture = true; + this.textMaterial.freeze(); + + this.translucentTextMaterial = this.textMaterial.clone('translucentTextMaterial'); + this.translucentTextMaterial.alpha = 0.25; + + { + const objet = this.meshes.find(m => m.name.includes('__OBJET__')); + objet.rotation = objet.rotationQuaternion.toEulerAngles(); + objet.rotationQuaternion = null; + + const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); + anim.setKeys([ + { frame: 0, value: 0 }, + { frame: 5000, value: -(Math.PI * 2) }, + ]); + objet.animations = [anim]; + this.engine.scene.beginAnimation(objet, 0, 5000, true); + } + + { + const ring = this.meshes.find(m => m.name.includes('__LED_RING__')); + ring.rotation = ring.rotationQuaternion.toEulerAngles(); + ring.rotationQuaternion = null; + + const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); + anim.setKeys([ + { frame: 0, value: 0 }, + { frame: 5000, value: -(Math.PI * 2) }, + ]); + ring.animations = [anim]; + this.engine.scene.beginAnimation(ring, 0, 5000, true); + } + + { + const messageRingRoot = new BABYLON.TransformNode('', this.engine.scene); + const messageRing = this.meshes.find(m => m.name.includes('__MESSAGE_RING_OUTER_1__')); + messageRing.parent = messageRingRoot; + messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles(); + messageRing.rotationQuaternion = null; + const text = new RecyvlingTextGrid(messageRing, 256, { + meshFlipped: true, + material: this.textMaterial, + }); + + text.write('Wellcome to Misskey World!'); + + //messageRingRoot.rotation.x = Math.PI / 4; + + const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); + anim.setKeys([ + { frame: 0, value: 0 }, + { frame: 10000, value: -(Math.PI * 2) }, + ]); + messageRing.animations = [anim]; + this.engine.scene.beginAnimation(messageRing, 0, 10000, true); + + const texts = [ + 'Wellcome to Misskey World!', + 'Enjoy your stay!', + 'Feel free to look around!', + 'This is a virtual space for Misskey users!', + //'You can chat, play games, and more!', + //'Check out the bulletin board for announcements', + 'Have a nice day with Misskey!', + 'MAINTENANCE will begin at 9:00 A.M.', + ]; + + let currentTextIndex = 1; + + this.timer.setInterval(() => { + const textToShow = texts[currentTextIndex]; + currentTextIndex = (currentTextIndex + 1) % texts.length; + text.writeWithAnimation(textToShow); + }, 10000); + } + + { + const messageRingRoot = new BABYLON.TransformNode('', this.engine.scene); + const messageRing = this.meshes.find(m => m.name.includes('__MESSAGE_RING_OUTER_2__')); + messageRing.parent = messageRingRoot; + messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles(); + messageRing.rotationQuaternion = null; + const text = new RecyvlingTextGrid(messageRing, 256, { + meshFlipped: true, + material: this.translucentTextMaterial, + repeatSeparator: ' ', + }); + + messageRingRoot.rotation.x = Math.PI / 2; + + const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); + anim.setKeys([ + { frame: 0, value: 0 }, + { frame: 10000, value: -(Math.PI * 2) }, + ]); + messageRing.animations = [anim]; + this.engine.scene.beginAnimation(messageRing, 0, 10000, true); + + this.timer.setInterval(() => { + text.write(Date.now().toString()); + }, 10); + } + + { + const messageRingRoot = new BABYLON.TransformNode('', this.engine.scene); + const messageRing = this.meshes.find(m => m.name.includes('__MESSAGE_RING_INNER_1__')); + messageRing.parent = messageRingRoot; + messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles(); + messageRing.rotationQuaternion = null; + const text = new RecyvlingTextGrid(messageRing, 64, { + material: this.textMaterial, + repeatSeparator: ' ', + }); + + //messageRingRoot.rotation.x = Math.PI / 4; + + const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); + anim.setKeys([ + { frame: 0, value: 0 }, + { frame: 10000, value: (Math.PI * 2) }, + ]); + messageRing.animations = [anim]; + this.engine.scene.beginAnimation(messageRing, 0, 10000, true); + + this.timer.setInterval(() => { + const now = new Date(); + const hours = now.getHours().toString().padStart(2, '0'); + const minutes = now.getMinutes().toString().padStart(2, '0'); + const seconds = now.getSeconds().toString().padStart(2, '0'); + text.write(`${hours}:${minutes}:${seconds}`); + }, 1000); + } + + { + const messageRingRoot = new BABYLON.TransformNode('', this.engine.scene); + const messageRing = this.meshes.find(m => m.name.includes('__MESSAGE_RING_INNER_2__')); + messageRing.parent = messageRingRoot; + messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles(); + messageRing.rotationQuaternion = null; + const text = new RecyvlingTextGrid(messageRing, 64, { + material: this.textMaterial, + repeatSeparator: ' ', + }); + + //messageRingRoot.rotation.x = Math.PI / 4; + + const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); + anim.setKeys([ + { frame: 0, value: 0 }, + { frame: 10000, value: -(Math.PI * 2) }, + ]); + messageRing.animations = [anim]; + this.engine.scene.beginAnimation(messageRing, 0, 10000, true); + + this.timer.setInterval(() => { + const now = new Date(); + const years = now.getFullYear().toString(); + const months = (now.getMonth() + 1).toString().padStart(2, '0'); + const days = now.getDate().toString().padStart(2, '0'); + text.write(`${years}/${months}/${days}`); + }, 1000); + } + + for (let i = 0; i < 16; i++) { + const sphereRoot = new BABYLON.TransformNode('', this.engine.scene); + sphereRoot.position = new BABYLON.Vector3(cm(0), cm(1000 + (100 * i)), cm(0)); + const rotation = Math.random() * Math.PI * 2; + const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(randomRange(50, 300)), segments: 16 }, this.engine.scene); + sphere.parent = sphereRoot; + sphere.position = new BABYLON.Vector3(cm(0), cm(0), cm(randomRange(2000, 7000))); + + const mat = new BABYLON.PBRMaterial('', this.engine.scene); + const color = tinycolor({ h: Math.random() * 360, s: 1, l: 0.5 }).toRgb(); + mat.emissiveColor = new BABYLON.Color3(color.r / 255, color.g / 255, color.b / 255); + mat.disableLighting = true; + this.engine.gl?.addExcludedMesh(sphere); + sphere.material = mat; + + const speed = randomRange(5000, 30000); + const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); + anim.setKeys([ + { frame: 0, value: rotation }, + { frame: speed, value: Math.random() < 0.5 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) }, + ]); + sphereRoot.animations = [anim]; + this.engine.scene.beginAnimation(sphereRoot, 0, speed, true); + } + + for (let i = 0; i < 64; i++) { + const sphereRoot = new BABYLON.TransformNode('', this.engine.scene); + sphereRoot.position = new BABYLON.Vector3(cm(0), cm(randomRange(-5000, 5000)), cm(0)); + const rotation = Math.random() * Math.PI * 2; + const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(randomRange(500, 3000)), segments: 16 }, this.engine.scene); + sphere.parent = sphereRoot; + sphere.position = new BABYLON.Vector3(cm(0), cm(0), cm(randomRange(10000, 15000))); + + const mat = new BABYLON.PBRMaterial('', this.engine.scene); + const color = tinycolor({ h: Math.random() * 360, s: randomRange(0, 1), l: randomRange(0.75, 1) }).toRgb(); + mat.emissiveColor = new BABYLON.Color3(color.r / 255, color.g / 255, color.b / 255); + mat.disableLighting = true; + this.engine.gl?.addExcludedMesh(sphere); + sphere.material = mat; + + const speed = randomRange(10000, 100000); + const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); + anim.setKeys([ + { frame: 0, value: rotation }, + { frame: speed, value: Math.random() < 0.5 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) }, + ]); + sphereRoot.animations = [anim]; + this.engine.scene.beginAnimation(sphereRoot, 0, speed, true); + } + + //const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(10) }, this.engine.scene); + + const adsCountCol = 4; + const adsCountRow = 2; + for (let j = 0; j < adsCountRow; j++) { + for (let i = 0; i < adsCountCol; i++) { + const adRoot = new BABYLON.TransformNode(`ad_${j}_${i}_root`, this.engine.scene); + adRoot.position = new BABYLON.Vector3(cm(0), cm(500 + (1000 * j)), cm(0)); + const rotation = (i / adsCountCol) * Math.PI * 2; + const adMesh = BABYLON.MeshBuilder.CreatePlane(`ad_${j}_${i}`, { width: cm(1000), height: cm(700) }, this.engine.scene); + adMesh.parent = adRoot; + adMesh.position = new BABYLON.Vector3(cm(0), cm(0), cm(7500)); + + const tex = new BABYLON.Texture('/client-assets/world/lobby/dummy-ads/angry_ai.png', this.engine.scene); + const adMat = new BABYLON.StandardMaterial(`ad_${j}_${i}_mat`, this.engine.scene); + adMat.emissiveTexture = tex; + adMat.disableLighting = true; + adMesh.material = adMat; + + const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); + anim.setKeys([ + { frame: 0, value: rotation }, + { frame: 15000, value: j % 2 === 0 ? rotation + (Math.PI * 2) : rotation - (Math.PI * 2) }, + ]); + adRoot.animations = [anim]; + this.engine.scene.beginAnimation(adRoot, 0, 15000, true); + } + } + + const worldRingH = this.meshes.find(m => m.name.includes('__WORLD_RING_H__')); + const worldRingM = this.meshes.find(m => m.name.includes('__WORLD_RING_M__')); + + worldRingH.material.reflectionTexture = null; + worldRingM.material.reflectionTexture = null; + + worldRingH.rotation = worldRingH.rotationQuaternion.toEulerAngles(); + worldRingM.rotation = worldRingM.rotationQuaternion.toEulerAngles(); + worldRingH.rotationQuaternion = null; + worldRingM.rotationQuaternion = null; + + const _1h = 1000 * 60 * 60; + const _12h = _1h * 12; + const _7days = _1h * 24 * 7; + const _30days = _1h * 24 * 30; + + this.timer.setInterval(() => { + const time = Date.now(); + worldRingH.rotation.x = ((time % _12h) / _12h) * Math.PI * 2; + worldRingM.rotation.y = -(((time % _1h) / _1h) * Math.PI); + }, 100); + + const screenMeshes = this.meshes.filter(m => m.name.includes('__SCREEN__')); + const screenMaterial = screenMeshes[0].material as BABYLON.PBRMaterial; + + const videoEl = document.createElement('video'); + videoEl.crossOrigin = 'anonymous'; + + const hls = new Hls(); + hls.loadSource('https://tvs.misskey.io/official/hq-beta/ts:abr.m3u8'); + hls.attachMedia(videoEl); + + this.timer.setTimeout(() => { + const tex = new BABYLON.VideoTexture('', videoEl, this.engine.scene, true, true); + tex.level = 0.5; + tex.video.loop = true; + tex.video.volume = 0.25; + tex.video.muted = true; + + screenMaterial.albedoColor = new BABYLON.Color3(0, 0, 0); + screenMaterial.emissiveTexture = tex; + screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1); + + tex.onLoadObservable.addOnce(() => { + tex.video.play(); + for (const mesh of screenMeshes) { + if (mesh instanceof BABYLON.InstancedMesh) continue; + //normalizeUvToSquare(mesh); + const updateUv = createPlaneUvMapper(mesh); + if (tex == null) return; + const srcAspect = tex.getSize().width / tex.getSize().height; + const targetAspect = 16 / 9; + updateUv(srcAspect, targetAspect, 'cover'); + } + }); + }, 3000); + + const emitter = new BABYLON.TransformNode('emitter', this.engine.scene); + emitter.position = new BABYLON.Vector3(0, cm(-1000), 0); + const ps = new BABYLON.ParticleSystem('', 128, this.engine.scene); + ps.particleTexture = new BABYLON.Texture('/client-assets/world/objects/lava-lamp/bubble.png'); + ps.emitter = emitter; + ps.isLocal = true; + ps.minEmitBox = new BABYLON.Vector3(cm(-1000), 0, cm(-1000)); + ps.maxEmitBox = new BABYLON.Vector3(cm(1000), 0, cm(1000)); + ps.minEmitPower = cm(100); + ps.maxEmitPower = cm(500); + ps.minLifeTime = 30; + ps.maxLifeTime = 30; + ps.minSize = cm(30); + ps.maxSize = cm(300); + ps.direction1 = new BABYLON.Vector3(0, 1, 0); + ps.direction2 = new BABYLON.Vector3(0, 1, 0); + ps.emitRate = 1.5; + ps.blendMode = BABYLON.ParticleSystem.BLENDMODE_ADD; + ps.color1 = new BABYLON.Color4(1, 1, 1, 0.3); + ps.color2 = new BABYLON.Color4(1, 1, 1, 0.2); + ps.colorDead = new BABYLON.Color4(1, 1, 1, 0); + ps.preWarmCycles = Math.random() * 1000; + ps.start(); + + this.registerMeshes(this.meshes); + } + + 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 dispose() { + this.timer.dispose(); + for (const m of this.meshes) { + m.dispose(false, true); + } + this.skybox?.dispose(); + this.skyboxMat?.dispose(); + this.envMapIndoor?.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(); + } +} + +class MessageRing { + constructor(mesh: BABYLON.Mesh, scene: BABYLON.Scene, options: { material: BABYLON.StandardMaterial; repeatSeparator: string; }) { + const messageRingRoot = new BABYLON.TransformNode('', this.engine.scene); + const messageRing = this.meshes.find(m => m.name.includes('__MESSAGE_RING_INNER_1__')); + messageRing.parent = messageRingRoot; + messageRing.rotation = messageRing.rotationQuaternion.toEulerAngles(); + messageRing.rotationQuaternion = null; + const text = new RecyvlingTextGrid(messageRing, 64, { + material: this.textMaterial, + repeatSeparator: ' ', + }); + + //messageRingRoot.rotation.x = Math.PI / 4; + + const anim = new BABYLON.Animation('', 'rotation.y', 60, BABYLON.Animation.ANIMATIONTYPE_FLOAT, BABYLON.Animation.ANIMATIONLOOPMODE_CYCLE); + anim.setKeys([ + { frame: 0, value: 0 }, + { frame: 10000, value: (Math.PI * 2) }, + ]); + messageRing.animations = [anim]; + this.engine.scene.beginAnimation(messageRing, 0, 10000, true); + + this.timer.setInterval(() => { + const now = new Date(); + const hours = now.getHours().toString().padStart(2, '0'); + const minutes = now.getMinutes().toString().padStart(2, '0'); + const seconds = now.getSeconds().toString().padStart(2, '0'); + text.write(`${hours}:${minutes}:${seconds}`); + }, 1000); + } +} diff --git a/packages/frontend-misskey-world-engine/src/nonWorker.ts b/packages/frontend-misskey-world-engine/src/nonWorker.ts new file mode 100644 index 0000000000..62a78c75b8 --- /dev/null +++ b/packages/frontend-misskey-world-engine/src/nonWorker.ts @@ -0,0 +1,38 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as BABYLON from '@babylonjs/core/pure.js'; +import { registerBabylonRuntime } from './babylonRuntime.js'; +import { WorldEngine } from './engine.js'; + +registerBabylonRuntime(); + +export async function createWorldEngine(params: { + canvas: HTMLCanvasElement; + options: { + antialias: boolean; + resolution: number; + fov: number; + graphicsQuality: number; + fps: number | null; + useVirtualJoystick?: boolean; + showUsernameOnAvatar: boolean; + show2dAvatarOnAvatar: boolean; + }; +}) { + const babylonEngine = new BABYLON.WebGPUEngine(params.canvas, { doNotHandleContextLost: true, powerPreference: 'high-performance', antialias: params.options.antialias }); + babylonEngine.compatibilityMode = false; + babylonEngine.enableOfflineSupport = false; + await babylonEngine.initAsync(); + if (params.options.resolution === 2) babylonEngine.setHardwareScalingLevel(0.5); + if (params.options.resolution === 0.5) babylonEngine.setHardwareScalingLevel(2); + + const engine = new WorldEngine({ + engine: babylonEngine, + ...params.options, + }); + + return engine; +} diff --git a/packages/frontend-misskey-world-engine/src/room/engine.ts b/packages/frontend-misskey-world-engine/src/room/engine.ts index e86709b75b..94c737fea8 100644 --- a/packages/frontend-misskey-world-engine/src/room/engine.ts +++ b/packages/frontend-misskey-world-engine/src/room/engine.ts @@ -34,7 +34,7 @@ import { CustomMadoriEnvManager } from './envs/customMadori.js'; import { FurnitureContainer } from './FurnitureContainer.js'; import type { FurnitureDef } from './furniture.js'; import type { GridMaterial } from '@babylonjs/materials'; -import type { EnvManager } from './env.js'; +import type { RoomEnvManager } from './env.js'; import type { RoomState_InstalledFurniture } from 'misskey-world/src/room/furniture.js'; import type { RoomAttachments, RoomState } from 'misskey-world/src/room/type.js'; import type { RawOptions } from 'misskey-world/src/mono.js'; @@ -97,7 +97,7 @@ export class RoomEngine extends EngineBase<{ private fov: number; private fixedCamera: BABYLON.FreeCamera; public furnitureContainers: Map = new Map(); - private envManager: EnvManager | null = null; + private envManager: RoomEnvManager | null = null; // TODO: たぶんオブジェクト内の値のmutateはsetで検知できないので、そのような操作を実際に行うようになった & それを検知する必要性が出てきたら専用の設定関数などを新設してそれを使わせる private _grabbingCtx: { @@ -561,7 +561,7 @@ export class RoomEngine extends EngineBase<{ this.pauseRender(); } - let envManager: EnvManager; + let envManager: RoomEnvManager; if (this.roomState.env.type === 'simple') { envManager = new SimpleEnvManager(this); diff --git a/packages/frontend-misskey-world-engine/src/room/env.ts b/packages/frontend-misskey-world-engine/src/room/env.ts index c4309d1222..98e797f125 100644 --- a/packages/frontend-misskey-world-engine/src/room/env.ts +++ b/packages/frontend-misskey-world-engine/src/room/env.ts @@ -5,12 +5,10 @@ */ import * as BABYLON from '@babylonjs/core/pure.js'; -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'; -export abstract class EnvManager { +export abstract class RoomEnvManager { protected engine: RoomEngine; public abstract envMapIndoor: BABYLON.CubeTexture | null; public abstract maxCameraZ: number; diff --git a/packages/frontend-misskey-world-engine/src/room/envs/customMadori.ts b/packages/frontend-misskey-world-engine/src/room/envs/customMadori.ts index 5e224cfa27..3b2b5b23c3 100644 --- a/packages/frontend-misskey-world-engine/src/room/envs/customMadori.ts +++ b/packages/frontend-misskey-world-engine/src/room/envs/customMadori.ts @@ -7,11 +7,11 @@ import * as BABYLON from '@babylonjs/core/pure.js'; 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 { EnvManager } from '../env.js'; +import { RoomEnvManager } from '../env.js'; import type { RoomEngine } from '../engine.js'; import type { CustomMadoriEnvOptions } from 'misskey-world/src/room/env.js'; -export class CustomMadoriEnvManager extends EnvManager { +export class CustomMadoriEnvManager extends RoomEnvManager { private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null; private meshes: BABYLON.Mesh[] = []; private rootNode: BABYLON.TransformNode; diff --git a/packages/frontend-misskey-world-engine/src/room/envs/japanese.ts b/packages/frontend-misskey-world-engine/src/room/envs/japanese.ts index 1b31ac7f25..4915d04c8b 100644 --- a/packages/frontend-misskey-world-engine/src/room/envs/japanese.ts +++ b/packages/frontend-misskey-world-engine/src/room/envs/japanese.ts @@ -7,11 +7,11 @@ import * as BABYLON from '@babylonjs/core/pure.js'; 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 { EnvManager } from '../env.js'; +import { RoomEnvManager } from '../env.js'; import type { RoomEngine } from '../engine.js'; import type { JapaneseEnvOptions } from 'misskey-world/src/room/env.js'; -export class JapaneseEnvManager extends EnvManager { +export class JapaneseEnvManager extends RoomEnvManager { private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null; private meshes: BABYLON.Mesh[] = []; private skybox: BABYLON.Mesh | null = null; diff --git a/packages/frontend-misskey-world-engine/src/room/envs/museum.ts b/packages/frontend-misskey-world-engine/src/room/envs/museum.ts index 602fd148e0..aed8c6d0ed 100644 --- a/packages/frontend-misskey-world-engine/src/room/envs/museum.ts +++ b/packages/frontend-misskey-world-engine/src/room/envs/museum.ts @@ -7,11 +7,11 @@ import * as BABYLON from '@babylonjs/core/pure.js'; 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 { EnvManager } from '../env.js'; +import { RoomEnvManager } from '../env.js'; import type { RoomEngine } from '../engine.js'; import type { MuseumEnvOptions } from 'misskey-world/src/room/env.js'; -export class MuseumEnvManager extends EnvManager { +export class MuseumEnvManager extends RoomEnvManager { private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null; private meshes: BABYLON.Mesh[] = []; private roomLight: BABYLON.DirectionalLight | null = null; diff --git a/packages/frontend-misskey-world-engine/src/room/envs/simple.ts b/packages/frontend-misskey-world-engine/src/room/envs/simple.ts index a5a00f8008..2256b75b63 100644 --- a/packages/frontend-misskey-world-engine/src/room/envs/simple.ts +++ b/packages/frontend-misskey-world-engine/src/room/envs/simple.ts @@ -7,13 +7,13 @@ import * as BABYLON from '@babylonjs/core/pure.js'; import { cm, WORLD_SCALE } from 'misskey-world/src/utility.js'; import { findMaterial, GRAPHICS_QUALITY, treeClone } from '../../utility.js'; import { SYSTEM_HEYA_MESH_NAMES } from '../utility.js'; -import { EnvManager } from '../env.js'; +import { RoomEnvManager } from '../env.js'; import type { RoomEngine } from '../engine.js'; import type { SimpleEnvOptions } from 'misskey-world/src/room/env.js'; // TODO: マテリアルは必要になるまで作成しないようにする -export class SimpleEnvManager extends EnvManager { +export class SimpleEnvManager extends RoomEnvManager { private loaderResult: BABYLON.ISceneLoaderAsyncResult | null = null; private rootNode: BABYLON.TransformNode; private wallRoots: Record<'zPositive' | 'zNegative' | 'xPositive' | 'xNegative', BABYLON.TransformNode>; diff --git a/packages/frontend-misskey-world-engine/src/worker.ts b/packages/frontend-misskey-world-engine/src/worker.ts new file mode 100644 index 0000000000..0208b10b49 --- /dev/null +++ b/packages/frontend-misskey-world-engine/src/worker.ts @@ -0,0 +1,108 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as BABYLON from '@babylonjs/core/pure.js'; +import { registerBabylonRuntime } from './babylonRuntime.js'; +import { WorldEngine } from './engine.js'; + +registerBabylonRuntime(); + +let engine: WorldEngine | null = null; +let canvas: OffscreenCanvas | null = null; + +// TODO: 他のWorkerと実装を共通化 +onmessage = async (event) => { + //console.log('Worker received message:', event.data); + + switch (event.data?.type) { + case 'init': { + canvas = event.data.canvas as OffscreenCanvas; + const babylonEngine = new BABYLON.WebGPUEngine(canvas, { doNotHandleContextLost: true, powerPreference: 'high-performance', antialias: event.data.options.antialias }); + babylonEngine.compatibilityMode = false; + babylonEngine.enableOfflineSupport = false; + await babylonEngine.initAsync(); + if (event.data.options.resolution === 2) babylonEngine.setHardwareScalingLevel(0.5); + if (event.data.options.resolution === 0.5) babylonEngine.setHardwareScalingLevel(2); + + engine = new WorldEngine({ + engine: babylonEngine, + ...event.data.options, + }); + + engine.on('ev', ({ type, ctx }) => { + self.postMessage({ type: 'ev', ev: { type, ctx } }); + }); + + await engine.init(); + + self.postMessage({ type: 'inited' }); + break; + } + case 'resize': { + canvas.width = event.data.width; + canvas.height = event.data.height; + if (engine != null) engine.resize(); + break; + } + case 'input:keydown': { + if (engine == null) break; + engine.inputs.emit('keydown', event.data.ev); + break; + } + case 'input:keyup': { + if (engine == null) break; + engine.inputs.emit('keyup', event.data.ev); + break; + } + case 'input:click': { + if (engine == null) break; + engine.inputs.emit('click', event.data.ev); + break; + } + case 'input:wheel': { + if (engine == null) break; + engine.inputs.emit('wheel', event.data.ev); + break; + } + case 'input:zoom': { + if (engine == null) break; + engine.inputs.emit('zoom', event.data.ev); + break; + } + case 'input:pointer': { + if (engine == null) break; + engine.inputs.emit('pointer', event.data.ev); + break; + } + case 'call': { + if (engine == null) { + console.error('Failed to call: Engine is not initialized yet!!!'); + break; + } + const res = engine[event.data.fn](...(event.data.args ?? [])); + if (event.data.needReturnValue) { + if (res instanceof Promise) { + res.then((r) => { + self.postMessage({ type: 'return', id: event.data.id, value: r }); + }); + } else { + self.postMessage({ type: 'return', id: event.data.id, value: res }); + } + } + break; + } + case 'set': { + if (engine == null) { + console.error('Failed to set: Engine is not initialized yet!!!'); + break; + } + engine[event.data.key] = event.data.value; + break; + } + default: { + console.warn('Unrecognized message type:', event.data?.type); + } + } +}; diff --git a/packages/frontend/assets/world/lobby/dummy-ads/1.png b/packages/frontend/assets/world/envs/lobby/dummy-ads/1.png similarity index 100% rename from packages/frontend/assets/world/lobby/dummy-ads/1.png rename to packages/frontend/assets/world/envs/lobby/dummy-ads/1.png diff --git a/packages/frontend/assets/world/lobby/dummy-ads/2.png b/packages/frontend/assets/world/envs/lobby/dummy-ads/2.png similarity index 100% rename from packages/frontend/assets/world/lobby/dummy-ads/2.png rename to packages/frontend/assets/world/envs/lobby/dummy-ads/2.png diff --git a/packages/frontend/assets/world/lobby/dummy-ads/3.png b/packages/frontend/assets/world/envs/lobby/dummy-ads/3.png similarity index 100% rename from packages/frontend/assets/world/lobby/dummy-ads/3.png rename to packages/frontend/assets/world/envs/lobby/dummy-ads/3.png diff --git a/packages/frontend/assets/world/lobby/dummy-ads/4.png b/packages/frontend/assets/world/envs/lobby/dummy-ads/4.png similarity index 100% rename from packages/frontend/assets/world/lobby/dummy-ads/4.png rename to packages/frontend/assets/world/envs/lobby/dummy-ads/4.png diff --git a/packages/frontend/assets/world/lobby/dummy-ads/angry_ai.png b/packages/frontend/assets/world/envs/lobby/dummy-ads/angry_ai.png similarity index 100% rename from packages/frontend/assets/world/lobby/dummy-ads/angry_ai.png rename to packages/frontend/assets/world/envs/lobby/dummy-ads/angry_ai.png diff --git a/packages/frontend/assets/world/lobby/default.blend b/packages/frontend/assets/world/envs/lobby/lobby.blend similarity index 100% rename from packages/frontend/assets/world/lobby/default.blend rename to packages/frontend/assets/world/envs/lobby/lobby.blend diff --git a/packages/frontend/assets/world/lobby/default.glb b/packages/frontend/assets/world/envs/lobby/lobby.glb similarity index 100% rename from packages/frontend/assets/world/lobby/default.glb rename to packages/frontend/assets/world/envs/lobby/lobby.glb diff --git a/packages/frontend/src/pages/rooms/room.core.vue b/packages/frontend/src/pages/rooms/room.core.vue index 30cddbf55c..0f412dccd3 100644 --- a/packages/frontend/src/pages/rooms/room.core.vue +++ b/packages/frontend/src/pages/rooms/room.core.vue @@ -189,7 +189,7 @@ import { prefer } from '@/preferences.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { miLocalStorage } from '@/local-storage.js'; import { FURNITURE_UI_DEFS } from '@/world/room/furniture-ui-defs.js'; -import { Multiplayer } from '@/world/room/multiplayer.js'; +import { RoomMultiplayer } from '@/world/room/multiplayer.js'; import { $i } from '@/i.js'; import { userPage } from '@/filters/user.js'; import MkUserCardMini from '@/components/MkUserCardMini.vue'; @@ -340,7 +340,7 @@ const roomControllerOptions = computed(() => ({ })); const controller = markRaw(new RoomController(deepClone(initialRoomState), roomControllerOptions.value)); -const multiplayer = markRaw(new Multiplayer(props.room.id, controller)); +const multiplayer = markRaw(new RoomMultiplayer(props.room.id, controller)); watch(controller.roomState, () => { controller.roomState.value.worldScale = WORLD_SCALE; diff --git a/packages/frontend/src/pages/world.vue b/packages/frontend/src/pages/world.vue index 6f0d3ff5db..9f5f5c40b6 100644 --- a/packages/frontend/src/pages/world.vue +++ b/packages/frontend/src/pages/world.vue @@ -4,36 +4,73 @@ SPDX-License-Identifier: AGPL-3.0-only -->