UNPKG

@polygonjs/polygonjs

Version:

node-based WebGL 3D engine https://polygonjs.com

205 lines (191 loc) 7.89 kB
import {ShaderMaterial, Material, IUniform, WebGLProgramParametersWithUniforms} from 'three'; import {PolyScene} from '../../../../../scene/PolyScene'; import {GlParamConfig, GlParamConfigJSON} from '../../utils/GLParamConfig'; import {ParamType} from '../../../../../poly/ParamType'; import {PolyDictionary} from '../../../../../../types/GlobalTypes'; import {IUniformTexture} from '../../../../utils/code/gl/Uniforms'; export interface OnBeforeCompileData { vertexShader: string; fragmentShader: string; paramConfigs: readonly GlParamConfig<ParamType>[]; // additionalUniformNames: string[]; additionalTextureUniforms: PolyDictionary<IUniformTexture>; timeDependent: boolean; resolutionDependent: boolean; raymarchingLightsWorldCoordsDependent: boolean; } interface CloneOptions { clonedParamConfigName: string; clonedParamConfig?: GlParamConfig<ParamType>; } export function cloneOnBeforeCompileData(data: OnBeforeCompileData, options: CloneOptions): OnBeforeCompileData { const {clonedParamConfigName} = options; let {clonedParamConfig} = options; const json = OnBeforeCompileDataConverter.toJSON(data); const clonedData = OnBeforeCompileDataConverter.fromJSON(json); const preservedParamConfigs = data.paramConfigs.filter((p) => p.name() != clonedParamConfigName); if (!clonedParamConfig) { clonedParamConfig = clonedData.paramConfigs.find((p) => p.name() == clonedParamConfigName); } const paramConfigs = preservedParamConfigs; if (clonedParamConfig) { paramConfigs.push(clonedParamConfig); } return { ...clonedData, paramConfigs, }; } // from https://www.typescriptlang.org/docs/handbook/2/mapped-types.html type RemoveParamConfigField<Type> = { [Property in keyof Type as Exclude<Property, 'paramConfigs' | 'additionalTextureUniforms'>]: Type[Property]; }; export interface OnBeforeCompileDataJSON extends RemoveParamConfigField<OnBeforeCompileData> { paramConfigs: GlParamConfigJSON<ParamType>[]; } type OnBeforeCompile = (shader: WebGLProgramParametersWithUniforms) => void; export function assignUniformViaUserData(material: Material, uniformName: string, newUniform: IUniformTexture) { const uniforms = MaterialUserDataUniforms.getUniforms(material); if (uniforms) { const currentUniform = uniforms[uniformName]; if (currentUniform) { currentUniform.value = newUniform.value; } else { uniforms[uniformName] = newUniform; } } else { // if there are no uniforms, the material has not been compiled yet. // we therefore must add those to userData so that they are picked up in onBeforeCompile. // For instance, this can be the case for materials assigned to particles, when run without assemblers. // Since the assembler is responsible for assigning the additionalTextures, this is not done when they are not loaded. // But for this to work, we also need to make sure that the persistedConfig will not contain the uniforms in the userData // so those must be stripped out when saving it OnBeforeCompileDataHandler.addAdditionalTexture(material, uniformName, newUniform); } } export class MaterialUserDataUniforms { static getUniforms(material: Material): PolyDictionary<IUniform> | undefined { return material.userData?.uniforms || (material as ShaderMaterial).uniforms; } static setUniforms(material: Material, uniforms: PolyDictionary<IUniform>) { material.userData.uniforms = uniforms; } static removeUniforms(material: Material) { const uniforms = this.getUniforms(material); if (uniforms) { const userData = material.userData; delete userData['uniforms']; } return uniforms; } } export function assignOnBeforeCompileDataAndFunction(scene: PolyScene, material: Material, data: OnBeforeCompileData) { OnBeforeCompileDataHandler.setData(material, data); material.onBeforeCompile = _createOnBeforeCompile(scene, material); // it is important that customProgramCacheKey is also set when there are no assemblers // as otherwise the material will all use the same key, and will override each other const key = `${material.uuid}:${performance.now()}`; material.customProgramCacheKey = () => key; } interface CopyParams { src: Material; dest: Material; shareCustomUniforms: boolean; } export function copyOnBeforeCompileData(scene: PolyScene, params: CopyParams) { const {src, dest, shareCustomUniforms} = params; const data = OnBeforeCompileDataHandler.getData(src); if (data) { function cloneData(data: OnBeforeCompileData) { const json = OnBeforeCompileDataConverter.toJSON(data); return OnBeforeCompileDataConverter.fromJSON(json); } const newData = shareCustomUniforms ? data : cloneData(data); assignOnBeforeCompileDataAndFunction(scene, dest, newData); } } function _createOnBeforeCompile(scene: PolyScene, material: Material): OnBeforeCompile { const onBeforeCompile = (shader: WebGLProgramParametersWithUniforms) => { const data = OnBeforeCompileDataHandler.getData(material); if (!data) { return; } const { vertexShader, fragmentShader, paramConfigs, additionalTextureUniforms, timeDependent, resolutionDependent, raymarchingLightsWorldCoordsDependent, } = data; shader.vertexShader = vertexShader; shader.fragmentShader = fragmentShader; scene.uniformsController.addUniforms(shader.uniforms, { paramConfigs, additionalTextureUniforms, timeDependent, resolutionDependent, raymarchingLightsWorldCoordsDependent, }); // also add to the material itself so that the material is easy to debug in the console, as well as in tests const shaderMaterial = material as ShaderMaterial; shaderMaterial.vertexShader = shader.vertexShader; shaderMaterial.fragmentShader = shader.fragmentShader; MaterialUserDataUniforms.setUniforms(material, shader.uniforms); // saving the compilations count here // seems to have side effects and make tests fail. // so not using it for now // shaderMaterial.userData.compilationsCount = shaderMaterial.userData.compilationsCount || 0; // shaderMaterial.userData.compilationsCount++; }; return onBeforeCompile; } export class OnBeforeCompileDataHandler { static setData(material: Material, data: OnBeforeCompileData) { material.userData.onBeforeCompileData = data; } static addAdditionalTexture(material: Material, uniformName: string, newUniform: IUniformTexture) { const data = this.getData(material); if (data) { const currentUniform = data.additionalTextureUniforms[uniformName]; if (currentUniform == null) { data.additionalTextureUniforms[uniformName] = newUniform; } } else { console.warn('no data found on material', material); } } static getData(material: Material): OnBeforeCompileData | undefined { return material.userData.onBeforeCompileData; } static removeData(material: Material): OnBeforeCompileData | undefined { const data = this.getData(material); if (data) { const userData = material.userData; delete userData['onBeforeCompileData']; } return data; } } export class OnBeforeCompileDataConverter { static toJSON(onBeforeCompileData: OnBeforeCompileData): OnBeforeCompileDataJSON { const onBeforeCompileDataJSON: OnBeforeCompileDataJSON = { vertexShader: onBeforeCompileData.vertexShader, fragmentShader: onBeforeCompileData.fragmentShader, timeDependent: onBeforeCompileData.timeDependent, resolutionDependent: onBeforeCompileData.resolutionDependent, raymarchingLightsWorldCoordsDependent: onBeforeCompileData.raymarchingLightsWorldCoordsDependent, paramConfigs: onBeforeCompileData.paramConfigs.map((pc) => pc.toJSON()), }; return onBeforeCompileDataJSON; } static fromJSON(json: OnBeforeCompileDataJSON): OnBeforeCompileData { const onBeforeCompileData: OnBeforeCompileData = { ...json, additionalTextureUniforms: {}, paramConfigs: json.paramConfigs.map((json) => GlParamConfig.fromJSON(json)), }; return onBeforeCompileData; } }