Skip to content

Commit 2bf89ef

Browse files
Smart Filters: Add const property support - consts in GLSL defined blocks that are settable at design time (#17479)
With this change, consts in fragment shaders of blocks defined using GLSL can be annotated to be treated as properties on the resulting block. Those properties can be set before runtime creation and then are baked into the shader as consts. The properties are annotated for the Smart Filter Editor to see and display in the property pane on the right when a block instance is selected. The optimizer is updated to know that such consts cannot be consolidated into a single const for all instances of that block type, although regular unannotated consts will still be consolidated. The syntax for the annotation can either be: ``` // { "property": true } const float mode = 1.0; ``` and this will show a text box in the Smart Filter Editor: <img width="569" height="150" alt="image" src="https://github.com/user-attachments/assets/c7b9f77f-2f0e-4018-9daf-c6067ba52139" /> Or you can supply a list of options for the value: ``` // { "property": { "options": { "Normal": 0, "Crazy": 1, "Odd": 2 } } } const float mode = 1.0; ``` and this will show a drop down in the Smart Filter Editor: <img width="550" height="155" alt="image" src="https://github.com/user-attachments/assets/9f564e6d-57fe-422f-98f6-a3be16f7e619" /> This change also updates the serialization of CustomShaderBlocks to include the values of these properties in the data section: ``` { "name": "Example", "uniqueId": 34, "blockType": "ConstPropertyExample", "namespace": "Test", "comments": null, "data": { "customProperties": [ { "name": "mode", "value": 1 } ] }, "outputTextureOptions": { "ratio": 1, "format": 5, "type": 0 } } ``` Note: this change is backwards compatible. --------- Co-authored-by: AmoebaChant <[email protected]>
1 parent 8f13e90 commit 2bf89ef

File tree

21 files changed

+699
-158
lines changed

21 files changed

+699
-158
lines changed
Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,40 @@
1-
uniform sampler2D background; // main
2-
uniform sampler2D foreground;
3-
4-
uniform vec2 scaleUV;
5-
uniform vec2 translateUV;
6-
uniform float alphaMode;
7-
uniform float foregroundAlphaScale;
8-
9-
vec4 composition(vec2 vUV) { // main
10-
vec4 background = texture2D(background, vUV);
11-
12-
vec2 transformedUV = vUV * (1.0 / scaleUV) + translateUV;
13-
if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || transformedUV.y < 0.0 || transformedUV.y > 1.0) {
14-
return background;
15-
}
16-
17-
vec4 foreground = texture2D(foreground, transformedUV);
18-
foreground.a *= foregroundAlphaScale;
19-
20-
// SRC is foreground, DEST is background
21-
if (alphaMode == 0.) {
22-
return foreground;
23-
}
24-
else if (alphaMode == 1.) {
25-
return foreground.a * foreground + background;
26-
}
27-
else if (alphaMode == 2.) {
28-
return mix(background, foreground, foreground.a);
29-
}
30-
else if (alphaMode == 3.) {
31-
return background - foreground * background;
32-
}
33-
else if (alphaMode == 4.) {
34-
return foreground * background;
35-
}
36-
37-
return background;
1+
uniform sampler2D background; // main
2+
uniform sampler2D foreground;
3+
4+
// { "property": true }
5+
const float alphaMode = 1;
6+
7+
uniform vec2 scaleUV;
8+
uniform vec2 translateUV;
9+
uniform float foregroundAlphaScale;
10+
11+
vec4 composition(vec2 vUV) { // main
12+
vec4 background = texture2D(background, vUV);
13+
14+
vec2 transformedUV = vUV * (1.0 / scaleUV) + translateUV;
15+
if (transformedUV.x < 0.0 || transformedUV.x > 1.0 || transformedUV.y < 0.0 || transformedUV.y > 1.0) {
16+
return background;
17+
}
18+
19+
vec4 foreground = texture2D(foreground, transformedUV);
20+
foreground.a *= foregroundAlphaScale;
21+
22+
// SRC is foreground, DEST is background
23+
if (alphaMode == 0.) {
24+
return foreground;
25+
}
26+
else if (alphaMode == 1.) {
27+
return foreground.a * foreground + background;
28+
}
29+
else if (alphaMode == 2.) {
30+
return mix(background, foreground, foreground.a);
31+
}
32+
else if (alphaMode == 3.) {
33+
return background - foreground * background;
34+
}
35+
else if (alphaMode == 4.) {
36+
return foreground * background;
37+
}
38+
39+
return background;
3840
}

packages/dev/smartFilterBlocks/src/blocks/babylon/demo/effects/compositionBlock.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
createStrongRef,
1212
PropertyTypeForEdition,
1313
editableInPropertyPage,
14+
CloneShaderProgram,
1415
} from "smart-filters";
1516
import { compositionBlockType } from "../../../blockTypes.js";
1617
import { babylonDemoEffectsNamespace } from "../../../blockNamespaces.js";
@@ -40,7 +41,6 @@ export class CompositionShaderBinding extends DisableableShaderBinding {
4041
private readonly _foregroundWidth: RuntimeData<ConnectionPointType.Float>;
4142
private readonly _foregroundHeight: RuntimeData<ConnectionPointType.Float>;
4243
private readonly _foregroundAlphaScale: RuntimeData<ConnectionPointType.Float>;
43-
private readonly _alphaMode: number;
4444

4545
/**
4646
* Creates a new shader binding instance for the Composition block.
@@ -52,7 +52,6 @@ export class CompositionShaderBinding extends DisableableShaderBinding {
5252
* @param foregroundWidth - the width of the foreground texture
5353
* @param foregroundHeight - the height of the foreground texture
5454
* @param foregroundAlphaScale - the alpha scale of the foreground texture
55-
* @param alphaMode - the alpha mode to use
5655
*/
5756
constructor(
5857
parentBlock: IDisableableBlock,
@@ -62,8 +61,7 @@ export class CompositionShaderBinding extends DisableableShaderBinding {
6261
foregroundLeft: RuntimeData<ConnectionPointType.Float>,
6362
foregroundWidth: RuntimeData<ConnectionPointType.Float>,
6463
foregroundHeight: RuntimeData<ConnectionPointType.Float>,
65-
foregroundAlphaScale: RuntimeData<ConnectionPointType.Float>,
66-
alphaMode: number
64+
foregroundAlphaScale: RuntimeData<ConnectionPointType.Float>
6765
) {
6866
super(parentBlock);
6967
this._backgroundTexture = backgroundTexture;
@@ -73,7 +71,6 @@ export class CompositionShaderBinding extends DisableableShaderBinding {
7371
this._foregroundWidth = foregroundWidth;
7472
this._foregroundHeight = foregroundHeight;
7573
this._foregroundAlphaScale = foregroundAlphaScale;
76-
this._alphaMode = alphaMode;
7774
}
7875

7976
/**
@@ -92,9 +89,7 @@ export class CompositionShaderBinding extends DisableableShaderBinding {
9289
const foregroundWidth = this._foregroundWidth.value;
9390
const foregroundHeight = this._foregroundHeight.value;
9491
const foregroundAlphaScale = this._foregroundAlphaScale.value;
95-
const alphaMode = this._alphaMode;
9692

97-
effect.setFloat(this.getRemappedName(uniforms.alphaMode), alphaMode);
9893
effect.setTexture(this.getRemappedName(uniforms.background), background);
9994
effect.setTexture(this.getRemappedName(uniforms.foreground), foreground);
10095

@@ -183,6 +178,22 @@ export class CompositionBlock extends DisableableShaderBlock {
183178
*/
184179
public static override ShaderCode = shaderProgram;
185180

181+
/**
182+
* Gets the shader program to use to render the block.
183+
* This adds the per-instance const values to the shader program.
184+
* @returns The shader program to use to render the block
185+
*/
186+
public override getShaderProgram() {
187+
const staticShaderProgram = super.getShaderProgram();
188+
189+
// Since we are making changes only for this instance of the block, and
190+
// the disableableShaderProgram is static, we make a copy and modify that.
191+
const shaderProgramForThisInstance = CloneShaderProgram(staticShaderProgram);
192+
shaderProgramForThisInstance.fragment.constPerInstance = `const float _alphaMode_ = ${this.alphaMode.toFixed(1)};`;
193+
194+
return shaderProgramForThisInstance;
195+
}
196+
186197
/**
187198
* Instantiates a new Block.
188199
* @param smartFilter - The smart filter this block belongs to
@@ -204,8 +215,7 @@ export class CompositionBlock extends DisableableShaderBlock {
204215
const foregroundHeight = this.foregroundHeight.runtimeData;
205216
const foregroundTop = this.foregroundTop.runtimeData;
206217
const foregroundAlphaScale = this.foregroundAlphaScale.runtimeData;
207-
const alphaMode = this.alphaMode;
208218

209-
return new CompositionShaderBinding(this, background, foreground, foregroundTop, foregroundLeft, foregroundWidth, foregroundHeight, foregroundAlphaScale, alphaMode);
219+
return new CompositionShaderBinding(this, background, foreground, foregroundTop, foregroundLeft, foregroundWidth, foregroundHeight, foregroundAlphaScale);
210220
}
211221
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { ISerializedBlockV1, SerializeBlockV1 } from "../serialization/v1/smartFilterSerialization.types.js";
2+
import type { BaseBlock } from "./baseBlock.js";
3+
import { CustomShaderBlock } from "./customShaderBlock.js";
4+
5+
/**
6+
* Data for a dynamic property on a CustomShaderBlock
7+
*/
8+
export type CustomShaderBlockData = {
9+
/**
10+
* The custom properties of the CustomShaderBlock
11+
*/
12+
customProperties: CustomPropertyData[];
13+
};
14+
15+
type CustomPropertyData = {
16+
name: string;
17+
value: any;
18+
};
19+
20+
/**
21+
* Serializes a CustomShaderBlock to V1 serialized data.
22+
* @param block - The block to serialize
23+
* @returns The serialized block
24+
*/
25+
export const CustomShaderBlockSerializer: SerializeBlockV1 = (block: BaseBlock): ISerializedBlockV1 => {
26+
if (block.getClassName() !== CustomShaderBlock.ClassName) {
27+
throw new Error("Was asked to serialize an unrecognized block type");
28+
}
29+
const customShaderBlock = block as CustomShaderBlock;
30+
31+
let data: CustomShaderBlockData | undefined;
32+
const dynamicPropertyNames = customShaderBlock.dynamicPropertyNames;
33+
if (dynamicPropertyNames.length > 0) {
34+
data = {
35+
customProperties: dynamicPropertyNames.map((propertyName) => ({
36+
name: propertyName,
37+
value: (customShaderBlock as any)[propertyName],
38+
})),
39+
};
40+
}
41+
42+
return {
43+
name: customShaderBlock.name,
44+
uniqueId: customShaderBlock.uniqueId,
45+
blockType: customShaderBlock.blockType,
46+
namespace: customShaderBlock.namespace,
47+
comments: customShaderBlock.comments,
48+
data,
49+
outputTextureOptions: customShaderBlock.outputTextureOptions,
50+
};
51+
};

packages/dev/smartFilters/src/blockFoundation/customShaderBlock.ts

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { ConnectionPointType, type ConnectionPointValue } from "../connection/co
44
import { ShaderBinding } from "../runtime/shaderRuntime.js";
55
import { CreateStrongRef } from "../runtime/strongRef.js";
66
import type { SerializedShaderBlockDefinition } from "../serialization/serializedShaderBlockDefinition.js";
7-
import type { SerializedInputConnectionPointV1 } from "../serialization/v1/shaderBlockSerialization.types.js";
7+
import type { SerializedInputConnectionPointV1, ConstPropertyMetadata } from "../serialization/v1/shaderBlockSerialization.types.js";
88
import type { SmartFilter } from "../smartFilter.js";
9-
import type { ShaderProgram } from "../utils/shaderCodeUtils.js";
9+
import { CloneShaderProgram, type ShaderProgram } from "../utils/shaderCodeUtils.js";
1010
import { ShaderBlock } from "./shaderBlock.js";
1111
import type { RuntimeData } from "../connection/connectionPoint.js";
1212
import type { Nullable } from "core/types.js";
13+
import { EditableInPropertyPage, type IEditablePropertyOption, PropertyTypeForEdition } from "../editorUtils/editableInPropertyPage.js";
14+
import type { CustomShaderBlockData } from "./customShaderBlock.serializer.js";
1315

1416
/**
1517
* The binding for a CustomShaderBlock
@@ -95,20 +97,33 @@ export class CustomShaderBlock extends ShaderBlock {
9597
* @param smartFilter - The smart filter this block belongs to
9698
* @param name - Defines the name of the block
9799
* @param blockDefinition - The serialized block definition
100+
* @param data - The data property from the serialized block, if applicable
98101
* @returns The deserialized CustomShaderBlock instance
99102
*/
100-
public static Create(smartFilter: SmartFilter, name: string, blockDefinition: SerializedShaderBlockDefinition): CustomShaderBlock {
103+
public static Create(smartFilter: SmartFilter, name: string, blockDefinition: SerializedShaderBlockDefinition, data?: any): CustomShaderBlock {
101104
// When a new version of SerializedBlockDefinition is created, this function should be updated to handle the new properties.
102105

103-
return new CustomShaderBlock(
106+
const newBlock = new CustomShaderBlock(
104107
smartFilter,
105108
name,
106109
blockDefinition.disableOptimization,
107110
blockDefinition.blockType,
108111
blockDefinition.namespace,
109112
blockDefinition.inputConnectionPoints,
113+
blockDefinition.fragmentConstProperties || [],
110114
blockDefinition.shaderProgram
111115
);
116+
117+
if (data && (data as CustomShaderBlockData).customProperties) {
118+
const customProperties = (data as CustomShaderBlockData).customProperties;
119+
for (const customProperty of customProperties) {
120+
if (newBlock.dynamicPropertyNames.indexOf(customProperty.name) !== -1) {
121+
(newBlock as any)[customProperty.name] = customProperty.value;
122+
}
123+
}
124+
}
125+
126+
return newBlock;
112127
}
113128

114129
/**
@@ -119,8 +134,16 @@ export class CustomShaderBlock extends ShaderBlock {
119134
private readonly _shaderProgram: ShaderProgram;
120135
private readonly _blockType: string;
121136
private readonly _namespace: Nullable<string>;
137+
private readonly _fragmentConstProperties: ConstPropertyMetadata[];
122138
private _autoBoundInputs: Nullable<SerializedInputConnectionPointV1[]> = null;
123139

140+
/**
141+
* A list of the names of the properties added to this instance of the block, for example,
142+
* fragment const properties.
143+
*
144+
*/
145+
public readonly dynamicPropertyNames: string[] = [];
146+
124147
/**
125148
* The type of the block - used when serializing / deserializing the block, and in the editor.
126149
*/
@@ -144,6 +167,7 @@ export class CustomShaderBlock extends ShaderBlock {
144167
* @param blockType - The type of the block
145168
* @param namespace - The namespace of the block
146169
* @param inputConnectionPoints - The input connection points of the
170+
* @param fragmentConstProperties - The define properties for the block
147171
* @param shaderProgram - The shader program for the block
148172
*/
149173
private constructor(
@@ -153,25 +177,75 @@ export class CustomShaderBlock extends ShaderBlock {
153177
blockType: string,
154178
namespace: Nullable<string>,
155179
inputConnectionPoints: SerializedInputConnectionPointV1[],
180+
fragmentConstProperties: ConstPropertyMetadata[],
156181
shaderProgram: ShaderProgram
157182
) {
158183
super(smartFilter, name, disableOptimization);
159184
this._blockType = blockType;
160185
this._namespace = namespace;
186+
this._shaderProgram = shaderProgram;
187+
this._fragmentConstProperties = fragmentConstProperties;
161188

162189
for (const input of inputConnectionPoints) {
163190
this._registerSerializedInputConnectionPointV1(input);
164191
}
165192

166-
this._shaderProgram = shaderProgram;
193+
for (const constProperty of fragmentConstProperties) {
194+
this._createConstProperty(constProperty);
195+
}
167196
}
168197

169198
/**
170199
* Gets the shader program to use to render the block.
171200
* @returns The shader program to use to render the block
172201
*/
173202
public override getShaderProgram() {
174-
return this._shaderProgram;
203+
if (this._fragmentConstProperties.length === 0) {
204+
return this._shaderProgram;
205+
} else {
206+
// Make a copy of the shader program and append const properties to the fragment shader consts
207+
const shaderProgramForThisInstance = CloneShaderProgram(this._shaderProgram);
208+
shaderProgramForThisInstance.fragment.constPerInstance =
209+
this._fragmentConstProperties
210+
.map((property) => {
211+
switch (property.type) {
212+
case "float": {
213+
const value = (this as any)[property.friendlyName] as number;
214+
const valueStr = Number.isInteger(value) ? value.toString() + "." : value.toString();
215+
return `const float ${property.name} = ${valueStr};`;
216+
}
217+
}
218+
})
219+
.join("\n") + "\n";
220+
221+
return shaderProgramForThisInstance;
222+
}
223+
}
224+
225+
/**
226+
* Creates a dynamic property for the supplied const property with EditableInPropertyPage decorator.
227+
* @param constProperty - The const property metadata
228+
*/
229+
private _createConstProperty(constProperty: ConstPropertyMetadata): void {
230+
// Create the property and assign the default value
231+
(this as any)[constProperty.friendlyName] = constProperty.defaultValue;
232+
this.dynamicPropertyNames.push(constProperty.friendlyName);
233+
234+
// Use the EditableInPropertyPage decorator to make the property editable in the Smart Filters Editor
235+
const editablePropertyOptions: IEditablePropertyOption = {
236+
notifiers: { rebuild: true },
237+
blockType: this._blockType,
238+
};
239+
if (constProperty.options) {
240+
editablePropertyOptions.options = Object.keys(constProperty.options).map((key) => {
241+
return { label: key, value: (constProperty.options as any)[key] };
242+
});
243+
}
244+
245+
const propertyType: PropertyTypeForEdition = constProperty.options ? PropertyTypeForEdition.List : PropertyTypeForEdition.Float;
246+
247+
const decoratorApplier = EditableInPropertyPage(constProperty.friendlyName, propertyType, "PROPERTIES", editablePropertyOptions);
248+
decoratorApplier(this, constProperty.friendlyName);
175249
}
176250

177251
/**

0 commit comments

Comments
 (0)