diff --git a/packages/frontend/src/world/EngineBase.ts b/packages/frontend/src/world/EngineBase.ts new file mode 100644 index 0000000000..e54e82e46e --- /dev/null +++ b/packages/frontend/src/world/EngineBase.ts @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as BABYLON from '@babylonjs/core'; +import EventEmitter from 'eventemitter3'; + +export type EngineBaseEvents = { + 'loadingProgress': (ctx: { progress: number }) => void; +}; + +export abstract class EngineBase extends EventEmitter<{ + 'ev': (ctx: { type: keyof EVs; ctx: Parameters[0] }) => void; +}> { + protected engine: BABYLON.WebGPUEngine; + protected scene: BABYLON.Scene; + protected fps: number | null = null; + protected disposed = false; + + public inputs: EventEmitter<{ + 'click': (event: { x: number; y: number; }) => void; + 'keydown': (event: { code: string; shiftKey: boolean; }) => void; + 'keyup': (event: { code: string; shiftKey: boolean; }) => void; + 'wheel': (event: { deltaY: number; }) => void; + 'zoom': (event: { delta: number; }) => void; + 'pointer': (event: { x: number; y: number; }) => void; + }> = new EventEmitter(); + + constructor(options: { + engine: BABYLON.WebGPUEngine; + fps: number | null; + }) { + super(); + + this.fps = options.fps; + + this.engine = options.engine; + this.scene = new BABYLON.Scene(this.engine); + } + + private currentRafId: number | null = null; + + private startRenderLoop() { + if (this.fps == null) { + this.engine.runRenderLoop(() => { + this.scene.render(); + }); + } else { + let then = 0; + const interval = 1000 / this.fps; + + const renderLoop = (timeStamp: number) => { + if (this.disposed) return; + + // workerで実行される可能性がある + this.currentRafId = requestAnimationFrame(renderLoop); + + const delta = timeStamp - then; + if (delta <= interval) return; + then = timeStamp - (delta % interval); + + this.engine.beginFrame(); + this.scene.render(); + this.engine.endFrame(); + }; + + // workerで実行される可能性がある + this.currentRafId = requestAnimationFrame(renderLoop); + } + } + + public pauseRender() { // TODO: srと同じく参照カウント方式にした方が便利そう + this.engine.stopRenderLoop(); + if (this.currentRafId != null) { + // workerで実行される可能性がある + cancelAnimationFrame(this.currentRafId); + this.currentRafId = null; + } + } + + public resumeRender() { + this.startRenderLoop(); + } + + public abstract init(): Promise; + + protected ev(type: K, ctx: Parameters[0]) { + this.emit('ev', { type, ctx }); + } + + public abstract resize(): void; + + public destroy() { + this.engine.stopRenderLoop(); + if (this.currentRafId != null) { + // workerで実行される可能性がある + cancelAnimationFrame(this.currentRafId); + this.currentRafId = null; + } + this.engine.dispose(); + this.scene.dispose(); + this.disposed = true; + } +} diff --git a/packages/frontend/src/world/controller.ts b/packages/frontend/src/world/controller.ts index e278f843ab..0c75c8ccb5 100644 --- a/packages/frontend/src/world/controller.ts +++ b/packages/frontend/src/world/controller.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { EngineControllerBase } from './engineControllerBase.js'; +import { EngineControllerBase } from './EngineControllerBase.js'; +import type { WorldEngine } from './engine.js'; export type WorldEngineControllerOptions = { workerMode?: boolean; @@ -14,7 +15,7 @@ export type WorldEngineControllerOptions = { }; // 抽象化レイヤー -export class WorldEngineController extends EngineControllerBase { +export class WorldEngineController extends EngineControllerBase { constructor(options: WorldEngineControllerOptions) { super({ ...options, diff --git a/packages/frontend/src/world/engine.ts b/packages/frontend/src/world/engine.ts index 0b78490a50..aea723df22 100644 --- a/packages/frontend/src/world/engine.ts +++ b/packages/frontend/src/world/engine.ts @@ -11,6 +11,7 @@ import tinycolor from 'tinycolor2'; import Hls from 'hls.js'; import { RecyvlingTextGrid, Timer, WORLD_SCALE, camelToKebab, cm, createPlaneUvMapper, normalizeUvToSquare, randomRange } from './utility.js'; import { TIME_MAP } from './utility.js'; +import { EngineBase } from './EngineBase.js'; import { genId } from '@/utility/id.js'; import { deepClone } from '@/utility/clone.js'; @@ -18,7 +19,7 @@ const SNAPSHOT_RENDERING = false; // 実験的 const USE_GLOW = true; // ドローコールが増えて重い const IN_WEB_WORKER = typeof window === 'undefined'; -export type WorldEngineEvents = { +export class WorldEngine extends EngineBase<{ 'playSfxUrl': (ctx: { url: string; options: { @@ -27,13 +28,7 @@ export type WorldEngineEvents = { }; }) => void; 'loadingProgress': (ctx: { progress: number }) => void; -}; - -// TODO: RoomEngineBaseとしてabstract classを抽出 -export class WorldEngine extends EventEmitter { - private canvas: HTMLCanvasElement; - private engine: BABYLON.WebGPUEngine; - public scene: BABYLON.Scene; +}> { private shadowGeneratorForSunLight: BABYLON.ShadowGenerator; public camera: BABYLON.UniversalCamera; private time: 0 | 1 | 2 = 0; // 0: 昼, 1: 夕, 2: 夜 @@ -45,30 +40,19 @@ export class WorldEngine extends EventEmitter { private translucentTextMaterial: BABYLON.StandardMaterial; private reflectionProbe: BABYLON.ReflectionProbe; public timer: Timer = new Timer(); - public isSitting = false; - private fps: number | null = null; - private disposed = false; - - public inputs: EventEmitter<{ - 'click': (event: { offsetX: number; offsetY: number; }) => void; - 'keydown': (event: { code: string; shiftKey: boolean; }) => void; - 'keyup': (event: { code: string; shiftKey: boolean; }) => void; - 'wheel': (event: { deltaY: number; }) => void; - }> = new EventEmitter(); constructor(options: { canvas: HTMLCanvasElement; engine: BABYLON.WebGPUEngine; }) { - super(); - - this.canvas = options.canvas; + super({ + engine: options.engine, + fps: null, + }); registerBuiltInLoaders(); - this.engine = options.engine; - this.scene = new BABYLON.Scene(this.engine); this.scene.autoClear = false; //this.scene.autoClearDepthAndStencil = false; this.scene.skipPointerMovePicking = true; @@ -206,31 +190,6 @@ export class WorldEngine extends EventEmitter { this.sr.enableSnapshotRendering(); } - if (this.fps == null) { - this.engine.runRenderLoop(() => { - this.scene.render(); - }); - } else { - let then = 0; - const interval = 1000 / this.fps; - - const renderLoop = (timeStamp: number) => { - if (this.disposed) return; - - window.requestAnimationFrame(renderLoop); - - const delta = timeStamp - then; - if (delta <= interval) return; - then = timeStamp - (delta % interval); - - this.engine.beginFrame(); - this.scene.render(); - this.engine.endFrame(); - }; - - window.requestAnimationFrame(renderLoop); - } - this.inputs.on('keydown', (ev) => { }); @@ -621,9 +580,8 @@ export class WorldEngine extends EventEmitter { } public destroy() { + super.destroy(); this.timer.dispose(); - this.engine.dispose(); - this.disposed = true; } } diff --git a/packages/frontend/src/world/engineControllerBase.ts b/packages/frontend/src/world/engineControllerBase.ts index c0404d4212..045aa53fcc 100644 --- a/packages/frontend/src/world/engineControllerBase.ts +++ b/packages/frontend/src/world/engineControllerBase.ts @@ -6,6 +6,7 @@ import { reactive, ref, shallowRef, triggerRef, watch } from 'vue'; import * as BABYLON from '@babylonjs/core'; import { EventEmitter } from 'eventemitter3'; +import type { EngineBase, EngineBaseEvents } from './EngineBase.js'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; @@ -17,9 +18,12 @@ export type EngineControllerBaseOptions = { antialias: boolean; }; +type EngineEventsOf = T extends EngineBase ? X : EngineBaseEvents; + // UIとエンジンの間に挟まり抽象化を行うレイヤー。 // UIからは、エンジンが直で動いててもワーカーで動いてても同じように操作できるように見える -export abstract class EngineControllerBase { +// infer EVs from T +export abstract class EngineControllerBase> { private worker: Worker | null = null; private engine: T | null = null; private canvas: HTMLCanvasElement | null = null; @@ -45,7 +49,7 @@ export abstract class EngineControllerBase { this.canvas.width = canvas.clientWidth > 4 ? canvas.clientWidth : 4; this.canvas.height = canvas.clientHeight > 4 ? canvas.clientHeight : 4; - const engineEvents = new EventEmitter(); + const engineEvents = new EventEmitter>(); engineEvents.on('loadingProgress', ({ progress }) => { this.initializeProgress.value = progress; diff --git a/packages/frontend/src/world/room/controller.ts b/packages/frontend/src/world/room/controller.ts index 28b27991c1..e789419ca8 100644 --- a/packages/frontend/src/world/room/controller.ts +++ b/packages/frontend/src/world/room/controller.ts @@ -5,7 +5,7 @@ import { ref, shallowRef } from 'vue'; import { cm } from '../utility.js'; -import { EngineControllerBase } from '../engineControllerBase.js'; +import { EngineControllerBase } from '../EngineControllerBase.js'; import type { ShallowRef } from 'vue'; import type { RoomEngine, RoomState } from './engine.js'; import type { RoomStateObject } from './object.js'; diff --git a/packages/frontend/src/world/room/engine.ts b/packages/frontend/src/world/room/engine.ts index 2c429db954..f48b750af3 100644 --- a/packages/frontend/src/world/room/engine.ts +++ b/packages/frontend/src/world/room/engine.ts @@ -14,6 +14,7 @@ import * as BABYLON from '@babylonjs/core'; import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic'; import { EventEmitter } from 'eventemitter3'; import { TIME_MAP, scaleMorph, camelToKebab, cm, WORLD_SCALE, getMeshesBoundingBox, Timer, getYRotationDirection, FreeCameraManualInput, remap } from '../utility.js'; +import { EngineBase } from '../EngineBase.js'; import { getObjectDef } from './object-defs.js'; import { findMaterial, GRAPHICS_QUALITY, ModelManager, SYSTEM_HEYA_MESH_NAMES, SYSTEM_MESH_NAMES } from './utility.js'; import { JapaneseEnvManager, MuseumEnvManager, SimpleEnvManager } from './env.js'; @@ -111,7 +112,7 @@ function enableObjectCollision(meshes: BABYLON.Mesh[]) { } } -export type RoomEngineEvents = { +export class RoomEngine extends EngineBase<{ 'changeSelectedState': (ctx: { selected: { objectId: string; @@ -130,13 +131,8 @@ export type RoomEngineEvents = { }; }) => void; 'loadingProgress': (ctx: { progress: number }) => void; -}; - -// TODO: RoomEngineBaseとしてabstract classを抽出 -export class RoomEngine extends EventEmitter { +}> { private useGlow: boolean; - private engine: BABYLON.WebGPUEngine; - public scene: BABYLON.Scene; public camera: BABYLON.UniversalCamera; private fixedCamera: BABYLON.FreeCamera; public objectEntities: Map void; - 'keydown': (event: { code: string; shiftKey: boolean; }) => void; - 'keyup': (event: { code: string; shiftKey: boolean; }) => void; - 'wheel': (event: { deltaY: number; }) => void; - 'zoom': (event: { delta: number; }) => void; - 'pointer': (event: { x: number; y: number; }) => void; - }> = new EventEmitter(); constructor(roomState: RoomState, roomAttachments: RoomAttachments, options: { engine: BABYLON.WebGPUEngine; @@ -242,7 +227,10 @@ export class RoomEngine extends EventEmitter { antialias: boolean; useVirtualJoystick?: boolean; }) { - super(); + super({ + engine: options.engine, + fps: options.fps, + }); this.roomState = { ...deepClone(roomState), @@ -253,14 +241,11 @@ export class RoomEngine extends EventEmitter { }; this.roomAttachments = roomAttachments; this.graphicsQuality = options.graphicsQuality; - this.fps = options.fps; this.useGlow = this.graphicsQuality >= GRAPHICS_QUALITY.MEDIUM; this.time = TIME_MAP[new Date().getHours() as keyof typeof TIME_MAP]; registerBuiltInLoaders(); - this.engine = options.engine; - this.scene = new BABYLON.Scene(this.engine); // なんかレンダリングがおかしくなるときがあるのでコメントアウト // オブジェクトを選択し、後ろを向いて別のオブジェクトを選択した後、最初のオブジェクトに振り返ると消えているなど //this.scene.performancePriority = BABYLON.ScenePerformancePriority.Intermediate; @@ -399,10 +384,6 @@ export class RoomEngine extends EventEmitter { } } - private ev(type: K, ctx: Parameters[0]) { - this.emit('ev', { type, ctx }); - } - public async init() { await this.loadEnv(); @@ -546,50 +527,6 @@ export class RoomEngine extends EventEmitter { this.inited = true; } - private currentRafId: number | null = null; - - private startRenderLoop() { - if (this.fps == null) { - this.engine.runRenderLoop(() => { - this.scene.render(); - }); - } else { - let then = 0; - const interval = 1000 / this.fps; - - const renderLoop = (timeStamp: number) => { - if (this.disposed) return; - - // workerで実行される可能性がある - this.currentRafId = requestAnimationFrame(renderLoop); - - const delta = timeStamp - then; - if (delta <= interval) return; - then = timeStamp - (delta % interval); - - this.engine.beginFrame(); - this.scene.render(); - this.engine.endFrame(); - }; - - // workerで実行される可能性がある - this.currentRafId = requestAnimationFrame(renderLoop); - } - } - - public pauseRender() { // TODO: srと同じく参照カウント方式にした方が便利そう - this.engine.stopRenderLoop(); - if (this.currentRafId != null) { - // workerで実行される可能性がある - cancelAnimationFrame(this.currentRafId); - this.currentRafId = null; - } - } - - public resumeRender() { - this.startRenderLoop(); - } - // TODO: 初回以外で呼び出すとエンジンがクラッシュするのを修正 public async changeEnvType(type: RoomState['env']['type'], forInit = false) { this.roomState.env.type = type; @@ -1778,16 +1715,8 @@ export class RoomEngine extends EventEmitter { } public destroy() { - this.engine.stopRenderLoop(); - if (this.currentRafId != null) { - // workerで実行される可能性がある - cancelAnimationFrame(this.currentRafId); - this.currentRafId = null; - } + super.destroy(); this.timer.dispose(); this.envManager.dispose(); - this.engine.dispose(); - this.scene.dispose(); - this.disposed = true; } } diff --git a/packages/frontend/src/world/room/previewEngine.ts b/packages/frontend/src/world/room/previewEngine.ts index c7e553a307..efc377ec16 100644 --- a/packages/frontend/src/world/room/previewEngine.ts +++ b/packages/frontend/src/world/room/previewEngine.ts @@ -6,8 +6,8 @@ import * as BABYLON from '@babylonjs/core'; import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic.js'; import { GridMaterial } from '@babylonjs/materials'; -import EventEmitter from 'eventemitter3'; import { camelToKebab, WORLD_SCALE, cm, getMeshesBoundingBox, Timer, sleep, ArcRotateCameraManualInput } from '../utility.js'; +import { EngineBase } from '../EngineBase.js'; import { getObjectDef } from './object-defs.js'; import { SYSTEM_MESH_NAMES, ModelManager, GRAPHICS_QUALITY } from './utility.js'; import { convertRawOptions } from './object.js'; @@ -16,10 +16,9 @@ import type { RoomAttachments } from './utility.js'; import { genId } from '@/utility/id.js'; import { deepClone } from '@/utility/clone.js'; -// TODO: RoomEngineBaseとしてabstract classを抽出 -export class RoomObjectPreviewEngine extends EventEmitter { - private engine: BABYLON.WebGPUEngine; - private scene: BABYLON.Scene; +export class RoomObjectPreviewEngine extends EngineBase<{ + 'loadingProgress': (ctx: { progress: number }) => void; +}> { private sr: BABYLON.SnapshotRenderingHelper; private shadowGenerator: BABYLON.ShadowGenerator; private camera: BABYLON.ArcRotateCamera; @@ -34,32 +33,21 @@ export class RoomObjectPreviewEngine extends EventEmitter { private timerForEachObject: Timer | null = null; private pipeline: BABYLON.DefaultRenderingPipeline; private graphicsQuality: number; - private fps: number | null = null; - private disposed = false; - - public inputs: EventEmitter<{ - 'click': (event: { x: number; y: number; }) => void; - 'keydown': (event: { code: string; shiftKey: boolean; }) => void; - 'keyup': (event: { code: string; shiftKey: boolean; }) => void; - 'wheel': (event: { deltaY: number; }) => void; - 'zoom': (event: { delta: number; }) => void; - 'pointer': (event: { x: number; y: number; }) => void; - }> = new EventEmitter(); constructor(options: { engine: BABYLON.WebGPUEngine; graphicsQuality: number; fps: number | null; }) { - super(); + super({ + engine: options.engine, + fps: options.fps, + }); registerBuiltInLoaders(); this.graphicsQuality = options.graphicsQuality; - this.fps = options.fps; - this.engine = options.engine; - this.scene = new BABYLON.Scene(this.engine); this.scene.autoClear = false; this.scene.skipPointerMovePicking = true; this.scene.skipFrustumClipping = true; // snapshot renderingでは全てのメッシュがアクティブになっている必要があるため @@ -158,50 +146,6 @@ export class RoomObjectPreviewEngine extends EventEmitter { } } - private currentRafId: number | null = null; - - private startRenderLoop() { - if (this.fps == null) { - this.engine.runRenderLoop(() => { - this.scene.render(); - }); - } else { - let then = 0; - const interval = 1000 / this.fps; - - const renderLoop = (timeStamp: number) => { - if (this.disposed) return; - - // workerで実行される可能性がある - this.currentRafId = requestAnimationFrame(renderLoop); - - const delta = timeStamp - then; - if (delta <= interval) return; - then = timeStamp - (delta % interval); - - this.engine.beginFrame(); - this.scene.render(); - this.engine.endFrame(); - }; - - // workerで実行される可能性がある - this.currentRafId = requestAnimationFrame(renderLoop); - } - } - - public pauseRender() { // TODO: srと同じく参照カウント方式にした方が便利そう - this.engine.stopRenderLoop(); - if (this.currentRafId != null) { - // workerで実行される可能性がある - cancelAnimationFrame(this.currentRafId); - this.currentRafId = null; - } - } - - public resumeRender() { - this.startRenderLoop(); - } - public async init() { await this.scene.whenReadyAsync(); this.sr.enableSnapshotRendering(); @@ -428,17 +372,9 @@ export class RoomObjectPreviewEngine extends EventEmitter { } public destroy() { - this.engine.stopRenderLoop(); - if (this.currentRafId != null) { - // workerで実行される可能性がある - cancelAnimationFrame(this.currentRafId); - this.currentRafId = null; - } + super.destroy(); if (this.timerForEachObject != null) { this.timerForEachObject.dispose(); } - this.engine.dispose(); - this.scene.dispose(); - this.disposed = true; } } diff --git a/packages/frontend/src/world/room/previewEngineController.ts b/packages/frontend/src/world/room/previewEngineController.ts index 9620d757c7..5777d3e4c5 100644 --- a/packages/frontend/src/world/room/previewEngineController.ts +++ b/packages/frontend/src/world/room/previewEngineController.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { EngineControllerBase } from '../engineControllerBase.js'; +import { EngineControllerBase } from '../EngineControllerBase.js'; import type { RoomObjectPreviewEngine } from './previewEngine.js'; import type { RoomAttachments } from './utility.js';