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.
218 lines • 9.45 kB
JavaScript
import { getOrCall, objectMap } from 'ts-browser-helpers';
import { shaderReplaceString, shaderUtils } from '../utils';
import { ShaderChunk } from 'three';
import { generateUUID } from '../three/utils';
export class MaterialExtender {
static ApplyMaterialExtensions(material, shader, materialExtensions, renderer) {
for (const materialExtension of materialExtensions) {
this.ApplyMaterialExtension(material, shader, materialExtension, renderer);
}
}
static ApplyMaterialExtension(material, shader, materialExtension, renderer) {
// Add parsFragmentSnippet just before void main in fragment shader
let a = getOrCall(materialExtension.parsFragmentSnippet, renderer, material) ?? '';
if (a.length) {
shader.fragmentShader = shaderReplaceString(shader.fragmentShader, this.VoidMain, '\n' + a + '\n', { prepend: true });
}
// Add parsVertexSnippet just before void main in vertex shader
a = getOrCall(materialExtension.parsVertexSnippet, renderer, material) ?? '';
if (a.length) {
shader.vertexShader = shaderReplaceString(shader.vertexShader, this.VoidMain, '\n' + a + '\n', { prepend: true });
}
// Add extra uniforms
if (materialExtension.extraUniforms) {
shader.uniforms = Object.assign(shader.uniforms, objectMap(materialExtension.extraUniforms, (v) => getOrCall(v, shader) || { value: null }));
}
// Add extra defines and set needsUpdate to true if needed
if (materialExtension.extraDefines)
updateMaterialDefines(materialExtension.extraDefines, material);
// Call shaderExtender if defined
materialExtension.shaderExtender && materialExtension.shaderExtender(shader, material, renderer);
// Save last shader so that it can be used to check if shader has changed in extensions
material.lastShader = shader;
}
static CacheKeyForExtensions(material, materialExtensions) {
let r = '';
for (const materialExtension of materialExtensions) {
r += this.CacheKeyForExtension(material, materialExtension);
}
return r;
}
static CacheKeyForExtension(material, materialExtension) {
let r = '';
if (materialExtension.computeCacheKey)
r += getOrCall(materialExtension.computeCacheKey, material);
else
r += materialExtension.uuid;
if (materialExtension.extraDefines)
r += Object.values(materialExtension.extraDefines).map(v => getOrCall(v) ?? '').join('');
return r;
}
static RegisterExtensions(material, customMaterialExtensions) {
const exts = [];
if (!Array.isArray(material.materialExtensions))
material.materialExtensions = [];
if (customMaterialExtensions)
for (const ext of customMaterialExtensions) {
if (material.materialExtensions.includes(ext))
continue;
if (ext.isCompatible !== undefined && (!ext.isCompatible || !ext.isCompatible(material)))
continue;
exts.push(ext);
if (!ext.uuid)
ext.uuid = generateUUID();
if (!ext.__setDirty)
ext.__setDirty = () => {
if (!ext.updateVersion)
ext.updateVersion = 0;
ext.updateVersion++;
};
if (!ext.setDirty)
ext.setDirty = ext.__setDirty;
}
if (!exts.length)
return [];
material.materialExtensions = [...material.materialExtensions || [], ...exts]
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
if (!material.__extListen) {
material.__extListen = true;
material.addEventListener('beforeRender', materialBeforeRender);
material.addEventListener('afterRender', materialAfterRender);
material.addEventListener('addToMesh', materialAddToMesh);
material.addEventListener('removeFromMesh', materialRemovedFromMesh);
material.addEventListener('materialUpdate', materialUpdate);
}
for (const ext of exts) {
ext.onRegister && ext.onRegister(material);
}
material.needsUpdate = true;
return exts;
}
static UnregisterExtensions(material, customMaterialExtensions) {
if (customMaterialExtensions) {
material.materialExtensions = material.materialExtensions?.filter((v) => !customMaterialExtensions.includes(v)) || [];
for (const ext of customMaterialExtensions) {
ext.onUnregister && ext.onUnregister(material);
}
}
if (!material.materialExtensions?.length && material.__extListen) {
material.removeEventListener('beforeRender', materialBeforeRender);
material.removeEventListener('afterRender', materialAfterRender);
material.removeEventListener('addToMesh', materialAddToMesh);
material.removeEventListener('removeFromMesh', materialRemovedFromMesh);
material.removeEventListener('materialUpdate', materialUpdate);
delete material.__extListen;
}
}
}
(() => {
Object.assign(ShaderChunk, shaderUtils); // for #include in the shaders
})();
MaterialExtender.VoidMain = 'void main()';
export function updateMaterialDefines(defines, material) {
if (!defines || !material)
return;
if (material.defines === undefined || material.defines === null) { // required for some three.js materials
material.defines = {};
}
let flag = false;
const entries = Object.entries(defines);
for (const [key, valF] of entries) {
const val = getOrCall(valF);
if (val === undefined) {
if (material.defines[key] !== undefined) {
delete material.defines[key];
flag = true;
}
}
else if (material.defines[key] !== val) {
material.defines[key] = typeof val === 'boolean' ? +val : val;
flag = true;
}
}
if (flag)
material.needsUpdate = true;
}
function materialBeforeRender({ target, object, renderer }) {
const material = target;
if (!material || !object || !renderer)
throw new Error('Invalid material, object or renderer');
if (!material.materialExtensions)
return;
for (const value of material.materialExtensions) {
value.onObjectRender && value.onObjectRender(object, material, renderer);
if (material.lastShader) {
const updater = getOrCall(value.updaters) || [];
for (const v2 of updater)
v2 && v2.updateShaderProperties(material.lastShader);
}
const udVersion = '_' + value.uuid + '_version';
if (value.updateVersion !== material.userData[udVersion]) {
material.userData[udVersion] = value.updateVersion;
material.needsUpdate = true;
}
}
}
function materialAfterRender({ target, object, renderer }) {
const material = target;
if (!material || !object || !renderer)
throw new Error('Invalid material, object or renderer');
if (!material.materialExtensions)
return;
for (const value of material.materialExtensions) {
value.onAfterRender && value.onAfterRender(object, material, renderer);
}
}
function materialAddToMesh({ target, object }) {
const material = target;
if (!material || !object)
throw new Error('Invalid material or object');
if (!material.materialExtensions)
return;
for (const value of material.materialExtensions) {
value.onAddToMesh && value.onAddToMesh(object, material);
}
}
function materialRemovedFromMesh({ target, object }) {
const material = target;
if (!material || !object)
throw new Error('Invalid material or object');
if (!material.materialExtensions)
return;
for (const value of material.materialExtensions) {
value.onRemoveFromMesh && value.onRemoveFromMesh(object, material);
}
}
function materialUpdate({ target }) {
const material = target;
if (!material)
throw new Error('Invalid material');
if (!material.materialExtensions)
return;
for (const value of material.materialExtensions) {
value.onMaterialUpdate && value.onMaterialUpdate(material);
}
}
/**
* Creates a {@link MaterialExtension} with getUiConfig that also caches the config for the material based on uuid
* @param getUiConfig - function that returns a ui config. make sure its static.
* @param uuid uuid to use.
*/
export function uiConfigMaterialExtension(getUiConfig, uuid) {
const uuid1 = uuid || generateUUID();
return {
uuid: uuid1,
// todo clean code.
getUiConfig: material => {
if (!material.__uiConfigs)
material.__uiConfigs = {}; // todo remove reference sometime after plugin removed
if (material.__uiConfigs[uuid1])
return material.__uiConfigs[uuid1];
const config = getUiConfig(material);
material.__uiConfigs[uuid1] = config;
return config;
},
isCompatible: () => true,
};
}
//# sourceMappingURL=MaterialExtender.js.map