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

MaterialManager.ts 13KB

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