threepipe
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

MaterialManager.ts 15KB

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