UNPKG

threepipe

Version:

A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.

357 lines 14.9 kB
import { ColorManagement, EventDispatcher } from 'three'; import { iMaterialCommons, LegacyPhongMaterial, LineMaterial2, PhysicalMaterial, UnlitLineMaterial, UnlitMaterial, } from '../core'; import { downloadFile } from 'ts-browser-helpers'; import { generateUUID, isInScene } from '../three'; /** * 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 extends EventDispatcher { constructor() { super(); this.templates = [ PhysicalMaterial.MaterialTemplate, UnlitMaterial.MaterialTemplate, UnlitLineMaterial.MaterialTemplate, LineMaterial2.MaterialTemplate, LegacyPhongMaterial.MaterialTemplate, ]; this._materials = []; this._disposeMaterial = (e) => { const mat = e.target; if (!mat || mat.assetType !== 'material') return; mat.setDirty(); this._getMapsForMaterial(mat) .forEach(map => !map.isRenderTargetTexture && map.userData.disposeOnIdle !== false && map.dispose && !isInScene(map) && map.dispose()); // this.unregisterMaterial(mat) // not unregistering on dispose, that has to be done explicitly. todo: make an easy way to do that. }; this._materialMaps = new Map(); this._materialUpdate = (e) => { const mat = e.material || e.target; if (!mat || mat.assetType !== 'material') return; this._refreshTextureRefs(mat); }; this._textureUpdate = function (e) { if (!this || this.assetType !== 'material') return; this.dispatchEvent({ texture: e.target, bubbleToParent: true, bubbleToObject: true, ...e, type: 'textureUpdate' }); }; // 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 // } this._materialExtensions = []; } /** * @param info: uuid or template name or material type * @param params */ findOrCreate(info, params) { 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 */ create(nameOrType, params = {}, register = true) { let template = { 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(template, params); if (material && register) this.registerMaterial(material); return material; } // make global function? _create(template, oldMaterial) { if (!template.generator) { console.error('Template has no generator', template); return undefined; } const legacyColors = oldMaterial?.metadata && oldMaterial?.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; } findTemplate(nameOrType, withGenerator = false) { 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)); } _getMapsForMaterial(material) { const maps = new Set(); // todo use MaterialProperties or similar to find the maps in the material. This is a bit hacky for (const val of Object.values(material)) { if (val && val.isTexture) { maps.add(val); } } for (const val of Object.values(material.userData ?? {})) { if (val && val.isTexture) { maps.add(val); } } return maps; } _refreshTextureRefs(mat) { if (!mat.__textureUpdate) mat.__textureUpdate = this._textureUpdate.bind(mat); const newMaps = this._getMapsForMaterial(mat); const oldMaps = this._materialMaps.get(mat.uuid) || new Set(); for (const map of newMaps) { if (oldMaps.has(map)) continue; if (!map._appliedMaterials) map._appliedMaterials = new Set(); 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 || map.userData.disposeOnIdle === false) continue; if (mats.size === 0) map.dispose(); } this._materialMaps.set(mat.uuid, newMaps); } registerMaterial(material) { if (!material) return; if (this._materials.includes(material)) return; const mat = this.findMaterial(material.uuid); if (mat) { console.warn('Material UUID already exists', material, mat); return; } // 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) { materials.forEach(material => this.registerMaterial(material)); } /** * This is done automatically on material dispose. * @param material */ unregisterMaterial(material) { this._materials = this._materials.filter(v => v.uuid !== material.uuid); material.unregisterMaterialExtensions?.(this._materialExtensions); material.removeEventListener('dispose', this._disposeMaterial); material.removeEventListener('materialUpdate', this._materialUpdate); } clearMaterials() { [...this._materials].forEach(material => this.unregisterMaterial(material)); } registerMaterialTemplate(template) { 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); } unregisterMaterialTemplate(template) { 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; } findMaterial(uuid) { return !uuid ? undefined : this._materials.find(v => v.uuid === uuid); } findMaterialsByName(name, regex = false) { return this._materials.filter(v => typeof name !== 'string' || regex ? v.name.match(typeof name === 'string' ? '^' + name + '$' : name) !== null : v.name === name); } getMaterialsOfType(typeSlug) { return typeSlug ? this._materials.filter(v => v.constructor.TypeSlug === typeSlug) : []; } getAllMaterials() { 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, { useSourceMaterial = true, materialTemplate, createFromTemplate = true } = {}) { if (!material) return; if (material.assetType) return 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; } registerMaterialExtension(extension) { if (this._materialExtensions.includes(extension)) return; this._materialExtensions.push(extension); for (const mat of this._materials) mat.registerMaterialExtensions?.([extension]); } unregisterMaterialExtension(extension) { 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, filename, minify = true, download = false) { 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, nameRegexOrUuid, regex = true) { let currentMats = this.findMaterialsByName(nameRegexOrUuid, regex); if (!currentMats || currentMats.length < 1) currentMats = [this.findMaterial(nameRegexOrUuid)]; 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); if (applied2) applied = true; } return applied; } /** * copyProps from material to c * @param c * @param material */ copyMaterialProps(c, material) { 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); c.name = n; applied = true; } else { // todo // if ((c as any)['__' + mType]) continue const newMat = c['__' + mType] || this.create(mType); if (newMat) { const n = c.name; newMat.setValues(material); newMat.name = n; const meshes = c.appliedMeshes; for (const mesh of [...meshes ?? []]) { if (!mesh) continue; mesh.material = newMat; applied = true; } c['__' + mType] = newMat; } } return applied; } } //# sourceMappingURL=MaterialManager.js.map