mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-05 19:54:04 +02:00
wip
This commit is contained in:
@@ -40,7 +40,7 @@ import { i18n } from '@/i18n.js';
|
|||||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { OBJECT_DEFS } from '@/world/room/object-defs.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<{
|
const emit = defineEmits<{
|
||||||
(ev: 'ok', id: string): void;
|
(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',
|
path: '/qr',
|
||||||
component: page(() => import('@/pages/qr.vue')),
|
component: page(() => import('@/pages/qr.vue')),
|
||||||
loginRequired: true,
|
loginRequired: true,
|
||||||
|
}, {
|
||||||
|
path: '/world',
|
||||||
|
component: page(() => import('@/pages/world.vue')),
|
||||||
}, {
|
}, {
|
||||||
path: '/room',
|
path: '/room',
|
||||||
component: page(() => import('@/pages/room.vue')),
|
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) {
|
public addObject(type: string) {
|
||||||
|
console.log(type);
|
||||||
if (this.worker != null) {
|
if (this.worker != null) {
|
||||||
this.worker.postMessage({ type: 'addObject', objectType: type });
|
this.worker.postMessage({ type: 'addObject', objectType: type });
|
||||||
} else if (this.engine != null) {
|
} else if (this.engine != null) {
|
||||||
|
|||||||
@@ -14,59 +14,19 @@ import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic';
|
|||||||
import { BoundingBoxRenderer } from '@babylonjs/core/Rendering/boundingBoxRenderer';
|
import { BoundingBoxRenderer } from '@babylonjs/core/Rendering/boundingBoxRenderer';
|
||||||
import { GridMaterial } from '@babylonjs/materials';
|
import { GridMaterial } from '@babylonjs/materials';
|
||||||
import { EventEmitter } from 'eventemitter3';
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
import { TIME_MAP, scaleMorph, HorizontalCameraKeyboardMoveInput, camelToKebab, cm, WORLD_SCALE, getMeshesBoundingBox } from '../utility.js';
|
||||||
import { getObjectDef } from './object-defs.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 { genId } from '@/utility/id.js';
|
||||||
import { deepClone } from '@/utility/clone.js';
|
import { deepClone } from '@/utility/clone.js';
|
||||||
|
|
||||||
const BAKE_TRANSFORM = false; // 実験的
|
const BAKE_TRANSFORM = false; // 実験的
|
||||||
const SNAPSHOT_RENDERING = true; // 実験的
|
const SNAPSHOT_RENDERING = true; // 実験的
|
||||||
const IGNORE_OBJECTS: string[] = []; // for debug
|
const IGNORE_OBJECTS: string[] = []; // for debug
|
||||||
const SYSTEM_MESH_NAMES = ['__TOP__', '__SIDE__', '__PICK__', '__COLLISION__'];
|
|
||||||
const USE_GLOW = true; // ドローコールが増えて重い
|
const USE_GLOW = true; // ドローコールが増えて重い
|
||||||
const IN_WEB_WORKER = typeof window === 'undefined';
|
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 = {
|
type SimpleHeyaWallBase = {
|
||||||
material: null | 'wood' | 'concrete';
|
material: null | 'wood' | 'concrete';
|
||||||
color: [number, number, number];
|
color: [number, number, number];
|
||||||
@@ -99,221 +59,6 @@ export type RoomState = {
|
|||||||
installedObjects: RoomStateObject<any>[];
|
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) {
|
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)));
|
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;
|
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[]) {
|
function enableObjectCollision(meshes: BABYLON.Mesh[]) {
|
||||||
for (const mesh of meshes) {
|
for (const mesh of meshes) {
|
||||||
if (mesh.name.includes('__COLLISION__')) {
|
if (mesh.name.includes('__COLLISION__')) {
|
||||||
@@ -444,8 +137,6 @@ export class RoomEngine extends EventEmitter<RoomEngineEvents> {
|
|||||||
private shadowGeneratorForRoomLight: BABYLON.ShadowGenerator;
|
private shadowGeneratorForRoomLight: BABYLON.ShadowGenerator;
|
||||||
private shadowGeneratorForSunLight: BABYLON.ShadowGenerator;
|
private shadowGeneratorForSunLight: BABYLON.ShadowGenerator;
|
||||||
public camera: BABYLON.UniversalCamera;
|
public camera: BABYLON.UniversalCamera;
|
||||||
private fixedCamera: BABYLON.UniversalCamera;
|
|
||||||
private birdeyeCamera: BABYLON.ArcRotateCamera;
|
|
||||||
public intervalIds: number[] = [];
|
public intervalIds: number[] = [];
|
||||||
public timeoutIds: number[] = [];
|
public timeoutIds: number[] = [];
|
||||||
public objectEntities: Map<string, {
|
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 = new BABYLON.ParticleSystem('', 64, this.scene);
|
||||||
this.putParticleSystem.particleTexture = new BABYLON.Texture('/client-assets/room/steam.png');
|
this.putParticleSystem.particleTexture = new BABYLON.Texture('/client-assets/room/steam.png');
|
||||||
this.putParticleSystem.createCylinderEmitter(cm(5), cm(1), cm(5));
|
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 }) {
|
private playSfxUrl(url: string, options: { volume: number; playbackRate: number }) {
|
||||||
this.emit('playSfxUrl', { url, options });
|
this.emit('playSfxUrl', { url, options });
|
||||||
}
|
}
|
||||||
@@ -1981,239 +1625,3 @@ export class RoomEngine extends EventEmitter<RoomEngineEvents> {
|
|||||||
this.disposed = true;
|
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 * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const a4Case = defineObject({
|
export const a4Case = defineObject({
|
||||||
id: 'a4Case',
|
id: 'a4Case',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const aircon = defineObject({
|
export const aircon = defineObject({
|
||||||
id: 'aircon',
|
id: 'aircon',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm, createPlaneUvMapper } from '../utility.js';
|
import { cm, WORLD_SCALE, createPlaneUvMapper } from '../../utility.js';
|
||||||
|
|
||||||
export const allInOnePc = defineObject({
|
export const allInOnePc = defineObject({
|
||||||
id: 'allInOnePc',
|
id: 'allInOnePc',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm } from '../utility.js';
|
import { cm } from '../../utility.js';
|
||||||
|
|
||||||
export const aquarium = defineObject({
|
export const aquarium = defineObject({
|
||||||
id: 'aquarium',
|
id: 'aquarium',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const aromaReedDiffuser = defineObject({
|
export const aromaReedDiffuser = defineObject({
|
||||||
id: 'aromaReedDiffuser',
|
id: 'aromaReedDiffuser',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const banknote = defineObject({
|
export const banknote = defineObject({
|
||||||
id: 'banknote',
|
id: 'banknote',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm } from '../utility.js';
|
import { cm, WORLD_SCALE } from '../../utility.js';
|
||||||
|
|
||||||
export const beamLamp = defineObject({
|
export const beamLamp = defineObject({
|
||||||
id: 'beamLamp',
|
id: 'beamLamp',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const bed = defineObject({
|
export const bed = defineObject({
|
||||||
id: 'bed',
|
id: 'bed',
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm, createOverridedStates } from '../utility.js';
|
import { cm } from '../../utility.js';
|
||||||
|
import { createOverridedStates } from '../utility.js';
|
||||||
|
|
||||||
export const blind = defineObject({
|
export const blind = defineObject({
|
||||||
id: 'blind',
|
id: 'blind',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const book = defineObject({
|
export const book = defineObject({
|
||||||
id: 'book',
|
id: 'book',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm } from '../utility.js';
|
import { cm } from '../../utility.js';
|
||||||
|
|
||||||
export const books = defineObject({
|
export const books = defineObject({
|
||||||
id: 'books',
|
id: 'books',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const cactusS = defineObject({
|
export const cactusS = defineObject({
|
||||||
id: 'cactusS',
|
id: 'cactusS',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const cardboardBox = defineObject({
|
export const cardboardBox = defineObject({
|
||||||
id: 'cardboardBox',
|
id: 'cardboardBox',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const ceilingFanLight = defineObject({
|
export const ceilingFanLight = defineObject({
|
||||||
id: 'ceilingFanLight',
|
id: 'ceilingFanLight',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const chair = defineObject({
|
export const chair = defineObject({
|
||||||
id: 'chair',
|
id: 'chair',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const coffeeCup = defineObject({
|
export const coffeeCup = defineObject({
|
||||||
id: 'coffeeCup',
|
id: 'coffeeCup',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const colorBox = defineObject({
|
export const colorBox = defineObject({
|
||||||
id: 'colorBox',
|
id: 'colorBox',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const cuboid = defineObject({
|
export const cuboid = defineObject({
|
||||||
id: 'cuboid',
|
id: 'cuboid',
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm, yuge } from '../utility.js';
|
import { cm } from '../../utility.js';
|
||||||
|
import { yuge } from '../utility.js';
|
||||||
|
|
||||||
export const cupNoodle = defineObject({
|
export const cupNoodle = defineObject({
|
||||||
id: 'cupNoodle',
|
id: 'cupNoodle',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const custardPudding = defineObject({
|
export const custardPudding = defineObject({
|
||||||
id: 'custardPudding',
|
id: 'custardPudding',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const debugHipoly = defineObject({
|
export const debugHipoly = defineObject({
|
||||||
id: 'debugHipoly',
|
id: 'debugHipoly',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const desk = defineObject({
|
export const desk = defineObject({
|
||||||
id: 'desk',
|
id: 'desk',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm } from '../utility.js';
|
import { cm, WORLD_SCALE } from '../../utility.js';
|
||||||
|
|
||||||
export const desktopPc = defineObject({
|
export const desktopPc = defineObject({
|
||||||
id: 'desktopPc',
|
id: 'desktopPc',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const djMixer = defineObject({
|
export const djMixer = defineObject({
|
||||||
id: 'djMixer',
|
id: 'djMixer',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { createPlaneUvMapper, normalizeUvToSquare } from '../utility.js';
|
import { createPlaneUvMapper, normalizeUvToSquare } from '../../utility.js';
|
||||||
|
|
||||||
export const djPlayer = defineObject({
|
export const djPlayer = defineObject({
|
||||||
id: 'djPlayer',
|
id: 'djPlayer',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const ductTape = defineObject({
|
export const ductTape = defineObject({
|
||||||
id: 'ductTape',
|
id: 'ductTape',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const emptyBento = defineObject({
|
export const emptyBento = defineObject({
|
||||||
id: 'emptyBento',
|
id: 'emptyBento',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const energyDrink = defineObject({
|
export const energyDrink = defineObject({
|
||||||
id: 'energyDrink',
|
id: 'energyDrink',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const envelope = defineObject({
|
export const envelope = defineObject({
|
||||||
id: 'envelope',
|
id: 'envelope',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const facialTissue = defineObject({
|
export const facialTissue = defineObject({
|
||||||
id: 'facialTissue',
|
id: 'facialTissue',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const hangingTShirt = defineObject({
|
export const hangingTShirt = defineObject({
|
||||||
id: 'hangingTShirt',
|
id: 'hangingTShirt',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const icosahedron = defineObject({
|
export const icosahedron = defineObject({
|
||||||
id: 'icosahedron',
|
id: 'icosahedron',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject, defineObjectClass } from '../engine.js';
|
import { defineObject, defineObjectClass } from '../object.js';
|
||||||
|
|
||||||
const base = defineObjectClass({
|
const base = defineObjectClass({
|
||||||
options: {
|
options: {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const ironFrameTable = defineObject({
|
export const ironFrameTable = defineObject({
|
||||||
id: 'ironFrameTable',
|
id: 'ironFrameTable',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const keyboard = defineObject({
|
export const keyboard = defineObject({
|
||||||
id: 'keyboard',
|
id: 'keyboard',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm, createPlaneUvMapper } from '../utility.js';
|
import { cm, WORLD_SCALE, createPlaneUvMapper } from '../../utility.js';
|
||||||
|
|
||||||
export const laptopPc = defineObject({
|
export const laptopPc = defineObject({
|
||||||
id: 'laptopPc',
|
id: 'laptopPc',
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm } from '../utility.js';
|
import { cm, WORLD_SCALE } from '../../utility.js';
|
||||||
|
|
||||||
export const lavaLamp = defineObject({
|
export const lavaLamp = defineObject({
|
||||||
id: 'lavaLamp',
|
id: 'lavaLamp',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const letterCase = defineObject({
|
export const letterCase = defineObject({
|
||||||
id: 'letterCase',
|
id: 'letterCase',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const miObjet = defineObject({
|
export const miObjet = defineObject({
|
||||||
id: 'miObjet',
|
id: 'miObjet',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const miPlate = defineObject({
|
export const miPlate = defineObject({
|
||||||
id: 'miPlate',
|
id: 'miPlate',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const miPlateDisplayed = defineObject({
|
export const miPlateDisplayed = defineObject({
|
||||||
id: 'miPlateDisplayed',
|
id: 'miPlateDisplayed',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const milk = defineObject({
|
export const milk = defineObject({
|
||||||
id: 'milk',
|
id: 'milk',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const mixer = defineObject({
|
export const mixer = defineObject({
|
||||||
id: 'mixer',
|
id: 'mixer',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const monitor = defineObject({
|
export const monitor = defineObject({
|
||||||
id: 'monitor',
|
id: 'monitor',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const monitorSpeaker = defineObject({
|
export const monitorSpeaker = defineObject({
|
||||||
id: 'monitorSpeaker',
|
id: 'monitorSpeaker',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const monstera = defineObject({
|
export const monstera = defineObject({
|
||||||
id: 'monstera',
|
id: 'monstera',
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm, yuge } from '../utility.js';
|
import { cm } from '../../utility.js';
|
||||||
|
import { yuge } from '../utility.js';
|
||||||
|
|
||||||
export const mug = defineObject({
|
export const mug = defineObject({
|
||||||
id: 'mug',
|
id: 'mug',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const newtonsCradle = defineObject({
|
export const newtonsCradle = defineObject({
|
||||||
id: 'newtonsCradle',
|
id: 'newtonsCradle',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const openedCardboardBox = defineObject({
|
export const openedCardboardBox = defineObject({
|
||||||
id: 'openedCardboardBox',
|
id: 'openedCardboardBox',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const pachira = defineObject({
|
export const pachira = defineObject({
|
||||||
id: 'pachira',
|
id: 'pachira',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const pc = defineObject({
|
export const pc = defineObject({
|
||||||
id: 'pc',
|
id: 'pc',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const petBottle = defineObject({
|
export const petBottle = defineObject({
|
||||||
id: 'petBottle',
|
id: 'petBottle',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const piano = defineObject({
|
export const piano = defineObject({
|
||||||
id: 'piano',
|
id: 'piano',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { createPlaneUvMapper } from '../utility.js';
|
import { createPlaneUvMapper } from '../../utility.js';
|
||||||
|
|
||||||
// NOTE: シェイプキーのnormalのエクスポートは無効にしないとmatを大きくしたときに面のレンダリングがグリッチする
|
// NOTE: シェイプキーのnormalのエクスポートは無効にしないとmatを大きくしたときに面のレンダリングがグリッチする
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const pizza = defineObject({
|
export const pizza = defineObject({
|
||||||
id: 'pizza',
|
id: 'pizza',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const plant = defineObject({
|
export const plant = defineObject({
|
||||||
id: 'plant',
|
id: 'plant',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const plant2 = defineObject({
|
export const plant2 = defineObject({
|
||||||
id: 'plant2',
|
id: 'plant2',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../utility.js';
|
import { createPlaneUvMapper, getPlaneUvIndexes } from '../../utility.js';
|
||||||
|
|
||||||
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||||
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const powerStrip = defineObject({
|
export const powerStrip = defineObject({
|
||||||
id: 'powerStrip',
|
id: 'powerStrip',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const radiometer = defineObject({
|
export const radiometer = defineObject({
|
||||||
id: 'radiometer',
|
id: 'radiometer',
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import seedrandom from 'seedrandom';
|
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) => {
|
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||||
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const rolledUpPoster = defineObject({
|
export const rolledUpPoster = defineObject({
|
||||||
id: 'rolledUpPoster',
|
id: 'rolledUpPoster',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const roundRug = defineObject({
|
export const roundRug = defineObject({
|
||||||
id: 'roundRug',
|
id: 'roundRug',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const router = defineObject({
|
export const router = defineObject({
|
||||||
id: 'router',
|
id: 'router',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const siphon = defineObject({
|
export const siphon = defineObject({
|
||||||
id: 'siphon',
|
id: 'siphon',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const snakeplant = defineObject({
|
export const snakeplant = defineObject({
|
||||||
id: 'snakeplant',
|
id: 'snakeplant',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const speaker = defineObject({
|
export const speaker = defineObject({
|
||||||
id: 'speaker',
|
id: 'speaker',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const sprayer = defineObject({
|
export const sprayer = defineObject({
|
||||||
id: 'sprayer',
|
id: 'sprayer',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const steelRack = defineObject({
|
export const steelRack = defineObject({
|
||||||
id: 'steelRack',
|
id: 'steelRack',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const tabletopCalendar = defineObject({
|
export const tabletopCalendar = defineObject({
|
||||||
id: 'tabletopCalendar',
|
id: 'tabletopCalendar',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm, get7segMeshesOfCurrentTime } from '../utility.js';
|
import { cm, get7segMeshesOfCurrentTime, WORLD_SCALE } from '@/world/utility.js';
|
||||||
|
|
||||||
export const tabletopDigitalClock = defineObject({
|
export const tabletopDigitalClock = defineObject({
|
||||||
id: 'tabletopDigitalClock',
|
id: 'tabletopDigitalClock',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { createPlaneUvMapper } from '../utility.js';
|
import { createPlaneUvMapper } from '../../utility.js';
|
||||||
|
|
||||||
export const tabletopFlag = defineObject({
|
export const tabletopFlag = defineObject({
|
||||||
id: 'tabletopFlag',
|
id: 'tabletopFlag',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../utility.js';
|
import { createPlaneUvMapper, getPlaneUvIndexes } from '../../utility.js';
|
||||||
|
|
||||||
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||||
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const tabletopIronFrameStand = defineObject({
|
export const tabletopIronFrameStand = defineObject({
|
||||||
id: 'tabletopIronFrameStand',
|
id: 'tabletopIronFrameStand',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { createPlaneUvMapper } from '../utility.js';
|
import { createPlaneUvMapper } from '../../utility.js';
|
||||||
|
|
||||||
// NOTE: シェイプキーのnormalのエクスポートは無効にしないとmatを大きくしたときに面のレンダリングがグリッチする
|
// NOTE: シェイプキーのnormalのエクスポートは無効にしないとmatを大きくしたときに面のレンダリングがグリッチする
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../utility.js';
|
import { createPlaneUvMapper, getPlaneUvIndexes } from '../../utility.js';
|
||||||
|
|
||||||
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||||
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const tetrapod = defineObject({
|
export const tetrapod = defineObject({
|
||||||
id: 'tetrapod',
|
id: 'tetrapod',
|
||||||
|
|||||||
@@ -4,8 +4,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm, createPlaneUvMapper, initTv } from '../utility.js';
|
import { initTv } from '../utility.js';
|
||||||
|
import { cm, WORLD_SCALE } from '@/world/utility.js';
|
||||||
|
|
||||||
export const tv = defineObject({
|
export const tv = defineObject({
|
||||||
id: 'tv',
|
id: 'tv',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const twistedCubeObjet = defineObject({
|
export const twistedCubeObjet = defineObject({
|
||||||
id: 'twistedCubeObjet',
|
id: 'twistedCubeObjet',
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const usedTissue = defineObject({
|
export const usedTissue = defineObject({
|
||||||
id: 'usedTissue',
|
id: 'usedTissue',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../utility.js';
|
import { createPlaneUvMapper, getPlaneUvIndexes } from '../../utility.js';
|
||||||
|
|
||||||
export const wallCanvas = defineObject({
|
export const wallCanvas = defineObject({
|
||||||
id: 'wallCanvas',
|
id: 'wallCanvas',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const wallClock = defineObject({
|
export const wallClock = defineObject({
|
||||||
id: 'wallClock',
|
id: 'wallClock',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { createPlaneUvMapper, getPlaneUvIndexes } from '../utility.js';
|
import { createPlaneUvMapper, getPlaneUvIndexes } from '../../utility.js';
|
||||||
|
|
||||||
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||||
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const wallMirror = defineObject({
|
export const wallMirror = defineObject({
|
||||||
id: 'wallMirror',
|
id: 'wallMirror',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const wallShelf = defineObject({
|
export const wallShelf = defineObject({
|
||||||
id: 'wallShelf',
|
id: 'wallShelf',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm } from '../utility.js';
|
import { cm, WORLD_SCALE } from '@/world/utility.js';
|
||||||
|
|
||||||
export const woodRingFloorLamp = defineObject({
|
export const woodRingFloorLamp = defineObject({
|
||||||
id: 'woodRingFloorLamp',
|
id: 'woodRingFloorLamp',
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as BABYLON from '@babylonjs/core';
|
import * as BABYLON from '@babylonjs/core';
|
||||||
import { defineObject, WORLD_SCALE } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
import { cm } from '../utility.js';
|
import { cm, WORLD_SCALE } from '@/world/utility.js';
|
||||||
|
|
||||||
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
const remap = (value: number, fromMin: number, fromMax: number, toMin: number, toMax: number) => {
|
||||||
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
return toMin + ((value - fromMin) / (fromMax - fromMin)) * (toMax - toMin);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { defineObject } from '../engine.js';
|
import { defineObject } from '../object.js';
|
||||||
|
|
||||||
export const woodSoundAbsorbingPanel = defineObject({
|
export const woodSoundAbsorbingPanel = defineObject({
|
||||||
id: 'woodSoundAbsorbingPanel',
|
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 * as BABYLON from '@babylonjs/core';
|
||||||
|
import { applyMorphTargetsToMesh, cm, getPlaneUvIndexes } from '../utility.js';
|
||||||
import type { RoomEngine } from './engine.js';
|
import type { RoomEngine } from './engine.js';
|
||||||
|
|
||||||
//export const cm = (value: number) => value / 100;
|
export const SYSTEM_MESH_NAMES = ['__TOP__', '__SIDE__', '__PICK__', '__COLLISION__'];
|
||||||
export const cm = (value: number) => value;
|
|
||||||
|
|
||||||
export function yuge(scene: BABYLON.Scene, mesh: BABYLON.Mesh, offset: BABYLON.Vector3) {
|
export function yuge(scene: BABYLON.Scene, mesh: BABYLON.Mesh, offset: BABYLON.Vector3) {
|
||||||
const emitter = new BABYLON.TransformNode('emitter', scene);
|
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 } {
|
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 overridedStates = {} as { [K in keyof T]: ReturnType<T[K]>; };
|
||||||
const result = {} as { [K in keyof T]: ReturnType<T[K]>; } & { $reset: () => void };
|
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 {
|
export function findMaterial(rootMesh: BABYLON.AbstractMesh, keyword: string, allowMultiMaterial = false): BABYLON.PBRMaterial {
|
||||||
for (const m of rootMesh.getChildMeshes()) {
|
for (const m of rootMesh.getChildMeshes()) {
|
||||||
if (m.material == null) continue;
|
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`);
|
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]) {
|
export class ModelManager {
|
||||||
if (!mesh.morphTargetManager) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const morphTargetManager = mesh.morphTargetManager;
|
const _toMerge = [] as BABYLON.Mesh[];
|
||||||
|
for (const mesh of childMeshes) {
|
||||||
|
let fixedMesh = mesh;
|
||||||
|
fixedMesh.setEnabled(false);
|
||||||
|
|
||||||
for (let targetIndex = 0; targetIndex < morphTargetManager.numTargets; targetIndex++) {
|
if (mesh instanceof BABYLON.InstancedMesh) {
|
||||||
const target = morphTargetManager.getTarget(targetIndex);
|
const sourceMesh = mesh.sourceMesh;
|
||||||
const newPos = target.getPositions();
|
const realizedMesh = sourceMesh.clone(mesh.name + '_realized', null, true);
|
||||||
for (let i = 0; i < newPos.length; i += 3) {
|
realizedMesh.getScene().removeMesh(realizedMesh);
|
||||||
newPos[i] = (newPos[i] + offset[0]) * scale[0];
|
|
||||||
newPos[i + 1] = (newPos[i + 1] + offset[1]) * scale[1];
|
realizedMesh.position = mesh.position.clone();
|
||||||
newPos[i + 2] = (newPos[i + 2] + offset[2]) * scale[2];
|
if (mesh.rotationQuaternion) {
|
||||||
|
realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone();
|
||||||
|
} else {
|
||||||
|
realizedMesh.rotation = mesh.rotation.clone();
|
||||||
}
|
}
|
||||||
target.setPositions(newPos);
|
realizedMesh.scaling = mesh.scaling.clone();
|
||||||
|
realizedMesh.parent = mesh.parent;
|
||||||
|
realizedMesh.setEnabled(false);
|
||||||
|
|
||||||
|
fixedMesh = realizedMesh;
|
||||||
}
|
}
|
||||||
|
|
||||||
morphTargetManager.synchronize();
|
_toMerge.push(fixedMesh);
|
||||||
|
|
||||||
mesh.refreshBoundingInfo();
|
|
||||||
mesh.computeWorldMatrix(true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyMorphTargetsToMesh(mesh: BABYLON.Mesh) {
|
const toMerge = [] as BABYLON.Mesh[];
|
||||||
if (!mesh.morphTargetManager) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const morphTargetManager = mesh.morphTargetManager;
|
const merged = BABYLON.Mesh.MergeMeshes(toMerge, true, false, undefined, false, true);
|
||||||
const positions = mesh.getVerticesData(BABYLON.VertexBuffer.PositionKind);
|
merged.parent = this.root;
|
||||||
|
merged.material.freeze();
|
||||||
if (!positions) {
|
if (merged.material instanceof BABYLON.MultiMaterial) {
|
||||||
return;
|
for (const subMat of merged.material.subMaterials) {
|
||||||
}
|
(subMat as BABYLON.PBRMaterial).freeze();
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
merged.freezeWorldMatrix();
|
||||||
|
merged.metadata = { ...this.root.metadata };
|
||||||
|
if (!this.hasTexture) merged.convertToUnIndexedMesh();
|
||||||
|
this.bakedMeshes = [merged];
|
||||||
|
|
||||||
// Update the mesh with the morphed positions
|
this.bakedCallback?.([...this.bakedMeshes, ...excludeMeshes]);
|
||||||
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
|
public unbakeMesh() {
|
||||||
export const camelToKebab = (s: string) => {
|
for (const m of this.bakedMeshes) {
|
||||||
return s
|
m.dispose();
|
||||||
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
}
|
||||||
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
this.bakedMeshes = [];
|
||||||
.toLowerCase();
|
|
||||||
};
|
const childMeshes = this.root.getChildMeshes();
|
||||||
|
|
||||||
|
for (const mesh of childMeshes) {
|
||||||
|
mesh.setEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bakedCallback?.(this.root.getChildMeshes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
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