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

MaterialManager.ts 16KB

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