mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-05-18 15:25:36 +02:00
wip
This commit is contained in:
@@ -40,7 +40,7 @@ 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/engine.js';
|
||||
import { createRoomObjectPreviewEngine, RoomObjectPreviewEngine } from '@/world/room/previewEngine.js';
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'ok', id: string): void;
|
||||
|
||||
421
packages/frontend/src/pages/world.vue
Normal file
421
packages/frontend/src/pages/world.vue
Normal file
@@ -0,0 +1,421 @@
|
||||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root" class="_pageScrollable">
|
||||
<div :class="[$style.screen, { [$style.zen]: isZenMode }]">
|
||||
<canvas ref="canvas" :class="$style.canvas" tabindex="-1" :style="{ visibility: controller.isReady.value ? 'visible' : 'hidden' }"></canvas>
|
||||
|
||||
<Transition
|
||||
:enterActiveClass="$style.transition_fade_enterActive"
|
||||
:leaveActiveClass="$style.transition_fade_leaveActive"
|
||||
:enterFromClass="$style.transition_fade_enterFrom"
|
||||
:leaveToClass="$style.transition_fade_leaveTo"
|
||||
>
|
||||
<div v-if="!controller.isReady.value" :class="$style.loading">
|
||||
<div :class="$style.progressBar">
|
||||
<div :class="$style.progressBarValue" :style="{ width: `${controller.initializeProgress.value * 100}%` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<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>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, useTemplateRef, watch } from 'vue';
|
||||
import { definePage } from '@/page.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { ensureSignin } from '@/i';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
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';
|
||||
|
||||
const canvas = useTemplateRef('canvas');
|
||||
|
||||
const interacions = shallowRef<{
|
||||
id: string;
|
||||
label: string;
|
||||
isPrimary: boolean;
|
||||
fn: () => void;
|
||||
}[]>([]);
|
||||
|
||||
function resize() {
|
||||
controller.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);
|
||||
|
||||
onMounted(async () => {
|
||||
controller.init(canvas.value!);
|
||||
|
||||
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(() => {
|
||||
controller.destroy();
|
||||
|
||||
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',
|
||||
needWideArea: true,
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.screen {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 90cqh;
|
||||
}
|
||||
.screen.zen {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
background: #000;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--MI_THEME-bg);
|
||||
}
|
||||
|
||||
.progressBar {
|
||||
width: 75%;
|
||||
height: 4px;
|
||||
border-radius: 999px;
|
||||
overflow: clip;
|
||||
background-color: var(--MI_THEME-accentedBg);
|
||||
}
|
||||
|
||||
.progressBarValue {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
|
||||
transition: all 0.5s cubic-bezier(0,.5,.5,1);
|
||||
}
|
||||
|
||||
.transition_fade_enterActive,
|
||||
.transition_fade_leaveActive {
|
||||
transition: opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
.transition_fade_enterFrom,
|
||||
.transition_fade_leaveTo {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -594,6 +594,9 @@ export const ROUTE_DEF = [{
|
||||
path: '/qr',
|
||||
component: page(() => import('@/pages/qr.vue')),
|
||||
loginRequired: true,
|
||||
}, {
|
||||
path: '/world',
|
||||
component: page(() => import('@/pages/world.vue')),
|
||||
}, {
|
||||
path: '/room',
|
||||
component: page(() => import('@/pages/room.vue')),
|
||||
|
||||
267
packages/frontend/src/world/engine.ts
Normal file
267
packages/frontend/src/world/engine.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* 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 { TIME_MAP } from './utility.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
|
||||
const SNAPSHOT_RENDERING = true; // 実験的
|
||||
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 envMapOutdoor: BABYLON.CubeTexture;
|
||||
public lightContainer: BABYLON.ClusteredLightContainer;
|
||||
public sr: BABYLON.SnapshotRenderingHelper;
|
||||
|
||||
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.performancePriority = BABYLON.ScenePerformancePriority.Intermediate;
|
||||
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(1000) }, 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];
|
||||
|
||||
if (this.time === 0) {
|
||||
skyboxMat.emissiveColor = new BABYLON.Color3(0.7, 0.9, 1.0);
|
||||
} else if (this.time === 1) {
|
||||
skyboxMat.emissiveColor = new BABYLON.Color3(0.8, 0.5, 0.3);
|
||||
} else {
|
||||
skyboxMat.emissiveColor = new BABYLON.Color3(0.05, 0.05, 0.2);
|
||||
}
|
||||
this.scene.ambientColor = new BABYLON.Color3(1.0, 0.9, 0.8);
|
||||
|
||||
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.scene.collisionsEnabled = true;
|
||||
|
||||
this.camera = new BABYLON.UniversalCamera('camera', new BABYLON.Vector3(0, cm(130), cm(0)), this.scene);
|
||||
this.camera.inputs.removeByType('FreeCameraKeyboardMoveInput');
|
||||
this.camera.inputs.add(new HorizontalCameraKeyboardMoveInput(this.camera));
|
||||
this.camera.attachControl(this.canvas);
|
||||
this.camera.minZ = cm(1);
|
||||
this.camera.maxZ = cm(2000);
|
||||
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 ambientLight = new BABYLON.HemisphericLight('ambientLight', new BABYLON.Vector3(0, 1, -0.5), this.scene);
|
||||
ambientLight.diffuse = new BABYLON.Color3(1.0, 1.0, 1.0);
|
||||
ambientLight.intensity = 0.5;
|
||||
//ambientLight.intensity = 0;
|
||||
|
||||
const sunLight = new BABYLON.DirectionalLight('sunLight', new BABYLON.Vector3(0.2, -1, -1), this.scene);
|
||||
sunLight.position = new BABYLON.Vector3(cm(-20), cm(1000), cm(1000));
|
||||
sunLight.diffuse = this.time === 0 ? new BABYLON.Color3(1.0, 0.9, 0.8) : 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 ? 3 : this.time === 1 ? 1 : 0.25;
|
||||
sunLight.shadowMinZ = cm(1000);
|
||||
sunLight.shadowMaxZ = cm(2000);
|
||||
|
||||
this.shadowGeneratorForSunLight = new BABYLON.ShadowGenerator(2048, 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.lightContainer = new BABYLON.ClusteredLightContainer('clustered', [], this.scene);
|
||||
|
||||
if (USE_GLOW) {
|
||||
const gl = new BABYLON.GlowLayer('glow', this.scene, {
|
||||
//mainTextureFixedSize: 512,
|
||||
blurKernelSize: 64,
|
||||
});
|
||||
gl.intensity = 0.5;
|
||||
this.scene.setRenderingAutoClearDepthStencil(gl.renderingGroupId, false);
|
||||
|
||||
if (SNAPSHOT_RENDERING) {
|
||||
this.sr.updateMeshesForEffectLayer(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() {
|
||||
this.scene.blockMaterialDirtyMechanism = true;
|
||||
|
||||
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/room/env.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;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -199,6 +199,7 @@ export class RoomController {
|
||||
}
|
||||
|
||||
public addObject(type: string) {
|
||||
console.log(type);
|
||||
if (this.worker != null) {
|
||||
this.worker.postMessage({ type: 'addObject', objectType: type });
|
||||
} else if (this.engine != null) {
|
||||
|
||||
@@ -14,59 +14,19 @@ import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic';
|
||||
import { BoundingBoxRenderer } from '@babylonjs/core/Rendering/boundingBoxRenderer';
|
||||
import { GridMaterial } from '@babylonjs/materials';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { TIME_MAP, scaleMorph, HorizontalCameraKeyboardMoveInput, camelToKebab, cm, WORLD_SCALE, getMeshesBoundingBox } from '../utility.js';
|
||||
import { getObjectDef } from './object-defs.js';
|
||||
import { HorizontalCameraKeyboardMoveInput, applyMorphTargetsToMesh, camelToKebab, cm, findMaterial, scaleMorph } from './utility.js';
|
||||
import { findMaterial, ModelManager, SYSTEM_MESH_NAMES } from './utility.js';
|
||||
import type { ObjectDef, RoomObjectInstance, RoomStateObject } from './object.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
|
||||
const BAKE_TRANSFORM = false; // 実験的
|
||||
const SNAPSHOT_RENDERING = true; // 実験的
|
||||
const IGNORE_OBJECTS: string[] = []; // for debug
|
||||
const SYSTEM_MESH_NAMES = ['__TOP__', '__SIDE__', '__PICK__', '__COLLISION__'];
|
||||
const USE_GLOW = true; // ドローコールが増えて重い
|
||||
const IN_WEB_WORKER = typeof window === 'undefined';
|
||||
|
||||
const TIME_MAP = {
|
||||
0: 2,
|
||||
1: 2,
|
||||
2: 2,
|
||||
3: 2,
|
||||
4: 2,
|
||||
5: 1,
|
||||
6: 1,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
10: 0,
|
||||
11: 0,
|
||||
12: 0,
|
||||
13: 0,
|
||||
14: 0,
|
||||
15: 0,
|
||||
16: 1,
|
||||
17: 1,
|
||||
18: 2,
|
||||
19: 2,
|
||||
20: 2,
|
||||
21: 2,
|
||||
22: 2,
|
||||
23: 2,
|
||||
} as const;
|
||||
|
||||
// babylonのドメイン知識は持たない
|
||||
export type RoomStateObject<Options = any> = {
|
||||
id: string;
|
||||
type: string;
|
||||
position: [number, number, number];
|
||||
rotation: [number, number, number];
|
||||
options: Options;
|
||||
|
||||
/**
|
||||
* 別のオブジェクトのID
|
||||
*/
|
||||
sticky?: string | null;
|
||||
};
|
||||
|
||||
type SimpleHeyaWallBase = {
|
||||
material: null | 'wood' | 'concrete';
|
||||
color: [number, number, number];
|
||||
@@ -99,221 +59,6 @@ export type RoomState = {
|
||||
installedObjects: RoomStateObject<any>[];
|
||||
};
|
||||
|
||||
type RoomObjectInstance<Options> = {
|
||||
onInited?: () => void;
|
||||
onOptionsUpdated?: <K extends keyof Options, V extends Options[K]>(kv: [K, V]) => void;
|
||||
interactions: Record<string, {
|
||||
label: string;
|
||||
fn: () => void;
|
||||
}>;
|
||||
primaryInteraction?: string | null;
|
||||
resetTemporaryState?: () => void;
|
||||
dispose?: () => void;
|
||||
};
|
||||
|
||||
export const WORLD_SCALE = 100;
|
||||
|
||||
type NumberOptionSchema = {
|
||||
type: 'number';
|
||||
label: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
};
|
||||
|
||||
type BooleanOptionSchema = {
|
||||
type: 'boolean';
|
||||
label: string;
|
||||
};
|
||||
|
||||
type ColorOptionSchema = {
|
||||
type: 'color';
|
||||
label: string;
|
||||
};
|
||||
|
||||
type EnumOptionSchema = {
|
||||
type: 'enum';
|
||||
label: string;
|
||||
enum: string[];
|
||||
};
|
||||
|
||||
type RangeOptionSchema = {
|
||||
type: 'range';
|
||||
label: string;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
};
|
||||
|
||||
type ImageOptionSchema = {
|
||||
type: 'image';
|
||||
label: string;
|
||||
};
|
||||
|
||||
type OptionsSchema = Record<string, NumberOptionSchema | BooleanOptionSchema | ColorOptionSchema | EnumOptionSchema | RangeOptionSchema | ImageOptionSchema>;
|
||||
|
||||
type GetOptionsSchemaValues<T extends OptionsSchema> = {
|
||||
[K in keyof T]:
|
||||
T[K] extends NumberOptionSchema ? number :
|
||||
T[K] extends BooleanOptionSchema ? boolean :
|
||||
T[K] extends ColorOptionSchema ? [number, number, number] :
|
||||
T[K] extends EnumOptionSchema ? T[K]['enum'][number] :
|
||||
T[K] extends RangeOptionSchema ? number :
|
||||
T[K] extends ImageOptionSchema ? string | null :
|
||||
never;
|
||||
};
|
||||
|
||||
class ModelManager {
|
||||
public root: BABYLON.Mesh;
|
||||
public bakedCallback: (() => void) | null = null;
|
||||
public bakeExcludeMeshes: BABYLON.Mesh[] = [];
|
||||
private originalMeshes: BABYLON.Mesh[] = [];
|
||||
private bakedMeshes: BABYLON.Mesh[] = [];
|
||||
private hasTexture: boolean;
|
||||
|
||||
constructor(root: BABYLON.Mesh, originalMeshes: BABYLON.Mesh[], hasTexture: boolean, bakedCallback: (() => void) | null = null) {
|
||||
this.root = root;
|
||||
this.originalMeshes = originalMeshes;
|
||||
this.hasTexture = hasTexture;
|
||||
this.bakedCallback = bakedCallback;
|
||||
}
|
||||
|
||||
public findMesh(keyword: string) {
|
||||
const mesh = this.root.getChildMeshes().find(m => m.name.includes(keyword));
|
||||
if (mesh == null) {
|
||||
throw new Error(`Mesh with keyword "${keyword}" not found for object ${this.root.metadata?.objectType}`);
|
||||
}
|
||||
return mesh as BABYLON.Mesh;
|
||||
}
|
||||
|
||||
public findMeshes(keyword: string) {
|
||||
const meshes = this.root.getChildMeshes().filter(m => m.name.includes(keyword));
|
||||
return meshes as BABYLON.Mesh[];
|
||||
}
|
||||
|
||||
public findMaterial(keyword: string) {
|
||||
return findMaterial(this.root, keyword);
|
||||
}
|
||||
|
||||
public findTransformNode(keyword: string) {
|
||||
const node = this.root.getChildTransformNodes().find(n => n.name.includes(keyword));
|
||||
if (node == null) {
|
||||
throw new Error(`TransformNode with keyword "${keyword}" not found for object ${this.root.metadata?.objectType}`);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
public updated() {
|
||||
}
|
||||
|
||||
public bakeMesh() {
|
||||
for (const m of this.bakedMeshes) {
|
||||
m.dispose();
|
||||
}
|
||||
this.bakedMeshes = [];
|
||||
|
||||
const excludeMeshes = [...this.bakeExcludeMeshes, ...this.root.getChildMeshes().filter(m => SYSTEM_MESH_NAMES.some(s => m.name.includes(s)))];
|
||||
|
||||
const childMeshes = this.root.getChildMeshes().filter(m => !excludeMeshes.some(x => x === m) && m.isVisible && !m.isDisposed());
|
||||
|
||||
if (childMeshes.length <= 1) {
|
||||
this.bakedCallback?.([...childMeshes, ...excludeMeshes]);
|
||||
return;
|
||||
}
|
||||
|
||||
const _toMerge = [] as BABYLON.Mesh[];
|
||||
for (const mesh of childMeshes) {
|
||||
let fixedMesh = mesh;
|
||||
fixedMesh.setEnabled(false);
|
||||
|
||||
if (mesh instanceof BABYLON.InstancedMesh) {
|
||||
const sourceMesh = mesh.sourceMesh;
|
||||
const realizedMesh = sourceMesh.clone(mesh.name + '_realized', null, true);
|
||||
realizedMesh.getScene().removeMesh(realizedMesh);
|
||||
|
||||
realizedMesh.position = mesh.position.clone();
|
||||
if (mesh.rotationQuaternion) {
|
||||
realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone();
|
||||
} else {
|
||||
realizedMesh.rotation = mesh.rotation.clone();
|
||||
}
|
||||
realizedMesh.scaling = mesh.scaling.clone();
|
||||
realizedMesh.parent = mesh.parent;
|
||||
realizedMesh.setEnabled(false);
|
||||
|
||||
fixedMesh = realizedMesh;
|
||||
}
|
||||
|
||||
_toMerge.push(fixedMesh);
|
||||
}
|
||||
|
||||
const toMerge = [] as BABYLON.Mesh[];
|
||||
for (const mesh of _toMerge) {
|
||||
const newMesh = mesh.name.endsWith('_realized') ? mesh : mesh.clone(mesh.name + '_bakeMerged', null, true);
|
||||
newMesh.makeGeometryUnique();
|
||||
applyMorphTargetsToMesh(newMesh);
|
||||
if (newMesh.parent === this.root) {
|
||||
newMesh.parent = null;
|
||||
} else {
|
||||
newMesh.setParent(this.root);
|
||||
//newMesh.bakeCurrentTransformIntoVertices();
|
||||
newMesh.parent = null;
|
||||
}
|
||||
//newMesh.bakeCurrentTransformIntoVertices();
|
||||
|
||||
if (this.hasTexture) {
|
||||
if (newMesh.getVerticesData(BABYLON.VertexBuffer.UVKind) == null) {
|
||||
const vertexCount = newMesh.getTotalVertices();
|
||||
const uvs = new Array(vertexCount * 2).fill(0);
|
||||
newMesh.setVerticesData(BABYLON.VertexBuffer.UVKind, uvs, false, 2);
|
||||
}
|
||||
if (newMesh.getVerticesData(BABYLON.VertexBuffer.UV2Kind) == null) {
|
||||
const vertexCount = newMesh.getTotalVertices();
|
||||
const uvs = new Array(vertexCount * 2).fill(0);
|
||||
newMesh.setVerticesData(BABYLON.VertexBuffer.UV2Kind, uvs, false, 2);
|
||||
}
|
||||
}
|
||||
|
||||
toMerge.push(newMesh);
|
||||
}
|
||||
|
||||
if (toMerge.length === 0) {
|
||||
this.bakedCallback?.([...childMeshes, ...excludeMeshes]);
|
||||
return;
|
||||
}
|
||||
|
||||
const merged = BABYLON.Mesh.MergeMeshes(toMerge, true, false, undefined, false, true);
|
||||
merged.parent = this.root;
|
||||
merged.material.freeze();
|
||||
if (merged.material instanceof BABYLON.MultiMaterial) {
|
||||
for (const subMat of merged.material.subMaterials) {
|
||||
(subMat as BABYLON.PBRMaterial).freeze();
|
||||
}
|
||||
}
|
||||
merged.freezeWorldMatrix();
|
||||
merged.metadata = { ...this.root.metadata };
|
||||
if (!this.hasTexture) merged.convertToUnIndexedMesh();
|
||||
this.bakedMeshes = [merged];
|
||||
|
||||
this.bakedCallback?.([...this.bakedMeshes, ...excludeMeshes]);
|
||||
}
|
||||
|
||||
public unbakeMesh() {
|
||||
for (const m of this.bakedMeshes) {
|
||||
m.dispose();
|
||||
}
|
||||
this.bakedMeshes = [];
|
||||
|
||||
const childMeshes = this.root.getChildMeshes();
|
||||
|
||||
for (const mesh of childMeshes) {
|
||||
mesh.setEnabled(true);
|
||||
}
|
||||
|
||||
this.bakedCallback?.(this.root.getChildMeshes());
|
||||
}
|
||||
}
|
||||
|
||||
function mergeMeshes(meshes: BABYLON.Mesh[], root: BABYLON.Mesh, hasTexture: boolean) {
|
||||
const excludeMeshes = root.getChildMeshes().filter(m => SYSTEM_MESH_NAMES.some(s => m.name.includes(s)));
|
||||
|
||||
@@ -352,58 +97,6 @@ function mergeMeshes(meshes: BABYLON.Mesh[], root: BABYLON.Mesh, hasTexture: boo
|
||||
return merged;
|
||||
}
|
||||
|
||||
export type ObjectDef<OpSc extends OptionsSchema = OptionsSchema> = {
|
||||
id: string;
|
||||
name: string;
|
||||
path?: string;
|
||||
options: {
|
||||
schema: OpSc;
|
||||
default: GetOptionsSchemaValues<OpSc>;
|
||||
};
|
||||
placement: 'top' | 'side' | 'bottom' | 'wall' | 'ceiling' | 'floor';
|
||||
hasCollisions?: boolean;
|
||||
hasTexture?: boolean;
|
||||
canPreMeshesMerging?: boolean;
|
||||
//groupingMeshes: string[]; // multi-materialなメッシュは複数のメッシュに分割されるが、それだと不便な場合に追加の親メッシュでグルーピングするための指定
|
||||
isChair?: boolean;
|
||||
treatLoaderResult?: (loaderResult: BABYLON.AssetContainer) => void;
|
||||
createInstance: (args: {
|
||||
room?: RoomEngine | null;
|
||||
scene: BABYLON.Scene;
|
||||
root: BABYLON.Mesh;
|
||||
options: Readonly<GetOptionsSchemaValues<OpSc>>;
|
||||
model: ModelManager;
|
||||
id: string;
|
||||
stickyMarkerMeshUpdated?: (mesh: BABYLON.Mesh) => void;
|
||||
}) => RoomObjectInstance<GetOptionsSchemaValues<OpSc>> | Promise<RoomObjectInstance<GetOptionsSchemaValues<OpSc>>>; // TODO: createInstanceをasyncにするのではなく、別にreadyみたいなものを返させる
|
||||
};
|
||||
|
||||
export function defineObject<const OpSc extends OptionsSchema>(def: ObjectDef<OpSc>): ObjectDef<OpSc> {
|
||||
return def;
|
||||
}
|
||||
|
||||
export function defineObjectClass<const OpSc extends OptionsSchema>(baseDef: Partial<ObjectDef<OpSc>>): {
|
||||
extend: (childDef: Partial<ObjectDef<OpSc>>) => ObjectDef<OpSc>;
|
||||
} {
|
||||
return {
|
||||
extend: (childDef) => ({ ...baseDef, ...childDef }) as ObjectDef<OpSc>,
|
||||
};
|
||||
}
|
||||
|
||||
// この実装方法だとマイナスの座標をうまく処理できず結果がおかしくなるので応急処置で全体を+10000cmオフセットしてから計算している
|
||||
function getMeshesBoundingBox(meshes: BABYLON.Mesh[]): BABYLON.BoundingBox {
|
||||
let min = new BABYLON.Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
|
||||
let max = new BABYLON.Vector3(Number.MIN_VALUE, Number.MIN_VALUE, Number.MIN_VALUE);
|
||||
|
||||
for (const mesh of meshes) {
|
||||
const boundingInfo = mesh.getBoundingInfo();
|
||||
min = BABYLON.Vector3.Minimize(min, boundingInfo.boundingBox.minimumWorld.add(new BABYLON.Vector3(10000, 10000, 10000)));
|
||||
max = BABYLON.Vector3.Maximize(max, boundingInfo.boundingBox.maximumWorld.add(new BABYLON.Vector3(10000, 10000, 10000)));
|
||||
}
|
||||
|
||||
return new BABYLON.BoundingBox(min.subtract(new BABYLON.Vector3(10000, 10000, 10000)), max.subtract(new BABYLON.Vector3(10000, 10000, 10000)));
|
||||
}
|
||||
|
||||
function enableObjectCollision(meshes: BABYLON.Mesh[]) {
|
||||
for (const mesh of meshes) {
|
||||
if (mesh.name.includes('__COLLISION__')) {
|
||||
@@ -444,8 +137,6 @@ export class RoomEngine extends EventEmitter<RoomEngineEvents> {
|
||||
private shadowGeneratorForRoomLight: BABYLON.ShadowGenerator;
|
||||
private shadowGeneratorForSunLight: BABYLON.ShadowGenerator;
|
||||
public camera: BABYLON.UniversalCamera;
|
||||
private fixedCamera: BABYLON.UniversalCamera;
|
||||
private birdeyeCamera: BABYLON.ArcRotateCamera;
|
||||
public intervalIds: number[] = [];
|
||||
public timeoutIds: number[] = [];
|
||||
public objectEntities: Map<string, {
|
||||
@@ -655,45 +346,6 @@ export class RoomEngine extends EventEmitter<RoomEngineEvents> {
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
//const postProcess = new BABYLON.ImageProcessingPostProcess('processing', 1.0, this.camera);
|
||||
//postProcess.exposure = 1.1;
|
||||
//postProcess.contrast = 0.9;
|
||||
|
||||
//const curve = new BABYLON.ColorCurves();
|
||||
//curve.highlightsHue = 40;
|
||||
//curve.highlightsDensity = 50;
|
||||
//curve.highlightsSaturation = 40;
|
||||
//curve.shadowsHue = 200;
|
||||
//curve.shadowsDensity = 100;
|
||||
//curve.shadowsSaturation = 40;
|
||||
//postProcess.colorCurvesEnabled = true;
|
||||
//postProcess.colorCurves = curve;
|
||||
|
||||
//const postProcess2 = new BABYLON.ImageProcessingPostProcess('processing2', 1.0, this.birdeyeCamera);
|
||||
//postProcess2.exposure = 2;
|
||||
//postProcess2.contrast = 0.9;
|
||||
|
||||
//const ssao = new BABYLON.SSAORenderingPipeline('ssao', this.scene, {
|
||||
// ssaoRatio: 4,
|
||||
// combineRatio: 1,
|
||||
//});
|
||||
//ssao.radius = 0.0001;
|
||||
//ssao.totalStrength = 0.8;
|
||||
//this.scene.postProcessRenderPipelineManager.attachCamerasToRenderPipeline('ssao', this.camera);
|
||||
|
||||
//const lensEffect = new BABYLON.LensRenderingPipeline('lens', {
|
||||
// edge_blur: 1.0,
|
||||
// distortion: 0.5,
|
||||
// dof_focus_distance: cm(90),
|
||||
// dof_aperture: 6.0,
|
||||
// dof_pentagon: true,
|
||||
// dof_gain: 2.0,
|
||||
// dof_threshold: 1.0,
|
||||
// dof_darken: 0,
|
||||
//}, this.scene, 1, [this.camera]);
|
||||
}
|
||||
|
||||
this.putParticleSystem = new BABYLON.ParticleSystem('', 64, this.scene);
|
||||
this.putParticleSystem.particleTexture = new BABYLON.Texture('/client-assets/room/steam.png');
|
||||
this.putParticleSystem.createCylinderEmitter(cm(5), cm(1), cm(5));
|
||||
@@ -1952,14 +1604,6 @@ export class RoomEngine extends EventEmitter<RoomEngineEvents> {
|
||||
}
|
||||
}
|
||||
|
||||
public showBoundingBox() {
|
||||
for (const mesh of this.objectMeshs.values()) {
|
||||
for (const m of mesh.getChildMeshes()) {
|
||||
m.showBoundingBox = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private playSfxUrl(url: string, options: { volume: number; playbackRate: number }) {
|
||||
this.emit('playSfxUrl', { url, options });
|
||||
}
|
||||
@@ -1981,239 +1625,3 @@ export class RoomEngine extends EventEmitter<RoomEngineEvents> {
|
||||
this.disposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createRoomObjectPreviewEngine(canvas: HTMLCanvasElement) {
|
||||
//const babylonEngine = new BABYLON.WebGPUEngine(canvas);
|
||||
//babylonEngine.compatibilityMode = false;
|
||||
//await babylonEngine.initAsync();
|
||||
const babylonEngine = new BABYLON.Engine(canvas, false, { alpha: false, antialias: false });
|
||||
return new RoomObjectPreviewEngine({ canvas, engine: babylonEngine });
|
||||
}
|
||||
|
||||
export class RoomObjectPreviewEngine {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private engine: BABYLON.WebGPUEngine;
|
||||
private scene: BABYLON.Scene;
|
||||
private shadowGenerator1: BABYLON.ShadowGenerator;
|
||||
private camera: BABYLON.ArcRotateCamera;
|
||||
private objectMesh: BABYLON.Mesh | null = null;
|
||||
private objectInstance: RoomObjectInstance<any> | null = null;
|
||||
private envMapIndoor: BABYLON.CubeTexture;
|
||||
private roomLight: BABYLON.SpotLight;
|
||||
private zGridPreviewPlane: BABYLON.Mesh;
|
||||
private fps = 60;
|
||||
|
||||
constructor(options: {
|
||||
canvas: HTMLCanvasElement;
|
||||
engine: BABYLON.WebGPUEngine;
|
||||
}) {
|
||||
this.canvas = options.canvas;
|
||||
|
||||
registerBuiltInLoaders();
|
||||
|
||||
this.engine = options.engine;
|
||||
this.scene = new BABYLON.Scene(this.engine);
|
||||
|
||||
this.scene.ambientColor = new BABYLON.Color3(1.0, 0.9, 0.8);
|
||||
|
||||
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.scene);
|
||||
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(500), cm(500), cm(500));
|
||||
|
||||
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;
|
||||
this.camera.lowerBetaLimit = 0;
|
||||
this.camera.upperBetaLimit = (Math.PI / 2) + 0.1;
|
||||
this.camera.lowerRadiusLimit = cm(50);
|
||||
this.camera.upperRadiusLimit = cm(1000);
|
||||
this.camera.useAutoRotationBehavior = true;
|
||||
this.camera.autoRotationBehavior!.idleRotationSpeed = 0.3;
|
||||
//this.camera.mode = BABYLON.Camera.ORTHOGRAPHIC_CAMERA;
|
||||
this.scene.activeCamera = this.camera;
|
||||
|
||||
const ambientLight = new BABYLON.HemisphericLight('ambientLight', new BABYLON.Vector3(0, 1, -0.5), this.scene);
|
||||
ambientLight.diffuse = new BABYLON.Color3(1.0, 1.0, 1.0);
|
||||
ambientLight.intensity = 0.5;
|
||||
//ambientLight.intensity = 0;
|
||||
|
||||
this.roomLight = new BABYLON.SpotLight('roomLight', new BABYLON.Vector3(0, cm(249), 0), new BABYLON.Vector3(0, -1, 0), 16, 8, this.scene);
|
||||
this.roomLight.diffuse = new BABYLON.Color3(1.0, 0.9, 0.8);
|
||||
this.roomLight.shadowMinZ = cm(10);
|
||||
this.roomLight.shadowMaxZ = cm(300);
|
||||
|
||||
this.shadowGenerator1 = new BABYLON.ShadowGenerator(4096, this.roomLight);
|
||||
this.shadowGenerator1.forceBackFacesOnly = true;
|
||||
this.shadowGenerator1.bias = 0.0001;
|
||||
this.shadowGenerator1.usePercentageCloserFiltering = true;
|
||||
this.shadowGenerator1.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
|
||||
//this.shadowGenerator1.useContactHardeningShadow = true;
|
||||
|
||||
const gridMaterial = new GridMaterial('grid', this.scene);
|
||||
gridMaterial.lineColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
gridMaterial.mainColor = new BABYLON.Color3(0, 0, 0);
|
||||
gridMaterial.minorUnitVisibility = 1;
|
||||
gridMaterial.opacity = 0.5;
|
||||
gridMaterial.gridRatio = cm(10);
|
||||
|
||||
//this.zGridPreviewPlane = BABYLON.MeshBuilder.CreatePlane('zGridPreviewPlane', { width: cm(1000), height: cm(1000) }, this.scene);
|
||||
//this.zGridPreviewPlane.material = gridMaterial;
|
||||
//this.zGridPreviewPlane.rotation = new BABYLON.Vector3(Math.PI / 2, 0, 0);
|
||||
|
||||
//this.scene.fogMode = BABYLON.Scene.FOGMODE_LINEAR;
|
||||
//this.scene.fogStart = cm(100);
|
||||
//this.scene.fogEnd = cm(110);
|
||||
//this.scene.fogColor = new BABYLON.Color3(0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
public async init() {
|
||||
const frameInterval = 1000 / this.fps;
|
||||
let lastTime = performance.now();
|
||||
|
||||
this.engine.runRenderLoop(() => {
|
||||
const currentTime = performance.now();
|
||||
const delta = currentTime - lastTime;
|
||||
|
||||
if (delta >= frameInterval) {
|
||||
this.scene.render();
|
||||
lastTime = currentTime - (delta % frameInterval);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async load(type: string) {
|
||||
if (this.objectInstance != null) {
|
||||
this.objectInstance.dispose?.();
|
||||
this.objectInstance = null;
|
||||
this.objectMesh!.dispose();
|
||||
}
|
||||
|
||||
// reset camera rotation
|
||||
this.camera.setPosition(new BABYLON.Vector3(0, cm(90), cm(300)));
|
||||
|
||||
const def = getObjectDef(type);
|
||||
|
||||
const options = deepClone(def.options.default);
|
||||
|
||||
const id = genId();
|
||||
|
||||
await this.loadObject({
|
||||
type,
|
||||
options,
|
||||
id,
|
||||
});
|
||||
|
||||
// なぜかちょっと待たないとbounding boxのサイズが正しくない
|
||||
window.setTimeout(() => {
|
||||
const boundingInfo = getMeshesBoundingBox(this.objectMesh!.getChildMeshes().filter(m => m.isEnabled() && m.isVisible));
|
||||
this.camera.setTarget(new BABYLON.Vector3(0, boundingInfo.center.y, 0));
|
||||
|
||||
// zoom to fit
|
||||
const size = boundingInfo.extendSize;
|
||||
const distance = Math.max(size.x, size.y, size.z) * 2;
|
||||
this.camera.radius = distance * 3;
|
||||
//this.camera.orthoLeft = -distance;
|
||||
//this.camera.orthoRight = distance;
|
||||
//this.camera.orthoTop = distance;
|
||||
//this.camera.orthoBottom = -distance;
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// TODO: RoomEngineのものとほぼ同じだからいい感じに共通化
|
||||
private async loadObject(args: {
|
||||
type: string;
|
||||
options: any;
|
||||
id: string;
|
||||
}) {
|
||||
const def = getObjectDef(args.type);
|
||||
|
||||
const root = new BABYLON.Mesh(`object_${args.type}`, this.scene);
|
||||
|
||||
const filePath = def.path != null ? `/client-assets/room/objects/${def.path}.glb` : `/client-assets/room/objects/${camelToKebab(args.type)}/${camelToKebab(args.type)}.glb`;
|
||||
const loaderResult = await BABYLON.LoadAssetContainerAsync(filePath, this.scene);
|
||||
|
||||
// 不要なUVを掃除
|
||||
if (!def.hasTexture) {
|
||||
for (const m of loaderResult.meshes) {
|
||||
if (m.geometry != null) {
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UVKind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV2Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV3Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV4Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV5Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV6Kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// babylonによって自動で追加される右手系変換用ノード
|
||||
const subRoot = loaderResult.meshes[0];
|
||||
subRoot.scaling = subRoot.scaling.scale(WORLD_SCALE);// cmをmに
|
||||
|
||||
def.treatLoaderResult?.(loaderResult);
|
||||
|
||||
root.addChild(subRoot);
|
||||
|
||||
const model = new ModelManager(subRoot, loaderResult.meshes.filter(m => !m.isDisposed() && m !== subRoot), def.hasTexture, (meshes) => {
|
||||
for (const m of meshes) {
|
||||
const mesh = m;
|
||||
|
||||
// シェイプキー(morph)を考慮してbounding boxを更新するために必要
|
||||
mesh.refreshBoundingInfo({ applyMorph: true });
|
||||
|
||||
if (SYSTEM_MESH_NAMES.some(n => mesh.name.includes(n))) {
|
||||
mesh.receiveShadows = false;
|
||||
mesh.isVisible = false;
|
||||
} else {
|
||||
if (def.receiveShadows !== false) mesh.receiveShadows = true;
|
||||
if (def.castShadows !== false) {
|
||||
this.shadowGenerator1.addShadowCaster(mesh);
|
||||
}
|
||||
|
||||
if (mesh.material) {
|
||||
if (mesh.material instanceof BABYLON.MultiMaterial) {
|
||||
for (const subMat of mesh.material.subMaterials) {
|
||||
(subMat as BABYLON.PBRMaterial).reflectionTexture = this.envMapIndoor;
|
||||
(subMat as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
|
||||
}
|
||||
} else {
|
||||
(mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.envMapIndoor;
|
||||
(mesh.material as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.scene.meshes.includes(mesh)) this.scene.addMesh(mesh);
|
||||
}
|
||||
});
|
||||
|
||||
const objectInstance = await def.createInstance({
|
||||
room: null,
|
||||
scene: this.scene,
|
||||
root,
|
||||
options: args.options,
|
||||
model,
|
||||
id: args.id,
|
||||
});
|
||||
|
||||
objectInstance.onInited?.();
|
||||
|
||||
model.bakeMesh();
|
||||
|
||||
this.objectInstance = objectInstance;
|
||||
this.objectMesh = root;
|
||||
}
|
||||
|
||||
public updateObjectOption(key: string, value: any) {
|
||||
this.objectInstance?.onOptionsUpdated?.([key, value]);
|
||||
}
|
||||
|
||||
public resize() {
|
||||
this.engine.resize();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.engine.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
122
packages/frontend/src/world/room/object.ts
Normal file
122
packages/frontend/src/world/room/object.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import type { RoomEngine } from './engine.js';
|
||||
import type { ModelManager } from './utility.js';
|
||||
|
||||
// babylonのドメイン知識は持たない
|
||||
export type RoomStateObject<Options = any> = {
|
||||
id: string;
|
||||
type: string;
|
||||
position: [number, number, number];
|
||||
rotation: [number, number, number];
|
||||
options: Options;
|
||||
|
||||
/**
|
||||
* 別のオブジェクトのID
|
||||
*/
|
||||
sticky?: string | null;
|
||||
};
|
||||
|
||||
export type RoomObjectInstance<Options> = {
|
||||
onInited?: () => void;
|
||||
onOptionsUpdated?: <K extends keyof Options, V extends Options[K]>(kv: [K, V]) => void;
|
||||
interactions: Record<string, {
|
||||
label: string;
|
||||
fn: () => void;
|
||||
}>;
|
||||
primaryInteraction?: string | null;
|
||||
resetTemporaryState?: () => void;
|
||||
dispose?: () => void;
|
||||
};
|
||||
|
||||
type NumberOptionSchema = {
|
||||
type: 'number';
|
||||
label: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
};
|
||||
|
||||
type BooleanOptionSchema = {
|
||||
type: 'boolean';
|
||||
label: string;
|
||||
};
|
||||
|
||||
type ColorOptionSchema = {
|
||||
type: 'color';
|
||||
label: string;
|
||||
};
|
||||
|
||||
type EnumOptionSchema = {
|
||||
type: 'enum';
|
||||
label: string;
|
||||
enum: string[];
|
||||
};
|
||||
|
||||
type RangeOptionSchema = {
|
||||
type: 'range';
|
||||
label: string;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
};
|
||||
|
||||
type ImageOptionSchema = {
|
||||
type: 'image';
|
||||
label: string;
|
||||
};
|
||||
|
||||
type OptionsSchema = Record<string, NumberOptionSchema | BooleanOptionSchema | ColorOptionSchema | EnumOptionSchema | RangeOptionSchema | ImageOptionSchema>;
|
||||
|
||||
type GetOptionsSchemaValues<T extends OptionsSchema> = {
|
||||
[K in keyof T]:
|
||||
T[K] extends NumberOptionSchema ? number :
|
||||
T[K] extends BooleanOptionSchema ? boolean :
|
||||
T[K] extends ColorOptionSchema ? [number, number, number] :
|
||||
T[K] extends EnumOptionSchema ? T[K]['enum'][number] :
|
||||
T[K] extends RangeOptionSchema ? number :
|
||||
T[K] extends ImageOptionSchema ? string | null :
|
||||
never;
|
||||
};
|
||||
|
||||
export type ObjectDef<OpSc extends OptionsSchema = OptionsSchema> = {
|
||||
id: string;
|
||||
name: string;
|
||||
path?: string;
|
||||
options: {
|
||||
schema: OpSc;
|
||||
default: GetOptionsSchemaValues<OpSc>;
|
||||
};
|
||||
placement: 'top' | 'side' | 'bottom' | 'wall' | 'ceiling' | 'floor';
|
||||
hasCollisions?: boolean;
|
||||
hasTexture?: boolean;
|
||||
canPreMeshesMerging?: boolean;
|
||||
//groupingMeshes: string[]; // multi-materialなメッシュは複数のメッシュに分割されるが、それだと不便な場合に追加の親メッシュでグルーピングするための指定
|
||||
isChair?: boolean;
|
||||
treatLoaderResult?: (loaderResult: BABYLON.AssetContainer) => void;
|
||||
createInstance: (args: {
|
||||
room?: RoomEngine | null;
|
||||
scene: BABYLON.Scene;
|
||||
root: BABYLON.Mesh;
|
||||
options: Readonly<GetOptionsSchemaValues<OpSc>>;
|
||||
model: ModelManager;
|
||||
id: string;
|
||||
stickyMarkerMeshUpdated?: (mesh: BABYLON.Mesh) => void;
|
||||
}) => RoomObjectInstance<GetOptionsSchemaValues<OpSc>> | Promise<RoomObjectInstance<GetOptionsSchemaValues<OpSc>>>; // TODO: createInstanceをasyncにするのではなく、別にreadyみたいなものを返させる
|
||||
};
|
||||
|
||||
export function defineObject<const OpSc extends OptionsSchema>(def: ObjectDef<OpSc>): ObjectDef<OpSc> {
|
||||
return def;
|
||||
}
|
||||
|
||||
export function defineObjectClass<const OpSc extends OptionsSchema>(baseDef: Partial<ObjectDef<OpSc>>): {
|
||||
extend: (childDef: Partial<ObjectDef<OpSc>>) => ObjectDef<OpSc>;
|
||||
} {
|
||||
return {
|
||||
extend: (childDef) => ({ ...baseDef, ...childDef }) as ObjectDef<OpSc>,
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const a4Case = defineObject({
|
||||
id: 'a4Case',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const aircon = defineObject({
|
||||
id: 'aircon',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
||||
import { cm, createPlaneUvMapper } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { cm, WORLD_SCALE, createPlaneUvMapper } from '../../utility.js';
|
||||
|
||||
export const allInOnePc = defineObject({
|
||||
id: 'allInOnePc',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { cm } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { cm } from '../../utility.js';
|
||||
|
||||
export const aquarium = defineObject({
|
||||
id: 'aquarium',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const aromaReedDiffuser = defineObject({
|
||||
id: 'aromaReedDiffuser',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const banknote = defineObject({
|
||||
id: 'banknote',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
||||
import { cm } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { cm, WORLD_SCALE } from '../../utility.js';
|
||||
|
||||
export const beamLamp = defineObject({
|
||||
id: 'beamLamp',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const bed = defineObject({
|
||||
id: 'bed',
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { cm, createOverridedStates } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { cm } from '../../utility.js';
|
||||
import { createOverridedStates } from '../utility.js';
|
||||
|
||||
export const blind = defineObject({
|
||||
id: 'blind',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const book = defineObject({
|
||||
id: 'book',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { cm } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { cm } from '../../utility.js';
|
||||
|
||||
export const books = defineObject({
|
||||
id: 'books',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const cactusS = defineObject({
|
||||
id: 'cactusS',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const cardboardBox = defineObject({
|
||||
id: 'cardboardBox',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const ceilingFanLight = defineObject({
|
||||
id: 'ceilingFanLight',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const chair = defineObject({
|
||||
id: 'chair',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const coffeeCup = defineObject({
|
||||
id: 'coffeeCup',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const colorBox = defineObject({
|
||||
id: 'colorBox',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const cuboid = defineObject({
|
||||
id: 'cuboid',
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { cm, yuge } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { cm } from '../../utility.js';
|
||||
import { yuge } from '../utility.js';
|
||||
|
||||
export const cupNoodle = defineObject({
|
||||
id: 'cupNoodle',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const custardPudding = defineObject({
|
||||
id: 'custardPudding',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const debugHipoly = defineObject({
|
||||
id: 'debugHipoly',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const desk = defineObject({
|
||||
id: 'desk',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
||||
import { cm } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { cm, WORLD_SCALE } from '../../utility.js';
|
||||
|
||||
export const desktopPc = defineObject({
|
||||
id: 'desktopPc',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const djMixer = defineObject({
|
||||
id: 'djMixer',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { createPlaneUvMapper, normalizeUvToSquare } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { createPlaneUvMapper, normalizeUvToSquare } from '../../utility.js';
|
||||
|
||||
export const djPlayer = defineObject({
|
||||
id: 'djPlayer',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const ductTape = defineObject({
|
||||
id: 'ductTape',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const emptyBento = defineObject({
|
||||
id: 'emptyBento',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const energyDrink = defineObject({
|
||||
id: 'energyDrink',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const envelope = defineObject({
|
||||
id: 'envelope',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const facialTissue = defineObject({
|
||||
id: 'facialTissue',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const hangingTShirt = defineObject({
|
||||
id: 'hangingTShirt',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const icosahedron = defineObject({
|
||||
id: 'icosahedron',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject, defineObjectClass } from '../engine.js';
|
||||
import { defineObject, defineObjectClass } from '../object.js';
|
||||
|
||||
const base = defineObjectClass({
|
||||
options: {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const ironFrameTable = defineObject({
|
||||
id: 'ironFrameTable',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const keyboard = defineObject({
|
||||
id: 'keyboard',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
||||
import { cm, createPlaneUvMapper } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { cm, WORLD_SCALE, createPlaneUvMapper } from '../../utility.js';
|
||||
|
||||
export const laptopPc = defineObject({
|
||||
id: 'laptopPc',
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
||||
import { cm } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { cm, WORLD_SCALE } from '../../utility.js';
|
||||
|
||||
export const lavaLamp = defineObject({
|
||||
id: 'lavaLamp',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const letterCase = defineObject({
|
||||
id: 'letterCase',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const miObjet = defineObject({
|
||||
id: 'miObjet',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const miPlate = defineObject({
|
||||
id: 'miPlate',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const miPlateDisplayed = defineObject({
|
||||
id: 'miPlateDisplayed',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const milk = defineObject({
|
||||
id: 'milk',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const mixer = defineObject({
|
||||
id: 'mixer',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const monitor = defineObject({
|
||||
id: 'monitor',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const monitorSpeaker = defineObject({
|
||||
id: 'monitorSpeaker',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const monstera = defineObject({
|
||||
id: 'monstera',
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { cm, yuge } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { cm } from '../../utility.js';
|
||||
import { yuge } from '../utility.js';
|
||||
|
||||
export const mug = defineObject({
|
||||
id: 'mug',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const newtonsCradle = defineObject({
|
||||
id: 'newtonsCradle',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const openedCardboardBox = defineObject({
|
||||
id: 'openedCardboardBox',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const pachira = defineObject({
|
||||
id: 'pachira',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const pc = defineObject({
|
||||
id: 'pc',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const petBottle = defineObject({
|
||||
id: 'petBottle',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const piano = defineObject({
|
||||
id: 'piano',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { createPlaneUvMapper } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { createPlaneUvMapper } from '../../utility.js';
|
||||
|
||||
// NOTE: シェイプキーのnormalのエクスポートは無効にしないとmatを大きくしたときに面のレンダリングがグリッチする
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const pizza = defineObject({
|
||||
id: 'pizza',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const plant = defineObject({
|
||||
id: 'plant',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const plant2 = defineObject({
|
||||
id: 'plant2',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../../utility.js';
|
||||
|
||||
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const powerStrip = defineObject({
|
||||
id: 'powerStrip',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const radiometer = defineObject({
|
||||
id: 'radiometer',
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import seedrandom from 'seedrandom';
|
||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { WORLD_SCALE } from '@/world/utility.js';
|
||||
|
||||
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const rolledUpPoster = defineObject({
|
||||
id: 'rolledUpPoster',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const roundRug = defineObject({
|
||||
id: 'roundRug',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const router = defineObject({
|
||||
id: 'router',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const siphon = defineObject({
|
||||
id: 'siphon',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const snakeplant = defineObject({
|
||||
id: 'snakeplant',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const speaker = defineObject({
|
||||
id: 'speaker',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const sprayer = defineObject({
|
||||
id: 'sprayer',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const steelRack = defineObject({
|
||||
id: 'steelRack',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const tabletopCalendar = defineObject({
|
||||
id: 'tabletopCalendar',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
||||
import { cm, get7segMeshesOfCurrentTime } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { cm, get7segMeshesOfCurrentTime, WORLD_SCALE } from '@/world/utility.js';
|
||||
|
||||
export const tabletopDigitalClock = defineObject({
|
||||
id: 'tabletopDigitalClock',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { createPlaneUvMapper } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { createPlaneUvMapper } from '../../utility.js';
|
||||
|
||||
export const tabletopFlag = defineObject({
|
||||
id: 'tabletopFlag',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../../utility.js';
|
||||
|
||||
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const tabletopIronFrameStand = defineObject({
|
||||
id: 'tabletopIronFrameStand',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { createPlaneUvMapper } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { createPlaneUvMapper } from '../../utility.js';
|
||||
|
||||
// NOTE: シェイプキーのnormalのエクスポートは無効にしないとmatを大きくしたときに面のレンダリングがグリッチする
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../../utility.js';
|
||||
|
||||
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const tetrapod = defineObject({
|
||||
id: 'tetrapod',
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
||||
import { cm, createPlaneUvMapper, initTv } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { initTv } from '../utility.js';
|
||||
import { cm, WORLD_SCALE } from '@/world/utility.js';
|
||||
|
||||
export const tv = defineObject({
|
||||
id: 'tv',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const twistedCubeObjet = defineObject({
|
||||
id: 'twistedCubeObjet',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const usedTissue = defineObject({
|
||||
id: 'usedTissue',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../../utility.js';
|
||||
|
||||
export const wallCanvas = defineObject({
|
||||
id: 'wallCanvas',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const wallClock = defineObject({
|
||||
id: 'wallClock',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../../utility.js';
|
||||
|
||||
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const wallMirror = defineObject({
|
||||
id: 'wallMirror',
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const wallShelf = defineObject({
|
||||
id: 'wallShelf',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
||||
import { cm } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { cm, WORLD_SCALE } from '@/world/utility.js';
|
||||
|
||||
export const woodRingFloorLamp = defineObject({
|
||||
id: 'woodRingFloorLamp',
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
||||
import { cm } from '../utility.js';
|
||||
import { defineObject } from '../object.js';
|
||||
import { cm, WORLD_SCALE } from '@/world/utility.js';
|
||||
|
||||
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { defineObject } from '../engine.js';
|
||||
import { defineObject } from '../object.js';
|
||||
|
||||
export const woodSoundAbsorbingPanel = defineObject({
|
||||
id: 'woodSoundAbsorbingPanel',
|
||||
|
||||
250
packages/frontend/src/world/room/previewEngine.ts
Normal file
250
packages/frontend/src/world/room/previewEngine.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic.js';
|
||||
import { GridMaterial } from '@babylonjs/materials';
|
||||
import { camelToKebab, WORLD_SCALE, cm, getMeshesBoundingBox } from '../utility.js';
|
||||
import { getObjectDef } from './object-defs.js';
|
||||
import { SYSTEM_MESH_NAMES, ModelManager } from './utility.js';
|
||||
import type { RoomObjectInstance } from './object.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
|
||||
export async function createRoomObjectPreviewEngine(canvas: HTMLCanvasElement) {
|
||||
//const babylonEngine = new BABYLON.WebGPUEngine(canvas);
|
||||
//babylonEngine.compatibilityMode = false;
|
||||
//await babylonEngine.initAsync();
|
||||
const babylonEngine = new BABYLON.Engine(canvas, false, { alpha: false, antialias: false });
|
||||
return new RoomObjectPreviewEngine({ canvas, engine: babylonEngine });
|
||||
}
|
||||
|
||||
export class RoomObjectPreviewEngine {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private engine: BABYLON.WebGPUEngine;
|
||||
private scene: BABYLON.Scene;
|
||||
private shadowGenerator1: BABYLON.ShadowGenerator;
|
||||
private camera: BABYLON.ArcRotateCamera;
|
||||
private objectMesh: BABYLON.Mesh | null = null;
|
||||
private objectInstance: RoomObjectInstance<any> | null = null;
|
||||
private envMapIndoor: BABYLON.CubeTexture;
|
||||
private roomLight: BABYLON.SpotLight;
|
||||
private zGridPreviewPlane: BABYLON.Mesh;
|
||||
private fps = 60;
|
||||
|
||||
constructor(options: {
|
||||
canvas: HTMLCanvasElement;
|
||||
engine: BABYLON.WebGPUEngine;
|
||||
}) {
|
||||
this.canvas = options.canvas;
|
||||
|
||||
registerBuiltInLoaders();
|
||||
|
||||
this.engine = options.engine;
|
||||
this.scene = new BABYLON.Scene(this.engine);
|
||||
|
||||
this.scene.ambientColor = new BABYLON.Color3(1.0, 0.9, 0.8);
|
||||
|
||||
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.scene);
|
||||
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(500), cm(500), cm(500));
|
||||
|
||||
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;
|
||||
this.camera.lowerBetaLimit = 0;
|
||||
this.camera.upperBetaLimit = (Math.PI / 2) + 0.1;
|
||||
this.camera.lowerRadiusLimit = cm(50);
|
||||
this.camera.upperRadiusLimit = cm(1000);
|
||||
this.camera.useAutoRotationBehavior = true;
|
||||
this.camera.autoRotationBehavior!.idleRotationSpeed = 0.3;
|
||||
//this.camera.mode = BABYLON.Camera.ORTHOGRAPHIC_CAMERA;
|
||||
this.scene.activeCamera = this.camera;
|
||||
|
||||
const ambientLight = new BABYLON.HemisphericLight('ambientLight', new BABYLON.Vector3(0, 1, -0.5), this.scene);
|
||||
ambientLight.diffuse = new BABYLON.Color3(1.0, 1.0, 1.0);
|
||||
ambientLight.intensity = 0.5;
|
||||
//ambientLight.intensity = 0;
|
||||
|
||||
this.roomLight = new BABYLON.SpotLight('roomLight', new BABYLON.Vector3(0, cm(249), 0), new BABYLON.Vector3(0, -1, 0), 16, 8, this.scene);
|
||||
this.roomLight.diffuse = new BABYLON.Color3(1.0, 0.9, 0.8);
|
||||
this.roomLight.shadowMinZ = cm(10);
|
||||
this.roomLight.shadowMaxZ = cm(300);
|
||||
|
||||
this.shadowGenerator1 = new BABYLON.ShadowGenerator(4096, this.roomLight);
|
||||
this.shadowGenerator1.forceBackFacesOnly = true;
|
||||
this.shadowGenerator1.bias = 0.0001;
|
||||
this.shadowGenerator1.usePercentageCloserFiltering = true;
|
||||
this.shadowGenerator1.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
|
||||
//this.shadowGenerator1.useContactHardeningShadow = true;
|
||||
|
||||
const gridMaterial = new GridMaterial('grid', this.scene);
|
||||
gridMaterial.lineColor = new BABYLON.Color3(0.5, 0.5, 0.5);
|
||||
gridMaterial.mainColor = new BABYLON.Color3(0, 0, 0);
|
||||
gridMaterial.minorUnitVisibility = 1;
|
||||
gridMaterial.opacity = 0.5;
|
||||
gridMaterial.gridRatio = cm(10);
|
||||
|
||||
//this.zGridPreviewPlane = BABYLON.MeshBuilder.CreatePlane('zGridPreviewPlane', { width: cm(1000), height: cm(1000) }, this.scene);
|
||||
//this.zGridPreviewPlane.material = gridMaterial;
|
||||
//this.zGridPreviewPlane.rotation = new BABYLON.Vector3(Math.PI / 2, 0, 0);
|
||||
|
||||
//this.scene.fogMode = BABYLON.Scene.FOGMODE_LINEAR;
|
||||
//this.scene.fogStart = cm(100);
|
||||
//this.scene.fogEnd = cm(110);
|
||||
//this.scene.fogColor = new BABYLON.Color3(0.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
public async init() {
|
||||
const frameInterval = 1000 / this.fps;
|
||||
let lastTime = performance.now();
|
||||
|
||||
this.engine.runRenderLoop(() => {
|
||||
const currentTime = performance.now();
|
||||
const delta = currentTime - lastTime;
|
||||
|
||||
if (delta >= frameInterval) {
|
||||
this.scene.render();
|
||||
lastTime = currentTime - (delta % frameInterval);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public async load(type: string) {
|
||||
if (this.objectInstance != null) {
|
||||
this.objectInstance.dispose?.();
|
||||
this.objectInstance = null;
|
||||
this.objectMesh!.dispose();
|
||||
}
|
||||
|
||||
// reset camera rotation
|
||||
this.camera.setPosition(new BABYLON.Vector3(0, cm(90), cm(300)));
|
||||
|
||||
const def = getObjectDef(type);
|
||||
|
||||
const options = deepClone(def.options.default);
|
||||
|
||||
const id = genId();
|
||||
|
||||
await this.loadObject({
|
||||
type,
|
||||
options,
|
||||
id,
|
||||
});
|
||||
|
||||
// なぜかちょっと待たないとbounding boxのサイズが正しくない
|
||||
window.setTimeout(() => {
|
||||
const boundingInfo = getMeshesBoundingBox(this.objectMesh!.getChildMeshes().filter(m => m.isEnabled() && m.isVisible));
|
||||
this.camera.setTarget(new BABYLON.Vector3(0, boundingInfo.center.y, 0));
|
||||
|
||||
// zoom to fit
|
||||
const size = boundingInfo.extendSize;
|
||||
const distance = Math.max(size.x, size.y, size.z) * 2;
|
||||
this.camera.radius = distance * 3;
|
||||
//this.camera.orthoLeft = -distance;
|
||||
//this.camera.orthoRight = distance;
|
||||
//this.camera.orthoTop = distance;
|
||||
//this.camera.orthoBottom = -distance;
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// TODO: RoomEngineのものとほぼ同じだからいい感じに共通化
|
||||
private async loadObject(args: {
|
||||
type: string;
|
||||
options: any;
|
||||
id: string;
|
||||
}) {
|
||||
const def = getObjectDef(args.type);
|
||||
|
||||
const root = new BABYLON.Mesh(`object_${args.type}`, this.scene);
|
||||
|
||||
const filePath = def.path != null ? `/client-assets/room/objects/${def.path}.glb` : `/client-assets/room/objects/${camelToKebab(args.type)}/${camelToKebab(args.type)}.glb`;
|
||||
const loaderResult = await BABYLON.LoadAssetContainerAsync(filePath, this.scene);
|
||||
|
||||
// 不要なUVを掃除
|
||||
if (!def.hasTexture) {
|
||||
for (const m of loaderResult.meshes) {
|
||||
if (m.geometry != null) {
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UVKind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV2Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV3Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV4Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV5Kind);
|
||||
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV6Kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// babylonによって自動で追加される右手系変換用ノード
|
||||
const subRoot = loaderResult.meshes[0];
|
||||
subRoot.scaling = subRoot.scaling.scale(WORLD_SCALE);// cmをmに
|
||||
|
||||
def.treatLoaderResult?.(loaderResult);
|
||||
|
||||
root.addChild(subRoot);
|
||||
|
||||
const model = new ModelManager(subRoot, loaderResult.meshes.filter(m => !m.isDisposed() && m !== subRoot), def.hasTexture, (meshes) => {
|
||||
for (const m of meshes) {
|
||||
const mesh = m;
|
||||
|
||||
// シェイプキー(morph)を考慮してbounding boxを更新するために必要
|
||||
mesh.refreshBoundingInfo({ applyMorph: true });
|
||||
|
||||
if (SYSTEM_MESH_NAMES.some(n => mesh.name.includes(n))) {
|
||||
mesh.receiveShadows = false;
|
||||
mesh.isVisible = false;
|
||||
} else {
|
||||
if (def.receiveShadows !== false) mesh.receiveShadows = true;
|
||||
if (def.castShadows !== false) {
|
||||
this.shadowGenerator1.addShadowCaster(mesh);
|
||||
}
|
||||
|
||||
if (mesh.material) {
|
||||
if (mesh.material instanceof BABYLON.MultiMaterial) {
|
||||
for (const subMat of mesh.material.subMaterials) {
|
||||
(subMat as BABYLON.PBRMaterial).reflectionTexture = this.envMapIndoor;
|
||||
(subMat as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
|
||||
}
|
||||
} else {
|
||||
(mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.envMapIndoor;
|
||||
(mesh.material as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.scene.meshes.includes(mesh)) this.scene.addMesh(mesh);
|
||||
}
|
||||
});
|
||||
|
||||
const objectInstance = await def.createInstance({
|
||||
room: null,
|
||||
scene: this.scene,
|
||||
root,
|
||||
options: args.options,
|
||||
model,
|
||||
id: args.id,
|
||||
});
|
||||
|
||||
objectInstance.onInited?.();
|
||||
|
||||
model.bakeMesh();
|
||||
|
||||
this.objectInstance = objectInstance;
|
||||
this.objectMesh = root;
|
||||
}
|
||||
|
||||
public updateObjectOption(key: string, value: any) {
|
||||
this.objectInstance?.onOptionsUpdated?.([key, value]);
|
||||
}
|
||||
|
||||
public resize() {
|
||||
this.engine.resize();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
this.engine.dispose();
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,10 @@
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
import { applyMorphTargetsToMesh, cm, getPlaneUvIndexes } from '../utility.js';
|
||||
import type { RoomEngine } from './engine.js';
|
||||
|
||||
//export const cm = (value: number) => value / 100;
|
||||
export const cm = (value: number) => value;
|
||||
export const SYSTEM_MESH_NAMES = ['__TOP__', '__SIDE__', '__PICK__', '__COLLISION__'];
|
||||
|
||||
export function yuge(scene: BABYLON.Scene, mesh: BABYLON.Mesh, offset: BABYLON.Vector3) {
|
||||
const emitter = new BABYLON.TransformNode('emitter', scene);
|
||||
@@ -41,182 +41,6 @@ export function yuge(scene: BABYLON.Scene, mesh: BABYLON.Mesh, offset: BABYLON.V
|
||||
};
|
||||
}
|
||||
|
||||
export class HorizontalCameraKeyboardMoveInput extends BABYLON.BaseCameraPointersInput {
|
||||
public camera: BABYLON.FreeCamera;
|
||||
private engine: BABYLON.AbstractEngine;
|
||||
private scene: BABYLON.Scene;
|
||||
preShift = false;
|
||||
codes = [];
|
||||
codesUp = ['KeyW'];
|
||||
codesDown = ['KeyS'];
|
||||
codesLeft = ['KeyA'];
|
||||
codesRight = ['KeyD'];
|
||||
onCanvasBlurObserver = null;
|
||||
onKeyboardObserver = null;
|
||||
public canMove = true;
|
||||
|
||||
constructor(camera: BABYLON.UniversalCamera) {
|
||||
super();
|
||||
this.camera = camera;
|
||||
this.scene = this.camera.getScene();
|
||||
this.engine = this.scene.getEngine();
|
||||
}
|
||||
|
||||
attachControl() {
|
||||
if (this.onCanvasBlurObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onCanvasBlurObserver = this.engine.onCanvasBlurObservable.add(() => {
|
||||
this.codes = [];
|
||||
});
|
||||
|
||||
this.onKeyboardObserver = this.scene.onKeyboardObservable.add(({ event, type }) => {
|
||||
const { code, shiftKey } = event;
|
||||
this.preShift = shiftKey;
|
||||
|
||||
if (type === BABYLON.KeyboardEventTypes.KEYDOWN) {
|
||||
if (this.codesUp.indexOf(code) >= 0 ||
|
||||
this.codesDown.indexOf(code) >= 0 ||
|
||||
this.codesLeft.indexOf(code) >= 0 ||
|
||||
this.codesRight.indexOf(code) >= 0) {
|
||||
const index = this.codes.findIndex(v => v.code === code);
|
||||
if (index < 0) { // 存在しなかったら追加する
|
||||
this.codes.push({ code });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.codesUp.indexOf(code) >= 0 ||
|
||||
this.codesDown.indexOf(code) >= 0 ||
|
||||
this.codesLeft.indexOf(code) >= 0 ||
|
||||
this.codesRight.indexOf(code) >= 0) {
|
||||
const index = this.codes.findIndex(v => v.code === code);
|
||||
if (index >= 0) { // 存在したら削除する
|
||||
this.codes.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
detachControl() {
|
||||
this.codes = [];
|
||||
|
||||
if (this.onKeyboardObserver) this.scene.onKeyboardObservable.remove(this.onKeyboardObserver);
|
||||
if (this.onCanvasBlurObserver) this.engine.onCanvasBlurObservable.remove(this.onCanvasBlurObserver);
|
||||
this.onKeyboardObserver = null;
|
||||
this.onCanvasBlurObserver = null;
|
||||
}
|
||||
|
||||
checkInputs() {
|
||||
if (!this.onKeyboardObserver) {
|
||||
return;
|
||||
}
|
||||
for (let index = 0; index < this.codes.length; index++) {
|
||||
const { code } = this.codes[index];
|
||||
|
||||
const local = new BABYLON.Vector3();
|
||||
if (this.codesLeft.indexOf(code) >= 0) {
|
||||
local.x += -1;
|
||||
} else if (this.codesUp.indexOf(code) >= 0) {
|
||||
local.z += this.scene.useRightHandedSystem ? -1 : 1;
|
||||
} else if (this.codesRight.indexOf(code) >= 0) {
|
||||
local.x += 1;
|
||||
} else if (this.codesDown.indexOf(code) >= 0) {
|
||||
local.z += this.scene.useRightHandedSystem ? 1 : -1;
|
||||
}
|
||||
|
||||
if (local.length() === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (this.canMove) {
|
||||
this.camera.cameraDirection.addInPlace(move);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getClassName() {
|
||||
return 'HorizontalCameraKeyboardMoveInput';
|
||||
}
|
||||
|
||||
getSimpleName() {
|
||||
return 'horizontalkeyboard';
|
||||
}
|
||||
}
|
||||
|
||||
const nanasegNumberMap = [
|
||||
['a', 'b', 'c', 'd', 'e', 'f'], // 0
|
||||
['b', 'c'], // 1
|
||||
['a', 'b', 'd', 'e', 'g'], // 2
|
||||
['a', 'b', 'c', 'd', 'g'], // 3
|
||||
['b', 'c', 'f', 'g'], // 4
|
||||
['a', 'c', 'd', 'f', 'g'], // 5
|
||||
['a', 'c', 'd', 'e', 'f', 'g'], // 6
|
||||
['a', 'b', 'c'], // 7
|
||||
['a', 'b', 'c', 'd', 'e', 'f', 'g'], // 8
|
||||
['a', 'b', 'c', 'd', 'f', 'g'], // 9
|
||||
];
|
||||
|
||||
export function get7segMeshesOfCurrentTime(meshes: {
|
||||
'1a'?: BABYLON.AbstractMesh;
|
||||
'1b'?: BABYLON.AbstractMesh;
|
||||
'1c'?: BABYLON.AbstractMesh;
|
||||
'1d'?: BABYLON.AbstractMesh;
|
||||
'1e'?: BABYLON.AbstractMesh;
|
||||
'1f'?: BABYLON.AbstractMesh;
|
||||
'1g'?: BABYLON.AbstractMesh;
|
||||
'2a'?: BABYLON.AbstractMesh;
|
||||
'2b'?: BABYLON.AbstractMesh;
|
||||
'2c'?: BABYLON.AbstractMesh;
|
||||
'2d'?: BABYLON.AbstractMesh;
|
||||
'2e'?: BABYLON.AbstractMesh;
|
||||
'2f'?: BABYLON.AbstractMesh;
|
||||
'2g'?: BABYLON.AbstractMesh;
|
||||
'3a'?: BABYLON.AbstractMesh;
|
||||
'3b'?: BABYLON.AbstractMesh;
|
||||
'3c'?: BABYLON.AbstractMesh;
|
||||
'3d'?: BABYLON.AbstractMesh;
|
||||
'3e'?: BABYLON.AbstractMesh;
|
||||
'3f'?: BABYLON.AbstractMesh;
|
||||
'3g'?: BABYLON.AbstractMesh;
|
||||
'4a'?: BABYLON.AbstractMesh;
|
||||
'4b'?: BABYLON.AbstractMesh;
|
||||
'4c'?: BABYLON.AbstractMesh;
|
||||
'4d'?: BABYLON.AbstractMesh;
|
||||
'4e'?: BABYLON.AbstractMesh;
|
||||
'4f'?: BABYLON.AbstractMesh;
|
||||
'4g'?: BABYLON.AbstractMesh;
|
||||
}) {
|
||||
const now = new Date();
|
||||
const h = now.getHours();
|
||||
const m = now.getMinutes();
|
||||
|
||||
const chars = [Math.floor(h / 10), h % 10, Math.floor(m / 10), m % 10];
|
||||
|
||||
const result: BABYLON.AbstractMesh[] = [];
|
||||
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const char = chars[i];
|
||||
const segs = nanasegNumberMap[char];
|
||||
for (const seg of segs) {
|
||||
const mesh = meshes[`${i + 1}${seg}`];
|
||||
if (mesh) {
|
||||
result.push(mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function createOverridedStates<T extends Record<string, (() => any)>>(stateDefs: T): { [K in keyof T]: ReturnType<T[K]>; } & { $reset: () => void } {
|
||||
const overridedStates = {} as { [K in keyof T]: ReturnType<T[K]>; };
|
||||
const result = {} as { [K in keyof T]: ReturnType<T[K]>; } & { $reset: () => void };
|
||||
@@ -337,86 +161,6 @@ export function initTv(room: RoomEngine, screenMesh: BABYLON.Mesh) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 0 1
|
||||
* 0 a(x,y) --- b(x,y)
|
||||
* | |
|
||||
* 1 c(x,y) --- d(x,y)
|
||||
*/
|
||||
export function getPlaneUvIndexes(mesh: BABYLON.Mesh) {
|
||||
const uvs = mesh.getVerticesData(BABYLON.VertexBuffer.UVKind);
|
||||
if (uvs == null) {
|
||||
throw new Error('Mesh does not have UV data');
|
||||
}
|
||||
|
||||
let aIndex = 0;
|
||||
let bIndex = 0;
|
||||
let cIndex = 0;
|
||||
let dIndex = 0;
|
||||
|
||||
for (let i = 0; i < 8; i += 2) {
|
||||
const x = uvs[i];
|
||||
const y = uvs[i + 1];
|
||||
|
||||
// 多少ずれがあってもいいように(例えばblenderではUV展開時にデフォルトでわずかなマージンを追加する)、中心より大きいか/小さいかで判定する
|
||||
if (x < 0.5 && y < 0.5) {
|
||||
aIndex = i;
|
||||
} else if (x > 0.5 && y < 0.5) {
|
||||
bIndex = i;
|
||||
} else if (x < 0.5 && y > 0.5) {
|
||||
cIndex = i;
|
||||
} else if (x > 0.5 && y > 0.5) {
|
||||
dIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return [aIndex, bIndex, cIndex, dIndex];
|
||||
}
|
||||
|
||||
export function normalizeUvToSquare(mesh: BABYLON.Mesh) {
|
||||
const uvs = mesh.getVerticesData(BABYLON.VertexBuffer.UVKind)!;
|
||||
const uvIndexes = getPlaneUvIndexes(mesh);
|
||||
uvs[uvIndexes[0]] = 0;
|
||||
uvs[uvIndexes[0] + 1] = 0;
|
||||
uvs[uvIndexes[1]] = 1;
|
||||
uvs[uvIndexes[1] + 1] = 0;
|
||||
uvs[uvIndexes[2]] = 0;
|
||||
uvs[uvIndexes[2] + 1] = 1;
|
||||
uvs[uvIndexes[3]] = 1;
|
||||
uvs[uvIndexes[3] + 1] = 1;
|
||||
mesh.updateVerticesData(BABYLON.VertexBuffer.UVKind, uvs);
|
||||
}
|
||||
|
||||
export function createPlaneUvMapper(mesh: BABYLON.Mesh) {
|
||||
mesh.markVerticesDataAsUpdatable(BABYLON.VertexBuffer.UVKind, true);
|
||||
|
||||
const uvs = mesh.getVerticesData(BABYLON.VertexBuffer.UVKind)!;
|
||||
const originalUvs = uvs.slice();
|
||||
|
||||
return (srcAspect: number, targetAspect: number, method: 'cover' | 'contain' | 'stretch') => {
|
||||
let uScale = 1;
|
||||
let vScale = 1;
|
||||
const ratio = targetAspect / srcAspect;
|
||||
if (method === 'cover') {
|
||||
uScale = ratio < 1 ? ratio : 1;
|
||||
vScale = ratio < 1 ? 1 : 1 / ratio;
|
||||
} else if (method === 'contain') {
|
||||
uScale = ratio > 1 ? ratio : 1;
|
||||
vScale = ratio > 1 ? 1 : 1 / ratio;
|
||||
} else if (method === 'stretch') {
|
||||
// nop
|
||||
}
|
||||
|
||||
// (0,0)を隅ではなく中心として扱いたいので0.5引いて計算してから最後に0.5足す
|
||||
for (let i = 0; i < uvs.length; i += 2) {
|
||||
uvs[i] = ((originalUvs[i] - 0.5) * uScale) + 0.5;
|
||||
uvs[i + 1] = ((originalUvs[i + 1] - 0.5) * vScale) + 0.5;
|
||||
}
|
||||
|
||||
mesh.updateVerticesData(BABYLON.VertexBuffer.UVKind, uvs);
|
||||
};
|
||||
}
|
||||
|
||||
export function findMaterial(rootMesh: BABYLON.AbstractMesh, keyword: string, allowMultiMaterial = false): BABYLON.PBRMaterial {
|
||||
for (const m of rootMesh.getChildMeshes()) {
|
||||
if (m.material == null) continue;
|
||||
@@ -442,87 +186,153 @@ export function findMaterial(rootMesh: BABYLON.AbstractMesh, keyword: string, al
|
||||
throw new Error(`Material with keyword "${keyword}" not found`);
|
||||
}
|
||||
|
||||
export function scaleMorph(mesh: BABYLON.Mesh, scale: [number, number, number], offset: [number, number, number] = [0, 0, 0]) {
|
||||
if (!mesh.morphTargetManager) {
|
||||
return;
|
||||
export class ModelManager {
|
||||
public root: BABYLON.Mesh;
|
||||
public bakedCallback: (() => void) | null = null;
|
||||
public bakeExcludeMeshes: BABYLON.Mesh[] = [];
|
||||
private originalMeshes: BABYLON.Mesh[] = [];
|
||||
private bakedMeshes: BABYLON.Mesh[] = [];
|
||||
private hasTexture: boolean;
|
||||
|
||||
constructor(root: BABYLON.Mesh, originalMeshes: BABYLON.Mesh[], hasTexture: boolean, bakedCallback: (() => void) | null = null) {
|
||||
this.root = root;
|
||||
this.originalMeshes = originalMeshes;
|
||||
this.hasTexture = hasTexture;
|
||||
this.bakedCallback = bakedCallback;
|
||||
}
|
||||
|
||||
const morphTargetManager = mesh.morphTargetManager;
|
||||
|
||||
for (let targetIndex = 0; targetIndex < morphTargetManager.numTargets; targetIndex++) {
|
||||
const target = morphTargetManager.getTarget(targetIndex);
|
||||
const newPos = target.getPositions();
|
||||
for (let i = 0; i < newPos.length; i += 3) {
|
||||
newPos[i] = (newPos[i] + offset[0]) * scale[0];
|
||||
newPos[i + 1] = (newPos[i + 1] + offset[1]) * scale[1];
|
||||
newPos[i + 2] = (newPos[i + 2] + offset[2]) * scale[2];
|
||||
public findMesh(keyword: string) {
|
||||
const mesh = this.root.getChildMeshes().find(m => m.name.includes(keyword));
|
||||
if (mesh == null) {
|
||||
throw new Error(`Mesh with keyword "${keyword}" not found for object ${this.root.metadata?.objectType}`);
|
||||
}
|
||||
target.setPositions(newPos);
|
||||
return mesh as BABYLON.Mesh;
|
||||
}
|
||||
|
||||
morphTargetManager.synchronize();
|
||||
public findMeshes(keyword: string) {
|
||||
const meshes = this.root.getChildMeshes().filter(m => m.name.includes(keyword));
|
||||
return meshes as BABYLON.Mesh[];
|
||||
}
|
||||
|
||||
mesh.refreshBoundingInfo();
|
||||
mesh.computeWorldMatrix(true);
|
||||
public findMaterial(keyword: string) {
|
||||
return findMaterial(this.root, keyword);
|
||||
}
|
||||
|
||||
public findTransformNode(keyword: string) {
|
||||
const node = this.root.getChildTransformNodes().find(n => n.name.includes(keyword));
|
||||
if (node == null) {
|
||||
throw new Error(`TransformNode with keyword "${keyword}" not found for object ${this.root.metadata?.objectType}`);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
public updated() {
|
||||
}
|
||||
|
||||
public bakeMesh() {
|
||||
for (const m of this.bakedMeshes) {
|
||||
m.dispose();
|
||||
}
|
||||
this.bakedMeshes = [];
|
||||
|
||||
const excludeMeshes = [...this.bakeExcludeMeshes, ...this.root.getChildMeshes().filter(m => SYSTEM_MESH_NAMES.some(s => m.name.includes(s)))];
|
||||
|
||||
const childMeshes = this.root.getChildMeshes().filter(m => !excludeMeshes.some(x => x === m) && m.isVisible && !m.isDisposed());
|
||||
|
||||
if (childMeshes.length <= 1) {
|
||||
this.bakedCallback?.([...childMeshes, ...excludeMeshes]);
|
||||
return;
|
||||
}
|
||||
|
||||
const _toMerge = [] as BABYLON.Mesh[];
|
||||
for (const mesh of childMeshes) {
|
||||
let fixedMesh = mesh;
|
||||
fixedMesh.setEnabled(false);
|
||||
|
||||
if (mesh instanceof BABYLON.InstancedMesh) {
|
||||
const sourceMesh = mesh.sourceMesh;
|
||||
const realizedMesh = sourceMesh.clone(mesh.name + '_realized', null, true);
|
||||
realizedMesh.getScene().removeMesh(realizedMesh);
|
||||
|
||||
realizedMesh.position = mesh.position.clone();
|
||||
if (mesh.rotationQuaternion) {
|
||||
realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone();
|
||||
} else {
|
||||
realizedMesh.rotation = mesh.rotation.clone();
|
||||
}
|
||||
realizedMesh.scaling = mesh.scaling.clone();
|
||||
realizedMesh.parent = mesh.parent;
|
||||
realizedMesh.setEnabled(false);
|
||||
|
||||
fixedMesh = realizedMesh;
|
||||
}
|
||||
|
||||
_toMerge.push(fixedMesh);
|
||||
}
|
||||
|
||||
const toMerge = [] as BABYLON.Mesh[];
|
||||
for (const mesh of _toMerge) {
|
||||
const newMesh = mesh.name.endsWith('_realized') ? mesh : mesh.clone(mesh.name + '_bakeMerged', null, true);
|
||||
newMesh.makeGeometryUnique();
|
||||
applyMorphTargetsToMesh(newMesh);
|
||||
if (newMesh.parent === this.root) {
|
||||
newMesh.parent = null;
|
||||
} else {
|
||||
newMesh.setParent(this.root);
|
||||
//newMesh.bakeCurrentTransformIntoVertices();
|
||||
newMesh.parent = null;
|
||||
}
|
||||
//newMesh.bakeCurrentTransformIntoVertices();
|
||||
|
||||
if (this.hasTexture) {
|
||||
if (newMesh.getVerticesData(BABYLON.VertexBuffer.UVKind) == null) {
|
||||
const vertexCount = newMesh.getTotalVertices();
|
||||
const uvs = new Array(vertexCount * 2).fill(0);
|
||||
newMesh.setVerticesData(BABYLON.VertexBuffer.UVKind, uvs, false, 2);
|
||||
}
|
||||
if (newMesh.getVerticesData(BABYLON.VertexBuffer.UV2Kind) == null) {
|
||||
const vertexCount = newMesh.getTotalVertices();
|
||||
const uvs = new Array(vertexCount * 2).fill(0);
|
||||
newMesh.setVerticesData(BABYLON.VertexBuffer.UV2Kind, uvs, false, 2);
|
||||
}
|
||||
}
|
||||
|
||||
toMerge.push(newMesh);
|
||||
}
|
||||
|
||||
if (toMerge.length === 0) {
|
||||
this.bakedCallback?.([...childMeshes, ...excludeMeshes]);
|
||||
return;
|
||||
}
|
||||
|
||||
const merged = BABYLON.Mesh.MergeMeshes(toMerge, true, false, undefined, false, true);
|
||||
merged.parent = this.root;
|
||||
merged.material.freeze();
|
||||
if (merged.material instanceof BABYLON.MultiMaterial) {
|
||||
for (const subMat of merged.material.subMaterials) {
|
||||
(subMat as BABYLON.PBRMaterial).freeze();
|
||||
}
|
||||
}
|
||||
merged.freezeWorldMatrix();
|
||||
merged.metadata = { ...this.root.metadata };
|
||||
if (!this.hasTexture) merged.convertToUnIndexedMesh();
|
||||
this.bakedMeshes = [merged];
|
||||
|
||||
this.bakedCallback?.([...this.bakedMeshes, ...excludeMeshes]);
|
||||
}
|
||||
|
||||
public unbakeMesh() {
|
||||
for (const m of this.bakedMeshes) {
|
||||
m.dispose();
|
||||
}
|
||||
this.bakedMeshes = [];
|
||||
|
||||
const childMeshes = this.root.getChildMeshes();
|
||||
|
||||
for (const mesh of childMeshes) {
|
||||
mesh.setEnabled(true);
|
||||
}
|
||||
|
||||
this.bakedCallback?.(this.root.getChildMeshes());
|
||||
}
|
||||
}
|
||||
|
||||
export function applyMorphTargetsToMesh(mesh: BABYLON.Mesh) {
|
||||
if (!mesh.morphTargetManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const morphTargetManager = mesh.morphTargetManager;
|
||||
const positions = mesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
|
||||
|
||||
if (!positions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a copy of the original positions to work with
|
||||
const finalPositions = positions.slice();
|
||||
|
||||
// Apply each morph target
|
||||
for (let targetIndex = 0; targetIndex < morphTargetManager.numTargets; targetIndex++) {
|
||||
const target = morphTargetManager.getTarget(targetIndex);
|
||||
const influence = target.influence;
|
||||
|
||||
if (influence === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the morph target positions
|
||||
const targetPositions = target.getPositions();
|
||||
|
||||
if (!targetPositions || targetPositions.length !== positions.length) {
|
||||
console.warn(`Morph target ${targetIndex} has invalid position data`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply the morph target influence to each vertex
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
finalPositions[i] += (targetPositions[i] - positions[i]) * influence;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the mesh with the morphed positions
|
||||
mesh.setVerticesData(BABYLON.VertexBuffer.PositionKind, finalPositions);
|
||||
|
||||
//// Update normals if available
|
||||
//const normals = mesh.getVerticesData(BABYLON.VertexBuffer.NormalKind);
|
||||
//if (normals) {
|
||||
// // For simplicity, we'll just recompute the normals
|
||||
// mesh.createNormals(true);
|
||||
//}
|
||||
|
||||
// Refresh the mesh
|
||||
mesh.refreshBoundingInfo();
|
||||
mesh.computeWorldMatrix(true);
|
||||
}
|
||||
|
||||
// ex) hangingTShirt -> hanging-t-shirt
|
||||
export const camelToKebab = (s: string) => {
|
||||
return s
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
||||
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
||||
.toLowerCase();
|
||||
};
|
||||
|
||||
393
packages/frontend/src/world/utility.ts
Normal file
393
packages/frontend/src/world/utility.ts
Normal file
@@ -0,0 +1,393 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import * as BABYLON from '@babylonjs/core';
|
||||
|
||||
export const WORLD_SCALE = 100;
|
||||
|
||||
//export const cm = (value: number) => value / 100;
|
||||
export const cm = (value: number) => value;
|
||||
|
||||
export const TIME_MAP = {
|
||||
0: 2,
|
||||
1: 2,
|
||||
2: 2,
|
||||
3: 2,
|
||||
4: 2,
|
||||
5: 1,
|
||||
6: 1,
|
||||
7: 0,
|
||||
8: 0,
|
||||
9: 0,
|
||||
10: 0,
|
||||
11: 0,
|
||||
12: 0,
|
||||
13: 0,
|
||||
14: 0,
|
||||
15: 0,
|
||||
16: 1,
|
||||
17: 1,
|
||||
18: 2,
|
||||
19: 2,
|
||||
20: 2,
|
||||
21: 2,
|
||||
22: 2,
|
||||
23: 2,
|
||||
} as const;
|
||||
|
||||
export class HorizontalCameraKeyboardMoveInput extends BABYLON.BaseCameraPointersInput {
|
||||
public camera: BABYLON.FreeCamera;
|
||||
private engine: BABYLON.AbstractEngine;
|
||||
private scene: BABYLON.Scene;
|
||||
preShift = false;
|
||||
codes = [];
|
||||
codesUp = ['KeyW'];
|
||||
codesDown = ['KeyS'];
|
||||
codesLeft = ['KeyA'];
|
||||
codesRight = ['KeyD'];
|
||||
onCanvasBlurObserver = null;
|
||||
onKeyboardObserver = null;
|
||||
public canMove = true;
|
||||
|
||||
constructor(camera: BABYLON.UniversalCamera) {
|
||||
super();
|
||||
this.camera = camera;
|
||||
this.scene = this.camera.getScene();
|
||||
this.engine = this.scene.getEngine();
|
||||
}
|
||||
|
||||
attachControl() {
|
||||
if (this.onCanvasBlurObserver) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onCanvasBlurObserver = this.engine.onCanvasBlurObservable.add(() => {
|
||||
this.codes = [];
|
||||
});
|
||||
|
||||
this.onKeyboardObserver = this.scene.onKeyboardObservable.add(({ event, type }) => {
|
||||
const { code, shiftKey } = event;
|
||||
this.preShift = shiftKey;
|
||||
|
||||
if (type === BABYLON.KeyboardEventTypes.KEYDOWN) {
|
||||
if (this.codesUp.indexOf(code) >= 0 ||
|
||||
this.codesDown.indexOf(code) >= 0 ||
|
||||
this.codesLeft.indexOf(code) >= 0 ||
|
||||
this.codesRight.indexOf(code) >= 0) {
|
||||
const index = this.codes.findIndex(v => v.code === code);
|
||||
if (index < 0) { // 存在しなかったら追加する
|
||||
this.codes.push({ code });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.codesUp.indexOf(code) >= 0 ||
|
||||
this.codesDown.indexOf(code) >= 0 ||
|
||||
this.codesLeft.indexOf(code) >= 0 ||
|
||||
this.codesRight.indexOf(code) >= 0) {
|
||||
const index = this.codes.findIndex(v => v.code === code);
|
||||
if (index >= 0) { // 存在したら削除する
|
||||
this.codes.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
detachControl() {
|
||||
this.codes = [];
|
||||
|
||||
if (this.onKeyboardObserver) this.scene.onKeyboardObservable.remove(this.onKeyboardObserver);
|
||||
if (this.onCanvasBlurObserver) this.engine.onCanvasBlurObservable.remove(this.onCanvasBlurObserver);
|
||||
this.onKeyboardObserver = null;
|
||||
this.onCanvasBlurObserver = null;
|
||||
}
|
||||
|
||||
checkInputs() {
|
||||
if (!this.onKeyboardObserver) {
|
||||
return;
|
||||
}
|
||||
for (let index = 0; index < this.codes.length; index++) {
|
||||
const { code } = this.codes[index];
|
||||
|
||||
const local = new BABYLON.Vector3();
|
||||
if (this.codesLeft.indexOf(code) >= 0) {
|
||||
local.x += -1;
|
||||
} else if (this.codesUp.indexOf(code) >= 0) {
|
||||
local.z += this.scene.useRightHandedSystem ? -1 : 1;
|
||||
} else if (this.codesRight.indexOf(code) >= 0) {
|
||||
local.x += 1;
|
||||
} else if (this.codesDown.indexOf(code) >= 0) {
|
||||
local.z += this.scene.useRightHandedSystem ? 1 : -1;
|
||||
}
|
||||
|
||||
if (local.length() === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (this.canMove) {
|
||||
this.camera.cameraDirection.addInPlace(move);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getClassName() {
|
||||
return 'HorizontalCameraKeyboardMoveInput';
|
||||
}
|
||||
|
||||
getSimpleName() {
|
||||
return 'horizontalkeyboard';
|
||||
}
|
||||
}
|
||||
|
||||
const nanasegNumberMap = [
|
||||
['a', 'b', 'c', 'd', 'e', 'f'], // 0
|
||||
['b', 'c'], // 1
|
||||
['a', 'b', 'd', 'e', 'g'], // 2
|
||||
['a', 'b', 'c', 'd', 'g'], // 3
|
||||
['b', 'c', 'f', 'g'], // 4
|
||||
['a', 'c', 'd', 'f', 'g'], // 5
|
||||
['a', 'c', 'd', 'e', 'f', 'g'], // 6
|
||||
['a', 'b', 'c'], // 7
|
||||
['a', 'b', 'c', 'd', 'e', 'f', 'g'], // 8
|
||||
['a', 'b', 'c', 'd', 'f', 'g'], // 9
|
||||
];
|
||||
|
||||
export function get7segMeshesOfCurrentTime(meshes: {
|
||||
'1a'?: BABYLON.AbstractMesh;
|
||||
'1b'?: BABYLON.AbstractMesh;
|
||||
'1c'?: BABYLON.AbstractMesh;
|
||||
'1d'?: BABYLON.AbstractMesh;
|
||||
'1e'?: BABYLON.AbstractMesh;
|
||||
'1f'?: BABYLON.AbstractMesh;
|
||||
'1g'?: BABYLON.AbstractMesh;
|
||||
'2a'?: BABYLON.AbstractMesh;
|
||||
'2b'?: BABYLON.AbstractMesh;
|
||||
'2c'?: BABYLON.AbstractMesh;
|
||||
'2d'?: BABYLON.AbstractMesh;
|
||||
'2e'?: BABYLON.AbstractMesh;
|
||||
'2f'?: BABYLON.AbstractMesh;
|
||||
'2g'?: BABYLON.AbstractMesh;
|
||||
'3a'?: BABYLON.AbstractMesh;
|
||||
'3b'?: BABYLON.AbstractMesh;
|
||||
'3c'?: BABYLON.AbstractMesh;
|
||||
'3d'?: BABYLON.AbstractMesh;
|
||||
'3e'?: BABYLON.AbstractMesh;
|
||||
'3f'?: BABYLON.AbstractMesh;
|
||||
'3g'?: BABYLON.AbstractMesh;
|
||||
'4a'?: BABYLON.AbstractMesh;
|
||||
'4b'?: BABYLON.AbstractMesh;
|
||||
'4c'?: BABYLON.AbstractMesh;
|
||||
'4d'?: BABYLON.AbstractMesh;
|
||||
'4e'?: BABYLON.AbstractMesh;
|
||||
'4f'?: BABYLON.AbstractMesh;
|
||||
'4g'?: BABYLON.AbstractMesh;
|
||||
}) {
|
||||
const now = new Date();
|
||||
const h = now.getHours();
|
||||
const m = now.getMinutes();
|
||||
|
||||
const chars = [Math.floor(h / 10), h % 10, Math.floor(m / 10), m % 10];
|
||||
|
||||
const result: BABYLON.AbstractMesh[] = [];
|
||||
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const char = chars[i];
|
||||
const segs = nanasegNumberMap[char];
|
||||
for (const seg of segs) {
|
||||
const mesh = meshes[`${i + 1}${seg}`];
|
||||
if (mesh) {
|
||||
result.push(mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 0 1
|
||||
* 0 a(x,y) --- b(x,y)
|
||||
* | |
|
||||
* 1 c(x,y) --- d(x,y)
|
||||
*/
|
||||
export function getPlaneUvIndexes(mesh: BABYLON.Mesh) {
|
||||
const uvs = mesh.getVerticesData(BABYLON.VertexBuffer.UVKind);
|
||||
if (uvs == null) {
|
||||
throw new Error('Mesh does not have UV data');
|
||||
}
|
||||
|
||||
let aIndex = 0;
|
||||
let bIndex = 0;
|
||||
let cIndex = 0;
|
||||
let dIndex = 0;
|
||||
|
||||
for (let i = 0; i < 8; i += 2) {
|
||||
const x = uvs[i];
|
||||
const y = uvs[i + 1];
|
||||
|
||||
// 多少ずれがあってもいいように(例えばblenderではUV展開時にデフォルトでわずかなマージンを追加する)、中心より大きいか/小さいかで判定する
|
||||
if (x < 0.5 && y < 0.5) {
|
||||
aIndex = i;
|
||||
} else if (x > 0.5 && y < 0.5) {
|
||||
bIndex = i;
|
||||
} else if (x < 0.5 && y > 0.5) {
|
||||
cIndex = i;
|
||||
} else if (x > 0.5 && y > 0.5) {
|
||||
dIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return [aIndex, bIndex, cIndex, dIndex];
|
||||
}
|
||||
|
||||
export function normalizeUvToSquare(mesh: BABYLON.Mesh) {
|
||||
const uvs = mesh.getVerticesData(BABYLON.VertexBuffer.UVKind)!;
|
||||
const uvIndexes = getPlaneUvIndexes(mesh);
|
||||
uvs[uvIndexes[0]] = 0;
|
||||
uvs[uvIndexes[0] + 1] = 0;
|
||||
uvs[uvIndexes[1]] = 1;
|
||||
uvs[uvIndexes[1] + 1] = 0;
|
||||
uvs[uvIndexes[2]] = 0;
|
||||
uvs[uvIndexes[2] + 1] = 1;
|
||||
uvs[uvIndexes[3]] = 1;
|
||||
uvs[uvIndexes[3] + 1] = 1;
|
||||
mesh.updateVerticesData(BABYLON.VertexBuffer.UVKind, uvs);
|
||||
}
|
||||
|
||||
export function createPlaneUvMapper(mesh: BABYLON.Mesh) {
|
||||
mesh.markVerticesDataAsUpdatable(BABYLON.VertexBuffer.UVKind, true);
|
||||
|
||||
const uvs = mesh.getVerticesData(BABYLON.VertexBuffer.UVKind)!;
|
||||
const originalUvs = uvs.slice();
|
||||
|
||||
return (srcAspect: number, targetAspect: number, method: 'cover' | 'contain' | 'stretch') => {
|
||||
let uScale = 1;
|
||||
let vScale = 1;
|
||||
const ratio = targetAspect / srcAspect;
|
||||
if (method === 'cover') {
|
||||
uScale = ratio < 1 ? ratio : 1;
|
||||
vScale = ratio < 1 ? 1 : 1 / ratio;
|
||||
} else if (method === 'contain') {
|
||||
uScale = ratio > 1 ? ratio : 1;
|
||||
vScale = ratio > 1 ? 1 : 1 / ratio;
|
||||
} else if (method === 'stretch') {
|
||||
// nop
|
||||
}
|
||||
|
||||
// (0,0)を隅ではなく中心として扱いたいので0.5引いて計算してから最後に0.5足す
|
||||
for (let i = 0; i < uvs.length; i += 2) {
|
||||
uvs[i] = ((originalUvs[i] - 0.5) * uScale) + 0.5;
|
||||
uvs[i + 1] = ((originalUvs[i + 1] - 0.5) * vScale) + 0.5;
|
||||
}
|
||||
|
||||
mesh.updateVerticesData(BABYLON.VertexBuffer.UVKind, uvs);
|
||||
};
|
||||
}
|
||||
|
||||
export function scaleMorph(mesh: BABYLON.Mesh, scale: [number, number, number], offset: [number, number, number] = [0, 0, 0]) {
|
||||
if (!mesh.morphTargetManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const morphTargetManager = mesh.morphTargetManager;
|
||||
|
||||
for (let targetIndex = 0; targetIndex < morphTargetManager.numTargets; targetIndex++) {
|
||||
const target = morphTargetManager.getTarget(targetIndex);
|
||||
const newPos = target.getPositions();
|
||||
for (let i = 0; i < newPos.length; i += 3) {
|
||||
newPos[i] = (newPos[i] + offset[0]) * scale[0];
|
||||
newPos[i + 1] = (newPos[i + 1] + offset[1]) * scale[1];
|
||||
newPos[i + 2] = (newPos[i + 2] + offset[2]) * scale[2];
|
||||
}
|
||||
target.setPositions(newPos);
|
||||
}
|
||||
|
||||
morphTargetManager.synchronize();
|
||||
|
||||
mesh.refreshBoundingInfo();
|
||||
mesh.computeWorldMatrix(true);
|
||||
}
|
||||
|
||||
export function applyMorphTargetsToMesh(mesh: BABYLON.Mesh) {
|
||||
if (!mesh.morphTargetManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const morphTargetManager = mesh.morphTargetManager;
|
||||
const positions = mesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
|
||||
|
||||
if (!positions) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a copy of the original positions to work with
|
||||
const finalPositions = positions.slice();
|
||||
|
||||
// Apply each morph target
|
||||
for (let targetIndex = 0; targetIndex < morphTargetManager.numTargets; targetIndex++) {
|
||||
const target = morphTargetManager.getTarget(targetIndex);
|
||||
const influence = target.influence;
|
||||
|
||||
if (influence === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the morph target positions
|
||||
const targetPositions = target.getPositions();
|
||||
|
||||
if (!targetPositions || targetPositions.length !== positions.length) {
|
||||
console.warn(`Morph target ${targetIndex} has invalid position data`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply the morph target influence to each vertex
|
||||
for (let i = 0; i < positions.length; i++) {
|
||||
finalPositions[i] += (targetPositions[i] - positions[i]) * influence;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the mesh with the morphed positions
|
||||
mesh.setVerticesData(BABYLON.VertexBuffer.PositionKind, finalPositions);
|
||||
|
||||
//// Update normals if available
|
||||
//const normals = mesh.getVerticesData(BABYLON.VertexBuffer.NormalKind);
|
||||
//if (normals) {
|
||||
// // For simplicity, we'll just recompute the normals
|
||||
// mesh.createNormals(true);
|
||||
//}
|
||||
|
||||
// Refresh the mesh
|
||||
mesh.refreshBoundingInfo();
|
||||
mesh.computeWorldMatrix(true);
|
||||
}
|
||||
|
||||
// ex) hangingTShirt -> hanging-t-shirt
|
||||
export const camelToKebab = (s: string) => {
|
||||
return s
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
||||
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
||||
.toLowerCase();
|
||||
};
|
||||
|
||||
// この実装方法だとマイナスの座標をうまく処理できず結果がおかしくなるので応急処置で全体を+10000cmオフセットしてから計算している
|
||||
export function getMeshesBoundingBox(meshes: BABYLON.Mesh[]): BABYLON.BoundingBox {
|
||||
let min = new BABYLON.Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
|
||||
let max = new BABYLON.Vector3(Number.MIN_VALUE, Number.MIN_VALUE, Number.MIN_VALUE);
|
||||
|
||||
for (const mesh of meshes) {
|
||||
const boundingInfo = mesh.getBoundingInfo();
|
||||
min = BABYLON.Vector3.Minimize(min, boundingInfo.boundingBox.minimumWorld.add(new BABYLON.Vector3(10000, 10000, 10000)));
|
||||
max = BABYLON.Vector3.Maximize(max, boundingInfo.boundingBox.maximumWorld.add(new BABYLON.Vector3(10000, 10000, 10000)));
|
||||
}
|
||||
|
||||
return new BABYLON.BoundingBox(min.subtract(new BABYLON.Vector3(10000, 10000, 10000)), max.subtract(new BABYLON.Vector3(10000, 10000, 10000)));
|
||||
}
|
||||
Reference in New Issue
Block a user