Explorar el Código

Change logic for disposing scene assets. Add iGeometryCommons.dispose and iMaterialCommons.dispose.

master
Palash Bansal hace 2 años
padre
commit
f2f6312a16
No account linked to committer's email address

+ 19
- 16
src/assetmanager/AssetImporter.ts Ver fichero

@@ -8,7 +8,6 @@ import {
ImportResult,
LoadFileOptions,
ProcessRawOptions,
RootSceneImportResult,
} from './IAssetImporter'
import {IAsset, IFile} from './IAsset'
import {IImporter, ILoader} from './IImporter'
@@ -138,21 +137,22 @@ export class AssetImporter extends EventDispatcher<IAssetImporterEvent, IAssetIm
// console.log(result)
if (!options.forceImport && result) {
const results = await this.processRaw<T>(result, options) // just in case its not processed. Internal check is done to ensure it's not processed twice
let isDisposed = false // if any of the objects is disposed
for (const r of results) {
// todo: check if this is still required.
if ((r as RootSceneImportResult)?.userData?.rootSceneModelRoot) { // in case processImported is false we need a special case check here
if (r?.children?.find((c: any) => c.__disposed)) {
isDisposed = true
break
}
}
if (r && !r.__disposed) continue // todo add __disposed to object, material, texture, etc
isDisposed = true
break
}
// let isDisposed = false // if any of the objects is disposed
// for (const r of results) {
// // todo: check if this is still required.
// if ((r as RootSceneImportResult)?.userData?.rootSceneModelRoot) { // in case processImported is false we need a special case check here
// if (r?.children?.find((c: any) => c.__disposed)) {
// isDisposed = true
// break
// }
// }
// if (r && !r.__disposed) continue // todo add __disposed to object, material, texture, etc
// isDisposed = true
// break
// }
// todo: should we check if any of it's children is disposed ?
if (!isDisposed || options.reimportDisposed === false) return results
// if (!isDisposed || options.reimportDisposed === false)
return results
}

// todo: add support to get cloned asset? if we want to import multiple times and everytime return a cloned asset
@@ -170,7 +170,7 @@ export class AssetImporter extends EventDispatcher<IAssetImporterEvent, IAssetIm
else arrs.push(result)
}
// remove preImportedRaw when any of the assets is disposed. This is to prevent memory leaks
arrs.forEach(r=>r.addEventListener?.('dispose', () => {
arrs.forEach(r=>r.addEventListener?.('dispose', () => { // todo: recheck after dispose logic change
if (asset?.preImportedRaw) asset.preImportedRaw = undefined
if (asset?.preImported) asset.preImported = undefined
}))
@@ -293,6 +293,9 @@ export class AssetImporter extends EventDispatcher<IAssetImporterEvent, IAssetIm
if (file) {
file.__loadedAsset = res


// todo: recheck below code after dispose logic change

// Clear the reference __loadedAsset when any one asset is disposed.
// it's a bit hacky to do this here, but it works for now. todo: move to a better place
let ress: any[] = []

+ 1
- 4
src/assetmanager/IAssetImporter.ts Ver fichero

@@ -30,12 +30,9 @@ export interface ImportResultExtras {

userData?: IImportResultUserData

// eslin t-disable-next-line @typescript-eslint/naming-convention
__rootPath?: string
// eslin t-disable-next-line @typescript-eslint/naming-convention
__rootBlob?: IFile
// eslin t-disable-next-line @typescript-eslint/naming-convention
__disposed?: boolean
// __disposed?: boolean

[key: string]: any
}

+ 6
- 9
src/assetmanager/MaterialManager.ts Ver fichero

@@ -12,7 +12,7 @@ import {
} from '../core'
import {downloadFile} from 'ts-browser-helpers'
import {MaterialExtension} from '../materials'
import {generateUUID} from '../three/utils/misc'
import {generateUUID, isInScene} from '../three'


export class MaterialManager<T = ''> extends EventDispatcher<BaseEvent, T> {
@@ -104,14 +104,11 @@ export class MaterialManager<T = ''> extends EventDispatcher<BaseEvent, T> {
const mat = e.target
if (!mat || mat.assetType !== 'material') return
mat.setDirty()
const maps = this._getMapsForMaterial(mat)
maps.forEach(map=>{
const mats = map.userData.__appliedMaterials!
mats?.delete(mat)
if (!mats || map.userData.disposeOnIdle === false) return
if (mats.size === 0) map.dispose()
})
this.unregisterMaterial(mat)
this._getMapsForMaterial(mat)
.forEach(map=>
!map.isRenderTargetTexture && map.userData.disposeOnIdle !== false &&
map.dispose && !isInScene(map) && map.dispose())
// this.unregisterMaterial(mat) // todo
}

private _materialMaps = new Map<string, Set<ITexture>>()

+ 12
- 1
src/core/IGeometry.ts Ver fichero

@@ -5,7 +5,11 @@ import {IObject3D} from './IObject'
import {IImportResultUserData} from '../assetmanager'

export interface IGeometryUserData extends IImportResultUserData{
disposeOnIdle?: boolean // default: true
/**
* Automatically dispose geometry when not used by any object in the scene
* @default true
*/
disposeOnIdle?: boolean
// [key: string]: any // commented for noe
}
export interface IGeometry<Attributes extends NormalOrGLBufferAttributes = NormalBufferAttributes> extends BufferGeometry<Attributes, IGeometryEvent, IGeometryEventTypes>, IUiConfigContainer {
@@ -18,6 +22,13 @@ export interface IGeometry<Attributes extends NormalOrGLBufferAttributes = Norma
// Note: for userData: add _ in front of for private use, which is preserved while cloning but not serialisation, and __ for private use, which is not preserved while cloning and serialisation
userData: IGeometryUserData

/**
* Disposes the geometry from the GPU.
* Set force to false if not sure the geometry is used by any object in the scene.
* // todo add check for visible in scene also? or is that overkill
* @param force - when true, same as three.js dispose. when false, only disposes if disposeOnIdle not false and not used by any object in the scene. default: true
*/
dispose(force?: boolean): void

// eslint-disable-next-line @typescript-eslint/naming-convention
_uiConfig?: UiObjectConfig

+ 14
- 3
src/core/IMaterial.ts Ver fichero

@@ -41,8 +41,11 @@ export interface IMaterialSetDirtyOptions {
}
export interface IMaterialUserData extends IImportResultUserData{
uuid?: string // adding to userdata also, so that its saved in gltf

disposeOnIdle?: boolean // default: true
/**
* Automatically dispose material when not used by any object in the scene
* @default true
*/
disposeOnIdle?: boolean

renderToGBuffer?: boolean
/**
@@ -61,7 +64,7 @@ export interface IMaterialUserData extends IImportResultUserData{

inverseAlphaMap?: boolean // only for physical material right now

// [key: string]: any // commented for noe
[key: string]: any // commented for noe


// legacy, to be removed
@@ -110,6 +113,14 @@ export interface IMaterial<E extends IMaterialEvent = IMaterialEvent, ET = IMate
// Note: for userData: add _ in front of for private use, which is preserved while cloning but not serialisation, and __ for private use, which is not preserved while cloning and serialisation
userData: IMaterialUserData

/**
* Disposes the material from the GPU.
* Set force to false if not sure the material is used by any object in the scene.
* // todo add check for visible in scene also? or is that overkill
* @param force - when true, same as three.js dispose. when false, only disposes if disposeOnIdle not false and not used by any object in the scene. default: true
*/
dispose(force?: boolean): void

// optional from subclasses, added here for autocomplete
flatShading?: boolean
map?: ITexture | null

+ 15
- 2
src/core/IObject.ts Ver fichero

@@ -90,8 +90,17 @@ export interface IObject3DUserData extends IImportResultUserData {
* @deprecated
*/
dispose?: any
/**
* @deprecated
*/
setMaterial?: any
/**
* @deprecated
*/
setGeometry?: any
/**
* @deprecated
*/
setDirty?: any

/**
@@ -173,8 +182,12 @@ export interface IObject3D<E extends Event = IObject3DEvent, ET = IObject3DEvent

objectProcessor?: IObjectProcessor

// eslint-disable-next-line @typescript-eslint/naming-convention
__disposed?: boolean
// __disposed?: boolean
/**
*
* @param removeFromParent - remove from parent. Default true
*/
dispose(removeFromParent?: boolean): void;

// region inherited type fixes


+ 7
- 1
src/core/ITexture.ts Ver fichero

@@ -1,11 +1,16 @@
import {IMaterial} from './IMaterial'
import {Event, Texture} from 'three'
import {ChangeEvent} from 'uiconfig.js'
import {IRenderTarget} from '../rendering'

export interface ITextureUserData{
mimeType?: string
embedUrlImagePreviews?: boolean
disposeOnIdle?: boolean // automatically dispose when added to a material and then not used in any material
/**
* Automatically dispose texture when not used by any material that's applied to some object in the scene.
* Works only after it's applied to a material once.
*/
disposeOnIdle?: boolean
__appliedMaterials?: Set<IMaterial>
}
export type ITextureEventTypes = 'dispose' | 'update'
@@ -28,6 +33,7 @@ export interface ITexture extends Texture {

setDirty?(): void

_target?: IRenderTarget // for internal use only. refers to the render target that this texture is attached to
}

export function upgradeTexture(this: ITexture) {

+ 14
- 3
src/core/geometry/iGeometryCommons.ts Ver fichero

@@ -1,6 +1,7 @@
import {UiObjectConfig} from 'uiconfig.js'
import {IGeometry, IGeometrySetDirtyOptions} from '../IGeometry'
import {toIndexedGeometry} from '../../three'
import {isInScene, toIndexedGeometry} from '../../three'
import {BufferGeometry} from 'three'

export const iGeometryCommons = {
setDirty: function(this: IGeometry, options?: IGeometrySetDirtyOptions): void {
@@ -10,6 +11,11 @@ export const iGeometryCommons = {
refreshUi: function(this: IGeometry) {
this.uiConfig?.uiRefresh?.(true, 'postFrame', 1)
},
dispose: (superDispose: BufferGeometry['dispose']): IGeometry['dispose'] =>
function(this: IGeometry, force = true): void {
if (!force && (this.userData.disposeOnIdle === false || isInScene(this))) return
superDispose.call(this)
},
upgradeGeometry: upgradeGeometry,
makeUiConfig: function(this: IGeometry): UiObjectConfig {
if (this.uiConfig) return this.uiConfig
@@ -117,18 +123,23 @@ export const iGeometryCommons = {
},
}

export function upgradeGeometry(this: IGeometry) {
function upgradeGeometry(this: IGeometry) {
if (this.assetType === 'geometry') return // already upgraded
if (!this.isBufferGeometry) {
console.error('Material is not a this', this)
console.error('Geometry is not a this', this)
return
}
this.assetType = 'geometry'

this.dispose = iGeometryCommons.dispose(this.dispose)

if (!this.setDirty) this.setDirty = iGeometryCommons.setDirty
if (!this.refreshUi) this.refreshUi = iGeometryCommons.refreshUi

if (!this.appliedMeshes) this.appliedMeshes = new Set()
if (!this.userData) this.userData = {}
this.uiConfig = iGeometryCommons.makeUiConfig.call(this)

// todo: dispose uiconfig on geometry dispose

// todo: add serialization?

+ 1
- 0
src/core/material/PhysicalMaterial.ts Ver fichero

@@ -44,6 +44,7 @@ export class PhysicalMaterial extends MeshPhysicalMaterial<IMaterialEvent, Physi

readonly appliedMeshes: Set<IObject3D> = new Set()
readonly setDirty = iMaterialCommons.setDirty
dispose(): this {return iMaterialCommons.dispose(super.dispose).call(this)}
clone(): this {return iMaterialCommons.clone(super.clone).call(this)}
dispatchEvent(event: IMaterialEvent): void {iMaterialCommons.dispatchEvent(super.dispatchEvent).call(this, event)}


+ 3
- 0
src/core/material/ShaderMaterial2.ts Ver fichero

@@ -44,6 +44,9 @@ export class ShaderMaterial2<E extends IMaterialEvent = IMaterialEvent, ET = IMa

readonly appliedMeshes: Set<any> = new Set()
readonly setDirty = iMaterialCommons.setDirty
dispose(): this {return iMaterialCommons.dispose(super.dispose).call(this)}
clone(): this {return iMaterialCommons.clone(super.clone).call(this)}
dispatchEvent(event: IMaterialEvent): void {iMaterialCommons.dispatchEvent(super.dispatchEvent).call(this, event)}

readonly isRawShaderMaterial: boolean


+ 1
- 0
src/core/material/UnlitMaterial.ts Ver fichero

@@ -44,6 +44,7 @@ export class UnlitMaterial extends MeshBasicMaterial<IMaterialEvent, UnlitMateri

readonly appliedMeshes: Set<IObject3D> = new Set()
readonly setDirty = iMaterialCommons.setDirty
dispose(): this {return iMaterialCommons.dispose(super.dispose).call(this)}
clone(): this {return iMaterialCommons.clone(super.clone).call(this)}
dispatchEvent(event: IMaterialEvent): void {iMaterialCommons.dispatchEvent(super.dispatchEvent).call(this, event)}


+ 7
- 0
src/core/material/iMaterialCommons.ts Ver fichero

@@ -19,6 +19,7 @@ import {copyMaterialUserData} from '../../utils/serialization'
import {MaterialExtender, MaterialExtension} from '../../materials'
import {IScene} from '../IScene'
import {IMaterial, IMaterialEvent, IMaterialSetDirtyOptions} from '../IMaterial'
import {isInScene} from '../../three'

/**
* Map of all material properties and their default values in three.js - Material.js
@@ -105,6 +106,11 @@ export const iMaterialCommons = {
this.setDirty?.()
return this
},
dispose: (superDispose: Material<any, any>['dispose']): IMaterial['dispose'] =>
function(this: IMaterial, force = true): void {
if (!force && (this.userData.disposeOnIdle === false || isInScene(this))) return
superDispose.call(this)
},
clone: (superClone: Material<any, any>['clone']): IMaterial['clone'] =>
function(this: IMaterial): IMaterial {
if (!this.userData.cloneId) {
@@ -202,6 +208,7 @@ export function upgradeMaterial(this: IMaterial): IMaterial {
if (this.assetType === 'material') return this // already upgraded
this.assetType = 'material'
this.setValues = iMaterialCommons.setValues(this.setValues)
this.dispose = iMaterialCommons.dispose(this.dispose)
this.clone = iMaterialCommons.clone(this.clone)
this.dispatchEvent = iMaterialCommons.dispatchEvent(this.dispatchEvent)


+ 69
- 70
src/core/object/iObjectCommons.ts Ver fichero

@@ -6,7 +6,7 @@ import {copyObject3DUserData} from '../../utils'
import {IGeometry, IGeometryEvent} from '../IGeometry'
import {Box3B} from '../../three'
import {makeIObject3DUiConfig} from './IObjectUi'
import {upgradeGeometry} from '../geometry/iGeometryCommons'
import {iGeometryCommons} from '../geometry/iGeometryCommons'
import {iMaterialCommons} from '../material/iMaterialCommons'

export const iObjectCommons = {
@@ -158,8 +158,8 @@ export const iObjectCommons = {
if (!mat) continue
if (mat.appliedMeshes) {
mat.appliedMeshes.delete(this)
if (mat.userData && mat.appliedMeshes?.size === 0 && mat.userData.disposeOnIdle !== false)
mat.dispose() // this will dispose textures(if they are idle) if the material is registered in the material manager
// if (mat.userData && mat.appliedMeshes?.size === 0 && mat.userData.disposeOnIdle !== false)
mat.dispose(false) // this will dispose textures(if they are idle) if the material is registered in the material manager
}
}

@@ -223,13 +223,13 @@ export const iObjectCommons = {
this._onGeometryUpdate && geom.removeEventListener('geometryUpdate', this._onGeometryUpdate)
if (geom.appliedMeshes) {
geom.appliedMeshes.delete(this)
if (geom.userData && geom.appliedMeshes.size === 0 && geom.userData.disposeOnIdle !== false) geom.dispose()
geom.dispose(false)
}
}
if (geometry) {
if (!geometry.assetType) {
// console.error('Geometry not upgraded')
upgradeGeometry.call(geometry)
iGeometryCommons.upgradeGeometry.call(geometry)
}
}
this._currentGeometry = geometry || null
@@ -286,27 +286,24 @@ export const iObjectCommons = {
return superAdd.call(this, ...args)
},
dispose: (superDispose?: IObject3D['dispose']) =>
function(this: IObject3D): void {
this.dispatchEvent({type: 'dispose', bubbleToParent: false})

if (this.__disposed) {
console.warn('Object already disposed', this)
return
function(this: IObject3D, removeFromParent = true): void {
if (removeFromParent && this.parent) {
this.removeFromParent()
delete this.parentRoot
}
this.__disposed = true

// this is first so that the leaf children are removed from parent first, removed event will be fired depth first
for (const c of [...this.children]) c?.dispose?.()
this.children = []
if (this.parent) this.removeFromParent()
this.dispatchEvent({type: 'dispose', bubbleToParent: false})

// if (this.__disposed) {
// console.warn('Object already disposed', this)
// return
// }
// this.__disposed = true

delete this.parentRoot
// safeSetProperty(this, 'modelObject', undefined, true) // in-case modelObject is just a getter.
this.userData = {} // todo: clear only our userdata and maybe any private variables?
for (const c of [...this.children]) c?.dispose && c.dispose(false) // not removing the children from parent to preserve hierarchy
// this.children = []

// this.uiConfig?.dispose?.() // todo: make uiConfig.dispose
this.uiConfig = undefined

superDispose?.call(this)
},
@@ -322,10 +319,10 @@ export const iObjectCommons = {
function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectProcessor?: IObjectProcessor): void { // parent is the root Object3DModel.
if (!this) return
// console.log('upgradeObject3D', this, parent, objectProcessor)
if (this.__disposed) {
console.warn('re-init/re-add disposed object, things might not work as intended', this)
delete this.__disposed
}
// if (this.__disposed) {
// console.warn('re-init/re-add disposed object, things might not work as intended', this)
// delete this.__disposed
// }
if (!this.userData) this.userData = {}
this.userData.uuid = this.uuid

@@ -346,14 +343,14 @@ function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectPr

if (parent) this.parentRoot = parent

const oldFunctions = {
dispatchEvent: this.dispatchEvent,
clone: this.clone,
copy: this.copy,
add: this.add,
dispose: this.dispose,
}
this.addEventListener('dispose', () => Object.assign(this, oldFunctions)) // todo: is this required?
// const oldFunctions = {
// dispatchEvent: this.dispatchEvent,
// clone: this.clone,
// copy: this.copy,
// add: this.add,
// dispose: this.dispose,
// }
// this.addEventListener('dispose', () => Object.assign(this, oldFunctions)) // todo: is this required?

// typed because of type-checking
this.dispatchEvent = iObjectCommons.dispatchEvent(this.dispatchEvent)
@@ -371,10 +368,10 @@ function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectPr
this.addEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent)
this.addEventListener('removed', iObjectCommons.eventCallbacks.onRemovedFromParent)

this.addEventListener('dispose', ()=>{
this.removeEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent)
this.removeEventListener('removed', iObjectCommons.eventCallbacks.onRemovedFromParent)
})
// this.addEventListener('dispose', ()=>{
// this.removeEventListener('added', iObjectCommons.eventCallbacks.onAddedToParent)
// this.removeEventListener('removed', iObjectCommons.eventCallbacks.onRemovedFromParent)
// })

if ((this.isMesh || this.isLine) && !this.userData.__meshSetup) {
this.userData.__meshSetup = true
@@ -393,22 +390,26 @@ function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectPr
}

this.addEventListener('dispose', ()=>{
if (this.material) {
// const oldMats = Array.isArray(this.material) ? [...(this.material as IMaterial[])] : [this.material!]
this.material = undefined // this will dispose material if not used by other meshes
// delete this.material
// for (const oldMat of oldMats) {
// if (oldMat && oldMat.userData && oldMat.appliedMeshes?.size === 0 && oldMat.userData.disposeOnIdle !== false) oldMat.dispose()
// }
}
if (this.geometry) {
// const oldGeom = this.geometry
this.geometry = undefined // this will dispose geometry if not used by other meshes
// delete this.geometry
// if (oldGeom && oldGeom.userData && oldGeom.appliedMeshes?.size === 0 && oldGeom.userData.disposeOnIdle !== false) oldGeom.dispose()
}

delete this._onGeometryUpdate
(this.materials || [<IMaterial> this.material]).forEach(m => m?.dispose(false))
this.geometry?.dispose(false)

// if (this.material) {
// // const oldMats = Array.isArray(this.material) ? [...(this.material as IMaterial[])] : [this.material!]
// this.material = undefined // this will dispose material if not used by other meshes
// // delete this.material
// // for (const oldMat of oldMats) {
// // if (oldMat && oldMat.userData && oldMat.appliedMeshes?.size === 0 && oldMat.userData.disposeOnIdle !== false) oldMat.dispose()
// // }
// }
// if (this.geometry) {
// // const oldGeom = this.geometry
// this.geometry = undefined // this will dispose geometry if not used by other meshes
// // delete this.geometry
// // if (oldGeom && oldGeom.userData && oldGeom.appliedMeshes?.size === 0 && oldGeom.userData.disposeOnIdle !== false) oldGeom.dispose()
// }
//
// delete this._onGeometryUpdate
})

}
@@ -424,29 +425,27 @@ function upgradeObject3D(this: IObject3D, parent?: IObject3D|undefined, objectPr
for (const c of children) upgradeObject3D.call(c, this)

// region Legacy
if (this.userData.dispose) console.warn('userData.dispose already defined')
this.userData.dispose = () => {

// eslint-disable-next-line deprecation/deprecation
!this.userData.dispose && (this.userData.dispose = () => {
console.warn('userData.dispose is deprecated, use dispose directly')
this.dispose?.()
}
if (!this.modelObject) {
Object.defineProperty(this, 'modelObject', {
get: ()=>{
console.error('modelObject is deprecated, use object directly')
return this
},
})
}
if (!this.userData.setDirty)
this.userData.setDirty = (e: any)=>{
console.error('object.userData.setDirty is deprecated, use object.setDirty directly')
this.setDirty?.(e)
}
this.dispose && this.dispose()
})
// eslint-disable-next-line deprecation/deprecation
!this.modelObject && Object.defineProperty(this, 'modelObject', {
get: ()=>{
console.error('modelObject is deprecated, use object directly')
return this
},
})
// eslint-disable-next-line deprecation/deprecation
!this.userData.setDirty && (this.userData.setDirty = (e: any)=>{
console.error('object.userData.setDirty is deprecated, use object.setDirty directly')
this.setDirty?.(e)
})

// endregion

// if (!this.objectProcessor) console.warn('objectProcessor not set for', this)
// else
this.objectProcessor?.processObject(this)

}

+ 22
- 0
src/three/utils/misc.ts Ver fichero

@@ -1,5 +1,6 @@
import {BufferGeometry, MathUtils} from 'three'
import {mergeVertices} from 'three/examples/jsm/utils/BufferGeometryUtils.js'
import {IGeometry, IMaterial, IObject3D, IScene, ITexture} from '../../core'

/**
* Convert geometry to BufferGeometry with indexed attributes.
@@ -11,3 +12,24 @@ export function toIndexedGeometry(geometry: BufferGeometry<any, any, any>, toler
export function generateUUID() {
return MathUtils.generateUUID()
}

/**
* Check if a single or multiple object/geometry/material/texture is in the scene.
* This is used internally to check if a material is used by any object in the scene, and if not, it can be disposed.
* @param sceneObj
*/
export function isInScene(...sceneObj: (IGeometry|IMaterial|IObject3D|ITexture)[]): boolean {
if (sceneObj.length > 1) return sceneObj.some((a)=>isInScene(a))
const o = sceneObj[0]
if ((<ITexture>o).isTexture) return Array.from((<ITexture>o).userData.__appliedMaterials || []).some((m) => isInScene(m)) ?? false

const objects =
(<IObject3D>o).isObject3D ? [<IObject3D>o] :
(<IGeometry|IMaterial>o).appliedMeshes
for (const obj of objects) {
let inScene = false
obj.traverseAncestors((ob: IObject3D) => (<IScene>ob).isScene && (inScene = true))
if (inScene) return true
}
return false
}

Cargando…
Cancelar
Guardar