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

MaterialManager.ts 13KB

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