import * as fs from "fs"; import { ExtractShaderProgramFromGlsl } from "./convertGlslIntoShaderProgram.js"; import { ConnectionPointType } from "../../connection/connectionPointType.js"; import { BlockDisableStrategy } from "../../blockFoundation/blockDisableStrategy.js"; // eslint-disable-next-line @typescript-eslint/naming-convention const EXTRA_IMPORTS = "@EXTRA_IMPORTS@"; // eslint-disable-next-line @typescript-eslint/naming-convention const BABYLON_CORE_PATH = "@BABYLON_CORE_PATH@"; // eslint-disable-next-line @typescript-eslint/naming-convention const SMART_FILTER_CORE_IMPORT = "@SMART_FILTER_CORE_IMPORT@"; const SHADER_PROGRAM = "@SHADER_PROGRAM@"; const BLOCK_NAME = "@BLOCK_NAME@"; const NAMESPACE = "@NAMESPACE@"; // eslint-disable-next-line @typescript-eslint/naming-convention const SHADER_BINDING_PRIVATE_VARIABLES = "@SHADER_BINDING_PRIVATE_VARIABLES@"; const CAMEL_CASE_UNIFORM = "@CAMEL_CASE_UNIFORM@"; const CONNECTION_POINT_TYPE = "@CONNECTION_POINT_TYPE@"; const CONNECTION_POINT_DEFAULT_VALUE = "@CONNECTION_POINT_DEFAULT_VALUE@"; const SHADER_BINDING_EXTENDS = "@SHADER_BINDING_EXTENDS@"; const SHADER_BINDING_CTOR_DOCSTRING_PARAMS = "@SHADER_BINDING_CTOR_DOCSTRING_PARAMS@"; const SHADER_BINDING_CTOR_PARAMS = "@SHADER_CTOR_PARAMS@"; const SHADER_BINDING_SUPER_PARAMS = "@SHADER_BINDING_SUPER_PARAMS@"; const SHADER_BINDING_CTOR = "@SHADER_BINDING_CTOR@"; const SHADER_BINDING_BIND = "@SHADER_BINDING_BIND@"; const SHADER_BLOCK_EXTENDS = "@SHADER_BLOCK_EXTENDS@"; // eslint-disable-next-line @typescript-eslint/naming-convention const BLOCK_INPUT_PROPERTIES = "@BLOCK_INPUT_PROPERTIES@"; // eslint-disable-next-line @typescript-eslint/naming-convention const BLOCK_DISABLE_OPTIMIZATION = "@BLOCK_DISABLE_OPTIMIZATION@"; const BLOCK_DISABLE_STRATEGY = "@BLOCK_DISABLE_STRATEGY@"; const BLOCK_GET_SHADER_BINDING_VARS = "@BLOCK_SHADER_BINDING_BIND_VARS@"; const BLOCK_GET_SHADER_PARAM_LIST = "@BLOCK_GET_SHADER_PARAM_LIST@"; const EFFECT_SETTER = "@EFFECT_SETTER@"; const EFFECT_VALUE = "@EFFECT_VALUE@"; const EXTRA_BIND_DOCSTRING = "@EXTRA_BIND_DOCSTRING@"; const EXTRA_BIND_PARAMS = "@EXTRA_BIND_PARAMS@"; const ShaderBindingPrivateVariablesTemplate = ` private readonly _${CAMEL_CASE_UNIFORM}: RuntimeData;`; const ShaderBindingCtorDocstringParams = ` * @param ${CAMEL_CASE_UNIFORM} - The ${CAMEL_CASE_UNIFORM} runtime value`; const ShaderBindingCtorParams = ` ${CAMEL_CASE_UNIFORM}: RuntimeData`; const ShaderBindingCtor = ` this._${CAMEL_CASE_UNIFORM} = ${CAMEL_CASE_UNIFORM};`; const ShaderBindingBind = ` effect.${EFFECT_SETTER}(this.getRemappedName(Uniforms.${CAMEL_CASE_UNIFORM}), ${EFFECT_VALUE});`; const ShaderBindingBindRegularValue = `this._${CAMEL_CASE_UNIFORM}.value`; const BlockInputProperty = ` /** * The ${CAMEL_CASE_UNIFORM} connection point. */ public readonly ${CAMEL_CASE_UNIFORM} = this._registerInput(Uniforms.${CAMEL_CASE_UNIFORM}, ConnectionPointType.${CONNECTION_POINT_TYPE}); `; const BlockInputOptionalProperty = ` /** /** * The ${CAMEL_CASE_UNIFORM} connection point. */ public readonly ${CAMEL_CASE_UNIFORM} = this._registerOptionalInput( "${CAMEL_CASE_UNIFORM}", ConnectionPointType.${CONNECTION_POINT_TYPE}, createStrongRef(${CONNECTION_POINT_DEFAULT_VALUE}) ); `; const BlockGetShaderBindingVars = ` const ${CAMEL_CASE_UNIFORM} = this._confirmRuntimeDataSupplied(this.${CAMEL_CASE_UNIFORM});`; const FileTemplate = `/* eslint-disable prettier/prettier */ // ************************************************************ // Note: this file is auto-generated, do not modify it directly // ************************************************************ // It was generated by convertGlslIntoBlock() from // an annotated .glsl file. Modify the .glsl file to make changes // to the block. This file will get overwritten when the build // is run or during a watch when the .glsl file is updated. import { type Effect } from "${BABYLON_CORE_PATH}/Materials/effect.js"; import { ${SHADER_BINDING_EXTENDS}, type RuntimeData, ConnectionPointType, type SmartFilter, ${SHADER_BLOCK_EXTENDS}, type ShaderProgram, ${EXTRA_IMPORTS}} from "${SMART_FILTER_CORE_IMPORT}";${SHADER_PROGRAM} /** * The shader binding for the ${BLOCK_NAME}, used by the runtime */ class ${BLOCK_NAME}ShaderBinding extends ${SHADER_BINDING_EXTENDS} { ${SHADER_BINDING_PRIVATE_VARIABLES} /** * Creates a new shader binding instance for the block. ${SHADER_BINDING_CTOR_DOCSTRING_PARAMS} */ constructor( ${SHADER_BINDING_CTOR_PARAMS} ) { super(${SHADER_BINDING_SUPER_PARAMS}); ${SHADER_BINDING_CTOR} } /** * Binds all the required data to the shader when rendering. * @param effect - defines the effect to bind the data to${EXTRA_BIND_DOCSTRING} */ public override bind(effect: Effect${EXTRA_BIND_PARAMS}): void { ${SHADER_BINDING_BIND} } } /** * The implementation of the ${BLOCK_NAME} */ export class ${BLOCK_NAME} extends ${SHADER_BLOCK_EXTENDS} { /** * The class name of the block. */ public static override ClassName = "${BLOCK_NAME}"; /** * The namespace of the block. */ public static override Namespace = "${NAMESPACE}"; ${BLOCK_INPUT_PROPERTIES} /** * The shader program (vertex and fragment code) to use to render the block */ public static override ShaderCode = BlockShaderProgram; /** * Instantiates a new ${BLOCK_NAME}. * @param smartFilter - The smart filter this block belongs to * @param name - The friendly name of the block */ constructor(smartFilter: SmartFilter, name: string) { super(smartFilter, name, ${BLOCK_DISABLE_OPTIMIZATION}${BLOCK_DISABLE_STRATEGY}); } /** * Get the class instance that binds all the required data to the shader (effect) when rendering. * @returns The class instance that binds the data to the effect */ public getShaderBinding(): ${SHADER_BINDING_EXTENDS} { ${BLOCK_GET_SHADER_BINDING_VARS} return new ${BLOCK_NAME}ShaderBinding(${BLOCK_GET_SHADER_PARAM_LIST}); } } `; /** * Converts a single shader to a .ts file which exports a Smart Filter block * @param fragmentShaderPath - The path to the fragment file for the shader * @param smartFiltersCorePath - The path to import the Smart Filters core from * @param babylonCorePath - The path to import the Babylon core from (optional) */ export function ConvertGlslIntoBlock(fragmentShaderPath: string, smartFiltersCorePath: string, babylonCorePath?: string): void { const extraImports: string[] = []; const { shaderProgramCode, fragmentShaderInfo } = ExtractShaderProgramFromGlsl(fragmentShaderPath, smartFiltersCorePath, false, false); // Validation if (!fragmentShaderInfo.blockType) { throw new Error("The glsl file must contain a header comment with a smartFilterBlockType value"); } // Generate shader binding private variables const shaderBindingPrivateVariables = fragmentShaderInfo.uniforms .map((uniform) => { return uniform.properties?.autoBind ? null : ShaderBindingPrivateVariablesTemplate.replace(CAMEL_CASE_UNIFORM, uniform.name).replace(CONNECTION_POINT_TYPE, GetConnectionPointTypeString(uniform.type)); }) .filter((line) => line !== null); // Generate the shader binding constructor docstring params const shaderBindingCtorDocstringParams = fragmentShaderInfo.uniforms .map((uniform) => { return uniform.properties?.autoBind ? null : ShaderBindingCtorDocstringParams.replace(new RegExp(CAMEL_CASE_UNIFORM, "g"), uniform.name); }) .filter((param) => param !== null); // Generate the shader binding constructor params const shaderBindingCtorParams = fragmentShaderInfo.uniforms .map((uniform) => { return uniform.properties?.autoBind ? null : ShaderBindingCtorParams.replace(CAMEL_CASE_UNIFORM, uniform.name).replace(CONNECTION_POINT_TYPE, GetConnectionPointTypeString(uniform.type)); }) .filter((param) => param !== null); // Generate the shader binding constructor const shaderBindingCtor = fragmentShaderInfo.uniforms .map((uniform) => { return uniform.properties?.autoBind ? null : ShaderBindingCtor.replace(new RegExp(CAMEL_CASE_UNIFORM, "g"), uniform.name); }) .filter((line) => line !== null); // Generate the shader binding bind let needWidthHeightParams = false; const shaderBindingBind = fragmentShaderInfo.uniforms.map((uniform) => { let effectValue: string; let effectSetter = GetEffectSetter(uniform.type); switch (uniform.properties?.autoBind) { case "outputResolution": { effectValue = "width, height"; effectSetter = "setFloat2"; needWidthHeightParams = true; } break; case "outputAspectRatio": { effectValue = "width / height, height / width"; effectSetter = "setFloat2"; needWidthHeightParams = true; } break; default: effectValue = ShaderBindingBindRegularValue; } return ShaderBindingBind.replace(EFFECT_VALUE, effectValue).replace(new RegExp(CAMEL_CASE_UNIFORM, "g"), uniform.name).replace(EFFECT_SETTER, effectSetter); }); // Add extra params to the bind method if needed let extraBindParams = ""; let extraBindDocstring = ""; if (needWidthHeightParams) { extraBindDocstring = ` * @param width - defines the width of the output * @param height - defines the height of the output`; extraBindParams = `, width: number, height: number`; } // Generate the block input properties const blockInputProperties = fragmentShaderInfo.uniforms .map((uniform) => { if (uniform.properties?.autoBind) { return null; } else if (uniform.properties?.default !== undefined) { if (extraImports.indexOf(" createStrongRef") === -1) { extraImports.push(" createStrongRef"); } return BlockInputOptionalProperty.replace(new RegExp(CAMEL_CASE_UNIFORM, "g"), uniform.name) .replace(CONNECTION_POINT_TYPE, GetConnectionPointTypeString(uniform.type)) .replace(CONNECTION_POINT_DEFAULT_VALUE, uniform.properties.default); } else { return BlockInputProperty.replace(new RegExp(CAMEL_CASE_UNIFORM, "g"), uniform.name).replace(CONNECTION_POINT_TYPE, GetConnectionPointTypeString(uniform.type)); } }) .filter((property) => property !== null); // Generate the block get shader binding vars const blockGetShaderBindingVars = fragmentShaderInfo.uniforms .map((uniform) => { return uniform.properties?.autoBind ? null : BlockGetShaderBindingVars.replace(new RegExp(CAMEL_CASE_UNIFORM, "g"), uniform.name); }) .filter((line) => line !== null); // Handle the disable optimization flag const disableOptimization = fragmentShaderInfo.disableOptimization === true ? "true" : "false"; // Generate the block get shader param list const blockGetShaderParamList = fragmentShaderInfo.uniforms .map((uniform) => { return uniform.properties?.autoBind ? null : uniform.name; }) .filter((param) => param !== null); // Decide if this is a disableable block or not let shaderBlockExtends = "ShaderBlock"; let shaderBindingExtends = "ShaderBinding"; let blockDisableStrategy = ""; let shaderBindingSuperParams = ""; if (fragmentShaderInfo.blockDisableStrategy) { shaderBlockExtends = "DisableableShaderBlock"; shaderBindingExtends = "DisableableShaderBinding"; blockDisableStrategy = `, BlockDisableStrategy.${BlockDisableStrategy[fragmentShaderInfo.blockDisableStrategy]}`; shaderBindingSuperParams = "parentBlock"; shaderBindingCtorDocstringParams.unshift(" * @param parentBlock - IDisableableBlock"); shaderBindingCtorParams.unshift(" parentBlock: IDisableableBlock"); blockGetShaderParamList.unshift("this"); extraImports.push(" type IDisableableBlock"); extraImports.push(" BlockDisableStrategy"); shaderBindingBind.unshift(" super.bind(effect);"); } // Additional validation if (fragmentShaderInfo.blockDisableStrategy !== undefined && fragmentShaderInfo.blockDisableStrategy !== BlockDisableStrategy.Manual) { if (fragmentShaderInfo.uniforms.findIndex((uniform) => uniform.name === "disabled") !== -1) { throw new Error("A block that uses a BlockDisableStrategy other than Manual should not declare its own 'disabled' uniform"); } } // Generate final contents const finalContents = FileTemplate.replace(SHADER_PROGRAM, shaderProgramCode) .replace(EXTRA_IMPORTS, extraImports.join(",\n")) .replace(BABYLON_CORE_PATH, babylonCorePath || "@babylonjs/core") .replace(SMART_FILTER_CORE_IMPORT, smartFiltersCorePath) .replace(new RegExp(BLOCK_NAME, "g"), fragmentShaderInfo.blockType) .replace(NAMESPACE, fragmentShaderInfo.namespace || "Other") .replace(new RegExp(SHADER_BINDING_EXTENDS, "g"), shaderBindingExtends) .replace(SHADER_BINDING_PRIVATE_VARIABLES, shaderBindingPrivateVariables.join("\n")) .replace(SHADER_BINDING_CTOR_DOCSTRING_PARAMS, shaderBindingCtorDocstringParams.join("\n")) .replace(SHADER_BINDING_CTOR_PARAMS, shaderBindingCtorParams.join(",\n")) .replace(SHADER_BINDING_CTOR, shaderBindingCtor.join("\n")) .replace(SHADER_BINDING_SUPER_PARAMS, shaderBindingSuperParams) .replace(SHADER_BINDING_BIND, shaderBindingBind.join("\n")) .replace(EXTRA_BIND_DOCSTRING, extraBindDocstring) .replace(EXTRA_BIND_PARAMS, extraBindParams) .replace(new RegExp(SHADER_BLOCK_EXTENDS, "g"), shaderBlockExtends) .replace(BLOCK_INPUT_PROPERTIES, blockInputProperties.join("\n")) .replace(BLOCK_DISABLE_OPTIMIZATION, disableOptimization) .replace(BLOCK_DISABLE_STRATEGY, blockDisableStrategy) .replace(BLOCK_GET_SHADER_BINDING_VARS, blockGetShaderBindingVars.join("\n")) .replace(BLOCK_GET_SHADER_PARAM_LIST, blockGetShaderParamList.join(",")); // Write the block class TS file const outputFullPathAndFileName = fragmentShaderPath.replace(".glsl", ".ts"); fs.writeFileSync(outputFullPathAndFileName, finalContents); } /** * Get the string representation of a connection point type * @param type - The connection point type * @returns - The string representation of the connection point type */ function GetConnectionPointTypeString(type: ConnectionPointType): string { switch (type) { case ConnectionPointType.Float: return "Float"; case ConnectionPointType.Texture: return "Texture"; case ConnectionPointType.Color3: return "Color3"; case ConnectionPointType.Color4: return "Color4"; case ConnectionPointType.Vector2: return "Vector2"; case ConnectionPointType.Boolean: return "Boolean"; } } /** * Get the effect setter for a connection point type * @param type - The connection point type * @returns - The effect setter for the connection point type */ function GetEffectSetter(type: ConnectionPointType): string { switch (type) { case ConnectionPointType.Float: return "setFloat"; case ConnectionPointType.Texture: return "setTexture"; case ConnectionPointType.Color3: return "setColor3"; case ConnectionPointType.Color4: return "setDirectColor4"; case ConnectionPointType.Vector2: return "setVector2"; case ConnectionPointType.Boolean: return "setBool"; } }