threepipe
Version:
A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.
392 lines (354 loc) • 16.9 kB
text/typescript
import {ColorManagement, Event, EventDispatcher, EventListener2, Material, ShaderChunk, Texture} from 'three'
import {
IMaterial,
iMaterialCommons,
IMaterialParameters,
IMaterialTemplate,
ITexture,
LegacyPhongMaterial,
LineMaterial2,
ObjectShaderMaterial,
PhysicalMaterial,
UnlitLineMaterial,
UnlitMaterial,
} from '../core'
import {downloadFile} from 'ts-browser-helpers'
import {MaterialExtension} from '../materials'
import {generateUUID} from '../three'
import {AnimateTimeMaterial, IMaterialEventMap} from '../core/IMaterial'
import {shaderReplaceString} from '../utils'
import {Object3DManager} from './Object3DManager'
/**
* Material Manager
* Utility class to manage materials.
* Maintains a library of materials and material templates that can be used to manage or create new materials.
* Used in {@link AssetManager} to manage materials.
* @category Asset Manager
*/
export class MaterialManager<TEventMap extends object = object> extends EventDispatcher<TEventMap> {
readonly templates: IMaterialTemplate[] = [
PhysicalMaterial.MaterialTemplate,
UnlitMaterial.MaterialTemplate,
UnlitLineMaterial.MaterialTemplate,
LineMaterial2.MaterialTemplate,
LegacyPhongMaterial.MaterialTemplate,
ObjectShaderMaterial.MaterialTemplate,
]
private _materials: IMaterial[] = []
constructor() {
super()
legacyBumpScaleFixSetup()
}
/**
* @param info: uuid or template name or material type
* @param params
*/
public findOrCreate(info: string, params?: IMaterialParameters|Material): IMaterial | undefined {
let mat = this.findMaterial(info)
if (!mat) mat = this.create(info, params)
return mat
}
/**
* Create a material from the template name or material type
* @param nameOrType
* @param register
* @param params
*/
public create<TM extends IMaterial>(nameOrType: string, params: IMaterialParameters = {}, register = true): TM | undefined {
let template: IMaterialTemplate<any> = {materialType: nameOrType, name: nameOrType}
while (!template.generator) { // looping so that we can inherit templates, not fully implemented yet
const t2 = this.findTemplate(template.materialType) // todo add a baseTemplate property to the template?
if (!t2) {
console.warn('Template has no generator or materialType', template, nameOrType)
return undefined
}
template = {...template, ...t2}
}
const material = this._create<TM>(template, params)
if (material && register) this.registerMaterial(material)
return material
}
// make global function?
protected _create<TM extends IMaterial>(template: IMaterialTemplate<TM>, oldMaterial?: IMaterialParameters|Partial<TM>): TM|undefined {
if (!template.generator) {
console.error('Template has no generator', template)
return undefined
}
const legacyColors = (oldMaterial as any)?.metadata && (oldMaterial as any)?.metadata.version <= 4.5
const lastColorManagementEnabled = ColorManagement.enabled
if (legacyColors) ColorManagement.enabled = false
const material = template.generator(template.params || {})
if (oldMaterial && material) material.setValues(oldMaterial, true)
if (legacyColors) ColorManagement.enabled = lastColorManagementEnabled
return material
}
public findTemplate(nameOrType: string, withGenerator = false): IMaterialTemplate|undefined {
if (!nameOrType) return undefined
return this.templates.find(v => (v.name === nameOrType || v.materialType === nameOrType) && (!withGenerator || v.generator))
|| this.templates.find(v => v.alias?.includes(nameOrType) && (!withGenerator || v.generator))
}
protected _disposeMaterial = (e: {target?: IMaterial})=>{
const mat = e.target
if (!mat || mat.assetType !== 'material') return
mat.setDirty()
// for (const map of MaterialManager.GetMapsForMaterial(mat)) {
// const dispose = !map.isRenderTargetTexture
// && map.userData.disposeOnIdle !== false
// // && !isInScene(map) // todo <- is this always required? this will be very slow if doing for every map of every material dispose on scene clear
//
// if (dispose && typeof map.dispose === 'function') {
// // console.log('disposing texture', map)
// map.dispose()
// }
// }
// this.unregisterMaterial(mat) // not unregistering on dispose, that has to be done explicitly. todo: make an easy way to do that.
}
private _materialMaps = new Map<string, Set<ITexture>>()
// private _textures = new Set<ITexture>()
protected _materialUpdate: EventListener2<'materialUpdate', IMaterialEventMap, IMaterial> = (e)=>{
const mat = e.material || e.target
if (!mat || mat.assetType !== 'material') return
this._refreshTextureRefs(mat)
}
protected _textureUpdate = function(this: IMaterial, e: Event<'update', Texture>) {
if (!this || this.assetType !== 'material') return
this.dispatchEvent({texture: e.target, bubbleToParent: true, bubbleToObject: true, ...e, type: 'textureUpdate'})
}
private _refreshTextureRefs(mat: any) {
if (!mat.__textureUpdate) mat.__textureUpdate = this._textureUpdate.bind(mat)
const newMaps = Object3DManager.GetMapsForMaterial(mat)
const oldMaps = this._materialMaps.get(mat.uuid) || new Set<ITexture>()
for (const map of newMaps) {
if (!map || !map.isTexture) continue
// this._textures.add(map)
// if (!map._appliedMaterials) map._appliedMaterials = new Set<IMaterial>()
// if (oldMaps.has(map)) continue
// map._appliedMaterials.add(mat)
map.addEventListener('update', mat.__textureUpdate)
}
for (const map of oldMaps) {
if (newMaps.has(map)) continue
map.removeEventListener('update', mat.__textureUpdate)
// if (!map._appliedMaterials) continue
// const mats = map._appliedMaterials
// mats?.delete(mat)
// if (!mats?.size) this._textures.delete(map)
// if (!mats || map.userData.disposeOnIdle === false) continue
// if (mats.size === 0) map.dispose()
}
this._materialMaps.set(mat.uuid, newMaps)
}
public registerMaterial(material: IMaterial): void {
if (!material) return
if (this._materials.includes(material)) return
const mat = this.findMaterial(material.uuid)
// todo make an option to return the same material instance and replace it, instead of replacing uuid
if (mat) {
console.warn('MaterialManager: imported material uuid already exists, creating new uuid')
material.uuid = generateUUID()
if (material.userData.uuid) material.userData.uuid = material.uuid
}
// todo: check for name exists also
// console.warn('Registering material', material)
material.addEventListener('dispose', this._disposeMaterial)
material.addEventListener('materialUpdate', this._materialUpdate) // from set dirty
material.registerMaterialExtensions?.(this._materialExtensions)
this._materials.push(material)
this._refreshTextureRefs(material)
}
registerMaterials(materials: IMaterial[]): void {
materials.forEach(material => this.registerMaterial(material))
}
/**
* This is done automatically on material dispose.
* @param material
*/
public unregisterMaterial(material: IMaterial): void {
this._materials = this._materials.filter(v=>v.uuid !== material.uuid)
this._materialMaps.delete(material.uuid)
material.unregisterMaterialExtensions?.(this._materialExtensions)
material.removeEventListener('dispose', this._disposeMaterial)
material.removeEventListener('materialUpdate', this._materialUpdate)
}
clearMaterials(): void {
[...this._materials].forEach(material => this.unregisterMaterial(material))
}
public registerMaterialTemplate(template: IMaterialTemplate): void {
if (!template.templateUUID) template.templateUUID = generateUUID()
const mat = this.templates.find(v=>v.templateUUID === template.templateUUID)
if (mat) {
console.error('MaterialTemplate already exists', template, mat)
return
}
this.templates.push(template)
}
public unregisterMaterialTemplate(template: IMaterialTemplate): void {
const i = this.templates.findIndex(v=>v.templateUUID === template.templateUUID)
if (i >= 0) this.templates.splice(i, 1)
}
dispose(disposeRuntimeMaterials = true) {
const mats = this._materials
this._materials = []
for (const material of mats) {
if (!disposeRuntimeMaterials && material.userData.runtimeMaterial) {
this._materials.push(material)
continue
}
material.dispose()
}
return
}
public findMaterial(uuid: string): IMaterial | undefined {
return !uuid ? undefined : this._materials.find(v=>v.uuid === uuid)
}
public findMaterialsByName(name: string|RegExp, regex = false): IMaterial[] {
return this._materials.filter(v=>
typeof name !== 'string' || regex ?
v.name.match(typeof name === 'string' ? '^' + name + '$' : name) !== null :
v.name === name
)
}
public getMaterialsOfType<TM extends IMaterial = IMaterial>(typeSlug: string | undefined): TM[] {
return typeSlug ? this._materials.filter(v=>v.constructor.TypeSlug === typeSlug) as TM[] : []
}
public getAllMaterials(): IMaterial[] {
return [...this._materials]
}
// processModel(object: IModel, options: AnyOptions): IModel {
// const k = this._processModel(object, options)
// safeSetProperty(object, 'modelObject', k)
// return object
// }
// protected abstract _processModel(object: any, options: AnyOptions): any
/**
* Creates a new material if a compatible template is found or apply minimal upgrades and returns the original material.
* Also checks from the registered materials, if one with the same uuid is found, it is returned instead with the new parameters.
* Also caches the response.
* Returns the same material if its already upgraded.
* @param material - the material to upgrade/check
* @param useSourceMaterial - if false, will not use the source material parameters in the new material. default = true
* @param materialTemplate - any specific material template to use instead of detecting from the material type.
* @param createFromTemplate - if false, will not create a new material from the template, but will apply minimal upgrades to the material instead. default = true
*/
convertToIMaterial(material: Material&{assetType?:'material', iMaterial?: IMaterial}, {useSourceMaterial = true, materialTemplate, createFromTemplate = true}: {useSourceMaterial?:boolean, materialTemplate?: string, createFromTemplate?: boolean} = {}): IMaterial|undefined {
if (!material) return
if (material.assetType) return <IMaterial>material
if (material.iMaterial?.assetType) return material.iMaterial
const uuid = material.userData?.uuid || material.uuid
let mat = this.findMaterial(uuid)
if (!mat && createFromTemplate !== false) {
const ignoreSource = useSourceMaterial === false || !material.isMaterial
const template = materialTemplate || (!ignoreSource && material.type ? material.type || 'physical' : 'physical')
mat = this.create(template, ignoreSource ? undefined : material)
} else if (mat) {
// if ((mat as any).iMaterial) mat = (mat as any).iMaterial
console.warn('Material with the same uuid already exists, copying properties')
if (material.type !== mat!.type) console.error('Material type mismatch, delete previous material first?', material, mat)
mat!.setValues(material)
}
if (mat) {
mat.uuid = uuid
mat.userData.uuid = uuid
material.iMaterial = mat
} else {
console.warn('Failed to convert material to IMaterial, just upgrading', material, useSourceMaterial, materialTemplate)
mat = iMaterialCommons.upgradeMaterial.call(material)
}
return mat
}
// use convertToIMaterial
// processMaterial(material: IMaterial, options: AnyOptions&{useSourceMaterial?:boolean, materialTemplate?: string, register?: boolean}): IMaterial {
// if (!material.materialObject)
// material = (this._processMaterial(material, {...options, register: false}))!
// if (options.register !== false) this.registerMaterial(material)
//
// return material
// }
protected _materialExtensions: MaterialExtension[] = []
registerMaterialExtension(extension: MaterialExtension): void {
if (this._materialExtensions.includes(extension)) return
this._materialExtensions.push(extension)
for (const mat of this._materials) mat.registerMaterialExtensions?.([extension])
}
unregisterMaterialExtension(extension: MaterialExtension): void {
const i = this._materialExtensions.indexOf(extension)
if (i < 0) return
this._materialExtensions.splice(i, 1)
for (const mat of this._materials) mat.unregisterMaterialExtensions?.([extension])
}
clearExtensions() {
[...this._materialExtensions].forEach(v=>this.unregisterMaterialExtension(v))
}
exportMaterial(material: IMaterial, filename?: string, minify = true, download = false): File {
const serialized = material.toJSON()
const json = JSON.stringify(serialized, null, minify ? 0 : 2)
const name = (filename || material.name || 'physical_material') + '.' + material.constructor.TypeSlug
const blob = new File([json], name, {type: 'application/json'})
if (download) downloadFile(blob)
return blob
}
applyMaterial(material: IMaterial, nameRegexOrUuid: string, regex = true, time?: AnimateTimeMaterial): boolean {
let currentMats = this.findMaterialsByName(nameRegexOrUuid, regex)
if (!currentMats || currentMats.length < 1) currentMats = [this.findMaterial(nameRegexOrUuid) as any]
let applied = false
for (const c of currentMats) {
// console.log(c)
if (!c) continue
if (c === material) continue
if (c.userData.__isVariation) continue
const applied2 = this.copyMaterialProps(c, material, time)
if (applied2) applied = true
}
return applied
}
/**
* copyProps from material to c
* @param c
* @param material
* @param time
*/
copyMaterialProps(c: IMaterial, material: IMaterial, time?: AnimateTimeMaterial) {
let applied = false
const mType = Object.getPrototypeOf(material).constructor.TYPE
const cType = Object.getPrototypeOf(c).constructor.TYPE
// console.log(cType, mType)
if (cType === mType) {
const n = c.name
c.setValues(material, undefined, undefined, time)
c.name = n
applied = true
} else {
// todo
// if ((c as any)['__' + mType]) continue
const newMat = (c as any)['__' + mType] || this.create(mType)
if (newMat) {
const n = c.name
// newMat.setValues(material, undefined, undefined, time)
if (newMat.setValues) newMat.setValues(material)
else Object.assign(newMat, material)
newMat.name = n
const meshes = c.appliedMeshes
for (const mesh of [...meshes ?? []]) {
if (!mesh) continue
mesh.material = newMat
applied = true
}
(c as any)['__' + mType] = newMat
}
}
return applied
}
}
function legacyBumpScaleFixSetup() {
const a = `
vec3 vSigmaX = normalize( dFdx( surf_pos.xyz ) );
vec3 vSigmaY = normalize( dFdy( surf_pos.xyz ) );
`
ShaderChunk.bumpmap_pars_fragment = shaderReplaceString(ShaderChunk.bumpmap_pars_fragment, a, `
#ifdef BUMP_MAP_SCALE_LEGACY
${a.replace(/normalize/g, '')}
#else
${a}
#endif
`)
}