@onerjs/smart-filters 8.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/license.md +21 -0
- package/package.json +52 -0
- package/readme.md +9 -0
- package/src/IDisposable.ts +9 -0
- package/src/blockFoundation/aggregateBlock.ts +148 -0
- package/src/blockFoundation/baseBlock.ts +339 -0
- package/src/blockFoundation/customAggregateBlock.ts +88 -0
- package/src/blockFoundation/customShaderBlock.ts +362 -0
- package/src/blockFoundation/disableableShaderBlock.ts +91 -0
- package/src/blockFoundation/index.ts +9 -0
- package/src/blockFoundation/inputBlock.deserializer.ts +72 -0
- package/src/blockFoundation/inputBlock.serialization.types.ts +126 -0
- package/src/blockFoundation/inputBlock.serializer.ts +150 -0
- package/src/blockFoundation/inputBlock.ts +181 -0
- package/src/blockFoundation/outputBlock.ts +144 -0
- package/src/blockFoundation/shaderBlock.ts +156 -0
- package/src/blockFoundation/textureOptions.ts +57 -0
- package/src/command/command.ts +59 -0
- package/src/command/commandBuffer.ts +71 -0
- package/src/command/commandBufferDebugger.ts +14 -0
- package/src/command/index.ts +7 -0
- package/src/connection/connectionPoint.ts +205 -0
- package/src/connection/connectionPointCompatibilityState.ts +31 -0
- package/src/connection/connectionPointDirection.ts +9 -0
- package/src/connection/connectionPointType.ts +45 -0
- package/src/connection/connectionPointWithDefault.ts +27 -0
- package/src/connection/index.ts +8 -0
- package/src/editorUtils/editableInPropertyPage.ts +106 -0
- package/src/editorUtils/index.ts +3 -0
- package/src/index.ts +16 -0
- package/src/optimization/dependencyGraph.ts +96 -0
- package/src/optimization/index.ts +1 -0
- package/src/optimization/optimizedShaderBlock.ts +131 -0
- package/src/optimization/smartFilterOptimizer.ts +757 -0
- package/src/runtime/index.ts +8 -0
- package/src/runtime/renderTargetGenerator.ts +222 -0
- package/src/runtime/shaderRuntime.ts +174 -0
- package/src/runtime/smartFilterRuntime.ts +112 -0
- package/src/runtime/strongRef.ts +18 -0
- package/src/serialization/importCustomBlockDefinition.ts +86 -0
- package/src/serialization/index.ts +10 -0
- package/src/serialization/serializedBlockDefinition.ts +12 -0
- package/src/serialization/serializedShaderBlockDefinition.ts +7 -0
- package/src/serialization/serializedSmartFilter.ts +6 -0
- package/src/serialization/smartFilterDeserializer.ts +190 -0
- package/src/serialization/smartFilterSerializer.ts +110 -0
- package/src/serialization/v1/defaultBlockSerializer.ts +21 -0
- package/src/serialization/v1/index.ts +4 -0
- package/src/serialization/v1/shaderBlockSerialization.types.ts +85 -0
- package/src/serialization/v1/smartFilterSerialization.types.ts +129 -0
- package/src/smartFilter.ts +255 -0
- package/src/utils/buildTools/buildShaders.ts +14 -0
- package/src/utils/buildTools/convertGlslIntoBlock.ts +370 -0
- package/src/utils/buildTools/convertGlslIntoShaderProgram.ts +173 -0
- package/src/utils/buildTools/convertShaders.ts +65 -0
- package/src/utils/buildTools/recordVersionNumber.js +24 -0
- package/src/utils/buildTools/shaderCode.types.ts +59 -0
- package/src/utils/buildTools/shaderConverter.ts +466 -0
- package/src/utils/buildTools/watchShaders.ts +44 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/renderTargetUtils.ts +30 -0
- package/src/utils/shaderCodeUtils.ts +192 -0
- package/src/utils/textureLoaders.ts +31 -0
- package/src/utils/textureUtils.ts +28 -0
- package/src/utils/uniqueIdGenerator.ts +28 -0
- package/src/version.ts +4 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import type { Nullable } from "core/types.js";
|
|
2
|
+
import { Logger } from "core/Misc/logger.js";
|
|
3
|
+
import type { ShaderCode, ShaderFunction } from "./shaderCode.types.js";
|
|
4
|
+
import { ConnectionPointType } from "../../connection/connectionPointType.js";
|
|
5
|
+
import { BlockDisableStrategy } from "../../blockFoundation/disableableShaderBlock.js";
|
|
6
|
+
|
|
7
|
+
// Note: creating a global RegExp object is risky, because it holds state (e.g. lastIndex) that has to be
|
|
8
|
+
// cleared at the right time to ensure correctness, which is easy to forget to do.
|
|
9
|
+
// Instead, for regular expressions used in multiple places, we define the string and options once, and create
|
|
10
|
+
// a new RegExp object from them when needed, to ensure state isn't accidentally reused.
|
|
11
|
+
|
|
12
|
+
// Matches a function's name and its parameters
|
|
13
|
+
const GetFunctionHeaderRegExString = `\\S*\\w+\\s+(\\w+)\\s*\\((.*?)\\)\\s*\\{`;
|
|
14
|
+
const GetFunctionHeaderRegExOptions = "g";
|
|
15
|
+
|
|
16
|
+
// Matches a #define statement line, capturing its name
|
|
17
|
+
const GetDefineRegExString = `^\\S*#define\\s+(\\w+).*$`;
|
|
18
|
+
const GetDefineRegExOptions = "gm";
|
|
19
|
+
|
|
20
|
+
const ReservedSymbols = ["main"];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Describes the supported metadata properties for a uniform
|
|
24
|
+
*/
|
|
25
|
+
export type UniformMetadataProperties = {
|
|
26
|
+
/**
|
|
27
|
+
* If supplied, the default value to use for the corresponding input connection point
|
|
28
|
+
*/
|
|
29
|
+
default?: any;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* If supplied, the input will be automatically bound to this value, instead of creating an input connection point
|
|
33
|
+
* @see InputAutoBindV1 for possible values.
|
|
34
|
+
*/
|
|
35
|
+
autoBind?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Describes a uniform in a shader
|
|
40
|
+
*/
|
|
41
|
+
export type UniformMetadata = {
|
|
42
|
+
/**
|
|
43
|
+
* The original name of the uniform (not renamed)
|
|
44
|
+
*/
|
|
45
|
+
name: string;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The type string of the uniform
|
|
49
|
+
*/
|
|
50
|
+
type: ConnectionPointType;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Optional properties of the uniform
|
|
54
|
+
*/
|
|
55
|
+
properties?: UniformMetadataProperties;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Information about a fragment shader
|
|
60
|
+
*/
|
|
61
|
+
export type FragmentShaderInfo = {
|
|
62
|
+
/**
|
|
63
|
+
* If supplied, the blockType to use for the block
|
|
64
|
+
*/
|
|
65
|
+
blockType?: string;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* If supplied, the namespace of the block
|
|
69
|
+
*/
|
|
70
|
+
namespace: Nullable<string>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* If true, optimization should be disabled for this shader
|
|
74
|
+
*/
|
|
75
|
+
disableOptimization?: boolean;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* If supplied, the strategy to use for making this block disableable
|
|
79
|
+
*/
|
|
80
|
+
blockDisableStrategy?: BlockDisableStrategy;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The shader code
|
|
84
|
+
*/
|
|
85
|
+
shaderCode: ShaderCode;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The set of uniforms
|
|
89
|
+
*/
|
|
90
|
+
uniforms: UniformMetadata[];
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Parses a fragment shader
|
|
95
|
+
* @param fragmentShader - The fragment shader to process
|
|
96
|
+
* @returns The processed fragment shader
|
|
97
|
+
*/
|
|
98
|
+
export function ParseFragmentShader(fragmentShader: string): FragmentShaderInfo {
|
|
99
|
+
const { header, fragmentShaderWithoutHeader } = ReadHeader(fragmentShader);
|
|
100
|
+
fragmentShader = fragmentShaderWithoutHeader;
|
|
101
|
+
const blockType = header?.[SmartFilterBlockTypeKey] || undefined;
|
|
102
|
+
const namespace = header?.namespace || null;
|
|
103
|
+
|
|
104
|
+
// Read the uniforms
|
|
105
|
+
const uniforms: UniformMetadata[] = [];
|
|
106
|
+
const uniformRegExp = new RegExp(/(\/\/\s*\{.*\}\s*(?:\r\n|\r|\n)+)?(uniform .*)/gm);
|
|
107
|
+
const uniformGroups = fragmentShader.matchAll(uniformRegExp);
|
|
108
|
+
for (const matches of uniformGroups) {
|
|
109
|
+
const annotationJSON = matches[1];
|
|
110
|
+
const uniformLine = matches[2];
|
|
111
|
+
|
|
112
|
+
if (!uniformLine) {
|
|
113
|
+
throw new Error("Uniform line not found");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const uniformLineMatches = new RegExp(/^uniform\s+(\w+)\s+(\w+)\s*;?/gm).exec(uniformLine);
|
|
117
|
+
if (!uniformLineMatches || uniformLineMatches.length < 3) {
|
|
118
|
+
throw new Error(`Uniforms must have a type and a name: '${uniformLine}'`);
|
|
119
|
+
}
|
|
120
|
+
const uniformTypeString = uniformLineMatches[1];
|
|
121
|
+
const uniformName = uniformLineMatches[2];
|
|
122
|
+
|
|
123
|
+
if (!uniformTypeString) {
|
|
124
|
+
throw new Error(`Uniforms must have a type: '${uniformLine}'`);
|
|
125
|
+
}
|
|
126
|
+
if (!uniformName) {
|
|
127
|
+
throw new Error(`Uniforms must have a name: '${uniformLine}'`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Convert to ConnectionPointType
|
|
131
|
+
let type: ConnectionPointType;
|
|
132
|
+
switch (uniformTypeString) {
|
|
133
|
+
case "float":
|
|
134
|
+
type = ConnectionPointType.Float;
|
|
135
|
+
break;
|
|
136
|
+
case "sampler2D":
|
|
137
|
+
type = ConnectionPointType.Texture;
|
|
138
|
+
break;
|
|
139
|
+
case "vec3":
|
|
140
|
+
type = ConnectionPointType.Color3;
|
|
141
|
+
break;
|
|
142
|
+
case "vec4":
|
|
143
|
+
type = ConnectionPointType.Color4;
|
|
144
|
+
break;
|
|
145
|
+
case "bool":
|
|
146
|
+
type = ConnectionPointType.Boolean;
|
|
147
|
+
break;
|
|
148
|
+
case "vec2":
|
|
149
|
+
type = ConnectionPointType.Vector2;
|
|
150
|
+
break;
|
|
151
|
+
default:
|
|
152
|
+
throw new Error(`Unsupported uniform type: '${uniformTypeString}'`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
uniforms.push({
|
|
156
|
+
name: uniformName,
|
|
157
|
+
type,
|
|
158
|
+
properties: annotationJSON ? JSON.parse(annotationJSON.replace("//", "").trim()) : undefined,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (annotationJSON) {
|
|
162
|
+
// Strip out any annotation so it isn't mistaken for function bodies
|
|
163
|
+
fragmentShader = fragmentShader.replace(annotationJSON, "");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const fragmentShaderWithNoFunctionBodies = RemoveFunctionBodies(fragmentShader);
|
|
168
|
+
|
|
169
|
+
// Collect uniform, const, and function names which need to be decorated
|
|
170
|
+
// eslint-disable-next-line prettier/prettier
|
|
171
|
+
const uniformNames = uniforms.map((uniform) => uniform.name);
|
|
172
|
+
Logger.Log(`Uniforms found: ${JSON.stringify(uniforms)}`);
|
|
173
|
+
const consts = [...fragmentShader.matchAll(/\S*const\s+\w*\s+(\w*)\s*=.*;/g)].map((match) => match[1]);
|
|
174
|
+
Logger.Log(`Consts found: ${JSON.stringify(consts)}`);
|
|
175
|
+
const defineNames = [...fragmentShader.matchAll(new RegExp(GetDefineRegExString, GetDefineRegExOptions))].map((match) => match[1]);
|
|
176
|
+
Logger.Log(`Defines found: ${JSON.stringify(defineNames)}`);
|
|
177
|
+
const functionNames = [...fragmentShaderWithNoFunctionBodies.matchAll(new RegExp(GetFunctionHeaderRegExString, GetFunctionHeaderRegExOptions))].map((match) => match[1]);
|
|
178
|
+
Logger.Log(`Functions found: ${JSON.stringify(functionNames)}`);
|
|
179
|
+
|
|
180
|
+
// Decorate the uniforms, consts, defines, and functions
|
|
181
|
+
const symbolsToDecorate = [...uniformNames, ...consts, ...defineNames, ...functionNames];
|
|
182
|
+
let fragmentShaderWithRenamedSymbols = fragmentShader;
|
|
183
|
+
for (const symbol of symbolsToDecorate) {
|
|
184
|
+
if (!symbol) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (ReservedSymbols.indexOf(symbol) !== -1) {
|
|
188
|
+
throw new Error(`Symbol "${symbol}" is reserved and cannot be used`);
|
|
189
|
+
}
|
|
190
|
+
const regex = new RegExp(`(?<=\\W+)${symbol}(?=\\W+)`, "gs");
|
|
191
|
+
fragmentShaderWithRenamedSymbols = fragmentShaderWithRenamedSymbols.replace(regex, `_${symbol}_`);
|
|
192
|
+
}
|
|
193
|
+
Logger.Log(`${symbolsToDecorate.length} symbol(s) renamed`);
|
|
194
|
+
|
|
195
|
+
// Extract all the uniforms
|
|
196
|
+
const finalUniforms = [...fragmentShaderWithRenamedSymbols.matchAll(/^\s*(uniform\s.*)/gm)].map((match) => match[1]);
|
|
197
|
+
|
|
198
|
+
// Extract all the consts
|
|
199
|
+
const finalConsts = [...fragmentShaderWithRenamedSymbols.matchAll(/^\s*(const\s.*)/gm)].map((match) => match[1]);
|
|
200
|
+
|
|
201
|
+
// Extract all the defines
|
|
202
|
+
const finalDefines = [...fragmentShaderWithRenamedSymbols.matchAll(new RegExp(GetDefineRegExString, GetDefineRegExOptions))].map((match) => match[0]);
|
|
203
|
+
|
|
204
|
+
// Find the main input
|
|
205
|
+
const mainInputs = [...fragmentShaderWithRenamedSymbols.matchAll(/\S*uniform.*\s(\w*);\s*\/\/\s*main/gm)].map((match) => match[1]);
|
|
206
|
+
if (mainInputs.length > 1) {
|
|
207
|
+
throw new Error("Shaders may have no more than 1 main input");
|
|
208
|
+
}
|
|
209
|
+
const mainInputTexture = mainInputs[0];
|
|
210
|
+
|
|
211
|
+
// Extract all the functions
|
|
212
|
+
const { extractedFunctions, mainFunctionName } = ExtractFunctions(fragmentShaderWithRenamedSymbols);
|
|
213
|
+
|
|
214
|
+
const shaderCode: ShaderCode = {
|
|
215
|
+
uniform: finalUniforms.join("\n"),
|
|
216
|
+
mainFunctionName,
|
|
217
|
+
mainInputTexture,
|
|
218
|
+
functions: extractedFunctions,
|
|
219
|
+
defines: finalDefines,
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
if (finalConsts.length > 0) {
|
|
223
|
+
shaderCode.const = finalConsts.join("\n");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
blockType,
|
|
228
|
+
namespace,
|
|
229
|
+
shaderCode,
|
|
230
|
+
uniforms,
|
|
231
|
+
disableOptimization: !!header?.disableOptimization,
|
|
232
|
+
blockDisableStrategy: header?.blockDisableStrategy,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Extracts all functions from the shader
|
|
238
|
+
* @param fragment - The shader code to process
|
|
239
|
+
* @returns A list of functions
|
|
240
|
+
*/
|
|
241
|
+
function ExtractFunctions(fragment: string): {
|
|
242
|
+
/**
|
|
243
|
+
* The extracted functions
|
|
244
|
+
*/
|
|
245
|
+
extractedFunctions: ShaderFunction[];
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The name of the main function
|
|
249
|
+
*/
|
|
250
|
+
mainFunctionName: string;
|
|
251
|
+
} {
|
|
252
|
+
const extractedFunctions: ShaderFunction[] = [];
|
|
253
|
+
let mainFunctionName: string | undefined;
|
|
254
|
+
let pos = 0;
|
|
255
|
+
|
|
256
|
+
const getFunctionHeaderRegEx = new RegExp(GetFunctionHeaderRegExString, GetFunctionHeaderRegExOptions);
|
|
257
|
+
|
|
258
|
+
while (pos < fragment.length) {
|
|
259
|
+
// Match the next available function header in the fragment code
|
|
260
|
+
getFunctionHeaderRegEx.lastIndex = pos;
|
|
261
|
+
const match = getFunctionHeaderRegEx.exec(fragment);
|
|
262
|
+
if (!match) {
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const functionName = match[1];
|
|
267
|
+
if (!functionName) {
|
|
268
|
+
throw new Error("No function name found in shader code");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const functionParams = match[2] || "";
|
|
272
|
+
|
|
273
|
+
// Store start index of the function definition
|
|
274
|
+
const startIndex = match.index;
|
|
275
|
+
|
|
276
|
+
// Balance braces to find end of function, starting just after the opening `{`
|
|
277
|
+
let endIndex = match.index + match[0].length;
|
|
278
|
+
let depth = 1;
|
|
279
|
+
while (depth > 0 && endIndex < fragment.length) {
|
|
280
|
+
if (fragment[endIndex] === "{") {
|
|
281
|
+
depth++;
|
|
282
|
+
} else if (fragment[endIndex] === "}") {
|
|
283
|
+
depth--;
|
|
284
|
+
}
|
|
285
|
+
endIndex++;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (depth !== 0) {
|
|
289
|
+
throw new Error(`Mismatched curly braces found near: ${functionName}`);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Finally, process the function code
|
|
293
|
+
let functionCode = fragment.substring(startIndex, endIndex).trim();
|
|
294
|
+
|
|
295
|
+
// Check if this function is the main function
|
|
296
|
+
if (functionCode.includes("// main")) {
|
|
297
|
+
if (mainFunctionName) {
|
|
298
|
+
throw new Error("Multiple main functions found in shader code");
|
|
299
|
+
}
|
|
300
|
+
mainFunctionName = functionName;
|
|
301
|
+
functionCode = functionCode.replace("// main", "");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
extractedFunctions.push({
|
|
305
|
+
name: functionName,
|
|
306
|
+
code: functionCode,
|
|
307
|
+
params: functionParams.trim(),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
pos = endIndex;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!mainFunctionName) {
|
|
314
|
+
throw new Error("No main function found in shader code");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return { extractedFunctions, mainFunctionName };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Removes all function bodies from the shader code, leaving just curly braces behind at the top level
|
|
322
|
+
* @param input - The shader code to process
|
|
323
|
+
* @returns The shader code with all function bodies removed
|
|
324
|
+
*/
|
|
325
|
+
function RemoveFunctionBodies(input: string): string {
|
|
326
|
+
let output: string = "";
|
|
327
|
+
let depth: number = 0;
|
|
328
|
+
|
|
329
|
+
for (let pos = 0; pos < input.length; pos++) {
|
|
330
|
+
if (input[pos] === "{") {
|
|
331
|
+
depth++;
|
|
332
|
+
// Special case - if we just hit the first { then include it
|
|
333
|
+
if (depth === 1) {
|
|
334
|
+
output += "{";
|
|
335
|
+
}
|
|
336
|
+
} else if (input[pos] === "}") {
|
|
337
|
+
depth--;
|
|
338
|
+
// Special case - if we just hit the last } then include it
|
|
339
|
+
if (depth === 0) {
|
|
340
|
+
output += "}";
|
|
341
|
+
}
|
|
342
|
+
} else if (depth === 0) {
|
|
343
|
+
output += input[pos];
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (depth !== 0) {
|
|
348
|
+
Logger.Error("Unbalanced curly braces in shader code");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return output;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const SmartFilterBlockTypeKey = "smartFilterBlockType";
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The format of the header we expect in a GLSL shader
|
|
358
|
+
*/
|
|
359
|
+
type GlslHeader = {
|
|
360
|
+
/**
|
|
361
|
+
* The block type to use for the shader, required when converting to a
|
|
362
|
+
* SerializedBlockDefinition, but not when exporting to a .ts file
|
|
363
|
+
* to be included in a hardcoded block definition
|
|
364
|
+
*/
|
|
365
|
+
[SmartFilterBlockTypeKey]: string;
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The namespace to use for the block
|
|
369
|
+
*/
|
|
370
|
+
namespace?: string;
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* If true, optimization should be disabled for this shader
|
|
374
|
+
*/
|
|
375
|
+
disableOptimization?: boolean;
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* If supplied, this will be an instance of DisableableShaderBlock, with this BlockDisableStrategy
|
|
379
|
+
* In the GLSL file, use the string key of the BlockDisableStrategy (e.g. "AutoSample").
|
|
380
|
+
*/
|
|
381
|
+
blockDisableStrategy?: BlockDisableStrategy;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Reads the GlslHeader from the shader code
|
|
386
|
+
* @param fragmentShader - The shader code to read
|
|
387
|
+
* @returns - The GlslHeader if found, otherwise null
|
|
388
|
+
*/
|
|
389
|
+
function ReadHeader(fragmentShader: string): {
|
|
390
|
+
/**
|
|
391
|
+
* The glsl header, or null if there wasn't one
|
|
392
|
+
*/
|
|
393
|
+
header: Nullable<GlslHeader>;
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* The fragment shader code with the header removed if there was one
|
|
397
|
+
*/
|
|
398
|
+
fragmentShaderWithoutHeader: string;
|
|
399
|
+
} {
|
|
400
|
+
const singleLineHeaderMatch = new RegExp(/^\n*\s*\/\/\s*(\{.*\})/g).exec(fragmentShader);
|
|
401
|
+
if (singleLineHeaderMatch && singleLineHeaderMatch[1]) {
|
|
402
|
+
return {
|
|
403
|
+
header: ParseHeader(singleLineHeaderMatch[1].trim()),
|
|
404
|
+
fragmentShaderWithoutHeader: fragmentShader.replace(singleLineHeaderMatch[0], ""),
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const multiLineHeaderMatch = new RegExp(/^\n*\s*\/\*\s*(\{.*\})\s*\*\//gs).exec(fragmentShader);
|
|
409
|
+
if (multiLineHeaderMatch && multiLineHeaderMatch[1]) {
|
|
410
|
+
return {
|
|
411
|
+
header: ParseHeader(multiLineHeaderMatch[1].trim()),
|
|
412
|
+
fragmentShaderWithoutHeader: fragmentShader.replace(multiLineHeaderMatch[0], ""),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return {
|
|
417
|
+
header: null,
|
|
418
|
+
fragmentShaderWithoutHeader: fragmentShader,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Parses the header from a string into a GlslHeader object
|
|
424
|
+
* @param header - The header string to parse
|
|
425
|
+
* @returns - The GlslHeader if the header is valid, otherwise null
|
|
426
|
+
*/
|
|
427
|
+
function ParseHeader(header: string): Nullable<GlslHeader> {
|
|
428
|
+
const parsedObject = JSON.parse(header);
|
|
429
|
+
|
|
430
|
+
if (!parsedObject || typeof parsedObject !== "object") {
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Check for required properties
|
|
435
|
+
if (!parsedObject[SmartFilterBlockTypeKey]) {
|
|
436
|
+
throw new Error("Missing required property: " + SmartFilterBlockTypeKey);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const glslHeader = parsedObject as GlslHeader;
|
|
440
|
+
|
|
441
|
+
// Fix up the disableStrategy to be a BlockDisableStrategy
|
|
442
|
+
if (glslHeader.blockDisableStrategy) {
|
|
443
|
+
const rawStrategyValue = glslHeader.blockDisableStrategy;
|
|
444
|
+
switch (rawStrategyValue as unknown as string) {
|
|
445
|
+
case "Manual":
|
|
446
|
+
glslHeader.blockDisableStrategy = BlockDisableStrategy.Manual;
|
|
447
|
+
break;
|
|
448
|
+
case "AutoSample":
|
|
449
|
+
glslHeader.blockDisableStrategy = BlockDisableStrategy.AutoSample;
|
|
450
|
+
break;
|
|
451
|
+
default:
|
|
452
|
+
throw new Error(`Invalid disableStrategy: ${rawStrategyValue}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return glslHeader;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Determines if a fragment shader has the GLSL header required for parsing
|
|
461
|
+
* @param fragmentShader - The fragment shader to check
|
|
462
|
+
* @returns True if the fragment shader has the GLSL header
|
|
463
|
+
*/
|
|
464
|
+
export function HasGlslHeader(fragmentShader: string): boolean {
|
|
465
|
+
return fragmentShader.indexOf(SmartFilterBlockTypeKey) !== -1;
|
|
466
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
/**
|
|
3
|
+
* Watches all .glsl files under <shaderPath> and rebuilds them when changed.
|
|
4
|
+
* @param shaderPath - The path to the shaders to watch
|
|
5
|
+
* @param smartFiltersCorePath - The path to import the Smart Filters core from
|
|
6
|
+
* @param babylonCorePath - The path to import the Babylon core from (optional)
|
|
7
|
+
* @example node watchShaders.js <shaderPath> @babylonjs/smart-filters
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { watch } from "chokidar";
|
|
11
|
+
import { extname } from "path";
|
|
12
|
+
import { ConvertShader } from "./convertShaders.js";
|
|
13
|
+
|
|
14
|
+
const ExternalArguments = process.argv.slice(2);
|
|
15
|
+
if (ExternalArguments.length >= 2 && ExternalArguments[0] && ExternalArguments[1]) {
|
|
16
|
+
const shaderPath = ExternalArguments[0];
|
|
17
|
+
const smartFiltersCorePath = ExternalArguments[1];
|
|
18
|
+
const babylonCorePath = ExternalArguments[2];
|
|
19
|
+
|
|
20
|
+
watch(shaderPath).on("all", (event, file) => {
|
|
21
|
+
// Only process file changes and added files
|
|
22
|
+
if (event !== "change" && event !== "add") {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Only process .glsl files
|
|
27
|
+
if (extname(file) !== ".glsl") {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`Change detected. Starting conversion...`);
|
|
32
|
+
|
|
33
|
+
// Wrap in try-catch to prevent the watcher from crashing
|
|
34
|
+
// if the new shader changes are invalid
|
|
35
|
+
try {
|
|
36
|
+
ConvertShader(file, smartFiltersCorePath, babylonCorePath);
|
|
37
|
+
console.log(`Successfully updated shader ${file}`);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
console.error(`Failed to convert shader ${file}: ${error}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
console.log(`Watching for changes in ${shaderPath}...`);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { CreateCommand } from "../command/command.js";
|
|
2
|
+
import type { BaseBlock } from "../blockFoundation/baseBlock.js";
|
|
3
|
+
import type { ShaderRuntime } from "../runtime/shaderRuntime.js";
|
|
4
|
+
import type { InternalSmartFilterRuntime } from "../runtime/smartFilterRuntime.js";
|
|
5
|
+
import type { OutputBlock } from "../blockFoundation/outputBlock.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Registers the final command of the command queue - the one that draws to either the canvas or
|
|
9
|
+
* renderTargetTexture.
|
|
10
|
+
* @param outputBlock - The output block.
|
|
11
|
+
* @param runtime - The smart filter runtime to use.
|
|
12
|
+
* @param commandOwner - The owner of the command.
|
|
13
|
+
* @param shaderBlockRuntime - The shader block runtime to use.
|
|
14
|
+
*/
|
|
15
|
+
export function RegisterFinalRenderCommand(outputBlock: OutputBlock, runtime: InternalSmartFilterRuntime, commandOwner: BaseBlock, shaderBlockRuntime: ShaderRuntime): void {
|
|
16
|
+
const commandOwnerBlockType = commandOwner.blockType;
|
|
17
|
+
if (outputBlock.renderTargetWrapper) {
|
|
18
|
+
runtime.registerCommand(
|
|
19
|
+
CreateCommand(`${commandOwnerBlockType}.renderToFinalTexture`, commandOwner, () => {
|
|
20
|
+
shaderBlockRuntime.renderToTargetWrapper(outputBlock);
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
} else {
|
|
24
|
+
runtime.registerCommand(
|
|
25
|
+
CreateCommand(`${commandOwnerBlockType}.renderToCanvas`, commandOwner, () => {
|
|
26
|
+
shaderBlockRuntime.renderToCanvas();
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
}
|