UNPKG

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