From ef60ca097c188c21422b4dc97c484ba32822402b Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 16 Jan 2024 15:57:06 -0800 Subject: [PATCH 01/67] Start of Resource State --- .../engine/src/assets/classes/AssetLoader.ts | 2 +- .../src/assets/functions/resourceHooks.ts | 1 + .../engine/src/assets/state/ResourceState.ts | 113 ++++++++++++++++++ .../src/scene/components/ModelComponent.tsx | 6 +- 4 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 packages/engine/src/assets/functions/resourceHooks.ts create mode 100644 packages/engine/src/assets/state/ResourceState.ts diff --git a/packages/engine/src/assets/classes/AssetLoader.ts b/packages/engine/src/assets/classes/AssetLoader.ts index b68a152a3d..80a9835374 100644 --- a/packages/engine/src/assets/classes/AssetLoader.ts +++ b/packages/engine/src/assets/classes/AssetLoader.ts @@ -369,7 +369,7 @@ const assetLoadCallback = const getAbsolutePath = (url) => (isAbsolutePath(url) ? url : getState(EngineState).publicPath + url) -type LoadingArgs = { +export type LoadingArgs = { ignoreDisposeGeometry?: boolean forceAssetType?: AssetType assetRoot?: Entity diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts new file mode 100644 index 0000000000..84874492fc --- /dev/null +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -0,0 +1 @@ +export function useGLTF() {} diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts new file mode 100644 index 0000000000..c0d08e5383 --- /dev/null +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -0,0 +1,113 @@ +import { defineState, getMutableState } from '@etherealengine/hyperflux' +import { Entity } from '../../ecs/classes/Entity' +import { AssetLoader, LoadingArgs } from '../classes/AssetLoader' + +//@ts-ignore +THREE.Cache.enabled + +//@ts-ignore +THREE.DefaultLoadingManager.onLoad = () => { + console.log('On Load') +} + +//Called when the item at the url passed in has completed loading +//@ts-ignore +THREE.DefaultLoadingManager.onProgress = (url: string, loaded: number, total: number) => { + console.log('On Progress', url, loaded, total) +} + +//@ts-ignore +THREE.DefaultLoadingManager.onError = (url: string) => { + console.log('On Error', url) +} + +//This doesn't work as you might imagine, it is only called once, the url parameter is pretty much useless +//@ts-ignore +THREE.DefaultLoadingManager.onStart = (url: string, loaded: number, total: number) => { + console.log('On Start', url, loaded, total) +} + +enum ResourceStatus { + Unloaded, + Loading, + Loaded, + Error +} + +export enum ResourceType { + GLTF, + Texture, + Geometry, + ECSData, + Audio, + Unknown +} + +type Resource = { + status: ResourceStatus + type: ResourceType + references: Entity[] + assetRef?: any + metadata: { + size?: number + } + onGPU: boolean +} + +export const ResourceState = defineState({ + name: 'ResourceManagerState', + initial: () => ({ + resources: {} as Record + }) +}) + +const load = ( + entity: Entity, + resourceType: ResourceType, + url: string, + args: LoadingArgs, + onLoad = (response: any) => {}, + onProgress = (request: ProgressEvent) => {}, + onError = (event: ErrorEvent | Error) => {} +) => { + const resourceState = getMutableState(ResourceState) + const resources = resourceState.nested('resources') + if (!resources[url].value) { + resources.merge({ + [url]: { + status: ResourceStatus.Unloaded, + type: resourceType, + references: [entity], + metadata: {}, + onGPU: false + } + }) + } else { + resources[url].references.merge([entity]) + } + + const resource = resources[url] + + AssetLoader.load( + url, + args, + (response) => { + resource.status.set(ResourceStatus.Loaded) + resource.assetRef.set(response) + onLoad(response) + }, + (request) => { + resource.status.set(ResourceStatus.Loading) + resource.metadata.size.set(request.total) + onProgress(request) + }, + (error) => { + resource.status.set(ResourceStatus.Error) + onError(error) + } + ) +} + +export const ResourceManager = { + load +} diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index cced8d5467..9e00642723 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -30,9 +30,9 @@ import { NO_PROXY, createState, getMutableState, getState, none, useHookstate } import { VRM } from '@pixiv/three-vrm' import React from 'react' -import { AssetLoader } from '../../assets/classes/AssetLoader' import { AssetType } from '../../assets/enum/AssetType' import { GLTF } from '../../assets/loaders/gltf/GLTFLoader' +import { ResourceManager, ResourceType } from '../../assets/state/ResourceState' import { AnimationComponent } from '../../avatar/components/AnimationComponent' import { SkinnedMeshComponent } from '../../avatar/components/SkinnedMeshComponent' import { autoconvertMixamoAvatar, isAvaturn } from '../../avatar/functions/avatarFunctions' @@ -166,7 +166,9 @@ function ModelReactor(): JSX.Element { /** @todo this is a hack */ const override = !isAvaturn(model.src) ? undefined : AssetType.glB - AssetLoader.load( + ResourceManager.load( + entity, + ResourceType.GLTF, modelComponent.src.value, { forceAssetType: override, From 10548242d86222aca4c9e6778fbd730fede2aee8 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 16 Jan 2024 15:57:44 -0800 Subject: [PATCH 02/67] licenses --- .../src/assets/functions/resourceHooks.ts | 25 +++++++++++++++++++ .../engine/src/assets/state/ResourceState.ts | 25 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 84874492fc..23678e3b46 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -1 +1,26 @@ +/* +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. +*/ + export function useGLTF() {} diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index c0d08e5383..042132b9f9 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -1,3 +1,28 @@ +/* +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 { defineState, getMutableState } from '@etherealengine/hyperflux' import { Entity } from '../../ecs/classes/Entity' import { AssetLoader, LoadingArgs } from '../classes/AssetLoader' From b49b8b541903d78261c11ac528ba5164651972cf Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 16 Jan 2024 19:29:45 -0800 Subject: [PATCH 03/67] Start of resource manager hooks --- .../src/assets/functions/resourceHooks.ts | 37 ++++++- .../engine/src/assets/state/ResourceState.ts | 20 +++- .../src/scene/components/ModelComponent.tsx | 103 ++++++++++-------- 3 files changed, 108 insertions(+), 52 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 23678e3b46..8a1b415513 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -23,4 +23,39 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -export function useGLTF() {} +import { State, useHookstate } from '@etherealengine/hyperflux' +import { useEffect } from 'react' +import { Entity, UndefinedEntity } from '../../ecs/classes/Entity' +import { LoadingArgs } from '../classes/AssetLoader' +import { GLTF } from '../loaders/gltf/GLTFLoader' +import { ResourceManager, ResourceType } from '../state/ResourceState' + +export function useGLTF( + src: string, + params: LoadingArgs = {}, + entity?: Entity +): [State, State | null>, State] { + const gltf = useHookstate(null) + const progress = useHookstate | null>(null) + const error = useHookstate(null) + + useEffect(() => { + ResourceManager.load( + src, + ResourceType.GLTF, + entity || UndefinedEntity, + params, + (response) => { + gltf.set(response) + }, + (request) => { + progress.set(request) + }, + (err) => { + error.set(err) + } + ) + }, [src]) + + return [gltf, progress, error] +} diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 042132b9f9..ba94598345 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.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 { defineState, getMutableState } from '@etherealengine/hyperflux' +import { defineState, getMutableState, getState } from '@etherealengine/hyperflux' import { Entity } from '../../ecs/classes/Entity' import { AssetLoader, LoadingArgs } from '../classes/AssetLoader' @@ -32,7 +32,8 @@ THREE.Cache.enabled //@ts-ignore THREE.DefaultLoadingManager.onLoad = () => { - console.log('On Load') + const totalSize = getCurrentSizeOfResources() + console.log('Loaded: ' + totalSize + ' bytes of resources') } //Called when the item at the url passed in has completed loading @@ -86,10 +87,21 @@ export const ResourceState = defineState({ }) }) +const getCurrentSizeOfResources = () => { + let size = 0 + const resources = getState(ResourceState).resources + for (const key in resources) { + const resource = resources[key] + if (resource.metadata.size) size += resource.metadata.size + } + + return size +} + const load = ( - entity: Entity, - resourceType: ResourceType, url: string, + resourceType: ResourceType, + entity: Entity, args: LoadingArgs, onLoad = (response: any) => {}, onProgress = (request: ProgressEvent) => {}, diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index 9e00642723..a250aa5a84 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -31,8 +31,8 @@ import { NO_PROXY, createState, getMutableState, getState, none, useHookstate } import { VRM } from '@pixiv/three-vrm' import React from 'react' import { AssetType } from '../../assets/enum/AssetType' +import { useGLTF } from '../../assets/functions/resourceHooks' import { GLTF } from '../../assets/loaders/gltf/GLTFLoader' -import { ResourceManager, ResourceType } from '../../assets/state/ResourceState' import { AnimationComponent } from '../../avatar/components/AnimationComponent' import { SkinnedMeshComponent } from '../../avatar/components/SkinnedMeshComponent' import { autoconvertMixamoAvatar, isAvaturn } from '../../avatar/functions/avatarFunctions' @@ -146,6 +146,61 @@ function ModelReactor(): JSX.Element { const modelComponent = useComponent(entity, ModelComponent) const variantComponent = useOptionalComponent(entity, VariantComponent) + /** @todo this is a hack */ + const override = !isAvaturn(modelComponent.value.src) ? undefined : AssetType.glB + const [gltf, progress, error] = useGLTF( + modelComponent.value.src, + { + forceAssetType: override, + ignoreDisposeGeometry: modelComponent.cameraOcclusion.value + }, + entity + ) + + useEffect(() => { + const onprogress = progress.value + if (!onprogress) return + if (hasComponent(entity, SceneAssetPendingTagComponent)) + SceneAssetPendingTagComponent.loadingProgress.merge({ + [entity]: { + loadedAmount: onprogress.loaded, + totalAmount: onprogress.total + } + }) + }, [progress]) + + useEffect(() => { + const loadedAsset = gltf.get(NO_PROXY) + if (!loadedAsset) return + + if (variantComponent && !variantComponent.calculated.value) return + if (typeof loadedAsset !== 'object') { + addError(entity, ModelComponent, 'INVALID_SOURCE', 'Invalid URL') + return + } + + const boneMatchedAsset = modelComponent.convertToVRM.value + ? (autoconvertMixamoAvatar(loadedAsset) as GLTF) + : loadedAsset + /**if we've loaded or converted to vrm, create animation component whose mixer's root is the normalized rig */ + if (boneMatchedAsset instanceof VRM) + setComponent(entity, AnimationComponent, { + animations: loadedAsset.animations, + mixer: new AnimationMixer(boneMatchedAsset.humanoid.normalizedHumanBones.hips.node) + }) + + modelComponent.asset.set(boneMatchedAsset) + }, [gltf]) + + useEffect(() => { + const err = error.value + if (!err) return + + console.error(err) + addError(entity, ModelComponent, 'INVALID_SOURCE', err.message) + removeComponent(entity, SceneAssetPendingTagComponent) + }, [error]) + useEffect(() => { let aborted = false if (variantComponent && !variantComponent.calculated.value) return @@ -163,52 +218,6 @@ function ModelReactor(): JSX.Element { proxifyParentChildRelationships(obj3d) } - /** @todo this is a hack */ - const override = !isAvaturn(model.src) ? undefined : AssetType.glB - - ResourceManager.load( - entity, - ResourceType.GLTF, - modelComponent.src.value, - { - forceAssetType: override, - ignoreDisposeGeometry: modelComponent.cameraOcclusion.value - }, - (loadedAsset) => { - if (variantComponent && !variantComponent.calculated.value) return - if (aborted) return - if (typeof loadedAsset !== 'object') { - addError(entity, ModelComponent, 'INVALID_SOURCE', 'Invalid URL') - return - } - const boneMatchedAsset = modelComponent.convertToVRM.value - ? (autoconvertMixamoAvatar(loadedAsset) as GLTF) - : loadedAsset - /**if we've loaded or converted to vrm, create animation component whose mixer's root is the normalized rig */ - if (boneMatchedAsset instanceof VRM) - setComponent(entity, AnimationComponent, { - animations: loadedAsset.animations, - mixer: new AnimationMixer(boneMatchedAsset.humanoid.normalizedHumanBones.hips.node) - }) - modelComponent.asset.set(boneMatchedAsset) - }, - (onprogress) => { - if (aborted) return - if (hasComponent(entity, SceneAssetPendingTagComponent)) - SceneAssetPendingTagComponent.loadingProgress.merge({ - [entity]: { - loadedAmount: onprogress.loaded, - totalAmount: onprogress.total - } - }) - }, - (err: Error) => { - if (aborted) return - console.error(err) - addError(entity, ModelComponent, 'INVALID_SOURCE', err.message) - removeComponent(entity, SceneAssetPendingTagComponent) - } - ) return () => { aborted = true } From 3c8243c20af6b859222b95e2cbb3c7024279f5f4 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 17 Jan 2024 11:40:57 -0800 Subject: [PATCH 04/67] Resource hook refactoring --- .../src/assets/functions/resourceHooks.ts | 37 ++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 8a1b415513..0861a4f0f0 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -25,28 +25,31 @@ Ethereal Engine. All Rights Reserved. import { State, useHookstate } from '@etherealengine/hyperflux' import { useEffect } from 'react' +import { Texture } from 'three' import { Entity, UndefinedEntity } from '../../ecs/classes/Entity' import { LoadingArgs } from '../classes/AssetLoader' import { GLTF } from '../loaders/gltf/GLTFLoader' import { ResourceManager, ResourceType } from '../state/ResourceState' -export function useGLTF( - src: string, +function useLoader( + url: string, + resourceType: ResourceType, params: LoadingArgs = {}, entity?: Entity -): [State, State | null>, State] { - const gltf = useHookstate(null) +): [State, State | null>, State] { + const value = useHookstate(null) const progress = useHookstate | null>(null) const error = useHookstate(null) useEffect(() => { + if (!url) return ResourceManager.load( - src, - ResourceType.GLTF, + url, + resourceType, entity || UndefinedEntity, params, (response) => { - gltf.set(response) + value.set(response) }, (request) => { progress.set(request) @@ -55,7 +58,23 @@ export function useGLTF( error.set(err) } ) - }, [src]) + }, [url]) + + return [value, progress, error] +} + +export function useGLTF( + url: string, + params: LoadingArgs = {}, + entity?: Entity +): [State, State | null>, State] { + return useLoader(url, ResourceType.GLTF, params, entity) +} - return [gltf, progress, error] +export function useTexture( + url: string, + params: LoadingArgs = {}, + entity?: Entity +): [State, State | null>, State] { + return useLoader(url, ResourceType.Texture, params, entity) } From 4c47a5a9ba5803c16386a2efc9b2369fab322722 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 17 Jan 2024 11:57:36 -0800 Subject: [PATCH 05/67] useTexture for SkyboxComponent --- .../src/assets/functions/resourceHooks.ts | 10 ++-- .../src/scene/components/ModelComponent.tsx | 2 +- .../src/scene/components/SkyboxComponent.ts | 47 ++++++++----------- 3 files changed, 25 insertions(+), 34 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 0861a4f0f0..2ba0954e95 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -36,10 +36,10 @@ function useLoader( resourceType: ResourceType, params: LoadingArgs = {}, entity?: Entity -): [State, State | null>, State] { +): [State, State, State | null>] { const value = useHookstate(null) - const progress = useHookstate | null>(null) const error = useHookstate(null) + const progress = useHookstate | null>(null) useEffect(() => { if (!url) return @@ -60,14 +60,14 @@ function useLoader( ) }, [url]) - return [value, progress, error] + return [value, error, progress] } export function useGLTF( url: string, params: LoadingArgs = {}, entity?: Entity -): [State, State | null>, State] { +): [State, State, State | null>] { return useLoader(url, ResourceType.GLTF, params, entity) } @@ -75,6 +75,6 @@ export function useTexture( url: string, params: LoadingArgs = {}, entity?: Entity -): [State, State | null>, State] { +): [State, State, State | null>] { return useLoader(url, ResourceType.Texture, params, entity) } diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index a250aa5a84..fba7b3796a 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -148,7 +148,7 @@ function ModelReactor(): JSX.Element { /** @todo this is a hack */ const override = !isAvaturn(modelComponent.value.src) ? undefined : AssetType.glB - const [gltf, progress, error] = useGLTF( + const [gltf, error, progress] = useGLTF( modelComponent.value.src, { forceAssetType: override, diff --git a/packages/engine/src/scene/components/SkyboxComponent.ts b/packages/engine/src/scene/components/SkyboxComponent.ts index b39577befc..b33f783773 100755 --- a/packages/engine/src/scene/components/SkyboxComponent.ts +++ b/packages/engine/src/scene/components/SkyboxComponent.ts @@ -24,19 +24,12 @@ Ethereal Engine. All Rights Reserved. */ import { useEffect } from 'react' -import { - Color, - CubeReflectionMapping, - CubeTexture, - EquirectangularReflectionMapping, - SRGBColorSpace, - Texture -} from 'three' +import { Color, CubeReflectionMapping, CubeTexture, EquirectangularReflectionMapping, SRGBColorSpace } from 'three' import { config } from '@etherealengine/common/src/config' -import { getMutableState, useHookstate } from '@etherealengine/hyperflux' +import { NO_PROXY, getMutableState, useHookstate } from '@etherealengine/hyperflux' -import { AssetLoader } from '../../assets/classes/AssetLoader' +import { useTexture } from '../../assets/functions/resourceHooks' import { isClient } from '../../common/functions/getEnvironment' import { SceneState } from '../../ecs/classes/Scene' import { defineComponent, useComponent } from '../../ecs/functions/ComponentFunctions' @@ -100,6 +93,22 @@ export const SkyboxComponent = defineComponent({ const skyboxState = useComponent(entity, SkyboxComponent) const background = useHookstate(getMutableState(SceneState).background) + const [texture, error] = useTexture(skyboxState.equirectangularPath.value) + + useEffect(() => { + if (skyboxState.backgroundType.value !== SkyTypeEnum.equirectangular) return + + const textureValue = texture.get(NO_PROXY) + if (textureValue) { + textureValue.colorSpace = SRGBColorSpace + textureValue.mapping = EquirectangularReflectionMapping + background.set(textureValue) + removeError(entity, SkyboxComponent, 'FILE_ERROR') + } else if (error.value) { + addError(entity, SkyboxComponent, 'FILE_ERROR', error.value.message) + } + }, [texture, error, skyboxState.backgroundType, skyboxState.equirectangularPath]) + useEffect(() => { if (skyboxState.backgroundType.value !== SkyTypeEnum.color) return background.set(skyboxState.backgroundColor.value) @@ -127,24 +136,6 @@ export const SkyboxComponent = defineComponent({ loadCubeMapTexture(...loadArgs) }, [skyboxState.backgroundType, skyboxState.cubemapPath]) - useEffect(() => { - if (skyboxState.backgroundType.value !== SkyTypeEnum.equirectangular) return - AssetLoader.load( - skyboxState.equirectangularPath.value, - {}, - (texture: Texture) => { - texture.colorSpace = SRGBColorSpace - texture.mapping = EquirectangularReflectionMapping - background.set(texture) - removeError(entity, SkyboxComponent, 'FILE_ERROR') - }, - undefined, - (error) => { - addError(entity, SkyboxComponent, 'FILE_ERROR', error.message) - } - ) - }, [skyboxState.backgroundType, skyboxState.equirectangularPath]) - useEffect(() => { if (skyboxState.backgroundType.value !== SkyTypeEnum.skybox) { if (skyboxState.sky.value) skyboxState.sky.set(null) From 2655b3f6ec078744e039823633536b82d9acd4b3 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 17 Jan 2024 15:01:34 -0800 Subject: [PATCH 06/67] Start of unloading logic --- .../src/assets/functions/resourceHooks.ts | 16 +++-- .../engine/src/assets/state/ResourceState.ts | 72 ++++++++++++++++++- .../src/scene/components/ModelComponent.tsx | 4 +- .../src/scene/components/SkyboxComponent.ts | 3 +- .../engine/src/scene/systems/ShadowSystem.tsx | 26 ++++--- 5 files changed, 99 insertions(+), 22 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 2ba0954e95..3449ee5f05 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -35,8 +35,8 @@ function useLoader( url: string, resourceType: ResourceType, params: LoadingArgs = {}, - entity?: Entity -): [State, State, State | null>] { + entity: Entity = UndefinedEntity +): [State, () => void, State, State | null>] { const value = useHookstate(null) const error = useHookstate(null) const progress = useHookstate | null>(null) @@ -46,7 +46,7 @@ function useLoader( ResourceManager.load( url, resourceType, - entity || UndefinedEntity, + entity, params, (response) => { value.set(response) @@ -60,14 +60,18 @@ function useLoader( ) }, [url]) - return [value, error, progress] + const unload = () => { + ResourceManager.unload(url, resourceType, entity) + } + + return [value, unload, error, progress] } export function useGLTF( url: string, params: LoadingArgs = {}, entity?: Entity -): [State, State, State | null>] { +): [State, () => void, State, State | null>] { return useLoader(url, ResourceType.GLTF, params, entity) } @@ -75,6 +79,6 @@ export function useTexture( url: string, params: LoadingArgs = {}, entity?: Entity -): [State, State, State | null>] { +): [State, () => void, State, State | null>] { return useLoader(url, ResourceType.Texture, params, entity) } diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index ba94598345..46c3d9c90e 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -23,9 +23,11 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { defineState, getMutableState, getState } from '@etherealengine/hyperflux' +import { NO_PROXY, defineState, getMutableState, getState, none } from '@etherealengine/hyperflux' +import { Texture } from 'three' import { Entity } from '../../ecs/classes/Entity' import { AssetLoader, LoadingArgs } from '../classes/AssetLoader' +import { GLTF } from '../loaders/gltf/GLTFLoader' //@ts-ignore THREE.Cache.enabled @@ -73,7 +75,7 @@ type Resource = { status: ResourceStatus type: ResourceType references: Entity[] - assetRef?: any + assetRef?: GLTF | Texture metadata: { size?: number } @@ -131,6 +133,12 @@ const load = ( (response) => { resource.status.set(ResourceStatus.Loaded) resource.assetRef.set(response) + if (response.isTexture) { + const height = response.image.naturalHeight + const width = response.image.naturalWidth + const size = width * height * 4 + resource.metadata.size.set(size) + } onLoad(response) }, (request) => { @@ -145,6 +153,64 @@ const load = ( ) } +const unload = (url: string, resourceType: ResourceType, entity: Entity) => { + const resourceState = getMutableState(ResourceState) + const resources = resourceState.nested('resources') + if (!resources[url].value) { + console.error('ResourceManager:unload No resource exists for url: ' + url) + return + } + + const resource = resources[url] + + resource.references.set((entities) => { + const index = entities.indexOf(entity) + if (index > -1) { + entities.splice(index, 1) + } + return entities + }) + + if (resource.references.length == 0) { + removeResource(url) + } +} + +const removeResource = (url: string) => { + const resourceState = getMutableState(ResourceState) + const resources = resourceState.nested('resources') + if (!resources[url].value) { + console.error('ResourceManager:removeResource No resource exists for url: ' + url) + return + } + + const resource = resources[url] + + if (resource.assetRef.value) { + switch (resource.type.value) { + case ResourceType.GLTF: + break + case ResourceType.Texture: + ;(resource.assetRef.get(NO_PROXY) as Texture).dispose() + break + case ResourceType.Geometry: + break + case ResourceType.ECSData: + break + case ResourceType.Audio: + break + case ResourceType.Unknown: + break + + default: + break + } + } + + resources[url].set(none) +} + export const ResourceManager = { - load + load, + unload } diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index fba7b3796a..e6303f63e8 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -148,7 +148,7 @@ function ModelReactor(): JSX.Element { /** @todo this is a hack */ const override = !isAvaturn(modelComponent.value.src) ? undefined : AssetType.glB - const [gltf, error, progress] = useGLTF( + const [gltf, unload, error, progress] = useGLTF( modelComponent.value.src, { forceAssetType: override, @@ -190,6 +190,8 @@ function ModelReactor(): JSX.Element { }) modelComponent.asset.set(boneMatchedAsset) + + return unload }, [gltf]) useEffect(() => { diff --git a/packages/engine/src/scene/components/SkyboxComponent.ts b/packages/engine/src/scene/components/SkyboxComponent.ts index b33f783773..8fde6d61a9 100755 --- a/packages/engine/src/scene/components/SkyboxComponent.ts +++ b/packages/engine/src/scene/components/SkyboxComponent.ts @@ -93,7 +93,7 @@ export const SkyboxComponent = defineComponent({ const skyboxState = useComponent(entity, SkyboxComponent) const background = useHookstate(getMutableState(SceneState).background) - const [texture, error] = useTexture(skyboxState.equirectangularPath.value) + const [texture, unload, error] = useTexture(skyboxState.equirectangularPath.value) useEffect(() => { if (skyboxState.backgroundType.value !== SkyTypeEnum.equirectangular) return @@ -104,6 +104,7 @@ export const SkyboxComponent = defineComponent({ textureValue.mapping = EquirectangularReflectionMapping background.set(textureValue) removeError(entity, SkyboxComponent, 'FILE_ERROR') + return unload } else if (error.value) { addError(entity, SkyboxComponent, 'FILE_ERROR', error.value.message) } diff --git a/packages/engine/src/scene/systems/ShadowSystem.tsx b/packages/engine/src/scene/systems/ShadowSystem.tsx index cb1fd02977..dbb957b81d 100644 --- a/packages/engine/src/scene/systems/ShadowSystem.tsx +++ b/packages/engine/src/scene/systems/ShadowSystem.tsx @@ -34,16 +34,15 @@ import { Quaternion, Raycaster, Sphere, - Texture, Vector3 } from 'three' import config from '@etherealengine/common/src/config' -import { defineState, getMutableState, getState, hookstate, useHookstate } from '@etherealengine/hyperflux' +import { NO_PROXY, defineState, getMutableState, getState, hookstate, useHookstate } from '@etherealengine/hyperflux' -import { AssetLoader } from '../../assets/classes/AssetLoader' import { CSM } from '../../assets/csm/CSM' import { CSMHelper } from '../../assets/csm/CSMHelper' +import { useTexture } from '../../assets/functions/resourceHooks' import { V_001 } from '../../common/constants/MathConstants' import { isClient } from '../../common/functions/getEnvironment' import { createPriorityQueue, createSortAndApplyPriorityQueue } from '../../ecs/PriorityQueue' @@ -397,15 +396,20 @@ const reactor = () => { const useShadows = useShadowsEnabled() + const [shadowTexture, unload] = useTexture( + `${config.client.fileServer}/projects/default-project/assets/drop-shadow.png` + ) + useEffect(() => { - AssetLoader.loadAsync(`${config.client.fileServer}/projects/default-project/assets/drop-shadow.png`).then( - (texture: Texture) => { - shadowMaterial.map = texture - shadowMaterial.needsUpdate = true - shadowState.set(shadowMaterial) - } - ) - }, []) + const texture = shadowTexture.get(NO_PROXY) + if (!texture) return + + shadowMaterial.map = texture + shadowMaterial.needsUpdate = true + shadowState.set(shadowMaterial) + + return unload + }, [shadowTexture]) EngineRenderer.instance.renderer.shadowMap.enabled = EngineRenderer.instance.renderer.shadowMap.autoUpdate = useShadows From 2be8cdd1df6ba5c8a9a2407e314888b5a696f039 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 17 Jan 2024 15:37:58 -0800 Subject: [PATCH 07/67] Include entity in useTexture --- .../engine/src/assets/functions/resourceHooks.ts | 16 ++++++++-------- .../src/scene/components/ModelComponent.tsx | 12 ++++-------- .../src/scene/components/SkyboxComponent.ts | 2 +- 3 files changed, 13 insertions(+), 17 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 3449ee5f05..e59bd30156 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -34,8 +34,8 @@ import { ResourceManager, ResourceType } from '../state/ResourceState' function useLoader( url: string, resourceType: ResourceType, - params: LoadingArgs = {}, - entity: Entity = UndefinedEntity + entity: Entity = UndefinedEntity, + params: LoadingArgs = {} ): [State, () => void, State, State | null>] { const value = useHookstate(null) const error = useHookstate(null) @@ -69,16 +69,16 @@ function useLoader( export function useGLTF( url: string, - params: LoadingArgs = {}, - entity?: Entity + entity?: Entity, + params?: LoadingArgs ): [State, () => void, State, State | null>] { - return useLoader(url, ResourceType.GLTF, params, entity) + return useLoader(url, ResourceType.GLTF, entity, params) } export function useTexture( url: string, - params: LoadingArgs = {}, - entity?: Entity + entity?: Entity, + params?: LoadingArgs ): [State, () => void, State, State | null>] { - return useLoader(url, ResourceType.Texture, params, entity) + return useLoader(url, ResourceType.Texture, entity, params) } diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index e6303f63e8..1fe50a9bf9 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -148,14 +148,10 @@ function ModelReactor(): JSX.Element { /** @todo this is a hack */ const override = !isAvaturn(modelComponent.value.src) ? undefined : AssetType.glB - const [gltf, unload, error, progress] = useGLTF( - modelComponent.value.src, - { - forceAssetType: override, - ignoreDisposeGeometry: modelComponent.cameraOcclusion.value - }, - entity - ) + const [gltf, unload, error, progress] = useGLTF(modelComponent.value.src, entity, { + forceAssetType: override, + ignoreDisposeGeometry: modelComponent.cameraOcclusion.value + }) useEffect(() => { const onprogress = progress.value diff --git a/packages/engine/src/scene/components/SkyboxComponent.ts b/packages/engine/src/scene/components/SkyboxComponent.ts index 8fde6d61a9..bd8b5cecfc 100755 --- a/packages/engine/src/scene/components/SkyboxComponent.ts +++ b/packages/engine/src/scene/components/SkyboxComponent.ts @@ -93,7 +93,7 @@ export const SkyboxComponent = defineComponent({ const skyboxState = useComponent(entity, SkyboxComponent) const background = useHookstate(getMutableState(SceneState).background) - const [texture, unload, error] = useTexture(skyboxState.equirectangularPath.value) + const [texture, unload, error] = useTexture(skyboxState.equirectangularPath.value, entity) useEffect(() => { if (skyboxState.backgroundType.value !== SkyTypeEnum.equirectangular) return From 2d9f5ed4d4b565bdc3108ce54cac01531d5bf1f6 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 17 Jan 2024 17:36:07 -0800 Subject: [PATCH 08/67] Seperate logic by resource type --- .../src/assets/functions/resourceHooks.ts | 2 +- .../engine/src/assets/state/ResourceState.ts | 50 +++++++++++++------ 2 files changed, 36 insertions(+), 16 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index e59bd30156..f1cd3e2441 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -49,7 +49,7 @@ function useLoader( entity, params, (response) => { - value.set(response) + value.set(response as T) }, (request) => { progress.set(request) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 46c3d9c90e..5c956a06b3 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.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 { NO_PROXY, defineState, getMutableState, getState, none } from '@etherealengine/hyperflux' +import { NO_PROXY, State, defineState, getMutableState, getState, none } from '@etherealengine/hyperflux' import { Texture } from 'three' import { Entity } from '../../ecs/classes/Entity' import { AssetLoader, LoadingArgs } from '../classes/AssetLoader' @@ -71,11 +71,13 @@ export enum ResourceType { Unknown } +export type AssetType = GLTF | Texture + type Resource = { status: ResourceStatus type: ResourceType references: Entity[] - assetRef?: GLTF | Texture + assetRef?: AssetType metadata: { size?: number } @@ -100,14 +102,34 @@ const getCurrentSizeOfResources = () => { return size } +const Callbacks = { + [ResourceType.GLTF]: { + onLoad: (response: GLTF, resource: State) => {}, + onProgress: (request: ProgressEvent, resource: State) => { + resource.metadata.size.set(request.total) + }, + onError: (event: ErrorEvent | Error, resource: State) => {} + }, + [ResourceType.Texture]: { + onLoad: (response: Texture, resource: State) => { + const height = response.image.naturalHeight + const width = response.image.naturalWidth + const size = width * height * 4 + resource.metadata.size.set(size) + }, + onProgress: (request: ProgressEvent, resource: State) => {}, + onError: (event: ErrorEvent | Error, resource: State) => {} + } +} + const load = ( url: string, resourceType: ResourceType, entity: Entity, args: LoadingArgs, - onLoad = (response: any) => {}, - onProgress = (request: ProgressEvent) => {}, - onError = (event: ErrorEvent | Error) => {} + onLoad: (response: AssetType) => void, + onProgress: (request: ProgressEvent) => void, + onError: (event: ErrorEvent | Error) => void ) => { const resourceState = getMutableState(ResourceState) const resources = resourceState.nested('resources') @@ -126,28 +148,24 @@ const load = ( } const resource = resources[url] - + const callback = Callbacks[resourceType] AssetLoader.load( url, args, (response) => { resource.status.set(ResourceStatus.Loaded) resource.assetRef.set(response) - if (response.isTexture) { - const height = response.image.naturalHeight - const width = response.image.naturalWidth - const size = width * height * 4 - resource.metadata.size.set(size) - } + callback?.onLoad(response, resource) onLoad(response) }, (request) => { resource.status.set(ResourceStatus.Loading) - resource.metadata.size.set(request.total) + callback?.onProgress(request, resource) onProgress(request) }, (error) => { resource.status.set(ResourceStatus.Error) + callback?.onError(error, resource) onError(error) } ) @@ -186,12 +204,14 @@ const removeResource = (url: string) => { const resource = resources[url] - if (resource.assetRef.value) { + let asset = resource.assetRef.get(NO_PROXY) + if (asset) { switch (resource.type.value) { case ResourceType.GLTF: + asset break case ResourceType.Texture: - ;(resource.assetRef.get(NO_PROXY) as Texture).dispose() + ;(asset as Texture).dispose() break case ResourceType.Geometry: break From 3cd761b224cd84e1d33e3824ac5cf2a8225f352e Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 17 Jan 2024 17:56:12 -0800 Subject: [PATCH 09/67] Update Three cache --- .../engine/src/assets/state/ResourceState.ts | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 5c956a06b3..fc084ed759 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -24,34 +24,29 @@ Ethereal Engine. All Rights Reserved. */ import { NO_PROXY, State, defineState, getMutableState, getState, none } from '@etherealengine/hyperflux' -import { Texture } from 'three' +import { Cache, DefaultLoadingManager, Texture } from 'three' import { Entity } from '../../ecs/classes/Entity' import { AssetLoader, LoadingArgs } from '../classes/AssetLoader' import { GLTF } from '../loaders/gltf/GLTFLoader' -//@ts-ignore -THREE.Cache.enabled +Cache.enabled = true -//@ts-ignore -THREE.DefaultLoadingManager.onLoad = () => { +DefaultLoadingManager.onLoad = () => { const totalSize = getCurrentSizeOfResources() console.log('Loaded: ' + totalSize + ' bytes of resources') } //Called when the item at the url passed in has completed loading -//@ts-ignore -THREE.DefaultLoadingManager.onProgress = (url: string, loaded: number, total: number) => { +DefaultLoadingManager.onProgress = (url: string, loaded: number, total: number) => { console.log('On Progress', url, loaded, total) } -//@ts-ignore -THREE.DefaultLoadingManager.onError = (url: string) => { +DefaultLoadingManager.onError = (url: string) => { console.log('On Error', url) } //This doesn't work as you might imagine, it is only called once, the url parameter is pretty much useless -//@ts-ignore -THREE.DefaultLoadingManager.onStart = (url: string, loaded: number, total: number) => { +DefaultLoadingManager.onStart = (url: string, loaded: number, total: number) => { console.log('On Start', url, loaded, total) } @@ -202,9 +197,10 @@ const removeResource = (url: string) => { return } + Cache.remove(url) const resource = resources[url] - let asset = resource.assetRef.get(NO_PROXY) + const asset = resource.assetRef.get(NO_PROXY) if (asset) { switch (resource.type.value) { case ResourceType.GLTF: From 284203cd6d92a149163f816c9692850c0d71b426 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Thu, 18 Jan 2024 10:55:38 -0800 Subject: [PATCH 10/67] Avatar loading system to resource manager --- .../engine/src/assets/state/ResourceState.ts | 25 ++++++----- .../avatar/systems/AvatarLoadingSystem.tsx | 42 ++++++++++++------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index fc084ed759..e5ab7f60e2 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -36,19 +36,19 @@ DefaultLoadingManager.onLoad = () => { console.log('Loaded: ' + totalSize + ' bytes of resources') } -//Called when the item at the url passed in has completed loading -DefaultLoadingManager.onProgress = (url: string, loaded: number, total: number) => { - console.log('On Progress', url, loaded, total) -} +// //Called when the item at the url passed in has completed loading +// DefaultLoadingManager.onProgress = (url: string, loaded: number, total: number) => { +// console.log('On Progress', url, loaded, total) +// } -DefaultLoadingManager.onError = (url: string) => { - console.log('On Error', url) -} +// DefaultLoadingManager.onError = (url: string) => { +// console.log('On Error', url) +// } -//This doesn't work as you might imagine, it is only called once, the url parameter is pretty much useless -DefaultLoadingManager.onStart = (url: string, loaded: number, total: number) => { - console.log('On Start', url, loaded, total) -} +// //This doesn't work as you might imagine, it is only called once, the url parameter is pretty much useless +// DefaultLoadingManager.onStart = (url: string, loaded: number, total: number) => { +// console.log('On Start', url, loaded, total) +// } enum ResourceStatus { Unloaded, @@ -144,6 +144,7 @@ const load = ( const resource = resources[url] const callback = Callbacks[resourceType] + console.log('Resource Manager Loading Asset at: ' + url) AssetLoader.load( url, args, @@ -197,6 +198,8 @@ const removeResource = (url: string) => { return } + console.log('Resource Manager Unloading Asset at: ' + url) + Cache.remove(url) const resource = resources[url] diff --git a/packages/engine/src/avatar/systems/AvatarLoadingSystem.tsx b/packages/engine/src/avatar/systems/AvatarLoadingSystem.tsx index 8172c4b525..c488ee1f05 100644 --- a/packages/engine/src/avatar/systems/AvatarLoadingSystem.tsx +++ b/packages/engine/src/avatar/systems/AvatarLoadingSystem.tsx @@ -28,7 +28,7 @@ import { SRGBColorSpace } from 'three' import { getMutableState, getState, useHookstate } from '@etherealengine/hyperflux' import React from 'react' -import { AssetLoader } from '../../assets/classes/AssetLoader' +import { useTexture } from '../../assets/functions/resourceHooks' import { isClient } from '../../common/functions/getEnvironment' import { EngineState } from '../../ecs/classes/EngineState' import { defineQuery, getComponent, setComponent } from '../../ecs/functions/ComponentFunctions' @@ -108,24 +108,38 @@ const reactor = () => { const assetsReady = useHookstate(false) + const [itemLight, lightUnload] = useTexture('/static/itemLight.png') + const [itemPlate, plateUnload] = useTexture('/static/itemPlate.png') + + useEffect(() => { + const texture = itemLight.value + if (!texture) return + + texture.colorSpace = SRGBColorSpace + texture.needsUpdate = true + SpawnEffectComponent.lightMesh.material.map = texture + return lightUnload + }, [itemLight]) + + useEffect(() => { + const texture = itemPlate.value + if (!texture) return + + texture.colorSpace = SRGBColorSpace + texture.needsUpdate = true + SpawnEffectComponent.plateMesh.material.map = texture + return plateUnload + }, [itemPlate]) + + useEffect(() => { + if (itemLight.value && itemPlate.value) assetsReady.set(true) + }, [itemLight, itemPlate]) + useEffect(() => { SpawnEffectComponent.lightMesh.geometry.computeBoundingSphere() SpawnEffectComponent.plateMesh.geometry.computeBoundingSphere() SpawnEffectComponent.lightMesh.name = 'light_obj' SpawnEffectComponent.plateMesh.name = 'plate_obj' - - AssetLoader.loadAsync('/static/itemLight.png').then((texture) => { - texture.colorSpace = SRGBColorSpace - texture.needsUpdate = true - SpawnEffectComponent.lightMesh.material.map = texture - }) - - AssetLoader.loadAsync('/static/itemPlate.png').then((texture) => { - texture.colorSpace = SRGBColorSpace - texture.needsUpdate = true - SpawnEffectComponent.plateMesh.material.map = texture - assetsReady.set(true) - }) }, []) const loadingEffect = useHookstate(getMutableState(EngineState).avatarLoadingEffect) From 9cf9fd37bffa04f4af4bb14e45eead09dd6a5dc0 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Thu, 18 Jan 2024 13:13:59 -0800 Subject: [PATCH 11/67] Particle system to resource manager --- .../components/ParticleSystemComponent.ts | 119 +++++++++--------- 1 file changed, 62 insertions(+), 57 deletions(-) diff --git a/packages/engine/src/scene/components/ParticleSystemComponent.ts b/packages/engine/src/scene/components/ParticleSystemComponent.ts index b7060c4524..b2893f649c 100644 --- a/packages/engine/src/scene/components/ParticleSystemComponent.ts +++ b/packages/engine/src/scene/components/ParticleSystemComponent.ts @@ -49,7 +49,7 @@ import { NO_PROXY, defineState, getMutableState, none, useHookstate } from '@eth import { AssetLoader } from '../../assets/classes/AssetLoader' import { AssetClass } from '../../assets/enum/AssetClass' -import { GLTF } from '../../assets/loaders/gltf/GLTFLoader' +import { useGLTF, useTexture } from '../../assets/functions/resourceHooks' import { defineComponent, getComponent, setComponent, useComponent } from '../../ecs/functions/ComponentFunctions' import { createEntity, useEntityContext } from '../../ecs/functions/EntityFunctions' import { TransformComponent } from '../../transform/components/TransformComponent' @@ -802,15 +802,52 @@ export const ParticleSystemComponent = defineComponent({ const component = componentState.value const batchRenderer = useHookstate(getMutableState(ParticleState).batchRenderer) + const [geoDependency, unloadGeo] = useGLTF(component.systemParameters.instancingGeometry!) + const [shapeMesh, unloadMesh] = useGLTF(component.systemParameters.shape.mesh!) + const [textureState, unloadTexture] = useTexture(component.systemParameters.texture!) + + const metadata = useHookstate({ textures: {}, geometries: {}, materials: {} } as ParticleSystemMetadata) + const dudMaterial = useHookstate( + new MeshBasicMaterial({ + color: 0xffffff, + transparent: component.systemParameters.transparent ?? true, + blending: component.systemParameters.blending as Blending + }) + ) + useEffect(() => { - if (component.system) { - const emitterAsObj3D = component.system.emitter as unknown as Object3D - if (emitterAsObj3D.userData['_refresh'] === component._refresh) return - removeObjectFromGroup(entity, emitterAsObj3D) - component.system.dispose() - componentState.system.set(none) - } + //add dud material + componentState.systemParameters.material.set('dud') + metadata.materials.nested('dud').set(dudMaterial.get(NO_PROXY)) + }, []) + + useEffect(() => { + if (!geoDependency.value || !geoDependency.value.scene) return + + const scene = geoDependency.value.scene + const geo = getFirstMesh(scene)?.geometry + !!geo && metadata.geometries.nested(component.systemParameters.instancingGeometry!).set(geo) + return unloadGeo + }, [geoDependency]) + useEffect(() => { + if (!shapeMesh.value || !shapeMesh.value.scene) return + + const scene = shapeMesh.value.scene + const mesh = getFirstMesh(scene) + mesh && metadata.geometries.nested(component.systemParameters.shape.mesh!).set(mesh.geometry) + return unloadMesh + }, [shapeMesh]) + + useEffect(() => { + const texture = textureState.get(NO_PROXY) + if (!texture) return + metadata.textures.nested(component.systemParameters.texture!).set(texture) + dudMaterial.map.set(texture) + return unloadTexture + }, [textureState]) + + useEffect(() => { function initParticleSystem(systemParameters: ParticleSystemJSONParameters, metadata: ParticleSystemMetadata) { const nuSystem = ParticleSystem.fromJSON(systemParameters, metadata, {}) batchRenderer.value.addSystem(nuSystem) @@ -842,59 +879,27 @@ export const ParticleSystemComponent = defineComponent({ component.systemParameters.texture && AssetLoader.getAssetClass(component.systemParameters.texture) === AssetClass.Image - const loadDependencies: Promise[] = [] - const metadata: ParticleSystemMetadata = { textures: {}, geometries: {}, materials: {} } + const loadedEmissionGeo = (doLoadEmissionGeo && shapeMesh.value) || !doLoadEmissionGeo + const loadedInstanceGeo = (doLoadInstancingGeo && geoDependency.value) || !doLoadInstancingGeo + const loadedTexture = (doLoadTexture && textureState.value) || !doLoadTexture - //add dud material - componentState.systemParameters.material.set('dud') - const dudMaterial = new MeshBasicMaterial({ - color: 0xffffff, - transparent: component.systemParameters.transparent ?? true, - blending: component.systemParameters.blending as Blending - }) - metadata.materials['dud'] = dudMaterial + if (loadedEmissionGeo && loadedInstanceGeo && loadedTexture) { + const processedParms = JSON.parse(JSON.stringify(component.systemParameters)) as ExpandedSystemJSON - const processedParms = JSON.parse(JSON.stringify(component.systemParameters)) as ExpandedSystemJSON - - function loadGeoDependency(src: string) { - return new Promise((resolve) => { - AssetLoader.load(src, {}, ({ scene }: GLTF) => { - const geo = getFirstMesh(scene)?.geometry - !!geo && (metadata.geometries[src] = geo) - resolve(null) - }) - }) + componentState._loadIndex.set(componentState._loadIndex.value + 1) + const currentIndex = componentState._loadIndex.value + currentIndex === componentState._loadIndex.value && initParticleSystem(processedParms, metadata.value) } + }, [geoDependency, shapeMesh, textureState]) - doLoadEmissionGeo && - loadDependencies.push( - new Promise((resolve) => { - AssetLoader.load(component.systemParameters.shape.mesh!, {}, ({ scene }: GLTF) => { - const mesh = getFirstMesh(scene) - mesh && (metadata.geometries[component.systemParameters.shape.mesh!] = mesh.geometry) - resolve(null) - }) - }) - ) - - doLoadInstancingGeo && loadDependencies.push(loadGeoDependency(component.systemParameters.instancingGeometry!)) - - doLoadTexture && - loadDependencies.push( - new Promise((resolve) => { - AssetLoader.load(component.systemParameters.texture!, {}, (texture: Texture) => { - metadata.textures[component.systemParameters.texture!] = texture - dudMaterial.map = texture - resolve(null) - }) - }) - ) - - componentState._loadIndex.set(componentState._loadIndex.value + 1) - const currentIndex = componentState._loadIndex.value - Promise.all(loadDependencies).then(() => { - currentIndex === componentState._loadIndex.value && initParticleSystem(processedParms, metadata) - }) + useEffect(() => { + if (component.system) { + const emitterAsObj3D = component.system.emitter as unknown as Object3D + if (emitterAsObj3D.userData['_refresh'] === component._refresh) return + removeObjectFromGroup(entity, emitterAsObj3D) + component.system.dispose() + componentState.system.set(none) + } }, [componentState._refresh]) return null } From 7bc8a6dbacc3ac8beb63102b8efcc8ca2a75249f Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Thu, 18 Jan 2024 14:50:45 -0800 Subject: [PATCH 12/67] Per type metadata --- .../engine/src/assets/state/ResourceState.ts | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index e5ab7f60e2..b0a62d49eb 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -68,14 +68,22 @@ export enum ResourceType { export type AssetType = GLTF | Texture +type BaseMetadata = { + size?: number +} + +type GLTFMetadata = BaseMetadata + +type TexutreMetadata = BaseMetadata + +type Metadata = GLTFMetadata | TexutreMetadata + type Resource = { status: ResourceStatus type: ResourceType references: Entity[] assetRef?: AssetType - metadata: { - size?: number - } + metadata: Metadata onGPU: boolean } @@ -107,10 +115,14 @@ const Callbacks = { }, [ResourceType.Texture]: { onLoad: (response: Texture, resource: State) => { - const height = response.image.naturalHeight - const width = response.image.naturalWidth - const size = width * height * 4 - resource.metadata.size.set(size) + if (response.mipmaps[0]) { + resource.metadata.size.set(response.mipmaps[0].data.length) + } else { + const height = response.image.height + const width = response.image.width + const size = width * height * 4 + resource.metadata.size.set(size) + } }, onProgress: (request: ProgressEvent, resource: State) => {}, onError: (event: ErrorEvent | Error, resource: State) => {} @@ -207,7 +219,6 @@ const removeResource = (url: string) => { if (asset) { switch (resource.type.value) { case ResourceType.GLTF: - asset break case ResourceType.Texture: ;(asset as Texture).dispose() From 531fb95549f1a8ae2b470edd1f9bec6e760d0bf0 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Thu, 18 Jan 2024 15:00:45 -0800 Subject: [PATCH 13/67] Include entity --- .../engine/src/scene/components/ParticleSystemComponent.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/engine/src/scene/components/ParticleSystemComponent.ts b/packages/engine/src/scene/components/ParticleSystemComponent.ts index b2893f649c..c772dc5101 100644 --- a/packages/engine/src/scene/components/ParticleSystemComponent.ts +++ b/packages/engine/src/scene/components/ParticleSystemComponent.ts @@ -802,9 +802,9 @@ export const ParticleSystemComponent = defineComponent({ const component = componentState.value const batchRenderer = useHookstate(getMutableState(ParticleState).batchRenderer) - const [geoDependency, unloadGeo] = useGLTF(component.systemParameters.instancingGeometry!) - const [shapeMesh, unloadMesh] = useGLTF(component.systemParameters.shape.mesh!) - const [textureState, unloadTexture] = useTexture(component.systemParameters.texture!) + const [geoDependency, unloadGeo] = useGLTF(component.systemParameters.instancingGeometry!, entity) + const [shapeMesh, unloadMesh] = useGLTF(component.systemParameters.shape.mesh!, entity) + const [textureState, unloadTexture] = useTexture(component.systemParameters.texture!, entity) const metadata = useHookstate({ textures: {}, geometries: {}, materials: {} } as ParticleSystemMetadata) const dudMaterial = useHookstate( From 038331d2cd46fb3876cf41ddb8efef6522982962 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Thu, 18 Jan 2024 18:26:55 -0800 Subject: [PATCH 14/67] Unload assets when changed in editor, onUnload callback --- .../src/assets/functions/resourceHooks.ts | 34 ++++++++++++++----- .../components/ParticleSystemComponent.ts | 32 ++++++++++------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index f1cd3e2441..f060752965 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -35,13 +35,31 @@ function useLoader( url: string, resourceType: ResourceType, entity: Entity = UndefinedEntity, - params: LoadingArgs = {} + params: LoadingArgs = {}, + onUnload: (url: string) => void = (url: string) => {} ): [State, () => void, State, State | null>] { + const urlState = useHookstate(url) const value = useHookstate(null) const error = useHookstate(null) const progress = useHookstate | null>(null) + const unload = () => { + ResourceManager.unload(url, resourceType, entity) + value.set(null) + progress.set(null) + error.set(null) + onUnload(url) + } + useEffect(() => { + if (url !== urlState.value) { + ResourceManager.unload(urlState.value, resourceType, entity) + value.set(null) + progress.set(null) + error.set(null) + onUnload(urlState.value) + urlState.set(url) + } if (!url) return ResourceManager.load( url, @@ -60,25 +78,23 @@ function useLoader( ) }, [url]) - const unload = () => { - ResourceManager.unload(url, resourceType, entity) - } - return [value, unload, error, progress] } export function useGLTF( url: string, entity?: Entity, - params?: LoadingArgs + params?: LoadingArgs, + onUnload?: (url: string) => void ): [State, () => void, State, State | null>] { - return useLoader(url, ResourceType.GLTF, entity, params) + return useLoader(url, ResourceType.GLTF, entity, params, onUnload) } export function useTexture( url: string, entity?: Entity, - params?: LoadingArgs + params?: LoadingArgs, + onUnload?: (url: string) => void ): [State, () => void, State, State | null>] { - return useLoader(url, ResourceType.Texture, entity, params) + return useLoader(url, ResourceType.Texture, entity, params, onUnload) } diff --git a/packages/engine/src/scene/components/ParticleSystemComponent.ts b/packages/engine/src/scene/components/ParticleSystemComponent.ts index c772dc5101..a0cd69f94f 100644 --- a/packages/engine/src/scene/components/ParticleSystemComponent.ts +++ b/packages/engine/src/scene/components/ParticleSystemComponent.ts @@ -802,9 +802,16 @@ export const ParticleSystemComponent = defineComponent({ const component = componentState.value const batchRenderer = useHookstate(getMutableState(ParticleState).batchRenderer) - const [geoDependency, unloadGeo] = useGLTF(component.systemParameters.instancingGeometry!, entity) - const [shapeMesh, unloadMesh] = useGLTF(component.systemParameters.shape.mesh!, entity) - const [textureState, unloadTexture] = useTexture(component.systemParameters.texture!, entity) + const [geoDependency, unloadGeo] = useGLTF(component.systemParameters.instancingGeometry!, entity, {}, (url) => { + metadata.geometries.nested(url).set(none) + }) + const [shapeMesh, unloadMesh] = useGLTF(component.systemParameters.shape.mesh!, entity, {}, (url) => { + metadata.geometries.nested(url).set(none) + }) + const [textureState, unloadTexture] = useTexture(component.systemParameters.texture!, entity, {}, (url) => { + metadata.textures.nested(url).set(none) + dudMaterial.map.set(none) + }) const metadata = useHookstate({ textures: {}, geometries: {}, materials: {} } as ParticleSystemMetadata) const dudMaterial = useHookstate( @@ -848,6 +855,14 @@ export const ParticleSystemComponent = defineComponent({ }, [textureState]) useEffect(() => { + if (component.system) { + const emitterAsObj3D = component.system.emitter as unknown as Object3D + if (emitterAsObj3D.userData['_refresh'] === component._refresh) return + removeObjectFromGroup(entity, emitterAsObj3D) + component.system.dispose() + componentState.system.set(none) + } + function initParticleSystem(systemParameters: ParticleSystemJSONParameters, metadata: ParticleSystemMetadata) { const nuSystem = ParticleSystem.fromJSON(systemParameters, metadata, {}) batchRenderer.value.addSystem(nuSystem) @@ -890,17 +905,8 @@ export const ParticleSystemComponent = defineComponent({ const currentIndex = componentState._loadIndex.value currentIndex === componentState._loadIndex.value && initParticleSystem(processedParms, metadata.value) } - }, [geoDependency, shapeMesh, textureState]) + }, [geoDependency, shapeMesh, textureState, componentState._refresh]) - useEffect(() => { - if (component.system) { - const emitterAsObj3D = component.system.emitter as unknown as Object3D - if (emitterAsObj3D.userData['_refresh'] === component._refresh) return - removeObjectFromGroup(entity, emitterAsObj3D) - component.system.dispose() - componentState.system.set(none) - } - }, [componentState._refresh]) return null } }) From d2820deca8c13e2b41a67ca7aef3dfd41240e28e Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Thu, 18 Jan 2024 18:49:33 -0800 Subject: [PATCH 15/67] Image component resource manager hooks --- .../src/scene/components/ImageComponent.ts | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/packages/engine/src/scene/components/ImageComponent.ts b/packages/engine/src/scene/components/ImageComponent.ts index 61473df6d5..bc6f2ffceb 100644 --- a/packages/engine/src/scene/components/ImageComponent.ts +++ b/packages/engine/src/scene/components/ImageComponent.ts @@ -41,12 +41,13 @@ import { } from 'three' import { EntityUUID } from '@etherealengine/common/src/interfaces/EntityUUID' -import { useHookstate } from '@etherealengine/hyperflux' +import { NO_PROXY, useHookstate } from '@etherealengine/hyperflux' import config from '@etherealengine/common/src/config' import { StaticResourceType } from '@etherealengine/common/src/schema.type.module' import { AssetLoader } from '../../assets/classes/AssetLoader' import { AssetClass } from '../../assets/enum/AssetClass' +import { useTexture } from '../../assets/functions/resourceHooks' import { defineComponent, removeComponent, setComponent, useComponent } from '../../ecs/functions/ComponentFunctions' import { useEntityContext } from '../../ecs/functions/EntityFunctions' import { EngineRenderer } from '../../renderer/WebGLRendererSystem' @@ -153,35 +154,35 @@ export function ImageReactor() { const image = useComponent(entity, ImageComponent) const texture = useHookstate(null as Texture | null) - useEffect( - function updateTextureSource() { - if (!image.source.value) { - return addError(entity, ImageComponent, `MISSING_TEXTURE_SOURCE`) - } + const [textureState, unload, error] = useTexture(image.source.value, entity) - const assetType = AssetLoader.getAssetClass(image.source.value) - if (assetType !== AssetClass.Image) { - return addError(entity, ImageComponent, `UNSUPPORTED_ASSET_CLASS`) - } + useEffect(() => { + const _texture = textureState.get(NO_PROXY) + if (_texture) { + texture.set(_texture) + removeComponent(entity, SceneAssetPendingTagComponent) + return unload + } + }, [textureState]) - setComponent(entity, SceneAssetPendingTagComponent) - AssetLoader.loadAsync(image.source.value) - .then((_texture) => { - texture.set(_texture) - }) - .catch((e) => { - addError(entity, ImageComponent, `LOADING_ERROR`, e.message) - }) - .finally(() => { - removeComponent(entity, SceneAssetPendingTagComponent) - }) + useEffect(() => { + if (!error.value) return + addError(entity, ImageComponent, `LOADING_ERROR`, error.value.message) + removeComponent(entity, SceneAssetPendingTagComponent) + }, [error]) - return () => { - // TODO: abort load request, pending https://github.com/mrdoob/three.js/pull/23070 - } - }, - [image.source] - ) + useEffect(() => { + if (!image.source.value) { + return addError(entity, ImageComponent, `MISSING_TEXTURE_SOURCE`) + } + + const assetType = AssetLoader.getAssetClass(image.source.value) + if (assetType !== AssetClass.Image) { + return addError(entity, ImageComponent, `UNSUPPORTED_ASSET_CLASS`) + } + + setComponent(entity, SceneAssetPendingTagComponent) + }, [image.source]) useEffect( function updateTexture() { From dba2b8d15ad658a79914a91d8f758420ad3087f4 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Thu, 18 Jan 2024 19:24:40 -0800 Subject: [PATCH 16/67] Don't use unloaded callback when the component is being unmounted --- .../engine/src/assets/functions/resourceHooks.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index f060752965..d08e375969 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -45,13 +45,10 @@ function useLoader( const unload = () => { ResourceManager.unload(url, resourceType, entity) - value.set(null) - progress.set(null) - error.set(null) - onUnload(url) } useEffect(() => { + let unmounted = false if (url !== urlState.value) { ResourceManager.unload(urlState.value, resourceType, entity) value.set(null) @@ -67,15 +64,19 @@ function useLoader( entity, params, (response) => { - value.set(response as T) + if (!unmounted) value.set(response as T) }, (request) => { - progress.set(request) + if (!unmounted) progress.set(request) }, (err) => { - error.set(err) + if (!unmounted) error.set(err) } ) + + return () => { + unmounted = true + } }, [url]) return [value, unload, error, progress] From ec4036cb7c6a941c53257ee03809f7b0fede1088 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Fri, 19 Jan 2024 12:03:35 -0800 Subject: [PATCH 17/67] Batch loader, loopAnimationComponent, AvatarAnimationSystem resource hooks --- .../src/assets/functions/resourceHooks.ts | 53 +++++++++++++++++++ .../components/LoopAnimationComponent.ts | 28 +++++----- .../src/avatar/functions/avatarFunctions.ts | 27 +--------- .../avatar/systems/AvatarAnimationSystem.ts | 49 ++++++++++++++--- 4 files changed, 110 insertions(+), 47 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index d08e375969..ec67d94500 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -36,6 +36,7 @@ function useLoader( resourceType: ResourceType, entity: Entity = UndefinedEntity, params: LoadingArgs = {}, + //Called when the asset url is changed, mostly useful for editor functions when changing an asset onUnload: (url: string) => void = (url: string) => {} ): [State, () => void, State, State | null>] { const urlState = useHookstate(url) @@ -82,6 +83,50 @@ function useLoader( return [value, unload, error, progress] } +function useBatchLoader( + urls: string[], + resourceType: ResourceType, + entity: Entity = UndefinedEntity, + params: LoadingArgs = {} +): [State, () => void, State<(ErrorEvent | Error)[]>, State[]>] { + const values = useHookstate([]) + const errors = useHookstate<(ErrorEvent | Error)[]>([]) + const progress = useHookstate[]>([]) + + const unload = () => { + for (const url of urls) ResourceManager.unload(url, resourceType, entity) + } + + useEffect(() => { + let unmounted = false + + for (let i = 0; i < urls.length; i++) { + const url = urls[i] + ResourceManager.load( + url, + resourceType, + entity, + params, + (response) => { + if (!unmounted) values[i].set(response as T) + }, + (request) => { + if (!unmounted) progress[i].set(request) + }, + (err) => { + if (!unmounted) errors[i].set(err) + } + ) + } + + return () => { + unmounted = true + } + }, [JSON.stringify(urls)]) + + return [values, unload, errors, progress] +} + export function useGLTF( url: string, entity?: Entity, @@ -91,6 +136,14 @@ export function useGLTF( return useLoader(url, ResourceType.GLTF, entity, params, onUnload) } +export function useBatchGLTF( + urls: string[], + entity?: Entity, + params?: LoadingArgs +): [State, () => void, State<(ErrorEvent | Error)[]>, State[]>] { + return useBatchLoader(urls, ResourceType.GLTF, entity, params) +} + export function useTexture( url: string, entity?: Entity, diff --git a/packages/engine/src/avatar/components/LoopAnimationComponent.ts b/packages/engine/src/avatar/components/LoopAnimationComponent.ts index 61737bf7e4..46609cd940 100644 --- a/packages/engine/src/avatar/components/LoopAnimationComponent.ts +++ b/packages/engine/src/avatar/components/LoopAnimationComponent.ts @@ -35,7 +35,7 @@ import { import { NO_PROXY, useHookstate } from '@etherealengine/hyperflux' import { VRM } from '@pixiv/three-vrm' -import { AssetLoader } from '../../assets/classes/AssetLoader' +import { useGLTF } from '../../assets/functions/resourceHooks' import { isClient } from '../../common/functions/getEnvironment' import { defineComponent, @@ -215,30 +215,28 @@ export const LoopAnimationComponent = defineComponent({ } }, [modelComponent?.asset]) + const [gltf, unload] = useGLTF(loopAnimationComponent.animationPack.value, entity) + useEffect(() => { const asset = modelComponent?.asset.get(NO_PROXY) ?? null + const model = gltf.value if ( - !asset?.scene || + !model || !animComponent || + !asset?.scene || !loopAnimationComponent.animationPack.value || lastAnimationPack.value === loopAnimationComponent.animationPack.value ) return - let aborted = false animComponent.mixer.time.set(0) - AssetLoader.loadAsync(loopAnimationComponent.animationPack.value).then((model) => { - if (aborted) return - const animations = model.animations ?? model.scene.animations - for (let i = 0; i < animations.length; i++) retargetAnimationClip(animations[i], model.scene) - lastAnimationPack.set(loopAnimationComponent.animationPack.get(NO_PROXY)) - animComponent.animations.set(animations) - }) - - return () => { - aborted = true - } - }, [animComponent, loopAnimationComponent.animationPack]) + const animations = model.animations ?? model.scene.animations + for (let i = 0; i < animations.length; i++) retargetAnimationClip(animations[i], model.scene) + lastAnimationPack.set(loopAnimationComponent.animationPack.get(NO_PROXY)) + animComponent.animations.set(animations) + + return unload + }, [gltf, animComponent, loopAnimationComponent.animationPack]) return null } diff --git a/packages/engine/src/avatar/functions/avatarFunctions.ts b/packages/engine/src/avatar/functions/avatarFunctions.ts index 61ff6682f4..fd3c466ac6 100644 --- a/packages/engine/src/avatar/functions/avatarFunctions.ts +++ b/packages/engine/src/avatar/functions/avatarFunctions.ts @@ -28,7 +28,6 @@ import { AnimationClip, AnimationMixer, Vector3 } from 'three' import { getMutableState, getState } from '@etherealengine/hyperflux' -import { AssetLoader } from '../../assets/classes/AssetLoader' import { Entity } from '../../ecs/classes/Entity' import { getComponent, @@ -62,7 +61,7 @@ import { AvatarControllerComponent } from '../components/AvatarControllerCompone import { AvatarDissolveComponent } from '../components/AvatarDissolveComponent' import { AvatarPendingComponent } from '../components/AvatarPendingComponent' import { AvatarMovementSettingsState } from '../state/AvatarMovementSettingsState' -import { bindAnimationClipFromMixamo, retargetAnimationClip } from './retargetMixamoRig' +import { bindAnimationClipFromMixamo } from './retargetMixamoRig' declare module '@pixiv/three-vrm/types/VRM' { export interface VRM { @@ -204,30 +203,6 @@ export const retargetAvatarAnimations = (entity: Entity) => { }) } -/**loads animation bundles. assumes the bundle is a glb */ -export const loadBundledAnimations = (animationFiles: string[]) => { - const manager = getMutableState(AnimationState) - - //preload animations - for (const animationFile of animationFiles) { - AssetLoader.loadAsync( - `${config.client.fileServer}/projects/default-project/assets/animations/${animationFile}.glb` - ).then((asset: GLTF) => { - // delete unneeded geometry data to save memory - asset.scene.traverse((node) => { - delete (node as any).geometry - delete (node as any).material - }) - for (let i = 0; i < asset.animations.length; i++) { - retargetAnimationClip(asset.animations[i], asset.scene) - } - //ensure animations are always placed in the scene - asset.scene.animations = asset.animations - manager.loadedAnimations[animationFile].set(asset) - }) - } -} - /** * @todo: stop using global state for avatar speed * in future this will be derrived from the actual root motion of a diff --git a/packages/engine/src/avatar/systems/AvatarAnimationSystem.ts b/packages/engine/src/avatar/systems/AvatarAnimationSystem.ts index 0be5da97e1..cfe035dee5 100644 --- a/packages/engine/src/avatar/systems/AvatarAnimationSystem.ts +++ b/packages/engine/src/avatar/systems/AvatarAnimationSystem.ts @@ -26,11 +26,20 @@ Ethereal Engine. All Rights Reserved. import { useEffect } from 'react' import { Euler, MathUtils, Matrix4, Quaternion, Vector3 } from 'three' -import { defineState, getMutableState, getState, none, useHookstate } from '@etherealengine/hyperflux' - +import { + NO_PROXY, + defineState, + getMutableState, + getState, + none, + useHookstate, + useMutableState +} from '@etherealengine/hyperflux' + +import config from '@etherealengine/common/src/config' import { EntityUUID } from '@etherealengine/common/src/interfaces/EntityUUID' +import { useBatchGLTF } from '../../assets/functions/resourceHooks' import { Q_X_90, Q_Y_180, V_001, V_010, V_100 } from '../../common/constants/MathConstants' -import { isClient } from '../../common/functions/getEnvironment' import { createPriorityQueue, createSortAndApplyPriorityQueue } from '../../ecs/PriorityQueue' import { Engine } from '../../ecs/classes/Engine' import { EngineState } from '../../ecs/classes/EngineState' @@ -48,6 +57,7 @@ import { XRControlsState, XRState } from '../../xr/XRState' import { AnimationComponent } from '.././components/AnimationComponent' import { AvatarAnimationComponent, AvatarRigComponent } from '.././components/AvatarAnimationComponent' import { AvatarHeadDecapComponent, AvatarIKTargetComponent } from '.././components/AvatarIKComponents' +import { AnimationState } from '../AnimationManager' import { IKSerialization } from '../IKSerialization' import { updateAnimationGraph } from '../animation/AvatarAnimationGraph' import { solveTwoBoneIK } from '../animation/TwoBoneIKSolver' @@ -55,7 +65,7 @@ import { ikTargets, preloadedAnimations } from '../animation/Util' import { getArmIKHint } from '../animation/getArmIKHint' import { AvatarComponent } from '../components/AvatarComponent' import { SkinnedMeshComponent } from '../components/SkinnedMeshComponent' -import { loadBundledAnimations } from '../functions/avatarFunctions' +import { retargetAnimationClip } from '../functions/retargetMixamoRig' import { updateVRMRetargeting } from '../functions/updateVRMRetargeting' import { AnimationSystem } from './AnimationSystem' @@ -337,11 +347,38 @@ const execute = () => { } const reactor = () => { + /**loads animation bundles. assumes the bundle is a glb */ + const animations = [preloadedAnimations.locomotion, preloadedAnimations.emotes] + const [gltfs, unload] = useBatchGLTF( + animations.map((animationFile) => { + return `${config.client.fileServer}/projects/default-project/assets/animations/${animationFile}.glb` + }) + ) + const manager = useMutableState(AnimationState) + useEffect(() => { - if (isClient) { - loadBundledAnimations([preloadedAnimations.locomotion, preloadedAnimations.emotes]) + const assets = gltfs.get(NO_PROXY) + if (assets.length !== animations.length) return + + for (let i = 0; i < assets.length; i++) { + const asset = assets[i] + // delete unneeded geometry data to save memory + asset.scene.traverse((node) => { + delete (node as any).geometry + delete (node as any).material + }) + for (let i = 0; i < asset.animations.length; i++) { + retargetAnimationClip(asset.animations[i], asset.scene) + } + //ensure animations are always placed in the scene + asset.scene.animations = asset.animations + manager.loadedAnimations[animations[i]].set(asset) } + return unload + }, [gltfs]) + + useEffect(() => { const networkState = getMutableState(NetworkState) networkState.networkSchema[IKSerialization.ID].set({ From df76a2cc6ef23eb273f95f6a09447d6a9fbc9f72 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Fri, 19 Jan 2024 15:11:19 -0800 Subject: [PATCH 18/67] Spawn point component to resource hooks, make sure unload only is called when unmounted --- .../src/assets/functions/resourceHooks.ts | 12 ++++--- .../components/ParticleSystemComponent.ts | 9 +++-- .../scene/components/SpawnPointComponent.ts | 34 +++++++++++-------- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index ec67d94500..d086f73640 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -51,11 +51,13 @@ function useLoader( useEffect(() => { let unmounted = false if (url !== urlState.value) { - ResourceManager.unload(urlState.value, resourceType, entity) - value.set(null) - progress.set(null) - error.set(null) - onUnload(urlState.value) + if (urlState.value) { + ResourceManager.unload(urlState.value, resourceType, entity) + value.set(null) + progress.set(null) + error.set(null) + onUnload(urlState.value) + } urlState.set(url) } if (!url) return diff --git a/packages/engine/src/scene/components/ParticleSystemComponent.ts b/packages/engine/src/scene/components/ParticleSystemComponent.ts index a0cd69f94f..e09ba30456 100644 --- a/packages/engine/src/scene/components/ParticleSystemComponent.ts +++ b/packages/engine/src/scene/components/ParticleSystemComponent.ts @@ -826,6 +826,12 @@ export const ParticleSystemComponent = defineComponent({ //add dud material componentState.systemParameters.material.set('dud') metadata.materials.nested('dud').set(dudMaterial.get(NO_PROXY)) + + return () => { + unloadGeo() + unloadMesh() + unloadTexture() + } }, []) useEffect(() => { @@ -834,7 +840,6 @@ export const ParticleSystemComponent = defineComponent({ const scene = geoDependency.value.scene const geo = getFirstMesh(scene)?.geometry !!geo && metadata.geometries.nested(component.systemParameters.instancingGeometry!).set(geo) - return unloadGeo }, [geoDependency]) useEffect(() => { @@ -843,7 +848,6 @@ export const ParticleSystemComponent = defineComponent({ const scene = shapeMesh.value.scene const mesh = getFirstMesh(scene) mesh && metadata.geometries.nested(component.systemParameters.shape.mesh!).set(mesh.geometry) - return unloadMesh }, [shapeMesh]) useEffect(() => { @@ -851,7 +855,6 @@ export const ParticleSystemComponent = defineComponent({ if (!texture) return metadata.textures.nested(component.systemParameters.texture!).set(texture) dudMaterial.map.set(texture) - return unloadTexture }, [textureState]) useEffect(() => { diff --git a/packages/engine/src/scene/components/SpawnPointComponent.ts b/packages/engine/src/scene/components/SpawnPointComponent.ts index 0ec54f41d3..1c2a3dae05 100755 --- a/packages/engine/src/scene/components/SpawnPointComponent.ts +++ b/packages/engine/src/scene/components/SpawnPointComponent.ts @@ -28,7 +28,7 @@ import { useEffect } from 'react' import { UserID } from '@etherealengine/common/src/schema.type.module' import { NO_PROXY, getMutableState, none, useHookstate } from '@etherealengine/hyperflux' -import { AssetLoader } from '../../assets/classes/AssetLoader' +import { useGLTF } from '../../assets/functions/resourceHooks' import { matches } from '../../common/functions/MatchesUtils' import { Entity } from '../../ecs/classes/Entity' import { defineComponent, setComponent, useComponent } from '../../ecs/functions/ComponentFunctions' @@ -37,7 +37,7 @@ import { EntityTreeComponent } from '../../ecs/functions/EntityTree' import { RendererState } from '../../renderer/RendererState' import { ObjectLayers } from '../constants/ObjectLayers' import { setObjectLayers } from '../functions/setObjectLayers' -import { addObjectToGroup } from './GroupComponent' +import { addObjectToGroup, removeObjectFromGroup } from './GroupComponent' import { NameComponent } from './NameComponent' import { setVisibleComponent } from './VisibleComponent' @@ -70,30 +70,34 @@ export const SpawnPointComponent = defineComponent({ const debugEnabled = useHookstate(getMutableState(RendererState).nodeHelperVisibility) const spawnPoint = useComponent(entity, SpawnPointComponent) + const [gltf, unload] = useGLTF(debugEnabled.value ? GLTF_PATH : '', entity) + + // Only call unload when unmounted + useEffect(() => { + return unload + }, []) + useEffect(() => { - if (!debugEnabled.value) return + const scene = gltf.get(NO_PROXY)?.scene + if (!scene || !debugEnabled.value) return const helperEntity = createEntity() setComponent(helperEntity, EntityTreeComponent, { parentEntity: entity }) - setVisibleComponent(helperEntity, true) - spawnPoint.helperEntity.set(helperEntity) - let active = true - AssetLoader.loadAsync(GLTF_PATH).then(({ scene: helper }) => { - if (!active) return - helper.name = `spawn-point-helper-${entity}` - addObjectToGroup(helperEntity, helper) - setObjectLayers(helper, ObjectLayers.NodeHelper) - setComponent(helperEntity, NameComponent, helper.name) - }) + scene.name = `spawn-point-helper-${entity}` + addObjectToGroup(helperEntity, scene) + setObjectLayers(scene, ObjectLayers.NodeHelper) + setComponent(helperEntity, NameComponent, scene.name) + + setVisibleComponent(spawnPoint.helperEntity.value!, true) return () => { - active = false + removeObjectFromGroup(helperEntity, scene) removeEntity(helperEntity) spawnPoint.helperEntity.set(none) } - }, [debugEnabled]) + }, [gltf, debugEnabled]) return null } From 3c38a6acaf03a110b1f5a7eb5f5d96445ee2c373 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Fri, 19 Jan 2024 17:51:00 -0800 Subject: [PATCH 19/67] Better typing --- .../src/assets/functions/resourceHooks.ts | 20 +++++++++---------- .../engine/src/assets/state/ResourceState.ts | 16 +++++++-------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index d086f73640..06db51e20a 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -29,9 +29,9 @@ import { Texture } from 'three' import { Entity, UndefinedEntity } from '../../ecs/classes/Entity' import { LoadingArgs } from '../classes/AssetLoader' import { GLTF } from '../loaders/gltf/GLTFLoader' -import { ResourceManager, ResourceType } from '../state/ResourceState' +import { AssetType, ResourceManager, ResourceType } from '../state/ResourceState' -function useLoader( +function useLoader( url: string, resourceType: ResourceType, entity: Entity = UndefinedEntity, @@ -45,14 +45,14 @@ function useLoader( const progress = useHookstate | null>(null) const unload = () => { - ResourceManager.unload(url, resourceType, entity) + ResourceManager.unload(url, entity) } useEffect(() => { let unmounted = false if (url !== urlState.value) { if (urlState.value) { - ResourceManager.unload(urlState.value, resourceType, entity) + ResourceManager.unload(urlState.value, entity) value.set(null) progress.set(null) error.set(null) @@ -61,13 +61,13 @@ function useLoader( urlState.set(url) } if (!url) return - ResourceManager.load( + ResourceManager.load( url, resourceType, entity, params, (response) => { - if (!unmounted) value.set(response as T) + if (!unmounted) value.set(response) }, (request) => { if (!unmounted) progress.set(request) @@ -85,7 +85,7 @@ function useLoader( return [value, unload, error, progress] } -function useBatchLoader( +function useBatchLoader( urls: string[], resourceType: ResourceType, entity: Entity = UndefinedEntity, @@ -96,7 +96,7 @@ function useBatchLoader( const progress = useHookstate[]>([]) const unload = () => { - for (const url of urls) ResourceManager.unload(url, resourceType, entity) + for (const url of urls) ResourceManager.unload(url, entity) } useEffect(() => { @@ -104,13 +104,13 @@ function useBatchLoader( for (let i = 0; i < urls.length; i++) { const url = urls[i] - ResourceManager.load( + ResourceManager.load( url, resourceType, entity, params, (response) => { - if (!unmounted) values[i].set(response as T) + if (!unmounted) values[i].set(response) }, (request) => { if (!unmounted) progress[i].set(request) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index b0a62d49eb..4f42d0602d 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -74,7 +74,9 @@ type BaseMetadata = { type GLTFMetadata = BaseMetadata -type TexutreMetadata = BaseMetadata +type TexutreMetadata = { + onGPU: boolean +} & BaseMetadata type Metadata = GLTFMetadata | TexutreMetadata @@ -84,7 +86,6 @@ type Resource = { references: Entity[] assetRef?: AssetType metadata: Metadata - onGPU: boolean } export const ResourceState = defineState({ @@ -129,12 +130,12 @@ const Callbacks = { } } -const load = ( +const load = ( url: string, resourceType: ResourceType, entity: Entity, args: LoadingArgs, - onLoad: (response: AssetType) => void, + onLoad: (response: T) => void, onProgress: (request: ProgressEvent) => void, onError: (event: ErrorEvent | Error) => void ) => { @@ -146,8 +147,7 @@ const load = ( status: ResourceStatus.Unloaded, type: resourceType, references: [entity], - metadata: {}, - onGPU: false + metadata: {} } }) } else { @@ -160,7 +160,7 @@ const load = ( AssetLoader.load( url, args, - (response) => { + (response: T) => { resource.status.set(ResourceStatus.Loaded) resource.assetRef.set(response) callback?.onLoad(response, resource) @@ -179,7 +179,7 @@ const load = ( ) } -const unload = (url: string, resourceType: ResourceType, entity: Entity) => { +const unload = (url: string, entity: Entity) => { const resourceState = getMutableState(ResourceState) const resources = resourceState.nested('resources') if (!resources[url].value) { From ab41c9223b014edffa7a4359983c8d48a1769895 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Fri, 19 Jan 2024 18:01:32 -0800 Subject: [PATCH 20/67] onStart callback for setting initial metadata per type --- packages/engine/src/assets/state/ResourceState.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 4f42d0602d..68913e0f19 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -108,6 +108,7 @@ const getCurrentSizeOfResources = () => { const Callbacks = { [ResourceType.GLTF]: { + onStart: (resource: State) => {}, onLoad: (response: GLTF, resource: State) => {}, onProgress: (request: ProgressEvent, resource: State) => { resource.metadata.size.set(request.total) @@ -115,6 +116,9 @@ const Callbacks = { onError: (event: ErrorEvent | Error, resource: State) => {} }, [ResourceType.Texture]: { + onStart: (resource: State) => { + resource.metadata.merge({ onGPU: false }) + }, onLoad: (response: Texture, resource: State) => { if (response.mipmaps[0]) { resource.metadata.size.set(response.mipmaps[0].data.length) @@ -155,25 +159,26 @@ const load = ( } const resource = resources[url] - const callback = Callbacks[resourceType] + const callbacks = Callbacks[resourceType] console.log('Resource Manager Loading Asset at: ' + url) + callbacks.onStart() AssetLoader.load( url, args, (response: T) => { resource.status.set(ResourceStatus.Loaded) resource.assetRef.set(response) - callback?.onLoad(response, resource) + callbacks.onLoad(response, resource) onLoad(response) }, (request) => { resource.status.set(ResourceStatus.Loading) - callback?.onProgress(request, resource) + callbacks.onProgress(request, resource) onProgress(request) }, (error) => { resource.status.set(ResourceStatus.Error) - callback?.onError(error, resource) + callbacks.onError(error, resource) onError(error) } ) From 4b9351cbf7c7efa5fc208f96797f228a8ba7ad42 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 22 Jan 2024 13:15:20 -0800 Subject: [PATCH 21/67] Update loaders to use abort controller signal --- .../engine/src/assets/classes/AssetLoader.ts | 5 +- .../src/assets/functions/resourceHooks.ts | 51 +++- .../src/assets/loaders/base/FileLoader.ts | 227 ++++++++++++++++++ .../engine/src/assets/loaders/base/Loader.ts | 68 ++++++ .../src/assets/loaders/corto/CORTOLoader.js | 3 +- .../src/assets/loaders/fbx/FBXLoader.js | 4 +- .../src/assets/loaders/gltf/DRACOLoader.js | 5 +- .../src/assets/loaders/gltf/GLTFLoader.d.ts | 5 +- .../src/assets/loaders/gltf/GLTFLoader.js | 9 +- .../src/assets/loaders/gltf/KTX2Loader.d.ts | 3 +- .../src/assets/loaders/gltf/KTX2Loader.js | 8 +- .../assets/loaders/gltf/NodeDracoLoader.js | 7 +- .../src/assets/loaders/tga/TGALoader.ts | 10 +- .../src/assets/loaders/usdz/USDZLoader.d.ts | 2 +- .../src/assets/loaders/usdz/USDZLoader.js | 8 +- .../engine/src/assets/state/ResourceState.ts | 19 +- 16 files changed, 389 insertions(+), 45 deletions(-) create mode 100644 packages/engine/src/assets/loaders/base/FileLoader.ts create mode 100644 packages/engine/src/assets/loaders/base/Loader.ts diff --git a/packages/engine/src/assets/classes/AssetLoader.ts b/packages/engine/src/assets/classes/AssetLoader.ts index 80a9835374..f916d3ca27 100644 --- a/packages/engine/src/assets/classes/AssetLoader.ts +++ b/packages/engine/src/assets/classes/AssetLoader.ts @@ -380,7 +380,8 @@ const load = async ( args: LoadingArgs, onLoad = (response: any) => {}, onProgress = (request: ProgressEvent) => {}, - onError = (event: ErrorEvent | Error) => {} + onError = (event: ErrorEvent | Error) => {}, + signal?: AbortSignal ) => { if (!_url) { onError(new Error('URL is empty')) @@ -431,7 +432,7 @@ const load = async ( const callback = assetLoadCallback(url, args, assetType, onLoad) try { - return loader.load(url, callback, onProgress, onError) + return loader.load(url, callback, onProgress, onError, signal) } catch (error) { onError(error) } diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 06db51e20a..883fe6629b 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -31,6 +31,16 @@ import { LoadingArgs } from '../classes/AssetLoader' import { GLTF } from '../loaders/gltf/GLTFLoader' import { AssetType, ResourceManager, ResourceType } from '../state/ResourceState' +function getAbortController(url: string, callback: () => void): AbortController { + const controller = new AbortController() + controller.signal.onabort = (event) => { + console.warn('resourceHook: Aborted resource fetch for url: ' + url, event) + callback() + } + + return controller +} + function useLoader( url: string, resourceType: ResourceType, @@ -49,7 +59,6 @@ function useLoader( } useEffect(() => { - let unmounted = false if (url !== urlState.value) { if (urlState.value) { ResourceManager.unload(urlState.value, entity) @@ -60,6 +69,10 @@ function useLoader( } urlState.set(url) } + + const controller = getAbortController(url, unload) + let completed = false + if (!url) return ResourceManager.load( url, @@ -67,18 +80,22 @@ function useLoader( entity, params, (response) => { - if (!unmounted) value.set(response) + completed = true + value.set(response) }, (request) => { - if (!unmounted) progress.set(request) + completed = true + progress.set(request) }, (err) => { - if (!unmounted) error.set(err) - } + completed = true + error.set(err) + }, + controller.signal ) return () => { - unmounted = true + if (!completed) controller.abort() } }, [url]) @@ -100,7 +117,8 @@ function useBatchLoader( } useEffect(() => { - let unmounted = false + const completedArr = new Array(urls.length).fill(false) as boolean[] + const controller = getAbortController(urls.toString(), unload) for (let i = 0; i < urls.length; i++) { const url = urls[i] @@ -110,19 +128,28 @@ function useBatchLoader( entity, params, (response) => { - if (!unmounted) values[i].set(response) + completedArr[i] = true + values[i].set(response) }, (request) => { - if (!unmounted) progress[i].set(request) + completedArr[i] = true + progress[i].set(request) }, (err) => { - if (!unmounted) errors[i].set(err) - } + completedArr[i] = true + errors[i].set(err) + }, + controller.signal ) } return () => { - unmounted = true + for (const completed of completedArr) { + if (!completed) { + controller.abort() + return + } + } } }, [JSON.stringify(urls)]) diff --git a/packages/engine/src/assets/loaders/base/FileLoader.ts b/packages/engine/src/assets/loaders/base/FileLoader.ts new file mode 100644 index 0000000000..c7aff8f142 --- /dev/null +++ b/packages/engine/src/assets/loaders/base/FileLoader.ts @@ -0,0 +1,227 @@ +import { Cache } from 'three' +import { Loader } from './Loader' + +const loading = {} + +class HttpError extends Error { + response: any + constructor(message, response) { + super(message) + this.response = response + } +} + +class FileLoader extends Loader { + mimeType: undefined | any + responseType: undefined | string + + constructor(manager) { + super(manager) + } + + load( + url: string, + onLoad: (data: TData) => void, + onProgress?: (event: ProgressEvent) => void, + onError?: (err: unknown) => void, + signal?: AbortSignal + ) { + if (url === undefined) url = '' + + if (this.path !== undefined) url = this.path + url + + url = this.manager.resolveURL(url) + + const cached = Cache.get(url) + + if (cached !== undefined) { + this.manager.itemStart(url) + + setTimeout(() => { + if (onLoad) onLoad(cached) + + this.manager.itemEnd(url) + }, 0) + + return cached + } + + // Check if request is duplicate + + if (loading[url] !== undefined) { + loading[url].push({ + onLoad: onLoad, + onProgress: onProgress, + onError: onError + }) + + return + } + + // Initialise array for duplicate requests + loading[url] = [] + + loading[url].push({ + onLoad: onLoad, + onProgress: onProgress, + onError: onError + }) + + // create request + const req = new Request(url, { + headers: new Headers(this.requestHeader), + credentials: this.withCredentials ? 'include' : 'same-origin' + // An abort controller could be added within a future PR + }) + + // record states ( avoid data race ) + const mimeType = this.mimeType + const responseType = this.responseType + + // start the fetch + fetch(req, { signal }) + .then((response) => { + if (response.status === 200 || response.status === 0) { + // Some browsers return HTTP Status 0 when using non-http protocol + // e.g. 'file://' or 'data://'. Handle as success. + + if (response.status === 0) { + console.warn('THREE.FileLoader: HTTP Status 0 received.') + } + + // Workaround: Checking if response.body === undefined for Alipay browser #23548 + + if ( + typeof ReadableStream === 'undefined' || + response.body == undefined || + response.body.getReader == undefined + ) { + return response + } + + const callbacks = loading[url] + const reader = response.body.getReader() + + // Nginx needs X-File-Size check + // https://serverfault.com/questions/482875/why-does-nginx-remove-content-length-header-for-chunked-content + const contentLength = response.headers.get('Content-Length') || response.headers.get('X-File-Size') + const total = contentLength ? parseInt(contentLength) : 0 + const lengthComputable = total !== 0 + let loaded = 0 + + // periodically read data into the new stream tracking while download progress + const stream = new ReadableStream({ + start(controller) { + readData() + + function readData() { + reader.read().then(({ done, value }) => { + if (done) { + controller.close() + } else { + loaded += value.byteLength + + const event = new ProgressEvent('progress', { lengthComputable, loaded, total }) + for (let i = 0, il = callbacks.length; i < il; i++) { + const callback = callbacks[i] + if (callback.onProgress) callback.onProgress(event) + } + + controller.enqueue(value) + readData() + } + }) + } + } + }) + + return new Response(stream) + } else { + throw new HttpError( + `fetch for "${response.url}" responded with ${response.status}: ${response.statusText}`, + response + ) + } + }) + .then((response) => { + switch (responseType) { + case 'arraybuffer': + return response.arrayBuffer() + + case 'blob': + return response.blob() + + case 'document': + return response.text().then((text) => { + const parser = new DOMParser() + return parser.parseFromString(text, mimeType) + }) + + case 'json': + return response.json() + + default: + if (mimeType === undefined) { + return response.text() + } else { + // sniff encoding + const re = /charset="?([^;"\s]*)"?/i + const exec = re.exec(mimeType) + const label = exec && exec[1] ? exec[1].toLowerCase() : undefined + const decoder = new TextDecoder(label) + return response.arrayBuffer().then((ab) => decoder.decode(ab)) + } + } + }) + .then((data) => { + // Add to cache only on HTTP success, so that we do not cache + // error response bodies as proper responses to requests. + Cache.add(url, data) + + const callbacks = loading[url] + delete loading[url] + + for (let i = 0, il = callbacks.length; i < il; i++) { + const callback = callbacks[i] + if (callback.onLoad) callback.onLoad(data) + } + }) + .catch((err) => { + // Abort errors and other errors are handled the same + + const callbacks = loading[url] + + if (callbacks === undefined) { + // When onLoad was called and url was deleted in `loading` + this.manager.itemError(url) + throw err + } + + delete loading[url] + + for (let i = 0, il = callbacks.length; i < il; i++) { + const callback = callbacks[i] + if (callback.onError) callback.onError(err) + } + + this.manager.itemError(url) + }) + .finally(() => { + this.manager.itemEnd(url) + }) + + this.manager.itemStart(url) + } + + setResponseType(value) { + this.responseType = value + return this + } + + setMimeType(value) { + this.mimeType = value + return this + } +} + +export { FileLoader } diff --git a/packages/engine/src/assets/loaders/base/Loader.ts b/packages/engine/src/assets/loaders/base/Loader.ts new file mode 100644 index 0000000000..8153874699 --- /dev/null +++ b/packages/engine/src/assets/loaders/base/Loader.ts @@ -0,0 +1,68 @@ +import { DefaultLoadingManager, LoadingManager } from 'three' + +class Loader { + static DEFAULT_MATERIAL_NAME = '__DEFAULT' + + manager: LoadingManager + crossOrigin: string + withCredentials: boolean + path: string + resourcePath: string + requestHeader: { [header: string]: string } + + constructor(manager?: LoadingManager) { + this.manager = manager !== undefined ? manager : DefaultLoadingManager + + this.crossOrigin = 'anonymous' + this.withCredentials = false + this.path = '' + this.resourcePath = '' + this.requestHeader = {} + } + + load( + url: TUrl, + onLoad: (data: TData) => void, + onProgress?: (event: ProgressEvent) => void, + onError?: (err: unknown) => void, + signal?: AbortSignal + ) {} + + loadAsync(url, onProgress) { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const scope = this + + return new Promise(function (resolve, reject) { + scope.load(url, resolve, onProgress, reject) + }) + } + + parse(data?: any) {} + + setCrossOrigin(crossOrigin) { + this.crossOrigin = crossOrigin + return this + } + + setWithCredentials(value) { + this.withCredentials = value + return this + } + + setPath(path) { + this.path = path + return this + } + + setResourcePath(resourcePath) { + this.resourcePath = resourcePath + return this + } + + setRequestHeader(requestHeader) { + this.requestHeader = requestHeader + return this + } +} + +export { Loader } diff --git a/packages/engine/src/assets/loaders/corto/CORTOLoader.js b/packages/engine/src/assets/loaders/corto/CORTOLoader.js index 7bcfdb5236..17f36abb89 100644 --- a/packages/engine/src/assets/loaders/corto/CORTOLoader.js +++ b/packages/engine/src/assets/loaders/corto/CORTOLoader.js @@ -25,7 +25,8 @@ Ethereal Engine. All Rights Reserved. */ -import { BufferAttribute, BufferGeometry, FileLoader } from 'three' +import { BufferAttribute, BufferGeometry } from 'three' +import { FileLoader } from "../base/FileLoader" class CORTOLoader { constructor() { diff --git a/packages/engine/src/assets/loaders/fbx/FBXLoader.js b/packages/engine/src/assets/loaders/fbx/FBXLoader.js index 6ce7b70b15..07bd00a4f1 100755 --- a/packages/engine/src/assets/loaders/fbx/FBXLoader.js +++ b/packages/engine/src/assets/loaders/fbx/FBXLoader.js @@ -99,7 +99,7 @@ class FBXLoader extends Loader { } - load( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError, signal ) { const scope = this; @@ -133,7 +133,7 @@ class FBXLoader extends Loader { } - }, onProgress, onError ); + }, onProgress, onError, signal ); } diff --git a/packages/engine/src/assets/loaders/gltf/DRACOLoader.js b/packages/engine/src/assets/loaders/gltf/DRACOLoader.js index c12fec4ec6..e142138c26 100755 --- a/packages/engine/src/assets/loaders/gltf/DRACOLoader.js +++ b/packages/engine/src/assets/loaders/gltf/DRACOLoader.js @@ -75,7 +75,7 @@ class DRACOLoader extends Loader { return this } - load(url, onLoad, onProgress, onError) { + load(url, onLoad, onProgress, onError, signal) { const loader = new FileLoader(this.manager) loader.setPath(this.path) @@ -95,7 +95,8 @@ class DRACOLoader extends Loader { this.decodeGeometry(buffer, taskConfig).then(onLoad).catch(onError) }, onProgress, - onError + onError, + signal ) } diff --git a/packages/engine/src/assets/loaders/gltf/GLTFLoader.d.ts b/packages/engine/src/assets/loaders/gltf/GLTFLoader.d.ts index d23ca4ba95..2a2a7f9372 100755 --- a/packages/engine/src/assets/loaders/gltf/GLTFLoader.d.ts +++ b/packages/engine/src/assets/loaders/gltf/GLTFLoader.d.ts @@ -48,6 +48,8 @@ import { Entity } from '../../../ecs/classes/Entity' import { DRACOLoader } from './DRACOLoader' import { KTX2Loader } from './KTX2Loader' +import { Loader } from '../base/Loader' + export interface GLTF { animations: AnimationClip[] scene: Scene @@ -74,7 +76,8 @@ export class GLTFLoader extends Loader { url: string, onLoad: (gltf: GLTF) => void, onProgress?: (event: ProgressEvent) => void, - onError?: (event: ErrorEvent) => void + onError?: (event: ErrorEvent) => void, + signal?: AbortSignal ): void loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise diff --git a/packages/engine/src/assets/loaders/gltf/GLTFLoader.js b/packages/engine/src/assets/loaders/gltf/GLTFLoader.js index 6c57276d26..209c427992 100755 --- a/packages/engine/src/assets/loaders/gltf/GLTFLoader.js +++ b/packages/engine/src/assets/loaders/gltf/GLTFLoader.js @@ -35,7 +35,6 @@ import { ColorManagement, DirectionalLight, DoubleSide, - FileLoader, FrontSide, Group, ImageBitmapLoader, @@ -53,7 +52,6 @@ import { LinearMipmapLinearFilter, LinearMipmapNearestFilter, LinearSRGBColorSpace, - Loader, LoaderUtils, Material, MathUtils, @@ -93,6 +91,9 @@ import { InstancedBufferAttribute } from 'three'; +import { FileLoader } from '../base/FileLoader'; +import { Loader } from '../base/Loader'; + /** * @param {BufferGeometry} geometry * @param {number} drawMode @@ -307,7 +308,7 @@ class GLTFLoader extends Loader { } - load( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError, signal ) { const scope = this; @@ -374,7 +375,7 @@ class GLTFLoader extends Loader { } - }, onProgress, _onError ); + }, onProgress, _onError, signal ); } diff --git a/packages/engine/src/assets/loaders/gltf/KTX2Loader.d.ts b/packages/engine/src/assets/loaders/gltf/KTX2Loader.d.ts index b2cf7d1c89..feb2f49e05 100644 --- a/packages/engine/src/assets/loaders/gltf/KTX2Loader.d.ts +++ b/packages/engine/src/assets/loaders/gltf/KTX2Loader.d.ts @@ -45,6 +45,7 @@ export class KTX2Loader extends CompressedTextureLoader { url: string, onLoad: (texture: CompressedTexture) => void, onProgress?: (requrest: ProgressEvent) => void | undefined, - onError?: ((event: ErrorEvent) => void) | undefined + onError?: ((event: ErrorEvent) => void) | undefined, + signal?: AbortSignal ): CompressedTexture } \ No newline at end of file diff --git a/packages/engine/src/assets/loaders/gltf/KTX2Loader.js b/packages/engine/src/assets/loaders/gltf/KTX2Loader.js index 195574bead..aab6928fca 100644 --- a/packages/engine/src/assets/loaders/gltf/KTX2Loader.js +++ b/packages/engine/src/assets/loaders/gltf/KTX2Loader.js @@ -45,7 +45,6 @@ import { Data3DTexture, DataTexture, DisplayP3ColorSpace, - FileLoader, FloatType, HalfFloatType, NoColorSpace, @@ -53,7 +52,6 @@ import { LinearMipmapLinearFilter, LinearDisplayP3ColorSpace, LinearSRGBColorSpace, - Loader, RedFormat, RGB_ETC1_Format, RGB_ETC2_Format, @@ -99,6 +97,8 @@ import { import { ZSTDDecoder } from './zstddec.module.js'; import WebWorker from 'web-worker' import { isClient } from '@etherealengine/engine/src/common/functions/getEnvironment' +import { FileLoader } from '../base/FileLoader'; +import { Loader } from '../base/Loader'; const _taskCache = new WeakMap(); @@ -257,7 +257,7 @@ class KTX2Loader extends Loader { } - load( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError, signal ) { if ( this.workerConfig === null ) { @@ -286,7 +286,7 @@ class KTX2Loader extends Loader { .then( ( texture ) => onLoad ? onLoad( texture ) : null ) .catch( onError ); - }, onProgress, onError ); + }, onProgress, onError, signal ); } diff --git a/packages/engine/src/assets/loaders/gltf/NodeDracoLoader.js b/packages/engine/src/assets/loaders/gltf/NodeDracoLoader.js index ba2254156a..9a032f1316 100644 --- a/packages/engine/src/assets/loaders/gltf/NodeDracoLoader.js +++ b/packages/engine/src/assets/loaders/gltf/NodeDracoLoader.js @@ -15,7 +15,6 @@ // 'use strict'; import { - FileLoader, BufferGeometry, DefaultLoadingManager, Float32BufferAttribute, @@ -28,6 +27,7 @@ import { Uint32BufferAttribute, Uint8BufferAttribute } from 'three' +import { FileLoader } from '../base/FileLoader' import draco from 'draco3dgltf' @@ -66,7 +66,7 @@ export class NodeDRACOLoader { return DRACO_ENCODER } - load(url, onLoad, onProgress, onError) { + load(url, onLoad, onProgress, onError, signal) { var scope = this var loader = new FileLoader(scope.manager) loader.setPath(this.path) @@ -77,7 +77,8 @@ export class NodeDRACOLoader { scope.decodeDracoFile(blob, onLoad) }, onProgress, - onError + onError, + signal ) } diff --git a/packages/engine/src/assets/loaders/tga/TGALoader.ts b/packages/engine/src/assets/loaders/tga/TGALoader.ts index c44b9816a2..711068e883 100755 --- a/packages/engine/src/assets/loaders/tga/TGALoader.ts +++ b/packages/engine/src/assets/loaders/tga/TGALoader.ts @@ -23,7 +23,9 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { FileLoader, Loader, Texture } from 'three' +import { Texture } from 'three' +import { FileLoader } from '../base/FileLoader' +import { Loader } from '../base/Loader' declare const OffscreenCanvas: { prototype: any @@ -37,7 +39,8 @@ export class TGALoader extends Loader { url: string, onLoad?: (texture: Texture) => void, onProgress?: (event: ProgressEvent) => void, - onError?: (event: ErrorEvent) => void + onError?: (event: ErrorEvent) => void, + signal?: AbortSignal ): Texture { const texture = new Texture() @@ -56,7 +59,8 @@ export class TGALoader extends Loader { } }, onProgress, - onError + onError, + signal ) return texture diff --git a/packages/engine/src/assets/loaders/usdz/USDZLoader.d.ts b/packages/engine/src/assets/loaders/usdz/USDZLoader.d.ts index 2cc5246a43..e71b523108 100644 --- a/packages/engine/src/assets/loaders/usdz/USDZLoader.d.ts +++ b/packages/engine/src/assets/loaders/usdz/USDZLoader.d.ts @@ -34,7 +34,7 @@ export class USDAParser { export class USDZLoader extends Loader { register(plugin: USDZLoaderPlugin): void unregister(plugin: USDZLoaderPlugin): void - load(url: string, onLoad: (result) => void, onProgress: (progress) => void, onError: (error) => void): void + load(url: string, onLoad: (result) => void, onProgress: (progress) => void, onError: (error) => void, signal?: AbortSignal): void parse(buffer: string, onLoad: (result) => void): object } diff --git a/packages/engine/src/assets/loaders/usdz/USDZLoader.js b/packages/engine/src/assets/loaders/usdz/USDZLoader.js index 2a48e461f7..7616b81518 100644 --- a/packages/engine/src/assets/loaders/usdz/USDZLoader.js +++ b/packages/engine/src/assets/loaders/usdz/USDZLoader.js @@ -29,10 +29,8 @@ import { BufferAttribute, BufferGeometry, ClampToEdgeWrapping, - FileLoader, Group, InstancedMesh, - Loader, Material, Matrix4, Mesh, @@ -45,6 +43,8 @@ import { TextureLoader, Vector3, } from 'three'; +import { FileLoader } from '../base/FileLoader'; +import { Loader } from '../base/Loader'; import * as fflate from 'fflate'; @@ -188,7 +188,7 @@ class USDZLoader extends Loader { this.plugins = this.plugins.filter(_plugin => plugin !== _plugin) } - load( url, onLoad, onProgress, onError ) { + load( url, onLoad, onProgress, onError, signal ) { const scope = this; @@ -217,7 +217,7 @@ class USDZLoader extends Loader { scope.manager.itemError( url ); } - }, onProgress, onError ); + }, onProgress, onError, signal ); } diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 68913e0f19..ce2c158bfd 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -132,6 +132,13 @@ const Callbacks = { onProgress: (request: ProgressEvent, resource: State) => {}, onError: (event: ErrorEvent | Error, resource: State) => {} } +} as { + [key in ResourceType]: { + onStart: (resource: State) => void + onLoad: (response: AssetType, resource: State) => void + onProgress: (request: ProgressEvent, resource: State) => void + onError: (event: ErrorEvent | Error, resource: State) => void + } } const load = ( @@ -141,7 +148,8 @@ const load = ( args: LoadingArgs, onLoad: (response: T) => void, onProgress: (request: ProgressEvent) => void, - onError: (event: ErrorEvent | Error) => void + onError: (event: ErrorEvent | Error) => void, + signal: AbortSignal ) => { const resourceState = getMutableState(ResourceState) const resources = resourceState.nested('resources') @@ -161,7 +169,7 @@ const load = ( const resource = resources[url] const callbacks = Callbacks[resourceType] console.log('Resource Manager Loading Asset at: ' + url) - callbacks.onStart() + callbacks.onStart(resource) AssetLoader.load( url, args, @@ -180,7 +188,8 @@ const load = ( resource.status.set(ResourceStatus.Error) callbacks.onError(error, resource) onError(error) - } + }, + signal ) } @@ -188,7 +197,7 @@ const unload = (url: string, entity: Entity) => { const resourceState = getMutableState(ResourceState) const resources = resourceState.nested('resources') if (!resources[url].value) { - console.error('ResourceManager:unload No resource exists for url: ' + url) + console.warn('ResourceManager:unload No resource exists for url: ' + url) return } @@ -211,7 +220,7 @@ const removeResource = (url: string) => { const resourceState = getMutableState(ResourceState) const resources = resourceState.nested('resources') if (!resources[url].value) { - console.error('ResourceManager:removeResource No resource exists for url: ' + url) + console.warn('ResourceManager:removeResource No resource exists for url: ' + url) return } From dc2a8d22e1481085032ee888a32aeea814cfa4d8 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 22 Jan 2024 13:15:53 -0800 Subject: [PATCH 22/67] license --- .../src/assets/loaders/base/FileLoader.ts | 25 +++++++++++++++++++ .../engine/src/assets/loaders/base/Loader.ts | 25 +++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/packages/engine/src/assets/loaders/base/FileLoader.ts b/packages/engine/src/assets/loaders/base/FileLoader.ts index c7aff8f142..45a25a47c7 100644 --- a/packages/engine/src/assets/loaders/base/FileLoader.ts +++ b/packages/engine/src/assets/loaders/base/FileLoader.ts @@ -1,3 +1,28 @@ +/* +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 { Cache } from 'three' import { Loader } from './Loader' diff --git a/packages/engine/src/assets/loaders/base/Loader.ts b/packages/engine/src/assets/loaders/base/Loader.ts index 8153874699..dadefb459d 100644 --- a/packages/engine/src/assets/loaders/base/Loader.ts +++ b/packages/engine/src/assets/loaders/base/Loader.ts @@ -1,3 +1,28 @@ +/* +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 { DefaultLoadingManager, LoadingManager } from 'three' class Loader { From a8c2b3f66d3f54c8378fce0f69cf73c3b0cc02e0 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 22 Jan 2024 13:18:25 -0800 Subject: [PATCH 23/67] loader typings --- packages/engine/src/assets/loaders/fbx/FBXLoader.d.ts | 1 + packages/engine/src/assets/loaders/gltf/DRACOLoader.d.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/engine/src/assets/loaders/fbx/FBXLoader.d.ts b/packages/engine/src/assets/loaders/fbx/FBXLoader.d.ts index ca301c200f..2807257abd 100644 --- a/packages/engine/src/assets/loaders/fbx/FBXLoader.d.ts +++ b/packages/engine/src/assets/loaders/fbx/FBXLoader.d.ts @@ -35,6 +35,7 @@ export class FBXLoader extends Loader { onLoad: (object: Group) => void, onProgress?: (event: ProgressEvent) => void, onError?: (event: ErrorEvent) => void, + signal?: AbortSignal ): void; loadAsync(url: string, onProgress?: (event: ProgressEvent) => void): Promise; parse(FBXBuffer: ArrayBuffer | string, path: string): Group; diff --git a/packages/engine/src/assets/loaders/gltf/DRACOLoader.d.ts b/packages/engine/src/assets/loaders/gltf/DRACOLoader.d.ts index 7d8f6501ed..c90997b53b 100755 --- a/packages/engine/src/assets/loaders/gltf/DRACOLoader.d.ts +++ b/packages/engine/src/assets/loaders/gltf/DRACOLoader.d.ts @@ -34,7 +34,8 @@ export class DRACOLoader extends Loader { url: string, onLoad: (geometry: BufferGeometry) => void, onProgress?: (event: ProgressEvent) => void, - onError?: (event: ErrorEvent) => void + onError?: (event: ErrorEvent) => void, + signal?: AbortSignal ): void setDecoderPath(path: string): DRACOLoader setDecoderConfig(config: object): DRACOLoader From 83f5b44d4d476e8c40fb8e2e6b4db5ccfb49fe24 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 22 Jan 2024 13:36:33 -0800 Subject: [PATCH 24/67] Portal texture resource hook --- .../src/scene/components/PortalComponent.ts | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/packages/engine/src/scene/components/PortalComponent.ts b/packages/engine/src/scene/components/PortalComponent.ts index 1c688465a0..dbeba5545c 100644 --- a/packages/engine/src/scene/components/PortalComponent.ts +++ b/packages/engine/src/scene/components/PortalComponent.ts @@ -25,23 +25,13 @@ Ethereal Engine. All Rights Reserved. import { RigidBodyType, ShapeType } from '@dimforge/rapier3d-compat' import { useEffect } from 'react' -import { - ArrowHelper, - BackSide, - Euler, - Mesh, - MeshBasicMaterial, - Quaternion, - SphereGeometry, - Texture, - Vector3 -} from 'three' - -import { defineState, getMutableState, getState, none, useHookstate } from '@etherealengine/hyperflux' +import { ArrowHelper, BackSide, Euler, Mesh, MeshBasicMaterial, Quaternion, SphereGeometry, Vector3 } from 'three' + +import { NO_PROXY, defineState, getMutableState, getState, none, useHookstate } from '@etherealengine/hyperflux' import { EntityUUID } from '@etherealengine/common/src/interfaces/EntityUUID' import { portalPath } from '@etherealengine/common/src/schema.type.module' -import { AssetLoader } from '../../assets/classes/AssetLoader' +import { useTexture } from '../../assets/functions/resourceHooks' import { V_100 } from '../../common/constants/MathConstants' import { matches } from '../../common/functions/MatchesUtils' import { isClient } from '../../common/functions/getEnvironment' @@ -240,20 +230,24 @@ export const PortalComponent = defineComponent({ previewImageURL: string }>(null) + const [textureState, unload] = useTexture(portalDetails.value?.previewImageURL || '', entity) + + useEffect(() => { + if (!textureState.value) return + return unload + }, [textureState]) + useEffect(() => { if (!portalDetails.value?.previewImageURL) return portalComponent.remoteSpawnPosition.value.copy(portalDetails.value.spawnPosition) portalComponent.remoteSpawnRotation.value.copy(portalDetails.value.spawnRotation) - AssetLoader.loadAsync(portalDetails.value.previewImageURL).then((texture: Texture) => { - if (!portalComponent.mesh.value || aborted) return - portalComponent.mesh.value.material.map = texture - portalComponent.mesh.value.material.needsUpdate = true - }) - let aborted = false - return () => { - aborted = true - } - }, [portalDetails, portalComponent.mesh]) + + const texture = textureState.get(NO_PROXY) + if (!texture || !portalComponent.mesh.value) return + + portalComponent.mesh.value.material.map = texture + portalComponent.mesh.value.material.needsUpdate = true + }, [portalDetails, portalComponent.mesh, textureState]) useEffect(() => { if (!isClient) return From 781f43e3d7ef5cacb355e3dc127979826c9d8d48 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 22 Jan 2024 14:50:38 -0800 Subject: [PATCH 25/67] Media component resource hooks, potential on gpu callback --- .../engine/src/assets/state/ResourceState.ts | 5 +++++ .../src/scene/components/MediaComponent.ts | 18 +++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index ce2c158bfd..1ddd0651fa 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -120,6 +120,11 @@ const Callbacks = { resource.metadata.merge({ onGPU: false }) }, onLoad: (response: Texture, resource: State) => { + response.onUpdate = () => { + resource.metadata.merge({ onGPU: true }) + //@ts-ignore + response.onUpdate = null + } if (response.mipmaps[0]) { resource.metadata.size.set(response.mipmaps[0].data.length) } else { diff --git a/packages/engine/src/scene/components/MediaComponent.ts b/packages/engine/src/scene/components/MediaComponent.ts index f2331fb1dc..fd5549dd97 100644 --- a/packages/engine/src/scene/components/MediaComponent.ts +++ b/packages/engine/src/scene/components/MediaComponent.ts @@ -27,9 +27,10 @@ import Hls from 'hls.js' import { startTransition, useEffect } from 'react' import { DoubleSide, Mesh, MeshBasicMaterial, PlaneGeometry } from 'three' -import { State, getMutableState, getState, none, useHookstate } from '@etherealengine/hyperflux' +import { NO_PROXY, State, getMutableState, getState, none, useHookstate } from '@etherealengine/hyperflux' import { AssetLoader } from '../../assets/classes/AssetLoader' +import { useTexture } from '../../assets/functions/resourceHooks' import { AudioState } from '../../audio/AudioState' import { removePannerNode } from '../../audio/PositionalAudioFunctions' import { isClient } from '../../common/functions/getEnvironment' @@ -481,15 +482,22 @@ export function MediaReactor() { ) const debugEnabled = useHookstate(getMutableState(RendererState).nodeHelperVisibility) + const [audioHelperTexture, unload] = useTexture(debugEnabled.value ? AUDIO_TEXTURE_PATH : '', entity) + + useEffect(() => { + if (!audioHelperTexture.value) return + return unload + }, [audioHelperTexture]) useEffect(() => { if (!debugEnabled.value) return const helper = new Mesh(new PlaneGeometry(), new MeshBasicMaterial({ transparent: true, side: DoubleSide })) helper.name = `audio-helper-${entity}` - AssetLoader.loadAsync(AUDIO_TEXTURE_PATH).then((AUDIO_HELPER_TEXTURE) => { - helper.material.map = AUDIO_HELPER_TEXTURE - }) + if (audioHelperTexture.value) { + const texture = audioHelperTexture.get(NO_PROXY) + helper.material.map = texture + } const helperEntity = createEntity() addObjectToGroup(helperEntity, helper) @@ -503,7 +511,7 @@ export function MediaReactor() { removeEntity(helperEntity) media.helperEntity.set(none) } - }, [debugEnabled]) + }, [debugEnabled, audioHelperTexture]) return null } From 3a9d19204856a1957706c09c699399afcd8d1b3a Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 22 Jan 2024 15:24:53 -0800 Subject: [PATCH 26/67] EnvmapComponent resource hooks --- .../src/scene/components/EnvmapComponent.tsx | 64 +++++++++++-------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/packages/engine/src/scene/components/EnvmapComponent.tsx b/packages/engine/src/scene/components/EnvmapComponent.tsx index eefdeffe0a..9cc898f86a 100644 --- a/packages/engine/src/scene/components/EnvmapComponent.tsx +++ b/packages/engine/src/scene/components/EnvmapComponent.tsx @@ -39,9 +39,10 @@ import { } from 'three' import { EntityUUID } from '@etherealengine/common/src/interfaces/EntityUUID' -import { getMutableState, useHookstate } from '@etherealengine/hyperflux' +import { NO_PROXY, getMutableState, useHookstate } from '@etherealengine/hyperflux' import { AssetLoader } from '../../assets/classes/AssetLoader' +import { useTexture } from '../../assets/functions/resourceHooks' import { isClient } from '../../common/functions/getEnvironment' import { Entity } from '../../ecs/classes/Entity' import { SceneState } from '../../ecs/classes/Scene' @@ -133,40 +134,47 @@ export const EnvmapComponent = defineComponent({ component.envmap.set(texture) }, [component.type, component.envMapSourceColor]) + const [envMapTexture, unload, error] = useTexture( + component.envMapTextureType.value === EnvMapTextureType.Equirectangular ? component.envMapSourceURL.value : '', + entity + ) + useEffect(() => { - if (component.type.value !== EnvMapSourceType.Texture) return + const texture = envMapTexture.get(NO_PROXY) + if (!texture) return - switch (component.envMapTextureType.value) { - case EnvMapTextureType.Cubemap: - loadCubeMapTexture( - component.envMapSourceURL.value, - (texture: CubeTexture | undefined) => { - if (texture) { - texture.mapping = CubeReflectionMapping - texture.colorSpace = SRGBColorSpace - component.envmap.set(texture) - removeError(entity, EnvmapComponent, 'MISSING_FILE') - } - }, - undefined, - (_) => { - component.envmap.set(null) - addError(entity, EnvmapComponent, 'MISSING_FILE', 'Skybox texture could not be found!') - } - ) - break + texture.mapping = EquirectangularReflectionMapping + component.envmap.set(texture) + return unload + }, [envMapTexture]) + + useEffect(() => { + if (!error.value) return + + component.envmap.set(null) + addError(entity, EnvmapComponent, 'MISSING_FILE', 'Skybox texture could not be found!') + }, [error]) + + useEffect(() => { + if (component.type.value !== EnvMapSourceType.Texture) return - case EnvMapTextureType.Equirectangular: - AssetLoader.loadAsync(component.envMapSourceURL.value, {}).then((texture) => { + if (component.envMapTextureType.value === EnvMapTextureType.Cubemap) { + loadCubeMapTexture( + component.envMapSourceURL.value, + (texture: CubeTexture | undefined) => { if (texture) { - texture.mapping = EquirectangularReflectionMapping + texture.mapping = CubeReflectionMapping + texture.colorSpace = SRGBColorSpace component.envmap.set(texture) removeError(entity, EnvmapComponent, 'MISSING_FILE') - } else { - component.envmap.set(null) - addError(entity, EnvmapComponent, 'MISSING_FILE', 'Skybox texture could not be found!') } - }) + }, + undefined, + (_) => { + component.envmap.set(null) + addError(entity, EnvmapComponent, 'MISSING_FILE', 'Skybox texture could not be found!') + } + ) } }, [component.type, component.envMapSourceURL]) From 018cab70043fbf9d774eebf70225011eb7feaaf1 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 22 Jan 2024 15:26:36 -0800 Subject: [PATCH 27/67] rename abort controller function --- packages/engine/src/assets/functions/resourceHooks.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 883fe6629b..84e822ed82 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -31,7 +31,7 @@ import { LoadingArgs } from '../classes/AssetLoader' import { GLTF } from '../loaders/gltf/GLTFLoader' import { AssetType, ResourceManager, ResourceType } from '../state/ResourceState' -function getAbortController(url: string, callback: () => void): AbortController { +function createAbortController(url: string, callback: () => void): AbortController { const controller = new AbortController() controller.signal.onabort = (event) => { console.warn('resourceHook: Aborted resource fetch for url: ' + url, event) @@ -70,7 +70,7 @@ function useLoader( urlState.set(url) } - const controller = getAbortController(url, unload) + const controller = createAbortController(url, unload) let completed = false if (!url) return @@ -118,7 +118,7 @@ function useBatchLoader( useEffect(() => { const completedArr = new Array(urls.length).fill(false) as boolean[] - const controller = getAbortController(urls.toString(), unload) + const controller = createAbortController(urls.toString(), unload) for (let i = 0; i < urls.length; i++) { const url = urls[i] From 64358ed3dbf2971827735e70fd759cb6ab3d0664 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 22 Jan 2024 16:37:09 -0800 Subject: [PATCH 28/67] fix ordering --- packages/engine/src/assets/functions/resourceHooks.ts | 2 +- packages/engine/src/scene/components/EnvmapComponent.tsx | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 84e822ed82..e7fa8b9b06 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -70,10 +70,10 @@ function useLoader( urlState.set(url) } + if (!url) return const controller = createAbortController(url, unload) let completed = false - if (!url) return ResourceManager.load( url, resourceType, diff --git a/packages/engine/src/scene/components/EnvmapComponent.tsx b/packages/engine/src/scene/components/EnvmapComponent.tsx index 9cc898f86a..85eb7f40e1 100644 --- a/packages/engine/src/scene/components/EnvmapComponent.tsx +++ b/packages/engine/src/scene/components/EnvmapComponent.tsx @@ -110,6 +110,10 @@ export const EnvmapComponent = defineComponent({ const group = useComponent(entity, GroupComponent) const background = useHookstate(getMutableState(SceneState).background) const mesh = useOptionalComponent(entity, MeshComponent)?.value as Mesh | null + const [envMapTexture, unload, error] = useTexture( + component.envMapTextureType.value === EnvMapTextureType.Equirectangular ? component.envMapSourceURL.value : '', + entity + ) useEffect(() => { updateEnvMapIntensity(mesh, component.envMapIntensity.value) @@ -134,11 +138,6 @@ export const EnvmapComponent = defineComponent({ component.envmap.set(texture) }, [component.type, component.envMapSourceColor]) - const [envMapTexture, unload, error] = useTexture( - component.envMapTextureType.value === EnvMapTextureType.Equirectangular ? component.envMapSourceURL.value : '', - entity - ) - useEffect(() => { const texture = envMapTexture.get(NO_PROXY) if (!texture) return From fc2ccf8868147cfb57db69cc4ad1712330781e1e Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 22 Jan 2024 18:59:18 -0800 Subject: [PATCH 29/67] Envmap and Hyperspace to resource hooks --- .../src/scene/components/EnvmapComponent.tsx | 26 ++++++++++--------- .../components/HyperspaceTagComponent.ts | 22 ++++++++++------ 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/packages/engine/src/scene/components/EnvmapComponent.tsx b/packages/engine/src/scene/components/EnvmapComponent.tsx index 85eb7f40e1..f707ab6239 100644 --- a/packages/engine/src/scene/components/EnvmapComponent.tsx +++ b/packages/engine/src/scene/components/EnvmapComponent.tsx @@ -41,7 +41,6 @@ import { import { EntityUUID } from '@etherealengine/common/src/interfaces/EntityUUID' import { NO_PROXY, getMutableState, useHookstate } from '@etherealengine/hyperflux' -import { AssetLoader } from '../../assets/classes/AssetLoader' import { useTexture } from '../../assets/functions/resourceHooks' import { isClient } from '../../common/functions/getEnvironment' import { Entity } from '../../ecs/classes/Entity' @@ -206,20 +205,23 @@ const EnvBakeComponentReactor = (props: { envmapEntity: Entity; bakeEntity: Enti const bakeComponent = useComponent(bakeEntity, EnvMapBakeComponent) const group = useComponent(envmapEntity, GroupComponent) const renderState = useHookstate(getMutableState(RendererState)) + const [envMaptexture, unload, error] = useTexture(bakeComponent.envMapOrigin.value, envmapEntity) /** @todo add an unmount cleanup for applyBoxprojection */ useEffect(() => { - AssetLoader.loadAsync(bakeComponent.envMapOrigin.value, {}).then((texture) => { - if (texture) { - texture.mapping = EquirectangularReflectionMapping - getMutableComponent(envmapEntity, EnvmapComponent).envmap.set(texture) - if (bakeComponent.boxProjection.value) applyBoxProjection(bakeEntity, group.value) - removeError(envmapEntity, EnvmapComponent, 'MISSING_FILE') - } else { - addError(envmapEntity, EnvmapComponent, 'MISSING_FILE', 'Skybox texture could not be found!') - } - }) - }, [renderState.forceBasicMaterials, bakeComponent.envMapOrigin]) + const texture = envMaptexture.get(NO_PROXY) + if (!texture) return + + texture.mapping = EquirectangularReflectionMapping + getMutableComponent(envmapEntity, EnvmapComponent).envmap.set(texture) + if (bakeComponent.boxProjection.value) applyBoxProjection(bakeEntity, group.value) + return unload + }, [envMaptexture]) + + useEffect(() => { + if (!error.value) return + addError(envmapEntity, EnvmapComponent, 'MISSING_FILE', 'Skybox texture could not be found!') + }, [error]) return null } diff --git a/packages/engine/src/scene/components/HyperspaceTagComponent.ts b/packages/engine/src/scene/components/HyperspaceTagComponent.ts index 136256cb95..e1e24c8cf6 100644 --- a/packages/engine/src/scene/components/HyperspaceTagComponent.ts +++ b/packages/engine/src/scene/components/HyperspaceTagComponent.ts @@ -24,7 +24,7 @@ Ethereal Engine. All Rights Reserved. */ import config from '@etherealengine/common/src/config' -import { getMutableState, getState } from '@etherealengine/hyperflux' +import { NO_PROXY, getMutableState, getState } from '@etherealengine/hyperflux' import { useEffect } from 'react' import { AmbientLight, @@ -42,7 +42,7 @@ import { TubeGeometry, Vector3 } from 'three' -import { AssetLoader } from '../../assets/classes/AssetLoader' +import { useTexture } from '../../assets/functions/resourceHooks' import { teleportAvatar } from '../../avatar/functions/moveAvatar' import { CameraComponent } from '../../camera/components/CameraComponent' import { ObjectDirection } from '../../common/constants/Axis3D' @@ -171,12 +171,6 @@ export const HyperspaceTagComponent = defineComponent({ addObjectToGroup(hyperspaceEffectEntity, hyperspaceEffect) setObjectLayers(hyperspaceEffect, ObjectLayers.Portal) - AssetLoader.loadAsync(`${config.client.fileServer}/projects/default-project/assets/galaxyTexture.jpg`).then( - (texture) => { - hyperspaceEffect.texture = texture - } - ) - getComponent(hyperspaceEffectEntity, TransformComponent).scale.set(10, 10, 10) setComponent(hyperspaceEffectEntity, EntityTreeComponent, { parentEntity: entity }) setComponent(hyperspaceEffectEntity, VisibleComponent) @@ -209,6 +203,18 @@ export const HyperspaceTagComponent = defineComponent({ const hyperspaceEffect = getComponent(hyperspaceEffectEntity, GroupComponent)[0] as any as PortalEffect const cameraTransform = getComponent(Engine.instance.cameraEntity, TransformComponent) const camera = getComponent(Engine.instance.cameraEntity, CameraComponent) + const [galaxyTexture, unload] = useTexture( + `${config.client.fileServer}/projects/default-project/assets/galaxyTexture.jpg`, + entity + ) + + useEffect(() => { + const texture = galaxyTexture.get(NO_PROXY) + if (!texture) return + + hyperspaceEffect.texture = texture + return unload + }, [galaxyTexture]) useEffect(() => { // TODO: add BPCEM of old and new scenes and fade them in and out too From 4edc20bc10cc496f1d4ef6ec1c598681914a90eb Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 23 Jan 2024 12:33:12 -0800 Subject: [PATCH 30/67] seperate resources by type, start of tests --- .../src/assets/functions/resourceHooks.ts | 6 +- .../src/assets/state/ResourceState.test.tsx | 65 +++++++++++++++++++ .../engine/src/assets/state/ResourceState.ts | 29 ++++++--- 3 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 packages/engine/src/assets/state/ResourceState.test.tsx diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index e7fa8b9b06..3da866620e 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -55,13 +55,13 @@ function useLoader( const progress = useHookstate | null>(null) const unload = () => { - ResourceManager.unload(url, entity) + ResourceManager.unload(url, resourceType, entity) } useEffect(() => { if (url !== urlState.value) { if (urlState.value) { - ResourceManager.unload(urlState.value, entity) + ResourceManager.unload(urlState.value, resourceType, entity) value.set(null) progress.set(null) error.set(null) @@ -113,7 +113,7 @@ function useBatchLoader( const progress = useHookstate[]>([]) const unload = () => { - for (const url of urls) ResourceManager.unload(url, entity) + for (const url of urls) ResourceManager.unload(url, resourceType, entity) } useEffect(() => { diff --git a/packages/engine/src/assets/state/ResourceState.test.tsx b/packages/engine/src/assets/state/ResourceState.test.tsx new file mode 100644 index 0000000000..14e2b85077 --- /dev/null +++ b/packages/engine/src/assets/state/ResourceState.test.tsx @@ -0,0 +1,65 @@ +/* +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 assert from 'assert' + +import { loadEmptyScene } from '../../../tests/util/loadEmptyScene' +import { destroyEngine } from '../../ecs/classes/Engine' +import { createEntity } from '../../ecs/functions/EntityFunctions' +import { createEngine } from '../../initializeEngine' +import { ResourceManager, ResourceType } from './ResourceState' + +describe('ResourceState', () => { + beforeEach(async () => { + createEngine() + loadEmptyScene() + }) + + afterEach(() => { + return destroyEngine() + }) + + it('Loads resource', async () => { + const entity = createEntity() + const controller = new AbortController() + ResourceManager.load( + './doesNotExist.glb', + ResourceType.GLTF, + entity, + {}, + (response) => { + console.log(response) + }, + (resquest) => { + console.log(resquest) + }, + (error) => { + assert(error.message) + // console.log(error) + }, + controller.signal + ) + }) +}) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 1ddd0651fa..14770b7413 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -91,16 +91,19 @@ type Resource = { export const ResourceState = defineState({ name: 'ResourceManagerState', initial: () => ({ - resources: {} as Record + resources: {} as Record> }) }) const getCurrentSizeOfResources = () => { let size = 0 const resources = getState(ResourceState).resources - for (const key in resources) { - const resource = resources[key] - if (resource.metadata.size) size += resource.metadata.size + for (const resourceType in resources) { + const resourcesOfType = resources[resourceType] + for (const key in resourcesOfType) { + const resource = resourcesOfType[key] + if (resource.metadata.size) size += resource.metadata.size + } } return size @@ -157,7 +160,13 @@ const load = ( signal: AbortSignal ) => { const resourceState = getMutableState(ResourceState) - const resources = resourceState.nested('resources') + const resourceRecord = resourceState.nested('resources') + if (!resourceRecord.nested(resourceType).value) { + resourceRecord.merge({ + [resourceType]: {} + }) + } + const resources = resourceRecord.nested(resourceType) if (!resources[url].value) { resources.merge({ [url]: { @@ -198,9 +207,9 @@ const load = ( ) } -const unload = (url: string, entity: Entity) => { +const unload = (url: string, resourceType: ResourceType, entity: Entity) => { const resourceState = getMutableState(ResourceState) - const resources = resourceState.nested('resources') + const resources = resourceState.nested('resources').nested(resourceType) if (!resources[url].value) { console.warn('ResourceManager:unload No resource exists for url: ' + url) return @@ -217,13 +226,13 @@ const unload = (url: string, entity: Entity) => { }) if (resource.references.length == 0) { - removeResource(url) + removeResource(url, resourceType) } } -const removeResource = (url: string) => { +const removeResource = (url: string, resourceType: ResourceType) => { const resourceState = getMutableState(ResourceState) - const resources = resourceState.nested('resources') + const resources = resourceState.nested('resources').nested(resourceType) if (!resources[url].value) { console.warn('ResourceManager:removeResource No resource exists for url: ' + url) return From e77f7f53d64aa3231ae0a01560e7242666d249c5 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 23 Jan 2024 13:13:09 -0800 Subject: [PATCH 31/67] Update typing --- packages/engine/src/assets/loaders/usdz/USDZLoader.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/src/assets/loaders/usdz/USDZLoader.d.ts b/packages/engine/src/assets/loaders/usdz/USDZLoader.d.ts index e71b523108..df0093f6e4 100644 --- a/packages/engine/src/assets/loaders/usdz/USDZLoader.d.ts +++ b/packages/engine/src/assets/loaders/usdz/USDZLoader.d.ts @@ -34,7 +34,7 @@ export class USDAParser { export class USDZLoader extends Loader { register(plugin: USDZLoaderPlugin): void unregister(plugin: USDZLoaderPlugin): void - load(url: string, onLoad: (result) => void, onProgress: (progress) => void, onError: (error) => void, signal?: AbortSignal): void + load(url: string, onLoad: (result) => void, onProgress?: (progress) => void, onError?: (error) => void, signal?: AbortSignal): void parse(buffer: string, onLoad: (result) => void): object } From db6c2b5dc2ba052230e449c8316b42530543e8dc Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 23 Jan 2024 14:58:38 -0800 Subject: [PATCH 32/67] Remove references to three FileLoader, Loader --- packages/engine/src/assets/classes/AssetLoader.ts | 3 ++- .../src/assets/compression/ModelTransformLoader.ts | 2 +- packages/engine/src/assets/font/FontLoader.ts | 4 +++- .../engine/src/assets/loaders/base/FileLoader.ts | 8 ++++---- packages/engine/src/assets/loaders/base/Loader.ts | 14 +++++++------- .../engine/src/assets/loaders/fbx/FBXLoader.js | 2 +- .../engine/src/assets/loaders/gltf/DRACOLoader.js | 6 +++--- .../engine/src/assets/loaders/gltf/GLTFLoader.d.ts | 1 - .../engine/src/assets/loaders/usdz/USDZLoader.d.ts | 3 ++- packages/engine/tests/util/loadGLTFAssetNode.ts | 2 +- .../server-core/src/assets/ModelTransformLoader.ts | 2 +- 11 files changed, 25 insertions(+), 22 deletions(-) diff --git a/packages/engine/src/assets/classes/AssetLoader.ts b/packages/engine/src/assets/classes/AssetLoader.ts index 1294b4fb10..77c391917b 100644 --- a/packages/engine/src/assets/classes/AssetLoader.ts +++ b/packages/engine/src/assets/classes/AssetLoader.ts @@ -28,7 +28,6 @@ import { AudioLoader, BufferAttribute, BufferGeometry, - FileLoader, Group, LOD, Material, @@ -44,6 +43,8 @@ import { TextureLoader } from 'three' +import { FileLoader } from '../loaders/base/FileLoader' + import { getState } from '@etherealengine/hyperflux' import { isClient } from '../../common/functions/getEnvironment' diff --git a/packages/engine/src/assets/compression/ModelTransformLoader.ts b/packages/engine/src/assets/compression/ModelTransformLoader.ts index 57928a98c8..b65bbc2325 100644 --- a/packages/engine/src/assets/compression/ModelTransformLoader.ts +++ b/packages/engine/src/assets/compression/ModelTransformLoader.ts @@ -42,7 +42,7 @@ import { import fetch from 'cross-fetch' import draco3d from 'draco3dgltf' import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer' -import { FileLoader } from 'three' +import { FileLoader } from '../loaders/base/FileLoader' import { EEMaterialExtension } from './extensions/EE_MaterialTransformer' import { EEResourceIDExtension } from './extensions/EE_ResourceIDTransformer' diff --git a/packages/engine/src/assets/font/FontLoader.ts b/packages/engine/src/assets/font/FontLoader.ts index 6d0c2e55d9..86a07928a0 100644 --- a/packages/engine/src/assets/font/FontLoader.ts +++ b/packages/engine/src/assets/font/FontLoader.ts @@ -23,8 +23,10 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { FileLoader, Loader, LoadingManager, ShapePath } from 'three' +import { LoadingManager, ShapePath } from 'three' +import { FileLoader } from '../loaders/base/FileLoader' +import { Loader } from '../loaders/base/Loader' export class FontLoader extends Loader { constructor(manager?: LoadingManager) { super(manager) diff --git a/packages/engine/src/assets/loaders/base/FileLoader.ts b/packages/engine/src/assets/loaders/base/FileLoader.ts index 45a25a47c7..0b34220408 100644 --- a/packages/engine/src/assets/loaders/base/FileLoader.ts +++ b/packages/engine/src/assets/loaders/base/FileLoader.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 { Cache } from 'three' +import { Cache, LoadingManager } from 'three' import { Loader } from './Loader' const loading = {} @@ -40,7 +40,7 @@ class FileLoader extends Loader { mimeType: undefined | any responseType: undefined | string - constructor(manager) { + constructor(manager?: LoadingManager) { super(manager) } @@ -238,12 +238,12 @@ class FileLoader extends Loader { this.manager.itemStart(url) } - setResponseType(value) { + setResponseType(value: string): this { this.responseType = value return this } - setMimeType(value) { + setMimeType(value): this { this.mimeType = value return this } diff --git a/packages/engine/src/assets/loaders/base/Loader.ts b/packages/engine/src/assets/loaders/base/Loader.ts index dadefb459d..17964d5d89 100644 --- a/packages/engine/src/assets/loaders/base/Loader.ts +++ b/packages/engine/src/assets/loaders/base/Loader.ts @@ -53,7 +53,7 @@ class Loader { signal?: AbortSignal ) {} - loadAsync(url, onProgress) { + loadAsync(url: TUrl, onProgress?: (event: ProgressEvent) => void): Promise { // eslint-disable-next-line @typescript-eslint/no-this-alias const scope = this @@ -62,29 +62,29 @@ class Loader { }) } - parse(data?: any) {} + parse(data?: TData) {} - setCrossOrigin(crossOrigin) { + setCrossOrigin(crossOrigin: string): this { this.crossOrigin = crossOrigin return this } - setWithCredentials(value) { + setWithCredentials(value: boolean): this { this.withCredentials = value return this } - setPath(path) { + setPath(path: string): this { this.path = path return this } - setResourcePath(resourcePath) { + setResourcePath(resourcePath: string): this { this.resourcePath = resourcePath return this } - setRequestHeader(requestHeader) { + setRequestHeader(requestHeader: { [header: string]: string }): this { this.requestHeader = requestHeader return this } diff --git a/packages/engine/src/assets/loaders/fbx/FBXLoader.js b/packages/engine/src/assets/loaders/fbx/FBXLoader.js index 07bd00a4f1..6d2f90e601 100755 --- a/packages/engine/src/assets/loaders/fbx/FBXLoader.js +++ b/packages/engine/src/assets/loaders/fbx/FBXLoader.js @@ -35,7 +35,6 @@ import { DirectionalLight, EquirectangularReflectionMapping, Euler, - FileLoader, Float32BufferAttribute, Group, Line, @@ -68,6 +67,7 @@ import { VectorKeyframeTrack, SRGBColorSpace } from 'three'; +import { FileLoader } from '../base/FileLoader'; import * as fflate from 'fflate'; import { NURBSCurve } from './NURBSCurve'; diff --git a/packages/engine/src/assets/loaders/gltf/DRACOLoader.js b/packages/engine/src/assets/loaders/gltf/DRACOLoader.js index e142138c26..337f1ed50c 100755 --- a/packages/engine/src/assets/loaders/gltf/DRACOLoader.js +++ b/packages/engine/src/assets/loaders/gltf/DRACOLoader.js @@ -1,4 +1,3 @@ - /* CPAL-1.0 License @@ -24,8 +23,9 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ - -import { BufferAttribute, BufferGeometry, FileLoader, Loader } from 'three' +import { BufferAttribute, BufferGeometry } from 'three' +import { FileLoader } from '../base/FileLoader' +import { Loader } from '../base/Loader' const _taskCache = new WeakMap() diff --git a/packages/engine/src/assets/loaders/gltf/GLTFLoader.d.ts b/packages/engine/src/assets/loaders/gltf/GLTFLoader.d.ts index 2a2a7f9372..a810a68c5e 100755 --- a/packages/engine/src/assets/loaders/gltf/GLTFLoader.d.ts +++ b/packages/engine/src/assets/loaders/gltf/GLTFLoader.d.ts @@ -33,7 +33,6 @@ import { ColorSpace, Group, InterleavedBufferAttribute, - Loader, LoadingManager, Material, Mesh, diff --git a/packages/engine/src/assets/loaders/usdz/USDZLoader.d.ts b/packages/engine/src/assets/loaders/usdz/USDZLoader.d.ts index df0093f6e4..08cd481883 100644 --- a/packages/engine/src/assets/loaders/usdz/USDZLoader.d.ts +++ b/packages/engine/src/assets/loaders/usdz/USDZLoader.d.ts @@ -25,7 +25,8 @@ Ethereal Engine. All Rights Reserved. */ -import { BufferGeometry, Group, Loader, Material } from "three"; +import { BufferGeometry, Group, Material } from "three"; +import { Loader } from "../base/Loader"; export class USDAParser { parse(text: string): object diff --git a/packages/engine/tests/util/loadGLTFAssetNode.ts b/packages/engine/tests/util/loadGLTFAssetNode.ts index 6ea5b7a9c0..d14818040b 100644 --- a/packages/engine/tests/util/loadGLTFAssetNode.ts +++ b/packages/engine/tests/util/loadGLTFAssetNode.ts @@ -26,7 +26,7 @@ Ethereal Engine. All Rights Reserved. import appRootPath from 'app-root-path' import fs from 'fs' import path from 'path' -import { FileLoader } from 'three' +import { FileLoader } from '../../src/assets/loaders/base/FileLoader' const toArrayBuffer = (buf) => { const arrayBuffer = new ArrayBuffer(buf.length) diff --git a/packages/server-core/src/assets/ModelTransformLoader.ts b/packages/server-core/src/assets/ModelTransformLoader.ts index 95447ee9b5..9ca40f4a21 100644 --- a/packages/server-core/src/assets/ModelTransformLoader.ts +++ b/packages/server-core/src/assets/ModelTransformLoader.ts @@ -23,6 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ +import { FileLoader } from '@etherealengine/engine/src/assets/loaders/base/FileLoader' import { NodeIO } from '@gltf-transform/core' import { EXTMeshGPUInstancing, @@ -42,7 +43,6 @@ import { import fetch from 'cross-fetch' import draco3d from 'draco3dgltf' import { MeshoptDecoder, MeshoptEncoder } from 'meshoptimizer' -import { FileLoader } from 'three' import { EEMaterialExtension } from './extensions/EE_MaterialTransformer' import { MOZLightmapExtension } from './extensions/MOZ_LightmapTransformer' From 33f30af3e3981e8d6dd9045398c624114a7cdc13 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 23 Jan 2024 15:03:34 -0800 Subject: [PATCH 33/67] Replace three loader reference --- packages/engine/src/assets/loaders/fbx/FBXLoader.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/engine/src/assets/loaders/fbx/FBXLoader.d.ts b/packages/engine/src/assets/loaders/fbx/FBXLoader.d.ts index 2807257abd..90836e765c 100644 --- a/packages/engine/src/assets/loaders/fbx/FBXLoader.d.ts +++ b/packages/engine/src/assets/loaders/fbx/FBXLoader.d.ts @@ -25,7 +25,8 @@ Ethereal Engine. All Rights Reserved. */ -import { Group, Loader, LoadingManager } from 'three' +import { Group, LoadingManager } from 'three' +import { Loader } from '../base/Loader' export class FBXLoader extends Loader { constructor(manager?: LoadingManager); From 3a6ec216a5d6a6b7009ef3c0510a71f520885307 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 23 Jan 2024 15:17:54 -0800 Subject: [PATCH 34/67] Parse function is optional --- packages/engine/src/assets/loaders/base/Loader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/src/assets/loaders/base/Loader.ts b/packages/engine/src/assets/loaders/base/Loader.ts index 17964d5d89..aa406cda6f 100644 --- a/packages/engine/src/assets/loaders/base/Loader.ts +++ b/packages/engine/src/assets/loaders/base/Loader.ts @@ -62,7 +62,7 @@ class Loader { }) } - parse(data?: TData) {} + parse?(data?: TData) {} setCrossOrigin(crossOrigin: string): this { this.crossOrigin = crossOrigin From eeed6746fad93b10e68aeb0e0adf7efce9992fb7 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 23 Jan 2024 19:24:09 -0800 Subject: [PATCH 35/67] Custom loading manager with more hooks, tests --- .../src/assets/functions/resourceHooks.ts | 37 ++++++ .../loaders/base/ResourceLoadingManager.ts | 112 ++++++++++++++++++ .../src/assets/state/ResourceState.test.tsx | 64 ++++++++-- .../engine/src/assets/state/ResourceState.ts | 90 ++++++++------ 4 files changed, 257 insertions(+), 46 deletions(-) create mode 100644 packages/engine/src/assets/loaders/base/ResourceLoadingManager.ts diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 3da866620e..2ec7c34a63 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -156,6 +156,35 @@ function useBatchLoader( return [values, unload, errors, progress] } +async function getLoader( + url: string, + resourceType: ResourceType, + entity: Entity = UndefinedEntity, + params: LoadingArgs = {} +): Promise<[T | null, () => void, ErrorEvent | Error | null]> { + const unload = () => { + ResourceManager.unload(url, resourceType, entity) + } + + return new Promise((resolve) => { + const controller = createAbortController(url, unload) + ResourceManager.load( + url, + resourceType, + entity, + params, + (response) => { + resolve([response, unload, null]) + }, + (request) => {}, + (err) => { + resolve([null, unload, err]) + }, + controller.signal + ) + }) +} + export function useGLTF( url: string, entity?: Entity, @@ -181,3 +210,11 @@ export function useTexture( ): [State, () => void, State, State | null>] { return useLoader(url, ResourceType.Texture, entity, params, onUnload) } + +export async function getTextureAsync( + url: string, + entity?: Entity, + params?: LoadingArgs +): Promise<[Texture | null, () => void, ErrorEvent | Error | null]> { + return getLoader(url, ResourceType.Texture, entity, params) +} diff --git a/packages/engine/src/assets/loaders/base/ResourceLoadingManager.ts b/packages/engine/src/assets/loaders/base/ResourceLoadingManager.ts new file mode 100644 index 0000000000..e478bb9f80 --- /dev/null +++ b/packages/engine/src/assets/loaders/base/ResourceLoadingManager.ts @@ -0,0 +1,112 @@ +import { Loader } from './Loader' + +class ResourceLoadingManager { + onItemStart?: (url: string) => void + onStart?: (url: string, loaded: number, total: number) => void + onLoad?: () => void + onProgress?: (url: string, loaded: number, total: number) => void + onError?: (url: string) => void + + isLoading = false + itemsLoaded = 0 + itemsTotal = 0 + urlModifier?: (url: string) => string + handlers = [] as (RegExp | Loader)[] + + constructor( + onItemStart?: (url: string) => void, + onStart?: (url: string, loaded: number, total: number) => void, + onLoad?: () => void, + onProgress?: (url: string, loaded: number, total: number) => void, + onError?: (url: string) => void + ) { + this.onItemStart = onItemStart + this.onStart = onStart + this.onLoad = onLoad + this.onProgress = onProgress + this.onError = onError + } + + itemStart = (url: string) => { + this.itemsTotal++ + + if (this.isLoading === false) { + if (this.onStart !== undefined) { + this.onStart(url, this.itemsLoaded, this.itemsTotal) + } + } + + if (this.onItemStart !== undefined) this.onItemStart(url) + + this.isLoading = true + } + + itemEnd = (url: string) => { + this.itemsLoaded++ + + if (this.onProgress !== undefined) { + this.onProgress(url, this.itemsLoaded, this.itemsTotal) + } + + if (this.itemsLoaded === this.itemsTotal) { + this.isLoading = false + + if (this.onLoad !== undefined) { + this.onLoad() + } + } + } + + itemError = (url: string) => { + if (this.onError !== undefined) { + this.onError(url) + } + } + + resolveURL = (url: string): string => { + if (this.urlModifier) { + return this.urlModifier(url) + } + + return url + } + + setURLModifier = (callback?: (url: string) => string): this => { + this.urlModifier = callback + + return this + } + + addHandler = (regex: RegExp, loader: Loader): this => { + this.handlers.push(regex, loader) + + return this + } + + removeHandler = (regex: RegExp): this => { + const index = this.handlers.indexOf(regex) + + if (index !== -1) { + this.handlers.splice(index, 2) + } + + return this + } + + getHandler = (file: string): Loader | null => { + for (let i = 0, l = this.handlers.length; i < l; i += 2) { + const regex = this.handlers[i] as RegExp + const loader = this.handlers[i + 1] as Loader + + if (regex.global) regex.lastIndex = 0 + + if (regex.test(file)) { + return loader + } + } + + return null + } +} + +export { ResourceLoadingManager } diff --git a/packages/engine/src/assets/state/ResourceState.test.tsx b/packages/engine/src/assets/state/ResourceState.test.tsx index 14e2b85077..c3ecbc5818 100644 --- a/packages/engine/src/assets/state/ResourceState.test.tsx +++ b/packages/engine/src/assets/state/ResourceState.test.tsx @@ -25,13 +25,17 @@ Ethereal Engine. All Rights Reserved. import assert from 'assert' +import { getState } from '@etherealengine/hyperflux' import { loadEmptyScene } from '../../../tests/util/loadEmptyScene' import { destroyEngine } from '../../ecs/classes/Engine' import { createEntity } from '../../ecs/functions/EntityFunctions' import { createEngine } from '../../initializeEngine' -import { ResourceManager, ResourceType } from './ResourceState' +import { GLTF } from '../loaders/gltf/GLTFLoader' +import { ResourceManager, ResourceState, ResourceStatus, ResourceType } from './ResourceState' describe('ResourceState', () => { + const url = '/packages/projects/default-project/assets/collisioncube.glb' + beforeEach(async () => { createEngine() loadEmptyScene() @@ -41,25 +45,71 @@ describe('ResourceState', () => { return destroyEngine() }) - it('Loads resource', async () => { + it('Errors when resource is missing', async () => { + const entity = createEntity() + const resourceState = getState(ResourceState) + const controller = new AbortController() + const nonExistingUrl = '/doesNotExist.glb' + ResourceManager.load( + nonExistingUrl, + ResourceType.GLTF, + entity, + {}, + (response) => { + assert(false) + }, + (resquest) => { + assert(false) + }, + (error) => { + assert(resourceState.resources[ResourceType.GLTF][nonExistingUrl].status === ResourceStatus.Error) + }, + controller.signal + ) + }) + + it('Errors on abort', async () => { const entity = createEntity() + const resourceState = getState(ResourceState) const controller = new AbortController() ResourceManager.load( - './doesNotExist.glb', + url, ResourceType.GLTF, entity, {}, (response) => { - console.log(response) + assert(false) }, (resquest) => { - console.log(resquest) + assert(false) + }, + (error) => { + assert(resourceState.resources[ResourceType.GLTF][url].status === ResourceStatus.Error) + }, + controller.signal + ) + controller.abort() + }) + + it('Load Asset', async () => { + const entity = createEntity() + const resourceState = getState(ResourceState) + const controller = new AbortController() + ResourceManager.load( + url, + ResourceType.GLTF, + entity, + {}, + (response) => { + assert(response.asset) + assert(resourceState.resources[ResourceType.GLTF][url].status === ResourceStatus.Loaded, 'Asset not loaded') }, + (resquest) => {}, (error) => { - assert(error.message) - // console.log(error) + assert(false) }, controller.signal ) + assert(resourceState.resources[ResourceType.GLTF][url].status === ResourceStatus.Loading, ' Asset not loading') }) }) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 14770b7413..91b17a6851 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -24,33 +24,15 @@ Ethereal Engine. All Rights Reserved. */ import { NO_PROXY, State, defineState, getMutableState, getState, none } from '@etherealengine/hyperflux' -import { Cache, DefaultLoadingManager, Texture } from 'three' +import { Cache, DefaultLoadingManager, LoadingManager, Texture } from 'three' import { Entity } from '../../ecs/classes/Entity' import { AssetLoader, LoadingArgs } from '../classes/AssetLoader' +import { ResourceLoadingManager } from '../loaders/base/ResourceLoadingManager' import { GLTF } from '../loaders/gltf/GLTFLoader' Cache.enabled = true -DefaultLoadingManager.onLoad = () => { - const totalSize = getCurrentSizeOfResources() - console.log('Loaded: ' + totalSize + ' bytes of resources') -} - -// //Called when the item at the url passed in has completed loading -// DefaultLoadingManager.onProgress = (url: string, loaded: number, total: number) => { -// console.log('On Progress', url, loaded, total) -// } - -// DefaultLoadingManager.onError = (url: string) => { -// console.log('On Error', url) -// } - -// //This doesn't work as you might imagine, it is only called once, the url parameter is pretty much useless -// DefaultLoadingManager.onStart = (url: string, loaded: number, total: number) => { -// console.log('On Start', url, loaded, total) -// } - -enum ResourceStatus { +export enum ResourceStatus { Unloaded, Loading, Loaded, @@ -91,19 +73,55 @@ type Resource = { export const ResourceState = defineState({ name: 'ResourceManagerState', initial: () => ({ - resources: {} as Record> + resources: {} as Record }) }) +const setDefaultLoadingManager = (loadingManager: LoadingManager) => { + DefaultLoadingManager.itemStart = loadingManager.itemStart + DefaultLoadingManager.itemEnd = loadingManager.itemEnd + DefaultLoadingManager.itemError = loadingManager.itemError + DefaultLoadingManager.resolveURL = loadingManager.resolveURL + DefaultLoadingManager.setURLModifier = loadingManager.setURLModifier + DefaultLoadingManager.addHandler = loadingManager.addHandler + DefaultLoadingManager.removeHandler = loadingManager.removeHandler + DefaultLoadingManager.getHandler = loadingManager.getHandler +} + +const onItemStart = (url: string) => { + const resourceState = getMutableState(ResourceState) + const resources = resourceState.nested('resources') + if (!resources[url].value) { + // console.warn('ResourceManager: asset loaded outside of the resource manager, url: ' + url) + return + } + + const resource = resources[url] + if (resource.status.value === ResourceStatus.Unloaded) { + resource.status.set(ResourceStatus.Loading) + } +} + +const onStart = (url: string, loaded: number, total: number) => {} +const onLoad = () => { + const totalSize = getCurrentSizeOfResources() + console.log('Loaded: ' + totalSize + ' bytes of resources') + //@ts-ignore + window.resources = getState(ResourceState) +} +const onProgress = (url: string, loaded: number, total: number) => {} +const onError = (url: string) => {} + +setDefaultLoadingManager( + new ResourceLoadingManager(onItemStart, onStart, onLoad, onProgress, onError) as LoadingManager +) + const getCurrentSizeOfResources = () => { let size = 0 const resources = getState(ResourceState).resources - for (const resourceType in resources) { - const resourcesOfType = resources[resourceType] - for (const key in resourcesOfType) { - const resource = resourcesOfType[key] - if (resource.metadata.size) size += resource.metadata.size - } + for (const key in resources) { + const resource = resources[key] + if (resource.metadata.size) size += resource.metadata.size } return size @@ -160,13 +178,7 @@ const load = ( signal: AbortSignal ) => { const resourceState = getMutableState(ResourceState) - const resourceRecord = resourceState.nested('resources') - if (!resourceRecord.nested(resourceType).value) { - resourceRecord.merge({ - [resourceType]: {} - }) - } - const resources = resourceRecord.nested(resourceType) + const resources = resourceState.nested('resources') if (!resources[url].value) { resources.merge({ [url]: { @@ -194,7 +206,6 @@ const load = ( onLoad(response) }, (request) => { - resource.status.set(ResourceStatus.Loading) callbacks.onProgress(request, resource) onProgress(request) }, @@ -209,7 +220,7 @@ const load = ( const unload = (url: string, resourceType: ResourceType, entity: Entity) => { const resourceState = getMutableState(ResourceState) - const resources = resourceState.nested('resources').nested(resourceType) + const resources = resourceState.nested('resources') if (!resources[url].value) { console.warn('ResourceManager:unload No resource exists for url: ' + url) return @@ -232,7 +243,7 @@ const unload = (url: string, resourceType: ResourceType, entity: Entity) => { const removeResource = (url: string, resourceType: ResourceType) => { const resourceState = getMutableState(ResourceState) - const resources = resourceState.nested('resources').nested(resourceType) + const resources = resourceState.nested('resources') if (!resources[url].value) { console.warn('ResourceManager:removeResource No resource exists for url: ' + url) return @@ -270,5 +281,6 @@ const removeResource = (url: string, resourceType: ResourceType) => { export const ResourceManager = { load, - unload + unload, + setDefaultLoadingManager } From 53aa851d0014ca5174c98ec11ae860fabdf26a38 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 23 Jan 2024 19:24:46 -0800 Subject: [PATCH 36/67] license --- .../loaders/base/ResourceLoadingManager.ts | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/engine/src/assets/loaders/base/ResourceLoadingManager.ts b/packages/engine/src/assets/loaders/base/ResourceLoadingManager.ts index e478bb9f80..9e4be202ad 100644 --- a/packages/engine/src/assets/loaders/base/ResourceLoadingManager.ts +++ b/packages/engine/src/assets/loaders/base/ResourceLoadingManager.ts @@ -1,3 +1,28 @@ +/* +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 { Loader } from './Loader' class ResourceLoadingManager { From fd19ef51d995d99be2959286ab468aa8bb04e640 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 23 Jan 2024 19:28:28 -0800 Subject: [PATCH 37/67] cleanup --- packages/engine/src/assets/state/ResourceState.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 91b17a6851..0fadbda755 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -106,9 +106,8 @@ const onStart = (url: string, loaded: number, total: number) => {} const onLoad = () => { const totalSize = getCurrentSizeOfResources() console.log('Loaded: ' + totalSize + ' bytes of resources') - //@ts-ignore - window.resources = getState(ResourceState) } + const onProgress = (url: string, loaded: number, total: number) => {} const onError = (url: string) => {} @@ -194,7 +193,6 @@ const load = ( const resource = resources[url] const callbacks = Callbacks[resourceType] - console.log('Resource Manager Loading Asset at: ' + url) callbacks.onStart(resource) AssetLoader.load( url, @@ -249,8 +247,6 @@ const removeResource = (url: string, resourceType: ResourceType) => { return } - console.log('Resource Manager Unloading Asset at: ' + url) - Cache.remove(url) const resource = resources[url] From c5e60dbc5ad31c3e058ae75ec7b09bdbfb3bd674 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 23 Jan 2024 19:58:10 -0800 Subject: [PATCH 38/67] tests --- .../src/assets/functions/resourceHooks.ts | 8 +- .../src/assets/state/ResourceState.test.tsx | 122 +++++++++++++++++- .../engine/src/assets/state/ResourceState.ts | 6 +- 3 files changed, 124 insertions(+), 12 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 2ec7c34a63..2ab6776112 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -55,13 +55,13 @@ function useLoader( const progress = useHookstate | null>(null) const unload = () => { - ResourceManager.unload(url, resourceType, entity) + ResourceManager.unload(url, entity) } useEffect(() => { if (url !== urlState.value) { if (urlState.value) { - ResourceManager.unload(urlState.value, resourceType, entity) + ResourceManager.unload(urlState.value, entity) value.set(null) progress.set(null) error.set(null) @@ -113,7 +113,7 @@ function useBatchLoader( const progress = useHookstate[]>([]) const unload = () => { - for (const url of urls) ResourceManager.unload(url, resourceType, entity) + for (const url of urls) ResourceManager.unload(url, entity) } useEffect(() => { @@ -163,7 +163,7 @@ async function getLoader( params: LoadingArgs = {} ): Promise<[T | null, () => void, ErrorEvent | Error | null]> { const unload = () => { - ResourceManager.unload(url, resourceType, entity) + ResourceManager.unload(url, entity) } return new Promise((resolve) => { diff --git a/packages/engine/src/assets/state/ResourceState.test.tsx b/packages/engine/src/assets/state/ResourceState.test.tsx index c3ecbc5818..fda7c31539 100644 --- a/packages/engine/src/assets/state/ResourceState.test.tsx +++ b/packages/engine/src/assets/state/ResourceState.test.tsx @@ -62,7 +62,7 @@ describe('ResourceState', () => { assert(false) }, (error) => { - assert(resourceState.resources[ResourceType.GLTF][nonExistingUrl].status === ResourceStatus.Error) + assert(resourceState.resources[nonExistingUrl].status === ResourceStatus.Error) }, controller.signal ) @@ -84,14 +84,14 @@ describe('ResourceState', () => { assert(false) }, (error) => { - assert(resourceState.resources[ResourceType.GLTF][url].status === ResourceStatus.Error) + assert(resourceState.resources[url].status === ResourceStatus.Error) }, controller.signal ) controller.abort() }) - it('Load Asset', async () => { + it('Loads asset', async () => { const entity = createEntity() const resourceState = getState(ResourceState) const controller = new AbortController() @@ -102,7 +102,120 @@ describe('ResourceState', () => { {}, (response) => { assert(response.asset) - assert(resourceState.resources[ResourceType.GLTF][url].status === ResourceStatus.Loaded, 'Asset not loaded') + assert(resourceState.resources[url].status === ResourceStatus.Loaded, 'Asset not loaded') + }, + (resquest) => {}, + (error) => { + assert(false) + }, + controller.signal + ) + }) + + it('Removes asset', async () => { + const entity = createEntity() + const resourceState = getState(ResourceState) + const controller = new AbortController() + ResourceManager.load( + url, + ResourceType.GLTF, + entity, + {}, + (response) => { + ResourceManager.unload(url, entity) + assert(resourceState.resources[url] === undefined, 'Asset not removed') + }, + (resquest) => {}, + (error) => { + assert(false) + }, + controller.signal + ) + }) + + it('Loads asset once, but references twice', async () => { + const entity = createEntity() + const entity2 = createEntity() + const resourceState = getState(ResourceState) + const controller = new AbortController() + ResourceManager.load( + url, + ResourceType.GLTF, + entity, + {}, + (response) => { + assert(resourceState.resources[url].references.length === 1, 'References not counted') + assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity not referenced') + + ResourceManager.load( + url, + ResourceType.GLTF, + entity2, + {}, + (response) => { + assert(response.asset) + assert(resourceState.resources[url].references.length === 2, 'References not counted') + assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity not referenced') + assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity2 not referenced') + ResourceManager.unload(url, entity) + + assert(resourceState.resources[url].references.length.valueOf() === 1, 'Entity reference not removed') + assert(resourceState.resources[url].references.indexOf(entity) === -1) + + ResourceManager.unload(url, entity2) + assert(resourceState.resources[url] === undefined, 'Asset not removed') + }, + (resquest) => {}, + (error) => { + assert(false) + }, + controller.signal + ) + }, + (resquest) => {}, + (error) => { + assert(false) + }, + controller.signal + ) + }) + + it('Counts references when entity is the same', async () => { + const entity = createEntity() + const resourceState = getState(ResourceState) + const controller = new AbortController() + ResourceManager.load( + url, + ResourceType.GLTF, + entity, + {}, + (response) => { + assert(resourceState.resources[url].references.length === 1, 'References not counted') + assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity not referenced') + + ResourceManager.load( + url, + ResourceType.GLTF, + entity, + {}, + (response) => { + assert(response.asset) + assert(resourceState.resources[url].references.length === 2, 'References not counted') + assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity not referenced') + ResourceManager.unload(url, entity) + + assert(resourceState.resources[url].references.length.valueOf() === 1, 'Entity reference not removed') + assert(resourceState.resources[url].references.indexOf(entity) === -1) + + ResourceManager.unload(url, entity) + assert(resourceState.resources[url] === undefined, 'Asset not removed') + }, + (resquest) => {}, + (error) => { + assert(false) + }, + controller.signal + ) }, (resquest) => {}, (error) => { @@ -110,6 +223,5 @@ describe('ResourceState', () => { }, controller.signal ) - assert(resourceState.resources[ResourceType.GLTF][url].status === ResourceStatus.Loading, ' Asset not loading') }) }) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 0fadbda755..9a22749d72 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -216,7 +216,7 @@ const load = ( ) } -const unload = (url: string, resourceType: ResourceType, entity: Entity) => { +const unload = (url: string, entity: Entity) => { const resourceState = getMutableState(ResourceState) const resources = resourceState.nested('resources') if (!resources[url].value) { @@ -235,11 +235,11 @@ const unload = (url: string, resourceType: ResourceType, entity: Entity) => { }) if (resource.references.length == 0) { - removeResource(url, resourceType) + removeResource(url) } } -const removeResource = (url: string, resourceType: ResourceType) => { +const removeResource = (url: string) => { const resourceState = getMutableState(ResourceState) const resources = resourceState.nested('resources') if (!resources[url].value) { From 7f78ef868750b41f10abc7f37f9c0f6ba0c48645 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 24 Jan 2024 11:57:59 -0800 Subject: [PATCH 39/67] tests --- .../src/assets/state/ResourceState.test.tsx | 301 +++++++++--------- 1 file changed, 148 insertions(+), 153 deletions(-) diff --git a/packages/engine/src/assets/state/ResourceState.test.tsx b/packages/engine/src/assets/state/ResourceState.test.tsx index fda7c31539..5a1d0d4aea 100644 --- a/packages/engine/src/assets/state/ResourceState.test.tsx +++ b/packages/engine/src/assets/state/ResourceState.test.tsx @@ -45,183 +45,178 @@ describe('ResourceState', () => { return destroyEngine() }) - it('Errors when resource is missing', async () => { + it('Errors when resource is missing', (done) => { const entity = createEntity() const resourceState = getState(ResourceState) const controller = new AbortController() const nonExistingUrl = '/doesNotExist.glb' - ResourceManager.load( - nonExistingUrl, - ResourceType.GLTF, - entity, - {}, - (response) => { - assert(false) - }, - (resquest) => { - assert(false) - }, - (error) => { - assert(resourceState.resources[nonExistingUrl].status === ResourceStatus.Error) - }, - controller.signal - ) + assert.doesNotThrow(() => { + ResourceManager.load( + nonExistingUrl, + ResourceType.GLTF, + entity, + {}, + (response) => { + assert(false) + }, + (resquest) => { + assert(false) + }, + (error) => { + assert(resourceState.resources[nonExistingUrl].status === ResourceStatus.Error) + done() + }, + controller.signal + ) + }, done) }) - it('Errors on abort', async () => { + it('Loads asset', (done) => { const entity = createEntity() const resourceState = getState(ResourceState) const controller = new AbortController() - ResourceManager.load( - url, - ResourceType.GLTF, - entity, - {}, - (response) => { - assert(false) - }, - (resquest) => { - assert(false) - }, - (error) => { - assert(resourceState.resources[url].status === ResourceStatus.Error) - }, - controller.signal - ) - controller.abort() + assert.doesNotThrow(() => { + ResourceManager.load( + url, + ResourceType.GLTF, + entity, + {}, + (response) => { + assert(response.asset) + assert(resourceState.resources[url].status === ResourceStatus.Loaded, 'Asset not loaded') + + done() + }, + (resquest) => {}, + (error) => { + assert(false) + }, + controller.signal + ) + }, done) }) - it('Loads asset', async () => { + it('Removes asset', (done) => { const entity = createEntity() const resourceState = getState(ResourceState) const controller = new AbortController() - ResourceManager.load( - url, - ResourceType.GLTF, - entity, - {}, - (response) => { - assert(response.asset) - assert(resourceState.resources[url].status === ResourceStatus.Loaded, 'Asset not loaded') - }, - (resquest) => {}, - (error) => { - assert(false) - }, - controller.signal - ) + assert.doesNotThrow(() => { + ResourceManager.load( + url, + ResourceType.GLTF, + entity, + {}, + (response) => { + ResourceManager.unload(url, entity) + assert(resourceState.resources[url] === undefined, 'Asset not removed') + + done() + }, + (resquest) => {}, + (error) => { + assert(false) + }, + controller.signal + ) + }, done) }) - it('Removes asset', async () => { - const entity = createEntity() - const resourceState = getState(ResourceState) - const controller = new AbortController() - ResourceManager.load( - url, - ResourceType.GLTF, - entity, - {}, - (response) => { - ResourceManager.unload(url, entity) - assert(resourceState.resources[url] === undefined, 'Asset not removed') - }, - (resquest) => {}, - (error) => { - assert(false) - }, - controller.signal - ) - }) - - it('Loads asset once, but references twice', async () => { + it('Loads asset once, but references twice', (done) => { const entity = createEntity() const entity2 = createEntity() const resourceState = getState(ResourceState) const controller = new AbortController() - ResourceManager.load( - url, - ResourceType.GLTF, - entity, - {}, - (response) => { - assert(resourceState.resources[url].references.length === 1, 'References not counted') - assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity not referenced') - - ResourceManager.load( - url, - ResourceType.GLTF, - entity2, - {}, - (response) => { - assert(response.asset) - assert(resourceState.resources[url].references.length === 2, 'References not counted') - assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity not referenced') - assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity2 not referenced') - ResourceManager.unload(url, entity) - - assert(resourceState.resources[url].references.length.valueOf() === 1, 'Entity reference not removed') - assert(resourceState.resources[url].references.indexOf(entity) === -1) - - ResourceManager.unload(url, entity2) - assert(resourceState.resources[url] === undefined, 'Asset not removed') - }, - (resquest) => {}, - (error) => { - assert(false) - }, - controller.signal - ) - }, - (resquest) => {}, - (error) => { - assert(false) - }, - controller.signal - ) + assert.doesNotThrow(() => { + ResourceManager.load( + url, + ResourceType.GLTF, + entity, + {}, + (response) => { + assert(resourceState.resources[url].references.length === 1, 'References not counted') + assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity not referenced') + + ResourceManager.load( + url, + ResourceType.GLTF, + entity2, + {}, + (response) => { + assert(response.asset) + assert(resourceState.resources[url].references.length === 2, 'References not counted') + assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity not referenced') + assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity2 not referenced') + ResourceManager.unload(url, entity) + + assert(resourceState.resources[url].references.length.valueOf() === 1, 'Entity reference not removed') + assert(resourceState.resources[url].references.indexOf(entity) === -1) + + ResourceManager.unload(url, entity2) + assert(resourceState.resources[url] === undefined, 'Asset not removed') + + done() + }, + (resquest) => {}, + (error) => { + assert(false) + }, + controller.signal + ) + }, + (resquest) => {}, + (error) => { + assert(false) + }, + controller.signal + ) + }, done) }) - it('Counts references when entity is the same', async () => { + it('Counts references when entity is the same', (done) => { const entity = createEntity() const resourceState = getState(ResourceState) const controller = new AbortController() - ResourceManager.load( - url, - ResourceType.GLTF, - entity, - {}, - (response) => { - assert(resourceState.resources[url].references.length === 1, 'References not counted') - assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity not referenced') - - ResourceManager.load( - url, - ResourceType.GLTF, - entity, - {}, - (response) => { - assert(response.asset) - assert(resourceState.resources[url].references.length === 2, 'References not counted') - assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity not referenced') - ResourceManager.unload(url, entity) - - assert(resourceState.resources[url].references.length.valueOf() === 1, 'Entity reference not removed') - assert(resourceState.resources[url].references.indexOf(entity) === -1) - - ResourceManager.unload(url, entity) - assert(resourceState.resources[url] === undefined, 'Asset not removed') - }, - (resquest) => {}, - (error) => { - assert(false) - }, - controller.signal - ) - }, - (resquest) => {}, - (error) => { - assert(false) - }, - controller.signal - ) + assert.doesNotThrow(() => { + ResourceManager.load( + url, + ResourceType.GLTF, + entity, + {}, + (response) => { + assert(resourceState.resources[url].references.length === 1, 'References not counted') + assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity not referenced') + + ResourceManager.load( + url, + ResourceType.GLTF, + entity, + {}, + (response) => { + assert(resourceState.resources[url].references.length === 2, 'References not counted') + assert(resourceState.resources[url].references.indexOf(entity) !== -1, 'Entity not referenced') + ResourceManager.unload(url, entity) + + assert(resourceState.resources[url].references.length.valueOf() === 1, 'Entity reference not removed') + assert(resourceState.resources[url].references.indexOf(entity) !== -1) + + ResourceManager.unload(url, entity) + assert(resourceState.resources[url] === undefined, 'Asset not removed') + + done() + }, + (resquest) => {}, + (error) => { + assert(false) + }, + controller.signal + ) + }, + (resquest) => {}, + (error) => { + assert(false) + }, + controller.signal + ) + }, done) }) }) From a2a57db73fb1376d96a1ced6d02e921df48226ed Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 24 Jan 2024 12:11:33 -0800 Subject: [PATCH 40/67] tests --- .../src/assets/state/ResourceState.test.tsx | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/engine/src/assets/state/ResourceState.test.tsx b/packages/engine/src/assets/state/ResourceState.test.tsx index 5a1d0d4aea..7a7a1d13e4 100644 --- a/packages/engine/src/assets/state/ResourceState.test.tsx +++ b/packages/engine/src/assets/state/ResourceState.test.tsx @@ -26,10 +26,12 @@ Ethereal Engine. All Rights Reserved. import assert from 'assert' import { getState } from '@etherealengine/hyperflux' +import { LoadingManager } from 'three' import { loadEmptyScene } from '../../../tests/util/loadEmptyScene' import { destroyEngine } from '../../ecs/classes/Engine' import { createEntity } from '../../ecs/functions/EntityFunctions' import { createEngine } from '../../initializeEngine' +import { ResourceLoadingManager } from '../loaders/base/ResourceLoadingManager' import { GLTF } from '../loaders/gltf/GLTFLoader' import { ResourceManager, ResourceState, ResourceStatus, ResourceType } from './ResourceState' @@ -219,4 +221,32 @@ describe('ResourceState', () => { ) }, done) }) + + it('Calls loading manager', (done) => { + const entity = createEntity() + const resourceState = getState(ResourceState) + const controller = new AbortController() + assert.doesNotThrow(() => { + ResourceManager.setDefaultLoadingManager( + new ResourceLoadingManager((startUrl) => { + assert(startUrl === url) + assert(resourceState.resources[url] !== undefined, 'Asset not added to resource manager') + done() + }) as LoadingManager + ) + + ResourceManager.load( + url, + ResourceType.GLTF, + entity, + {}, + (response) => {}, + (resquest) => {}, + (error) => { + assert(false) + }, + controller.signal + ) + }, done) + }) }) From a63b58ca3e79f26b1769d76b2e5929b186b5f28f Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 24 Jan 2024 14:10:30 -0800 Subject: [PATCH 41/67] Remove materials on model unload --- packages/engine/src/assets/state/ResourceState.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 9a22749d72..09597ce4de 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -24,8 +24,10 @@ Ethereal Engine. All Rights Reserved. */ import { NO_PROXY, State, defineState, getMutableState, getState, none } from '@etherealengine/hyperflux' -import { Cache, DefaultLoadingManager, LoadingManager, Texture } from 'three' +import { Cache, CompressedTexture, DefaultLoadingManager, LoadingManager, Texture } from 'three' import { Entity } from '../../ecs/classes/Entity' +import { SourceType } from '../../renderer/materials/components/MaterialSource' +import { removeMaterialSource } from '../../renderer/materials/functions/MaterialLibraryFunctions' import { AssetLoader, LoadingArgs } from '../classes/AssetLoader' import { ResourceLoadingManager } from '../loaders/base/ResourceLoadingManager' import { GLTF } from '../loaders/gltf/GLTFLoader' @@ -48,7 +50,7 @@ export enum ResourceType { Unknown } -export type AssetType = GLTF | Texture +export type AssetType = GLTF | Texture | CompressedTexture type BaseMetadata = { size?: number @@ -139,7 +141,7 @@ const Callbacks = { onStart: (resource: State) => { resource.metadata.merge({ onGPU: false }) }, - onLoad: (response: Texture, resource: State) => { + onLoad: (response: Texture | CompressedTexture, resource: State) => { response.onUpdate = () => { resource.metadata.merge({ onGPU: true }) //@ts-ignore @@ -254,6 +256,7 @@ const removeResource = (url: string) => { if (asset) { switch (resource.type.value) { case ResourceType.GLTF: + removeMaterialSource({ type: SourceType.MODEL, path: url }) break case ResourceType.Texture: ;(asset as Texture).dispose() From 6d725a4c31b8aad29a1b1cfbe1766999571ea881 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 24 Jan 2024 19:06:30 -0800 Subject: [PATCH 42/67] Model component fix wip --- .../src/assets/functions/resourceHooks.ts | 2 - .../engine/src/assets/state/ResourceState.ts | 11 +++- .../src/scene/components/ModelComponent.tsx | 52 +++++++++---------- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 2ab6776112..e5734cf51a 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -84,7 +84,6 @@ function useLoader( value.set(response) }, (request) => { - completed = true progress.set(request) }, (err) => { @@ -132,7 +131,6 @@ function useBatchLoader( values[i].set(response) }, (request) => { - completedArr[i] = true progress[i].set(request) }, (err) => { diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 09597ce4de..8cc3d73c6e 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -29,6 +29,7 @@ import { Entity } from '../../ecs/classes/Entity' import { SourceType } from '../../renderer/materials/components/MaterialSource' import { removeMaterialSource } from '../../renderer/materials/functions/MaterialLibraryFunctions' import { AssetLoader, LoadingArgs } from '../classes/AssetLoader' +import { Geometry } from '../constants/Geometry' import { ResourceLoadingManager } from '../loaders/base/ResourceLoadingManager' import { GLTF } from '../loaders/gltf/GLTFLoader' @@ -50,7 +51,7 @@ export enum ResourceType { Unknown } -export type AssetType = GLTF | Texture | CompressedTexture +export type AssetType = GLTF | Texture | CompressedTexture | Geometry type BaseMetadata = { size?: number @@ -196,6 +197,9 @@ const load = ( const resource = resources[url] const callbacks = Callbacks[resourceType] callbacks.onStart(resource) + + console.log('ResourceManager:load Loading resource: ' + url + ' for entity: ' + entity) + AssetLoader.load( url, args, @@ -226,6 +230,8 @@ const unload = (url: string, entity: Entity) => { return } + console.log('ResourceManager:unload Unloading resource: ' + url + ' for entity: ' + entity) + const resource = resources[url] resource.references.set((entities) => { @@ -249,6 +255,8 @@ const removeResource = (url: string) => { return } + console.log('ResourceManager:removeResource: Removing resource: ' + url) + Cache.remove(url) const resource = resources[url] @@ -262,6 +270,7 @@ const removeResource = (url: string) => { ;(asset as Texture).dispose() break case ResourceType.Geometry: + ;(asset as Geometry).dispose break case ResourceType.ECSData: break diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index e926740933..990f01d98d 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -151,6 +151,12 @@ function ModelReactor(): JSX.Element { ignoreDisposeGeometry: modelComponent.cameraOcclusion.value }) + useEffect(() => { + /* unload should only be called when the component is unmounted + the useGLTF hook will handle unloading if the model source is changed ie. the user changes their avatar model */ + return unload + }, []) + useEffect(() => { const onprogress = progress.value if (!onprogress) return @@ -163,6 +169,15 @@ function ModelReactor(): JSX.Element { }) }, [progress]) + useEffect(() => { + const err = error.value + if (!err) return + + console.error(err) + addError(entity, ModelComponent, 'INVALID_SOURCE', err.message) + removeComponent(entity, SceneAssetPendingTagComponent) + }, [error]) + useEffect(() => { const loadedAsset = gltf.get(NO_PROXY) if (!loadedAsset) return @@ -176,6 +191,7 @@ function ModelReactor(): JSX.Element { const boneMatchedAsset = modelComponent.convertToVRM.value ? (autoconvertMixamoAvatar(loadedAsset) as GLTF) : loadedAsset + /**if we've loaded or converted to vrm, create animation component whose mixer's root is the normalized rig */ if (boneMatchedAsset instanceof VRM) setComponent(entity, AnimationComponent, { @@ -183,30 +199,6 @@ function ModelReactor(): JSX.Element { mixer: new AnimationMixer(boneMatchedAsset.humanoid.normalizedHumanBones.hips.node) }) - modelComponent.asset.set(boneMatchedAsset) - - return unload - }, [gltf]) - - useEffect(() => { - const err = error.value - if (!err) return - - console.error(err) - addError(entity, ModelComponent, 'INVALID_SOURCE', err.message) - removeComponent(entity, SceneAssetPendingTagComponent) - }, [error]) - - useEffect(() => { - let aborted = false - if (variantComponent && !variantComponent.calculated.value) return - const model = modelComponent.value - if (!model.src) { - modelComponent.scene.set(null) - modelComponent.asset.set(null) - return - } - if (!hasComponent(entity, GroupComponent)) { const obj3d = new Group() obj3d.entity = entity @@ -214,17 +206,22 @@ function ModelReactor(): JSX.Element { proxifyParentChildRelationships(obj3d) } + modelComponent.asset.set(boneMatchedAsset) + return () => { - aborted = true + modelComponent.scene.set(null) + modelComponent.asset.set(null) } - }, [modelComponent.src, modelComponent.convertToVRM, variantComponent?.calculated]) + }, [gltf]) useEffect(() => { const model = modelComponent.get(NO_PROXY)! const asset = model.asset as GLTF | null if (!asset) return + const group = getOptionalComponent(entity, GroupComponent) if (!group) return + removeError(entity, ModelComponent, 'INVALID_SOURCE') removeError(entity, ModelComponent, 'LOADING_ERROR') const sceneObj = group[0] as Scene @@ -270,14 +267,13 @@ function ModelReactor(): JSX.Element { project: '', thumbnailUrl: '' }) - const src = modelComponent.src.value + if (!hasComponent(entity, AvatarRigComponent)) { //if this is not an avatar, add bbox snap setComponent(entity, ObjectGridSnapComponent) } return () => { - if (!(asset instanceof VRM)) clearMaterials(src) // [TODO] Replace with hooks and refrence counting getMutableState(SceneState).scenes[uuid].set(none) } }, [modelComponent.scene]) From 48fdabe24c2039bea29b80bfea8bd525215645ee Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Thu, 25 Jan 2024 16:43:12 -0800 Subject: [PATCH 43/67] Track textures loaded by the GLTFLoader --- .../src/assets/loaders/gltf/GLTFLoader.js | 3 + .../src/assets/state/ResourceState.test.tsx | 1 + .../engine/src/assets/state/ResourceState.ts | 107 +++++++++++++++--- 3 files changed, 94 insertions(+), 17 deletions(-) diff --git a/packages/engine/src/assets/loaders/gltf/GLTFLoader.js b/packages/engine/src/assets/loaders/gltf/GLTFLoader.js index 209c427992..11d59956db 100755 --- a/packages/engine/src/assets/loaders/gltf/GLTFLoader.js +++ b/packages/engine/src/assets/loaders/gltf/GLTFLoader.js @@ -3311,6 +3311,9 @@ class GLTFParser { parser.associations.set( texture, { textures: textureIndex } ); + if(parser.fileLoader.manager.itemEndFor) + parser.fileLoader.manager.itemEndFor(parser.options.url, 1, texture.uuid, texture) + return texture; } ).catch( function (error) { diff --git a/packages/engine/src/assets/state/ResourceState.test.tsx b/packages/engine/src/assets/state/ResourceState.test.tsx index 7a7a1d13e4..827f555813 100644 --- a/packages/engine/src/assets/state/ResourceState.test.tsx +++ b/packages/engine/src/assets/state/ResourceState.test.tsx @@ -232,6 +232,7 @@ describe('ResourceState', () => { assert(startUrl === url) assert(resourceState.resources[url] !== undefined, 'Asset not added to resource manager') done() + ResourceManager.setDefaultLoadingManager() }) as LoadingManager ) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 8cc3d73c6e..14a331eff7 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -66,21 +66,32 @@ type TexutreMetadata = { type Metadata = GLTFMetadata | TexutreMetadata type Resource = { + id: string status: ResourceStatus type: ResourceType references: Entity[] - assetRef?: AssetType + asset?: AssetType + assetRefs: string[] metadata: Metadata } export const ResourceState = defineState({ name: 'ResourceManagerState', initial: () => ({ - resources: {} as Record + resources: {} as Record, + referencedAssets: {} as Record }) }) -const setDefaultLoadingManager = (loadingManager: LoadingManager) => { +const setDefaultLoadingManager = ( + loadingManager: LoadingManager = new ResourceLoadingManager( + onItemStart, + onStart, + onLoad, + onProgress, + onError + ) as LoadingManager +) => { DefaultLoadingManager.itemStart = loadingManager.itemStart DefaultLoadingManager.itemEnd = loadingManager.itemEnd DefaultLoadingManager.itemError = loadingManager.itemError @@ -89,6 +100,8 @@ const setDefaultLoadingManager = (loadingManager: LoadingManager) => { DefaultLoadingManager.addHandler = loadingManager.addHandler DefaultLoadingManager.removeHandler = loadingManager.removeHandler DefaultLoadingManager.getHandler = loadingManager.getHandler + //@ts-ignore + DefaultLoadingManager.itemEndFor = onItemLoadedFor } const onItemStart = (url: string) => { @@ -109,14 +122,50 @@ const onStart = (url: string, loaded: number, total: number) => {} const onLoad = () => { const totalSize = getCurrentSizeOfResources() console.log('Loaded: ' + totalSize + ' bytes of resources') + + //@ts-ignore + window.resources = getState(ResourceState) +} + +const onItemLoadedFor = (url: string, resourceType: ResourceType, id: string, asset: T) => { + const resourceState = getMutableState(ResourceState) + const resources = resourceState.nested('resources') + const referencedAssets = resourceState.nested('referencedAssets') + if (!resources[url].value) { + console.warn('ResourceManager:loadedFor asset loaded for asset that is not loaded: ' + url) + return + } + if (!referencedAssets[id].value) { + referencedAssets.merge({ + [id]: [] + }) + } + + if (!resources[id].value) { + resources.merge({ + [id]: { + id: id, + status: ResourceStatus.Loaded, + type: resourceType, + references: [], + asset: asset, + assetRefs: [], + metadata: {} + } + }) + const callbacks = Callbacks[resourceType] + callbacks.onStart(resources[id]) + callbacks.onLoad(asset, resources[id]) + } + + resources[url].assetRefs.merge([id]) + referencedAssets[id].merge([url]) } const onProgress = (url: string, loaded: number, total: number) => {} const onError = (url: string) => {} -setDefaultLoadingManager( - new ResourceLoadingManager(onItemStart, onStart, onLoad, onProgress, onError) as LoadingManager -) +setDefaultLoadingManager() const getCurrentSizeOfResources = () => { let size = 0 @@ -184,10 +233,12 @@ const load = ( if (!resources[url].value) { resources.merge({ [url]: { + id: url, status: ResourceStatus.Unloaded, type: resourceType, references: [entity], - metadata: {} + metadata: {}, + assetRefs: [] } }) } else { @@ -205,7 +256,7 @@ const load = ( args, (response: T) => { resource.status.set(ResourceStatus.Loaded) - resource.assetRef.set(response) + resource.asset.set(response) callbacks.onLoad(response, resource) onLoad(response) }, @@ -247,24 +298,46 @@ const unload = (url: string, entity: Entity) => { } } -const removeResource = (url: string) => { +const removeReferencedResources = (resource: State) => { + const resourceState = getMutableState(ResourceState) + const referencedAssets = resourceState.nested('referencedAssets') + + for (const ref of resource.assetRefs.value) { + referencedAssets[ref].set((refs) => { + const index = refs.indexOf(resource.id.value) + if (index > -1) { + refs.splice(index, 1) + } + return refs + }) + + if (referencedAssets[ref].length == 0) { + removeResource(ref) + referencedAssets[ref].set(none) + } + } +} + +const removeResource = (id: string) => { const resourceState = getMutableState(ResourceState) const resources = resourceState.nested('resources') - if (!resources[url].value) { - console.warn('ResourceManager:removeResource No resource exists for url: ' + url) + if (!resources[id].value) { + console.warn('ResourceManager:removeResource No resource exists at id: ' + id) return } - console.log('ResourceManager:removeResource: Removing resource: ' + url) + console.log('ResourceManager:removeResource: Removing resource: ' + id) - Cache.remove(url) - const resource = resources[url] + Cache.remove(id) + const resource = resources[id] + + removeReferencedResources(resource) - const asset = resource.assetRef.get(NO_PROXY) + const asset = resource.asset.get(NO_PROXY) if (asset) { switch (resource.type.value) { case ResourceType.GLTF: - removeMaterialSource({ type: SourceType.MODEL, path: url }) + removeMaterialSource({ type: SourceType.MODEL, path: id }) break case ResourceType.Texture: ;(asset as Texture).dispose() @@ -284,7 +357,7 @@ const removeResource = (url: string) => { } } - resources[url].set(none) + resources[id].set(none) } export const ResourceManager = { From c1ac967afb91746fec32fd59a0f0d5f50c490c22 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Thu, 25 Jan 2024 17:59:54 -0800 Subject: [PATCH 44/67] Update imports --- packages/engine/src/assets/functions/resourceHooks.ts | 2 +- packages/engine/src/assets/state/ResourceState.test.tsx | 3 +-- packages/engine/src/assets/state/ResourceState.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index e5734cf51a..d2bc37870e 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -23,10 +23,10 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ +import { Entity, UndefinedEntity } from '@etherealengine/ecs' import { State, useHookstate } from '@etherealengine/hyperflux' import { useEffect } from 'react' import { Texture } from 'three' -import { Entity, UndefinedEntity } from '../../ecs/classes/Entity' import { LoadingArgs } from '../classes/AssetLoader' import { GLTF } from '../loaders/gltf/GLTFLoader' import { AssetType, ResourceManager, ResourceType } from '../state/ResourceState' diff --git a/packages/engine/src/assets/state/ResourceState.test.tsx b/packages/engine/src/assets/state/ResourceState.test.tsx index 827f555813..c190cf2b5f 100644 --- a/packages/engine/src/assets/state/ResourceState.test.tsx +++ b/packages/engine/src/assets/state/ResourceState.test.tsx @@ -25,11 +25,10 @@ Ethereal Engine. All Rights Reserved. import assert from 'assert' +import { createEntity, destroyEngine } from '@etherealengine/ecs' import { getState } from '@etherealengine/hyperflux' import { LoadingManager } from 'three' import { loadEmptyScene } from '../../../tests/util/loadEmptyScene' -import { destroyEngine } from '../../ecs/classes/Engine' -import { createEntity } from '../../ecs/functions/EntityFunctions' import { createEngine } from '../../initializeEngine' import { ResourceLoadingManager } from '../loaders/base/ResourceLoadingManager' import { GLTF } from '../loaders/gltf/GLTFLoader' diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 14a331eff7..44308f116c 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -23,9 +23,9 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ +import { Entity } from '@etherealengine/ecs' import { NO_PROXY, State, defineState, getMutableState, getState, none } from '@etherealengine/hyperflux' import { Cache, CompressedTexture, DefaultLoadingManager, LoadingManager, Texture } from 'three' -import { Entity } from '../../ecs/classes/Entity' import { SourceType } from '../../renderer/materials/components/MaterialSource' import { removeMaterialSource } from '../../renderer/materials/functions/MaterialLibraryFunctions' import { AssetLoader, LoadingArgs } from '../classes/AssetLoader' From 760f7207d8c3c8da58df7515f240ca1966842b2b Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Thu, 25 Jan 2024 18:50:31 -0800 Subject: [PATCH 45/67] Resource manager track and manage materials, geometry --- .../src/assets/loaders/gltf/GLTFLoader.js | 11 ++++++- .../engine/src/assets/state/ResourceState.ts | 32 +++++++++++++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/engine/src/assets/loaders/gltf/GLTFLoader.js b/packages/engine/src/assets/loaders/gltf/GLTFLoader.js index 11d59956db..23bcd63f97 100755 --- a/packages/engine/src/assets/loaders/gltf/GLTFLoader.js +++ b/packages/engine/src/assets/loaders/gltf/GLTFLoader.js @@ -94,6 +94,8 @@ import { import { FileLoader } from '../base/FileLoader'; import { Loader } from '../base/Loader'; +import { ResourceType } from "../../state/ResourceState" + /** * @param {BufferGeometry} geometry * @param {number} drawMode @@ -3312,7 +3314,7 @@ class GLTFParser { parser.associations.set( texture, { textures: textureIndex } ); if(parser.fileLoader.manager.itemEndFor) - parser.fileLoader.manager.itemEndFor(parser.options.url, 1, texture.uuid, texture) + parser.fileLoader.manager.itemEndFor(parser.options.url, ResourceType.Texture, texture.uuid, texture) return texture; @@ -3729,6 +3731,9 @@ class GLTFParser { if ( materialDef.extensions ) addUnknownExtensionsToUserData( extensions, material, materialDef ); + if(parser.fileLoader.manager.itemEndFor) + parser.fileLoader.manager.itemEndFor(parser.options.url, ResourceType.Material, material.uuid, material) + return material; } ); @@ -4800,6 +4805,10 @@ function addPrimitiveAttributes( geometry, primitiveDef, parser ) { return Promise.all( pending ).then( function () { + if(parser.fileLoader.manager.itemEndFor) + parser.fileLoader.manager.itemEndFor(parser.options.url, ResourceType.Geometry, geometry.uuid, geometry) + + return primitiveDef.targets !== undefined ? addMorphTargets( geometry, primitiveDef.targets, parser ) : geometry; diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 44308f116c..e37e1211f3 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -25,7 +25,7 @@ Ethereal Engine. All Rights Reserved. import { Entity } from '@etherealengine/ecs' import { NO_PROXY, State, defineState, getMutableState, getState, none } from '@etherealengine/hyperflux' -import { Cache, CompressedTexture, DefaultLoadingManager, LoadingManager, Texture } from 'three' +import { Cache, CompressedTexture, DefaultLoadingManager, LoadingManager, Material, Texture } from 'three' import { SourceType } from '../../renderer/materials/components/MaterialSource' import { removeMaterialSource } from '../../renderer/materials/functions/MaterialLibraryFunctions' import { AssetLoader, LoadingArgs } from '../classes/AssetLoader' @@ -46,12 +46,13 @@ export enum ResourceType { GLTF, Texture, Geometry, + Material, ECSData, Audio, Unknown } -export type AssetType = GLTF | Texture | CompressedTexture | Geometry +export type AssetType = GLTF | Texture | CompressedTexture | Geometry | Material type BaseMetadata = { size?: number @@ -208,6 +209,28 @@ const Callbacks = { }, onProgress: (request: ProgressEvent, resource: State) => {}, onError: (event: ErrorEvent | Error, resource: State) => {} + }, + [ResourceType.Material]: { + onStart: (resource: State) => {}, + onLoad: (response: Material, resource: State) => {}, + onProgress: (request: ProgressEvent, resource: State) => {}, + onError: (event: ErrorEvent | Error, resource: State) => {} + }, + [ResourceType.Geometry]: { + onStart: (resource: State) => {}, + onLoad: (response: Geometry, resource: State) => { + let size = 0 + for (const name in response.attributes) { + const attr = response.getAttribute(name) + size += attr.count * attr.itemSize * attr.array.BYTES_PER_ELEMENT + } + + const indices = response.getIndex() + size += indices ? indices.count * indices.itemSize * indices.array.BYTES_PER_ELEMENT : 0 + resource.metadata.size.set(size) + }, + onProgress: (request: ProgressEvent, resource: State) => {}, + onError: (event: ErrorEvent | Error, resource: State) => {} } } as { [key in ResourceType]: { @@ -343,7 +366,10 @@ const removeResource = (id: string) => { ;(asset as Texture).dispose() break case ResourceType.Geometry: - ;(asset as Geometry).dispose + ;(asset as Geometry).dispose() + break + case ResourceType.Material: + ;(asset as Material).dispose() break case ResourceType.ECSData: break From e4d376c8760e4ba79803c3aae2e292b9affa0ea0 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Fri, 26 Jan 2024 12:33:51 -0800 Subject: [PATCH 46/67] Cleanup, no need to set asset to null when resources are disposed --- .../engine/src/assets/state/ResourceState.ts | 24 +++++++++---------- .../components/LoopAnimationComponent.ts | 6 +++-- .../src/scene/components/ModelComponent.tsx | 5 ---- 3 files changed, 15 insertions(+), 20 deletions(-) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index e37e1211f3..99c16daa24 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -123,9 +123,6 @@ const onStart = (url: string, loaded: number, total: number) => {} const onLoad = () => { const totalSize = getCurrentSizeOfResources() console.log('Loaded: ' + totalSize + ' bytes of resources') - - //@ts-ignore - window.resources = getState(ResourceState) } const onItemLoadedFor = (url: string, resourceType: ResourceType, id: string, asset: T) => { @@ -198,8 +195,14 @@ const Callbacks = { //@ts-ignore response.onUpdate = null } + //Compressed texture size if (response.mipmaps[0]) { - resource.metadata.size.set(response.mipmaps[0].data.length) + let size = 0 + for (const mip of response.mipmaps) { + size += mip.data.byteLength + } + resource.metadata.size.set(size) + // Non compressed texture size } else { const height = response.image.height const width = response.image.width @@ -219,6 +222,7 @@ const Callbacks = { [ResourceType.Geometry]: { onStart: (resource: State) => {}, onLoad: (response: Geometry, resource: State) => { + // Estimated geometry size let size = 0 for (const name in response.attributes) { const attr = response.getAttribute(name) @@ -271,9 +275,7 @@ const load = ( const resource = resources[url] const callbacks = Callbacks[resourceType] callbacks.onStart(resource) - - console.log('ResourceManager:load Loading resource: ' + url + ' for entity: ' + entity) - + // console.log('ResourceManager:load Loading resource: ' + url + ' for entity: ' + entity) AssetLoader.load( url, args, @@ -304,10 +306,8 @@ const unload = (url: string, entity: Entity) => { return } - console.log('ResourceManager:unload Unloading resource: ' + url + ' for entity: ' + entity) - + // console.log('ResourceManager:unload Unloading resource: ' + url + ' for entity: ' + entity) const resource = resources[url] - resource.references.set((entities) => { const index = entities.indexOf(entity) if (index > -1) { @@ -349,11 +349,9 @@ const removeResource = (id: string) => { return } - console.log('ResourceManager:removeResource: Removing resource: ' + id) - + // console.log('ResourceManager:removeResource: Removing resource: ' + id) Cache.remove(id) const resource = resources[id] - removeReferencedResources(resource) const asset = resource.asset.get(NO_PROXY) diff --git a/packages/engine/src/avatar/components/LoopAnimationComponent.ts b/packages/engine/src/avatar/components/LoopAnimationComponent.ts index 1e8a6b174d..72c7005bad 100644 --- a/packages/engine/src/avatar/components/LoopAnimationComponent.ts +++ b/packages/engine/src/avatar/components/LoopAnimationComponent.ts @@ -217,6 +217,10 @@ export const LoopAnimationComponent = defineComponent({ const [gltf, unload] = useGLTF(loopAnimationComponent.animationPack.value, entity) + useEffect(() => { + return unload + }, []) + useEffect(() => { const asset = modelComponent?.asset.get(NO_PROXY) ?? null const model = gltf.value @@ -234,8 +238,6 @@ export const LoopAnimationComponent = defineComponent({ for (let i = 0; i < animations.length; i++) retargetAnimationClip(animations[i], model.scene) lastAnimationPack.set(loopAnimationComponent.animationPack.get(NO_PROXY)) animComponent.animations.set(animations) - - return unload }, [gltf, animComponent, loopAnimationComponent.animationPack]) return null diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index 9a602754f1..fdf95ef175 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -206,11 +206,6 @@ function ModelReactor(): JSX.Element { } modelComponent.asset.set(boneMatchedAsset) - - return () => { - modelComponent.scene.set(null) - modelComponent.asset.set(null) - } }, [gltf]) useEffect(() => { From 093893bd57dec8ae305eef85990bf4aab8a50586 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Fri, 26 Jan 2024 18:26:20 -0800 Subject: [PATCH 47/67] Track resources used by avatar preview, preliminary fixes for unloading assets race condition --- .../common/components/AvatarPreview/index.tsx | 71 ++++++++++--------- .../AssetPreviewPanels/ModelPreviewPanel.tsx | 24 +++---- .../src/assets/functions/resourceHooks.ts | 13 ++-- .../engine/src/assets/state/ResourceState.ts | 10 +++ 4 files changed, 66 insertions(+), 52 deletions(-) diff --git a/packages/client-core/src/common/components/AvatarPreview/index.tsx b/packages/client-core/src/common/components/AvatarPreview/index.tsx index e0b113eeb9..b4baf87fa6 100644 --- a/packages/client-core/src/common/components/AvatarPreview/index.tsx +++ b/packages/client-core/src/common/components/AvatarPreview/index.tsx @@ -23,7 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import React, { useEffect, useRef, useState } from 'react' +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import commonStyles from '@etherealengine/client-core/src/common/components/common.module.scss' @@ -37,14 +37,13 @@ import Tooltip from '@etherealengine/ui/src/primitives/mui/Tooltip' import { SxProps, Theme } from '@mui/material/styles' -import { AssetLoader } from '@etherealengine/engine/src/assets/classes/AssetLoader' import styles from './index.module.scss' import { setupSceneForPreview } from '@etherealengine/client-core/src/user/components/Panel3D/helperFunctions' import { AssetType } from '@etherealengine/engine/src/assets/enum/AssetType' -import { initializeKTX2Loader } from '@etherealengine/engine/src/assets/functions/createGLTFLoader' -import { GLTFLoader } from '@etherealengine/engine/src/assets/loaders/gltf/GLTFLoader' +import { useGLTF } from '@etherealengine/engine/src/assets/functions/resourceHooks' import { isAvaturn } from '@etherealengine/engine/src/avatar/functions/avatarFunctions' +import { NO_PROXY } from '@etherealengine/hyperflux' interface Props { fill?: boolean avatarUrl?: string @@ -62,43 +61,47 @@ const AvatarPreview = ({ fill, avatarUrl, sx, onAvatarError, onAvatarLoaded }: P const renderPanel = useRender3DPanelSystem(panelRef) const { entity, camera, scene, renderer } = renderPanel.state - useEffect(() => { - loadAvatarPreview() - }, [avatarUrl]) - - const loadAvatarPreview = async () => { - const oldAvatar = scene.value.children.find((item) => item.name === 'avatar') - if (oldAvatar) { - scene.value.remove(oldAvatar) - } + const override = !isAvaturn(avatarUrl || '') ? undefined : AssetType.glB + const [model, unload, error] = useGLTF(avatarUrl || '', entity.value, { + forceAssetType: override + }) + useEffect(() => { if (!avatarUrl) return setAvatarLoading(true) resetAnimationLogic(entity.value) - /** @todo this is a hack */ - const override = !isAvaturn(avatarUrl) ? undefined : AssetType.glB + }, [avatarUrl]) - const gltfLoader = AssetLoader.getLoader(AssetType.glTF) as GLTFLoader - if (!gltfLoader.ktx2Loader) { - initializeKTX2Loader(gltfLoader) - } + useEffect(() => { + return unload + }, []) - AssetLoader.loadAsync(avatarUrl, { - forceAssetType: override - }).then((avatar) => { - const loadedAvatar = setupSceneForPreview(avatar) - scene.value.add(loadedAvatar) - loadedAvatar.name = 'avatar' - loadedAvatar.rotateY(Math.PI) - setAvatarLoading(false) - onAvatarLoaded && onAvatarLoaded() - - loadedAvatar.getWorldPosition(camera.value.position) - camera.value.position.y += 1.8 - camera.value.position.z = 1 - }) - } + useEffect(() => { + if (!error.value) return + onAvatarError && onAvatarError(error.value.message) + }, [error]) + + useLayoutEffect(() => { + const avatar = model.get(NO_PROXY) + if (!avatar) return + + const loadedAvatar = setupSceneForPreview(avatar) + loadedAvatar.name = 'avatar' + loadedAvatar.rotateY(Math.PI) + setAvatarLoading(false) + onAvatarLoaded && onAvatarLoaded() + + loadedAvatar.getWorldPosition(camera.value.position) + camera.value.position.y += 1.8 + camera.value.position.z = 1 + + scene.value.add(loadedAvatar) + + return () => { + scene.value.remove(loadedAvatar) + } + }, [model]) return ( diff --git a/packages/editor/src/components/assets/AssetPreviewPanels/ModelPreviewPanel.tsx b/packages/editor/src/components/assets/AssetPreviewPanels/ModelPreviewPanel.tsx index 084f7123e1..11ac988174 100644 --- a/packages/editor/src/components/assets/AssetPreviewPanels/ModelPreviewPanel.tsx +++ b/packages/editor/src/components/assets/AssetPreviewPanels/ModelPreviewPanel.tsx @@ -30,13 +30,11 @@ import ResizeObserver from 'resize-observer-polyfill' import LoadingView from '@etherealengine/client-core/src/common/components/LoadingView' import { setupSceneForPreview } from '@etherealengine/client-core/src/user/components/Panel3D/helperFunctions' import { useRender3DPanelSystem } from '@etherealengine/client-core/src/user/components/Panel3D/useRender3DPanelSystem' -import { SourceType } from '@etherealengine/engine/src/renderer/materials/components/MaterialSource' -import { removeMaterialSource } from '@etherealengine/engine/src/renderer/materials/functions/MaterialLibraryFunctions' import { InfiniteGridHelper } from '@etherealengine/engine/src/scene/classes/InfiniteGridHelper' import { ObjectLayers } from '@etherealengine/engine/src/scene/constants/ObjectLayers' -import { useHookstate } from '@etherealengine/hyperflux' +import { NO_PROXY, useHookstate } from '@etherealengine/hyperflux' -import { AssetLoader } from '@etherealengine/engine/src/assets/classes/AssetLoader' +import { useGLTF } from '@etherealengine/engine/src/assets/functions/resourceHooks' import styles from '../styles.module.scss' export const ModelPreviewPanel = (props) => { @@ -53,6 +51,7 @@ export const ModelPreviewPanel = (props) => { gridHelper.children.forEach((child) => { child.layers.set(ObjectLayers.Panel) }) + const [model, unload] = useGLTF(url, entity.value) useEffect(() => { scene.value.add(gridHelper) @@ -77,23 +76,22 @@ export const ModelPreviewPanel = (props) => { resizeObserver.disconnect() handleSizeChangeDebounced.cancel() scene.value.remove(gridHelper) + unload() } }, []) useEffect(() => { + const avatar = model.get(NO_PROXY) + if (!avatar) return + //add to the threejs scene for previewing - AssetLoader.loadAsync(url).then((avatar) => { - scene.value.add(setupSceneForPreview(avatar)) - }) + const avatarObj = setupSceneForPreview(avatar) + scene.value.add(avatarObj) return () => { - const sceneVal = scene.value - const avatar = sceneVal.children.find((child) => child.name === 'avatar') - if (avatar?.userData['src']) { - removeMaterialSource({ type: SourceType.MODEL, path: avatar.userData['src'] }) - } + scene.value.remove(avatarObj) } - }, [url]) + }, [model]) return ( <> diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index d2bc37870e..1b2de96894 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -61,11 +61,14 @@ function useLoader( useEffect(() => { if (url !== urlState.value) { if (urlState.value) { - ResourceManager.unload(urlState.value, entity) - value.set(null) - progress.set(null) - error.set(null) - onUnload(urlState.value) + const oldUrl = urlState.value + window.requestAnimationFrame(() => { + ResourceManager.unload(oldUrl, entity) + value.set(null) + progress.set(null) + error.set(null) + onUnload(oldUrl) + }) } urlState.set(url) } diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 99c16daa24..971d5530c3 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -26,6 +26,7 @@ Ethereal Engine. All Rights Reserved. import { Entity } from '@etherealengine/ecs' import { NO_PROXY, State, defineState, getMutableState, getState, none } from '@etherealengine/hyperflux' import { Cache, CompressedTexture, DefaultLoadingManager, LoadingManager, Material, Texture } from 'three' +import { EngineRenderer } from '../../renderer/WebGLRendererSystem' import { SourceType } from '../../renderer/materials/components/MaterialSource' import { removeMaterialSource } from '../../renderer/materials/functions/MaterialLibraryFunctions' import { AssetLoader, LoadingArgs } from '../classes/AssetLoader' @@ -176,6 +177,13 @@ const getCurrentSizeOfResources = () => { return size } +const getRendererInfo = () => { + return { + memory: EngineRenderer.instance.renderer.info.memory, + programCount: EngineRenderer.instance.renderer.info.programs?.length + } +} + const Callbacks = { [ResourceType.GLTF]: { onStart: (resource: State) => {}, @@ -317,7 +325,9 @@ const unload = (url: string, entity: Entity) => { }) if (resource.references.length == 0) { + // console.log("Before Removing Resources", JSON.stringify(getRendererInfo())) removeResource(url) + // console.log("After Removing Resources", JSON.stringify(getRendererInfo())) } } From b9656d06218112153f8642ea8112eb966a598307 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 29 Jan 2024 15:02:27 -0800 Subject: [PATCH 48/67] unload correctly in avatar preview, envmapcomponent, model component --- .../common/components/AvatarPreview/index.tsx | 24 ++++++++++++------- .../src/assets/functions/resourceHooks.ts | 14 +++++------ .../src/scene/components/EnvmapComponent.tsx | 5 +++- .../src/scene/components/ModelComponent.tsx | 16 +++++++++---- 4 files changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/client-core/src/common/components/AvatarPreview/index.tsx b/packages/client-core/src/common/components/AvatarPreview/index.tsx index b4baf87fa6..886c1e0b31 100644 --- a/packages/client-core/src/common/components/AvatarPreview/index.tsx +++ b/packages/client-core/src/common/components/AvatarPreview/index.tsx @@ -23,7 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import React, { useEffect, useLayoutEffect, useRef, useState } from 'react' +import React, { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import commonStyles from '@etherealengine/client-core/src/common/components/common.module.scss' @@ -62,9 +62,19 @@ const AvatarPreview = ({ fill, avatarUrl, sx, onAvatarError, onAvatarLoaded }: P const { entity, camera, scene, renderer } = renderPanel.state const override = !isAvaturn(avatarUrl || '') ? undefined : AssetType.glB - const [model, unload, error] = useGLTF(avatarUrl || '', entity.value, { - forceAssetType: override - }) + const [model, unload, error] = useGLTF( + avatarUrl || '', + entity.value, + { + forceAssetType: override + }, + () => { + const oldAvatar = scene.value.children.find((item) => item.name === 'avatar') + if (oldAvatar) { + scene.value.remove(oldAvatar) + } + } + ) useEffect(() => { if (!avatarUrl) return @@ -82,7 +92,7 @@ const AvatarPreview = ({ fill, avatarUrl, sx, onAvatarError, onAvatarLoaded }: P onAvatarError && onAvatarError(error.value.message) }, [error]) - useLayoutEffect(() => { + useEffect(() => { const avatar = model.get(NO_PROXY) if (!avatar) return @@ -97,10 +107,6 @@ const AvatarPreview = ({ fill, avatarUrl, sx, onAvatarError, onAvatarLoaded }: P camera.value.position.z = 1 scene.value.add(loadedAvatar) - - return () => { - scene.value.remove(loadedAvatar) - } }, [model]) return ( diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 1b2de96894..c9de20e6b1 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -55,20 +55,18 @@ function useLoader( const progress = useHookstate | null>(null) const unload = () => { - ResourceManager.unload(url, entity) + if (url) ResourceManager.unload(url, entity) } useEffect(() => { if (url !== urlState.value) { if (urlState.value) { const oldUrl = urlState.value - window.requestAnimationFrame(() => { - ResourceManager.unload(oldUrl, entity) - value.set(null) - progress.set(null) - error.set(null) - onUnload(oldUrl) - }) + ResourceManager.unload(oldUrl, entity) + value.set(null) + progress.set(null) + error.set(null) + onUnload(oldUrl) } urlState.set(url) } diff --git a/packages/engine/src/scene/components/EnvmapComponent.tsx b/packages/engine/src/scene/components/EnvmapComponent.tsx index fd6276b3b3..d3c3c9ea36 100644 --- a/packages/engine/src/scene/components/EnvmapComponent.tsx +++ b/packages/engine/src/scene/components/EnvmapComponent.tsx @@ -207,6 +207,10 @@ const EnvBakeComponentReactor = (props: { envmapEntity: Entity; bakeEntity: Enti const renderState = useHookstate(getMutableState(RendererState)) const [envMaptexture, unload, error] = useTexture(bakeComponent.envMapOrigin.value, envmapEntity) + useEffect(() => { + return unload + }, []) + /** @todo add an unmount cleanup for applyBoxprojection */ useEffect(() => { const texture = envMaptexture.get(NO_PROXY) @@ -215,7 +219,6 @@ const EnvBakeComponentReactor = (props: { envmapEntity: Entity; bakeEntity: Enti texture.mapping = EquirectangularReflectionMapping getMutableComponent(envmapEntity, EnvmapComponent).envmap.set(texture) if (bakeComponent.boxProjection.value) applyBoxProjection(bakeEntity, group.value) - return unload }, [envMaptexture]) useEffect(() => { diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index fdf95ef175..5dabc364af 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -145,10 +145,18 @@ function ModelReactor(): JSX.Element { /** @todo this is a hack */ const override = !isAvaturn(modelComponent.value.src) ? undefined : AssetType.glB - const [gltf, unload, error, progress] = useGLTF(modelComponent.value.src, entity, { - forceAssetType: override, - ignoreDisposeGeometry: modelComponent.cameraOcclusion.value - }) + const [gltf, unload, error, progress] = useGLTF( + modelComponent.value.src, + entity, + { + forceAssetType: override, + ignoreDisposeGeometry: modelComponent.cameraOcclusion.value + }, + () => { + const uuid = getModelSceneID(entity) + getMutableState(SceneState).scenes[uuid].set(none) + } + ) useEffect(() => { /* unload should only be called when the component is unmounted From 16a304b010bba7f1b2333a32483daade72a8647a Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 29 Jan 2024 15:49:51 -0800 Subject: [PATCH 49/67] Update imports --- packages/engine/src/assets/state/ResourceState.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 971d5530c3..0f09aa17f4 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -27,8 +27,8 @@ import { Entity } from '@etherealengine/ecs' import { NO_PROXY, State, defineState, getMutableState, getState, none } from '@etherealengine/hyperflux' import { Cache, CompressedTexture, DefaultLoadingManager, LoadingManager, Material, Texture } from 'three' import { EngineRenderer } from '../../renderer/WebGLRendererSystem' -import { SourceType } from '../../renderer/materials/components/MaterialSource' -import { removeMaterialSource } from '../../renderer/materials/functions/MaterialLibraryFunctions' +import { SourceType } from '../../scene/materials/components/MaterialSource' +import { removeMaterialSource } from '../../scene/materials/functions/MaterialLibraryFunctions' import { AssetLoader, LoadingArgs } from '../classes/AssetLoader' import { Geometry } from '../constants/Geometry' import { ResourceLoadingManager } from '../loaders/base/ResourceLoadingManager' From 7aea3ba03f86a15ad166423a6c50445a0b624f42 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 29 Jan 2024 18:41:10 -0800 Subject: [PATCH 50/67] Start of variant resource manager --- .../src/assets/functions/resourceHooks.ts | 27 +++++++++++--- .../avatar/systems/AvatarAnimationSystem.ts | 22 ++++++----- .../functions/loaders/VariantFunctions.ts | 37 +++++++++++++++++++ 3 files changed, 71 insertions(+), 15 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index c9de20e6b1..04555d1bc1 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -107,10 +107,16 @@ function useBatchLoader( resourceType: ResourceType, entity: Entity = UndefinedEntity, params: LoadingArgs = {} -): [State, () => void, State<(ErrorEvent | Error)[]>, State[]>] { - const values = useHookstate([]) - const errors = useHookstate<(ErrorEvent | Error)[]>([]) - const progress = useHookstate[]>([]) +): [ + State<(T | null)[]>, + () => void, + State<(ErrorEvent | Error | null)[]>, + State<(ProgressEvent | null)[]> +] { + const urlsState = useHookstate(urls) + const values = useHookstate(new Array(urls.length).fill(null)) + const errors = useHookstate<(ErrorEvent | Error)[]>(new Array(urls.length).fill(null)) + const progress = useHookstate[]>(new Array(urls.length).fill(null)) const unload = () => { for (const url of urls) ResourceManager.unload(url, entity) @@ -120,8 +126,14 @@ function useBatchLoader( const completedArr = new Array(urls.length).fill(false) as boolean[] const controller = createAbortController(urls.toString(), unload) + for (const url of urlsState.value) { + if (!urls.includes(url)) ResourceManager.unload(url, entity) + } + urlsState.set(urls) + for (let i = 0; i < urls.length; i++) { const url = urls[i] + if (!url) continue ResourceManager.load( url, resourceType, @@ -197,7 +209,12 @@ export function useBatchGLTF( urls: string[], entity?: Entity, params?: LoadingArgs -): [State, () => void, State<(ErrorEvent | Error)[]>, State[]>] { +): [ + State<(GLTF | null)[]>, + () => void, + State<(ErrorEvent | Error | null)[]>, + State<(ProgressEvent | null)[]> +] { return useBatchLoader(urls, ResourceType.GLTF, entity, params) } diff --git a/packages/engine/src/avatar/systems/AvatarAnimationSystem.ts b/packages/engine/src/avatar/systems/AvatarAnimationSystem.ts index 0200fef15e..1190ee7b3e 100644 --- a/packages/engine/src/avatar/systems/AvatarAnimationSystem.ts +++ b/packages/engine/src/avatar/systems/AvatarAnimationSystem.ts @@ -313,17 +313,19 @@ const reactor = () => { for (let i = 0; i < assets.length; i++) { const asset = assets[i] - // delete unneeded geometry data to save memory - asset.scene.traverse((node) => { - delete (node as any).geometry - delete (node as any).material - }) - for (let i = 0; i < asset.animations.length; i++) { - retargetAnimationClip(asset.animations[i], asset.scene) + if (asset) { + // delete unneeded geometry data to save memory + asset.scene.traverse((node) => { + delete (node as any).geometry + delete (node as any).material + }) + for (let i = 0; i < asset.animations.length; i++) { + retargetAnimationClip(asset.animations[i], asset.scene) + } + //ensure animations are always placed in the scene + asset.scene.animations = asset.animations + manager.loadedAnimations[animations[i]].set(asset) } - //ensure animations are always placed in the scene - asset.scene.animations = asset.animations - manager.loadedAnimations[animations[i]].set(asset) } return unload diff --git a/packages/engine/src/scene/functions/loaders/VariantFunctions.ts b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts index 7b8dadb50d..5652b38946 100644 --- a/packages/engine/src/scene/functions/loaders/VariantFunctions.ts +++ b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts @@ -5,8 +5,11 @@ import { DistanceFromCameraComponent } from '@etherealengine/engine/src/transfor import { getComponent, getMutableComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Engine } from '@etherealengine/ecs/src/Engine' import { Entity } from '@etherealengine/ecs/src/Entity' +import { NO_PROXY } from '@etherealengine/hyperflux' +import { useEffect } from 'react' import { AssetLoader } from '../../../assets/classes/AssetLoader' import { pathResolver } from '../../../assets/functions/pathResolver' +import { useBatchGLTF } from '../../../assets/functions/resourceHooks' import { addOBCPlugin } from '../../../common/functions/OnBeforeCompilePlugin' import { isMobile } from '../../../common/functions/isMobile' import { GroupComponent, addObjectToGroup, removeObjectFromGroup } from '../../../renderer/components/GroupComponent' @@ -91,6 +94,40 @@ export function setMeshVariant(entity: Entity) { } } +export function useMeshVariant(entities: Entity[]) { + const variantUrls = new Array(entities.length).fill('') as string[] + const meshComponents = entities.map((entity, index) => { + const variantComponent = getComponent(entity, VariantComponent) + const meshComponent = getComponent(entity, MeshComponent) + + if (variantComponent.heuristic === 'DEVICE') { + const targetDevice = isMobileXRHeadset ? 'XR' : isMobile ? 'MOBILE' : 'DESKTOP' + //set model src to mobile variant src + const deviceVariant = variantComponent.levels.find((level) => level.metadata['device'] === targetDevice) + if (deviceVariant) variantUrls[index] = deviceVariant.src + } + + return meshComponent + }) + + const [models, unload] = useBatchGLTF(variantUrls) + + useEffect(() => { + return unload + }, []) + + useEffect(() => { + const gltfs = models.get(NO_PROXY) + gltfs.map((gltf, index) => { + if (!gltf) return + const mesh = getFirstMesh(gltf.scene) + if (!mesh) return + meshComponents[index].geometry = mesh.geometry + meshComponents[index].material = mesh.material + }) + }, [models]) +} + export function setInstancedMeshVariant(entity: Entity) { const variantComponent = getComponent(entity, VariantComponent) const meshComponent = getComponent(entity, MeshComponent) From de97c1a51e264fe5e17bf7c6f3ad3c6aa11c77dd Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 30 Jan 2024 17:39:52 -0800 Subject: [PATCH 51/67] Resource manager loads variants if exists --- .../src/assets/functions/resourceHooks.ts | 11 +-- .../engine/src/assets/state/ResourceState.ts | 24 ++++- .../src/scene/components/ModelComponent.tsx | 4 - .../src/scene/components/VariantComponent.tsx | 8 +- .../functions/loaders/VariantFunctions.ts | 97 +++++++++---------- 5 files changed, 72 insertions(+), 72 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 04555d1bc1..5488ef746c 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -27,6 +27,7 @@ import { Entity, UndefinedEntity } from '@etherealengine/ecs' import { State, useHookstate } from '@etherealengine/hyperflux' import { useEffect } from 'react' import { Texture } from 'three' +import { useVariant } from '../../scene/functions/loaders/VariantFunctions' import { LoadingArgs } from '../classes/AssetLoader' import { GLTF } from '../loaders/gltf/GLTFLoader' import { AssetType, ResourceManager, ResourceType } from '../state/ResourceState' @@ -113,7 +114,6 @@ function useBatchLoader( State<(ErrorEvent | Error | null)[]>, State<(ProgressEvent | null)[]> ] { - const urlsState = useHookstate(urls) const values = useHookstate(new Array(urls.length).fill(null)) const errors = useHookstate<(ErrorEvent | Error)[]>(new Array(urls.length).fill(null)) const progress = useHookstate[]>(new Array(urls.length).fill(null)) @@ -126,11 +126,6 @@ function useBatchLoader( const completedArr = new Array(urls.length).fill(false) as boolean[] const controller = createAbortController(urls.toString(), unload) - for (const url of urlsState.value) { - if (!urls.includes(url)) ResourceManager.unload(url, entity) - } - urlsState.set(urls) - for (let i = 0; i < urls.length; i++) { const url = urls[i] if (!url) continue @@ -202,6 +197,10 @@ export function useGLTF( params?: LoadingArgs, onUnload?: (url: string) => void ): [State, () => void, State, State | null>] { + const variantUrl = useVariant(entity) + if (variantUrl) { + url = variantUrl + } return useLoader(url, ResourceType.GLTF, entity, params, onUnload) } diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 0f09aa17f4..25c6bb43a1 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -59,13 +59,15 @@ type BaseMetadata = { size?: number } -type GLTFMetadata = BaseMetadata +type GLTFMetadata = { + verts: number +} & BaseMetadata type TexutreMetadata = { onGPU: boolean } & BaseMetadata -type Metadata = GLTFMetadata | TexutreMetadata +type Metadata = GLTFMetadata | TexutreMetadata | BaseMetadata type Resource = { id: string @@ -123,7 +125,9 @@ const onItemStart = (url: string) => { const onStart = (url: string, loaded: number, total: number) => {} const onLoad = () => { const totalSize = getCurrentSizeOfResources() + const totalVerts = getCurrentVertCountOfResources() console.log('Loaded: ' + totalSize + ' bytes of resources') + console.log(totalVerts + ' Vertices') } const onItemLoadedFor = (url: string, resourceType: ResourceType, id: string, asset: T) => { @@ -177,6 +181,17 @@ const getCurrentSizeOfResources = () => { return size } +const getCurrentVertCountOfResources = () => { + let verts = 0 + const resources = getState(ResourceState).resources + for (const key in resources) { + const resource = resources[key] + if ((resource.metadata as GLTFMetadata).verts) verts += (resource.metadata as GLTFMetadata).verts + } + + return verts +} + const getRendererInfo = () => { return { memory: EngineRenderer.instance.renderer.info.memory, @@ -238,7 +253,10 @@ const Callbacks = { } const indices = response.getIndex() - size += indices ? indices.count * indices.itemSize * indices.array.BYTES_PER_ELEMENT : 0 + if (indices) { + resource.metadata.merge({ verts: indices.count }) + size += indices.count * indices.itemSize * indices.array.BYTES_PER_ELEMENT + } resource.metadata.size.set(size) }, onProgress: (request: ProgressEvent, resource: State) => {}, diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index 80c522147a..83d34c6bd8 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -68,7 +68,6 @@ import { SceneAssetPendingTagComponent } from './SceneAssetPendingTagComponent' import { SceneObjectComponent } from './SceneObjectComponent' import { ShadowComponent } from './ShadowComponent' import { SourceComponent } from './SourceComponent' -import { VariantComponent } from './VariantComponent' function clearMaterials(src: string) { try { @@ -140,8 +139,6 @@ export const ModelComponent = defineComponent({ function ModelReactor(): JSX.Element { const entity = useEntityContext() const modelComponent = useComponent(entity, ModelComponent) - const uuidComponent = useComponent(entity, UUIDComponent) - const variantComponent = useOptionalComponent(entity, VariantComponent) /** @todo this is a hack */ const override = !isAvaturn(modelComponent.value.src) ? undefined : AssetType.glB @@ -189,7 +186,6 @@ function ModelReactor(): JSX.Element { const loadedAsset = gltf.get(NO_PROXY) if (!loadedAsset) return - if (variantComponent && !variantComponent.calculated.value) return if (typeof loadedAsset !== 'object') { addError(entity, ModelComponent, 'INVALID_SOURCE', 'Invalid URL') return diff --git a/packages/engine/src/scene/components/VariantComponent.tsx b/packages/engine/src/scene/components/VariantComponent.tsx index 43c56e4a2b..e04c21d8e7 100644 --- a/packages/engine/src/scene/components/VariantComponent.tsx +++ b/packages/engine/src/scene/components/VariantComponent.tsx @@ -38,7 +38,7 @@ import { import { Entity } from '@etherealengine/ecs/src/Entity' import { useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' import { DistanceFromCameraComponent } from '../../transform/components/DistanceComponents' -import { setInstancedMeshVariant, setMeshVariant, setModelVariant } from '../functions/loaders/VariantFunctions' +import { setInstancedMeshVariant } from '../functions/loaders/VariantFunctions' import { InstancingComponent } from './InstancingComponent' import { MeshComponent } from './MeshComponent' import { ModelComponent } from './ModelComponent' @@ -131,15 +131,11 @@ const VariantLevelReactor = React.memo(({ entity, level }: { level: number; enti } }, [variantComponent.heuristic]) - useEffect(() => { - modelComponent && setModelVariant(entity) - }, [variantLevel.src, variantLevel.metadata, modelComponent]) - const meshComponent = useOptionalComponent(entity, MeshComponent) const instancingComponent = getOptionalComponent(entity, InstancingComponent) useEffect(() => { - meshComponent && !instancingComponent && setMeshVariant(entity) + // meshComponent && !instancingComponent && setMeshVariant(entity) meshComponent && instancingComponent && setInstancedMeshVariant(entity) }, [variantLevel.src, variantLevel.metadata, meshComponent]) diff --git a/packages/engine/src/scene/functions/loaders/VariantFunctions.ts b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts index 5652b38946..0999c27c5e 100644 --- a/packages/engine/src/scene/functions/loaders/VariantFunctions.ts +++ b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts @@ -2,14 +2,11 @@ import { InstancedMesh, Material, Object3D, Vector3 } from 'three' import { DistanceFromCameraComponent } from '@etherealengine/engine/src/transform/components/DistanceComponents' -import { getComponent, getMutableComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { getComponent, getMutableComponent, useOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Engine } from '@etherealengine/ecs/src/Engine' import { Entity } from '@etherealengine/ecs/src/Entity' -import { NO_PROXY } from '@etherealengine/hyperflux' -import { useEffect } from 'react' import { AssetLoader } from '../../../assets/classes/AssetLoader' import { pathResolver } from '../../../assets/functions/pathResolver' -import { useBatchGLTF } from '../../../assets/functions/resourceHooks' import { addOBCPlugin } from '../../../common/functions/OnBeforeCompilePlugin' import { isMobile } from '../../../common/functions/isMobile' import { GroupComponent, addObjectToGroup, removeObjectFromGroup } from '../../../renderer/components/GroupComponent' @@ -46,21 +43,14 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 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 = getMutableComponent(entity, VariantComponent) - const modelComponent = getMutableComponent(entity, ModelComponent) - +function getModelVariant(entity: Entity, variantComponent, modelComponent): string | null { if (variantComponent.heuristic.value === 'DEVICE') { const targetDevice = isMobile || isMobileXRHeadset ? 'MOBILE' : 'DESKTOP' - //set model src to mobile variant src + //get model src to mobile variant src const deviceVariant = variantComponent.levels.find((level) => level.value.metadata['device'] === targetDevice) const modelRelativePath = pathResolver().exec(modelComponent.src.value)?.[2] const deviceRelativePath = deviceVariant ? pathResolver().exec(deviceVariant.src.value)?.[2] : '' - deviceVariant && modelRelativePath !== deviceRelativePath && modelComponent.src.set(deviceVariant.src.value) + if (deviceVariant && modelRelativePath !== deviceRelativePath) return deviceVariant.src.value } else if (variantComponent.heuristic.value === 'DISTANCE') { const distance = DistanceFromCameraComponent.squaredDistance[entity] for (let i = 0; i < variantComponent.levels.length; i++) { @@ -69,63 +59,64 @@ export function setModelVariant(entity: Entity) { 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 + if (useLevel && level.src) return level.src } } - variantComponent.calculated.set(true) -} -export function setMeshVariant(entity: Entity) { - const variantComponent = getComponent(entity, VariantComponent) - const meshComponent = getComponent(entity, MeshComponent) + return null +} +function getMeshVariant(entity: Entity, variantComponent): string | null { if (variantComponent.heuristic === 'DEVICE') { const targetDevice = isMobileXRHeadset ? 'XR' : isMobile ? 'MOBILE' : 'DESKTOP' - //set model src to mobile variant src + //get model src to mobile variant src const deviceVariant = variantComponent.levels.find((level) => level.metadata['device'] === targetDevice) - if (!deviceVariant) return - AssetLoader.load(deviceVariant.src, {}, (gltf) => { - const mesh = getFirstMesh(gltf.scene) - if (!mesh) return - meshComponent.geometry = mesh.geometry - meshComponent.material = mesh.material - }) + if (deviceVariant) return deviceVariant.src } + + return null } -export function useMeshVariant(entities: Entity[]) { - const variantUrls = new Array(entities.length).fill('') as string[] - const meshComponents = entities.map((entity, index) => { - const variantComponent = getComponent(entity, VariantComponent) - const meshComponent = getComponent(entity, MeshComponent) +export function useVariant(entity?: Entity): string | null { + if (!entity) return null + const variantComponent = useOptionalComponent(entity, VariantComponent) + if (!variantComponent || !variantComponent.value) return null - if (variantComponent.heuristic === 'DEVICE') { - const targetDevice = isMobileXRHeadset ? 'XR' : isMobile ? 'MOBILE' : 'DESKTOP' - //set model src to mobile variant src - const deviceVariant = variantComponent.levels.find((level) => level.metadata['device'] === targetDevice) - if (deviceVariant) variantUrls[index] = deviceVariant.src - } + const modelComponent = useOptionalComponent(entity, ModelComponent) + const meshComponent = useOptionalComponent(entity, MeshComponent) - return meshComponent - }) + if (modelComponent) return getModelVariant(entity, variantComponent, modelComponent) + else if (meshComponent) return getMeshVariant(entity, variantComponent) + else return null +} - const [models, unload] = useBatchGLTF(variantUrls) +/** + * Handles setting model src for model component based on variant component + * @param entity + */ +export function setModelVariant(entity: Entity) { + const variantComponent = getMutableComponent(entity, VariantComponent) + const modelComponent = getMutableComponent(entity, ModelComponent) - useEffect(() => { - return unload - }, []) + const src = getModelVariant(entity, variantComponent, modelComponent) + if (src && modelComponent.src.value !== src) modelComponent.src.set(src) - useEffect(() => { - const gltfs = models.get(NO_PROXY) - gltfs.map((gltf, index) => { - if (!gltf) return + variantComponent.calculated.set(true) +} + +export function setMeshVariant(entity: Entity) { + const variantComponent = getComponent(entity, VariantComponent) + const meshComponent = getComponent(entity, MeshComponent) + + const src = getMeshVariant(entity, variantComponent) + if (src) { + AssetLoader.load(src, {}, (gltf) => { const mesh = getFirstMesh(gltf.scene) if (!mesh) return - meshComponents[index].geometry = mesh.geometry - meshComponents[index].material = mesh.material + meshComponent.geometry = mesh.geometry + meshComponent.material = mesh.material }) - }, [models]) + } } export function setInstancedMeshVariant(entity: Entity) { From 588cbe283ee5aeed5976ed50cfdedef0f14181ca Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 30 Jan 2024 19:31:07 -0800 Subject: [PATCH 52/67] hookstate fix --- .../engine/src/assets/functions/resourceHooks.ts | 4 ++-- .../src/scene/functions/loaders/VariantFunctions.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/engine/src/assets/functions/resourceHooks.ts b/packages/engine/src/assets/functions/resourceHooks.ts index 5488ef746c..5e6a4e2a03 100644 --- a/packages/engine/src/assets/functions/resourceHooks.ts +++ b/packages/engine/src/assets/functions/resourceHooks.ts @@ -27,7 +27,7 @@ import { Entity, UndefinedEntity } from '@etherealengine/ecs' import { State, useHookstate } from '@etherealengine/hyperflux' import { useEffect } from 'react' import { Texture } from 'three' -import { useVariant } from '../../scene/functions/loaders/VariantFunctions' +import { getVariant } from '../../scene/functions/loaders/VariantFunctions' import { LoadingArgs } from '../classes/AssetLoader' import { GLTF } from '../loaders/gltf/GLTFLoader' import { AssetType, ResourceManager, ResourceType } from '../state/ResourceState' @@ -197,7 +197,7 @@ export function useGLTF( params?: LoadingArgs, onUnload?: (url: string) => void ): [State, () => void, State, State | null>] { - const variantUrl = useVariant(entity) + const variantUrl = getVariant(entity) if (variantUrl) { url = variantUrl } diff --git a/packages/engine/src/scene/functions/loaders/VariantFunctions.ts b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts index 57a36cbb47..b93e6b7b5a 100644 --- a/packages/engine/src/scene/functions/loaders/VariantFunctions.ts +++ b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts @@ -2,7 +2,7 @@ import { InstancedMesh, Material, Object3D, Vector3 } from 'three' import { DistanceFromCameraComponent } from '@etherealengine/spatial/src/transform/components/DistanceComponents' -import { getComponent, getMutableComponent, useOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { getComponent, getMutableComponent, getOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { Engine } from '@etherealengine/ecs/src/Engine' import { Entity } from '@etherealengine/ecs/src/Entity' import { addOBCPlugin } from '@etherealengine/spatial/src/common/functions/OnBeforeCompilePlugin' @@ -81,13 +81,13 @@ function getMeshVariant(entity: Entity, variantComponent): string | null { return null } -export function useVariant(entity?: Entity): string | null { +export function getVariant(entity?: Entity): string | null { if (!entity) return null - const variantComponent = useOptionalComponent(entity, VariantComponent) - if (!variantComponent || !variantComponent.value) return null + const variantComponent = getOptionalComponent(entity, VariantComponent) + if (!variantComponent) return null - const modelComponent = useOptionalComponent(entity, ModelComponent) - const meshComponent = useOptionalComponent(entity, MeshComponent) + const modelComponent = getOptionalComponent(entity, ModelComponent) + const meshComponent = getOptionalComponent(entity, MeshComponent) if (modelComponent) return getModelVariant(entity, variantComponent, modelComponent) else if (meshComponent) return getMeshVariant(entity, variantComponent) From 30e3985453bbe74505aa3860c8d7b75855fb95f1 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 31 Jan 2024 13:59:46 -0800 Subject: [PATCH 53/67] Component typing --- .../functions/loaders/VariantFunctions.ts | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/engine/src/scene/functions/loaders/VariantFunctions.ts b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts index b93e6b7b5a..41929d8936 100644 --- a/packages/engine/src/scene/functions/loaders/VariantFunctions.ts +++ b/packages/engine/src/scene/functions/loaders/VariantFunctions.ts @@ -2,7 +2,12 @@ import { InstancedMesh, Material, Object3D, Vector3 } from 'three' import { DistanceFromCameraComponent } from '@etherealengine/spatial/src/transform/components/DistanceComponents' -import { getComponent, getMutableComponent, getOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' +import { + ComponentType, + getComponent, + getMutableComponent, + getOptionalComponent +} from '@etherealengine/ecs/src/ComponentFunctions' import { Engine } from '@etherealengine/ecs/src/Engine' import { Entity } from '@etherealengine/ecs/src/Entity' import { addOBCPlugin } from '@etherealengine/spatial/src/common/functions/OnBeforeCompilePlugin' @@ -47,18 +52,22 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -function getModelVariant(entity: Entity, variantComponent, modelComponent): string | null { - if (variantComponent.heuristic.value === 'DEVICE') { +function getModelVariant( + entity: Entity, + variantComponent: ComponentType, + modelComponent: ComponentType +): string | null { + if (variantComponent.heuristic === 'DEVICE') { const targetDevice = isMobile || isMobileXRHeadset ? 'MOBILE' : 'DESKTOP' //get model src to mobile variant src - const deviceVariant = variantComponent.levels.find((level) => level.value.metadata['device'] === targetDevice) - const modelRelativePath = pathResolver().exec(modelComponent.src.value)?.[2] - const deviceRelativePath = deviceVariant ? pathResolver().exec(deviceVariant.src.value)?.[2] : '' - if (deviceVariant && modelRelativePath !== deviceRelativePath) return deviceVariant.src.value - } else if (variantComponent.heuristic.value === 'DISTANCE') { + const deviceVariant = variantComponent.levels.find((level) => level.metadata['device'] === targetDevice) + const modelRelativePath = pathResolver().exec(modelComponent.src)?.[2] + const deviceRelativePath = deviceVariant ? pathResolver().exec(deviceVariant.src)?.[2] : '' + if (deviceVariant && modelRelativePath !== deviceRelativePath) return 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].value + 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) @@ -70,7 +79,7 @@ function getModelVariant(entity: Entity, variantComponent, modelComponent): stri return null } -function getMeshVariant(entity: Entity, variantComponent): string | null { +function getMeshVariant(entity: Entity, variantComponent: ComponentType): string | null { if (variantComponent.heuristic === 'DEVICE') { const targetDevice = isMobileXRHeadset ? 'XR' : isMobile ? 'MOBILE' : 'DESKTOP' //get model src to mobile variant src @@ -102,7 +111,7 @@ export function setModelVariant(entity: Entity) { const variantComponent = getMutableComponent(entity, VariantComponent) const modelComponent = getMutableComponent(entity, ModelComponent) - const src = getModelVariant(entity, variantComponent, modelComponent) + const src = getModelVariant(entity, variantComponent.value, modelComponent.value) if (src && modelComponent.src.value !== src) modelComponent.src.set(src) variantComponent.calculated.set(true) From 208c4b6fc9667952c1aacd06b8d67e57344000a4 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 5 Feb 2024 19:53:27 -0800 Subject: [PATCH 54/67] Avatar preview animation fixes --- .../engine/src/avatar/components/LoopAnimationComponent.ts | 2 +- packages/engine/src/avatar/systems/AvatarAnimationSystem.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/engine/src/avatar/components/LoopAnimationComponent.ts b/packages/engine/src/avatar/components/LoopAnimationComponent.ts index 5e8c634db1..44b8abac57 100644 --- a/packages/engine/src/avatar/components/LoopAnimationComponent.ts +++ b/packages/engine/src/avatar/components/LoopAnimationComponent.ts @@ -222,7 +222,7 @@ export const LoopAnimationComponent = defineComponent({ useEffect(() => { const asset = modelComponent?.asset.get(NO_PROXY) ?? null - const model = gltf.value + const model = gltf.get(NO_PROXY) if ( !model || !animComponent || diff --git a/packages/engine/src/avatar/systems/AvatarAnimationSystem.ts b/packages/engine/src/avatar/systems/AvatarAnimationSystem.ts index 3aedc3311f..6ecfd9e5d8 100644 --- a/packages/engine/src/avatar/systems/AvatarAnimationSystem.ts +++ b/packages/engine/src/avatar/systems/AvatarAnimationSystem.ts @@ -322,6 +322,10 @@ const reactor = () => { ) const manager = useMutableState(AnimationState) + useEffect(() => { + return unload + }, []) + useEffect(() => { const assets = gltfs.get(NO_PROXY) if (assets.length !== animations.length) return @@ -342,8 +346,6 @@ const reactor = () => { manager.loadedAnimations[animations[i]].set(asset) } } - - return unload }, [gltfs]) useEffect(() => { From 2892502ef3293bb0a690decdcc84b5c6949f3dae Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 6 Feb 2024 14:29:02 -0800 Subject: [PATCH 55/67] Mesh bvh hookstate and race condition fix, shadow system unmount fix --- .../src/scene/components/MeshBVHComponent.ts | 40 ++++++++++++------- .../src/scene/functions/bvhWorkerPool.ts | 11 +++-- .../engine/src/scene/systems/ShadowSystem.tsx | 6 ++- .../common/classes/GenerateMeshBVHWorker.ts | 2 + 4 files changed, 39 insertions(+), 20 deletions(-) diff --git a/packages/engine/src/scene/components/MeshBVHComponent.ts b/packages/engine/src/scene/components/MeshBVHComponent.ts index 38ba0d5138..4b6dc8d2c8 100644 --- a/packages/engine/src/scene/components/MeshBVHComponent.ts +++ b/packages/engine/src/scene/components/MeshBVHComponent.ts @@ -30,7 +30,7 @@ import { useOptionalComponent } from '@etherealengine/ecs/src/ComponentFunctions' import { useEntityContext } from '@etherealengine/ecs/src/EntityFunctions' -import { getMutableState, useHookstate } from '@etherealengine/hyperflux' +import { NO_PROXY, getMutableState, useHookstate } from '@etherealengine/hyperflux' import { RendererState } from '@etherealengine/spatial/src/renderer/RendererState' import { addObjectToGroup, removeObjectFromGroup } from '@etherealengine/spatial/src/renderer/components/GroupComponent' import { MeshComponent } from '@etherealengine/spatial/src/renderer/components/MeshComponent' @@ -54,8 +54,13 @@ const edgeMaterial = new LineBasicMaterial({ depthWrite: false }) -function ValidMeshForBVH(mesh: Mesh): boolean { - return mesh && mesh.isMesh && !(mesh as InstancedMesh).isInstancedMesh && !(mesh as SkinnedMesh).isSkinnedMesh +function ValidMeshForBVH(mesh: Mesh | undefined): boolean { + return ( + mesh !== undefined && + mesh.isMesh && + !(mesh as InstancedMesh).isInstancedMesh && + !(mesh as SkinnedMesh).isSkinnedMesh + ) } export const MeshBVHComponent = defineComponent({ @@ -88,23 +93,33 @@ export const MeshBVHComponent = defineComponent({ const visible = useOptionalComponent(entity, VisibleComponent) const model = useComponent(entity, ModelComponent) const childEntities = useHookstate(ModelComponent.entitiesInModelHierarchyState[entity]) + const abortControllerState = useHookstate(new AbortController()) + + useEffect(() => { + const abortController = abortControllerState.get(NO_PROXY) + + return () => { + abortController.abort() + } + }, []) useEffect(() => { - let aborted = false if (!component.generated.value && visible?.value) { + const abortController = abortControllerState.get(NO_PROXY) const entities = childEntities.value + const cameraOcclusion = model.cameraOcclusion.get(NO_PROXY) let toGenerate = 0 for (const currEntity of entities) { const mesh = getOptionalComponent(currEntity, MeshComponent) - if (ValidMeshForBVH(mesh!)) { + if (ValidMeshForBVH(mesh)) { toGenerate += 1 - generateMeshBVH(mesh!).then(() => { - if (!aborted) { + generateMeshBVH(mesh!, abortController.signal).then(() => { + if (!abortController.signal.aborted) { toGenerate -= 1 if (toGenerate == 0) { component.generated.set(true) } - if (model.cameraOcclusion) { + if (cameraOcclusion) { ObjectLayerMaskComponent.enableLayers(currEntity, ObjectLayers.Camera) } } @@ -112,18 +127,15 @@ export const MeshBVHComponent = defineComponent({ } } } - - return () => { - aborted = true - } }, [visible, childEntities]) useEffect(() => { if (!component.generated.value) return + const visualizers = component.visualizers.get(NO_PROXY) const remove = () => { - if (component.visualizers.value) { - for (const visualizer of component.visualizers.value) { + if (visualizers) { + for (const visualizer of visualizers) { removeObjectFromGroup(visualizer.entity, visualizer) } } diff --git a/packages/engine/src/scene/functions/bvhWorkerPool.ts b/packages/engine/src/scene/functions/bvhWorkerPool.ts index 728ce27eb6..dddb8d3344 100644 --- a/packages/engine/src/scene/functions/bvhWorkerPool.ts +++ b/packages/engine/src/scene/functions/bvhWorkerPool.ts @@ -32,7 +32,7 @@ const poolSize = 1 const bvhWorkers: GenerateMeshBVHWorker[] = [] const meshQueue: Mesh[] = [] -export function generateMeshBVH(mesh: Mesh | InstancedMesh) { +export function generateMeshBVH(mesh: Mesh | InstancedMesh, signal: AbortSignal) { if ( !mesh.isMesh || (mesh as InstancedMesh).isInstancedMesh || @@ -48,14 +48,16 @@ export function generateMeshBVH(mesh: Mesh | InstancedMesh) { } meshQueue.push(mesh) - runBVHGenerator() + runBVHGenerator(signal) return new Promise((resolve) => { ;(mesh as any).resolvePromiseBVH = resolve }) } -function runBVHGenerator() { +function runBVHGenerator(signal: AbortSignal) { + if (signal.aborted) return + for (const worker of bvhWorkers) { if (meshQueue.length < 1) { break @@ -68,8 +70,9 @@ function runBVHGenerator() { const mesh = meshQueue.shift() as Mesh worker.generate(mesh.geometry).then((bvh) => { + if (signal.aborted) return mesh.geometry.boundsTree = bvh - runBVHGenerator() + runBVHGenerator(signal) ;(mesh as any).resolvePromiseBVH && (mesh as any).resolvePromiseBVH() }) } diff --git a/packages/engine/src/scene/systems/ShadowSystem.tsx b/packages/engine/src/scene/systems/ShadowSystem.tsx index 6fd00721d9..9e03f23478 100644 --- a/packages/engine/src/scene/systems/ShadowSystem.tsx +++ b/packages/engine/src/scene/systems/ShadowSystem.tsx @@ -415,6 +415,10 @@ const reactor = () => { `${config.client.fileServer}/projects/default-project/assets/drop-shadow.png` ) + useEffect(() => { + return unload + }, []) + useEffect(() => { const texture = shadowTexture.get(NO_PROXY) if (!texture) return @@ -422,8 +426,6 @@ const reactor = () => { shadowMaterial.map = texture shadowMaterial.needsUpdate = true shadowState.set(shadowMaterial) - - return unload }, [shadowTexture]) EngineRenderer.instance.renderer.shadowMap.enabled = EngineRenderer.instance.renderer.shadowMap.autoUpdate = diff --git a/packages/spatial/src/common/classes/GenerateMeshBVHWorker.ts b/packages/spatial/src/common/classes/GenerateMeshBVHWorker.ts index 05e450acb8..d854992074 100644 --- a/packages/spatial/src/common/classes/GenerateMeshBVHWorker.ts +++ b/packages/spatial/src/common/classes/GenerateMeshBVHWorker.ts @@ -88,6 +88,8 @@ export class GenerateMeshBVHWorker { } const index = geometry.index ? Uint32Array.from(geometry.index.array) : null + // If geometry has been disposed in the time that the last mesh bvh was generated + if (!geometry.attributes.position) return const position = Float32Array.from( (geometry.attributes.position as BufferAttribute | InterleavedBufferAttribute).array ) From f68e097f51832ccee2f1fd3a1480bdbb5aea3bf9 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 6 Feb 2024 15:13:07 -0800 Subject: [PATCH 56/67] Mesh bvh fixes, object layer component fixes --- .../src/scene/components/MeshBVHComponent.ts | 21 +++++++------------ .../src/scene/functions/bvhWorkerPool.ts | 1 - .../common/classes/GenerateMeshBVHWorker.ts | 8 +++++-- .../components/ObjectLayerComponent.ts | 3 +++ 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/engine/src/scene/components/MeshBVHComponent.ts b/packages/engine/src/scene/components/MeshBVHComponent.ts index 4b6dc8d2c8..8935f5344b 100644 --- a/packages/engine/src/scene/components/MeshBVHComponent.ts +++ b/packages/engine/src/scene/components/MeshBVHComponent.ts @@ -132,16 +132,6 @@ export const MeshBVHComponent = defineComponent({ useEffect(() => { if (!component.generated.value) return - const visualizers = component.visualizers.get(NO_PROXY) - const remove = () => { - if (visualizers) { - for (const visualizer of visualizers) { - removeObjectFromGroup(visualizer.entity, visualizer) - } - } - component.visualizers.set(null) - } - if (debug.value && !component.visualizers.value) { component.visualizers.set([]) const entities = childEntities.value @@ -158,12 +148,17 @@ export const MeshBVHComponent = defineComponent({ component.visualizers.merge([meshBVHVisualizer]) } } - } else if (!debug.value) { - remove() } + const visualizers = component.visualizers.get(NO_PROXY) + return () => { - remove() + if (visualizers) { + for (const visualizer of visualizers) { + removeObjectFromGroup(visualizer.entity, visualizer) + } + } + component.visualizers.set(null) } }, [component.generated, debug]) diff --git a/packages/engine/src/scene/functions/bvhWorkerPool.ts b/packages/engine/src/scene/functions/bvhWorkerPool.ts index dddb8d3344..e393b638a2 100644 --- a/packages/engine/src/scene/functions/bvhWorkerPool.ts +++ b/packages/engine/src/scene/functions/bvhWorkerPool.ts @@ -70,7 +70,6 @@ function runBVHGenerator(signal: AbortSignal) { const mesh = meshQueue.shift() as Mesh worker.generate(mesh.geometry).then((bvh) => { - if (signal.aborted) return mesh.geometry.boundsTree = bvh runBVHGenerator(signal) ;(mesh as any).resolvePromiseBVH && (mesh as any).resolvePromiseBVH() diff --git a/packages/spatial/src/common/classes/GenerateMeshBVHWorker.ts b/packages/spatial/src/common/classes/GenerateMeshBVHWorker.ts index d854992074..f36126df1c 100644 --- a/packages/spatial/src/common/classes/GenerateMeshBVHWorker.ts +++ b/packages/spatial/src/common/classes/GenerateMeshBVHWorker.ts @@ -56,6 +56,12 @@ export class GenerateMeshBVHWorker { throw new Error('GenerateMeshBVHWorker: Already running job.') } + // If geometry has been disposed in the time that the last mesh bvh was generated + if (!geometry.attributes.position) + return new Promise((_, reject) => { + reject() + }) + const { worker } = this this.running = true @@ -88,8 +94,6 @@ export class GenerateMeshBVHWorker { } const index = geometry.index ? Uint32Array.from(geometry.index.array) : null - // If geometry has been disposed in the time that the last mesh bvh was generated - if (!geometry.attributes.position) return const position = Float32Array.from( (geometry.attributes.position as BufferAttribute | InterleavedBufferAttribute).array ) diff --git a/packages/spatial/src/renderer/components/ObjectLayerComponent.ts b/packages/spatial/src/renderer/components/ObjectLayerComponent.ts index 230f0aa416..2417154b5c 100644 --- a/packages/spatial/src/renderer/components/ObjectLayerComponent.ts +++ b/packages/spatial/src/renderer/components/ObjectLayerComponent.ts @@ -23,6 +23,7 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ +import { entityExists } from '@etherealengine/ecs' import { defineComponent, hasComponent, @@ -86,6 +87,7 @@ export const ObjectLayerMaskComponent = defineComponent({ }, enableLayer(entity: Entity, layer: number) { + if (!entityExists(entity)) return if (!hasComponent(entity, ObjectLayerMaskComponent)) setComponent(entity, ObjectLayerMaskComponent) setComponent(entity, ObjectLayerComponents[layer]) }, @@ -98,6 +100,7 @@ export const ObjectLayerMaskComponent = defineComponent({ }, disableLayer(entity: Entity, layer: number) { + if (!entityExists(entity)) return if (!hasComponent(entity, ObjectLayerMaskComponent)) setComponent(entity, ObjectLayerMaskComponent) removeComponent(entity, ObjectLayerComponents[layer]) }, From 9c3fdcd1003bc6ff0f4cb683c72ce6aaeba361ed Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Tue, 6 Feb 2024 20:00:14 -0800 Subject: [PATCH 57/67] Preliminary fix for avatar switching --- .../common/components/AvatarPreview/index.tsx | 22 +++------ .../src/scene/components/ModelComponent.tsx | 47 +++++++++---------- 2 files changed, 29 insertions(+), 40 deletions(-) diff --git a/packages/client-core/src/common/components/AvatarPreview/index.tsx b/packages/client-core/src/common/components/AvatarPreview/index.tsx index 03d5444899..f62824c01b 100644 --- a/packages/client-core/src/common/components/AvatarPreview/index.tsx +++ b/packages/client-core/src/common/components/AvatarPreview/index.tsx @@ -42,7 +42,7 @@ import { SxProps, Theme } from '@mui/material/styles' import styles from './index.module.scss' import { EntityUUID } from '@etherealengine/common/src/interfaces/EntityUUID' -import { hasComponent, removeComponent, setComponent } from '@etherealengine/ecs' +import { setComponent } from '@etherealengine/ecs' import { defaultAnimationPath, preloadedAnimations } from '@etherealengine/engine/src/avatar/animation/Util' import { LoopAnimationComponent } from '@etherealengine/engine/src/avatar/components/LoopAnimationComponent' import { AssetPreviewCameraComponent } from '@etherealengine/engine/src/camera/components/AssetPreviewCameraComponent' @@ -68,16 +68,10 @@ interface Props { const AvatarPreview = ({ fill, avatarUrl, sx, onAvatarError, onAvatarLoaded }: Props) => { const { t } = useTranslation() const panelRef = useRef() as React.MutableRefObject - useRender3DPanelSystem(panelRef) - - useEffect(() => { - loadAvatarPreview() - }, [avatarUrl]) - const renderPanelState = getMutableState(PreviewPanelRendererState) - const loadAvatarPreview = () => { + useEffect(() => { if (!avatarUrl) return const renderPanelEntities = renderPanelState.entities[panelRef.current.id] @@ -86,20 +80,18 @@ const AvatarPreview = ({ fill, avatarUrl, sx, onAvatarError, onAvatarLoaded }: P setComponent(entity, UUIDComponent, uuid) setComponent(entity, NameComponent, '3D Preview Entity') - if (hasComponent(entity, LoopAnimationComponent)) removeComponent(entity, LoopAnimationComponent) - if (hasComponent(entity, ModelComponent)) removeComponent(entity, ModelComponent) - - setComponent(entity, VisibleComponent, true) - ObjectLayerMaskComponent.setLayer(entity, ObjectLayers.AssetPreview) - setComponent(entity, ModelComponent, { src: avatarUrl, convertToVRM: true }) setComponent(entity, LoopAnimationComponent, { animationPack: defaultAnimationPath + preloadedAnimations.locomotion + '.glb', activeClipIndex: 5 }) + setComponent(entity, ModelComponent, { src: avatarUrl, convertToVRM: true, manageScene: false }) + + setComponent(entity, VisibleComponent, true) + ObjectLayerMaskComponent.setLayer(entity, ObjectLayers.AssetPreview) setComponent(entity, EnvmapComponent, { type: EnvMapSourceType.Skybox }) const cameraEntity = renderPanelEntities[PanelEntities.camera].value setComponent(cameraEntity, AssetPreviewCameraComponent, { targetModelEntity: entity }) - } + }, [avatarUrl]) return ( diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index b3127b7382..bf9cf11f7b 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -82,6 +82,7 @@ export const ModelComponent = defineComponent({ cameraOcclusion: true, /** optional, only for bone matchable avatars */ convertToVRM: false as boolean, + manageScene: true as boolean, // internal scene: null as Scene | null, asset: null as VRM | GLTF | null @@ -92,7 +93,8 @@ export const ModelComponent = defineComponent({ return { src: component.src.value, cameraOcclusion: component.cameraOcclusion.value, - convertToVRM: component.convertToVRM.value + convertToVRM: component.convertToVRM.value, + manageScene: component.manageScene.value } }, @@ -103,6 +105,7 @@ export const ModelComponent = defineComponent({ component.cameraOcclusion.set(!(json as any).avoidCameraOcclusion) if (typeof json.cameraOcclusion === 'boolean') component.cameraOcclusion.set(json.cameraOcclusion) if (typeof json.convertToVRM === 'boolean') component.convertToVRM.set(json.convertToVRM) + if (typeof json.manageScene === 'boolean') component.manageScene.set(json.manageScene) /** * Add SceneAssetPendingTagComponent to tell scene loading system we should wait for this asset to load @@ -131,18 +134,10 @@ function ModelReactor(): JSX.Element { /** @todo this is a hack */ const override = !isAvaturn(modelComponent.value.src) ? undefined : AssetType.glB - const [gltf, unload, error, progress] = useGLTF( - modelComponent.value.src, - entity, - { - forceAssetType: override, - ignoreDisposeGeometry: modelComponent.cameraOcclusion.value - }, - () => { - const uuid = getModelSceneID(entity) - getMutableState(SceneState).scenes[uuid].set(none) - } - ) + const [gltf, unload, error, progress] = useGLTF(modelComponent.value.src, entity, { + forceAssetType: override, + ignoreDisposeGeometry: modelComponent.cameraOcclusion.value + }) useEffect(() => { /* unload should only be called when the component is unmounted @@ -243,17 +238,19 @@ function ModelReactor(): JSX.Element { const loadedJsonHierarchy = parseGLTFModel(entity, asset.scene as Scene) const uuid = getModelSceneID(entity) - SceneState.loadScene(uuid, { - scene: { - entities: loadedJsonHierarchy, - root: getComponent(entity, UUIDComponent), - version: 0 - }, - scenePath: uuid, - name: '', - project: '', - thumbnailUrl: '' - }) + if (modelComponent.manageScene) { + SceneState.loadScene(uuid, { + scene: { + entities: loadedJsonHierarchy, + root: getComponent(entity, UUIDComponent), + version: 0 + }, + scenePath: uuid, + name: '', + project: '', + thumbnailUrl: '' + }) + } if (!hasComponent(entity, AvatarRigComponent)) { //if this is not an avatar, add bbox snap @@ -261,7 +258,7 @@ function ModelReactor(): JSX.Element { } return () => { - getMutableState(SceneState).scenes[uuid].set(none) + if (modelComponent.manageScene) getMutableState(SceneState).scenes[uuid].set(none) } }, [modelComponent.scene]) From 7714d55272422a5e01f66ec4882bee0f5433c992 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 7 Feb 2024 14:52:11 -0800 Subject: [PATCH 58/67] Fix errors when selecting a new avatar --- .../engine/src/assets/classes/AssetLoader.ts | 11 ------- .../src/scene/components/ModelComponent.tsx | 32 ++++++++----------- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/packages/engine/src/assets/classes/AssetLoader.ts b/packages/engine/src/assets/classes/AssetLoader.ts index 1a03356b96..10b60ce085 100644 --- a/packages/engine/src/assets/classes/AssetLoader.ts +++ b/packages/engine/src/assets/classes/AssetLoader.ts @@ -88,14 +88,6 @@ const onUploadDropBuffer = () => this.array = new this.array.constructor(1) } -const onTextureUploadDropSource = () => - function (this: Texture) { - // source.data can't be null because the WebGLRenderer checks for it - this.source.data = { width: this.source.data.width, height: this.source.data.height, __deleted: true } - this.mipmaps.map((b) => delete b.data) - this.mipmaps = [] - } - export const cleanupAllMeshData = (child: Mesh, args: LoadingArgs) => { if (getState(EngineState).isEditor || !child.isMesh) return const geo = child.geometry as BufferGeometry @@ -105,9 +97,6 @@ export const cleanupAllMeshData = (child: Mesh, args: LoadingArgs) => { for (const name in attributes) (attributes[name] as BufferAttribute).onUploadCallback = onUploadDropBuffer() if (geo.index) geo.index.onUploadCallback = onUploadDropBuffer() } - Object.entries(mat) - .filter(([k, v]: [keyof typeof mat, Texture]) => v?.isTexture) - .map(([_, v]) => (v.onUpdate = onTextureUploadDropSource())) } const processModelAsset = (asset: Mesh, args: LoadingArgs): void => { diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index bf9cf11f7b..bfb9d1bad3 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -82,7 +82,6 @@ export const ModelComponent = defineComponent({ cameraOcclusion: true, /** optional, only for bone matchable avatars */ convertToVRM: false as boolean, - manageScene: true as boolean, // internal scene: null as Scene | null, asset: null as VRM | GLTF | null @@ -93,8 +92,7 @@ export const ModelComponent = defineComponent({ return { src: component.src.value, cameraOcclusion: component.cameraOcclusion.value, - convertToVRM: component.convertToVRM.value, - manageScene: component.manageScene.value + convertToVRM: component.convertToVRM.value } }, @@ -105,7 +103,6 @@ export const ModelComponent = defineComponent({ component.cameraOcclusion.set(!(json as any).avoidCameraOcclusion) if (typeof json.cameraOcclusion === 'boolean') component.cameraOcclusion.set(json.cameraOcclusion) if (typeof json.convertToVRM === 'boolean') component.convertToVRM.set(json.convertToVRM) - if (typeof json.manageScene === 'boolean') component.manageScene.set(json.manageScene) /** * Add SceneAssetPendingTagComponent to tell scene loading system we should wait for this asset to load @@ -237,20 +234,17 @@ function ModelReactor(): JSX.Element { const loadedJsonHierarchy = parseGLTFModel(entity, asset.scene as Scene) const uuid = getModelSceneID(entity) - - if (modelComponent.manageScene) { - SceneState.loadScene(uuid, { - scene: { - entities: loadedJsonHierarchy, - root: getComponent(entity, UUIDComponent), - version: 0 - }, - scenePath: uuid, - name: '', - project: '', - thumbnailUrl: '' - }) - } + SceneState.loadScene(uuid, { + scene: { + entities: loadedJsonHierarchy, + root: getComponent(entity, UUIDComponent), + version: 0 + }, + scenePath: uuid, + name: '', + project: '', + thumbnailUrl: '' + }) if (!hasComponent(entity, AvatarRigComponent)) { //if this is not an avatar, add bbox snap @@ -258,7 +252,7 @@ function ModelReactor(): JSX.Element { } return () => { - if (modelComponent.manageScene) getMutableState(SceneState).scenes[uuid].set(none) + getMutableState(SceneState).scenes[uuid].set(none) } }, [modelComponent.scene]) From 2a02c329cf3ca2b4295f96147ab3c9e178c56d34 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 7 Feb 2024 19:10:24 -0800 Subject: [PATCH 59/67] track textures by source uuid, remove dispose calls, mesh bvh hookstate fix, portal component reactivity fix --- .../src/assets/loaders/gltf/GLTFLoader.js | 2 +- .../engine/src/assets/state/ResourceState.ts | 48 +++++++++++++++---- .../src/scene/components/MeshBVHComponent.ts | 17 ++----- .../src/scene/components/PortalComponent.ts | 16 +++---- .../src/scene/systems/SceneObjectSystem.tsx | 17 ------- 5 files changed, 54 insertions(+), 46 deletions(-) diff --git a/packages/engine/src/assets/loaders/gltf/GLTFLoader.js b/packages/engine/src/assets/loaders/gltf/GLTFLoader.js index 23bcd63f97..c6c9dfe4b3 100755 --- a/packages/engine/src/assets/loaders/gltf/GLTFLoader.js +++ b/packages/engine/src/assets/loaders/gltf/GLTFLoader.js @@ -3314,7 +3314,7 @@ class GLTFParser { parser.associations.set( texture, { textures: textureIndex } ); if(parser.fileLoader.manager.itemEndFor) - parser.fileLoader.manager.itemEndFor(parser.options.url, ResourceType.Texture, texture.uuid, texture) + parser.fileLoader.manager.itemEndFor(parser.options.url, ResourceType.Texture, texture.source.uuid, texture) return texture; diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index b31419e43b..87044562f2 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -53,6 +53,16 @@ export enum ResourceType { Unknown } +const resourceTypeName = { + [ResourceType.GLTF]: 'GLTF', + [ResourceType.Texture]: 'Texture', + [ResourceType.Geometry]: 'Geometry', + [ResourceType.Material]: 'Material', + [ResourceType.ECSData]: 'ECSData', + [ResourceType.Audio]: 'Audio', + [ResourceType.Unknown]: 'Unknown' +} + export type AssetType = GLTF | Texture | CompressedTexture | Geometry | Material type BaseMetadata = { @@ -79,6 +89,13 @@ type Resource = { metadata: Metadata } +const debug = false +const debugLog = debug + ? (message?: any, ...optionalParams: any[]) => { + console.log(message) + } + : (message?: any, ...optionalParams: any[]) => {} + export const ResourceState = defineState({ name: 'ResourceManagerState', initial: () => ({ @@ -126,8 +143,11 @@ const onStart = (url: string, loaded: number, total: number) => {} const onLoad = () => { const totalSize = getCurrentSizeOfResources() const totalVerts = getCurrentVertCountOfResources() - console.log('Loaded: ' + totalSize + ' bytes of resources') - console.log(totalVerts + ' Vertices') + debugLog('Loaded: ' + totalSize + ' bytes of resources') + debugLog(totalVerts + ' Vertices') + + //@ts-ignore + window.resources = getState(ResourceState) } const onItemLoadedFor = (url: string, resourceType: ResourceType, id: string, asset: T) => { @@ -138,6 +158,16 @@ const onItemLoadedFor = (url: string, resourceType: Resourc console.warn('ResourceManager:loadedFor asset loaded for asset that is not loaded: ' + url) return } + + debugLog( + 'ResourceManager:loadedFor loading asset of type ' + + resourceTypeName[resourceType] + + ' with ID: ' + + id + + ' for asset at url: ' + + url + ) + if (!referencedAssets[id].value) { referencedAssets.merge({ [id]: [] @@ -301,7 +331,7 @@ const load = ( const resource = resources[url] const callbacks = Callbacks[resourceType] callbacks.onStart(resource) - // console.log('ResourceManager:load Loading resource: ' + url + ' for entity: ' + entity) + debugLog('ResourceManager:load Loading resource: ' + url + ' for entity: ' + entity) AssetLoader.load( url, args, @@ -332,7 +362,7 @@ const unload = (url: string, entity: Entity) => { return } - // console.log('ResourceManager:unload Unloading resource: ' + url + ' for entity: ' + entity) + debugLog('ResourceManager:unload Unloading resource: ' + url + ' for entity: ' + entity) const resource = resources[url] resource.references.set((entities) => { const index = entities.indexOf(entity) @@ -343,9 +373,9 @@ const unload = (url: string, entity: Entity) => { }) if (resource.references.length == 0) { - // console.log("Before Removing Resources", JSON.stringify(getRendererInfo())) + debugLog('Before Removing Resources', JSON.stringify(getRendererInfo())) removeResource(url) - // console.log("After Removing Resources", JSON.stringify(getRendererInfo())) + debugLog('After Removing Resources', JSON.stringify(getRendererInfo())) } } @@ -377,9 +407,11 @@ const removeResource = (id: string) => { return } - // console.log('ResourceManager:removeResource: Removing resource: ' + id) - Cache.remove(id) const resource = resources[id] + debugLog( + 'ResourceManager:removeResource: Removing ' + resourceTypeName[resource.type.value] + ' resource with ID: ' + id + ) + Cache.remove(id) removeReferencedResources(resource) const asset = resource.asset.get(NO_PROXY) diff --git a/packages/engine/src/scene/components/MeshBVHComponent.ts b/packages/engine/src/scene/components/MeshBVHComponent.ts index 8935f5344b..b93749e2e1 100644 --- a/packages/engine/src/scene/components/MeshBVHComponent.ts +++ b/packages/engine/src/scene/components/MeshBVHComponent.ts @@ -68,21 +68,17 @@ export const MeshBVHComponent = defineComponent({ onInit(entity) { return { - generated: false, - visualizers: null as Object3D[] | null + generated: false } }, onSet(entity, component, json) { if (!json) return - - if (json.visualizers) component.visualizers.set(json.visualizers) }, toJSON(entity, component) { return { - generated: component.generated.value, - visualizers: component.visualizers.value + generated: component.generated.value } }, @@ -132,8 +128,8 @@ export const MeshBVHComponent = defineComponent({ useEffect(() => { if (!component.generated.value) return - if (debug.value && !component.visualizers.value) { - component.visualizers.set([]) + const visualizers = [] as Object3D[] + if (debug.value) { const entities = childEntities.value for (const currEntity of entities) { const mesh = getOptionalComponent(currEntity, MeshComponent) @@ -145,20 +141,17 @@ export const MeshBVHComponent = defineComponent({ meshBVHVisualizer.update() addObjectToGroup(currEntity, meshBVHVisualizer) - component.visualizers.merge([meshBVHVisualizer]) + visualizers.push(meshBVHVisualizer) } } } - const visualizers = component.visualizers.get(NO_PROXY) - return () => { if (visualizers) { for (const visualizer of visualizers) { removeObjectFromGroup(visualizer.entity, visualizer) } } - component.visualizers.set(null) } }, [component.generated, debug]) diff --git a/packages/engine/src/scene/components/PortalComponent.ts b/packages/engine/src/scene/components/PortalComponent.ts index ceb028a7ae..acd45be261 100644 --- a/packages/engine/src/scene/components/PortalComponent.ts +++ b/packages/engine/src/scene/components/PortalComponent.ts @@ -218,7 +218,6 @@ export const PortalComponent = defineComponent({ return () => { removeObjectFromGroup(entity, portalMesh) - portalComponent.mesh.set(null) } }, [portalComponent.previewType]) @@ -231,21 +230,22 @@ export const PortalComponent = defineComponent({ const [textureState, unload] = useTexture(portalDetails.value?.previewImageURL || '', entity) useEffect(() => { - if (!textureState.value) return return unload - }, [textureState]) + }, []) useEffect(() => { - if (!portalDetails.value?.previewImageURL) return - portalComponent.remoteSpawnPosition.value.copy(portalDetails.value.spawnPosition) - portalComponent.remoteSpawnRotation.value.copy(portalDetails.value.spawnRotation) - const texture = textureState.get(NO_PROXY) if (!texture || !portalComponent.mesh.value) return portalComponent.mesh.value.material.map = texture portalComponent.mesh.value.material.needsUpdate = true - }, [portalDetails, portalComponent.mesh, textureState]) + }, [textureState, portalComponent.mesh]) + + useEffect(() => { + if (!portalDetails.value?.previewImageURL) return + portalComponent.remoteSpawnPosition.value.copy(portalDetails.value.spawnPosition) + portalComponent.remoteSpawnRotation.value.copy(portalDetails.value.spawnRotation) + }, [portalDetails]) useEffect(() => { if (!isClient) return diff --git a/packages/engine/src/scene/systems/SceneObjectSystem.tsx b/packages/engine/src/scene/systems/SceneObjectSystem.tsx index 4c733eb24b..43408faaf8 100644 --- a/packages/engine/src/scene/systems/SceneObjectSystem.tsx +++ b/packages/engine/src/scene/systems/SceneObjectSystem.tsx @@ -82,23 +82,6 @@ export const disposeMaterial = (material: Material) => { } export const disposeObject3D = (obj: Object3D) => { - const mesh = obj as Mesh - - if (mesh.material) { - if (Array.isArray(mesh.material)) { - mesh.material.forEach(disposeMaterial) - } else { - disposeMaterial(mesh.material) - } - } - - if (mesh.geometry) { - mesh.geometry.dispose() - for (const key in mesh.geometry.attributes) { - mesh.geometry.deleteAttribute(key) - } - } - const skinnedMesh = obj as SkinnedMesh if (skinnedMesh.isSkinnedMesh) { skinnedMesh.skeleton?.dispose() From 2f3b46bd57610b14b02788736ddd94766f233439 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Wed, 7 Feb 2024 19:13:38 -0800 Subject: [PATCH 60/67] debug change --- packages/engine/src/assets/state/ResourceState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 87044562f2..07b8b30496 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -147,7 +147,7 @@ const onLoad = () => { debugLog(totalVerts + ' Vertices') //@ts-ignore - window.resources = getState(ResourceState) + if (debug) window.resources = getState(ResourceState) } const onItemLoadedFor = (url: string, resourceType: ResourceType, id: string, asset: T) => { From b1330e126a4847eefe37ccff6d306f8747d38c0f Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Thu, 8 Feb 2024 11:39:09 -0800 Subject: [PATCH 61/67] Update mesh bvh --- packages/engine/package.json | 2 +- packages/engine/src/scene/components/MeshBVHComponent.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/engine/package.json b/packages/engine/package.json index b73fff4f64..a553206622 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -69,7 +69,7 @@ "sift": "^17.0.1", "simplex-noise": "^4.0.1", "three": "0.158.0", - "three-mesh-bvh": "^0.6.8", + "three-mesh-bvh": "^0.7.1", "three.quarks": "0.11.1", "troika-three-text": "^0.49.0", "ts-matches": "5.3.0", diff --git a/packages/engine/src/scene/components/MeshBVHComponent.ts b/packages/engine/src/scene/components/MeshBVHComponent.ts index b93749e2e1..b54b88287c 100644 --- a/packages/engine/src/scene/components/MeshBVHComponent.ts +++ b/packages/engine/src/scene/components/MeshBVHComponent.ts @@ -39,7 +39,7 @@ import { VisibleComponent } from '@etherealengine/spatial/src/renderer/component import { ObjectLayers } from '@etherealengine/spatial/src/renderer/constants/ObjectLayers' import { useEffect } from 'react' import { BufferGeometry, InstancedMesh, LineBasicMaterial, Mesh, Object3D, SkinnedMesh } from 'three' -import { MeshBVHVisualizer, acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh' +import { MeshBVHHelper, acceleratedRaycast, computeBoundsTree, disposeBoundsTree } from 'three-mesh-bvh' import { generateMeshBVH } from '../functions/bvhWorkerPool' import { ModelComponent } from './ModelComponent' @@ -134,7 +134,7 @@ export const MeshBVHComponent = defineComponent({ for (const currEntity of entities) { const mesh = getOptionalComponent(currEntity, MeshComponent) if (ValidMeshForBVH(mesh!)) { - const meshBVHVisualizer = new MeshBVHVisualizer(mesh!) + const meshBVHVisualizer = new MeshBVHHelper(mesh!) meshBVHVisualizer.edgeMaterial = edgeMaterial meshBVHVisualizer.depth = 20 meshBVHVisualizer.displayParents = false From ba0aeb42ce8f74094f5fa8873987cb47c55e2fd3 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Thu, 8 Feb 2024 13:55:56 -0800 Subject: [PATCH 62/67] remove property --- .../client-core/src/common/components/AvatarPreview/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client-core/src/common/components/AvatarPreview/index.tsx b/packages/client-core/src/common/components/AvatarPreview/index.tsx index f62824c01b..5a3928c212 100644 --- a/packages/client-core/src/common/components/AvatarPreview/index.tsx +++ b/packages/client-core/src/common/components/AvatarPreview/index.tsx @@ -84,7 +84,7 @@ const AvatarPreview = ({ fill, avatarUrl, sx, onAvatarError, onAvatarLoaded }: P animationPack: defaultAnimationPath + preloadedAnimations.locomotion + '.glb', activeClipIndex: 5 }) - setComponent(entity, ModelComponent, { src: avatarUrl, convertToVRM: true, manageScene: false }) + setComponent(entity, ModelComponent, { src: avatarUrl, convertToVRM: true }) setComponent(entity, VisibleComponent, true) ObjectLayerMaskComponent.setLayer(entity, ObjectLayers.AssetPreview) From d4a4906e8bba33291b3e8e26eceb8e2290ba7d90 Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Thu, 8 Feb 2024 14:00:06 -0800 Subject: [PATCH 63/67] test fix --- packages/engine/src/assets/state/ResourceState.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index 07b8b30496..aabc057b18 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -373,9 +373,9 @@ const unload = (url: string, entity: Entity) => { }) if (resource.references.length == 0) { - debugLog('Before Removing Resources', JSON.stringify(getRendererInfo())) + debugLog('Before Removing Resources', debug && JSON.stringify(getRendererInfo())) removeResource(url) - debugLog('After Removing Resources', JSON.stringify(getRendererInfo())) + debugLog('After Removing Resources', debug && JSON.stringify(getRendererInfo())) } } From 1d5d89faae8a1e2541472ee55fb3df532e62daad Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 12 Feb 2024 15:22:56 -0800 Subject: [PATCH 64/67] Fix for multiple avtars shown in avatar selection panel --- .../src/common/components/AvatarPreview/index.tsx | 4 +++- .../components/Panel3D/useRender3DPanelSystem.tsx | 3 ++- .../engine/src/scene/components/ModelComponent.tsx | 12 ++++++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/client-core/src/common/components/AvatarPreview/index.tsx b/packages/client-core/src/common/components/AvatarPreview/index.tsx index 5a3928c212..3adbc688a3 100644 --- a/packages/client-core/src/common/components/AvatarPreview/index.tsx +++ b/packages/client-core/src/common/components/AvatarPreview/index.tsx @@ -42,7 +42,7 @@ import { SxProps, Theme } from '@mui/material/styles' import styles from './index.module.scss' import { EntityUUID } from '@etherealengine/common/src/interfaces/EntityUUID' -import { setComponent } from '@etherealengine/ecs' +import { setComponent, UndefinedEntity } from '@etherealengine/ecs' import { defaultAnimationPath, preloadedAnimations } from '@etherealengine/engine/src/avatar/animation/Util' import { LoopAnimationComponent } from '@etherealengine/engine/src/avatar/components/LoopAnimationComponent' import { AssetPreviewCameraComponent } from '@etherealengine/engine/src/camera/components/AssetPreviewCameraComponent' @@ -55,6 +55,7 @@ import { UUIDComponent } from '@etherealengine/spatial/src/common/UUIDComponent' import { ObjectLayerMaskComponent } from '@etherealengine/spatial/src/renderer/components/ObjectLayerComponent' import { VisibleComponent } from '@etherealengine/spatial/src/renderer/components/VisibleComponent' import { ObjectLayers } from '@etherealengine/spatial/src/renderer/constants/ObjectLayers' +import { EntityTreeComponent } from '@etherealengine/spatial/src/transform/components/EntityTree' import { MathUtils } from 'three' interface Props { @@ -85,6 +86,7 @@ const AvatarPreview = ({ fill, avatarUrl, sx, onAvatarError, onAvatarLoaded }: P activeClipIndex: 5 }) setComponent(entity, ModelComponent, { src: avatarUrl, convertToVRM: true }) + setComponent(entity, EntityTreeComponent, { parentEntity: UndefinedEntity }) setComponent(entity, VisibleComponent, true) ObjectLayerMaskComponent.setLayer(entity, ObjectLayers.AssetPreview) diff --git a/packages/client-core/src/user/components/Panel3D/useRender3DPanelSystem.tsx b/packages/client-core/src/user/components/Panel3D/useRender3DPanelSystem.tsx index f49657a29c..4a303190e7 100644 --- a/packages/client-core/src/user/components/Panel3D/useRender3DPanelSystem.tsx +++ b/packages/client-core/src/user/components/Panel3D/useRender3DPanelSystem.tsx @@ -39,7 +39,7 @@ import { removeEntity, setComponent } from '@etherealengine/ecs' -import { defineState, getMutableState, none } from '@etherealengine/hyperflux' +import { NO_PROXY, defineState, getMutableState, none } from '@etherealengine/hyperflux' import { DirectionalLightComponent, TransformComponent } from '@etherealengine/spatial' import { CameraComponent } from '@etherealengine/spatial/src/camera/components/CameraComponent' import { @@ -156,6 +156,7 @@ export function useRender3DPanelSystem(panel: React.MutableRefObject value === id) rendererState.entities[id].set(none) + rendererState.renderers[id].get(NO_PROXY).dispose() rendererState.renderers[id].set(none) rendererState.ids[thisIdIndex].set(none) } diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index 791945d39e..0534dd5ece 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -130,8 +130,8 @@ function ModelReactor(): JSX.Element { const modelComponent = useComponent(entity, ModelComponent) /** @todo this is a hack */ - const override = !isAvaturn(modelComponent.value.src) ? undefined : AssetType.glB - const [gltf, unload, error, progress] = useGLTF(modelComponent.value.src, entity, { + const override = !isAvaturn(modelComponent.src.value) ? undefined : AssetType.glB + const [gltf, unload, error, progress] = useGLTF(modelComponent.src.value, entity, { forceAssetType: override, ignoreDisposeGeometry: modelComponent.cameraOcclusion.value }) @@ -160,7 +160,6 @@ function ModelReactor(): JSX.Element { console.error(err) addError(entity, ModelComponent, 'INVALID_SOURCE', err.message) - removeComponent(entity, SceneAssetPendingTagComponent) SceneAssetPendingTagComponent.removeResource(entity, modelComponent.src.value) }, [error]) @@ -214,8 +213,8 @@ function ModelReactor(): JSX.Element { // update scene useEffect(() => { - const scene = getComponent(entity, ModelComponent).scene - const asset = getComponent(entity, ModelComponent).asset + const scene = modelComponent.scene.value + const asset = modelComponent.asset.value if (!scene || !asset) return @@ -224,6 +223,7 @@ function ModelReactor(): JSX.Element { const loadedJsonHierarchy = parseGLTFModel(entity, asset.scene as Scene) const uuid = getModelSceneID(entity) + SceneState.loadScene(uuid, { scene: { entities: loadedJsonHierarchy, @@ -248,7 +248,7 @@ function ModelReactor(): JSX.Element { addError(entity, ModelComponent, 'LOADING_ERROR', 'Error compiling model') }) .finally(() => { - SceneAssetPendingTagComponent.removeResource(entity, modelComponent.value.src) + SceneAssetPendingTagComponent.removeResource(entity, modelComponent.src.value) }) return () => { From dae6e7429b859c39437de011e4ea0045ef5573ae Mon Sep 17 00:00:00 2001 From: Michael Estes Date: Mon, 12 Feb 2024 17:39:43 -0800 Subject: [PATCH 65/67] Avatar selection working --- .../Panel3D/useRender3DPanelSystem.tsx | 32 +++++++++---------- .../engine/src/assets/state/ResourceState.ts | 10 +++++- .../src/scene/systems/SceneObjectSystem.tsx | 16 ++++++++++ 3 files changed, 41 insertions(+), 17 deletions(-) diff --git a/packages/client-core/src/user/components/Panel3D/useRender3DPanelSystem.tsx b/packages/client-core/src/user/components/Panel3D/useRender3DPanelSystem.tsx index e3866706bf..0c408ec90b 100644 --- a/packages/client-core/src/user/components/Panel3D/useRender3DPanelSystem.tsx +++ b/packages/client-core/src/user/components/Panel3D/useRender3DPanelSystem.tsx @@ -27,7 +27,6 @@ import React, { useEffect } from 'react' import { Euler, Quaternion, Vector3, WebGLRenderer } from 'three' import { - Engine, Entity, PresentationSystemGroup, UndefinedEntity, @@ -35,6 +34,7 @@ import { defineQuery, defineSystem, getComponent, + getOptionalComponent, removeComponent, removeEntity, setComponent @@ -49,6 +49,7 @@ import { import { NameComponent } from '@etherealengine/spatial/src/common/NameComponent' import { InputSourceComponent } from '@etherealengine/spatial/src/input/components/InputSourceComponent' import { addClientInputListeners } from '@etherealengine/spatial/src/input/systems/ClientInputSystem' +import { GroupComponent } from '@etherealengine/spatial/src/renderer/components/GroupComponent' import { ObjectLayerComponents, ObjectLayerMaskComponent @@ -194,21 +195,20 @@ export const render3DPanelSystem = defineSystem({ iterateEntityNode(previewEntity, (entity) => { setComponent(entity, ObjectLayerComponents[ObjectLayers.AssetPreview]) }) - const cameraComponent = getComponent(cameraEntity, CameraComponent) - // sync with view camera - const viewCamera = cameraComponent.cameras[0] - viewCamera.projectionMatrix.copy(cameraComponent.projectionMatrix) - viewCamera.quaternion.copy(cameraComponent.quaternion) - viewCamera.position.copy(cameraComponent.position) - viewCamera.layers.mask = getComponent(cameraEntity, ObjectLayerMaskComponent) - // hack to make the background transparent for the preview - const lastBackground = Engine.instance.scene.background - Engine.instance.scene.background = null - rendererState.renderers[id].value.render(Engine.instance.scene, viewCamera) - Engine.instance.scene.background = lastBackground - iterateEntityNode(previewEntity, (entity) => { - removeComponent(entity, ObjectLayerComponents[ObjectLayers.AssetPreview]) - }) + const group = getOptionalComponent(previewEntity, GroupComponent) + if (group && group[0]) { + const cameraComponent = getComponent(cameraEntity, CameraComponent) + // sync with view camera + const viewCamera = cameraComponent.cameras[0] + viewCamera.projectionMatrix.copy(cameraComponent.projectionMatrix) + viewCamera.quaternion.copy(cameraComponent.quaternion) + viewCamera.position.copy(cameraComponent.position) + viewCamera.layers.mask = getComponent(cameraEntity, ObjectLayerMaskComponent) + rendererState.renderers[id].value.render(group[0], viewCamera) + iterateEntityNode(previewEntity, (entity) => { + removeComponent(entity, ObjectLayerComponents[ObjectLayers.AssetPreview]) + }) + } } } } diff --git a/packages/engine/src/assets/state/ResourceState.ts b/packages/engine/src/assets/state/ResourceState.ts index aabc057b18..6d9ef86f7a 100644 --- a/packages/engine/src/assets/state/ResourceState.ts +++ b/packages/engine/src/assets/state/ResourceState.ts @@ -427,7 +427,15 @@ const removeResource = (id: string) => { ;(asset as Geometry).dispose() break case ResourceType.Material: - ;(asset as Material).dispose() + { + const material = asset as Material + for (const [key, val] of Object.entries(material) as [string, Texture][]) { + if (val && typeof val.dispose === 'function') { + val.dispose() + } + } + material.dispose() + } break case ResourceType.ECSData: break diff --git a/packages/engine/src/scene/systems/SceneObjectSystem.tsx b/packages/engine/src/scene/systems/SceneObjectSystem.tsx index 43408faaf8..21582fd75e 100644 --- a/packages/engine/src/scene/systems/SceneObjectSystem.tsx +++ b/packages/engine/src/scene/systems/SceneObjectSystem.tsx @@ -82,6 +82,22 @@ export const disposeMaterial = (material: Material) => { } export const disposeObject3D = (obj: Object3D) => { + const mesh = obj as Mesh + if (mesh.material) { + if (Array.isArray(mesh.material)) { + mesh.material.forEach(disposeMaterial) + } else { + disposeMaterial(mesh.material) + } + } + + if (mesh.geometry) { + mesh.geometry.dispose() + for (const key in mesh.geometry.attributes) { + mesh.geometry.deleteAttribute(key) + } + } + const skinnedMesh = obj as SkinnedMesh if (skinnedMesh.isSkinnedMesh) { skinnedMesh.skeleton?.dispose() From 04d1dc003e553dac1f4153f88115b714048ec3ea Mon Sep 17 00:00:00 2001 From: HexaField Date: Wed, 14 Feb 2024 14:25:58 +1100 Subject: [PATCH 66/67] bug fixes --- .../engine/src/scene/components/ModelComponent.tsx | 14 +++++++++++--- .../engine/src/scene/components/PortalComponent.ts | 8 +------- .../src/networking/state/EntityNetworkState.tsx | 4 ++-- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index 0534dd5ece..994fd737b2 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -26,7 +26,15 @@ Ethereal Engine. All Rights Reserved. import { useEffect } from 'react' import { AnimationMixer, BoxGeometry, CapsuleGeometry, CylinderGeometry, Group, Scene, SphereGeometry } from 'three' -import { NO_PROXY, createState, getMutableState, getState, none, useHookstate } from '@etherealengine/hyperflux' +import { + NO_PROXY, + NO_PROXY_STEALTH, + createState, + getMutableState, + getState, + none, + useHookstate +} from '@etherealengine/hyperflux' import { defineComponent, @@ -213,8 +221,8 @@ function ModelReactor(): JSX.Element { // update scene useEffect(() => { - const scene = modelComponent.scene.value - const asset = modelComponent.asset.value + const scene = modelComponent.scene.get(NO_PROXY_STEALTH) + const asset = modelComponent.asset.get(NO_PROXY_STEALTH) if (!scene || !asset) return diff --git a/packages/engine/src/scene/components/PortalComponent.ts b/packages/engine/src/scene/components/PortalComponent.ts index acd45be261..bb169db6c1 100644 --- a/packages/engine/src/scene/components/PortalComponent.ts +++ b/packages/engine/src/scene/components/PortalComponent.ts @@ -23,7 +23,6 @@ All portions of the code written by the Ethereal Engine team are Copyright © 20 Ethereal Engine. All Rights Reserved. */ -import { RigidBodyDesc } from '@dimforge/rapier3d-compat' import { useEffect } from 'react' import { ArrowHelper, BackSide, Euler, Mesh, MeshBasicMaterial, Quaternion, SphereGeometry, Vector3 } from 'three' @@ -48,12 +47,10 @@ import { NameComponent } from '@etherealengine/spatial/src/common/NameComponent' import { UUIDComponent } from '@etherealengine/spatial/src/common/UUIDComponent' import { V_100 } from '@etherealengine/spatial/src/common/constants/MathConstants' import { matches } from '@etherealengine/spatial/src/common/functions/MatchesUtils' -import { Physics } from '@etherealengine/spatial/src/physics/classes/Physics' import { ColliderComponent } from '@etherealengine/spatial/src/physics/components/ColliderComponent' import { RigidBodyComponent } from '@etherealengine/spatial/src/physics/components/RigidBodyComponent' import { TriggerComponent } from '@etherealengine/spatial/src/physics/components/TriggerComponent' import { CollisionGroups } from '@etherealengine/spatial/src/physics/enums/CollisionGroups' -import { PhysicsState } from '@etherealengine/spatial/src/physics/state/PhysicsState' import { RendererState } from '@etherealengine/spatial/src/renderer/RendererState' import { addObjectToGroup, removeObjectFromGroup } from '@etherealengine/spatial/src/renderer/components/GroupComponent' import { @@ -163,10 +160,7 @@ export const PortalComponent = defineComponent({ /** Allow scene data populating rigidbody component too */ if (hasComponent(entity, RigidBodyComponent)) return - setComponent(entity, RigidBodyComponent, { - type: 'fixed', - body: Physics.createRigidBody(entity, getState(PhysicsState).physicsWorld, RigidBodyDesc.fixed()) - }) + setComponent(entity, RigidBodyComponent, { type: 'fixed' }) setComponent(entity, ColliderComponent, { shape: 'box', collisionLayer: CollisionGroups.Trigger, diff --git a/packages/spatial/src/networking/state/EntityNetworkState.tsx b/packages/spatial/src/networking/state/EntityNetworkState.tsx index 53575acfb3..b67d791496 100644 --- a/packages/spatial/src/networking/state/EntityNetworkState.tsx +++ b/packages/spatial/src/networking/state/EntityNetworkState.tsx @@ -74,8 +74,8 @@ export const EntityNetworkState = defineState({ networkId: action.networkId, authorityPeerId: action.authorityPeerId ?? action.$peer, ownerPeer: action.$peer, - spawnPosition: action.position ?? new Vector3(), - spawnRotation: action.rotation ?? new Quaternion() + spawnPosition: action.position ? new Vector3().copy(action.position) : new Vector3(), + spawnRotation: action.rotation ? new Quaternion().copy(action.rotation) : new Quaternion() }) }), From 746388b1ec23033a228e9fa43bbac28b42f7732e Mon Sep 17 00:00:00 2001 From: HexaField Date: Wed, 14 Feb 2024 14:49:46 +1100 Subject: [PATCH 67/67] src ref --- .../src/scene/components/ModelComponent.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/engine/src/scene/components/ModelComponent.tsx b/packages/engine/src/scene/components/ModelComponent.tsx index 994fd737b2..2bf1cc24f5 100644 --- a/packages/engine/src/scene/components/ModelComponent.tsx +++ b/packages/engine/src/scene/components/ModelComponent.tsx @@ -26,15 +26,7 @@ Ethereal Engine. All Rights Reserved. import { useEffect } from 'react' import { AnimationMixer, BoxGeometry, CapsuleGeometry, CylinderGeometry, Group, Scene, SphereGeometry } from 'three' -import { - NO_PROXY, - NO_PROXY_STEALTH, - createState, - getMutableState, - getState, - none, - useHookstate -} from '@etherealengine/hyperflux' +import { NO_PROXY, createState, getMutableState, getState, none, useHookstate } from '@etherealengine/hyperflux' import { defineComponent, @@ -221,8 +213,7 @@ function ModelReactor(): JSX.Element { // update scene useEffect(() => { - const scene = modelComponent.scene.get(NO_PROXY_STEALTH) - const asset = modelComponent.asset.get(NO_PROXY_STEALTH) + const { scene, asset, src } = getComponent(entity, ModelComponent) if (!scene || !asset) return @@ -256,7 +247,7 @@ function ModelReactor(): JSX.Element { addError(entity, ModelComponent, 'LOADING_ERROR', 'Error compiling model') }) .finally(() => { - SceneAssetPendingTagComponent.removeResource(entity, modelComponent.src.value) + SceneAssetPendingTagComponent.removeResource(entity, src) }) return () => {