import { Logger } from "core/Misc/logger.js"; import { type Effect } from "core/Materials/effect.js"; import { ConnectionPointType, type ConnectionPointValue } from "../connection/connectionPointType.js"; import { ShaderBinding } from "../runtime/shaderRuntime.js"; import { CreateStrongRef } from "../runtime/strongRef.js"; import { type SerializedShaderBlockDefinition } from "../serialization/serializedShaderBlockDefinition.js"; import { type SerializedInputConnectionPointV1, type ConstPropertyMetadata } from "../serialization/v1/shaderBlockSerialization.types.js"; import { type SmartFilter } from "../smartFilter.js"; import { CloneShaderProgram, type ShaderProgram } from "../utils/shaderCodeUtils.js"; import { ShaderBlock } from "./shaderBlock.js"; import { type RuntimeData } from "../connection/connectionPoint.js"; import { type Nullable } from "core/types.js"; import { EditableInPropertyPage, type IEditablePropertyOption, PropertyTypeForEdition } from "../editorUtils/editableInPropertyPage.js"; import { type CustomShaderBlockData } from "./customShaderBlock.serializer.js"; /** * The binding for a CustomShaderBlock */ class CustomShaderBlockBinding extends ShaderBinding { private readonly _bindSteps: ((effect: Effect, width: number, height: number) => void)[] = []; /** * Creates a new shader binding instance for the CustomShaderBlock block. * @param inputsWithRuntimeData - The input connection points of the block */ public constructor(inputsWithRuntimeData: AnyInputWithRuntimeData[]) { super(); for (const input of inputsWithRuntimeData) { switch (input.type) { case ConnectionPointType.Float: this._bindSteps.push((effect) => { effect.setFloat(this.getRemappedName(input.name), input.runtimeData.value); }); break; case ConnectionPointType.Texture: this._bindSteps.push((effect) => { effect.setTexture(this.getRemappedName(input.name), input.runtimeData.value); }); break; case ConnectionPointType.Color3: this._bindSteps.push((effect) => { effect.setColor3(this.getRemappedName(input.name), input.runtimeData.value); }); break; case ConnectionPointType.Color4: this._bindSteps.push((effect) => { effect.setDirectColor4(this.getRemappedName(input.name), input.runtimeData.value); }); break; case ConnectionPointType.Boolean: this._bindSteps.push((effect) => { effect.setBool(this.getRemappedName(input.name), input.runtimeData.value); }); break; case ConnectionPointType.Vector2: switch (input.autoBind) { case "outputResolution": this._bindSteps.push((effect, width, height) => { effect.setFloat2(this.getRemappedName(input.name), width, height); }); break; case "outputAspectRatio": this._bindSteps.push((effect, width, height) => { effect.setFloat2(this.getRemappedName(input.name), width / height, height / width); }); break; default: this._bindSteps.push((effect) => { effect.setVector2(this.getRemappedName(input.name), input.runtimeData.value); }); } break; } } } /** * Binds all the required data to the shader when rendering. * @param effect - The effect to bind the data to * @param width - defines the width of the output * @param height - defines the height of the output */ public override bind(effect: Effect, width: number, height: number): void { for (let i = 0; i < this._bindSteps.length; i++) { this._bindSteps[i]!(effect, width, height); } } } /** * A block which loads a SerializedBlockDefinition for use in a SmartFilter. */ export class CustomShaderBlock extends ShaderBlock { /** * Deserializes a CustomShaderBlock from a serialized block definition. * @param smartFilter - The smart filter this block belongs to * @param name - Defines the name of the block * @param blockDefinition - The serialized block definition * @param data - The data property from the serialized block, if applicable * @returns The deserialized CustomShaderBlock instance */ public static Create(smartFilter: SmartFilter, name: string, blockDefinition: SerializedShaderBlockDefinition, data?: any): CustomShaderBlock { // When a new version of SerializedBlockDefinition is created, this function should be updated to handle the new properties. const newBlock = new CustomShaderBlock( smartFilter, name, blockDefinition.disableOptimization, blockDefinition.blockType, blockDefinition.namespace, blockDefinition.inputConnectionPoints, blockDefinition.fragmentConstProperties || [], blockDefinition.shaderProgram ); if (data && (data as CustomShaderBlockData).customProperties) { const customProperties = (data as CustomShaderBlockData).customProperties; for (const customProperty of customProperties) { if (newBlock.dynamicPropertyNames.indexOf(customProperty.name) !== -1) { (newBlock as any)[customProperty.name] = customProperty.value; } } } return newBlock; } /** * The class name of the block. */ public static override ClassName = "CustomShaderBlock"; private readonly _shaderProgram: ShaderProgram; private readonly _blockType: string; private readonly _namespace: Nullable; private readonly _fragmentConstProperties: ConstPropertyMetadata[]; private _autoBoundInputs: Nullable = null; /** * A list of the names of the properties added to this instance of the block, for example, * fragment const properties. * */ public readonly dynamicPropertyNames: string[] = []; /** * The type of the block - used when serializing / deserializing the block, and in the editor. */ public override get blockType(): string { return this._blockType; } /** * The namespace of the block, which is used to reduce name collisions between blocks and also to group blocks in the editor UI. * By convention, sub namespaces are separated by a period (e.g. "Babylon.Demo.Effects"). */ public override get namespace(): Nullable { return this._namespace; } /** * Instantiates a new custom shader block. * @param smartFilter - The smart filter this block belongs to * @param name - The name of the block * @param disableOptimization - If true, this optimizer will not attempt to optimize this block * @param blockType - The type of the block * @param namespace - The namespace of the block * @param inputConnectionPoints - The input connection points of the * @param fragmentConstProperties - The define properties for the block * @param shaderProgram - The shader program for the block */ private constructor( smartFilter: SmartFilter, name: string, disableOptimization: boolean, blockType: string, namespace: Nullable, inputConnectionPoints: SerializedInputConnectionPointV1[], fragmentConstProperties: ConstPropertyMetadata[], shaderProgram: ShaderProgram ) { super(smartFilter, name, disableOptimization); this._blockType = blockType; this._namespace = namespace; this._shaderProgram = shaderProgram; this._fragmentConstProperties = fragmentConstProperties; for (const input of inputConnectionPoints) { this._registerSerializedInputConnectionPointV1(input); } for (const constProperty of fragmentConstProperties) { this._createConstProperty(constProperty); } } /** * Gets the shader program to use to render the block. * @returns The shader program to use to render the block */ public override getShaderProgram() { if (this._fragmentConstProperties.length === 0) { return this._shaderProgram; } else { // Make a copy of the shader program and append const properties to the fragment shader consts const shaderProgramForThisInstance = CloneShaderProgram(this._shaderProgram); shaderProgramForThisInstance.fragment.constPerInstance = this._fragmentConstProperties .map((property) => { switch (property.type) { case "float": { const value = (this as any)[property.friendlyName] as number; const valueStr = Number.isInteger(value) ? value.toString() + "." : value.toString(); return `const float ${property.name} = ${valueStr};`; } } }) .join("\n") + "\n"; return shaderProgramForThisInstance; } } /** * Creates a dynamic property for the supplied const property with EditableInPropertyPage decorator. * @param constProperty - The const property metadata */ private _createConstProperty(constProperty: ConstPropertyMetadata): void { // Create the property and assign the default value (this as any)[constProperty.friendlyName] = constProperty.defaultValue; this.dynamicPropertyNames.push(constProperty.friendlyName); // Use the EditableInPropertyPage decorator to make the property editable in the Smart Filters Editor const editablePropertyOptions: IEditablePropertyOption = { notifiers: { rebuild: true }, blockType: this._blockType, }; if (constProperty.options) { editablePropertyOptions.options = Object.keys(constProperty.options).map((key) => { return { label: key, value: (constProperty.options as any)[key] }; }); } const propertyType: PropertyTypeForEdition = constProperty.options ? PropertyTypeForEdition.List : PropertyTypeForEdition.Float; const decoratorApplier = EditableInPropertyPage(constProperty.friendlyName, propertyType, "PROPERTIES", editablePropertyOptions); const metadata = (this.constructor as any)[Symbol.metadata] ?? ((this.constructor as any)[Symbol.metadata] = Object.create(null)); decoratorApplier(undefined, { name: constProperty.friendlyName, metadata }); } /** * Checks a specific input connection point type to see if it has a default value, and registers the input * connection point accordingly. * @param connectionPoint - The input connection point to register */ private _registerSerializedInputConnectionPointV1(connectionPoint: SerializedInputConnectionPointV1): void { if (connectionPoint.autoBind) { // Auto bound inputs are not registered as input connection points if (this._autoBoundInputs === null) { this._autoBoundInputs = []; } this._autoBoundInputs.push(connectionPoint); } else { // If not auto bound, register as an input connection point const defaultValue = this._validateDefaultValue(connectionPoint.type, connectionPoint.name, connectionPoint.defaultValue); if (defaultValue != null) { this._registerOptionalInput(connectionPoint.name, connectionPoint.type, CreateStrongRef(defaultValue)); } else { this._registerInput(connectionPoint.name, connectionPoint.type); } } } /** * Gets the shader binding for the custom shader block. * @returns The shader binding for the custom shader block */ public override getShaderBinding(): ShaderBinding { const inputs = this.inputs; const inputsToBind: AnyInputWithRuntimeData[] = inputs.map((input) => { return { name: input.name, type: input.type, runtimeData: this._confirmRuntimeDataSupplied(input), autoBind: undefined, }; }); if (this._autoBoundInputs) { for (const autoBoundInput of this._autoBoundInputs) { if ( (autoBoundInput.autoBind === "outputResolution" && autoBoundInput.type == ConnectionPointType.Vector2) || (autoBoundInput.autoBind === "outputAspectRatio" && autoBoundInput.type == ConnectionPointType.Vector2) ) { inputsToBind.push({ name: autoBoundInput.name, type: autoBoundInput.type, autoBind: autoBoundInput.autoBind, }); } else { throw new Error(`Auto bound input ${autoBoundInput.name} has an unsupported type or auto bind value`); } } } return new CustomShaderBlockBinding(inputsToBind); } /** * Validates the default value of a connection point and returns it if valid. * If the default value is not provided or is invalid, this returns null. * @param connectionPointType - The type of the connection point * @param connectionPointName - The name of the connection point * @param defaultValue - The default value of the connection point * @returns The default value, or null if no default value is provided or it was invalid */ private _validateDefaultValue( connectionPointType: U, connectionPointName: string, defaultValue?: ConnectionPointValue ): Nullable> { if (defaultValue === undefined || defaultValue === null) { return null; } // Validate the default value based on the connection point type let returnValue: Nullable>; switch (connectionPointType) { case ConnectionPointType.Float: returnValue = typeof defaultValue === "number" ? defaultValue : null; break; case ConnectionPointType.Color3: returnValue = typeof defaultValue === "object" && "r" in defaultValue && "g" in defaultValue && "b" in defaultValue ? defaultValue : null; break; case ConnectionPointType.Color4: returnValue = typeof defaultValue === "object" && "r" in defaultValue && "g" in defaultValue && "b" in defaultValue && "a" in defaultValue ? defaultValue : null; break; case ConnectionPointType.Boolean: returnValue = typeof defaultValue === "boolean" ? defaultValue : null; break; case ConnectionPointType.Vector2: returnValue = typeof defaultValue === "object" && "x" in defaultValue && "y" in defaultValue ? defaultValue : null; break; default: { Logger.Warn(`Default value supplied when unsupported. Block Type: "${this.blockType}" Connection Point: "${connectionPointName}"`); return null; } } if (returnValue === null) { Logger.Warn(`Invalid default value. Block Type: "${this.blockType}" Connection Point: "${connectionPointName}"`); } return returnValue; } } /** * Represents an input with its runtime data, enforcing type safety. */ type InputWithRuntimeData = { /** * The name of the input connection point */ name: string; /** * The type of the input connection point */ type: U; /** * The runtime data for the input connection point */ runtimeData: RuntimeData; /** * Since this is an input connection point, it will not be auto bound */ autoBind: undefined; }; /** * Represents an input which is auto-bound to the output resolution instead of an input connection point */ type AutoBindOutputResolution = { /** * The name of the input */ name: string; /** * The type of the input */ type: ConnectionPointType.Vector2; /** * The auto bind value for the input connection point */ autoBind: "outputResolution"; }; /** * Represents an input which is auto-bound to the output's aspect ratio instead of an input connection point. * The x value is the aspect ratio (width / height) and the y value is the inverse aspect ratio (height / width). */ type AutoBindOutputAspectRatio = { /** * The name of the input */ name: string; /** * The type of the input */ type: ConnectionPointType.Vector2; /** * The auto bind value for the input connection point */ autoBind: "outputAspectRatio"; }; /** * All possible input types with runtime data. */ type AnyInputWithRuntimeData = | InputWithRuntimeData | InputWithRuntimeData | InputWithRuntimeData | InputWithRuntimeData | InputWithRuntimeData | InputWithRuntimeData | AutoBindOutputResolution | AutoBindOutputAspectRatio;