diff --git a/packages/client-core/i18n/en/editor.json b/packages/client-core/i18n/en/editor.json index 43f67cdfa7..f11740ae0b 100755 --- a/packages/client-core/i18n/en/editor.json +++ b/packages/client-core/i18n/en/editor.json @@ -258,6 +258,7 @@ "lbl-receiveShadow": "Receive Shadow", "lbl-interactable": "Interactable", "lbl-generateBVH": "Generate BVH", + "lbl-avoidCameraOcclusion": "Avoid Camera Occlusion", "lbl-matrixAutoUpdate": "MatrixAutoUpdate", "lbl-name": "Name", "lbl-url": "Model Url", diff --git a/packages/editor/src/components/hierarchy/HierarchyTreeNode.tsx b/packages/editor/src/components/hierarchy/HierarchyTreeNode.tsx index 24e3617238..97f5d80e0d 100644 --- a/packages/editor/src/components/hierarchy/HierarchyTreeNode.tsx +++ b/packages/editor/src/components/hierarchy/HierarchyTreeNode.tsx @@ -151,7 +151,7 @@ export const HierarchyTreeNode = (props: HierarchyTreeNodeProps) => { const entityTreeComponent = getComponent(node.entityNode as Entity, EntityTreeComponent) parentNode = entityTreeComponent?.parentEntity const parentTreeComponent = getComponent(entityTreeComponent?.parentEntity!, EntityTreeComponent) - if (!node.lastChild && parentNode && parentTreeComponent.children.length > node.childIndex + 1) { + if (!node.lastChild && parentNode && parentTreeComponent?.children.length > node.childIndex + 1) { beforeNode = parentTreeComponent.children[node.childIndex + 1] } } else { diff --git a/packages/editor/src/components/inputs/AudioInput.tsx b/packages/editor/src/components/inputs/AudioInput.tsx index 83387aed30..be92025619 100755 --- a/packages/editor/src/components/inputs/AudioInput.tsx +++ b/packages/editor/src/components/inputs/AudioInput.tsx @@ -4,6 +4,7 @@ import { AudioFileTypes } from '@etherealengine/engine/src/assets/constants/file import { ItemTypes } from '../../constants/AssetTypes' import FileBrowserInput from './FileBrowserInput' +import { StringInputProps } from './StringInput' /** * AudioInput used to render component view for audio inputs. @@ -12,7 +13,7 @@ import FileBrowserInput from './FileBrowserInput' * @param {any} rest * @constructor */ -export function AudioInput({ onChange, ...rest }) { +export function AudioInput({ onChange, ...rest }: StringInputProps) { return ( onChange?.(value, {}, e)} error={isOver && !canDrop} canDrop={isOver && canDrop} diff --git a/packages/editor/src/components/inputs/FolderInput.tsx b/packages/editor/src/components/inputs/FolderInput.tsx index 5da7c4ec40..f1b59a58a8 100755 --- a/packages/editor/src/components/inputs/FolderInput.tsx +++ b/packages/editor/src/components/inputs/FolderInput.tsx @@ -4,6 +4,7 @@ import { AllFileTypes } from '@etherealengine/engine/src/assets/constants/fileTy import { ItemTypes } from '../../constants/AssetTypes' import FileBrowserInput from './FileBrowserInput' +import { StringInputProps } from './StringInput' /** * FolderInput used to render component view for folder inputs. @@ -12,7 +13,7 @@ import FileBrowserInput from './FileBrowserInput' * @param {any} rest * @constructor */ -export function FolderInput({ onChange, ...rest }) { +export function FolderInput({ onChange, ...rest }: StringInputProps) { return ( @@ -35,7 +36,7 @@ export default function ImagePreviewInput({ value, onChange, ...rest }) { ) } -export function ImagePreviewInputGroup({ name, label, value, onChange, ...rest }) { +export function ImagePreviewInputGroup({ name, label, value, onChange, ...rest }: StringInputProps & InputGroupProps) { return ( diff --git a/packages/editor/src/components/inputs/InputGroup.tsx b/packages/editor/src/components/inputs/InputGroup.tsx index 704b9ae527..854b7c5f9f 100755 --- a/packages/editor/src/components/inputs/InputGroup.tsx +++ b/packages/editor/src/components/inputs/InputGroup.tsx @@ -149,7 +149,7 @@ export function InputGroupInfo({ info }: InputGroupInfoProp) { * * @type {Object} */ -type InputGroupPropType = React.PropsWithChildren< +export type InputGroupProps = React.PropsWithChildren< { name: string disabled?: boolean @@ -169,7 +169,7 @@ type InputGroupPropType = React.PropsWithChildren< * @param {string} label * @constructor */ -export function InputGroup({ name, children, disabled, info, label, ...rest }: InputGroupPropType) { +export function InputGroup({ name, children, disabled, info, label, ...rest }: InputGroupProps) { const styles = useStyles() return ( diff --git a/packages/editor/src/components/inputs/MaterialInput.tsx b/packages/editor/src/components/inputs/MaterialInput.tsx index 5b9a70f56d..42bcb1c81b 100644 --- a/packages/editor/src/components/inputs/MaterialInput.tsx +++ b/packages/editor/src/components/inputs/MaterialInput.tsx @@ -6,7 +6,9 @@ import { EntityOrObjectUUID } from '@etherealengine/engine/src/ecs/functions/Ent import { ItemTypes } from '../../constants/AssetTypes' import { ControlledStringInput } from './StringInput' -export function MaterialInput({ value, onChange, ...rest }) { +export function MaterialInput< + T extends { value: EntityOrObjectUUID; onChange: (val: EntityOrObjectUUID) => any; [key: string]: any } +>({ value, onChange, ...rest }: T) { function onDrop(item, monitor: DropTargetMonitor) { const value = item.value let element = value as EntityOrObjectUUID | EntityOrObjectUUID[] | undefined @@ -29,7 +31,13 @@ export function MaterialInput({ value, onChange, ...rest }) { return ( <> - + ) } diff --git a/packages/editor/src/components/inputs/ModelInput.tsx b/packages/editor/src/components/inputs/ModelInput.tsx index f115725ad5..b279f07769 100755 --- a/packages/editor/src/components/inputs/ModelInput.tsx +++ b/packages/editor/src/components/inputs/ModelInput.tsx @@ -4,6 +4,7 @@ import { ModelFileTypes } from '@etherealengine/engine/src/assets/constants/file import { ItemTypes } from '../../constants/AssetTypes' import FileBrowserInput from './FileBrowserInput' +import { StringInputProps } from './StringInput' /** * ModelInput used to render component view for script inputs. @@ -12,7 +13,7 @@ import FileBrowserInput from './FileBrowserInput' * @param {any} rest * @constructor */ -export function ModelInput({ onChange, ...rest }) { +export function ModelInput({ onChange, ...rest }: StringInputProps) { return ( any; [key: string]: any } +>({ value, onChange, ...rest }: T) { function onDrop(item, monitor: DropTargetMonitor) { const value = item.value let element = value as EntityOrObjectUUID | EntityOrObjectUUID[] | undefined @@ -31,7 +33,13 @@ export function SceneObjectInput({ value, onChange, ...rest }) { return ( <> - + ) } diff --git a/packages/editor/src/components/inputs/ScriptInput.tsx b/packages/editor/src/components/inputs/ScriptInput.tsx index 22a63c91bb..c487d29441 100755 --- a/packages/editor/src/components/inputs/ScriptInput.tsx +++ b/packages/editor/src/components/inputs/ScriptInput.tsx @@ -4,6 +4,7 @@ import { CustomScriptFileTypes } from '@etherealengine/engine/src/assets/constan import { ItemTypes } from '../../constants/AssetTypes' import FileBrowserInput from './FileBrowserInput' +import { StringInputProps } from './StringInput' /** * ScriptInput used to render component view for script inputs. @@ -12,7 +13,7 @@ import FileBrowserInput from './FileBrowserInput' * @param {any} rest * @constructor */ -export function ScriptInput({ onChange, ...rest }) { +export function ScriptInput({ onChange, ...rest }: StringInputProps) { return ( (({ onChange, ...rest }, ref) => ( +const StringInput = React.forwardRef<{}, StringInputProps>(({ onChange, ...rest }, ref) => ( onChange?.(e.target.value, e)} {...rest} ref={ref} /> )) @@ -45,7 +45,7 @@ const DropContainer = (styled as any).div` width: 100%; ` -export const ControlledStringInput = React.forwardRef<{}, StringInputProp>((values, ref) => { +export const ControlledStringInput = React.forwardRef<{}, StringInputProps>((values, ref) => { const { onChange, value, ...rest } = values const inputRef = useRef() diff --git a/packages/editor/src/components/inputs/TexturePreviewInput.tsx b/packages/editor/src/components/inputs/TexturePreviewInput.tsx index d41c53be62..badc26fba0 100644 --- a/packages/editor/src/components/inputs/TexturePreviewInput.tsx +++ b/packages/editor/src/components/inputs/TexturePreviewInput.tsx @@ -12,6 +12,7 @@ import { ItemTypes } from '../../constants/AssetTypes' import FileBrowserInput from './FileBrowserInput' import { ImageContainer } from './ImagePreviewInput' import InputGroup from './InputGroup' +import { StringInputProps } from './StringInput' import Vector2Input from './Vector2Input' /** @@ -21,7 +22,7 @@ import Vector2Input from './Vector2Input' * @param {any} rest * @constructor */ -export function TextureInput({ onChange, ...rest }) { +export function TextureInput({ onChange, ...rest }: StringInputProps) { return ( { {envmapComponent.envMapTextureType.value === EnvMapTextureType.Cubemap && ( - + )} {envmapComponent.envMapTextureType.value === EnvMapTextureType.Equirectangular && ( { onChange={updateProperty(ModelComponent, 'generateBVH')} /> + + + diff --git a/packages/editor/src/components/properties/NodeEditor.tsx b/packages/editor/src/components/properties/NodeEditor.tsx index ed827102fd..d8d830260b 100755 --- a/packages/editor/src/components/properties/NodeEditor.tsx +++ b/packages/editor/src/components/properties/NodeEditor.tsx @@ -8,6 +8,47 @@ import { SelectionAction } from '../../services/SelectionServices' import PropertyGroup from './PropertyGroup' import { EditorPropType } from './Util' +interface NodeErrorProps { + name?: string + children?: React.ReactNode +} + +interface NodeErrorState { + error: Error | null +} + +class NodeEditorErrorBoundary extends React.Component { + public state: NodeErrorState = { + error: null + } + + public static getDerivedStateFromError(error: Error): NodeErrorState { + // Update state so the next render will show the fallback UI. + return { error } + } + + public componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error('Uncaught error:', error, errorInfo) + } + + public render() { + if (this.state.error) { + return ( +
+

+ + [{this.props.name}] {this.state.error.message}` + +

+
{this.state.error.stack}
+
+ ) + } + + return this.props.children + } +} + //declaring NodeEditorProps type NodeEditorProps = EditorPropType & { description?: string @@ -40,7 +81,7 @@ export const NodeEditor: React.FC> = ({ : undefined } > - {children} + {children} ) } diff --git a/packages/editor/src/components/properties/Object3DNodeEditor.tsx b/packages/editor/src/components/properties/Object3DNodeEditor.tsx index ca69fd4797..e76a83cb98 100755 --- a/packages/editor/src/components/properties/Object3DNodeEditor.tsx +++ b/packages/editor/src/components/properties/Object3DNodeEditor.tsx @@ -274,9 +274,9 @@ export const Object3DNodeEditor = (props: Object3DProps) => { onChange={(nuId) => { if (!!materialLibrary.materials[nuId].value) { if (Array.isArray(mesh.material)) { - mesh.material[currentMaterialId.value] = materialFromId(nuId).material + mesh.material[currentMaterialId.value] = materialFromId('' + nuId).material } else { - mesh.material = materialFromId(nuId).material + mesh.material = materialFromId('' + nuId).material mesh.material.needsUpdate = true } } diff --git a/packages/engine/package.json b/packages/engine/package.json index 7fa2616313..d8e902bcbc 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -30,6 +30,7 @@ "version": "1.2.0-rc6", "dependencies": { "@dimforge/rapier3d-compat": "0.11.1", + "@hookstate/subscribable": "^4.0.0", "@etherealengine/common": "^1.2.0-rc6", "@etherealengine/hyperflux": "^1.2.0-rc6", "@etherealengine/volumetric": "^1.1.1", diff --git a/packages/engine/src/assets/classes/AssetLoader.ts b/packages/engine/src/assets/classes/AssetLoader.ts index 388a3bf1bf..32c618fc12 100644 --- a/packages/engine/src/assets/classes/AssetLoader.ts +++ b/packages/engine/src/assets/classes/AssetLoader.ts @@ -26,14 +26,11 @@ import { Entity } from '../../ecs/classes/Entity' import { matchActionOnce } from '../../networking/functions/matchActionOnce' import { SourceType } from '../../renderer/materials/components/MaterialSource' import loadVideoTexture from '../../renderer/materials/functions/LoadVideoTexture' -import { getRendererSceneMetadataState } from '../../renderer/WebGLRendererSystem' -import { generateMeshBVH } from '../../scene/functions/bvhWorkerPool' import { DEFAULT_LOD_DISTANCES, LODS_REGEXP } from '../constants/LoaderConstants' import { AssetClass } from '../enum/AssetClass' import { AssetType } from '../enum/AssetType' import { FBXLoader } from '../loaders/fbx/FBXLoader' import { registerMaterials } from '../loaders/gltf/extensions/RegisterMaterialsExtension' -import type { GLTF } from '../loaders/gltf/GLTFLoader' import { TGALoader } from '../loaders/tga/TGALoader' import { USDZLoader } from '../loaders/usdz/USDZLoader' import { DependencyTreeActions } from './DependencyTree' @@ -55,16 +52,6 @@ export function disposeDracoLoaderWorkers(): void { Engine.instance.gltfLoader.dracoLoader?.dispose() } -export const loadExtensions = async (gltf: GLTF) => { - if (isClient) { - const bvhTraverse: Promise[] = [] - gltf.scene.traverse((mesh) => { - ;(mesh as Mesh).isMesh && bvhTraverse.push(generateMeshBVH(mesh as Mesh)) - }) - await Promise.all(bvhTraverse) - } -} - const onUploadDropBuffer = (uuid?: string) => function (this: BufferAttribute) { const dropBuffer = () => { diff --git a/packages/engine/src/debug/systems/DebugRendererSystem.ts b/packages/engine/src/debug/systems/DebugRendererSystem.ts index 0d16f54692..d63a52a62e 100644 --- a/packages/engine/src/debug/systems/DebugRendererSystem.ts +++ b/packages/engine/src/debug/systems/DebugRendererSystem.ts @@ -1,14 +1,18 @@ +import { useEffect } from 'react' import { BufferAttribute, BufferGeometry, Line, LineBasicMaterial, LineSegments, Vector3 } from 'three' +import { MeshBVHVisualizer } from 'three-mesh-bvh' -import { createActionQueue, getState, removeActionQueue } from '@etherealengine/hyperflux' +import { createActionQueue, getState, removeActionQueue, startReactor, useHookstate } from '@etherealengine/hyperflux' import { Engine } from '../../ecs/classes/Engine' import { EngineActions } from '../../ecs/classes/EngineState' import { World } from '../../ecs/classes/World' +import { useOptionalComponent } from '../../ecs/functions/ComponentFunctions' import { RaycastArgs } from '../../physics/classes/Physics' import { RaycastHit } from '../../physics/types/PhysicsTypes' import { RendererState } from '../../renderer/RendererState' import InfiniteGridHelper from '../../scene/classes/InfiniteGridHelper' +import { GroupComponent, startGroupQueryReactor } from '../../scene/components/GroupComponent' import { ObjectLayers } from '../../scene/constants/ObjectLayers' import { setObjectLayers } from '../../scene/functions/setObjectLayers' @@ -35,6 +39,44 @@ export default async function DebugRendererSystem(world: World) { const sceneLoadQueue = createActionQueue(EngineActions.sceneLoaded.matches) const debugEnable = getState(RendererState).debugEnable + const visualizers = [] as MeshBVHVisualizer[] + + startGroupQueryReactor(function DebugReactor(props) { + const entity = props.entity + const group = useOptionalComponent(entity, GroupComponent) + const debug = useHookstate(debugEnable) + + // add MeshBVHVisualizer to meshes when debugEnable is true + useEffect(() => { + const groupVisualizers = [] as MeshBVHVisualizer[] + + function addMeshVVHVisualizer(obj: THREE.Mesh) { + const mesh = obj as any as THREE.Mesh + if (mesh.isMesh && mesh.geometry?.boundsTree) { + const meshBVHVisualizer = new MeshBVHVisualizer(mesh) + mesh.parent!.add(meshBVHVisualizer) + visualizers.push(meshBVHVisualizer) + groupVisualizers.push(meshBVHVisualizer) + meshBVHVisualizer.depth = 20 + meshBVHVisualizer.displayParents = false + meshBVHVisualizer.update() + return meshBVHVisualizer + } + } + + if (debug.value && group) { + for (const obj of group.value) obj.traverse(addMeshVVHVisualizer) + return () => { + for (const visualizer of groupVisualizers) { + visualizer.removeFromParent() + visualizers.splice(visualizers.indexOf(visualizer), 1) + } + } + } + }, [group, debug]) + + return null + }) const execute = () => { const _enabled = debugEnable.value @@ -71,6 +113,10 @@ export default async function DebugRendererSystem(world: World) { debugLines.clear() } + for (const visualizer of visualizers) { + visualizer.updateMatrixWorld(true) + } + for (const line of debugLines) { line.updateMatrixWorld() if (Date.now() - line.userData.originTime > debugLineLifetime) { diff --git a/packages/engine/src/ecs/functions/ComponentFunctions.ts b/packages/engine/src/ecs/functions/ComponentFunctions.ts index 3ca607f562..2e68d2443a 100755 --- a/packages/engine/src/ecs/functions/ComponentFunctions.ts +++ b/packages/engine/src/ecs/functions/ComponentFunctions.ts @@ -1,3 +1,4 @@ +import { subscribable } from '@hookstate/subscribable' import * as bitECS from 'bitecs' import React, { startTransition, useEffect, useLayoutEffect } from 'react' @@ -198,8 +199,13 @@ export const setComponent = ( if (!hasComponent(entity, Component)) { value = Component.onInit(entity, world) ?? args Component.existenceMap[entity].set(true) - if (!Component.stateMap[entity]) Component.stateMap[entity] = hookstate(value) - else Component.stateMap[entity]!.set(value) + if (!Component.stateMap[entity]) { + const state = hookstate(value, subscribable()) + Component.stateMap[entity] = state + state.subscribe((value) => { + Component.valueMap[entity] = value + }) + } else Component.stateMap[entity]!.set(value) bitECS.addComponent(world, Component, entity, false) // don't clear data on-add if (Component.reactor) { if (!Component.reactor.name || Component.reactor.name === 'reactor') diff --git a/packages/engine/src/scene/components/GroundPlaneComponent.ts b/packages/engine/src/scene/components/GroundPlaneComponent.ts index 2868368c07..9888c276d1 100644 --- a/packages/engine/src/scene/components/GroundPlaneComponent.ts +++ b/packages/engine/src/scene/components/GroundPlaneComponent.ts @@ -4,14 +4,13 @@ import { Color, Mesh, MeshLambertMaterial, PlaneGeometry, ShadowMaterial } from import { matches } from '../../common/functions/MatchesUtils' import { Engine } from '../../ecs/classes/Engine' -import { defineComponent, useComponent } from '../../ecs/functions/ComponentFunctions' +import { defineComponent, getComponentState, useComponent } from '../../ecs/functions/ComponentFunctions' import { Physics } from '../../physics/classes/Physics' import { CollisionGroups } from '../../physics/enums/CollisionGroups' import { getInteractionGroups } from '../../physics/functions/getInteractionGroups' import { ObjectLayers } from '../constants/ObjectLayers' -import { generateMeshBVH } from '../functions/bvhWorkerPool' import { enableObjectLayer } from '../functions/setObjectLayers' -import { addObjectToGroup, removeObjectFromGroup } from './GroupComponent' +import { addObjectToGroup, GroupComponent, removeObjectFromGroup } from './GroupComponent' export const GroundPlaneComponent = defineComponent({ name: 'GroundPlaneComponent', @@ -57,7 +56,6 @@ export const GroundPlaneComponent = defineComponent({ mesh.name = 'GroundPlaneMesh' mesh.material.polygonOffset = true mesh.material.polygonOffsetUnits = -0.01 - mesh.traverse(generateMeshBVH) enableObjectLayer(mesh, ObjectLayers.Camera, true) addObjectToGroup(entity, mesh) diff --git a/packages/engine/src/scene/components/ModelComponent.ts b/packages/engine/src/scene/components/ModelComponent.ts index 436d2bf558..2d45c1c282 100644 --- a/packages/engine/src/scene/components/ModelComponent.ts +++ b/packages/engine/src/scene/components/ModelComponent.ts @@ -1,6 +1,6 @@ import { entityExists } from 'bitecs' import { useEffect } from 'react' -import { Object3D, Scene } from 'three' +import { Mesh, Object3D, Scene } from 'three' import { AssetLoader } from '../../assets/classes/AssetLoader' import { DependencyTree } from '../../assets/classes/DependencyTree' @@ -9,6 +9,7 @@ import { Engine } from '../../ecs/classes/Engine' import { defineComponent, getComponent, + getComponentState, hasComponent, removeComponent, useComponent, @@ -36,6 +37,7 @@ export const ModelComponent = defineComponent({ return { src: '', generateBVH: true, + avoidCameraOcclusion: false, scene: undefined as undefined | Scene } }, @@ -43,7 +45,8 @@ export const ModelComponent = defineComponent({ toJSON: (entity, component) => { return { src: component.src.value, - generateBVH: component.generateBVH.value + generateBVH: component.generateBVH.value, + avoidCameraOcclusion: component.avoidCameraOcclusion.value } }, @@ -128,24 +131,45 @@ function ModelReactor({ root }: EntityReactorProps) { loadModel() }, [modelComponent.src]) - // update scene useEffect(() => { const scene = modelComponent.scene.value - if (!scene || groupComponent?.value?.find((group: any) => group === scene)) return + if (!scene) return + enableObjectLayer(scene, ObjectLayers.Camera, model.avoidCameraOcclusion) + }, [modelComponent.avoidCameraOcclusion, modelComponent.scene]) + + // update scene + useEffect(() => { + const scene = modelComponent.scene.get({ noproxy: true }) + if (!scene) return addObjectToGroup(entity, scene) + + if (groupComponent?.value?.find((group: any) => group === scene)) return parseGLTFModel(entity) + setBoundingBoxComponent(entity) + removeComponent(entity, SceneAssetPendingTagComponent) + + let active = true if (model.generateBVH) { - scene.traverse(generateMeshBVH) + const bvhDone = [] as Promise[] + scene.traverse((obj: Mesh) => { + if (obj.geometry?.boundsTree) return + bvhDone.push(generateMeshBVH(obj)) + }) + // trigger group state invalidation when bvh is done + Promise.all(bvhDone).then(() => { + if (!active) return + const group = getComponentState(entity, GroupComponent) + if (group) group.set([...group.value]) + }) } - setBoundingBoxComponent(entity) - /** @todo - improve BVH implementation */ - // enableObjectLayer(scene, ObjectLayers.Camera, modelComponent.generateBVH.value) - removeComponent(entity, SceneAssetPendingTagComponent) - return () => removeObjectFromGroup(entity, scene) - }, [modelComponent.scene]) + return () => { + removeObjectFromGroup(entity, scene) + active = false + } + }, [modelComponent.scene, model.generateBVH]) return null } diff --git a/packages/engine/src/scene/components/NameComponent.ts b/packages/engine/src/scene/components/NameComponent.ts index 29949e2aec..306d25617d 100755 --- a/packages/engine/src/scene/components/NameComponent.ts +++ b/packages/engine/src/scene/components/NameComponent.ts @@ -6,7 +6,7 @@ import { defineComponent } from '../../ecs/functions/ComponentFunctions' export const NameComponent = defineComponent({ name: 'NameComponent', - onInit: () => '', + onInit: () => undefined as any as string, onSet: (entity, component, name?: string) => { if (typeof name !== 'string') throw new Error('NameComponent expects a non-empty string')