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 14KB

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