Skip to content

Commit be8f552

Browse files
authored
NME: Add SmartFilterFragmentOutputBlock (#16751)
I previously neglected use cases for what we call "generator" SmartFilters-- ones that don't sample a texture ever. To keep SFE logic contained in its own blocks, while ensuring it is mandatory, this PR introduces an extension of FragmentOutputBlock. Non-texture-related logic is moved out of SmartFilterTextureBlock and into SmartFilterFragmentOutputBlock.
1 parent 6cd198f commit be8f552

File tree

8 files changed

+148
-101
lines changed

8 files changed

+148
-101
lines changed

packages/dev/core/src/Materials/Node/Blocks/Dual/smartFilterTextureBlock.ts

Lines changed: 9 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
1-
import { NodeMaterialBlockConnectionPointTypes } from "../../Enums/nodeMaterialBlockConnectionPointTypes";
21
import type { NodeMaterialBuildState } from "../../nodeMaterialBuildState";
32
import { NodeMaterialBlockTargets } from "../../Enums/nodeMaterialBlockTargets";
4-
import { NodeMaterialModes } from "../../Enums/nodeMaterialModes";
53
import { CurrentScreenBlock } from "./currentScreenBlock";
64
import { RegisterClass } from "core/Misc/typeStore";
75
import { InputBlock } from "../Input/inputBlock";
86
import type { NodeMaterialBlock } from "../../nodeMaterialBlock";
97
import type { NodeMaterial } from "../../nodeMaterial";
10-
import { ScreenSizeBlock } from "../Fragment/screenSizeBlock";
11-
import { ShaderLanguage } from "core/Materials/shaderLanguage";
128
import { editableInPropertyPage, PropertyTypeForEdition } from "core/Decorators/nodeDecorator";
139
import type { Scene } from "core/scene";
14-
15-
/** @internal */
16-
export const SfeModeDefine = "USE_SFE_FRAMEWORK";
10+
import { SfeModeDefine } from "../Fragment/smartFilterFragmentOutputBlock";
11+
import { NodeMaterialBlockConnectionPointTypes } from "../../Enums/nodeMaterialBlockConnectionPointTypes";
1712

1813
/**
1914
* Base block used for creating Smart Filter shader blocks for the SFE framework.
2015
* This block extends the functionality of CurrentScreenBlock, as both are used
2116
* to represent arbitrary 2D textures to compose, and work similarly.
2217
*/
2318
export class SmartFilterTextureBlock extends CurrentScreenBlock {
19+
private _firstInit: boolean = true;
20+
2421
/**
2522
* A boolean indicating whether this block should be the main input for the SFE pipeline.
2623
* If true, it can be used in SFE for auto-disabling.
@@ -49,58 +46,10 @@ export class SmartFilterTextureBlock extends CurrentScreenBlock {
4946
* @param state defines the state that will be used for the build
5047
*/
5148
public override initialize(state: NodeMaterialBuildState) {
52-
super.initialize(state);
53-
54-
this._samplerName = state._getFreeVariableName(this.name);
55-
56-
if (state.sharedData.nodeMaterial.mode !== NodeMaterialModes.SFE) {
57-
state.sharedData.raiseBuildError("SmartFilterTextureBlock should not be used outside of SFE mode.");
58-
}
59-
60-
if (state.sharedData.nodeMaterial.shaderLanguage !== ShaderLanguage.GLSL) {
61-
state.sharedData.raiseBuildError("WebGPU is not supported by SmartFilterTextureBlock.");
49+
if (this._firstInit) {
50+
this._samplerName = state._getFreeVariableName(this.name);
51+
this._firstInit = false;
6252
}
63-
64-
// Tell FragmentOutputBlock ahead of time to store the final color in a temp variable
65-
if (!state._customOutputName && state.target === NodeMaterialBlockTargets.Fragment) {
66-
state._customOutputName = "outColor";
67-
}
68-
69-
// Annotate uniforms of InputBlocks and bindable blocks with their current values
70-
if (!state.sharedData.formatConfig.getUniformAnnotation) {
71-
state.sharedData.formatConfig.getUniformAnnotation = (name: string) => {
72-
for (const block of state.sharedData.nodeMaterial.attachedBlocks) {
73-
if (block instanceof InputBlock && block.isUniform && block.associatedVariableName === name) {
74-
return this._generateInputBlockAnnotation(block);
75-
}
76-
if (block instanceof ScreenSizeBlock && block.associatedVariableName === name) {
77-
return this._generateScreenSizeBlockAnnotation();
78-
}
79-
}
80-
return "";
81-
};
82-
}
83-
84-
// Do our best to clean up variable names, as they will be used as display names.
85-
state.sharedData.formatConfig.formatVariablename = (n: string) => {
86-
let name = n;
87-
88-
const hasUnderscoredPrefix = name.length > 1 && name[1] === "_";
89-
if (hasUnderscoredPrefix) {
90-
name = name.substring(2);
91-
}
92-
93-
return name.replace(/[^a-zA-Z]+/g, "");
94-
};
95-
}
96-
97-
private _generateInputBlockAnnotation(inputBlock: InputBlock): string {
98-
const value = inputBlock.valueCallback ? inputBlock.valueCallback() : inputBlock.value;
99-
return `// { "default": ${JSON.stringify(value)} }\n`;
100-
}
101-
102-
private _generateScreenSizeBlockAnnotation(): string {
103-
return `// { "autoBind": "outputResolution" }\n`;
10453
}
10554

10655
protected override _getMainUvName(state: NodeMaterialBuildState): string {
@@ -137,35 +86,8 @@ export class SmartFilterTextureBlock extends CurrentScreenBlock {
13786
}
13887
}
13988

140-
protected override _buildBlock(state: NodeMaterialBuildState) {
141-
super._buildBlock(state);
142-
143-
if (state.target === NodeMaterialBlockTargets.Fragment) {
144-
// Add the header JSON for the SFE block
145-
if (!state._injectAtTop) {
146-
state._injectAtTop = `// { "smartFilterBlockType": "${state.sharedData.nodeMaterial.name}", "namespace": "Babylon.NME.Exports" }`;
147-
}
148-
149-
// Convert the main fragment function into a helper function, to later be inserted in an SFE pipeline.
150-
if (!state._customEntryHeader) {
151-
state._customEntryHeader += `#ifdef ${SfeModeDefine}\n`;
152-
state._customEntryHeader += `vec4 nmeMain(vec2 ${this._mainUVName}) { // main\n`;
153-
state._customEntryHeader += `#else\n`;
154-
state._customEntryHeader += `void main(void) {\n`;
155-
state._customEntryHeader += `#endif\n`;
156-
state._customEntryHeader += `vec4 outColor = vec4(0.0);\n`;
157-
}
158-
159-
if (!state._injectAtEnd) {
160-
state._injectAtEnd += `\n#ifndef ${SfeModeDefine}\n`;
161-
state._injectAtEnd += `gl_FragColor = outColor;\n`;
162-
state._injectAtEnd += `#else\n`;
163-
state._injectAtEnd += `return outColor;\n`;
164-
state._injectAtEnd += `#endif\n`;
165-
}
166-
}
167-
168-
return this;
89+
public override _postBuildBlock(): void {
90+
this._firstInit = true;
16991
}
17092

17193
public override serialize(): any {

packages/dev/core/src/Materials/Node/Blocks/Fragment/fragmentOutputBlock.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class FragmentOutputBlock extends NodeMaterialBlock {
3131
private _linearDefineName: string;
3232
private _gammaDefineName: string;
3333
private _additionalColorDefineName: string;
34+
protected _outputString: string;
3435

3536
/**
3637
* Create a new FragmentOutputBlock
@@ -133,6 +134,10 @@ export class FragmentOutputBlock extends NodeMaterialBlock {
133134
return this._inputs[3];
134135
}
135136

137+
protected _getOutputString(state: NodeMaterialBuildState): string {
138+
return state.shaderLanguage === ShaderLanguage.WGSL ? "fragmentOutputsColor" : "gl_FragColor";
139+
}
140+
136141
public override prepareDefines(defines: NodeMaterialDefines, nodeMaterial: NodeMaterial) {
137142
defines.setValue(this._linearDefineName, this.convertToLinearSpace, true);
138143
defines.setValue(this._gammaDefineName, this.convertToGammaSpace, true);
@@ -175,12 +180,9 @@ export class FragmentOutputBlock extends NodeMaterialBlock {
175180
const comments = `//${this.name}`;
176181
state._emitFunctionFromInclude("helperFunctions", comments);
177182

178-
let outputString = "gl_FragColor";
179-
if (state._customOutputName) {
180-
outputString = state._customOutputName;
181-
} else if (state.shaderLanguage === ShaderLanguage.WGSL) {
182-
state.compilationString += `var fragmentOutputsColor : vec4<f32>;\r\n`;
183-
outputString = "fragmentOutputsColor";
183+
const outputString = this._getOutputString(state);
184+
if (state.shaderLanguage === ShaderLanguage.WGSL) {
185+
state.compilationString += `var ${outputString} : vec4<f32>;\r\n`;
184186
}
185187

186188
const vec4 = state._getShaderType(NodeMaterialBlockConnectionPointTypes.Vector4);
@@ -236,7 +238,7 @@ export class FragmentOutputBlock extends NodeMaterialBlock {
236238

237239
if (state.shaderLanguage === ShaderLanguage.WGSL) {
238240
state.compilationString += `#if !defined(PREPASS)\r\n`;
239-
state.compilationString += `fragmentOutputs.color = fragmentOutputsColor;\r\n`;
241+
state.compilationString += `fragmentOutputs.color = ${outputString};\r\n`;
240242
state.compilationString += `#endif\r\n`;
241243
}
242244

packages/dev/core/src/Materials/Node/Blocks/Fragment/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from "./fragmentOutputBlock";
2+
export * from "./smartFilterFragmentOutputBlock";
23
export * from "./imageProcessingBlock";
34
export * from "./perturbNormalBlock";
45
export * from "./discardBlock";
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { FragmentOutputBlock } from "./fragmentOutputBlock";
2+
import type { NodeMaterialBuildState } from "../../nodeMaterialBuildState";
3+
import { NodeMaterialModes } from "../../Enums/nodeMaterialModes";
4+
import { RegisterClass } from "core/Misc/typeStore";
5+
import { InputBlock } from "../Input/inputBlock";
6+
import { ScreenSizeBlock } from "../Fragment/screenSizeBlock";
7+
import { ShaderLanguage } from "core/Materials/shaderLanguage";
8+
9+
/** @internal */
10+
export const SfeModeDefine = "USE_SFE_FRAMEWORK";
11+
12+
/**
13+
* Block used to output the final color with Smart Filters structural support.
14+
*/
15+
export class SmartFilterFragmentOutputBlock extends FragmentOutputBlock {
16+
/**
17+
* Create a new SmartFilterFragmentOutputBlock
18+
* @param name defines the block name
19+
*/
20+
public constructor(name: string) {
21+
super(name);
22+
}
23+
24+
/**
25+
* Gets the current class name
26+
* @returns the class name
27+
*/
28+
public override getClassName() {
29+
return "SmartFilterFragmentOutputBlock";
30+
}
31+
32+
/**
33+
* Initialize the block and prepare the context for build
34+
* @param state defines the state that will be used for the build
35+
*/
36+
public override initialize(state: NodeMaterialBuildState) {
37+
super.initialize(state);
38+
39+
if (state.sharedData.nodeMaterial.mode !== NodeMaterialModes.SFE) {
40+
state.sharedData.raiseBuildError("SmartFilterFragmentOutputBlock should not be used outside of SFE mode.");
41+
}
42+
43+
if (state.sharedData.nodeMaterial.shaderLanguage !== ShaderLanguage.GLSL) {
44+
state.sharedData.raiseBuildError("WebGPU is not supported in SmartFilters mode.");
45+
}
46+
47+
// Annotate uniforms of InputBlocks and bindable blocks with their current values
48+
if (!state.sharedData.formatConfig.getUniformAnnotation) {
49+
state.sharedData.formatConfig.getUniformAnnotation = (name: string) => {
50+
for (const block of state.sharedData.nodeMaterial.attachedBlocks) {
51+
if (block instanceof InputBlock && block.isUniform && block.associatedVariableName === name) {
52+
return this._generateInputBlockAnnotation(block);
53+
}
54+
if (block instanceof ScreenSizeBlock && block.associatedVariableName === name) {
55+
return this._generateScreenSizeBlockAnnotation();
56+
}
57+
}
58+
return "";
59+
};
60+
}
61+
62+
// Do our best to clean up variable names, as they will be used as display names.
63+
state.sharedData.formatConfig.formatVariablename = (n: string) => {
64+
let name = n;
65+
66+
const hasUnderscoredPrefix = name.length > 1 && name[1] === "_";
67+
if (hasUnderscoredPrefix) {
68+
name = name.substring(2);
69+
}
70+
71+
return name.replace(/[^a-zA-Z]+/g, "");
72+
};
73+
}
74+
75+
private _generateInputBlockAnnotation(inputBlock: InputBlock): string {
76+
const value = inputBlock.valueCallback ? inputBlock.valueCallback() : inputBlock.value;
77+
return `// { "default": ${JSON.stringify(value)} }\n`;
78+
}
79+
80+
private _generateScreenSizeBlockAnnotation(): string {
81+
return `// { "autoBind": "outputResolution" }\n`;
82+
}
83+
84+
private _getMainUvName(state: NodeMaterialBuildState): string {
85+
// Get the ScreenUVBlock's name, which is required for SFE and should be vUV.
86+
// NOTE: In the future, when we move to vertex shaders, update this to check for the nearest vec2 varying output.
87+
const screenUv = state.sharedData.nodeMaterial.getInputBlockByPredicate((b) => b.isAttribute && b.name === "postprocess_uv");
88+
if (!screenUv || !screenUv.isAnAncestorOf(this)) {
89+
return "";
90+
}
91+
return screenUv.associatedVariableName;
92+
}
93+
94+
protected override _getOutputString(): string {
95+
return "outColor";
96+
}
97+
98+
protected override _buildBlock(state: NodeMaterialBuildState) {
99+
super._buildBlock(state);
100+
101+
const outputString = this._getOutputString();
102+
103+
state._injectAtTop = `// { "smartFilterBlockType": "${state.sharedData.nodeMaterial.name}", "namespace": "Babylon.NME.Exports" }`;
104+
105+
state._customEntryHeader += `#ifdef ${SfeModeDefine}\n`;
106+
state._customEntryHeader += `vec4 nmeMain(vec2 ${this._getMainUvName(state)}) { // main\n`;
107+
state._customEntryHeader += `#else\n`;
108+
state._customEntryHeader += `void main(void) {\n`;
109+
state._customEntryHeader += `#endif\n`;
110+
state._customEntryHeader += `vec4 ${outputString} = vec4(0.0);\n`;
111+
112+
state.compilationString += `\n#ifndef ${SfeModeDefine}\n`;
113+
state.compilationString += `gl_FragColor = ${outputString};\n`;
114+
state.compilationString += `#else\n`;
115+
state.compilationString += `return ${outputString};\n`;
116+
state.compilationString += `#endif\n`;
117+
118+
return this;
119+
}
120+
}
121+
122+
RegisterClass("BABYLON.SmartFilterFragmentOutputBlock", SmartFilterFragmentOutputBlock);

packages/dev/core/src/Materials/Node/nodeMaterial.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import type { ImageProcessingConfiguration } from "../imageProcessingConfigurati
2121
import type { Nullable } from "../../types";
2222
import { VertexBuffer } from "../../Buffers/buffer";
2323
import { Tools } from "../../Misc/tools";
24-
import { SfeModeDefine } from "./Blocks/Dual/smartFilterTextureBlock";
24+
import { SfeModeDefine } from "./Blocks/Fragment/smartFilterFragmentOutputBlock";
2525
import { TransformBlock } from "./Blocks/transformBlock";
2626
import { VertexOutputBlock } from "./Blocks/Vertex/vertexOutputBlock";
2727
import { FragmentOutputBlock } from "./Blocks/Fragment/fragmentOutputBlock";

packages/dev/core/src/Materials/Node/nodeMaterialBuildState.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,6 @@ export class NodeMaterialBuildState {
8383
/** @internal */
8484
public _customEntryHeader = "";
8585
/** @internal */
86-
public _customOutputName = "";
87-
/** @internal */
8886
private _repeatableContentAnchorIndex = 0;
8987
/** @internal */
9088
public _builtCompilationString = "";

packages/dev/core/src/Materials/Node/nodeMaterialDefault.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { Tools } from "core/Misc/tools";
1414
import { SmartFilterTextureBlock } from "./Blocks/Dual/smartFilterTextureBlock";
1515
import { Color4 } from "core/Maths/math.color";
1616
import { AddBlock } from "./Blocks/addBlock";
17+
import { SmartFilterFragmentOutputBlock } from "./Blocks/Fragment/smartFilterFragmentOutputBlock";
1718

1819
/**
1920
* Clear the material and set it to a default state for gaussian splatting
@@ -107,7 +108,7 @@ export function SetToDefaultSFE(nodeMaterial: NodeMaterial): void {
107108
color.connectTo(multiply);
108109
currentScreen.connectTo(multiply);
109110

110-
const fragmentOutput = new FragmentOutputBlock("FragmentOutput");
111+
const fragmentOutput = new SmartFilterFragmentOutputBlock("FragmentOutput");
111112
multiply.connectTo(fragmentOutput);
112113

113114
nodeMaterial.addOutputNode(fragmentOutput);

packages/tools/nodeEditor/src/components/nodeList/nodeListComponent.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export class NodeListComponent extends React.Component<INodeListComponentProps,
8989
FragmentOutputBlock: "A mandatory final node for outputing the color of each pixel",
9090
PrePassOutputBlock: "An optional final node for outputing geometry data on prepass textures",
9191
VertexOutputBlock: "A mandatory final node for outputing the position of each vertex",
92+
SmartFilterFragmentOutputBlock: "A mandatory final node for outputing the color of each pixel in Smart Filters mode",
9293
ClampBlock: "Outputs values above the maximum or below minimum as maximum or minimum values respectively",
9394
NormalizeBlock: "Remaps the length of a vector or color to 1",
9495
RemapBlock: "Remaps input value between sourceMin and sourceMax to a new range between targetMin and targetMax",
@@ -341,7 +342,7 @@ export class NodeListComponent extends React.Component<INodeListComponentProps,
341342
const allBlocks: Record<string, string[]> = {
342343
Custom_Frames: customFrameNames,
343344
Custom_Blocks: customBlockNames,
344-
SFE: ["ScreenUVBlock", "SmartFilterTextureBlock"],
345+
SFE: ["ScreenUVBlock", "SmartFilterTextureBlock", "SmartFilterFragmentOutputBlock"],
345346
Animation: ["BonesBlock", "MorphTargetsBlock"],
346347
Color_Management: ["ReplaceColorBlock", "PosterizeBlock", "GradientBlock", "DesaturateBlock", "ColorConverterBlock"],
347348
Conversion_Blocks: ["ColorMergerBlock", "ColorSplitterBlock", "VectorMergerBlock", "VectorSplitterBlock"],
@@ -492,9 +493,9 @@ export class NodeListComponent extends React.Component<INodeListComponentProps,
492493
excludeCategories = ["SFE", "PostProcess", "Particle", "Procedural__Texture", "GaussianSplatting"];
493494
break;
494495
case NodeMaterialModes.SFE:
495-
excludeCategories = ["Animation", "Mesh", "Particle", "Procedural__Texture", "PBR", "Scene", "GaussianSplatting"];
496+
excludeCategories = ["Animation", "Mesh", "Particle", "Procedural__Texture", "PostProcess", "PBR", "Scene", "GaussianSplatting"];
496497
excludeNodes = {
497-
Output_Nodes: ["VertexOutputBlock", "PrePassOutputBlock", "ClipPlanesBlock", "FragDepthBlock"],
498+
Output_Nodes: ["VertexOutputBlock", "FragmentOutputBlock", "PrePassOutputBlock", "ClipPlanesBlock", "FragDepthBlock"],
498499
Inputs: [
499500
"TextureBlock",
500501
"MaterialAlphaBlock",

0 commit comments

Comments
 (0)