1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-13 17:35:40 +02:00
Files
misskey/packages/frontend/src/world/engine.ts
syuilo a85f05ca29 wip
2026-04-18 20:58:52 +09:00

611 lines
22 KiB
TypeScript

/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as BABYLON from '@babylonjs/core';
import { AxesViewer } from '@babylonjs/core/Debug/axesViewer';
import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic';
import { EventEmitter } from 'eventemitter3';
import tinycolor from 'tinycolor2';
import { HorizontalCameraKeyboardMoveInput, RecyvlingTextGrid, WORLD_SCALE, camelToKebab, cm, createPlaneUvMapper, normalizeUvToSquare, randomRange } from './utility.js';
import { TIME_MAP } from './utility.js';
import { genId } from '@/utility/id.js';
import { deepClone } from '@/utility/clone.js';
const SNAPSHOT_RENDERING = false; // 実験的
const USE_GLOW = true; // ドローコールが増えて重い
const IN_WEB_WORKER = typeof window === 'undefined';
export type WorldEngineEvents = {
'playSfxUrl': (ctx: {
url: string;
options: {
volume: number;
playbackRate: number;
};
}) => void;
'loadingProgress': (ctx: { progress: number }) => void;
};
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;
public intervalIds: number[] = [];
public timeoutIds: number[] = [];
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;
public textMaterial: BABYLON.StandardMaterial;
public isSitting = false;
private fps: number | null = null;
private disposed = false;
public domEvents: 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;
registerBuiltInLoaders();
this.engine = options.engine;
this.scene = new BABYLON.Scene(this.engine);
this.scene.autoClear = false;
//this.scene.autoClearDepthAndStencil = false;
this.scene.skipPointerMovePicking = true;
this.scene.skipFrustumClipping = true; // snapshot renderingでは全てのメッシュがアクティブになっている必要があるため
this.sr = new BABYLON.SnapshotRenderingHelper(this.scene);
const skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(50000) }, this.scene);
const skyboxMat = new BABYLON.StandardMaterial('skyboxMat', this.scene);
skyboxMat.backFaceCulling = false;
skyboxMat.disableLighting = true;
skybox.material = skyboxMat;
skybox.infiniteDistance = true;
this.time = TIME_MAP[new Date().getHours() as keyof typeof TIME_MAP];
//this.time = TIME_MAP[12 as keyof typeof TIME_MAP];
if (this.time === 0) {
skyboxMat.emissiveColor = new BABYLON.Color3(1, 1, 1);
} else if (this.time === 1) {
skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.68, 0.66);
} else {
skyboxMat.emissiveColor = new BABYLON.Color3(0.48, 0.5, 0.6);
}
this.scene.ambientColor = new BABYLON.Color3(0.9, 0.9, 0.9);
this.envMap = BABYLON.CubeTexture.CreateFromPrefilteredData(this.time === 2 ? '/client-assets/room/outdoor-night.env' : '/client-assets/room/outdoor-day.env', this.scene);
//this.envMap.level = 0.3;
this.envMap.level = 0;
this.scene.collisionsEnabled = true;
this.camera = new BABYLON.UniversalCamera('camera', new BABYLON.Vector3(cm(0), cm(200), cm(3000)), this.scene);
this.camera.inputs.removeByType('FreeCameraKeyboardMoveInput');
this.camera.inputs.add(new HorizontalCameraKeyboardMoveInput(this.camera, 0.3));
this.camera.attachControl(this.canvas);
this.camera.minZ = cm(1);
this.camera.maxZ = cm(100000);
this.camera.fov = 1;
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.scene.activeCamera = this.camera;
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.lightContainer = new BABYLON.ClusteredLightContainer('clustered', [], this.scene);
if (USE_GLOW) {
this.gl = new BABYLON.GlowLayer('glow', this.scene, {
//mainTextureFixedSize: 512,
blurKernelSize: 64,
});
this.gl.intensity = 0.5;
this.gl.addExcludedMesh(skybox);
this.scene.setRenderingAutoClearDepthStencil(this.gl.renderingGroupId, false);
if (SNAPSHOT_RENDERING) {
this.sr.updateMeshesForEffectLayer(this.gl);
}
}
if (_DEV_) {
// snapshot renderingかつglow layerが有効だとなんかクラッシュする
if (!(SNAPSHOT_RENDERING && USE_GLOW)) {
const axes = new AxesViewer(this.scene, 30);
axes.xAxis.position = new BABYLON.Vector3(0, 30, 0);
axes.yAxis.position = new BABYLON.Vector3(0, 30, 0);
axes.zAxis.position = new BABYLON.Vector3(0, 30, 0);
}
if (!IN_WEB_WORKER) {
(window as any).showBabylonInspector = () => {
import('@babylonjs/inspector').then(({ ShowInspector }) => {
ShowInspector(this.scene);
});
};
}
}
}
public async init() {
await this.loadEnvModel();
if (SNAPSHOT_RENDERING) {
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.domEvents.on('keydown', (ev) => {
});
this.domEvents.on('wheel', (ev) => {
this.camera.fov += ev.deltaY * 0.001;
this.camera.fov = Math.max(0.25, Math.min(1, this.camera.fov));
});
this.domEvents.on('click', (ev) => {
});
}
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;
mesh.checkCollisions = true;
if (mesh.material) (mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.envMap;
}
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();
{
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',
'MAINTENANCE will begin at 9:00 A.M.',
];
let currentTextIndex = 1;
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.textMaterial,
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);
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);
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);
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);
}
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);
}
//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/${1 + Math.floor(Math.random() * 4)}.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.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;
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 = envObj.meshes.filter(m => m.name.includes('__SCREEN__'));
const screenMaterial = screenMeshes[0].material as BABYLON.PBRMaterial;
setTimeout(() => {
const tex = new BABYLON.VideoTexture('', 'http://syu-win.local:3000/files/931c02c3-6238-4c29-9371-06bab78950bb', 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');
}
});
}, 3000);
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/room/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 = 100;
ps.maxEmitPower = 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();
}
public sitChair(objectId: string) {
this.isSitting = true;
this.fixedCamera.parent = this.objectMeshs.get(objectId);
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.selectObject(null);
}
public standUp() {
this.isSitting = false;
this.scene.activeCamera = this.camera;
this.fixedCamera.parent = null;
}
private playSfxUrl(url: string, options: { volume: number; playbackRate: number }) {
this.emit('playSfxUrl', { url, options });
}
public resize() {
this.engine.resize();
}
public destroy() {
for (const id of this.intervalIds) {
window.clearInterval(id);
}
for (const id of this.timeoutIds) {
window.clearTimeout(id);
}
this.intervalIds = [];
this.timeoutIds = [];
this.engine.dispose();
this.disposed = true;
}
}
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);
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);
}
}