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.

221 lines (192 loc) 9.93 kB
import {IMaterial, IMaterialUserData, IWebGLRenderer} from '../core' import {getOrCall, objectMap} from 'ts-browser-helpers' import {shaderReplaceString, shaderUtils} from '../utils' import {Object3D, ShaderChunk, WebGLRenderer} from 'three' import {MaterialExtension, MaterialExtensionShader} from './MaterialExtension' import {generateUUID} from '../three/utils' export class MaterialExtender { static { Object.assign(ShaderChunk, shaderUtils) // for #include in the shaders } static VoidMain = 'void main()' static ApplyMaterialExtensions(material: IMaterial, shader: MaterialExtensionShader, materialExtensions: MaterialExtension[], renderer: WebGLRenderer) { for (const materialExtension of materialExtensions) { this.ApplyMaterialExtension(material, shader, materialExtension, renderer) } } static ApplyMaterialExtension(material: IMaterial, shader: MaterialExtensionShader, materialExtension: MaterialExtension, renderer: WebGLRenderer) { // 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 as any, 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: IMaterial, materialExtensions: MaterialExtension[]): string { let r = '' for (const materialExtension of materialExtensions) { r += this.CacheKeyForExtension(material, materialExtension) } return r } static CacheKeyForExtension(material: IMaterial, materialExtension: MaterialExtension): string { 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: IMaterial, customMaterialExtensions?: MaterialExtension[]): MaterialExtension[] { 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 as any).__extListen) { (material as any).__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: IMaterial, customMaterialExtensions?: MaterialExtension[]) { 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 as any).__extListen) { material.removeEventListener('beforeRender', materialBeforeRender) material.removeEventListener('afterRender', materialAfterRender) material.removeEventListener('addToMesh', materialAddToMesh) material.removeEventListener('removeFromMesh', materialRemovedFromMesh) material.removeEventListener('materialUpdate', materialUpdate) delete (material as any).__extListen } } } export function updateMaterialDefines(defines: MaterialExtension['extraDefines'], material: IMaterial) { 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}:{object?: Object3D, renderer?: IWebGLRenderer, target: IMaterial}) { 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 as any).lastShader) { const updater = getOrCall(value.updaters) || [] for (const v2 of updater) v2 && v2.updateShaderProperties((material as any).lastShader) } const udVersion: keyof IMaterialUserData = '_' + value.uuid + '_version' as any if (value.updateVersion !== material.userData[udVersion]) { material.userData[udVersion] = value.updateVersion material.needsUpdate = true } } } function materialAfterRender({target, object, renderer}:{object?: Object3D, renderer?: IWebGLRenderer, target: IMaterial}) { 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}:{object?: Object3D, target: IMaterial}) { 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}:{object?: Object3D, target: IMaterial}) { 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}:{target: IMaterial}) { 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: Required<MaterialExtension>['getUiConfig'], uuid?: string) { const uuid1 = uuid || generateUUID() return { uuid: uuid1, // todo clean code. getUiConfig: material => { if (!(material as any).__uiConfigs) (material as any).__uiConfigs = {} as any // todo remove reference sometime after plugin removed if ((material as any).__uiConfigs[uuid1]) return (material as any).__uiConfigs[uuid1] const config = getUiConfig(material); (material as any).__uiConfigs[uuid1] = config return config }, isCompatible: () => true, } as MaterialExtension }