diff --git a/packages/editor/src/components/properties/LightmapBakerProperties.tsx b/packages/editor/src/components/properties/LightmapBakerProperties.tsx new file mode 100644 index 00000000000..cdb50bedcb1 --- /dev/null +++ b/packages/editor/src/components/properties/LightmapBakerProperties.tsx @@ -0,0 +1,149 @@ +/* +CPAL-1.0 License + +The contents of this file are subject to the Common Public Attribution License +Version 1.0. (the "License"); you may not use this file except in compliance +with the License. You may obtain a copy of the License at +https://github.com/EtherealEngine/etherealengine/blob/dev/LICENSE. +The License is based on the Mozilla Public License Version 1.1, but Sections 14 +and 15 have been added to cover use of software over a computer network and +provide for limited attribution for the Original Developer. In addition, +Exhibit A has been modified to be consistent with Exhibit B. + +Software distributed under the License is distributed on an "AS IS" basis, +WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the +specific language governing rights and limitations under the License. + +The Original Code is Ethereal Engine. + +The Original Developer is the Initial Developer. The Initial Developer of the +Original Code is the Ethereal Engine team. + +All portions of the code written by the Ethereal Engine team are Copyright © 2021-2023 +Ethereal Engine. All Rights Reserved. +*/ + +import React, { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import { WebGLRenderer } from 'three' + +import { ComponentType } from '@etherealengine/engine/src/ecs/functions/ComponentFunctions' +import { BoolArg, FloatArg } from '@etherealengine/engine/src/renderer/materials/constants/DefaultArgs' +import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' +import { useHookstate } from '@etherealengine/hyperflux' +import { State } from '@etherealengine/hyperflux/functions/StateFunctions' + +import { Grid, Typography } from '@mui/material' + +import { bakeLightmaps } from '../../lightmapper/lightmap' +import { WorkbenchSettings } from '../../lightmapper/workbench' +import { Button } from '../inputs/Button' +import ParameterInput from '../inputs/ParameterInput' +import CollapsibleBlock from '../layout/CollapsibleBlock' +import Well from '../layout/Well' + +export default function LightmapBakerProperties({ + modelState +}: { + modelState: State> +}) { + const { t } = useTranslation() + const bakeProperties = useHookstate(() => ({ + ao: false, + aoDistance: 0.05, + bounceMultiplier: 1, + emissiveMultiplier: 1, + lightMapSize: 1024, + texelsPerUnit: 16, + samplerSettings: { + targetSize: 64, + offset: 0, + near: 0.05, + far: 50 + } + })) + + const baking = useHookstate(false) + + const doLightmapBake = useCallback(async () => { + const scene = modelState.scene.value + if (!scene) throw Error('model has no scene') + baking.set(true) + const bakeRenderer = new WebGLRenderer() + const disposeRenderer = () => { + bakeRenderer.dispose() + baking.set(false) + } + try { + const lightmap = await bakeLightmaps(scene, bakeProperties.value, async () => bakeRenderer) + disposeRenderer() + return lightmap + } catch (e) { + disposeRenderer() + throw e + } + }, [modelState.scene]) + + return ( + <> + + + { + return (val) => { + bakeProperties[k].set(val) + } + }} + /> + + + Sampler Settings + + + { + return (val) => { + bakeProperties.samplerSettings[k].set(val) + } + }} + /> + + + + {!baking.value && } + {baking.value && Baking...} + + + ) +} diff --git a/packages/editor/src/components/properties/LoopAnimationNodeEditor.tsx b/packages/editor/src/components/properties/LoopAnimationNodeEditor.tsx index 511013fc79a..1a2487e985d 100755 --- a/packages/editor/src/components/properties/LoopAnimationNodeEditor.tsx +++ b/packages/editor/src/components/properties/LoopAnimationNodeEditor.tsx @@ -69,7 +69,7 @@ export const LoopAnimationNodeEditor: EditorComponentType = (props) => { { label: 'None', value: -1 }, ...animationComponent.animations.map((clip, index) => ({ label: clip.name, value: index })) ]) - }, [modelComponent?.asset, loopAnimationComponent.hasAvatarAnimations]) + }, [modelComponent?.scene, loopAnimationComponent.hasAvatarAnimations]) const onChangePlayingAnimation = (index) => { commitProperties(LoopAnimationComponent, { diff --git a/packages/engine/src/assets/csm/CSM.ts b/packages/engine/src/assets/csm/CSM.ts index ee90df6b43d..dc0d8c0f371 100644 --- a/packages/engine/src/assets/csm/CSM.ts +++ b/packages/engine/src/assets/csm/CSM.ts @@ -417,9 +417,7 @@ export class CSM { } teardownMaterial(mesh: Mesh): void { - if (!mesh?.isMesh) return const material = mesh.material as Material - if (!material) return if (!material.userData) material.userData = {} if (material.userData.CSMPlugin) { removeOBCPlugin(material, material.userData.CSMPlugin) diff --git a/packages/engine/src/assets/functions/exportModelGLTF.ts b/packages/engine/src/assets/functions/exportModelGLTF.ts index 68a2694167e..62715718daa 100644 --- a/packages/engine/src/assets/functions/exportModelGLTF.ts +++ b/packages/engine/src/assets/functions/exportModelGLTF.ts @@ -25,7 +25,6 @@ Ethereal Engine. All Rights Reserved. import { Entity } from '../../ecs/classes/Entity' import { getComponent } from '../../ecs/functions/ComponentFunctions' -import { GroupComponent } from '../../scene/components/GroupComponent' import { ModelComponent } from '../../scene/components/ModelComponent' import createGLTFExporter from './createGLTFExporter' @@ -39,7 +38,7 @@ export default async function exportModelGLTF( embedImages: true } ) { - const scene = getComponent(entity, ModelComponent).scene ?? getComponent(entity, GroupComponent)[0] + const scene = getComponent(entity, ModelComponent).scene! const exporter = createGLTFExporter() const modelName = options.relativePath.split('/').at(-1)!.split('.').at(0)! const resourceURI = `model-resources/${modelName}` diff --git a/packages/engine/src/avatar/components/LoopAnimationComponent.ts b/packages/engine/src/avatar/components/LoopAnimationComponent.ts index 80416ebf562..b9cfd2c8a0a 100644 --- a/packages/engine/src/avatar/components/LoopAnimationComponent.ts +++ b/packages/engine/src/avatar/components/LoopAnimationComponent.ts @@ -126,8 +126,7 @@ export const LoopAnimationComponent = defineComponent({ useEffect(() => { if (!animComponent?.animations?.value) return const clip = animComponent.animations.value[loopAnimationComponent.activeClipIndex.value] - const asset = modelComponent?.asset.get(NO_PROXY) ?? null - if (!modelComponent || !asset?.scene || !clip) { + if (!modelComponent?.scene?.value || !clip) { loopAnimationComponent._action.set(null) return } @@ -135,7 +134,7 @@ export const LoopAnimationComponent = defineComponent({ const assetObject = modelComponent.asset.get(NO_PROXY) try { const action = animComponent.mixer.value.clipAction( - assetObject instanceof VRM ? retargetMixamoAnimation(clip, asset.scene, assetObject) : clip + assetObject instanceof VRM ? retargetMixamoAnimation(clip, modelComponent.scene.value, assetObject) : clip ) loopAnimationComponent._action.set(action) return () => { @@ -207,22 +206,20 @@ export const LoopAnimationComponent = defineComponent({ * A model is required for LoopAnimationComponent. */ useEffect(() => { - const asset = modelComponent?.asset.get(NO_PROXY) ?? null - if (!asset?.scene) return + if (!modelComponent?.scene?.value) return const model = getComponent(entity, ModelComponent) if (!hasComponent(entity, AnimationComponent)) { setComponent(entity, AnimationComponent, { - mixer: new AnimationMixer(model.asset!.scene), + mixer: new AnimationMixer(model.scene!), animations: [] }) } - }, [modelComponent?.asset, loopAnimationComponent.hasAvatarAnimations]) + }, [modelComponent?.scene, loopAnimationComponent.hasAvatarAnimations]) useEffect(() => { - const asset = modelComponent?.asset.get(NO_PROXY) ?? null if ( - !asset?.scene || + !modelComponent?.scene?.value || !animComponent || !loopAnimationComponent.animationPack.value || lastAnimationPack.value === loopAnimationComponent.animationPack.value diff --git a/packages/engine/src/avatar/functions/spawnAvatarReceptor.ts b/packages/engine/src/avatar/functions/spawnAvatarReceptor.ts index 69b2c48e24a..e0a263403a5 100644 --- a/packages/engine/src/avatar/functions/spawnAvatarReceptor.ts +++ b/packages/engine/src/avatar/functions/spawnAvatarReceptor.ts @@ -48,13 +48,11 @@ import { AvatarCollisionMask, CollisionGroups } from '../../physics/enums/Collis import { getInteractionGroups } from '../../physics/functions/getInteractionGroups' import { PhysicsState } from '../../physics/state/PhysicsState' import { EnvmapComponent } from '../../scene/components/EnvmapComponent' -import { addObjectToGroup } from '../../scene/components/GroupComponent' import { NameComponent } from '../../scene/components/NameComponent' import { ShadowComponent } from '../../scene/components/ShadowComponent' import { UUIDComponent } from '../../scene/components/UUIDComponent' import { VisibleComponent } from '../../scene/components/VisibleComponent' import { EnvMapSourceType } from '../../scene/constants/EnvMapEnum' -import { proxifyParentChildRelationships } from '../../scene/functions/loadGLTFModel' import { DistanceFromCameraComponent, FrustumCullCameraComponent } from '../../transform/components/DistanceComponents' import { TransformComponent } from '../../transform/components/TransformComponent' import { AnimationComponent } from '../components/AnimationComponent' @@ -89,10 +87,6 @@ export const spawnAvatarReceptor = (entityUUID: EntityUUID) => { const userName = userNames[entityUUID] const shortId = ownerID.substring(0, 7) setComponent(entity, NameComponent, 'avatar-' + (userName ? shortId + ' (' + userName + ')' : shortId)) - const obj3d = new Object3D() - obj3d.entity = entity - addObjectToGroup(entity, obj3d) - proxifyParentChildRelationships(obj3d) setComponent(entity, VisibleComponent, true) diff --git a/packages/engine/src/input/systems/ClientInputSystem.ts b/packages/engine/src/input/systems/ClientInputSystem.ts index 16f732b9c2c..eb2963faf89 100755 --- a/packages/engine/src/input/systems/ClientInputSystem.ts +++ b/packages/engine/src/input/systems/ClientInputSystem.ts @@ -515,7 +515,7 @@ const execute = () => { if (hits.length && hits[0].distance < hitDistance) { const object = hits[0].object const parentObject = Object3DUtils.findAncestor(object, (obj) => obj.parent === Engine.instance.scene) - if (parentObject?.entity) { + if (parentObject.entity) { assignedInputEntity = parentObject.entity hitDistance = hits[0].distance } diff --git a/packages/engine/src/scene/components/GroupComponent.tsx b/packages/engine/src/scene/components/GroupComponent.tsx index cfa20224cb6..e92514e3a1f 100644 --- a/packages/engine/src/scene/components/GroupComponent.tsx +++ b/packages/engine/src/scene/components/GroupComponent.tsx @@ -56,9 +56,7 @@ export const GroupComponent = defineComponent({ onRemove: (entity, component) => { for (const obj of component.value) { - if (obj.parent) { - obj.removeFromParent() - } + obj.removeFromParent() } } }) diff --git a/packages/engine/src/scene/components/ModelComponent.ts b/packages/engine/src/scene/components/ModelComponent.ts index a57880692a9..9545a4f238f 100644 --- a/packages/engine/src/scene/components/ModelComponent.ts +++ b/packages/engine/src/scene/components/ModelComponent.ts @@ -24,7 +24,7 @@ Ethereal Engine. All Rights Reserved. */ import { useEffect } from 'react' -import { Scene } from 'three' +import { Object3D, Scene } from 'three' import { NO_PROXY, createState, getMutableState, getState, none, useHookstate } from '@etherealengine/hyperflux' @@ -42,6 +42,7 @@ import { SceneState } from '../../ecs/classes/Scene' import { defineComponent, getComponent, + getOptionalComponent, hasComponent, removeComponent, serializeComponent, @@ -51,6 +52,7 @@ import { useQuery } from '../../ecs/functions/ComponentFunctions' import { useEntityContext } from '../../ecs/functions/EntityFunctions' +import { EntityTreeComponent } from '../../ecs/functions/EntityTree' import { EngineRenderer } from '../../renderer/WebGLRendererSystem' import { SourceType } from '../../renderer/materials/components/MaterialSource' import { removeMaterialSource } from '../../renderer/materials/functions/MaterialLibraryFunctions' @@ -143,11 +145,41 @@ function ModelReactor() { const model = modelComponent.value if (!model.src) { - // const dudScene = new Scene() as Scene & Object3D - // dudScene.entity = entity - // addObjectToGroup(entity, dudScene) - // proxifyParentChildRelationships(dudScene) - modelComponent.scene.set(null) + const dudScene = new Scene() as Scene & Object3D + dudScene.entity = entity + Object.defineProperties(dudScene, { + parent: { + get() { + if (EngineRenderer.instance?.rendering) return null + if (getComponent(entity, EntityTreeComponent)?.parentEntity) { + return ( + getComponent(getComponent(entity, EntityTreeComponent).parentEntity!, GroupComponent)?.[0] ?? + Engine.instance.scene + ) + } + }, + set(value) { + throw new Error('Cannot set parent of proxified object') + } + }, + children: { + get() { + if (EngineRenderer.instance?.rendering) return [] + return hasComponent(entity, EntityTreeComponent) + ? getComponent(entity, EntityTreeComponent) + .children.filter((child) => getOptionalComponent(child, GroupComponent)?.length) + .flatMap((child) => getComponent(child, GroupComponent)) + : [] + }, + set(value) { + throw new Error('Cannot set children of proxified object') + } + }, + isProxified: { + value: true + } + }) + modelComponent.scene.set(dudScene) modelComponent.asset.set(null) return } @@ -168,8 +200,7 @@ function ModelReactor() { addError(entity, ModelComponent, 'INVALID_SOURCE', 'Invalid URL') return } - const boneMatchedAsset = autoconvertMixamoAvatar(loadedAsset) as GLTF - boneMatchedAsset.scene.animations = boneMatchedAsset.animations + const boneMatchedAsset = autoconvertMixamoAvatar(loadedAsset) modelComponent.asset.set(boneMatchedAsset) }, (onprogress) => { @@ -200,12 +231,11 @@ function ModelReactor() { if (!asset) return removeError(entity, ModelComponent, 'INVALID_SOURCE') removeError(entity, ModelComponent, 'LOADING_ERROR') - const sceneObj = getComponent(entity, GroupComponent)[0] as Scene - - sceneObj.userData.src = model.src - sceneObj.userData.sceneID = getModelSceneID(entity) - //sceneObj.userData.type === 'glb' && delete asset.scene.userData.type - modelComponent.scene.set(sceneObj) + asset.scene.animations = asset.animations + asset.scene.userData.src = model.src + asset.scene.userData.sceneID = getModelSceneID(entity) + asset.scene.userData.type === 'glb' && delete asset.scene.userData.type + modelComponent.scene.set(asset.scene) }, [modelComponent.asset]) // update scene @@ -226,7 +256,7 @@ function ModelReactor() { }) else removeComponent(entity, SceneAssetPendingTagComponent) - const loadedJsonHierarchy = parseGLTFModel(entity, asset.scene as Scene) + const loadedJsonHierarchy = parseGLTFModel(entity) const uuid = getModelSceneID(entity) SceneState.loadScene(uuid, { @@ -244,9 +274,6 @@ function ModelReactor() { return () => { clearMaterials(src) getMutableState(SceneState).scenes[uuid].set(none) - // for(const child of scene.children) { - // removeEntity(child.entity) - // } } }, [modelComponent.scene]) diff --git a/packages/engine/src/scene/functions/loadGLTFModel.test.ts b/packages/engine/src/scene/functions/loadGLTFModel.test.ts index 4395cdc3cb5..6233ff9a063 100644 --- a/packages/engine/src/scene/functions/loadGLTFModel.test.ts +++ b/packages/engine/src/scene/functions/loadGLTFModel.test.ts @@ -32,7 +32,13 @@ import { createMockNetwork } from '../../../tests/util/createMockNetwork' import { loadEmptyScene } from '../../../tests/util/loadEmptyScene' import { destroyEngine } from '../../ecs/classes/Engine' import { SceneState } from '../../ecs/classes/Scene' -import { defineComponent, defineQuery, getComponent, setComponent } from '../../ecs/functions/ComponentFunctions' +import { + defineComponent, + defineQuery, + getComponent, + getMutableComponent, + setComponent +} from '../../ecs/functions/ComponentFunctions' import { createEntity } from '../../ecs/functions/EntityFunctions' import { EntityTreeComponent } from '../../ecs/functions/EntityTree' import { createEngine } from '../../initializeEngine' @@ -82,14 +88,14 @@ describe.skip('loadGLTFModel', () => { }) const entityName = 'entity name' const number = Math.random() - const scene = new Scene() - const mesh = new Mesh() + const mesh = new Scene() mesh.userData = { 'xrengine.entity': entityName, // 'xrengine.spawn-point': '', 'xrengine.CustomComponent.val': number } - scene.add(mesh) + const modelComponent = getMutableComponent(entity, ModelComponent) + modelComponent.scene.set(mesh) addObjectToGroup(entity, mesh) const modelQuery = defineQuery([TransformComponent, GroupComponent]) const childQuery = defineQuery([ @@ -100,7 +106,7 @@ describe.skip('loadGLTFModel', () => { ]) //todo: revise this so that we're forcing the sceneloadingsystem to execute its reactors, // then we can validate the ECS data directly like we were doing before - const jsonHierarchy = parseGLTFModel(entity, scene) + const jsonHierarchy = parseGLTFModel(entity) const sceneID = getModelSceneID(entity) getMutableState(SceneState).scenes[sceneID].set({ metadata: { diff --git a/packages/engine/src/scene/functions/loadGLTFModel.ts b/packages/engine/src/scene/functions/loadGLTFModel.ts index f192da4c3e0..1d0ec5086ab 100644 --- a/packages/engine/src/scene/functions/loadGLTFModel.ts +++ b/packages/engine/src/scene/functions/loadGLTFModel.ts @@ -23,7 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { AnimationMixer, Bone, InstancedMesh, Mesh, Object3D, Scene, SkinnedMesh } from 'three' +import { AnimationMixer, Bone, InstancedMesh, Mesh, Object3D, SkinnedMesh } from 'three' import { EntityUUID } from '@etherealengine/common/src/interfaces/EntityUUID' import { AnimationComponent } from '../../avatar/components/AnimationComponent' @@ -147,25 +147,20 @@ export const parseObjectComponentsFromGLTF = ( return entityJson } -export const parseGLTFModel = (entity: Entity, scene: Scene) => { +export const parseGLTFModel = (entity: Entity) => { const model = getComponent(entity, ModelComponent) - + const scene = model.scene! scene.updateMatrixWorld(true) computeTransformMatrix(entity) // always parse components first using old ECS parsing schema const entityJson = parseObjectComponentsFromGLTF(entity, scene) // current ECS parsing schema - - const children = [...scene.children] - for (const child of children) { - child.parent = model.scene - iterateObject3D(child, (obj: Object3D) => { - const uuid = obj.uuid as EntityUUID - const eJson = generateEntityJsonFromObject(entity, obj, entityJson[uuid]) - entityJson[uuid] = eJson - }) - } + iterateObject3D(scene, (obj: Object3D) => { + const uuid = obj.uuid as EntityUUID + const eJson = generateEntityJsonFromObject(entity, obj, entityJson[uuid]) + entityJson[uuid] = eJson + }) enableObjectLayer(scene, ObjectLayers.Scene, true) @@ -182,42 +177,6 @@ export const parseGLTFModel = (entity: Entity, scene: Scene) => { return entityJson } -export const proxifyParentChildRelationships = (obj: Object3D) => { - const objEntity = obj.entity - Object.defineProperties(obj, { - parent: { - get() { - if (EngineRenderer.instance?.rendering) return null - if (getComponent(objEntity, EntityTreeComponent)?.parentEntity) { - const result = - getComponent(getComponent(objEntity, EntityTreeComponent).parentEntity!, GroupComponent)?.[0] ?? - Engine.instance.scene - return result ?? null - } - }, - set(value) { - throw new Error('Cannot set parent of proxified object') - } - }, - children: { - get() { - if (EngineRenderer.instance?.rendering) return [] - return hasComponent(objEntity, EntityTreeComponent) - ? getComponent(objEntity, EntityTreeComponent) - .children.filter((child) => getOptionalComponent(child, GroupComponent)?.length) - .flatMap((child) => getComponent(child, GroupComponent)) - : [] - }, - set(value) { - throw new Error('Cannot set children of proxified object') - } - }, - isProxified: { - value: true - } - }) -} - export const generateEntityJsonFromObject = (rootEntity: Entity, obj: Object3D, entityJson?: EntityJsonType) => { // create entity outside of scene loading reactor since we need to access it before the reactor is guaranteed to have executed const objEntity = UUIDComponent.getOrCreateEntityByUUID(obj.uuid as EntityUUID) @@ -255,7 +214,38 @@ export const generateEntityJsonFromObject = (rootEntity: Entity, obj: Object3D, setComponent(objEntity, GLTFLoadedComponent, ['entity']) /** Proxy children with EntityTreeComponent if it exists */ - proxifyParentChildRelationships(obj) + Object.defineProperties(obj, { + parent: { + get() { + if (EngineRenderer.instance?.rendering) return null + if (getComponent(objEntity, EntityTreeComponent)?.parentEntity) { + return ( + getComponent(getComponent(objEntity, EntityTreeComponent).parentEntity!, GroupComponent)?.[0] ?? + Engine.instance.scene + ) + } + }, + set(value) { + throw new Error('Cannot set parent of proxified object') + } + }, + children: { + get() { + if (EngineRenderer.instance?.rendering) return [] + return hasComponent(objEntity, EntityTreeComponent) + ? getComponent(objEntity, EntityTreeComponent) + .children.filter((child) => getOptionalComponent(child, GroupComponent)?.length) + .flatMap((child) => getComponent(child, GroupComponent)) + : [] + }, + set(value) { + throw new Error('Cannot set children of proxified object') + } + }, + isProxified: { + value: true + } + }) obj.removeFromParent = () => { if (getComponent(objEntity, EntityTreeComponent)?.parentEntity) { diff --git a/packages/engine/src/scene/systems/SceneLoadingSystem.tsx b/packages/engine/src/scene/systems/SceneLoadingSystem.tsx index 3b0058435b1..01e7329fb1c 100755 --- a/packages/engine/src/scene/systems/SceneLoadingSystem.tsx +++ b/packages/engine/src/scene/systems/SceneLoadingSystem.tsx @@ -40,7 +40,6 @@ import { SystemImportType, getSystemsFromSceneData } from '@etherealengine/proje import { Not } from 'bitecs' import React from 'react' -import { Group } from 'three' import { AppLoadingState, AppLoadingStates } from '../../common/AppLoadingService' import { Engine } from '../../ecs/classes/Engine' import { EngineActions, EngineState } from '../../ecs/classes/EngineState' @@ -65,7 +64,6 @@ import { PhysicsState } from '../../physics/state/PhysicsState' import { ComponentJsonType, EntityJsonType, SceneID, scenePath } from '../../schemas/projects/scene.schema' import { TransformComponent } from '../../transform/components/TransformComponent' import { GLTFLoadedComponent } from '../components/GLTFLoadedComponent' -import { GroupComponent, addObjectToGroup } from '../components/GroupComponent' import { NameComponent } from '../components/NameComponent' import { SceneAssetPendingTagComponent } from '../components/SceneAssetPendingTagComponent' import { SceneDynamicLoadTagComponent } from '../components/SceneDynamicLoadTagComponent' @@ -74,7 +72,6 @@ import { SceneTagComponent } from '../components/SceneTagComponent' import { SourceComponent } from '../components/SourceComponent' import { UUIDComponent } from '../components/UUIDComponent' import { VisibleComponent } from '../components/VisibleComponent' -import { proxifyParentChildRelationships } from '../functions/loadGLTFModel' const reactor = () => { const scenes = useHookstate(getMutableState(SceneState).scenes) @@ -321,14 +318,6 @@ const EntityChildLoadReactor = (props: { uuid: props.entityUUID, childIndex: entityJSONState.index.value }) - - if (!hasComponent(entity, GroupComponent)) { - const obj3d = new Group() - obj3d.entity = entity - addObjectToGroup(entity, obj3d) - proxifyParentChildRelationships(obj3d) - } - setComponent(entity, SourceComponent, props.sceneID) return () => { removeEntity(entity)