1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-20 15:15:37 +02:00
This commit is contained in:
syuilo
2026-04-17 17:53:11 +09:00
parent 02c6e1b876
commit 623b4f087f
12 changed files with 302 additions and 316 deletions

View File

@@ -23,60 +23,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="!isZenMode">
<div v-if="controller.isReady.value" class="_buttonsCenter" :class="$style.overlayControls">
<template v-if="controller.isEditMode.value">
<MkButton v-if="controller.grabbing.value && !controller.grabbing.value.forInstall" @click="endGrabbing"><i class="ti ti-check"></i> (E)</MkButton>
<MkButton v-else-if="controller.grabbing.value && controller.grabbing.value.forInstall" @click="endGrabbing"><i class="ti ti-check"></i> (E)</MkButton>
<MkButton v-else-if="controller.selected.value != null" @click="beginSelectedInstalledObjectGrabbing"><i class="ti ti-hand-grab"></i> (E)</MkButton>
<MkButton v-if="controller.grabbing.value" @click="rotate"><i class="ti ti-view-360-arrow"></i> (R)</MkButton>
<MkButton :primary="controller.gridSnapping.value.enabled" @click="showSnappingMenu">Grid Snap: {{ controller.gridSnapping.value.enabled ? 'on' : 'off' }}</MkButton>
<MkButton v-if="!controller.grabbing.value && controller.selected.value != null" @click="removeSelectedObject"><i class="ti ti-trash"></i> (X)</MkButton>
</template>
<MkButton v-if="controller.isSitting.value" @click="controller.standUp()">降りる (Q)</MkButton>
<template v-for="interaction in interacions" :key="interaction.id">
<MkButton inline @click="interaction.fn()">{{ interaction.label }}{{ interaction.isPrimary ? ' (E)' : '' }}</MkButton>
</template>
</div>
<div v-if="controller.isReady.value && controller.isEditMode.value && controller.selected.value != null" :key="controller.selected.value.objectId" class="_panel" :class="$style.overlayObjectInfoPanel">
{{ controller.selected.value.objectDef.name }}
<div class="_gaps">
<div v-for="[k, s] in Object.entries(controller.selected.value.objectDef.options.schema)" :key="k">
<div>{{ s.label }}</div>
<div v-if="s.type === 'color'">
<MkInput :modelValue="getHex(controller.selected.value.objectState.options[k])" type="color" @update:modelValue="v => { const c = getRgb(v); if (c != null) controller.updateObjectOption(controller.selected.value.objectId, k, c); }"></MkInput>
</div>
<div v-else-if="s.type === 'boolean'">
<MkSwitch :modelValue="controller.selected.value.objectState.options[k]" @update:modelValue="v => controller.updateObjectOption(controller.selected.value.objectId, k, v)"></MkSwitch>
</div>
<div v-else-if="s.type === 'enum'">
<MkSelect :items="s.enum.map(e => ({ label: e, value: e }))" :modelValue="controller.selected.value.objectState.options[k]" @update:modelValue="v => controller.updateObjectOption(controller.selected.value.objectId, k, v)"></MkSelect>
</div>
<div v-else-if="s.type === 'range'">
<MkRange :continuousUpdate="true" :min="s.min" :max="s.max" :step="s.step" :modelValue="controller.selected.value.objectState.options[k]" @update:modelValue="v => controller.updateObjectOption(controller.selected.value.objectId, k, v)"></MkRange>
</div>
<div v-else-if="s.type === 'image'">
<MkInput type="text" :modelValue="controller.selected.value.objectState.options[k]" @update:modelValue="v => controller.updateObjectOption(controller.selected.value.objectId, k, v)"></MkInput>
</div>
</div>
</div>
</div>
</template>
</div>
<template v-if="!isZenMode">
<div v-if="controller.isReady.value" class="_buttons" :class="$style.controls">
<!--<MkButton v-for="action in actions" :key="action.key" @click="action.fn">{{ action.label }}{{ hotkeyToLabel(action.hotkey) }}</MkButton>-->
<MkButton @click="toggleLight">Toggle Light</MkButton>
<MkButton v-if="controller.isEditMode.value" primary @click="save">Save</MkButton>
<MkButton v-if="controller.isEditMode.value" @click="exitEditMode">Exit edit mode</MkButton>
<MkButton v-if="!controller.isEditMode.value" @click="enterEditMode">Edit mode</MkButton>
<MkButton v-if="controller.isEditMode.value" @click="addObject">addObject</MkButton>
<MkButton @click="expor">Export</MkButton>
<MkButton @click="impor">Import</MkButton>
</div>
</template>
</div>
@@ -93,7 +45,7 @@ import * as os from '@/os.js';
import MkInput from '@/components/MkInput.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import { RoomController } from '@/world/room/controller.js';
import { WorldController } from '@/world/controller.js';
const canvas = useTemplateRef('canvas');
@@ -110,74 +62,7 @@ function resize() {
const isZenMode = ref(false);
const data = localStorage.getItem('roomData') != null ? { ...JSON.parse(localStorage.getItem('roomData')!), ...{
heya: {
type: 'simple',
options: {
dimension: [300, 300],
window: 'demado',
wallN: {
material: null,
color: [0.9, 0.9, 0.9],
},
wallE: {
material: null,
color: [0.33, 0.34, 0.35],
},
wallS: {
material: null,
color: [0.9, 0.9, 0.9],
},
wallW: {
material: null,
color: [0.9, 0.9, 0.9],
},
flooring: {
material: 'wood',
color: [0.9, 0.9, 0.9],
},
ceiling: {
material: null,
color: [0.9, 0.9, 0.9],
},
},
},
} } : {
heya: {
type: 'simple',
options: {
dimension: [300, 300],
window: 'demado',
wallN: {
material: null,
color: [0.9, 0.9, 0.9],
},
wallE: {
material: null,
color: [0.9, 0.9, 0.9],
},
wallS: {
material: null,
color: [0.9, 0.9, 0.9],
},
wallW: {
material: null,
color: [0.9, 0.9, 0.9],
},
flooring: {
material: 'wood',
color: [0.9, 0.9, 0.9],
},
ceiling: {
material: null,
color: [0.9, 0.9, 0.9],
},
},
},
installedObjects: [],
};
const controller = new RoomController(data);
const controller = new WorldController();
onMounted(async () => {
controller.init(canvas.value!);
@@ -185,19 +70,6 @@ onMounted(async () => {
canvas.value!.focus();
window.addEventListener('resize', resize);
//watch(controller.selected, (v) => {
// if (v == null) {
// interacions.value = [];
// } else {
// interacions.value = Object.entries(v.objectEntity.instance.interactions).map(([interactionId, interactionInfo]) => ({
// id: interactionId,
// label: interactionInfo.label,
// isPrimary: v.objectEntity.instance.primaryInteraction === interactionId,
// fn: interactionInfo.fn,
// }));
// }
//});
});
onUnmounted(() => {
@@ -206,132 +78,6 @@ onUnmounted(() => {
window.removeEventListener('resize', resize);
});
function beginSelectedInstalledObjectGrabbing() {
controller.beginSelectedInstalledObjectGrabbing();
canvas.value!.focus();
}
function endGrabbing() {
controller.endGrabbing();
canvas.value!.focus();
}
function toggleLight() {
controller.toggleRoomLight();
canvas.value!.focus();
}
function showSnappingMenu(ev: PointerEvent) {
os.popupMenu([{
type: 'switch',
text: i18n.ts._room.snapToGrid,
ref: computed({
get: () => controller.gridSnapping.value.enabled,
set: v => controller.setGridSnapping({ ...controller.gridSnapping.value, enabled: v }),
}),
}, {
type: 'radioOption',
text: '1cm',
active: computed(() => controller.gridSnapping.value.scale === 1),
action: () => controller.setGridSnapping({ ...controller.gridSnapping.value, scale: 1 }),
}, {
type: 'radioOption',
text: '2cm',
active: computed(() => controller.gridSnapping.value.scale === 2),
action: () => controller.setGridSnapping({ ...controller.gridSnapping.value, scale: 2 }),
}, {
type: 'radioOption',
text: '4cm',
active: computed(() => controller.gridSnapping.value.scale === 4),
action: () => controller.setGridSnapping({ ...controller.gridSnapping.value, scale: 4 }),
}, {
type: 'radioOption',
text: '8cm',
active: computed(() => controller.gridSnapping.value.scale === 8),
action: () => controller.setGridSnapping({ ...controller.gridSnapping.value, scale: 8 }),
}], ev.currentTarget ?? ev.target);
}
function rotate() {
controller.changeGrabbingRotationY(Math.PI / 8);
canvas.value!.focus();
}
async function addObject(ev: PointerEvent) {
const { dispose } = await os.popupAsyncWithDialog(import('./room.add-object-dialog.vue').then(x => x.default), {
}, {
ok: async (res) => {
controller.addObject(res);
canvas.value!.focus();
},
closed: () => dispose(),
});
}
function removeSelectedObject() {
controller.removeSelectedObject();
canvas.value!.focus();
}
function enterEditMode() {
controller.enterEditMode();
}
function exitEditMode() {
controller.exitEditMode();
}
function getHex(c: [number, number, number]) {
return `#${c.map(x => Math.round(x * 255).toString(16).padStart(2, '0')).join('')}`;
}
function getRgb(hex: string | number): [number, number, number] | null {
if (
typeof hex === 'number' ||
typeof hex !== 'string' ||
!/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(hex)
) {
return null;
}
const m = hex.slice(1).match(/[0-9a-fA-F]{2}/g);
if (m == null) return [0, 0, 0];
return m.map(x => parseInt(x, 16) / 255) as [number, number, number];
}
function save() {
localStorage.setItem('roomData', JSON.stringify(controller.roomState.value));
}
function expor() {
const dataStr = 'data:text/json;charset=utf-8,' + encodeURIComponent(JSON.stringify(controller.roomState.value));
const dlAnchorElem = window.document.createElement('a');
dlAnchorElem.setAttribute('href', dataStr);
dlAnchorElem.setAttribute('download', 'room.json');
dlAnchorElem.click();
}
function impor() {
const inputElem = window.document.createElement('input');
inputElem.setAttribute('type', 'file');
inputElem.setAttribute('accept', 'application/json');
inputElem.addEventListener('change', () => {
const file = inputElem.files?.[0];
if (file == null) return;
const reader = new FileReader();
reader.onload = () => {
try {
localStorage.setItem('roomData', reader.result as string);
window.location.reload();
} catch (e) {
alert('Failed to load room data: ' + e);
}
};
reader.readAsText(file);
});
inputElem.click();
}
definePage(() => ({
title: 'Room',
icon: 'ti ti-door',
@@ -367,24 +113,6 @@ definePage(() => ({
.controls {
}
.overlayControls {
position: absolute;
bottom: 0;
left: 0;
z-index: 1;
width: 100%;
}
.overlayObjectInfoPanel {
position: absolute;
top: 16px;
right: 16px;
z-index: 1;
padding: 16px;
box-sizing: border-box;
width: 300px;
}
.loading {
position: absolute;
top: 0;

View File

@@ -0,0 +1,141 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { reactive, ref, shallowRef, triggerRef, watch } from 'vue';
import * as BABYLON from '@babylonjs/core';
import { WorldEngine } from './engine.js';
import type { ShallowRef } from 'vue';
import * as sound from '@/utility/sound.js';
// 抽象化レイヤー
export class WorldController {
private worker: Worker | null = null;
private engine: WorldEngine | null = null;
private canvas: HTMLCanvasElement | null = null;
public isReady = ref(false);
public isSitting = ref(false);
public initializeProgress = ref(0);
constructor() {
}
public async init(canvas: HTMLCanvasElement, workerMode = false) {
this.canvas = canvas;
this.canvas.width = canvas.clientWidth;
this.canvas.height = canvas.clientHeight;
if (workerMode) {
//const offscreen = canvas.transferControlToOffscreen();
//this.worker = new RoomWorker();
//this.worker.postMessage({ type: 'init', canvas: offscreen }, [offscreen]);
//this.isReady.value = true;
} else {
const babylonEngine = new BABYLON.WebGPUEngine(canvas, { doNotHandleContextLost: true });
babylonEngine.compatibilityMode = false;
await babylonEngine.initAsync();
this.engine = new WorldEngine({ canvas, engine: babylonEngine });
this.engine.on('loadingProgress', ({ progress }) => {
this.initializeProgress.value = progress;
});
await this.engine.init();
this.initializeProgress.value = 1;
this.isReady.value = true;
this.engine.on('playSfxUrl', ({ url, options }) => {
sound.playUrl(url, options);
});
}
this.canvas.addEventListener('keydown', (ev) => {
if (this.worker != null) {
this.worker.postMessage({ type: 'dom:keydown', ev: { code: ev.code, shiftKey: ev.shiftKey } });
} else if (this.engine != null) {
this.engine.domEvents.emit('keydown', { code: ev.code, shiftKey: ev.shiftKey });
}
ev.preventDefault();
ev.stopPropagation();
return false;
});
this.canvas.addEventListener('keyup', (ev) => {
if (this.worker != null) {
this.worker.postMessage({ type: 'dom:keyup', ev: { code: ev.code, shiftKey: ev.shiftKey } });
} else if (this.engine != null) {
this.engine.domEvents.emit('keyup', { code: ev.code, shiftKey: ev.shiftKey });
}
ev.preventDefault();
ev.stopPropagation();
return false;
});
this.canvas.addEventListener('pointerdown', (ev) => {
// todo
});
this.canvas.addEventListener('wheel', (ev) => {
if (this.worker != null) {
this.worker.postMessage({ type: 'dom:wheel', ev: { deltaY: ev.deltaY } });
} else if (this.engine != null) {
this.engine.domEvents.emit('wheel', { deltaY: ev.deltaY });
}
ev.preventDefault();
ev.stopPropagation();
return false;
});
let isDragging = false;
this.canvas.addEventListener('pointerdown', (ev) => {
this.canvas.setPointerCapture(ev.pointerId);
});
this.canvas.addEventListener('pointermove', (ev) => {
if (this.canvas.hasPointerCapture(ev.pointerId)) {
isDragging = true;
}
});
this.canvas.addEventListener('pointerup', (ev) => {
window.setTimeout(() => {
isDragging = false;
this.canvas.releasePointerCapture(ev.pointerId);
}, 0);
});
this.canvas.addEventListener('click', (ev) => {
if (isDragging) return;
if (this.worker != null) {
this.worker.postMessage({ type: 'dom:click', ev: { offsetX: ev.offsetX, offsetY: ev.offsetY } });
} else if (this.engine != null) {
this.engine.domEvents.emit('click', { offsetX: ev.offsetX, offsetY: ev.offsetY });
}
ev.preventDefault();
ev.stopPropagation();
return false;
});
}
public resize() {
if (this.canvas == null) return;
const width = this.canvas.clientWidth;
const height = this.canvas.clientHeight;
if (this.worker != null) {
this.worker.postMessage({ type: 'resize', width, height });
} else if (this.engine != null) {
this.engine.resize();
}
}
public destroy() {
if (this.worker != null) {
this.worker.terminate();
this.worker = null;
}
if (this.engine != null) {
this.engine.destroy();
this.engine = null;
}
}
}

View File

@@ -3,21 +3,17 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
// TODO: 家具設置時のコリジョン判定(めりこんで設置されないようにする)
// TODO: 近くのオブジェクトの端にスナップオプション
// TODO: 近くのオブジェクトの原点に軸を揃えるオプション
// TODO: glbを事前に最適化(なるべくメッシュをマージするなど)するツールもしくはMisskeyビルド時処理。ついでにカタログ用スクショも自動生成したい
import * as BABYLON from '@babylonjs/core';
import { AxesViewer } from '@babylonjs/core/Debug/axesViewer';
import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic';
import { EventEmitter } from 'eventemitter3';
import { HorizontalCameraKeyboardMoveInput, camelToKebab, cm } from './utility.js';
import tinycolor from 'tinycolor2';
import { HorizontalCameraKeyboardMoveInput, WORLD_SCALE, camelToKebab, cm, createPlaneUvMapper, normalizeUvToSquare } from './utility.js';
import { TIME_MAP } from './utility.js';
import { genId } from '@/utility/id.js';
import { deepClone } from '@/utility/clone.js';
const SNAPSHOT_RENDERING = true; // 実験的
const SNAPSHOT_RENDERING = false; // 実験的
const USE_GLOW = true; // ドローコールが増えて重い
const IN_WEB_WORKER = typeof window === 'undefined';
@@ -41,9 +37,10 @@ export class WorldEngine extends EventEmitter<WorldEngineEvents> {
public intervalIds: number[] = [];
public timeoutIds: number[] = [];
private time: 0 | 1 | 2 = 0; // 0: 昼, 1: 夕, 2: 夜
private envMapOutdoor: BABYLON.CubeTexture;
private envMap: BABYLON.CubeTexture;
public lightContainer: BABYLON.ClusteredLightContainer;
public sr: BABYLON.SnapshotRenderingHelper;
private gl: BABYLON.GlowLayer | null = null;
public isSitting = false;
private fps: number | null = null;
@@ -68,9 +65,6 @@ export class WorldEngine extends EventEmitter<WorldEngineEvents> {
this.engine = options.engine;
this.scene = new BABYLON.Scene(this.engine);
// なんかレンダリングがおかしくなるときがあるのでコメントアウト
// オブジェクトを選択し、後ろを向いて別のオブジェクトを選択した後、最初のオブジェクトに振り返ると消えているなど
//this.scene.performancePriority = BABYLON.ScenePerformancePriority.Intermediate;
this.scene.autoClear = false;
//this.scene.autoClearDepthAndStencil = false;
this.scene.skipPointerMovePicking = true;
@@ -78,35 +72,36 @@ export class WorldEngine extends EventEmitter<WorldEngineEvents> {
this.sr = new BABYLON.SnapshotRenderingHelper(this.scene);
const skybox = BABYLON.MeshBuilder.CreateBox('skybox', { size: cm(1000) }, 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[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(0.7, 0.9, 1.0);
skyboxMat.emissiveColor = new BABYLON.Color3(0.87, 0.89, 0.9);
} else if (this.time === 1) {
skyboxMat.emissiveColor = new BABYLON.Color3(0.8, 0.5, 0.3);
skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.68, 0.66);
} else {
skyboxMat.emissiveColor = new BABYLON.Color3(0.05, 0.05, 0.2);
skyboxMat.emissiveColor = new BABYLON.Color3(0.48, 0.48, 0.5);
}
this.scene.ambientColor = new BABYLON.Color3(1.0, 0.9, 0.8);
this.scene.ambientColor = new BABYLON.Color3(0.9, 0.9, 0.9);
this.envMapOutdoor = BABYLON.CubeTexture.CreateFromPrefilteredData(this.time === 2 ? '/client-assets/room/outdoor-night.env' : '/client-assets/room/outdoor-day.env', this.scene);
this.envMapOutdoor.level = this.time === 0 ? 0.5 : this.time === 1 ? 0.3 : 0.1;
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.scene.collisionsEnabled = true;
this.camera = new BABYLON.UniversalCamera('camera', new BABYLON.Vector3(0, cm(130), cm(0)), this.scene);
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));
this.camera.inputs.add(new HorizontalCameraKeyboardMoveInput(this.camera, 0.3));
this.camera.attachControl(this.canvas);
this.camera.minZ = cm(1);
this.camera.maxZ = cm(2000);
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;
@@ -127,25 +122,26 @@ export class WorldEngine extends EventEmitter<WorldEngineEvents> {
sunLight.shadowMinZ = cm(1000);
sunLight.shadowMaxZ = cm(2000);
this.shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(2048, sunLight);
this.shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(4096, sunLight);
this.shadowGeneratorForSunLight.forceBackFacesOnly = true;
this.shadowGeneratorForSunLight.bias = 0.0001;
this.shadowGeneratorForSunLight.usePercentageCloserFiltering = true;
this.shadowGeneratorForSunLight.usePoissonSampling = true;
if (!SNAPSHOT_RENDERING) this.shadowGeneratorForSunLight.getShadowMap().refreshRate = 60; // snapshot renderingではrefreshRateが設定されているとなぜかクラッシュする
//this.shadowGeneratorForSunLight.getShadowMap().refreshRate = 60;
this.lightContainer = new BABYLON.ClusteredLightContainer('clustered', [], this.scene);
if (USE_GLOW) {
const gl = new BABYLON.GlowLayer('glow', this.scene, {
this.gl = new BABYLON.GlowLayer('glow', this.scene, {
//mainTextureFixedSize: 512,
blurKernelSize: 64,
});
gl.intensity = 0.5;
this.scene.setRenderingAutoClearDepthStencil(gl.renderingGroupId, false);
this.gl.intensity = 0.5;
this.gl.addExcludedMesh(skybox);
this.scene.setRenderingAutoClearDepthStencil(this.gl.renderingGroupId, false);
if (SNAPSHOT_RENDERING) {
this.sr.updateMeshesForEffectLayer(gl);
this.sr.updateMeshesForEffectLayer(this.gl);
}
}
@@ -169,7 +165,7 @@ export class WorldEngine extends EventEmitter<WorldEngineEvents> {
}
public async init() {
this.scene.blockMaterialDirtyMechanism = true;
await this.loadEnvModel();
if (SNAPSHOT_RENDERING) {
this.sr.enableSnapshotRendering();
@@ -214,19 +210,138 @@ export class WorldEngine extends EventEmitter<WorldEngineEvents> {
}
private async loadEnvModel() {
const envObj = await BABYLON.ImportMeshAsync('/client-assets/room/env.glb', this.scene);
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();
envObj.meshes[0].position = new BABYLON.Vector3(0, cm(-900), 0); // 4階くらいの想定
envObj.meshes[0].rotation = new BABYLON.Vector3(0, -Math.PI, 0);
for (const mesh of envObj.meshes) {
mesh.isPickable = false;
mesh.checkCollisions = false;
//if (mesh.name === '__root__') continue;
mesh.receiveShadows = false;
if (mesh.material) (mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.envMapOutdoor;
if (mesh.name === '__root__') continue;
mesh.checkCollisions = true;
if (mesh.material) (mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.envMap;
}
for (let i = 0; i < 16; i++) {
const sphereRoot = new BABYLON.TransformNode('', this.scene);
sphereRoot.position = new BABYLON.Vector3(cm(0), cm(500 + (100 * i)), cm(0));
const rotation = Math.random() * Math.PI * 2;
const sphere = BABYLON.MeshBuilder.CreateSphere('', { diameter: cm(50 + (Math.random() * 250)) }, this.scene);
sphere.parent = sphereRoot;
sphere.position = new BABYLON.Vector3(cm(0), cm(0), cm(4000 + (Math.random() * 500)));
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 = 10000 + (Math.random() * 10000);
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.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 % _30days) / _30days) * Math.PI * 2;
worldRingH.rotation.z = ((time % _12h) / _12h) * Math.PI * 2;
worldRingM.rotation.x = ((time % _7days) / _7days) * Math.PI * 2;
worldRingM.rotation.z = ((time % _1h) / _1h) * Math.PI * 2;
}, 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/cbf69c5f-ea63-4b14-a4c7-9148557d87d4', this.scene, true, true);
tex.level = 0.5;
tex.video.loop = true;
tex.video.volume = 0.25;
//tex.video.muted = true;
screenMaterial.emissiveTexture = tex;
screenMaterial.emissiveColor = new BABYLON.Color3(1, 1, 1);
tex.onLoadObservable.addOnce(() => {
tex.video.play();
for (const mesh of screenMeshes) {
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) {

View File

@@ -50,12 +50,14 @@ export class HorizontalCameraKeyboardMoveInput extends BABYLON.BaseCameraPointer
onCanvasBlurObserver = null;
onKeyboardObserver = null;
public canMove = true;
private moveSpeed = 0.1;
constructor(camera: BABYLON.UniversalCamera) {
constructor(camera: BABYLON.UniversalCamera, moveSpeed = 0.1) {
super();
this.camera = camera;
this.scene = this.camera.getScene();
this.engine = this.scene.getEngine();
this.moveSpeed = moveSpeed;
}
attachControl() {
@@ -129,9 +131,9 @@ export class HorizontalCameraKeyboardMoveInput extends BABYLON.BaseCameraPointer
const dir = this.camera.getDirection(local.normalize());
dir.y = 0;
dir.normalize();
const rate = this.preShift ? 3 : 1;
const moveSpeed = 0.1 * this.scene.getAnimationRatio();
const move = dir.scale(moveSpeed * rate);
const dashFactor = this.preShift ? 3 : 1;
const moveSpeed = this.moveSpeed * this.scene.getAnimationRatio();
const move = dir.scale(moveSpeed * dashFactor);
if (this.canMove) {
this.camera.cameraDirection.addInPlace(move);