mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-28 11:14:36 +02:00
wip
This commit is contained in:
@@ -62,24 +62,29 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick, shallowRef, computed, triggerRef } from 'vue';
|
||||
import { ref, useTemplateRef, watch, onMounted, onUnmounted, reactive, nextTick, shallowRef, computed, triggerRef, markRaw } from 'vue';
|
||||
import XObjectCustomizeForm from './room.object-customize-form.vue';
|
||||
import XItem from './room.add-object-dialog.item.vue';
|
||||
import type { RoomObjectInstance, RoomStateObject } from '@/world/room/object.js';
|
||||
import type { PreviewEngineControllerOptions } from '@/world/room/previewEngineController.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { OBJECT_DEFS } from '@/world/room/object-defs.js';
|
||||
import { createRoomObjectPreviewEngine, RoomObjectPreviewEngine } from '@/world/room/previewEngine.js';
|
||||
import { camelToKebab } from '@/world/utility.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import { store } from '@/store.js';
|
||||
import MkFoldableSection from '@/components/MkFoldableSection.vue';
|
||||
import { PreviewEngineController } from '@/world/room/previewEngineController.js';
|
||||
|
||||
// TODO: instanceのidと紛らわしいのでid -> typeにする
|
||||
|
||||
const props = defineProps<{
|
||||
graphicsQuality: number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'ok', ctx: {
|
||||
id: string;
|
||||
@@ -97,7 +102,15 @@ const selectedInstanceId = ref<string | null>(null);
|
||||
const selectedObjectOptionsState = ref<RoomStateObject | null>(null);
|
||||
const selectedObjectDef = computed(() => OBJECT_DEFS.find(def => def.id === selectedId.value) ?? null);
|
||||
const showObjectOptions = ref(false);
|
||||
const engine = shallowRef<RoomObjectPreviewEngine | null>(null);
|
||||
|
||||
const previewEngineControllerOptions = computed<PreviewEngineControllerOptions>(() => ({
|
||||
graphicsQuality: props.graphicsQuality,
|
||||
fps: null,
|
||||
resolution: 1,
|
||||
workerMode: true,
|
||||
}));
|
||||
|
||||
const controller = markRaw(new PreviewEngineController(previewEngineControllerOptions.value));
|
||||
|
||||
const recentlyUsedDefs = computed(() => {
|
||||
const recentlyUsed = store.s.recentlyUsedRoomObjects;
|
||||
@@ -105,13 +118,23 @@ const recentlyUsedDefs = computed(() => {
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
engine.value = await createRoomObjectPreviewEngine(canvas.value!);
|
||||
try {
|
||||
await controller.init(canvas.value!);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts._room.failedToInitialize,
|
||||
text: (err instanceof Error ? err.message : String(err)),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await engine.value.init();
|
||||
canvas.value!.focus();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
engine.value.destroy();
|
||||
controller.destroy();
|
||||
});
|
||||
|
||||
watch(selectedId, (newId) => {
|
||||
@@ -119,21 +142,21 @@ watch(selectedId, (newId) => {
|
||||
showPreview.value = false;
|
||||
|
||||
if (newId == null) {
|
||||
engine.value!.clear();
|
||||
engine.value!.pauseRender();
|
||||
controller.clearObject();
|
||||
controller.pauseRender();
|
||||
selectedInstanceId.value = null;
|
||||
selectedObjectOptionsState.value = null;
|
||||
} else {
|
||||
const closeWaiting = os.waiting();
|
||||
nextTick(() => {
|
||||
engine.value!.load(newId).then(res => {
|
||||
controller.loadObject(newId).then(res => {
|
||||
selectedInstanceId.value = res.id;
|
||||
selectedObjectOptionsState.value = deepClone(res.options);
|
||||
engine.value!.resumeRender();
|
||||
controller.resumeRender();
|
||||
closeWaiting();
|
||||
showPreview.value = true;
|
||||
nextTick(() => {
|
||||
engine.value!.resize();
|
||||
controller.resize();
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error(err);
|
||||
@@ -144,8 +167,8 @@ watch(selectedId, (newId) => {
|
||||
});
|
||||
|
||||
function updateObjectOption(k: string, v: any) {
|
||||
const updatedOptions = engine.value!.updateObjectOption(k, v);
|
||||
selectedObjectOptionsState.value = deepClone(updatedOptions);
|
||||
controller.updateObjectOption(k, v);
|
||||
selectedObjectOptionsState.value![k] = v;
|
||||
}
|
||||
|
||||
function ok() {
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { ShallowRef } from 'vue';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
|
||||
// 抽象化レイヤー
|
||||
// TODO: 他のcontrollerと共通部分を抽出してabstract classを作ってそこから派生する形にする
|
||||
export class WorldController {
|
||||
private worker: Worker | null = null;
|
||||
private engine: WorldEngine | null = null;
|
||||
@@ -50,9 +51,9 @@ export class WorldController {
|
||||
|
||||
this.canvas.addEventListener('keydown', (ev) => {
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'dom:keydown', ev: { code: ev.code, shiftKey: ev.shiftKey } });
|
||||
this.worker.postMessage({ type: 'input:keydown', ev: { code: ev.code, shiftKey: ev.shiftKey } });
|
||||
} else if (this.engine != null) {
|
||||
this.engine.domEvents.emit('keydown', { code: ev.code, shiftKey: ev.shiftKey });
|
||||
this.engine.inputs.emit('keydown', { code: ev.code, shiftKey: ev.shiftKey });
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -61,9 +62,9 @@ export class WorldController {
|
||||
|
||||
this.canvas.addEventListener('keyup', (ev) => {
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'dom:keyup', ev: { code: ev.code, shiftKey: ev.shiftKey } });
|
||||
this.worker.postMessage({ type: 'input:keyup', ev: { code: ev.code, shiftKey: ev.shiftKey } });
|
||||
} else if (this.engine != null) {
|
||||
this.engine.domEvents.emit('keyup', { code: ev.code, shiftKey: ev.shiftKey });
|
||||
this.engine.inputs.emit('keyup', { code: ev.code, shiftKey: ev.shiftKey });
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -76,9 +77,9 @@ export class WorldController {
|
||||
|
||||
this.canvas.addEventListener('wheel', (ev) => {
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'dom:wheel', ev: { deltaY: ev.deltaY } });
|
||||
this.worker.postMessage({ type: 'input:wheel', ev: { deltaY: ev.deltaY } });
|
||||
} else if (this.engine != null) {
|
||||
this.engine.domEvents.emit('wheel', { deltaY: ev.deltaY });
|
||||
this.engine.inputs.emit('wheel', { deltaY: ev.deltaY });
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -107,9 +108,9 @@ export class WorldController {
|
||||
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 } });
|
||||
this.worker.postMessage({ type: 'input:click', ev: { offsetX: ev.offsetX, offsetY: ev.offsetY } });
|
||||
} else if (this.engine != null) {
|
||||
this.engine.domEvents.emit('click', { offsetX: ev.offsetX, offsetY: ev.offsetY });
|
||||
this.engine.inputs.emit('click', { offsetX: ev.offsetX, offsetY: ev.offsetY });
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
@@ -49,7 +49,7 @@ export class WorldEngine extends EventEmitter<WorldEngineEvents> {
|
||||
private fps: number | null = null;
|
||||
private disposed = false;
|
||||
|
||||
public domEvents: EventEmitter<{
|
||||
public inputs: EventEmitter<{
|
||||
'click': (event: { offsetX: number; offsetY: number; }) => void;
|
||||
'keydown': (event: { code: string; shiftKey: boolean; }) => void;
|
||||
'keyup': (event: { code: string; shiftKey: boolean; }) => void;
|
||||
@@ -230,15 +230,15 @@ export class WorldEngine extends EventEmitter<WorldEngineEvents> {
|
||||
window.requestAnimationFrame(renderLoop);
|
||||
}
|
||||
|
||||
this.domEvents.on('keydown', (ev) => {
|
||||
this.inputs.on('keydown', (ev) => {
|
||||
});
|
||||
|
||||
this.domEvents.on('wheel', (ev) => {
|
||||
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));
|
||||
});
|
||||
|
||||
this.domEvents.on('click', (ev) => {
|
||||
this.inputs.on('click', (ev) => {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,11 +27,13 @@ export type RoomControllerOptions = {
|
||||
};
|
||||
|
||||
// 抽象化レイヤー
|
||||
// TODO: 他のcontrollerと共通部分を抽出してabstract classを作ってそこから派生する形にする
|
||||
export class RoomController {
|
||||
private worker: Worker | null = null;
|
||||
private engine: RoomEngine | null = null;
|
||||
private canvas: HTMLCanvasElement | null = null;
|
||||
private options: RoomControllerOptions;
|
||||
private returnHooks = new Map<number, (value: any) => void>();
|
||||
public isReady = ref(false);
|
||||
public isSitting = ref(false);
|
||||
public isEditMode = ref(false);
|
||||
@@ -75,6 +77,15 @@ export class RoomController {
|
||||
engineEvents.emit(type, ctx);
|
||||
break;
|
||||
}
|
||||
case 'return': {
|
||||
const { id, value } = event.data;
|
||||
const hook = this.returnHooks.get(id);
|
||||
if (hook != null) {
|
||||
hook(value);
|
||||
this.returnHooks.delete(id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.warn('Unrecognized message from worker:', event.data?.type);
|
||||
}
|
||||
@@ -164,9 +175,9 @@ export class RoomController {
|
||||
|
||||
this.canvas.addEventListener('keydown', (ev) => {
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'dom:keydown', ev: { code: ev.code, shiftKey: ev.shiftKey } });
|
||||
this.worker.postMessage({ type: 'input:keydown', ev: { code: ev.code, shiftKey: ev.shiftKey } });
|
||||
} else if (this.engine != null) {
|
||||
this.engine.domEvents.emit('keydown', { code: ev.code, shiftKey: ev.shiftKey });
|
||||
this.engine.inputs.emit('keydown', { code: ev.code, shiftKey: ev.shiftKey });
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -175,9 +186,9 @@ export class RoomController {
|
||||
|
||||
this.canvas.addEventListener('keyup', (ev) => {
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'dom:keyup', ev: { code: ev.code, shiftKey: ev.shiftKey } });
|
||||
this.worker.postMessage({ type: 'input:keyup', ev: { code: ev.code, shiftKey: ev.shiftKey } });
|
||||
} else if (this.engine != null) {
|
||||
this.engine.domEvents.emit('keyup', { code: ev.code, shiftKey: ev.shiftKey });
|
||||
this.engine.inputs.emit('keyup', { code: ev.code, shiftKey: ev.shiftKey });
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -186,9 +197,9 @@ export class RoomController {
|
||||
|
||||
this.canvas.addEventListener('wheel', (ev) => {
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'dom:wheel', ev: { deltaY: ev.deltaY } });
|
||||
this.worker.postMessage({ type: 'input:wheel', ev: { deltaY: ev.deltaY } });
|
||||
} else if (this.engine != null) {
|
||||
this.engine.domEvents.emit('wheel', { deltaY: ev.deltaY });
|
||||
this.engine.inputs.emit('wheel', { deltaY: ev.deltaY });
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
@@ -231,9 +242,9 @@ export class RoomController {
|
||||
if (prevTwoTouchPointsDistance > 0) {
|
||||
const delta = distance - prevTwoTouchPointsDistance;
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'dom:wheel', ev: { deltaY: -delta * 3 } });
|
||||
this.worker.postMessage({ type: 'input:wheel', ev: { deltaY: -delta * 3 } });
|
||||
} else if (this.engine != null) {
|
||||
this.engine.domEvents.emit('wheel', { deltaY: -delta * 3 });
|
||||
this.engine.inputs.emit('wheel', { deltaY: -delta * 3 });
|
||||
}
|
||||
}
|
||||
prevTwoTouchPointsDistance = distance;
|
||||
@@ -284,9 +295,9 @@ export class RoomController {
|
||||
if (dx < 10 && dy < 10) {
|
||||
const median = { x: (ev.offsetX + this.pointerDownPosition.x) / 2, y: (ev.offsetY + this.pointerDownPosition.y) / 2 };
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'dom:click', ev: { x: median.x, y: median.y } });
|
||||
this.worker.postMessage({ type: 'input:click', ev: { x: median.x, y: median.y } });
|
||||
} else if (this.engine != null) {
|
||||
this.engine.domEvents.emit('click', { x: median.x, y: median.y });
|
||||
this.engine.inputs.emit('click', { x: median.x, y: median.y });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,12 +320,24 @@ export class RoomController {
|
||||
await this.init(canvas ?? this.canvas!);
|
||||
}
|
||||
|
||||
private callCounter = 0;
|
||||
|
||||
// TODO: いい感じに型付け
|
||||
private call(fn, args = []) {
|
||||
private call(fn, args = [], needReturnValue = false) {
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'call', fn, args });
|
||||
if (needReturnValue) {
|
||||
return new Promise((resolve) => {
|
||||
const id = this.callCounter++;
|
||||
this.returnHooks.set(id, (value) => {
|
||||
resolve(value);
|
||||
});
|
||||
this.worker!.postMessage({ type: 'call', fn, args, needReturnValue: true, id });
|
||||
});
|
||||
} else {
|
||||
this.worker.postMessage({ type: 'call', fn, args });
|
||||
}
|
||||
} else if (this.engine != null) {
|
||||
this.engine[fn](...args);
|
||||
return this.engine[fn](...args);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ export class RoomEngine extends EventEmitter {
|
||||
private fps: number | null = null;
|
||||
private disposed = false;
|
||||
|
||||
public domEvents: EventEmitter<{
|
||||
public inputs: EventEmitter<{
|
||||
'click': (event: { x: number; y: number; }) => void;
|
||||
'keydown': (event: { code: string; shiftKey: boolean; }) => void;
|
||||
'keyup': (event: { code: string; shiftKey: boolean; }) => void;
|
||||
@@ -394,8 +394,8 @@ export class RoomEngine extends EventEmitter {
|
||||
|
||||
const box = BABYLON.MeshBuilder.CreateBox('', { size: cm(10) }, this.scene);
|
||||
window.setInterval(() => {
|
||||
box.position = new BABYLON.Vector3(0, Math.random() * cm(10), 0);
|
||||
}, 10);
|
||||
box.position = new BABYLON.Vector3(0, Math.random() * cm(10), 0);
|
||||
}, 10);
|
||||
|
||||
this.startRenderLoop();
|
||||
|
||||
@@ -404,7 +404,7 @@ export class RoomEngine extends EventEmitter {
|
||||
// 必ずシーンが少なくとも1フレームレンダリングがされてから呼ばれるように注意すること。そうしないとタイミングによってはエンジンがクラッシュする
|
||||
this.sr.enableSnapshotRendering();
|
||||
|
||||
this.domEvents.on('keydown', (ev) => {
|
||||
this.inputs.on('keydown', (ev) => {
|
||||
if (ev.code === 'KeyE') {
|
||||
if (this.isEditMode) {
|
||||
if (this.grabbingCtx != null) {
|
||||
@@ -432,7 +432,7 @@ export class RoomEngine extends EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
this.domEvents.on('wheel', (ev) => {
|
||||
this.inputs.on('wheel', (ev) => {
|
||||
if (this.grabbingCtx != null) {
|
||||
this.changeGrabbingDistance(ev.deltaY * 0.025);
|
||||
} else {
|
||||
@@ -441,7 +441,7 @@ export class RoomEngine extends EventEmitter {
|
||||
}
|
||||
});
|
||||
|
||||
this.domEvents.on('click', (ev) => {
|
||||
this.inputs.on('click', (ev) => {
|
||||
if (this.grabbingCtx != null) return;
|
||||
|
||||
this.selectObject(null);
|
||||
@@ -454,7 +454,7 @@ export class RoomEngine extends EventEmitter {
|
||||
const oid = pickingInfo.pickedMesh.metadata.objectId;
|
||||
if (oid != null && this.objectEntities.has(oid)) {
|
||||
const o = this.objectEntities.get(oid)!;
|
||||
const boundingInfo = getMeshesBoundingBox(o.rootMesh.getChildMeshes().filter(m => m.isEnabled() && m.isVisible && !m.isDisposed()));
|
||||
const boundingInfo = getMeshesBoundingBox(o.rootMesh.getChildMeshes().filter(m => m.isEnabled() && m.isVisible && !m.isDisposed()), true);
|
||||
this.selectObject(oid);
|
||||
|
||||
{ // camera animation
|
||||
|
||||
@@ -6,24 +6,15 @@
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic.js';
|
||||
import { GridMaterial } from '@babylonjs/materials';
|
||||
import { camelToKebab, WORLD_SCALE, cm, getMeshesBoundingBox, Timer, sleep } from '../utility.js';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { camelToKebab, WORLD_SCALE, cm, getMeshesBoundingBox, Timer, sleep, ArcRotateCameraManualInput } from '../utility.js';
|
||||
import { getObjectDef } from './object-defs.js';
|
||||
import { SYSTEM_MESH_NAMES, ModelManager, GRAPHICS_QUALITY } from './utility.js';
|
||||
import type { RoomObjectInstance } from './object.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import { store } from '@/store.js';
|
||||
|
||||
export async function createRoomObjectPreviewEngine(canvas: HTMLCanvasElement) {
|
||||
const babylonEngine = new BABYLON.WebGPUEngine(canvas, { doNotHandleContextLost: true, powerPreference: 'low-power' });
|
||||
babylonEngine.compatibilityMode = false;
|
||||
babylonEngine.enableOfflineSupport = false;
|
||||
await babylonEngine.initAsync();
|
||||
return new RoomObjectPreviewEngine({ canvas, engine: babylonEngine });
|
||||
}
|
||||
|
||||
export class RoomObjectPreviewEngine {
|
||||
private canvas: HTMLCanvasElement;
|
||||
export class RoomObjectPreviewEngine extends EventEmitter {
|
||||
private engine: BABYLON.WebGPUEngine;
|
||||
private scene: BABYLON.Scene;
|
||||
private sr: BABYLON.SnapshotRenderingHelper;
|
||||
@@ -38,17 +29,29 @@ export class RoomObjectPreviewEngine {
|
||||
private zGridPreviewPlane: BABYLON.Mesh;
|
||||
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;
|
||||
}> = new EventEmitter();
|
||||
|
||||
constructor(options: {
|
||||
canvas: HTMLCanvasElement;
|
||||
engine: BABYLON.WebGPUEngine;
|
||||
graphicsQuality: number;
|
||||
fps: number | null;
|
||||
}) {
|
||||
this.canvas = options.canvas;
|
||||
super();
|
||||
|
||||
registerBuiltInLoaders();
|
||||
|
||||
this.graphicsQuality = options.graphicsQuality;
|
||||
this.fps = options.fps;
|
||||
|
||||
this.engine = options.engine;
|
||||
this.scene = new BABYLON.Scene(this.engine);
|
||||
this.scene.autoClear = false;
|
||||
@@ -78,7 +81,7 @@ export class RoomObjectPreviewEngine {
|
||||
this.shadowGenerator.getShadowMap().refreshRate = 60;
|
||||
|
||||
const gridMaterial = new GridMaterial('grid', this.scene);
|
||||
gridMaterial.lineColor = store.s.darkMode ? new BABYLON.Color3(1, 1, 1) : new BABYLON.Color3(0, 0, 0);
|
||||
gridMaterial.lineColor = new BABYLON.Color3(1, 1, 1);
|
||||
gridMaterial.mainColor = new BABYLON.Color3(0, 0, 0);
|
||||
gridMaterial.minorUnitVisibility = 1;
|
||||
gridMaterial.opacity = 0.05;
|
||||
@@ -97,16 +100,17 @@ export class RoomObjectPreviewEngine {
|
||||
|
||||
this.pipeline = new BABYLON.DefaultRenderingPipeline('default', true, this.scene);
|
||||
this.pipeline.samples = 4;
|
||||
// 重い
|
||||
//this.pipeline.bloomEnabled = true;
|
||||
//this.pipeline.bloomThreshold = 0.95;
|
||||
//this.pipeline.bloomWeight = 0.1;
|
||||
//this.pipeline.bloomKernel = 256;
|
||||
//this.pipeline.bloomScale = 2;
|
||||
if (this.graphicsQuality >= GRAPHICS_QUALITY.HIGH) {
|
||||
this.pipeline.bloomEnabled = true;
|
||||
this.pipeline.bloomThreshold = 0.95;
|
||||
this.pipeline.bloomWeight = 0.1;
|
||||
this.pipeline.bloomKernel = 256;
|
||||
this.pipeline.bloomScale = 2;
|
||||
}
|
||||
this.pipeline.sharpenEnabled = true;
|
||||
this.pipeline.sharpen.edgeAmount = 0.5;
|
||||
|
||||
if (_DEV_) {
|
||||
if (_DEV_ && typeof window !== 'undefined') {
|
||||
window.takeScreenshot = () => {
|
||||
const def = getObjectDef(this.objectType);
|
||||
|
||||
@@ -190,11 +194,16 @@ export class RoomObjectPreviewEngine {
|
||||
public async init() {
|
||||
await this.scene.whenReadyAsync();
|
||||
this.sr.enableSnapshotRendering();
|
||||
|
||||
this.inputs.on('wheel', (ev) => {
|
||||
this.camera.fov += ev.deltaY * 0.001;
|
||||
this.camera.fov = Math.max(0.25, Math.min(0.5, this.camera.fov));
|
||||
});
|
||||
}
|
||||
|
||||
public async load(type: string) {
|
||||
public async loadObject(type: string) {
|
||||
this.sr.disableSnapshotRendering();
|
||||
this.clear();
|
||||
this.clearObject();
|
||||
|
||||
const id = genId();
|
||||
const def = getObjectDef(type);
|
||||
@@ -205,7 +214,7 @@ export class RoomObjectPreviewEngine {
|
||||
}
|
||||
}
|
||||
|
||||
await this.loadObject({
|
||||
await this.loadObject_({
|
||||
type,
|
||||
options: this.objectOptions,
|
||||
id,
|
||||
@@ -217,7 +226,6 @@ export class RoomObjectPreviewEngine {
|
||||
this.camera.dispose();
|
||||
|
||||
this.camera = new BABYLON.ArcRotateCamera('camera', Math.PI / 2, Math.PI / 2.5, cm(300), new BABYLON.Vector3(0, cm(90), 0), this.scene);
|
||||
this.camera.attachControl(this.canvas);
|
||||
this.camera.minZ = cm(1);
|
||||
this.camera.maxZ = cm(100000);
|
||||
this.camera.fov = 0.5;
|
||||
@@ -229,6 +237,11 @@ export class RoomObjectPreviewEngine {
|
||||
this.camera.wheelDeltaPercentage = 0.01;
|
||||
//this.camera.mode = BABYLON.Camera.ORTHOGRAPHIC_CAMERA;
|
||||
this.camera.setTarget(new BABYLON.Vector3(0, boundingInfo.centerWorld.y, 0));
|
||||
this.camera.inputs.clear();
|
||||
|
||||
this.camera.inputs.add(new ArcRotateCameraManualInput(this.scene, {
|
||||
rotationSensitivity: 0.0005,
|
||||
}));
|
||||
|
||||
if (def.placement === 'wall' || def.placement === 'side') {
|
||||
this.camera.lowerBetaLimit = 0;
|
||||
@@ -260,12 +273,11 @@ export class RoomObjectPreviewEngine {
|
||||
|
||||
return {
|
||||
id,
|
||||
objectInstance: this.objectInstance,
|
||||
options: this.objectOptions,
|
||||
};
|
||||
}
|
||||
|
||||
private async loadObject(args: {
|
||||
private async loadObject_(args: {
|
||||
type: string;
|
||||
options: any;
|
||||
id: string;
|
||||
@@ -365,7 +377,7 @@ export class RoomObjectPreviewEngine {
|
||||
return this.objectOptions;
|
||||
}
|
||||
|
||||
public clear() {
|
||||
public clearObject() {
|
||||
this.sr.disableSnapshotRendering();
|
||||
if (this.timerForEachObject != null) {
|
||||
this.timerForEachObject.dispose();
|
||||
@@ -382,6 +394,10 @@ export class RoomObjectPreviewEngine {
|
||||
this.sr.enableSnapshotRendering();
|
||||
}
|
||||
|
||||
public cameraRotate(vector: { x: number; y: number; }) {
|
||||
(this.camera.inputs.attached.manual as ArcRotateCameraManualInput).setRotationVector(vector);
|
||||
}
|
||||
|
||||
public resize() {
|
||||
// 一旦snapshot renderingを無効にしておかないとエラーが出る(babylonのバグ?)
|
||||
// ~~...が、一旦無効にしたらしたで複数のマテリアルがそれぞれ入れ替わる(?)という謎の現象が発生するためコメントアウトしとく(エラー出てもレンダリングが止まったりするわけでもないし)~~
|
||||
|
||||
293
packages/frontend/src/world/room/previewEngineController.ts
Normal file
293
packages/frontend/src/world/room/previewEngineController.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
/*
|
||||
* 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 { EventEmitter } from 'eventemitter3';
|
||||
import PreviewEngineWorker from './previewEngineWorker?worker';
|
||||
import { RoomObjectPreviewEngine } from './previewEngine.js';
|
||||
import type { ShallowRef } from 'vue';
|
||||
import type { RoomEngineEvents, RoomState } from './engine.js';
|
||||
import * as sound from '@/utility/sound.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { deepEqual } from '@/utility/deep-equal.js';
|
||||
|
||||
export type PreviewEngineControllerOptions = {
|
||||
workerMode?: boolean;
|
||||
graphicsQuality: number;
|
||||
fps: number | null;
|
||||
resolution: number;
|
||||
};
|
||||
|
||||
// 抽象化レイヤー
|
||||
// TODO: 他のcontrollerと共通部分を抽出してabstract classを作ってそこから派生する形にする
|
||||
export class PreviewEngineController {
|
||||
private worker: Worker | null = null;
|
||||
private engine: RoomObjectPreviewEngine | null = null;
|
||||
private canvas: HTMLCanvasElement | null = null;
|
||||
private options: PreviewEngineControllerOptions;
|
||||
private returnHooks = new Map<number, (value: any) => void>();
|
||||
public isReady = ref(false);
|
||||
private pointerDownPosition: { x: number; y: number } | null = null;
|
||||
private abortController = new AbortController();
|
||||
|
||||
constructor(options: PreviewEngineControllerOptions) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public async init(canvas: HTMLCanvasElement) {
|
||||
this.canvas = canvas;
|
||||
this.canvas.width = canvas.clientWidth;
|
||||
this.canvas.height = canvas.clientHeight;
|
||||
|
||||
const engineEvents = new EventEmitter<RoomEngineEvents>();
|
||||
|
||||
if (this.options.workerMode) {
|
||||
const offscreen = canvas.transferControlToOffscreen();
|
||||
this.worker = new PreviewEngineWorker();
|
||||
this.worker.postMessage({ type: 'init', canvas: offscreen, options: this.options }, [offscreen]);
|
||||
this.worker.onmessage = (event) => {
|
||||
switch (event.data?.type) {
|
||||
case 'inited': {
|
||||
this.isReady.value = true;
|
||||
break;
|
||||
}
|
||||
case 'ev': {
|
||||
const { type, ctx } = event.data.ev;
|
||||
engineEvents.emit(type, ctx);
|
||||
break;
|
||||
}
|
||||
case 'return': {
|
||||
const { id, value } = event.data;
|
||||
const hook = this.returnHooks.get(id);
|
||||
if (hook != null) {
|
||||
hook(value);
|
||||
this.returnHooks.delete(id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.warn('Unrecognized message from worker:', event.data?.type);
|
||||
}
|
||||
}
|
||||
};
|
||||
} else {
|
||||
const babylonEngine = new BABYLON.WebGPUEngine(canvas, { doNotHandleContextLost: true, powerPreference: 'low-power', antialias: true });
|
||||
babylonEngine.compatibilityMode = false;
|
||||
babylonEngine.enableOfflineSupport = false;
|
||||
babylonEngine.onContextLostObservable.add(() => {
|
||||
os.alert({
|
||||
type: 'error',
|
||||
title: i18n.ts.somethingHappened,
|
||||
text: i18n.ts._room.crushed_description,
|
||||
});
|
||||
});
|
||||
await babylonEngine.initAsync();
|
||||
if (this.options.resolution === 2) babylonEngine.setHardwareScalingLevel(0.5);
|
||||
if (this.options.resolution === 0.5) babylonEngine.setHardwareScalingLevel(2);
|
||||
|
||||
this.engine = new RoomObjectPreviewEngine({
|
||||
engine: babylonEngine,
|
||||
...this.options,
|
||||
});
|
||||
|
||||
this.engine.on('ev', ({ type, ctx }) => {
|
||||
engineEvents.emit(type, ctx);
|
||||
});
|
||||
|
||||
await this.engine.init();
|
||||
|
||||
this.isReady.value = true;
|
||||
}
|
||||
|
||||
this.canvas.addEventListener('wheel', (ev) => {
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'input:wheel', ev: { deltaY: ev.deltaY } });
|
||||
} else if (this.engine != null) {
|
||||
this.engine.inputs.emit('wheel', { deltaY: ev.deltaY });
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return false;
|
||||
}, { signal: this.abortController.signal });
|
||||
|
||||
const pointerEventCache = new Map<number, PointerEvent>();
|
||||
let pointerVec = { x: 0, y: 0 };
|
||||
|
||||
this.canvas.addEventListener('pointerdown', (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.canvas!.focus();
|
||||
pointerEventCache.set(ev.pointerId, ev);
|
||||
this.canvas!.setPointerCapture(ev.pointerId);
|
||||
|
||||
pointerVec = { x: ev.clientX, y: ev.clientY };
|
||||
|
||||
this.pointerDownPosition = { x: ev.offsetX, y: ev.offsetY };
|
||||
|
||||
return false;
|
||||
}, { signal: this.abortController.signal });
|
||||
|
||||
let prevTwoTouchPointsDistance = 0;
|
||||
|
||||
this.canvas!.addEventListener('pointermove', (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (pointerEventCache.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
pointerEventCache.set(ev.pointerId, ev);
|
||||
|
||||
if (pointerEventCache.size > 1) {
|
||||
const a = Array.from(pointerEventCache.values())[0];
|
||||
const b = Array.from(pointerEventCache.values())[1];
|
||||
const distance = Math.hypot(a.clientX - b.clientX, a.clientY - b.clientY);
|
||||
if (prevTwoTouchPointsDistance > 0) {
|
||||
const delta = distance - prevTwoTouchPointsDistance;
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'input:wheel', ev: { deltaY: -delta * 3 } });
|
||||
} else if (this.engine != null) {
|
||||
this.engine.inputs.emit('wheel', { deltaY: -delta * 3 });
|
||||
}
|
||||
}
|
||||
prevTwoTouchPointsDistance = distance;
|
||||
|
||||
pointerVec = { x: 0, y: 0 };
|
||||
this.setCameraRotateVector({
|
||||
x: 0,
|
||||
y: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
prevTwoTouchPointsDistance = 0;
|
||||
|
||||
const before = pointerVec;
|
||||
const after = { x: ev.clientX, y: ev.clientY };
|
||||
|
||||
if (pointerVec.x !== 0 || pointerVec.y !== 0) {
|
||||
this.setCameraRotateVector({
|
||||
x: after.x - before.x,
|
||||
y: after.y - before.y,
|
||||
});
|
||||
}
|
||||
|
||||
pointerVec.x = after.x;
|
||||
pointerVec.y = after.y;
|
||||
|
||||
return false;
|
||||
}, { signal: this.abortController.signal });
|
||||
|
||||
this.canvas.addEventListener('pointerup', (ev) => {
|
||||
pointerEventCache.delete(ev.pointerId);
|
||||
this.canvas!.releasePointerCapture(ev.pointerId);
|
||||
|
||||
prevTwoTouchPointsDistance = 0;
|
||||
|
||||
if (pointerEventCache.size > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
pointerVec.x = 0;
|
||||
pointerVec.y = 0;
|
||||
this.setCameraRotateVector(pointerVec);
|
||||
|
||||
if (this.pointerDownPosition != null) {
|
||||
const dx = Math.abs(ev.offsetX - this.pointerDownPosition.x);
|
||||
const dy = Math.abs(ev.offsetY - this.pointerDownPosition.y);
|
||||
if (dx < 10 && dy < 10) {
|
||||
const median = { x: (ev.offsetX + this.pointerDownPosition.x) / 2, y: (ev.offsetY + this.pointerDownPosition.y) / 2 };
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'input:click', ev: { x: median.x, y: median.y } });
|
||||
} else if (this.engine != null) {
|
||||
this.engine.inputs.emit('click', { x: median.x, y: median.y });
|
||||
}
|
||||
}
|
||||
}
|
||||
this.pointerDownPosition = null;
|
||||
}, { signal: this.abortController.signal });
|
||||
}
|
||||
|
||||
private callCounter = 0;
|
||||
|
||||
// TODO: いい感じに型付け
|
||||
private call(fn, args = [], needReturnValue = false) {
|
||||
if (this.worker != null) {
|
||||
if (needReturnValue) {
|
||||
return new Promise((resolve) => {
|
||||
const id = this.callCounter++;
|
||||
this.returnHooks.set(id, (value) => {
|
||||
resolve(value);
|
||||
});
|
||||
this.worker!.postMessage({ type: 'call', fn, args, needReturnValue: true, id });
|
||||
});
|
||||
} else {
|
||||
this.worker.postMessage({ type: 'call', fn, args });
|
||||
}
|
||||
} else if (this.engine != null) {
|
||||
return this.engine[fn](...args);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: いい感じに型付け
|
||||
private set(key, value) {
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'set', key, value });
|
||||
} else if (this.engine != null) {
|
||||
this.engine[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
public pauseRender() {
|
||||
this.call('pauseRender');
|
||||
}
|
||||
|
||||
public resumeRender() {
|
||||
this.call('resumeRender');
|
||||
}
|
||||
|
||||
public setCameraRotateVector(vec: { x: number; y: number }) {
|
||||
this.call('cameraRotate', [vec]);
|
||||
}
|
||||
|
||||
public updateObjectOption(key: string, value: any) {
|
||||
this.call('updateObjectOption', [key, value]);
|
||||
}
|
||||
|
||||
public loadObject(type: string) {
|
||||
return this.call('loadObject', [type], true);
|
||||
}
|
||||
|
||||
public clearObject() {
|
||||
this.call('clearObject');
|
||||
}
|
||||
|
||||
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() {
|
||||
this.abortController.abort();
|
||||
|
||||
if (this.worker != null) {
|
||||
this.worker.terminate();
|
||||
this.worker = null;
|
||||
}
|
||||
if (this.engine != null) {
|
||||
this.engine.destroy();
|
||||
this.engine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
88
packages/frontend/src/world/room/previewEngineWorker.ts
Normal file
88
packages/frontend/src/world/room/previewEngineWorker.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { RoomObjectPreviewEngine } from './previewEngine.js';
|
||||
|
||||
let engine: RoomObjectPreviewEngine | null = null;
|
||||
let canvas: OffscreenCanvas | null = null;
|
||||
|
||||
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: 'low-power', antialias: true });
|
||||
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 RoomObjectPreviewEngine({
|
||||
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 'call': {
|
||||
if (engine != null) {
|
||||
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) engine[event.data.key] = event.data.value;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
console.warn('Unrecognized message type:', event.data?.type);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -44,28 +44,39 @@ onmessage = async (event) => {
|
||||
if (engine != null) engine.resize();
|
||||
break;
|
||||
}
|
||||
case 'dom:keydown': {
|
||||
case 'input:keydown': {
|
||||
if (engine == null) break;
|
||||
engine.domEvents.emit('keydown', event.data.ev);
|
||||
engine.inputs.emit('keydown', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'dom:keyup': {
|
||||
case 'input:keyup': {
|
||||
if (engine == null) break;
|
||||
engine.domEvents.emit('keyup', event.data.ev);
|
||||
engine.inputs.emit('keyup', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'dom:click': {
|
||||
case 'input:click': {
|
||||
if (engine == null) break;
|
||||
engine.domEvents.emit('click', event.data.ev);
|
||||
engine.inputs.emit('click', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'dom:wheel': {
|
||||
case 'input:wheel': {
|
||||
if (engine == null) break;
|
||||
engine.domEvents.emit('wheel', event.data.ev);
|
||||
engine.inputs.emit('wheel', event.data.ev);
|
||||
break;
|
||||
}
|
||||
case 'call': {
|
||||
if (engine != null) engine[event.data.fn](...(event.data.args ?? []));
|
||||
if (engine != null) {
|
||||
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': {
|
||||
|
||||
@@ -701,3 +701,38 @@ export class FreeCameraManualInput implements BABYLON.ICameraInput<BABYLON.FreeC
|
||||
//this.camera.cameraDirection.addInPlace(this.camera._transformedDirection);
|
||||
}
|
||||
}
|
||||
|
||||
export class ArcRotateCameraManualInput implements BABYLON.ICameraInput<BABYLON.ArcRotateCamera> {
|
||||
public camera: BABYLON.ArcRotateCamera;
|
||||
private scene: BABYLON.Scene;
|
||||
private rotationSensitivity: number;
|
||||
|
||||
constructor(scene: BABYLON.Scene, options: {
|
||||
rotationSensitivity?: number;
|
||||
}) {
|
||||
this.scene = scene;
|
||||
this.rotationSensitivity = options.rotationSensitivity ?? 0.01;
|
||||
}
|
||||
|
||||
getClassName = () => this.constructor.name;
|
||||
|
||||
getSimpleName = () => 'manual';
|
||||
|
||||
attachControl(noPreventDefault) {
|
||||
}
|
||||
|
||||
detachControl() {
|
||||
}
|
||||
|
||||
public setRotationVector(vec: { x: number; y: number }) {
|
||||
let directionAdjust = 1;
|
||||
if (this.scene.useRightHandedSystem) directionAdjust *= -1;
|
||||
if (this.camera.parent && this.camera.parent._getWorldMatrixDeterminant() < 0) directionAdjust *= -1;
|
||||
|
||||
this.camera.inertialBetaOffset += -vec.y * this.rotationSensitivity * directionAdjust;
|
||||
this.camera.inertialAlphaOffset += -vec.x * this.rotationSensitivity * directionAdjust;
|
||||
}
|
||||
|
||||
checkInputs() {
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user