1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-05-30 21:33:56 +02:00
This commit is contained in:
syuilo
2026-04-17 15:31:56 +09:00
parent ebdf627b19
commit 6cca5706f7
98 changed files with 1722 additions and 1042 deletions

View File

@@ -14,59 +14,19 @@ import { registerBuiltInLoaders } from '@babylonjs/loaders/dynamic';
import { BoundingBoxRenderer } from '@babylonjs/core/Rendering/boundingBoxRenderer';
import { GridMaterial } from '@babylonjs/materials';
import { EventEmitter } from 'eventemitter3';
import { TIME_MAP, scaleMorph, HorizontalCameraKeyboardMoveInput, camelToKebab, cm, WORLD_SCALE, getMeshesBoundingBox } from '../utility.js';
import { getObjectDef } from './object-defs.js';
import { HorizontalCameraKeyboardMoveInput, applyMorphTargetsToMesh, camelToKebab, cm, findMaterial, scaleMorph } from './utility.js';
import { findMaterial, ModelManager, SYSTEM_MESH_NAMES } from './utility.js';
import type { ObjectDef, RoomObjectInstance, RoomStateObject } from './object.js';
import { genId } from '@/utility/id.js';
import { deepClone } from '@/utility/clone.js';
const BAKE_TRANSFORM = false; // 実験的
const SNAPSHOT_RENDERING = true; // 実験的
const IGNORE_OBJECTS: string[] = []; // for debug
const SYSTEM_MESH_NAMES = ['__TOP__', '__SIDE__', '__PICK__', '__COLLISION__'];
const USE_GLOW = true; // ドローコールが増えて重い
const IN_WEB_WORKER = typeof window === 'undefined';
const TIME_MAP = {
0: 2,
1: 2,
2: 2,
3: 2,
4: 2,
5: 1,
6: 1,
7: 0,
8: 0,
9: 0,
10: 0,
11: 0,
12: 0,
13: 0,
14: 0,
15: 0,
16: 1,
17: 1,
18: 2,
19: 2,
20: 2,
21: 2,
22: 2,
23: 2,
} as const;
// babylonのドメイン知識は持たない
export type RoomStateObject<Options = any> = {
id: string;
type: string;
position: [number, number, number];
rotation: [number, number, number];
options: Options;
/**
* 別のオブジェクトのID
*/
sticky?: string | null;
};
type SimpleHeyaWallBase = {
material: null | 'wood' | 'concrete';
color: [number, number, number];
@@ -99,221 +59,6 @@ export type RoomState = {
installedObjects: RoomStateObject<any>[];
};
type RoomObjectInstance<Options> = {
onInited?: () => void;
onOptionsUpdated?: <K extends keyof Options, V extends Options[K]>(kv: [K, V]) => void;
interactions: Record<string, {
label: string;
fn: () => void;
}>;
primaryInteraction?: string | null;
resetTemporaryState?: () => void;
dispose?: () => void;
};
export const WORLD_SCALE = 100;
type NumberOptionSchema = {
type: 'number';
label: string;
min?: number;
max?: number;
step?: number;
};
type BooleanOptionSchema = {
type: 'boolean';
label: string;
};
type ColorOptionSchema = {
type: 'color';
label: string;
};
type EnumOptionSchema = {
type: 'enum';
label: string;
enum: string[];
};
type RangeOptionSchema = {
type: 'range';
label: string;
min: number;
max: number;
step?: number;
};
type ImageOptionSchema = {
type: 'image';
label: string;
};
type OptionsSchema = Record<string, NumberOptionSchema | BooleanOptionSchema | ColorOptionSchema | EnumOptionSchema | RangeOptionSchema | ImageOptionSchema>;
type GetOptionsSchemaValues<T extends OptionsSchema> = {
[K in keyof T]:
T[K] extends NumberOptionSchema ? number :
T[K] extends BooleanOptionSchema ? boolean :
T[K] extends ColorOptionSchema ? [number, number, number] :
T[K] extends EnumOptionSchema ? T[K]['enum'][number] :
T[K] extends RangeOptionSchema ? number :
T[K] extends ImageOptionSchema ? string | null :
never;
};
class ModelManager {
public root: BABYLON.Mesh;
public bakedCallback: (() => void) | null = null;
public bakeExcludeMeshes: BABYLON.Mesh[] = [];
private originalMeshes: BABYLON.Mesh[] = [];
private bakedMeshes: BABYLON.Mesh[] = [];
private hasTexture: boolean;
constructor(root: BABYLON.Mesh, originalMeshes: BABYLON.Mesh[], hasTexture: boolean, bakedCallback: (() => void) | null = null) {
this.root = root;
this.originalMeshes = originalMeshes;
this.hasTexture = hasTexture;
this.bakedCallback = bakedCallback;
}
public findMesh(keyword: string) {
const mesh = this.root.getChildMeshes().find(m => m.name.includes(keyword));
if (mesh == null) {
throw new Error(`Mesh with keyword "${keyword}" not found for object ${this.root.metadata?.objectType}`);
}
return mesh as BABYLON.Mesh;
}
public findMeshes(keyword: string) {
const meshes = this.root.getChildMeshes().filter(m => m.name.includes(keyword));
return meshes as BABYLON.Mesh[];
}
public findMaterial(keyword: string) {
return findMaterial(this.root, keyword);
}
public findTransformNode(keyword: string) {
const node = this.root.getChildTransformNodes().find(n => n.name.includes(keyword));
if (node == null) {
throw new Error(`TransformNode with keyword "${keyword}" not found for object ${this.root.metadata?.objectType}`);
}
return node;
}
public updated() {
}
public bakeMesh() {
for (const m of this.bakedMeshes) {
m.dispose();
}
this.bakedMeshes = [];
const excludeMeshes = [...this.bakeExcludeMeshes, ...this.root.getChildMeshes().filter(m => SYSTEM_MESH_NAMES.some(s => m.name.includes(s)))];
const childMeshes = this.root.getChildMeshes().filter(m => !excludeMeshes.some(x => x === m) && m.isVisible && !m.isDisposed());
if (childMeshes.length <= 1) {
this.bakedCallback?.([...childMeshes, ...excludeMeshes]);
return;
}
const _toMerge = [] as BABYLON.Mesh[];
for (const mesh of childMeshes) {
let fixedMesh = mesh;
fixedMesh.setEnabled(false);
if (mesh instanceof BABYLON.InstancedMesh) {
const sourceMesh = mesh.sourceMesh;
const realizedMesh = sourceMesh.clone(mesh.name + '_realized', null, true);
realizedMesh.getScene().removeMesh(realizedMesh);
realizedMesh.position = mesh.position.clone();
if (mesh.rotationQuaternion) {
realizedMesh.rotationQuaternion = mesh.rotationQuaternion.clone();
} else {
realizedMesh.rotation = mesh.rotation.clone();
}
realizedMesh.scaling = mesh.scaling.clone();
realizedMesh.parent = mesh.parent;
realizedMesh.setEnabled(false);
fixedMesh = realizedMesh;
}
_toMerge.push(fixedMesh);
}
const toMerge = [] as BABYLON.Mesh[];
for (const mesh of _toMerge) {
const newMesh = mesh.name.endsWith('_realized') ? mesh : mesh.clone(mesh.name + '_bakeMerged', null, true);
newMesh.makeGeometryUnique();
applyMorphTargetsToMesh(newMesh);
if (newMesh.parent === this.root) {
newMesh.parent = null;
} else {
newMesh.setParent(this.root);
//newMesh.bakeCurrentTransformIntoVertices();
newMesh.parent = null;
}
//newMesh.bakeCurrentTransformIntoVertices();
if (this.hasTexture) {
if (newMesh.getVerticesData(BABYLON.VertexBuffer.UVKind) == null) {
const vertexCount = newMesh.getTotalVertices();
const uvs = new Array(vertexCount * 2).fill(0);
newMesh.setVerticesData(BABYLON.VertexBuffer.UVKind, uvs, false, 2);
}
if (newMesh.getVerticesData(BABYLON.VertexBuffer.UV2Kind) == null) {
const vertexCount = newMesh.getTotalVertices();
const uvs = new Array(vertexCount * 2).fill(0);
newMesh.setVerticesData(BABYLON.VertexBuffer.UV2Kind, uvs, false, 2);
}
}
toMerge.push(newMesh);
}
if (toMerge.length === 0) {
this.bakedCallback?.([...childMeshes, ...excludeMeshes]);
return;
}
const merged = BABYLON.Mesh.MergeMeshes(toMerge, true, false, undefined, false, true);
merged.parent = this.root;
merged.material.freeze();
if (merged.material instanceof BABYLON.MultiMaterial) {
for (const subMat of merged.material.subMaterials) {
(subMat as BABYLON.PBRMaterial).freeze();
}
}
merged.freezeWorldMatrix();
merged.metadata = { ...this.root.metadata };
if (!this.hasTexture) merged.convertToUnIndexedMesh();
this.bakedMeshes = [merged];
this.bakedCallback?.([...this.bakedMeshes, ...excludeMeshes]);
}
public unbakeMesh() {
for (const m of this.bakedMeshes) {
m.dispose();
}
this.bakedMeshes = [];
const childMeshes = this.root.getChildMeshes();
for (const mesh of childMeshes) {
mesh.setEnabled(true);
}
this.bakedCallback?.(this.root.getChildMeshes());
}
}
function mergeMeshes(meshes: BABYLON.Mesh[], root: BABYLON.Mesh, hasTexture: boolean) {
const excludeMeshes = root.getChildMeshes().filter(m => SYSTEM_MESH_NAMES.some(s => m.name.includes(s)));
@@ -352,58 +97,6 @@ function mergeMeshes(meshes: BABYLON.Mesh[], root: BABYLON.Mesh, hasTexture: boo
return merged;
}
export type ObjectDef<OpSc extends OptionsSchema = OptionsSchema> = {
id: string;
name: string;
path?: string;
options: {
schema: OpSc;
default: GetOptionsSchemaValues<OpSc>;
};
placement: 'top' | 'side' | 'bottom' | 'wall' | 'ceiling' | 'floor';
hasCollisions?: boolean;
hasTexture?: boolean;
canPreMeshesMerging?: boolean;
//groupingMeshes: string[]; // multi-materialなメッシュは複数のメッシュに分割されるが、それだと不便な場合に追加の親メッシュでグルーピングするための指定
isChair?: boolean;
treatLoaderResult?: (loaderResult: BABYLON.AssetContainer) => void;
createInstance: (args: {
room?: RoomEngine | null;
scene: BABYLON.Scene;
root: BABYLON.Mesh;
options: Readonly<GetOptionsSchemaValues<OpSc>>;
model: ModelManager;
id: string;
stickyMarkerMeshUpdated?: (mesh: BABYLON.Mesh) => void;
}) => RoomObjectInstance<GetOptionsSchemaValues<OpSc>> | Promise<RoomObjectInstance<GetOptionsSchemaValues<OpSc>>>; // TODO: createInstanceをasyncにするのではなく、別にreadyみたいなものを返させる
};
export function defineObject<const OpSc extends OptionsSchema>(def: ObjectDef<OpSc>): ObjectDef<OpSc> {
return def;
}
export function defineObjectClass<const OpSc extends OptionsSchema>(baseDef: Partial<ObjectDef<OpSc>>): {
extend: (childDef: Partial<ObjectDef<OpSc>>) => ObjectDef<OpSc>;
} {
return {
extend: (childDef) => ({ ...baseDef, ...childDef }) as ObjectDef<OpSc>,
};
}
// この実装方法だとマイナスの座標をうまく処理できず結果がおかしくなるので応急処置で全体を+10000cmオフセットしてから計算している
function getMeshesBoundingBox(meshes: BABYLON.Mesh[]): BABYLON.BoundingBox {
let min = new BABYLON.Vector3(Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE);
let max = new BABYLON.Vector3(Number.MIN_VALUE, Number.MIN_VALUE, Number.MIN_VALUE);
for (const mesh of meshes) {
const boundingInfo = mesh.getBoundingInfo();
min = BABYLON.Vector3.Minimize(min, boundingInfo.boundingBox.minimumWorld.add(new BABYLON.Vector3(10000, 10000, 10000)));
max = BABYLON.Vector3.Maximize(max, boundingInfo.boundingBox.maximumWorld.add(new BABYLON.Vector3(10000, 10000, 10000)));
}
return new BABYLON.BoundingBox(min.subtract(new BABYLON.Vector3(10000, 10000, 10000)), max.subtract(new BABYLON.Vector3(10000, 10000, 10000)));
}
function enableObjectCollision(meshes: BABYLON.Mesh[]) {
for (const mesh of meshes) {
if (mesh.name.includes('__COLLISION__')) {
@@ -444,8 +137,6 @@ export class RoomEngine extends EventEmitter<RoomEngineEvents> {
private shadowGeneratorForRoomLight: BABYLON.ShadowGenerator;
private shadowGeneratorForSunLight: BABYLON.ShadowGenerator;
public camera: BABYLON.UniversalCamera;
private fixedCamera: BABYLON.UniversalCamera;
private birdeyeCamera: BABYLON.ArcRotateCamera;
public intervalIds: number[] = [];
public timeoutIds: number[] = [];
public objectEntities: Map<string, {
@@ -655,45 +346,6 @@ export class RoomEngine extends EventEmitter<RoomEngineEvents> {
}
}
{
//const postProcess = new BABYLON.ImageProcessingPostProcess('processing', 1.0, this.camera);
//postProcess.exposure = 1.1;
//postProcess.contrast = 0.9;
//const curve = new BABYLON.ColorCurves();
//curve.highlightsHue = 40;
//curve.highlightsDensity = 50;
//curve.highlightsSaturation = 40;
//curve.shadowsHue = 200;
//curve.shadowsDensity = 100;
//curve.shadowsSaturation = 40;
//postProcess.colorCurvesEnabled = true;
//postProcess.colorCurves = curve;
//const postProcess2 = new BABYLON.ImageProcessingPostProcess('processing2', 1.0, this.birdeyeCamera);
//postProcess2.exposure = 2;
//postProcess2.contrast = 0.9;
//const ssao = new BABYLON.SSAORenderingPipeline('ssao', this.scene, {
// ssaoRatio: 4,
// combineRatio: 1,
//});
//ssao.radius = 0.0001;
//ssao.totalStrength = 0.8;
//this.scene.postProcessRenderPipelineManager.attachCamerasToRenderPipeline('ssao', this.camera);
//const lensEffect = new BABYLON.LensRenderingPipeline('lens', {
// edge_blur: 1.0,
// distortion: 0.5,
// dof_focus_distance: cm(90),
// dof_aperture: 6.0,
// dof_pentagon: true,
// dof_gain: 2.0,
// dof_threshold: 1.0,
// dof_darken: 0,
//}, this.scene, 1, [this.camera]);
}
this.putParticleSystem = new BABYLON.ParticleSystem('', 64, this.scene);
this.putParticleSystem.particleTexture = new BABYLON.Texture('/client-assets/room/steam.png');
this.putParticleSystem.createCylinderEmitter(cm(5), cm(1), cm(5));
@@ -1952,14 +1604,6 @@ export class RoomEngine extends EventEmitter<RoomEngineEvents> {
}
}
public showBoundingBox() {
for (const mesh of this.objectMeshs.values()) {
for (const m of mesh.getChildMeshes()) {
m.showBoundingBox = true;
}
}
}
private playSfxUrl(url: string, options: { volume: number; playbackRate: number }) {
this.emit('playSfxUrl', { url, options });
}
@@ -1981,239 +1625,3 @@ export class RoomEngine extends EventEmitter<RoomEngineEvents> {
this.disposed = true;
}
}
export async function createRoomObjectPreviewEngine(canvas: HTMLCanvasElement) {
//const babylonEngine = new BABYLON.WebGPUEngine(canvas);
//babylonEngine.compatibilityMode = false;
//await babylonEngine.initAsync();
const babylonEngine = new BABYLON.Engine(canvas, false, { alpha: false, antialias: false });
return new RoomObjectPreviewEngine({ canvas, engine: babylonEngine });
}
export class RoomObjectPreviewEngine {
private canvas: HTMLCanvasElement;
private engine: BABYLON.WebGPUEngine;
private scene: BABYLON.Scene;
private shadowGenerator1: BABYLON.ShadowGenerator;
private camera: BABYLON.ArcRotateCamera;
private objectMesh: BABYLON.Mesh | null = null;
private objectInstance: RoomObjectInstance<any> | null = null;
private envMapIndoor: BABYLON.CubeTexture;
private roomLight: BABYLON.SpotLight;
private zGridPreviewPlane: BABYLON.Mesh;
private fps = 60;
constructor(options: {
canvas: HTMLCanvasElement;
engine: BABYLON.WebGPUEngine;
}) {
this.canvas = options.canvas;
registerBuiltInLoaders();
this.engine = options.engine;
this.scene = new BABYLON.Scene(this.engine);
this.scene.ambientColor = new BABYLON.Color3(1.0, 0.9, 0.8);
this.envMapIndoor = BABYLON.CubeTexture.CreateFromPrefilteredData('/client-assets/room/indoor.env', this.scene);
this.envMapIndoor.boundingBoxSize = new BABYLON.Vector3(cm(500), cm(500), cm(500));
this.camera = new BABYLON.ArcRotateCamera('camera', -Math.PI / 2, Math.PI / 2.5, cm(300), new BABYLON.Vector3(0, cm(90), 0), this.scene);
this.camera.attachControl(this.canvas);
this.camera.minZ = cm(1);
this.camera.maxZ = cm(100000);
this.camera.fov = 0.5;
this.camera.lowerBetaLimit = 0;
this.camera.upperBetaLimit = (Math.PI / 2) + 0.1;
this.camera.lowerRadiusLimit = cm(50);
this.camera.upperRadiusLimit = cm(1000);
this.camera.useAutoRotationBehavior = true;
this.camera.autoRotationBehavior!.idleRotationSpeed = 0.3;
//this.camera.mode = BABYLON.Camera.ORTHOGRAPHIC_CAMERA;
this.scene.activeCamera = this.camera;
const ambientLight = new BABYLON.HemisphericLight('ambientLight', new BABYLON.Vector3(0, 1, -0.5), this.scene);
ambientLight.diffuse = new BABYLON.Color3(1.0, 1.0, 1.0);
ambientLight.intensity = 0.5;
//ambientLight.intensity = 0;
this.roomLight = new BABYLON.SpotLight('roomLight', new BABYLON.Vector3(0, cm(249), 0), new BABYLON.Vector3(0, -1, 0), 16, 8, this.scene);
this.roomLight.diffuse = new BABYLON.Color3(1.0, 0.9, 0.8);
this.roomLight.shadowMinZ = cm(10);
this.roomLight.shadowMaxZ = cm(300);
this.shadowGenerator1 = new BABYLON.ShadowGenerator(4096, this.roomLight);
this.shadowGenerator1.forceBackFacesOnly = true;
this.shadowGenerator1.bias = 0.0001;
this.shadowGenerator1.usePercentageCloserFiltering = true;
this.shadowGenerator1.filteringQuality = BABYLON.ShadowGenerator.QUALITY_HIGH;
//this.shadowGenerator1.useContactHardeningShadow = true;
const gridMaterial = new GridMaterial('grid', this.scene);
gridMaterial.lineColor = new BABYLON.Color3(0.5, 0.5, 0.5);
gridMaterial.mainColor = new BABYLON.Color3(0, 0, 0);
gridMaterial.minorUnitVisibility = 1;
gridMaterial.opacity = 0.5;
gridMaterial.gridRatio = cm(10);
//this.zGridPreviewPlane = BABYLON.MeshBuilder.CreatePlane('zGridPreviewPlane', { width: cm(1000), height: cm(1000) }, this.scene);
//this.zGridPreviewPlane.material = gridMaterial;
//this.zGridPreviewPlane.rotation = new BABYLON.Vector3(Math.PI / 2, 0, 0);
//this.scene.fogMode = BABYLON.Scene.FOGMODE_LINEAR;
//this.scene.fogStart = cm(100);
//this.scene.fogEnd = cm(110);
//this.scene.fogColor = new BABYLON.Color3(0.0, 0.0, 0.0);
}
public async init() {
const frameInterval = 1000 / this.fps;
let lastTime = performance.now();
this.engine.runRenderLoop(() => {
const currentTime = performance.now();
const delta = currentTime - lastTime;
if (delta >= frameInterval) {
this.scene.render();
lastTime = currentTime - (delta % frameInterval);
}
});
}
public async load(type: string) {
if (this.objectInstance != null) {
this.objectInstance.dispose?.();
this.objectInstance = null;
this.objectMesh!.dispose();
}
// reset camera rotation
this.camera.setPosition(new BABYLON.Vector3(0, cm(90), cm(300)));
const def = getObjectDef(type);
const options = deepClone(def.options.default);
const id = genId();
await this.loadObject({
type,
options,
id,
});
// なぜかちょっと待たないとbounding boxのサイズが正しくない
window.setTimeout(() => {
const boundingInfo = getMeshesBoundingBox(this.objectMesh!.getChildMeshes().filter(m => m.isEnabled() && m.isVisible));
this.camera.setTarget(new BABYLON.Vector3(0, boundingInfo.center.y, 0));
// zoom to fit
const size = boundingInfo.extendSize;
const distance = Math.max(size.x, size.y, size.z) * 2;
this.camera.radius = distance * 3;
//this.camera.orthoLeft = -distance;
//this.camera.orthoRight = distance;
//this.camera.orthoTop = distance;
//this.camera.orthoBottom = -distance;
}, 10);
}
// TODO: RoomEngineのものとほぼ同じだからいい感じに共通化
private async loadObject(args: {
type: string;
options: any;
id: string;
}) {
const def = getObjectDef(args.type);
const root = new BABYLON.Mesh(`object_${args.type}`, this.scene);
const filePath = def.path != null ? `/client-assets/room/objects/${def.path}.glb` : `/client-assets/room/objects/${camelToKebab(args.type)}/${camelToKebab(args.type)}.glb`;
const loaderResult = await BABYLON.LoadAssetContainerAsync(filePath, this.scene);
// 不要なUVを掃除
if (!def.hasTexture) {
for (const m of loaderResult.meshes) {
if (m.geometry != null) {
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UVKind);
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV2Kind);
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV3Kind);
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV4Kind);
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV5Kind);
m.geometry.removeVerticesData(BABYLON.VertexBuffer.UV6Kind);
}
}
}
// babylonによって自動で追加される右手系変換用ード
const subRoot = loaderResult.meshes[0];
subRoot.scaling = subRoot.scaling.scale(WORLD_SCALE);// cmをmに
def.treatLoaderResult?.(loaderResult);
root.addChild(subRoot);
const model = new ModelManager(subRoot, loaderResult.meshes.filter(m => !m.isDisposed() && m !== subRoot), def.hasTexture, (meshes) => {
for (const m of meshes) {
const mesh = m;
// シェイプキー(morph)を考慮してbounding boxを更新するために必要
mesh.refreshBoundingInfo({ applyMorph: true });
if (SYSTEM_MESH_NAMES.some(n => mesh.name.includes(n))) {
mesh.receiveShadows = false;
mesh.isVisible = false;
} else {
if (def.receiveShadows !== false) mesh.receiveShadows = true;
if (def.castShadows !== false) {
this.shadowGenerator1.addShadowCaster(mesh);
}
if (mesh.material) {
if (mesh.material instanceof BABYLON.MultiMaterial) {
for (const subMat of mesh.material.subMaterials) {
(subMat as BABYLON.PBRMaterial).reflectionTexture = this.envMapIndoor;
(subMat as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
}
} else {
(mesh.material as BABYLON.PBRMaterial).reflectionTexture = this.envMapIndoor;
(mesh.material as BABYLON.PBRMaterial).useGLTFLightFalloff = true; // Clustered Lightingではphysical falloffを持つマテリアルはアーチファクトが発生する https://doc.babylonjs.com/features/featuresDeepDive/lights/clusteredLighting/#materials-with-a-physical-falloff-may-cause-artefacts
}
}
}
if (!this.scene.meshes.includes(mesh)) this.scene.addMesh(mesh);
}
});
const objectInstance = await def.createInstance({
room: null,
scene: this.scene,
root,
options: args.options,
model,
id: args.id,
});
objectInstance.onInited?.();
model.bakeMesh();
this.objectInstance = objectInstance;
this.objectMesh = root;
}
public updateObjectOption(key: string, value: any) {
this.objectInstance?.onOptionsUpdated?.([key, value]);
}
public resize() {
this.engine.resize();
}
public destroy() {
this.engine.dispose();
}
}