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
JavaScript
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