From f25cef63a100c7f3d299cc1ac54f59e914ad0612 Mon Sep 17 00:00:00 2001 From: David Gordon Date: Wed, 21 Jun 2023 14:22:56 -0700 Subject: [PATCH 1/2] re-commit --- .../components/properties/ModelNodeEditor.tsx | 56 +---- ...ODProperties.tsx => VariantNodeEditor.tsx} | 58 ++--- .../editor/src/functions/PrefabEditors.tsx | 6 +- .../editor/src/functions/lodsFromModel.ts | 193 ----------------- .../engine/src/scene/SceneClientModule.ts | 4 +- .../src/scene/components/LODComponent.tsx | 198 ------------------ .../src/scene/components/ModelComponent.ts | 3 - .../src/scene/components/VariantComponent.tsx | 114 ++++++++++ .../scene/functions/loaders/LODFunctions.ts | 191 ----------------- .../functions/loaders/VariantFunctions.ts | 46 ++++ .../engine/src/scene/systems/LODSystem.tsx | 161 -------------- .../src/scene/systems/VariantSystem.tsx | 63 ++++++ 12 files changed, 248 insertions(+), 845 deletions(-) rename packages/editor/src/components/properties/{LODProperties.tsx => VariantNodeEditor.tsx} (66%) delete mode 100644 packages/editor/src/functions/lodsFromModel.ts delete mode 100644 packages/engine/src/scene/components/LODComponent.tsx create mode 100644 packages/engine/src/scene/components/VariantComponent.tsx delete mode 100644 packages/engine/src/scene/functions/loaders/LODFunctions.ts create mode 100644 packages/engine/src/scene/functions/loaders/VariantFunctions.ts delete mode 100644 packages/engine/src/scene/systems/LODSystem.tsx create mode 100644 packages/engine/src/scene/systems/VariantSystem.tsx diff --git a/packages/editor/src/components/properties/ModelNodeEditor.tsx b/packages/editor/src/components/properties/ModelNodeEditor.tsx index 669e0f0de8..96edc9188d 100755 --- a/packages/editor/src/components/properties/ModelNodeEditor.tsx +++ b/packages/editor/src/components/properties/ModelNodeEditor.tsx @@ -26,42 +26,30 @@ Ethereal Engine. All Rights Reserved. import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' -import { - DefaultModelTransformParameters, - ModelTransformParameters -} from '@etherealengine/engine/src/assets/classes/ModelTransform' import { AnimationManager } from '@etherealengine/engine/src/avatar/AnimationManager' import { LoopAnimationComponent } from '@etherealengine/engine/src/avatar/components/LoopAnimationComponent' import { addComponent, - getComponent, getOptionalComponent, hasComponent, removeComponent, - setComponent, useComponent } from '@etherealengine/engine/src/ecs/functions/ComponentFunctions' import { EquippableComponent } from '@etherealengine/engine/src/interaction/components/EquippableComponent' -import { CallbackComponent, getCallback } from '@etherealengine/engine/src/scene/components/CallbackComponent' +import { getCallback } from '@etherealengine/engine/src/scene/components/CallbackComponent' import { getEntityErrors } from '@etherealengine/engine/src/scene/components/ErrorComponent' import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' -import { addError, clearErrors } from '@etherealengine/engine/src/scene/functions/ErrorFunctions' -import { State, useState } from '@etherealengine/hyperflux' +import { useState } from '@etherealengine/hyperflux' import ViewInArIcon from '@mui/icons-material/ViewInAr' import exportGLTF from '../../functions/exportGLTF' -import { convertToScaffold, createLODsFromModel } from '../../functions/lodsFromModel' -import { LODsFromModelParameters } from '../../functions/lodsFromModel' import BooleanInput from '../inputs/BooleanInput' -import { Button, PropertiesPanelButton } from '../inputs/Button' +import { PropertiesPanelButton } from '../inputs/Button' import InputGroup from '../inputs/InputGroup' import ModelInput from '../inputs/ModelInput' import SelectInput from '../inputs/SelectInput' -import CollapsibleBlock from '../layout/CollapsibleBlock' -import PaginatedList from '../layout/PaginatedList' import Well from '../layout/Well' -import GLTFTransformProperties from './GLTFTransformProperties' import ModelTransformProperties from './ModelTransformProperties' import NodeEditor from './NodeEditor' import ScreenshareTargetNodeEditor from './ScreenshareTargetNodeEditor' @@ -89,11 +77,6 @@ export const ModelNodeEditor: EditorComponentType = (props) => { const loopAnimationComponent = getOptionalComponent(entity, LoopAnimationComponent) - const lodParms = useState(() => ({ - serialize: false, - levels: [] - })) - const onChangeEquippable = useCallback(() => { if (isEquippable.value) { removeComponent(props.entity, EquippableComponent) @@ -156,12 +139,6 @@ export const ModelNodeEditor: EditorComponentType = (props) => { getCallback(props.entity, 'xre.play')!() } - const onAddLODLevel = useCallback(() => { - lodParms.levels[lodParms.levels.length].set({ - ...DefaultModelTransformParameters - }) - }, []) - return ( { - -
-
-

LODs

-
- - - - - - - ) => { - return - }} - /> -
- -
-
- -
-
-
modelComponent.src.set(val)} /> {!exporting.value && modelComponent.src.value && ( diff --git a/packages/editor/src/components/properties/LODProperties.tsx b/packages/editor/src/components/properties/VariantNodeEditor.tsx similarity index 66% rename from packages/editor/src/components/properties/LODProperties.tsx rename to packages/editor/src/components/properties/VariantNodeEditor.tsx index 95a1c41f8b..338e98f062 100644 --- a/packages/editor/src/components/properties/LODProperties.tsx +++ b/packages/editor/src/components/properties/VariantNodeEditor.tsx @@ -27,32 +27,24 @@ import React, { useCallback } from 'react' import { useTranslation } from 'react-i18next' import { Entity } from '@etherealengine/engine/src/ecs/classes/Entity' -import { - getComponent, - getMutableComponent, - useComponent -} from '@etherealengine/engine/src/ecs/functions/ComponentFunctions' -import { LODComponent, LODLevel } from '@etherealengine/engine/src/scene/components/LODComponent' -import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' +import { getComponent, useComponent } from '@etherealengine/engine/src/ecs/functions/ComponentFunctions' import { NameComponent } from '@etherealengine/engine/src/scene/components/NameComponent' +import { VariantComponent, VariantLevel } from '@etherealengine/engine/src/scene/components/VariantComponent' import { State } from '@etherealengine/hyperflux' import DeblurIcon from '@mui/icons-material/Deblur' -import { serializeLOD } from '../../functions/lodsFromModel' import { Button } from '../inputs/Button' -import InputGroup, { InputGroupContainer } from '../inputs/InputGroup' +import InputGroup from '../inputs/InputGroup' import ModelInput from '../inputs/ModelInput' -import NumericInput from '../inputs/NumericInput' -import NumericInputGroup from '../inputs/NumericInputGroup' import SelectInput from '../inputs/SelectInput' import PaginatedList from '../layout/PaginatedList' import { EditorComponentType } from './Util' -export const LODProperties: EditorComponentType = ({ entity }: { entity: Entity }) => { +export const VariantNodeEditor: EditorComponentType = ({ entity }: { entity: Entity }) => { const { t } = useTranslation() - const lodComponent = useComponent(entity, LODComponent) + const variantComponent = useComponent(entity, VariantComponent) const nameComponent = getComponent(entity, NameComponent) const onChangeLevelProperty = useCallback( @@ -70,8 +62,8 @@ export const LODProperties: EditorComponentType = ({ entity }: { entity: Entity

{nameComponent}

lodComponent.lodHeuristic.set(val)} + value={variantComponent.heuristic.value} + onChange={(val: typeof variantComponent.heuristic.value) => variantComponent.heuristic.set(val)} options={[ { value: 'DISTANCE', label: t('editor:properties.lod.heuristic-distance') }, { value: 'SCENE_SCALE', label: t('editor:properties.lod.heuristic-sceneScale') }, @@ -82,31 +74,25 @@ export const LODProperties: EditorComponentType = ({ entity }: { entity: Entity ) => { + options={{ countPerPage: 6 }} + list={variantComponent.levels} + element={(level: State) => { return (
- - - - {lodComponent.lodHeuristic.value === 'DEVICE' && ( + {variantComponent.heuristic.value === 'DEVICE' && ( <>
-
- -
) }} @@ -150,4 +126,4 @@ export const LODProperties: EditorComponentType = ({ entity }: { entity: Entity ) } -LODProperties.iconComponent = DeblurIcon +VariantNodeEditor.iconComponent = DeblurIcon diff --git a/packages/editor/src/functions/PrefabEditors.tsx b/packages/editor/src/functions/PrefabEditors.tsx index 762d291957..f26f23df03 100644 --- a/packages/editor/src/functions/PrefabEditors.tsx +++ b/packages/editor/src/functions/PrefabEditors.tsx @@ -41,7 +41,6 @@ import { HemisphereLightComponent } from '@etherealengine/engine/src/scene/compo import { ImageComponent } from '@etherealengine/engine/src/scene/components/ImageComponent' import { InstancingComponent } from '@etherealengine/engine/src/scene/components/InstancingComponent' import { InteriorComponent } from '@etherealengine/engine/src/scene/components/InteriorComponent' -import { LODComponent } from '@etherealengine/engine/src/scene/components/LODComponent' import { MediaComponent } from '@etherealengine/engine/src/scene/components/MediaComponent' import { MediaSettingsComponent } from '@etherealengine/engine/src/scene/components/MediaSettingsComponent' import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' @@ -60,6 +59,7 @@ import { SpawnPointComponent } from '@etherealengine/engine/src/scene/components import { SplineComponent } from '@etherealengine/engine/src/scene/components/SplineComponent' import { SpotLightComponent } from '@etherealengine/engine/src/scene/components/SpotLightComponent' import { SystemComponent } from '@etherealengine/engine/src/scene/components/SystemComponent' +import { VariantComponent } from '@etherealengine/engine/src/scene/components/VariantComponent' import { VideoComponent } from '@etherealengine/engine/src/scene/components/VideoComponent' import { VolumetricComponent } from '@etherealengine/engine/src/scene/components/VolumetricComponent' import { WaterComponent } from '@etherealengine/engine/src/scene/components/WaterComponent' @@ -84,7 +84,6 @@ import HemisphereLightNodeEditor from '../components/properties/HemisphereLightN import ImageNodeEditor from '../components/properties/ImageNodeEditor' import InstancingNodeEditor from '../components/properties/InstancingNodeEditor' import InteriorNodeEditor from '../components/properties/InteriorNodeEditor' -import { LODProperties } from '../components/properties/LODProperties' import MediaNodeEditor from '../components/properties/MediaNodeEditor' import { MediaSettingsEditor } from '../components/properties/MediaSettingsEditor' import ModelNodeEditor from '../components/properties/ModelNodeEditor' @@ -107,6 +106,7 @@ import SpotLightNodeEditor from '../components/properties/SpotLightNodeEditor' import SystemNodeEditor from '../components/properties/SystemNodeEditor' import TransformPropertyGroup from '../components/properties/TransformPropertyGroup' import { EditorComponentType } from '../components/properties/Util' +import { VariantNodeEditor } from '../components/properties/VariantNodeEditor' import VideoNodeEditor from '../components/properties/VideoNodeEditor' import VolumetricNodeEditor from '../components/properties/VolumetricNodeEditor' import WaterNodeEditor from '../components/properties/WaterNodeEditor' @@ -150,7 +150,7 @@ EntityNodeEditor.set(EnvmapComponent, EnvMapEditor) EntityNodeEditor.set(EnvMapBakeComponent, EnvMapBakeNodeEditor) EntityNodeEditor.set(InstancingComponent, InstancingNodeEditor) EntityNodeEditor.set(PersistentAnchorComponent, PersistentAnchorNodeEditor) -EntityNodeEditor.set(LODComponent, LODProperties) +EntityNodeEditor.set(VariantComponent, VariantNodeEditor) export const prefabIcons = { [LightPrefabs.ambientLight]: AmbientLightNodeEditor.iconComponent, diff --git a/packages/editor/src/functions/lodsFromModel.ts b/packages/editor/src/functions/lodsFromModel.ts deleted file mode 100644 index be413ab5d3..0000000000 --- a/packages/editor/src/functions/lodsFromModel.ts +++ /dev/null @@ -1,193 +0,0 @@ -/* -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 { BufferGeometry, InstancedMesh, Material, Mesh, MeshBasicMaterial } from 'three' - -import { ModelTransformParameters } from '@etherealengine/engine/src/assets/classes/ModelTransform' -import createGLTFExporter from '@etherealengine/engine/src/assets/functions/createGLTFExporter' -import { pathResolver } from '@etherealengine/engine/src/assets/functions/pathResolver' -import { Engine } from '@etherealengine/engine/src/ecs/classes/Engine' -import { Entity } from '@etherealengine/engine/src/ecs/classes/Entity' -import { - addComponent, - getComponent, - getMutableComponent, - setComponent -} from '@etherealengine/engine/src/ecs/functions/ComponentFunctions' -import { createEntity, removeEntity } from '@etherealengine/engine/src/ecs/functions/EntityFunctions' -import { addEntityNodeChild } from '@etherealengine/engine/src/ecs/functions/EntityTree' -import { SourceType } from '@etherealengine/engine/src/renderer/materials/components/MaterialSource' -import { - removeMaterialSource, - unregisterMaterial -} from '@etherealengine/engine/src/renderer/materials/functions/MaterialLibraryFunctions' -import { LODComponent, LODLevel } from '@etherealengine/engine/src/scene/components/LODComponent' -import { ModelComponent } from '@etherealengine/engine/src/scene/components/ModelComponent' -import { NameComponent } from '@etherealengine/engine/src/scene/components/NameComponent' -import { getLodPath, processLoadedLODLevel } from '@etherealengine/engine/src/scene/functions/loaders/LODFunctions' -import getFirstMesh from '@etherealengine/engine/src/scene/util/getFirstMesh' -import iterateObject3D from '@etherealengine/engine/src/scene/util/iterateObject3D' -import { State } from '@etherealengine/hyperflux' - -import { uploadProjectFiles } from './assetFunctions' -import exportGLTF from './exportGLTF' - -export type LODsFromModelParameters = { - serialize: boolean - levels: ModelTransformParameters[] -} - -/** - * Iterates through a model's meshes and creates LODComponents for each one - * @param entity : entity to add the LODs to - * @returns array of generated LOD entities - */ -export async function createLODsFromModel( - entity: Entity, - options: LODsFromModelParameters = { - serialize: false, - levels: [] - } -): Promise { - LODComponent.lodsByEntity[entity].value?.map((entity) => removeEntity(entity)) - const model = getComponent(entity, ModelComponent) - const lods: Entity[] = [] - if (model.scene) { - const meshes = iterateObject3D( - model.scene, - (mesh: Mesh) => { - getLodPath(mesh) - return mesh - }, - (mesh: Mesh) => mesh?.isMesh - ) - const exporter = createGLTFExporter() - for (let i = 0; i < meshes.length; i++) { - const mesh = meshes[i] - const lodEntity = createEntity() - addEntityNodeChild(lodEntity, entity) - setComponent(lodEntity, LODComponent, { - target: entity, - lodPath: mesh.userData['lodPath'], - levels: [ - { - model: mesh, - distance: 0, - loaded: true, - metadata: {}, - src: '' - } - ], - instanced: mesh instanceof InstancedMesh - }) - setComponent(lodEntity, NameComponent, mesh.name) - processLoadedLODLevel(lodEntity, 0, mesh) - if (options.serialize) { - const lodComponent = getMutableComponent(lodEntity, LODComponent) - const lodURL = await serializeLOD(model.src, lodEntity, lodComponent.levels[0], exporter) - const lodDistance = 5 - for (let i = 0; i < options.levels.length; i++) { - const lodParms = options.levels[i] - const levelURL = await Engine.instance.api.service('model-transform').create({ - src: lodURL, - transformParameters: lodParms - }) - lodComponent.levels[i].set({ - model: null, - distance: lodDistance * (i + 1), - loaded: false, - metadata: {}, - src: levelURL - }) - } - } - lods.push(lodEntity) - } - } - LODComponent.lodsByEntity[entity].set(lods) - return Promise.resolve(lods) -} - -export async function serializeLOD( - rootSrc: string, - entity: Entity, - level: State, - gltfExporter = createGLTFExporter() -) { - const mesh = getFirstMesh(level.model.value!)! - //clone the mesh and remove its world matrix so it can be exported - //also convert instanced meshes into singleton version as instance matrix data is stored in the scaffold - const toExport: Mesh = - mesh instanceof InstancedMesh ? new Mesh(mesh.geometry, mesh.material) : mesh.clone() - //remove lodIndex property which will be re-added by the lod system on import - toExport.geometry.hasAttribute('lodIndex') && toExport.geometry.deleteAttribute('lodIndex') - toExport.removeFromParent() - toExport.position.set(0, 0, 0) - toExport.rotation.set(0, 0, 0) - toExport.scale.set(1, 1, 1) - toExport.updateMatrixWorld() - const [, , projectName, fileName] = pathResolver().exec(rootSrc)! - //create a new filename for the lod - const nuRelativePath = fileName.replace(/\.[^.]*$/, `_${mesh.name}.gltf`) - const nuFileName = nuRelativePath.split('/').pop()!.split('.').shift()! - const lodURL = rootSrc.replace(/(.*)\/([^/]*)\.[^.]*$/, `$1/model-resources/${nuFileName}/${nuFileName}.gltf`) - const gltf = await gltfExporter.parseAsync(toExport, { - binary: false, - embedImages: false, - includeCustomExtensions: true, - path: lodURL, - resourceURI: '..' - }) - const [, , , nuNuRelativePath] = pathResolver().exec(lodURL)! - const file = new File([JSON.stringify(gltf)], nuNuRelativePath) - const urls = await Promise.all(uploadProjectFiles(projectName, [file]).promises) - const result = urls[0][0] - return Promise.resolve(result) -} - -export function convertToScaffold(entity: Entity) { - const modelComponent = getComponent(entity, ModelComponent) - modelComponent.scene && - iterateObject3D(modelComponent.scene, (mesh: Mesh | InstancedMesh) => { - if (!mesh?.isMesh) return - mesh.geometry = new BufferGeometry() - mesh.material = new MeshBasicMaterial() - }) - const scaffoldPath = modelComponent.src.replace(/(\.[^.]*$)/, '_scaffold$1') - exportGLTF(entity, scaffoldPath).then(() => { - getMutableComponent(entity, ModelComponent).src.set(scaffoldPath) - LODComponent.lodsByEntity[entity].value?.map((lodEntity) => { - const lodComponent = getMutableComponent(lodEntity, LODComponent) - lodComponent.levels.map((level) => { - level.model.set(null) - removeMaterialSource({ - path: level.src.value, - type: SourceType.MODEL - }) - level.loaded.set(false) - }) - }) - }) -} diff --git a/packages/engine/src/scene/SceneClientModule.ts b/packages/engine/src/scene/SceneClientModule.ts index 891f56e1e4..ad46290f04 100644 --- a/packages/engine/src/scene/SceneClientModule.ts +++ b/packages/engine/src/scene/SceneClientModule.ts @@ -28,13 +28,13 @@ import { defineSystem } from '../ecs/functions/SystemFunctions' import { MountPointSystem } from '../interaction/systems/MountPointSystem' import { MaterialLibrarySystem } from '../renderer/materials/systems/MaterialLibrarySystem' import { LightSystem } from './systems/LightSystem' -import { LODSystem } from './systems/LODSystem' import { ParticleSystem } from './systems/ParticleSystemSystem' import { PortalLoadSystem } from './systems/PortalLoadSystem' import { SceneLoadingSystem } from './systems/SceneLoadingSystem' import { SceneObjectDynamicLoadSystem } from './systems/SceneObjectDynamicLoadSystem' import { SceneObjectSystem } from './systems/SceneObjectSystem' import { SceneObjectUpdateSystem } from './systems/SceneObjectUpdateSystem' +import { VariantSystem } from './systems/VariantSystem' export const SceneSystemUpdateGroup = defineSystem({ uuid: 'ee.engine.scene.SceneSystemUpdateGroup', @@ -45,7 +45,7 @@ export const SceneSystemLoadGroup = defineSystem({ uuid: 'ee.engine.scene.SceneSystemLoadGroup', subSystems: [ SceneLoadingSystem, - LODSystem, + VariantSystem, PortalLoadSystem, SceneObjectDynamicLoadSystem, MaterialLibrarySystem, diff --git a/packages/engine/src/scene/components/LODComponent.tsx b/packages/engine/src/scene/components/LODComponent.tsx deleted file mode 100644 index 328149192d..0000000000 --- a/packages/engine/src/scene/components/LODComponent.tsx +++ /dev/null @@ -1,198 +0,0 @@ -/* -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 { ReactElement, useEffect } from 'react' -import React from 'react' -import { InstancedBufferAttribute, InstancedMesh, Mesh } from 'three' -import matches from 'ts-matches' - -import { createState } from '@etherealengine/hyperflux' - -import { AssetLoader } from '../../assets/classes/AssetLoader' -import { GLTF } from '../../assets/loaders/gltf/GLTFLoader' -import { Entity, UndefinedEntity } from '../../ecs/classes/Entity' -import { defineComponent, getComponent, useComponent } from '../../ecs/functions/ComponentFunctions' -import { useEntityContext } from '../../ecs/functions/EntityFunctions' -import { isMobileXRHeadset } from '../../xr/XRState' -import { LODPath, processLoadedLODLevel, unloadLODLevel } from '../functions/loaders/LODFunctions' -import getFirstMesh from '../util/getFirstMesh' -import { UUIDComponent } from './UUIDComponent' - -export type LODLevel = { - distance: number - loaded: boolean - src: string - model: Mesh | InstancedMesh | null - metadata: Record -} - -export const LODComponent = defineComponent({ - name: 'EE_LOD', - - jsonID: 'lod', - - onInit: (entity) => ({ - target: entity, - lodPath: '' as LODPath, - instanced: false, - levels: [] as LODLevel[], - lodHeuristic: 'MANUAL' as 'DISTANCE' | 'SCENE_SCALE' | 'MANUAL' | 'DEVICE', - instanceMatrix: new InstancedBufferAttribute(new Float32Array(), 16), - instanceLevels: new InstancedBufferAttribute(new Uint8Array(), 1) - }), - - onSet: (entity, component, json) => { - if (!json) return - if (['number', 'string'].includes(typeof json.target)) { - const targetEntity = typeof json.target === 'string' ? UUIDComponent.entitiesByUUID[json.target] : json.target - if (targetEntity && component.target.value !== targetEntity) { - LODComponent.lodsByEntity[targetEntity].set( - ( - (LODComponent.lodsByEntity[targetEntity].value ?? []).filter( - (e) => ![component.target.value, entity].includes(e) - ) ?? [] - ).concat(entity) - ) - component.target.set(targetEntity) - } - } - - if (typeof json.lodHeuristic === 'string') component.lodHeuristic.set(json.lodHeuristic) - if (typeof json.lodPath === 'string') component.lodPath.set(json.lodPath) - if (typeof json.instanced === 'boolean') component.instanced.set(json.instanced) - - if ( - !!json.levels && - matches - .arrayOf( - matches.shape({ - distance: matches.number, - model: matches.any, - src: matches.string, - loaded: matches.boolean - }) - ) - .test(json.levels) - ) { - component.levels.set(json.levels) - } - if (typeof json.instanceMatrix === 'object') { - if ( - json.instanceMatrix instanceof InstancedBufferAttribute && - json.instanceMatrix.array instanceof Float32Array - ) { - component.instanceMatrix.set(json.instanceMatrix) - } else if (json.instanceMatrix instanceof Array && matches.arrayOf(matches.number).test(json.instanceMatrix)) { - component.instanceMatrix.set(new InstancedBufferAttribute(new Float32Array(json.instanceMatrix), 16)) - } - } - if (typeof json.instanceLevels === 'object') { - if (json.instanceLevels instanceof InstancedBufferAttribute && json.instanceLevels.array instanceof Uint8Array) { - component.instanceLevels.set(json.instanceLevels) - } else if (json.instanceLevels instanceof Array && matches.arrayOf(matches.number).test(json.instanceLevels)) { - component.instanceLevels.set(new InstancedBufferAttribute(new Uint8Array(json.instanceLevels), 1)) - } - } - }, - - onRemove: (entity, component) => { - const targetEntity = component.target.value - if (targetEntity) { - LODComponent.lodsByEntity[targetEntity].set( - (LODComponent.lodsByEntity[targetEntity].value ?? []).filter( - (e) => ![component.target.value, entity].includes(e) - ) ?? [] - ) - } - }, - - toJSON: (entity, component) => ({ - instanced: component.instanced.value, - target: getComponent(component.target.value, UUIDComponent), - levels: component.levels.value.map((level) => { - return { - distance: level.distance, - model: null, - src: level.src, - metadata: level.metadata, - loaded: false - } - }), - lodPath: component.lodPath.value, - lodHeuristic: component.lodHeuristic.value, - instanceMatrix: Array.from(component.instanceMatrix.value.array), - instanceLevels: Array.from(component.instanceLevels.value.array) - }), - - reactor: LODReactor, - - lodsByEntity: createState({} as Record) -}) - -function LODReactor(): ReactElement { - const entity = useEntityContext() - const lodComponent = useComponent(entity, LODComponent) - - useEffect(() => { - if (lodComponent.lodHeuristic.value === 'DEVICE') { - const mobileLodIdx = lodComponent.levels.findIndex((level) => level.metadata.value['device'] === 'MOBILE') - const mobileLod = lodComponent.levels[mobileLodIdx] - const desktopLodIdx = lodComponent.levels.findIndex((level) => level.metadata.value['device'] === 'DESKTOP') - const desktopLod = lodComponent.levels[desktopLodIdx] - const toLoad = isMobileXRHeadset ? mobileLod : desktopLod - const toLoadIdx = isMobileXRHeadset ? mobileLodIdx : desktopLodIdx - const toUnload = isMobileXRHeadset ? desktopLod : mobileLod - new Promise((resolve) => { - toLoad.loaded.value && resolve() - AssetLoader.load(toLoad.src.value, {}, (loadedScene: GLTF) => { - const mesh = getFirstMesh(loadedScene.scene) - mesh && processLoadedLODLevel(entity, toLoadIdx, mesh) - toLoad.loaded.set(true) - resolve() - }) - }).then(() => { - toUnload && unloadLODLevel(toUnload) - }) - } - }, [lodComponent.lodHeuristic]) - - return ( - <> - {lodComponent.levels.map((level, index) => ( - - ))} - - ) -} - -const LodLevelReactor = React.memo(({ entity, level }: { level: number; entity: Entity }) => { - const lodComponent = useComponent(entity, LODComponent) - - useEffect(() => { - const levelModel = lodComponent.levels[level].model.value - }, [lodComponent.levels[level].model]) - - return null -}) diff --git a/packages/engine/src/scene/components/ModelComponent.ts b/packages/engine/src/scene/components/ModelComponent.ts index 3b6e0d1369..0c65d29ec7 100644 --- a/packages/engine/src/scene/components/ModelComponent.ts +++ b/packages/engine/src/scene/components/ModelComponent.ts @@ -53,7 +53,6 @@ import { addError, removeError } from '../functions/ErrorFunctions' import { parseGLTFModel } from '../functions/loadGLTFModel' import { enableObjectLayer } from '../functions/setObjectLayers' import { addObjectToGroup, GroupComponent, removeObjectFromGroup } from './GroupComponent' -import { LODComponent } from './LODComponent' import { SceneAssetPendingTagComponent } from './SceneAssetPendingTagComponent' import { SceneObjectComponent } from './SceneObjectComponent' import { UUIDComponent } from './UUIDComponent' @@ -109,8 +108,6 @@ export const ModelComponent = defineComponent({ removeObjectFromGroup(entity, component.scene.value) component.scene.set(null) } - LODComponent.lodsByEntity[entity].value && LODComponent.lodsByEntity[entity].set(none) - removeMaterialSource({ type: SourceType.MODEL, path: component.src.value }) }, errors: ['LOADING_ERROR', 'INVALID_URL'], diff --git a/packages/engine/src/scene/components/VariantComponent.tsx b/packages/engine/src/scene/components/VariantComponent.tsx new file mode 100644 index 0000000000..a37f1c806c --- /dev/null +++ b/packages/engine/src/scene/components/VariantComponent.tsx @@ -0,0 +1,114 @@ +/* +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 { ReactElement, useEffect } from 'react' +import React from 'react' +import matches from 'ts-matches' + +import { Entity } from '../../ecs/classes/Entity' +import { + defineComponent, + getComponent, + getMutableComponent, + useComponent, + useOptionalComponent, + useQuery +} from '../../ecs/functions/ComponentFunctions' +import { useEntityContext } from '../../ecs/functions/EntityFunctions' +import { isMobileXRHeadset } from '../../xr/XRState' +import { setModelVariant } from '../functions/loaders/VariantFunctions' +import { ModelComponent } from './ModelComponent' + +export type VariantLevel = { + src: string + metadata: Record +} + +export const VariantComponent = defineComponent({ + name: 'EE_variant', + + jsonID: 'variant', + + onInit: (entity) => ({ + levels: [] as VariantLevel[], + heuristic: 'MANUAL' as 'DISTANCE' | 'SCENE_SCALE' | 'MANUAL' | 'DEVICE' + }), + + onSet: (entity, component, json) => { + if (!json) return + + if (typeof json.heuristic === 'string') component.heuristic.set(json.heuristic) + if ( + !!json.levels && + matches + .arrayOf( + matches.shape({ + src: matches.string, + metadata: matches.any + }) + ) + .test(json.levels) + ) { + component.levels.set(json.levels) + } + }, + + toJSON: (entity, component) => ({ + levels: component.levels.value.map((level) => { + return { + src: level.src, + metadata: level.metadata + } + }), + heuristic: component.heuristic.value + }), + + reactor: VariantReactor +}) + +function VariantReactor(): ReactElement { + const entity = useEntityContext() + const variantComponent = useComponent(entity, VariantComponent) + + return ( + <> + {variantComponent.levels.map((level, index) => ( + + ))} + + ) +} + +const VariantLevelReactor = React.memo(({ entity, level }: { level: number; entity: Entity }) => { + const variantComponent = useComponent(entity, VariantComponent) + const variantLevel = variantComponent.levels[level] + + const modelComponent = useOptionalComponent(entity, ModelComponent) + + useEffect(() => { + modelComponent && setModelVariant(entity) + }, [variantLevel.src, variantLevel.metadata, modelComponent]) + return null +}) diff --git a/packages/engine/src/scene/functions/loaders/LODFunctions.ts b/packages/engine/src/scene/functions/loaders/LODFunctions.ts deleted file mode 100644 index 54f400ed43..0000000000 --- a/packages/engine/src/scene/functions/loaders/LODFunctions.ts +++ /dev/null @@ -1,191 +0,0 @@ -/* -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 { DynamicDrawUsage, InstancedBufferAttribute, InstancedMesh, Material, Matrix4, Mesh, Object3D } from 'three' - -import { OpaqueType } from '@etherealengine/common/src/interfaces/OpaqueType' -import { NO_PROXY, State } from '@etherealengine/hyperflux' - -import { addOBCPlugin } from '../../../common/functions/OnBeforeCompilePlugin' -import { Engine } from '../../../ecs/classes/Engine' -import { Entity } from '../../../ecs/classes/Entity' -import { ComponentType, getComponent, getMutableComponent } from '../../../ecs/functions/ComponentFunctions' -import { SourceType } from '../../../renderer/materials/components/MaterialSource' -import { removeMaterialSource } from '../../../renderer/materials/functions/MaterialLibraryFunctions' -import { Object3DWithEntity } from '../../components/GroupComponent' -import { LODComponent, LODLevel } from '../../components/LODComponent' -import { ModelComponent } from '../../components/ModelComponent' - -/** - * Processes a loaded LOD level, adding it to the entity's group and adding instanced attributes if necessary - * @param entity : entity to add the level to - * @param index : index of the level in the LODComponent.levels array - * @param mesh : mesh to add to the entity - * @returns - */ -export function processLoadedLODLevel(entity: Entity, index: number, mesh: Mesh) { - if (mesh === null) { - console.warn('trying to process an empty model file') - return - } - const lodComponentState = getMutableComponent(entity, LODComponent) - const lodComponent = getComponent(entity, LODComponent) - const targetModel = getMutableComponent(lodComponent.target, ModelComponent) - const level = lodComponentState.levels[index] - - let previousModel: Object3D | null = lodComponent.levels.find((level) => level.loaded && level.model)?.model ?? null - function addPlugin(mesh: Mesh) { - delete mesh.geometry.attributes['lodIndex'] - delete mesh.geometry.attributes['_lodIndex'] - mesh.geometry.setAttribute('lodIndex', lodComponentState.instanceLevels.get(NO_PROXY)) - const minDistance = index === 0 ? 0 : lodComponent.levels[index - 1].distance - const maxDistance = lodComponent.levels[index].distance - const materials: Material[] = Array.isArray(mesh.material) ? mesh.material : [mesh.material] - materials.forEach((material) => { - addOBCPlugin(material, { - id: 'lod-culling', - priority: 1, - compile: (shader, renderer) => { - shader.fragmentShader = shader.fragmentShader.replace( - 'void main() {\n', - ` - void main() { - float maxDistance = ${maxDistance.toFixed(1)}; - float minDistance = ${minDistance.toFixed(1)}; - // Calculate the camera distance from the geometry - float cameraDistance = length(vViewPosition); - - // Discard fragments outside the minDistance and maxDistance range - if (cameraDistance <= minDistance || cameraDistance >= maxDistance) { - discard; - } -` - ) - } - }) - }) - } - - let loadedMesh: Mesh | InstancedMesh = mesh - - if (mesh instanceof InstancedMesh) { - mesh.instanceMatrix.setUsage(DynamicDrawUsage) - mesh.instanceMatrix.needsUpdate = true - if (lodComponent.instanced) { - if (lodComponent.instanceMatrix.array.length === 0) { - const transforms = new Float32Array(mesh.count * 16) - const matrix = new Matrix4() - for (let i = 0; i < mesh.count; i++) { - mesh.getMatrixAt(i, matrix) - for (let j = 0; j < 16; j++) { - transforms[i * 16 + j] = matrix.elements[j] - } - } - lodComponentState.instanceMatrix.set(new InstancedBufferAttribute(transforms, 16)) - lodComponentState.instanceLevels.set(new InstancedBufferAttribute(new Uint8Array(mesh.count), 1)) - } else { - mesh.instanceMatrix = lodComponent.instanceMatrix - } - } - } else if (lodComponent.instanced) { - const instancedModel = new InstancedMesh(mesh.geometry, mesh.material, lodComponent.instanceMatrix.count) - instancedModel.instanceMatrix = lodComponent.instanceMatrix - loadedMesh = instancedModel - } - - let removeLoaded = () => {} - if (!previousModel) { - previousModel = objectFromLodPath(targetModel.value, lodComponent.lodPath) - removeLoaded = () => { - previousModel?.removeFromParent() - } - } - - loadedMesh instanceof InstancedMesh && addPlugin(loadedMesh) - level.model.set(loadedMesh) - - if (lodComponent.instanceMatrix.array.length === 0) { - lodComponentState.instanceMatrix.set( - new InstancedBufferAttribute(new Float32Array([...loadedMesh.matrix.elements]), 16) - ) - lodComponentState.instanceLevels.set(new InstancedBufferAttribute(new Uint8Array([index]), 1)) - } - if (loadedMesh !== previousModel) { - previousModel.parent?.add(loadedMesh) - loadedMesh.name = previousModel.name - loadedMesh.position.copy(previousModel.position) - loadedMesh.quaternion.copy(previousModel.quaternion) - loadedMesh.scale.copy(previousModel.scale) - loadedMesh.updateMatrixWorld(true) - - removeLoaded() - } -} - -export type LODPath = OpaqueType<'LODPath'> & string - -export function getLodPath(object: Object3D): LODPath { - let walker: Object3D | null = object - let path = '' - while (walker !== null && !(walker as Object3DWithEntity).entity) { - if (walker.userData['lodPath']) { - path = `${walker.userData['lodPath']}${path ? `/${path}` : ''}` - break - } - path = `${walker.name}/${path}` - walker = walker.parent - } - object.userData['lodPath'] = path - return path as LODPath -} - -export function objectFromLodPath(model: ComponentType, path: LODPath): Object3D { - let walker: Object3D | null = model.scene - let prev: typeof walker = null - const pathParts = path.split('/') - while (pathParts.length > 0) { - const part = pathParts.shift() - if (!part) break - prev = walker - walker = walker?.children.find((child) => child.name === part) ?? null - } - if (!walker) { - console.error('walker', walker, 'prev', prev) - throw new Error(`Could not find object from path ${path} in model ${model.scene}`) - } - return walker -} - -export function unloadLODLevel(levelToUnload: State | null) { - if (!levelToUnload?.loaded.value) return - levelToUnload && - removeMaterialSource({ - type: SourceType.MODEL, - path: levelToUnload.src.value - }) - levelToUnload?.model.value?.removeFromParent() - levelToUnload?.model.set(null) - levelToUnload?.loaded.set(false) -} diff --git a/packages/engine/src/scene/functions/loaders/VariantFunctions.ts b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts new file mode 100644 index 0000000000..1e939ff899 --- /dev/null +++ b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts @@ -0,0 +1,46 @@ +import { Entity } from '../../../ecs/classes/Entity' +import { getComponent, getMutableComponent } from '../../../ecs/functions/ComponentFunctions' +import { isMobileXRHeadset } from '../../../xr/XRState' +import { ModelComponent } from '../../components/ModelComponent' +import { VariantComponent } from '../../components/VariantComponent' + +/* +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. +*/ + +/** + * Handles setting model src for model component based on variant component + * @param entity + */ +export function setModelVariant(entity: Entity) { + const variantComponent = getComponent(entity, VariantComponent) + const modelComponent = getMutableComponent(entity, ModelComponent) + + if (variantComponent.heuristic === 'DEVICE') { + const targetDevice = isMobileXRHeadset ? 'MOBILE' : 'DESKTOP' + //set model src to mobile variant src + const deviceVariant = variantComponent.levels.find((level) => level.metadata['device'] === targetDevice) + deviceVariant && modelComponent.src.value !== deviceVariant.src && modelComponent.src.set(deviceVariant.src) + } +} diff --git a/packages/engine/src/scene/systems/LODSystem.tsx b/packages/engine/src/scene/systems/LODSystem.tsx deleted file mode 100644 index 0acf0158cd..0000000000 --- a/packages/engine/src/scene/systems/LODSystem.tsx +++ /dev/null @@ -1,161 +0,0 @@ -/* -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 { Mesh, Vector3 } from 'three' - -import { getState, NO_PROXY, State } from '@etherealengine/hyperflux' - -import { AssetLoader } from '../../assets/classes/AssetLoader' -import { GLTF } from '../../assets/loaders/gltf/GLTFLoader' -import { Engine } from '../../ecs/classes/Engine' -import { EngineState } from '../../ecs/classes/EngineState' -import { ComponentType, defineQuery, getComponent, getMutableComponent } from '../../ecs/functions/ComponentFunctions' -import { defineSystem } from '../../ecs/functions/SystemFunctions' -import { TransformComponent } from '../../transform/components/TransformComponent' -import { LODComponent, LODLevel } from '../components/LODComponent' -import { ModelComponent } from '../components/ModelComponent' -import { objectFromLodPath, processLoadedLODLevel, unloadLODLevel } from '../functions/loaders/LODFunctions' -import getFirstMesh from '../util/getFirstMesh' - -const lodQuery = defineQuery([LODComponent]) -const updateFrequency = 0.1 -let lastUpdate = 0 - -function updateLOD( - index: number, - currentLevel: number, - lodComponent: State>, - lodDistances: number[], - position: Vector3 -) { - const heuristic = lodComponent.lodHeuristic.value - if (['DISTANCE', 'SCENE_SCALE'].includes(heuristic)) { - const cameraPosition = Engine.instance.camera.position - const distance = cameraPosition.distanceToSquared(position) - for (let j = 0; j < lodDistances.length; j++) { - if (distance < Math.pow(lodDistances[j], 2) || j === lodDistances.length - 1) { - const instanceLevels = lodComponent.instanceLevels.get(NO_PROXY) - if (currentLevel !== j) { - instanceLevels.setX(index, j) - } - return j - } - } - } else if (heuristic === 'MANUAL') { - return 0 - //todo: implement manual LOD setting - } else { - throw Error('Invalid LOD heuristic') - } -} - -function execute() { - const engineState = getState(EngineState) - if (engineState.elapsedSeconds - lastUpdate < updateFrequency) return - lastUpdate = engineState.elapsedSeconds - - const referencedLods = new Set() - const position = new Vector3() - - for (const entity of lodQuery()) { - const lodComponent = getMutableComponent(entity, LODComponent) - if (lodComponent.lodHeuristic.value === 'DISTANCE') { - const modelComponent = getComponent(lodComponent.target.value, ModelComponent) - if (!modelComponent.scene) continue - const lodDistances = lodComponent.levels.map((level) => level.distance.value) - const transform = getComponent(lodComponent.target.value, TransformComponent) - let levelsChanged = false - if (lodComponent.instanced.value) { - /*const instancePositions = lodComponent.instanceMatrix.value - const instanceLevels = lodComponent.instanceLevels.value.array - for (let i = 0; i < instancePositions.count; i++) { - position.fromArray(instancePositions.array, i * 16 + 12) - position.applyMatrix4(transform.matrix) - const currentLevel = instanceLevels[i] - const newLevel = updateLOD(i, currentLevel, lodComponent, lodDistances, position) - newLevel !== undefined && referencedLods.add(newLevel) - newLevel !== currentLevel && (levelsChanged = true) - }*/ - lodComponent.levels.forEach((level, i) => { - referencedLods.add(i) - }) - } else { - const currentLevel = lodComponent.instanceLevels.value.array[0] - position.fromArray(lodComponent.instanceMatrix.value.array, 12) - position.applyMatrix4(transform.matrix) - const newLevel = updateLOD(0, currentLevel, lodComponent, lodDistances, position) - newLevel !== undefined && referencedLods.add(newLevel) - newLevel !== currentLevel && (levelsChanged = true) - } - levelsChanged && (lodComponent.instanceLevels.get(NO_PROXY).needsUpdate = true) - const levelsToUnload: State[] = [] - const levelsToLoad: [number, State][] = [] - for (let i = 0; i < lodComponent.levels.length; i++) { - const level = lodComponent.levels[i] - if (referencedLods.has(i)) { - levelsToLoad.push([i, level]) - } else { - if (/*!lodComponent.instanced.value && */ level.loaded.value) { - levelsToUnload.push(level) - } - } - } - const loadPromises: Promise[] = [] - - while (levelsToLoad.length > 0) { - const [i, level] = levelsToLoad.pop()! - if (!level.loaded.value) { - if (level.src.value) { - loadPromises.push( - new Promise((resolve) => { - AssetLoader.load(level.src.value, {}, (loadedScene: GLTF) => { - const mesh = getFirstMesh(loadedScene.scene) - mesh && processLoadedLODLevel(entity, i, mesh) - resolve() - }) - }) - ) - } else { - processLoadedLODLevel(entity, i, objectFromLodPath(modelComponent, lodComponent.lodPath.value) as Mesh) - } - level.loaded.set(true) - } - } - - Promise.all(loadPromises).then(() => { - while (levelsToUnload.length > 0) { - const levelToUnload = levelsToUnload.pop() - levelToUnload && unloadLODLevel(levelToUnload) - } - }) - referencedLods.clear() - } - } -} - -export const LODSystem = defineSystem({ - uuid: 'ee.engine.scene.LODSystem', - execute -}) diff --git a/packages/engine/src/scene/systems/VariantSystem.tsx b/packages/engine/src/scene/systems/VariantSystem.tsx new file mode 100644 index 0000000000..d0b00a1e37 --- /dev/null +++ b/packages/engine/src/scene/systems/VariantSystem.tsx @@ -0,0 +1,63 @@ +/* +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 { useEffect } from 'react' +import { Vector3 } from 'three' + +import { getState } from '@etherealengine/hyperflux' + +import { EngineState } from '../../ecs/classes/EngineState' +import { defineQuery, getMutableComponent, useQuery } from '../../ecs/functions/ComponentFunctions' +import { defineSystem } from '../../ecs/functions/SystemFunctions' +import { ModelComponent } from '../components/ModelComponent' +import { VariantComponent } from '../components/VariantComponent' +import { setModelVariant } from '../functions/loaders/VariantFunctions' + +const updateFrequency = 0.1 +let lastUpdate = 0 + +function execute() { + const engineState = getState(EngineState) + if (engineState.elapsedSeconds - lastUpdate < updateFrequency) return + lastUpdate = engineState.elapsedSeconds +} + +function reactor() { + const modelVariantQuery = useQuery([VariantComponent, ModelComponent]) + + /*useEffect(() => { + for (const entity of modelVariantQuery) { + setModelVariant(entity) + } + }, [modelVariantQuery]) + */ + return null +} + +export const VariantSystem = defineSystem({ + uuid: 'ee.engine.scene.VariantSystem', + execute, + reactor +}) From f360e5ef24f0415287417bb34c183c9477c95a82 Mon Sep 17 00:00:00 2001 From: David Gordon Date: Wed, 21 Jun 2023 17:12:51 -0700 Subject: [PATCH 2/2] reimplement distance heuristic --- packages/client-core/i18n/en/editor.json | 7 ++- .../properties/VariantNodeEditor.tsx | 50 +++++++++++++------ .../src/scene/components/VariantComponent.tsx | 16 ++++++ .../functions/loaders/VariantFunctions.ts | 16 ++++++ .../src/scene/systems/VariantSystem.tsx | 13 ++--- 5 files changed, 77 insertions(+), 25 deletions(-) diff --git a/packages/client-core/i18n/en/editor.json b/packages/client-core/i18n/en/editor.json index 78d9d25224..873d00d957 100755 --- a/packages/client-core/i18n/en/editor.json +++ b/packages/client-core/i18n/en/editor.json @@ -237,7 +237,9 @@ "description": "Link to a room or a website.", "lbl-url": "URL" }, - "lod": { + "variant": { + "name": "Variant", + "description": "Load different assets based on heuristics", "device": "Device", "device-mobile": "Mobile", "device-desktop": "Desktop", @@ -246,7 +248,8 @@ "heuristic-sceneScale": "Scene Scale", "heuristic-manual": "Manual", "heuristic-device": "Device", - "distance": "Distance", + "minDistance": "Minimum Distance", + "maxDistance": "Maximum Distance", "src": "Source" }, "map": { diff --git a/packages/editor/src/components/properties/VariantNodeEditor.tsx b/packages/editor/src/components/properties/VariantNodeEditor.tsx index 338e98f062..5a13e5906a 100644 --- a/packages/editor/src/components/properties/VariantNodeEditor.tsx +++ b/packages/editor/src/components/properties/VariantNodeEditor.tsx @@ -37,13 +37,15 @@ import DeblurIcon from '@mui/icons-material/Deblur' import { Button } from '../inputs/Button' import InputGroup from '../inputs/InputGroup' import ModelInput from '../inputs/ModelInput' +import NumericInput from '../inputs/NumericInput' import SelectInput from '../inputs/SelectInput' import PaginatedList from '../layout/PaginatedList' +import NodeEditor from './NodeEditor' import { EditorComponentType } from './Util' -export const VariantNodeEditor: EditorComponentType = ({ entity }: { entity: Entity }) => { +export const VariantNodeEditor: EditorComponentType = (props: { entity: Entity }) => { const { t } = useTranslation() - + const entity = props.entity const variantComponent = useComponent(entity, VariantComponent) const nameComponent = getComponent(entity, NameComponent) @@ -56,19 +58,21 @@ export const VariantNodeEditor: EditorComponentType = ({ entity }: { entity: Ent [entity] ) return ( -
-

LODs

+
-

{nameComponent}

- + variantComponent.heuristic.set(val)} options={[ - { value: 'DISTANCE', label: t('editor:properties.lod.heuristic-distance') }, - { value: 'SCENE_SCALE', label: t('editor:properties.lod.heuristic-sceneScale') }, - { value: 'MANUAL', label: t('editor:properties.lod.heuristic-manual') }, - { value: 'DEVICE', label: t('editor:properties.lod.heuristic-device') } + { value: 'DISTANCE', label: t('editor:properties.variant.heuristic-distance') }, + { value: 'SCENE_SCALE', label: t('editor:properties.variant.heuristic-sceneScale') }, + { value: 'MANUAL', label: t('editor:properties.variant.heuristic-manual') }, + { value: 'DEVICE', label: t('editor:properties.variant.heuristic-device') } ]} /> @@ -89,23 +93,39 @@ export const VariantNodeEditor: EditorComponentType = ({ entity }: { entity: Ent return (
- + {variantComponent.heuristic.value === 'DEVICE' && ( <> - + )} + {variantComponent.heuristic.value === 'DISTANCE' && ( + <> + + + + + + + + )}
-
+ ) } diff --git a/packages/engine/src/scene/components/VariantComponent.tsx b/packages/engine/src/scene/components/VariantComponent.tsx index a37f1c806c..c7cb74f52a 100644 --- a/packages/engine/src/scene/components/VariantComponent.tsx +++ b/packages/engine/src/scene/components/VariantComponent.tsx @@ -32,11 +32,15 @@ import { defineComponent, getComponent, getMutableComponent, + hasComponent, + removeComponent, + setComponent, useComponent, useOptionalComponent, useQuery } from '../../ecs/functions/ComponentFunctions' import { useEntityContext } from '../../ecs/functions/EntityFunctions' +import { DistanceFromCameraComponent } from '../../transform/components/DistanceComponents' import { isMobileXRHeadset } from '../../xr/XRState' import { setModelVariant } from '../functions/loaders/VariantFunctions' import { ModelComponent } from './ModelComponent' @@ -107,6 +111,18 @@ const VariantLevelReactor = React.memo(({ entity, level }: { level: number; enti const modelComponent = useOptionalComponent(entity, ModelComponent) + useEffect(() => { + //if the variant heuristic is set to Distance, add the DistanceFromCameraComponent + if (variantComponent.heuristic.value === 'DISTANCE') { + setComponent(entity, DistanceFromCameraComponent) + variantLevel.metadata['minDistance'].value === undefined && variantLevel.metadata['minDistance'].set(0) + variantLevel.metadata['maxDistance'].value === undefined && variantLevel.metadata['maxDistance'].set(0) + } else { + //otherwise, remove the DistanceFromCameraComponent + hasComponent(entity, DistanceFromCameraComponent) && removeComponent(entity, DistanceFromCameraComponent) + } + }, [variantComponent.heuristic]) + useEffect(() => { modelComponent && setModelVariant(entity) }, [variantLevel.src, variantLevel.metadata, modelComponent]) diff --git a/packages/engine/src/scene/functions/loaders/VariantFunctions.ts b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts index 1e939ff899..07759d8f7d 100644 --- a/packages/engine/src/scene/functions/loaders/VariantFunctions.ts +++ b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts @@ -1,5 +1,10 @@ +import { DistanceFromCameraComponent } from '@etherealengine/engine/src/transform/components/DistanceComponents' +import { getState } from '@etherealengine/hyperflux' + +import { EngineState } from '../../../ecs/classes/EngineState' import { Entity } from '../../../ecs/classes/Entity' import { getComponent, getMutableComponent } from '../../../ecs/functions/ComponentFunctions' +import { TransformComponent } from '../../../transform/components/TransformComponent' import { isMobileXRHeadset } from '../../../xr/XRState' import { ModelComponent } from '../../components/ModelComponent' import { VariantComponent } from '../../components/VariantComponent' @@ -42,5 +47,16 @@ export function setModelVariant(entity: Entity) { //set model src to mobile variant src const deviceVariant = variantComponent.levels.find((level) => level.metadata['device'] === targetDevice) deviceVariant && modelComponent.src.value !== deviceVariant.src && modelComponent.src.set(deviceVariant.src) + } else if (variantComponent.heuristic === 'DISTANCE') { + const distance = DistanceFromCameraComponent.squaredDistance[entity] + for (let i = 0; i < variantComponent.levels.length; i++) { + const level = variantComponent.levels[i] + if ([level.metadata['minDistance'], level.metadata['maxDistance']].includes(undefined)) continue + const minDistance = Math.pow(level.metadata['minDistance'], 2) + const maxDistance = Math.pow(level.metadata['maxDistance'], 2) + const useLevel = minDistance <= distance && distance <= maxDistance + useLevel && modelComponent.src.value !== level.src && modelComponent.src.set(level.src) + if (useLevel) break + } } } diff --git a/packages/engine/src/scene/systems/VariantSystem.tsx b/packages/engine/src/scene/systems/VariantSystem.tsx index d0b00a1e37..396bc742db 100644 --- a/packages/engine/src/scene/systems/VariantSystem.tsx +++ b/packages/engine/src/scene/systems/VariantSystem.tsx @@ -31,6 +31,7 @@ import { getState } from '@etherealengine/hyperflux' import { EngineState } from '../../ecs/classes/EngineState' import { defineQuery, getMutableComponent, useQuery } from '../../ecs/functions/ComponentFunctions' import { defineSystem } from '../../ecs/functions/SystemFunctions' +import { TransformComponent } from '../../transform/components/TransformComponent' import { ModelComponent } from '../components/ModelComponent' import { VariantComponent } from '../components/VariantComponent' import { setModelVariant } from '../functions/loaders/VariantFunctions' @@ -38,21 +39,17 @@ import { setModelVariant } from '../functions/loaders/VariantFunctions' const updateFrequency = 0.1 let lastUpdate = 0 +const modelVariantQuery = defineQuery([VariantComponent, ModelComponent, TransformComponent]) + function execute() { const engineState = getState(EngineState) if (engineState.elapsedSeconds - lastUpdate < updateFrequency) return lastUpdate = engineState.elapsedSeconds + + modelVariantQuery().forEach(setModelVariant) } function reactor() { - const modelVariantQuery = useQuery([VariantComponent, ModelComponent]) - - /*useEffect(() => { - for (const entity of modelVariantQuery) { - setModelVariant(entity) - } - }, [modelVariantQuery]) - */ return null }