@shaderfrog/core 0.0.1

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.
@@ -0,0 +1,520 @@
1
+ import { generate } from '@shaderfrog/glsl-parser';
2
+ import {
3
+ visit,
4
+ AstNode,
5
+ NodeVisitors,
6
+ Program,
7
+ Scope,
8
+ ScopeIndex,
9
+ DeclarationNode,
10
+ DeclarationStatementNode,
11
+ KeywordNode,
12
+ } from '@shaderfrog/glsl-parser/ast';
13
+ import { findAssignmentTo, findDeclarationOf } from '../ast/manipulate';
14
+ import { ComputedInput, GraphNode, mangleName } from './graph';
15
+ import { SourceNode } from './nodes/code-nodes';
16
+ import { InputCategory, nodeInput, NodeInput } from './nodes/core-node';
17
+ import { GraphDataType } from './nodes/data-nodes';
18
+
19
+ export enum StrategyType {
20
+ VARIABLE = 'Variable Names',
21
+ ASSIGNMENT_TO = 'Assignment To',
22
+ DECLARATION_OF = 'Variable Declaration',
23
+ TEXTURE_2D = 'Texture2D',
24
+ NAMED_ATTRIBUTE = 'Named Attribute',
25
+ UNIFORM = 'Uniform',
26
+ HARD_CODE = 'Hard Code Inputs',
27
+ }
28
+
29
+ export interface BaseStrategy {
30
+ type: StrategyType;
31
+ config: Object;
32
+ }
33
+
34
+ export interface AssignemntToStrategy extends BaseStrategy {
35
+ type: StrategyType.ASSIGNMENT_TO;
36
+ config: {
37
+ assignTo: string;
38
+ };
39
+ }
40
+ export const assignemntToStrategy = (
41
+ assignTo: string
42
+ ): AssignemntToStrategy => ({
43
+ type: StrategyType.ASSIGNMENT_TO,
44
+ config: { assignTo },
45
+ });
46
+
47
+ export interface Texture2DStrategy extends BaseStrategy {
48
+ type: StrategyType.TEXTURE_2D;
49
+ }
50
+ export const texture2DStrategy = (): Texture2DStrategy => ({
51
+ type: StrategyType.TEXTURE_2D,
52
+ config: {},
53
+ });
54
+
55
+ export interface UniformStrategy extends BaseStrategy {
56
+ type: StrategyType.UNIFORM;
57
+ }
58
+ export const uniformStrategy = (): UniformStrategy => ({
59
+ type: StrategyType.UNIFORM,
60
+ config: {},
61
+ });
62
+
63
+ export interface HardCodeStrategy extends BaseStrategy {
64
+ type: StrategyType.HARD_CODE;
65
+ config: { inputs: NodeInput[] };
66
+ }
67
+ export const hardCodeStrategy = (inputs: NodeInput[]): HardCodeStrategy => ({
68
+ type: StrategyType.HARD_CODE,
69
+ config: { inputs },
70
+ });
71
+
72
+ export const namedAttributeStrategy = (
73
+ attributeName: string
74
+ ): NamedAttributeStrategy => ({
75
+ type: StrategyType.NAMED_ATTRIBUTE,
76
+ config: { attributeName },
77
+ });
78
+ export interface NamedAttributeStrategy extends BaseStrategy {
79
+ type: StrategyType.NAMED_ATTRIBUTE;
80
+ config: {
81
+ attributeName: string;
82
+ };
83
+ }
84
+
85
+ export const declarationOfStrategy = (
86
+ declarationOf: string
87
+ ): DeclarationOfStrategy => ({
88
+ type: StrategyType.DECLARATION_OF,
89
+ config: { declarationOf },
90
+ });
91
+ export interface DeclarationOfStrategy extends BaseStrategy {
92
+ type: StrategyType.DECLARATION_OF;
93
+ config: {
94
+ declarationOf: string;
95
+ };
96
+ }
97
+
98
+ export interface VariableStrategy extends BaseStrategy {
99
+ type: StrategyType.VARIABLE;
100
+ }
101
+ export const variableStrategy = (): VariableStrategy => ({
102
+ type: StrategyType.VARIABLE,
103
+ config: {},
104
+ });
105
+
106
+ export type Strategy =
107
+ | UniformStrategy
108
+ | AssignemntToStrategy
109
+ | Texture2DStrategy
110
+ | NamedAttributeStrategy
111
+ | VariableStrategy
112
+ | HardCodeStrategy
113
+ | DeclarationOfStrategy;
114
+
115
+ type StrategyImpl = (
116
+ node: SourceNode,
117
+ ast: AstNode | Program,
118
+ strategy: Strategy
119
+ ) => ComputedInput[];
120
+
121
+ type Strategies = Record<StrategyType, StrategyImpl>;
122
+
123
+ const DATA_TYPE_MAP: Readonly<[GraphDataType, Set<string>][]> = [
124
+ ['vector2', new Set(['bvec2', 'dvec2', 'ivec2', 'uvec2', 'vec2'])],
125
+ ['number', new Set(['float', 'double', 'int', 'uint', 'atomic_uint'])],
126
+ ['vector3', new Set(['bvec3', 'dvec3', 'ivec3', 'uvec3', 'vec3'])],
127
+ ['vector4', new Set(['bvec4', 'dvec4', 'ivec4', 'uvec4', 'vec4'])],
128
+ ['texture', new Set(['sampler2D'])],
129
+ ['mat2', new Set(['mat2', 'dmat2'])],
130
+ ['mat3', new Set(['mat3', 'dmat3'])],
131
+ ['mat4', new Set(['mat4', 'dmat4'])],
132
+ ['mat2x2', new Set(['mat2x2', 'dmat2x2'])],
133
+ ['mat2x3', new Set(['mat2x3', 'dmat2x3'])],
134
+ ['mat2x4', new Set(['mat2x4', 'dmat2x4'])],
135
+ ['mat3x2', new Set(['mat3x2', 'dmat3x2'])],
136
+ ['mat3x3', new Set(['mat3x3', 'dmat3x3'])],
137
+ ['mat3x4', new Set(['mat3x4', 'dmat3x4'])],
138
+ ['mat4x2', new Set(['mat4x2', 'dmat4x2'])],
139
+ ['mat4x3', new Set(['mat4x3', 'dmat4x3'])],
140
+ ['mat4x4', new Set(['mat4x4', 'dmat4x4'])],
141
+ ];
142
+ /**
143
+ * Uncategorized:
144
+ *
145
+ "sampler1D"
146
+ "sampler3D"
147
+ "samplerCube"
148
+ "sampler1DShadow"
149
+ "sampler2DShadow"
150
+ "samplerCubeShadow"
151
+ "sampler1DArray"
152
+ "sampler2DArray"
153
+ "sampler1DArrayShadow"
154
+ "sampler2DArrayshadow"
155
+ "isampler1D"
156
+ "isampler2D"
157
+ "isampler3D"
158
+ "isamplerCube"
159
+ "isampler1Darray"
160
+ "isampler2DArray"
161
+ "usampler1D"
162
+ "usampler2D"
163
+ "usampler3D"
164
+ "usamplerCube"
165
+ "usampler1DArray"
166
+ "usampler2DArray"
167
+ "sampler2DRect"
168
+ "sampler2DRectshadow"
169
+ "isampler2DRect"
170
+ "usampler2DRect"
171
+ "samplerBuffer"
172
+ "isamplerBuffer"
173
+ "usamplerBuffer"
174
+ "samplerCubeArray"
175
+ "samplerCubeArrayShadow"
176
+ "isamplerCubeArray"
177
+ "usamplerCubeArray"
178
+ "sampler2DMS"
179
+ "isampler2DMS"
180
+ "usampler2DMS"
181
+ "sampler2DMSArray"
182
+ "isampler2DMSArray"
183
+ "usampler2DMSArray"
184
+ "image1D"
185
+ "iimage1D"
186
+ "uimage1D"
187
+ "image2D"
188
+ "iimage2D"
189
+ "uimage2D"
190
+ "image3D"
191
+ "iimage3D"
192
+ "uimage3D"
193
+ "image2DRect"
194
+ "iimage2DRect"
195
+ "uimage2DRect"
196
+ "imageCube"
197
+ "iimageCube"
198
+ "uimageCube"
199
+ "imageBuffer"
200
+ "iimageBuffer"
201
+ "uimageBuffer"
202
+ "image1DArray"
203
+ "iimage1DArray"
204
+ "uimage1DArray"
205
+ "image2DArray"
206
+ "iimage2DArray"
207
+ "uimage2DArray"
208
+ "imageCubeArray"
209
+ "iimageCubeArray"
210
+ "uimageCubeArray"
211
+ "image2DMS"
212
+ "iimage2DMS"
213
+ "uimage2DMS"
214
+ "image2DMArray"
215
+ "iimage2DMSArray"
216
+ "uimage2DMSArray"
217
+ "struct"
218
+ */
219
+
220
+ const mapUniformType = (type: string): GraphDataType | undefined => {
221
+ const found = DATA_TYPE_MAP.find(([_, set]) => set.has(type));
222
+ if (found) {
223
+ return found[0];
224
+ }
225
+ // console.log(`Unknown uniform type, can't map to graph: ${type}`);
226
+ };
227
+
228
+ export const applyStrategy = (
229
+ strategy: Strategy,
230
+ node: SourceNode,
231
+ ast: AstNode | Program
232
+ ) => strategyRunners[strategy.type](node, ast, strategy);
233
+
234
+ export const strategyRunners: Strategies = {
235
+ [StrategyType.HARD_CODE]: (graphNode, ast, strategy) => {
236
+ return (strategy as HardCodeStrategy).config.inputs.map((input) => [
237
+ input,
238
+ (filler) => filler,
239
+ ]);
240
+ },
241
+ [StrategyType.UNIFORM]: (graphNode, ast, strategy) => {
242
+ const program = ast as Program;
243
+ return (program.program || []).flatMap<ComputedInput>((node) => {
244
+ // The uniform declration type, like vec4
245
+ const uniformType = (node as DeclarationStatementNode).declaration
246
+ ?.specified_type?.specifier?.specifier?.token;
247
+ const graphDataType = mapUniformType(uniformType);
248
+
249
+ // If this is a uniform declaration line
250
+ if (
251
+ node.type === 'declaration_statement' &&
252
+ node.declaration?.specified_type?.qualifiers?.find(
253
+ (n: KeywordNode) => n.token === 'uniform'
254
+ )
255
+ // commented this out to allow for sampler2D uniforms to appear as inputs
256
+ // && uniformType !== 'sampler2D'
257
+ ) {
258
+ // Capture all the declared names, removing mangling suffix
259
+ const { declarations } = node.declaration;
260
+ const names = declarations.map(
261
+ (d: any) => d.identifier.identifier
262
+ ) as string[];
263
+
264
+ // Tricky code warning: The flow of preparing a node for the graph is:
265
+ // 1. Produce/mangle the AST (with unmangled names)
266
+ // 2. findInputs() (with unmangled names)
267
+ // 3. The AST is *then* mangled in graph.ts
268
+ // 4. Later, the inputs are filled in, and now, we have an input with
269
+ // the name "x" but the ast now has the mangled name "x_1". So
270
+ // here, we look for the *mangled* name in the strategy runner
271
+ return names.map<ComputedInput>((name) => [
272
+ nodeInput(
273
+ name,
274
+ `uniform_${name}`,
275
+ 'uniform',
276
+ graphDataType,
277
+ new Set<InputCategory>(['code', 'data']),
278
+ true
279
+ ),
280
+ (filler) => {
281
+ const mangledName = mangleName(name, graphNode);
282
+ // Remove the declaration line, or the declared uniform
283
+ if (declarations.length === 1) {
284
+ program.program.splice(program.program.indexOf(node), 1);
285
+ } else {
286
+ node.declaration.declarations =
287
+ node.declaration.declarations.filter(
288
+ (d: any) => d.identifier.identifier !== mangledName
289
+ );
290
+ }
291
+ // And rename all the references to said uniform
292
+ program.scopes[0].bindings[name].references.forEach((ref) => {
293
+ if (ref.type === 'identifier' && ref.identifier === mangledName) {
294
+ ref.identifier = generate(filler);
295
+ } else if (
296
+ ref.type === 'parameter_declaration' &&
297
+ 'identifier' in ref.declaration &&
298
+ ref.declaration.identifier.identifier === mangledName
299
+ ) {
300
+ ref.declaration.identifier.identifier = generate(filler);
301
+ } else if ('identifier' in ref) {
302
+ ref.identifier = generate(filler);
303
+ } else {
304
+ console.warn(
305
+ 'Unknown uniform reference for',
306
+ graphNode.name,
307
+ 'ref'
308
+ );
309
+ }
310
+ });
311
+
312
+ return ast;
313
+ },
314
+ ]);
315
+ }
316
+ return [];
317
+ });
318
+ },
319
+ [StrategyType.ASSIGNMENT_TO]: (node, ast, strategy) => {
320
+ const cast = strategy as AssignemntToStrategy;
321
+ const assignNode = findAssignmentTo(ast, cast.config.assignTo);
322
+
323
+ const name = cast.config.assignTo;
324
+ return assignNode
325
+ ? [
326
+ [
327
+ nodeInput(
328
+ name,
329
+ `filler_${name}`,
330
+ 'filler',
331
+ undefined, // Data type for what plugs into this filler
332
+ new Set<InputCategory>(['code', 'data']),
333
+ false
334
+ ),
335
+ (fillerAst) => {
336
+ assignNode.expression.right = fillerAst;
337
+ return ast;
338
+ },
339
+ ],
340
+ ]
341
+ : [];
342
+ },
343
+ [StrategyType.DECLARATION_OF]: (node, ast, strategy) => {
344
+ const cast = strategy as DeclarationOfStrategy;
345
+ const declaration = findDeclarationOf(ast, cast.config.declarationOf);
346
+ const name = cast.config.declarationOf;
347
+ return declaration
348
+ ? [
349
+ [
350
+ nodeInput(
351
+ name,
352
+ `filler_${name}`,
353
+ 'filler',
354
+ undefined, // Data type for what plugs into this filler
355
+ new Set<InputCategory>(['code', 'data']),
356
+ false
357
+ ),
358
+ (fillerAst) => {
359
+ declaration.initializer = fillerAst;
360
+ return ast;
361
+ },
362
+ ],
363
+ ]
364
+ : [];
365
+ },
366
+ [StrategyType.TEXTURE_2D]: (node, ast, strategy) => {
367
+ let texture2Dcalls: [string, AstNode, string, AstNode[]][] = [];
368
+ const seen: { [key: string]: number } = {};
369
+ const visitors: NodeVisitors = {
370
+ function_call: {
371
+ enter: (path) => {
372
+ if (
373
+ // TODO: 100 vs 300
374
+ // @ts-ignore
375
+ (path.node.identifier?.specifier?.identifier === 'texture2D' ||
376
+ // @ts-ignore
377
+ path.node.identifier?.specifier?.identifier === 'texture') &&
378
+ path.key
379
+ ) {
380
+ if (!path.parent) {
381
+ throw new Error(
382
+ 'This error is impossible. A function call always has a parent.'
383
+ );
384
+ }
385
+
386
+ const name = generate(path.node.args[0]);
387
+ seen[name] = (seen[name] || 0) + 1;
388
+ texture2Dcalls.push([
389
+ name,
390
+ path.parent as AstNode,
391
+ path.key,
392
+ // Remove the first argument and comma
393
+ (path.node.args as AstNode[]).slice(2),
394
+ ]);
395
+ }
396
+ },
397
+ },
398
+ };
399
+ visit(ast, visitors);
400
+ const names = new Set(
401
+ Object.entries(seen).reduce<string[]>(
402
+ (arr, [name, count]) => [...arr, ...(count > 1 ? [name] : [])],
403
+ []
404
+ )
405
+ );
406
+ const inputs = texture2Dcalls.map<ComputedInput>(
407
+ ([name, parent, key, texture2dArgs], index) => {
408
+ // Suffix input name if it's used more than once
409
+ const iName = names.has(name) ? `${name}_${index}` : name;
410
+ return [
411
+ nodeInput(
412
+ iName,
413
+ `filler_${iName}`,
414
+ 'filler',
415
+ 'vector4', // Data type for what plugs into this filler
416
+ new Set<InputCategory>(['code', 'data']),
417
+ false
418
+ ),
419
+ (fillerAst) => {
420
+ // @ts-ignore
421
+ parent[key] = fillerAst;
422
+ return ast;
423
+ },
424
+ texture2dArgs,
425
+ ];
426
+ }
427
+ );
428
+
429
+ return inputs;
430
+ },
431
+ [StrategyType.NAMED_ATTRIBUTE]: (node, ast, strategy) => {
432
+ const program = ast as Program;
433
+ const cast = strategy as NamedAttributeStrategy;
434
+ const { attributeName } = cast.config;
435
+ return [
436
+ [
437
+ nodeInput(
438
+ attributeName,
439
+ `filler_${attributeName}`,
440
+ 'filler',
441
+ undefined, // Data type for what plugs into this filler
442
+ new Set<InputCategory>(['code', 'data']),
443
+ true
444
+ ),
445
+ (fillerAst) => {
446
+ Object.entries(program.scopes[0].bindings).forEach(
447
+ ([name, binding]: [string, any]) => {
448
+ binding.references.forEach((ref: AstNode) => {
449
+ if (
450
+ ref.type === 'identifier' &&
451
+ ref.identifier === attributeName
452
+ ) {
453
+ ref.identifier = generate(fillerAst);
454
+ } else if (
455
+ ref.type === 'parameter_declaration' &&
456
+ 'identifier' in ref.declaration &&
457
+ ref.declaration.identifier.identifier === attributeName
458
+ ) {
459
+ ref.declaration.identifier.identifier = generate(fillerAst);
460
+ }
461
+ });
462
+ }
463
+ );
464
+ return ast;
465
+ },
466
+ ],
467
+ ];
468
+ },
469
+ [StrategyType.VARIABLE]: (node, ast, strategy) => {
470
+ const program = ast as Program;
471
+ return Object.values(
472
+ (program.scopes as Scope[]).reduce<ScopeIndex>(
473
+ (acc, scope) => ({ ...acc, ...scope.bindings }),
474
+ {}
475
+ )
476
+ ).flatMap((binding: any) => {
477
+ return (binding.references as AstNode[]).reduce<ComputedInput[]>(
478
+ (acc, ref) => {
479
+ let identifier: string, replacer;
480
+
481
+ if (ref.type === 'declaration') {
482
+ identifier = ref.identifier.identifier;
483
+ replacer = (fillerAst: AstNode | Program) => {
484
+ ref.identifier.identifier = generate(fillerAst);
485
+ return ast;
486
+ };
487
+ } else if (ref.type === 'identifier') {
488
+ identifier = ref.identifier;
489
+ replacer = (fillerAst: AstNode | Program) => {
490
+ ref.identifier = generate(fillerAst);
491
+ return ast;
492
+ };
493
+ // } else if (ref.type === 'parameter_declaration') {
494
+ // identifier = ref.declaration.identifier.identifier;
495
+ // replacer = (fillerAst: AstNode) => {
496
+ // ref.declaration.identifier.identifier = generate(fillerAst);
497
+ // };
498
+ } else {
499
+ return acc;
500
+ }
501
+ return [
502
+ ...acc,
503
+ [
504
+ nodeInput(
505
+ identifier,
506
+ `filler_${identifier}`,
507
+ 'filler',
508
+ undefined, // Data type for what plugs into this filler
509
+ new Set<InputCategory>(['code', 'data']),
510
+ false
511
+ ),
512
+ replacer,
513
+ ],
514
+ ];
515
+ },
516
+ []
517
+ );
518
+ });
519
+ },
520
+ };