This commit is contained in:
syuilo 2026-05-19 13:15:20 +09:00
commit 646b0ca041
8 changed files with 141 additions and 208 deletions

View file

@ -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<EVs extends EngineBaseEvents> extends EventEmitter<{
'ev': (ctx: { type: keyof EVs; ctx: Parameters<EVs[keyof EVs]>[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<void>;
protected ev<K extends keyof EVs>(type: K, ctx: Parameters<EVs[K]>[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;
}
}

View file

@ -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<WorldEngine> {
constructor(options: WorldEngineControllerOptions) {
super({
...options,

View file

@ -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<WorldEngineEvents> {
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<WorldEngineEvents> {
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<WorldEngineEvents> {
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<WorldEngineEvents> {
}
public destroy() {
super.destroy();
this.timer.dispose();
this.engine.dispose();
this.disposed = true;
}
}

View file

@ -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> = T extends EngineBase<infer X> ? X : EngineBaseEvents;
// UIとエンジンの間に挟まり抽象化を行うレイヤー。
// UIからは、エンジンが直で動いててもワーカーで動いてても同じように操作できるように見える
export abstract class EngineControllerBase<T extends RoomEngineBase> {
// infer EVs from T
export abstract class EngineControllerBase<T extends EngineBase<EngineBaseEvents>> {
private worker: Worker | null = null;
private engine: T | null = null;
private canvas: HTMLCanvasElement | null = null;
@ -45,7 +49,7 @@ export abstract class EngineControllerBase<T extends RoomEngineBase> {
this.canvas.width = canvas.clientWidth > 4 ? canvas.clientWidth : 4;
this.canvas.height = canvas.clientHeight > 4 ? canvas.clientHeight : 4;
const engineEvents = new EventEmitter<RoomEngineBaseEvents>();
const engineEvents = new EventEmitter<EngineEventsOf<T>>();
engineEvents.on('loadingProgress', ({ progress }) => {
this.initializeProgress.value = progress;

View file

@ -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';

View file

@ -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<string, {
@ -223,17 +219,6 @@ export class RoomEngine extends EventEmitter {
public isSitting = false;
private inited = false;
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(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<K extends keyof RoomEngineEvents>(type: K, ctx: Parameters<RoomEngineEvents[K]>[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;
}
}

View file

@ -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;
}
}

View file

@ -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';