threepipe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

MaterialManager.ts 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331
  1. import {BaseEvent, ColorManagement, EventDispatcher, Material} from 'three'
  2. import {
  3. IMaterial,
  4. iMaterialCommons,
  5. IMaterialEvent,
  6. IMaterialParameters,
  7. IMaterialTemplate,
  8. ITexture,
  9. ITextureEvent,
  10. PhysicalMaterial,
  11. UnlitMaterial,
  12. } from '../core'
  13. import {downloadFile} from 'ts-browser-helpers'
  14. import {MaterialExtension} from '../materials'
  15. import {generateUUID, isInScene} from '../three'
  16. /**
  17. * Material Manager
  18. * Utility class to manage materials.
  19. * Maintains a library of materials and material templates that can be used to manage or create new materials.
  20. * Used in {@link AssetManager} to manage materials.
  21. * @category Asset Manager
  22. */
  23. export class MaterialManager<T = ''> extends EventDispatcher<BaseEvent, T> {
  24. readonly templates: IMaterialTemplate[] = [
  25. PhysicalMaterial.MaterialTemplate,
  26. UnlitMaterial.MaterialTemplate,
  27. ]
  28. private _materials: IMaterial[] = []
  29. constructor() {
  30. super()
  31. }
  32. /**
  33. * @param info: uuid or template name or material type
  34. * @param params
  35. */
  36. public findOrCreate(info: string, params?: IMaterialParameters|Material): IMaterial | undefined {
  37. let mat = this.findMaterial(info)
  38. if (!mat) mat = this.create(info, params)
  39. return mat
  40. }
  41. /**
  42. * Create a material from the template name or material type
  43. * @param nameOrType
  44. * @param register
  45. * @param params
  46. */
  47. public create<TM extends IMaterial>(nameOrType: string, params: IMaterialParameters = {}, register = true): TM | undefined {
  48. let template: IMaterialTemplate<any> = {materialType: nameOrType, name: nameOrType}
  49. while (!template.generator) { // looping so that we can inherit templates, not fully implemented yet
  50. const t2 = this.findTemplate(template.materialType) // todo add a baseTemplate property to the template?
  51. if (!t2) {
  52. console.error('Template has no generator or materialType', template, nameOrType)
  53. return undefined
  54. }
  55. template = {...template, ...t2}
  56. }
  57. const material = this._create<TM>(template, params)
  58. if (material && register) this.registerMaterial(material)
  59. return material
  60. }
  61. // make global function?
  62. protected _create<TM extends IMaterial>(template: IMaterialTemplate<TM>, oldMaterial?: IMaterialParameters|Partial<TM>): TM|undefined {
  63. if (!template.generator) {
  64. console.error('Template has no generator', template)
  65. return undefined
  66. }
  67. const legacyColors = (oldMaterial as any)?.metadata && (oldMaterial as any)?.metadata.version <= 4.5
  68. const lastColorManagementEnabled = ColorManagement.enabled
  69. if (legacyColors) ColorManagement.enabled = false
  70. const material = template.generator(template.params || {})
  71. if (oldMaterial && material) material.setValues(oldMaterial, true)
  72. if (legacyColors) ColorManagement.enabled = lastColorManagementEnabled
  73. return material
  74. }
  75. public findTemplate(nameOrType: string, withGenerator = false): IMaterialTemplate|undefined {
  76. if (!nameOrType) return undefined
  77. return this.templates.find(v => (v.name === nameOrType || v.materialType === nameOrType) && (!withGenerator || v.generator))
  78. || this.templates.find(v => v.alias?.includes(nameOrType) && (!withGenerator || v.generator))
  79. }
  80. protected _getMapsForMaterial(material: IMaterial) {
  81. const maps = new Set<ITexture>()
  82. // todo use MaterialProperties or similar to find the maps in the material. This is a bit hacky
  83. for (const val of Object.values(material)) {
  84. if (val && val.isTexture) {
  85. maps.add(val)
  86. }
  87. }
  88. for (const val of Object.values(material.userData ?? {})) {
  89. if (val && (val as any).isTexture) {
  90. maps.add(val as ITexture)
  91. }
  92. }
  93. return maps
  94. }
  95. protected _disposeMaterial = (e: {target?: IMaterial})=>{
  96. const mat = e.target
  97. if (!mat || mat.assetType !== 'material') return
  98. mat.setDirty()
  99. this._getMapsForMaterial(mat)
  100. .forEach(map=>
  101. !map.isRenderTargetTexture && map.userData.disposeOnIdle !== false &&
  102. map.dispose && !isInScene(map) && map.dispose())
  103. // this.unregisterMaterial(mat) // todo
  104. }
  105. private _materialMaps = new Map<string, Set<ITexture>>()
  106. protected _materialUpdate = (e: IMaterialEvent<'materialUpdate'>)=>{
  107. const mat = e.material || e.target
  108. if (!mat || mat.assetType !== 'material') return
  109. this._refreshTextureRefs(mat)
  110. }
  111. protected _textureUpdate = function(this: IMaterial, e: ITextureEvent<'update'>) {
  112. if (!this || this.assetType !== 'material') return
  113. this.dispatchEvent({texture: e.target, bubbleToParent: true, bubbleToObject: true, ...e, type: 'textureUpdate'})
  114. }
  115. private _refreshTextureRefs(mat: any) {
  116. if (!mat.__textureUpdate) mat.__textureUpdate = this._textureUpdate.bind(mat)
  117. const newMaps = this._getMapsForMaterial(mat)
  118. const oldMaps = this._materialMaps.get(mat.uuid) || new Set<ITexture>()
  119. for (const map of newMaps) {
  120. if (oldMaps.has(map)) continue
  121. if (!map.userData.__appliedMaterials) map.userData.__appliedMaterials = new Set<IMaterial>()
  122. map.userData.__appliedMaterials.add(mat)
  123. map.addEventListener('update', mat.__textureUpdate)
  124. }
  125. for (const map of oldMaps) {
  126. if (newMaps.has(map)) continue
  127. map.removeEventListener('update', mat.__textureUpdate)
  128. if (!map.userData.__appliedMaterials) continue
  129. const mats = map.userData.__appliedMaterials
  130. mats?.delete(mat)
  131. if (!mats || map.userData.disposeOnIdle === false) continue
  132. if (mats.size === 0) map.dispose()
  133. }
  134. this._materialMaps.set(mat.uuid, newMaps)
  135. }
  136. public registerMaterial(material: IMaterial): void {
  137. if (!material) return
  138. if (this._materials.includes(material)) return
  139. const mat = this.findMaterial(material.uuid)
  140. if (mat) {
  141. console.warn('Material UUID already exists', material, mat)
  142. return
  143. }
  144. // console.warn('Registering material', material)
  145. material.addEventListener('dispose', this._disposeMaterial)
  146. material.addEventListener('materialUpdate', this._materialUpdate) // from set dirty
  147. material.registerMaterialExtensions?.(this._materialExtensions)
  148. this._materials.push(material)
  149. this._refreshTextureRefs(material)
  150. }
  151. registerMaterials(materials: IMaterial[]): void {
  152. materials.forEach(material => this.registerMaterial(material))
  153. }
  154. /**
  155. * This is done automatically on material dispose.
  156. * @param material
  157. */
  158. public unregisterMaterial(material: IMaterial): void {
  159. this._materials = this._materials.filter(v=>v.uuid !== material.uuid)
  160. material.unregisterMaterialExtensions?.(this._materialExtensions)
  161. material.removeEventListener('dispose', this._disposeMaterial)
  162. material.removeEventListener('materialUpdate', this._materialUpdate)
  163. }
  164. clearMaterials(): void {
  165. [...this._materials].forEach(material => this.unregisterMaterial(material))
  166. }
  167. public registerMaterialTemplate(template: IMaterialTemplate): void {
  168. if (!template.templateUUID) template.templateUUID = generateUUID()
  169. const mat = this.templates.find(v=>v.templateUUID === template.templateUUID)
  170. if (mat) {
  171. console.error('MaterialTemplate already exists', template, mat)
  172. return
  173. }
  174. this.templates.push(template)
  175. }
  176. public unregisterMaterialTemplate(template: IMaterialTemplate): void {
  177. const i = this.templates.findIndex(v=>v.templateUUID === template.templateUUID)
  178. if (i >= 0) this.templates.splice(i, 1)
  179. }
  180. dispose() {
  181. for (const material of this._materials) {
  182. material.dispose()
  183. }
  184. this._materials = []
  185. return
  186. }
  187. public findMaterial(uuid: string): IMaterial | undefined {
  188. return !uuid ? undefined : this._materials.find(v=>v.uuid === uuid)
  189. }
  190. public findMaterialsByName(name: string): IMaterial[] {
  191. return this._materials.filter(v=>v.name === name)
  192. }
  193. public getMaterialsOfType<TM extends IMaterial = IMaterial>(typeSlug: string | undefined): TM[] {
  194. return typeSlug ? this._materials.filter(v=>v.constructor.TypeSlug === typeSlug) as TM[] : []
  195. }
  196. public getAllMaterials(): IMaterial[] {
  197. return [...this._materials]
  198. }
  199. // processModel(object: IModel, options: AnyOptions): IModel {
  200. // const k = this._processModel(object, options)
  201. // safeSetProperty(object, 'modelObject', k)
  202. // return object
  203. // }
  204. // protected abstract _processModel(object: any, options: AnyOptions): any
  205. convertToIMaterial(material: Material&{assetType?:'material', iMaterial?: IMaterial}, options: {useSourceMaterial?:boolean, materialTemplate?: string} = {}): IMaterial|undefined {
  206. if (!material) return
  207. if (material.assetType) return <IMaterial>material
  208. if (material.iMaterial?.assetType) return material.iMaterial
  209. const uuid = material.userData?.uuid || material.uuid
  210. let mat = this.findMaterial(uuid)
  211. if (!mat) {
  212. const ignoreSource = options.useSourceMaterial === false || !material.isMaterial
  213. const template = options.materialTemplate || (!ignoreSource && material.type ? material.type || 'physical' : 'physical')
  214. mat = this.create(template, ignoreSource ? undefined : material)
  215. } else {
  216. console.warn('Material with the same uuid already exists, copying properties')
  217. if (material.type !== mat.type) console.error('Material type mismatch, delete previous material first?', material.type, mat.type)
  218. mat.setValues(material)
  219. }
  220. if (mat) {
  221. mat.uuid = uuid
  222. mat.userData.uuid = uuid
  223. material.iMaterial = mat
  224. } else {
  225. console.warn('Failed to convert material to IMaterial, just upgrading', material, options)
  226. mat = iMaterialCommons.upgradeMaterial.call(material)
  227. }
  228. return mat
  229. }
  230. // processMaterial(material: IMaterial, options: AnyOptions&{useSourceMaterial?:boolean, materialTemplate?: string, register?: boolean}): IMaterial {
  231. // if (!material.materialObject)
  232. // material = (this._processMaterial(material, {...options, register: false}))!
  233. // if (options.register !== false) this.registerMaterial(material)
  234. //
  235. // return material
  236. // }
  237. protected _materialExtensions: MaterialExtension[] = []
  238. registerMaterialExtension(extension: MaterialExtension): void {
  239. if (this._materialExtensions.includes(extension)) return
  240. this._materialExtensions.push(extension)
  241. for (const mat of this._materials) mat.registerMaterialExtensions?.([extension])
  242. }
  243. unregisterMaterialExtension(extension: MaterialExtension): void {
  244. const i = this._materialExtensions.indexOf(extension)
  245. if (i < 0) return
  246. this._materialExtensions.splice(i, 1)
  247. for (const mat of this._materials) mat.unregisterMaterialExtensions?.([extension])
  248. }
  249. clearExtensions() {
  250. [...this._materialExtensions].forEach(v=>this.unregisterMaterialExtension(v))
  251. }
  252. exportMaterial(material: IMaterial, filename?: string, minify = true, download = false): File {
  253. const serialized = material.toJSON()
  254. const json = JSON.stringify(serialized, null, minify ? 0 : 2)
  255. const name = (filename || material.name || 'physical_material') + '.' + material.constructor.TypeSlug
  256. const blob = new File([json], name, {type: 'application/json'})
  257. if (download) downloadFile(blob)
  258. return blob
  259. }
  260. applyMaterial(material: IMaterial, nameOrUuid: string): boolean {
  261. const mType = Object.getPrototypeOf(material).constructor.TYPE
  262. let currentMats = this.findMaterialsByName(nameOrUuid)
  263. if (!currentMats || currentMats.length < 1) currentMats = [this.findMaterial(nameOrUuid) as any]
  264. let applied = false
  265. for (const c of currentMats) {
  266. // console.log(c)
  267. if (!c) continue
  268. if (c === material) continue
  269. if (c.userData.__isVariation) continue
  270. const cType = Object.getPrototypeOf(c).constructor.TYPE
  271. // console.log(cType, mType)
  272. if (cType === mType) {
  273. const n = c.name
  274. c.setValues(material)
  275. c.name = n
  276. applied = true
  277. } else {
  278. // todo
  279. // if ((c as any)['__' + mType]) continue
  280. const newMat = (c as any)['__' + mType] || this.create(mType)
  281. if (!newMat) continue
  282. const n = c.name
  283. newMat.setValues(material)
  284. newMat.name = n
  285. const meshes = c.appliedMeshes
  286. for (const mesh of [...meshes ?? []]) {
  287. if (!mesh) continue
  288. mesh.material = newMat
  289. applied = true
  290. }
  291. (c as any)['__' + mType] = newMat
  292. }
  293. }
  294. return applied
  295. }
  296. }