@shaderfrog/core 0.0.2 → 0.1.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/README.md CHANGED
@@ -1,3 +1,186 @@
1
1
  # Shaderfrog Core
2
2
 
3
- Experimental. Do not use this.
3
+ 🚨 This library is experimental! 🚨
4
+ 🚨 The API can change at any time! 🚨
5
+
6
+ The core graph API that powers Shaderfrog. This API, built on top of the
7
+ [@Shaderfrog/glsl-parser](https://github.com/ShaderFrog/glsl-parser), compiles
8
+ Shaderfrog graphs into an intermediate result, which you then pass off to an
9
+ _engine_ (aka a plugin), to create a running GLSL shader.
10
+
11
+ ### Graph
12
+
13
+ ```typescript
14
+ interface Graph {
15
+ nodes: GraphNode[];
16
+ edges: Edge[];
17
+ }
18
+ ```
19
+
20
+ The Shaderfrog _graph_ is a list of nodes and edges. It represents all of the
21
+ GLSL code and configurations in your material. Conceptually, a graph is similar
22
+ to a dependency graph for source code, where edges represent relationships
23
+ (including dependencies) between nodes.
24
+
25
+ Each _node_ in the graph is some type of GLSL (raw source code) and configuration.
26
+ Some graph node GLSL is hard coded, as in written by you, like in a
27
+ `SourceNode`. Some source code is generated at runtime by an engine, and
28
+ injected into a node right before the graph is compiled.
29
+
30
+ Each _edge_ in the graph represents a dependency between two nodes. Edges have
31
+ different types and meanings, based on which inputs and outputs they're
32
+ connected to.
33
+
34
+ The main API function for working with graphs are `compileGraph` and
35
+ `computeGraphContext`:
36
+
37
+ ```typescript
38
+ type compileGraph = (
39
+ engineContext: EngineContext,
40
+ engine: Engine,
41
+ graph: Graph
42
+ ): CompileGraphResult
43
+
44
+ type computeGraphContext = async (
45
+ engineContext: EngineContext,
46
+ engine: Engine,
47
+ graph: Graph
48
+ ): void
49
+ ```
50
+
51
+ A graph's _context_, more specifically a node's context, is the runtime /
52
+ in-memory computed data associated with a graph node. It includes the parsed AST
53
+ representation of the node, as well as the inputs found in that AST.
54
+
55
+ ### Parsers
56
+
57
+ A graph is a vanilla Javscript object. To convert it to context, there's one
58
+ "parser" per node type in the graph, defined in the engine configuration. A
59
+ parser is an object with this interface:
60
+
61
+ ```typescript
62
+ type NodeParser = {
63
+ // cacheKey?: (graph: Graph, node: GraphNode, sibling?: GraphNode) => string;
64
+ // Callback hook to manipulate the node right before it's compiled by the
65
+ // graph. Engines use this to dynamically generate node source code.
66
+ onBeforeCompile?: OnBeforeCompile;
67
+ // Callback hook to manipulate the parsed AST. Example use is to convert
68
+ // standalone GLSL programs into code that can be used in the graph, like
69
+ // turning `void main() { out = color; }` into `vec4 main() { return color; }`
70
+ manipulateAst?: ManipulateAst;
71
+ // Find the inputs for this node type. Done dynamically because it's based on
72
+ // the source code of the node.
73
+ findInputs?: FindInputs;
74
+ // Create the filler AST offered up to nodes that import this node.
75
+ produceFiller?: ProduceNodeFiller;
76
+ };
77
+ ```
78
+
79
+ ### Engine
80
+
81
+ Shaderfrog is a GLSL editor. It's not a Three.js editor, nor a Babylon.js
82
+ editor, etc. The output of Shaderfrog is raw GLSL and metadata.
83
+
84
+ To use shaders in your _engine_, like Three.js, or even your own home grown
85
+ engine, you implement your engine as a _plugin_ to Shaderfrog. An engine
86
+ definition is verbose and likely to change:
87
+
88
+ ```typescript
89
+ export interface Engine {
90
+ // The name of your engine, like "three"
91
+ name: string;
92
+ // Which GLSL variables are defined in your engine's materials
93
+ preserve: Set<string>;
94
+ // Rules for how to merge source code from different nodes together
95
+ mergeOptions: MergeOptions;
96
+ // Parsers for your engine node types. These are combined with the
97
+ // core engine parsers
98
+ parsers: Record<string, NodeParser>;
99
+ // Functions to import graphs/code from other engines into your own
100
+ importers: EngineImporters;
101
+ // How to evaluate a node, like turning a node of { type: 'vec3' } into a
102
+ // THREE.Vector3
103
+ evaluateNode: (node: DataNode) => any;
104
+ // How to create specific nodes in your engine
105
+ constructors: {
106
+ [EngineNodeType]: NodeConstructor;
107
+ };
108
+ }
109
+ ```
110
+
111
+ ### Inputs, Holes and Fillers
112
+
113
+ Shaderfrog works by searching each node's AST for certain patterns, like
114
+ `uniform` variables, and creating an interface where you can replace each
115
+ `uniform` variable with the output of another node.
116
+
117
+ Each fillable part of the AST is called a __hole__. Holes are found by executing
118
+ user defined _strategies_ against an AST. With a program such as:
119
+
120
+ ```glsl
121
+ uniform vec2 uv;
122
+ void main() {
123
+ vec2 someVar = uv * 2.0;
124
+ }
125
+ ```
126
+
127
+ If you apply the `uniform` strategy to this code, it will mark the AST nodes
128
+ relevant to the uniform as _holes_:
129
+
130
+ ```glsl
131
+ uniform vec2 [uv];
132
+ void main() {
133
+ vec2 someVar = [uv] * 2.0;
134
+ }
135
+ ```
136
+
137
+ And it adds a new _input_ to your node, named `uv` in this case.
138
+
139
+ When you plug in the output of another node into this input, it _"fills in"_ the
140
+ hole with the _filler_ output of another node. A _filler_ is an AST node. For
141
+ example, if you have another node like:
142
+
143
+ ```glsl
144
+ vec2 myFn() {
145
+ return vec2(1.0, 1.0);
146
+ }
147
+ ```
148
+
149
+ And you plug in the `myFn` output into the `uv` input, the hole is _filled_,
150
+ resulting in:
151
+
152
+ ```glsl
153
+ vec2 myFn() {
154
+ return vec2(1.0, 1.0);
155
+ }
156
+
157
+ void main() {
158
+ vec2 someVar = myFn() * 2.0;
159
+ }
160
+ ```
161
+
162
+ Note that this is not a simple find and replace. Not only was the `uv` variable
163
+ replaced, but the declaration line `uniform vec2 uv;` was removed, and `myFn`
164
+ was inlined into the final program.
165
+
166
+ Hole filling always produces a new AST, or more accurately, a new
167
+ `ShaderSections`, which is the intermediary representation of the compilation
168
+ process.
169
+
170
+ ### Static Monkeypatching
171
+
172
+ This whole process allows Shaderfrog to monkeypatch engine shaders. When
173
+ modifying an engine shader, the process is:
174
+
175
+ - Shaderfrog creates a `BABYLON.PBRMaterial` or `Three.MeshPhysicalMaterial` (or
176
+ whatever built in material type you want)
177
+ - Shaderfrog reads the engine material's generated GLSL, and then modifies it to
178
+ add new effects by injecting new GLSL
179
+ - Shaderfrog dumps the new compiled GLSL back into the `BABYLON.PBRMaterial` or
180
+ `Three.MeshPhysicalMaterial`, and updates the material to add a new uniforms.
181
+
182
+ Injecting new GLSL into an engine shader is essentially _monkeypatching_ it:
183
+ your code is modifying an external library's code. I call this _static
184
+ monkeypatching_ because compiles new source code. This is opposed to traditional
185
+ monkeypatching in languages like Ruby, where you modify external modules by
186
+ changing them at runtime.
package/package.json CHANGED
@@ -1,10 +1,13 @@
1
1
  {
2
2
  "name": "@shaderfrog/core",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "description": "Shaderfrog core",
5
5
  "main": "index.js",
6
6
  "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
7
+ "prepare": "npm run build",
8
+ "build": "./build.sh",
9
+ "watch-test": "jest --watch",
10
+ "test": "jest --colors"
8
11
  },
9
12
  "repository": {
10
13
  "type": "git",
@@ -13,7 +16,7 @@
13
16
  "author": "Andrew Ray",
14
17
  "license": "ISC",
15
18
  "files": [
16
- "src"
19
+ "dist"
17
20
  ],
18
21
  "bugs": {
19
22
  "url": "https://github.com/ShaderFrog/core/issues"
@@ -37,8 +40,10 @@
37
40
  "lodash.groupby": "^4.6.0"
38
41
  },
39
42
  "peerDependencies": {
43
+ "@shaderfrog/glsl-parser": "^2.0.0-beta.5",
40
44
  "babylonjs": ">=4",
41
- "three": ">=0.50"
45
+ "three": ">=0.50",
46
+ "playcanvas": "^1.65.3"
42
47
  },
43
48
  "peerDependenciesMeta": {
44
49
  "babylonjs": {
@@ -46,6 +51,9 @@
46
51
  },
47
52
  "three": {
48
53
  "optional": true
54
+ },
55
+ "playcanvas": {
56
+ "optional": true
49
57
  }
50
58
  }
51
59
  }
@@ -1,392 +0,0 @@
1
- /**
2
- * Utility functions to work with ASTs
3
- */
4
- import { parser, generate } from '@shaderfrog/glsl-parser';
5
- import {
6
- visit,
7
- AstNode,
8
- NodeVisitors,
9
- ExpressionStatementNode,
10
- FunctionNode,
11
- AssignmentNode,
12
- DeclarationStatementNode,
13
- KeywordNode,
14
- DeclarationNode,
15
- } from '@shaderfrog/glsl-parser/ast';
16
- import { Program } from '@shaderfrog/glsl-parser/ast';
17
- import { ShaderStage } from '../core/graph';
18
-
19
- export const findVec4Constructor = (ast: AstNode): AstNode | undefined => {
20
- let parent: AstNode | undefined;
21
- const visitors: NodeVisitors = {
22
- function_call: {
23
- enter: (path) => {
24
- if (
25
- 'specifier' in path.node.identifier &&
26
- path.node.identifier?.specifier?.token === 'vec4'
27
- ) {
28
- parent = path.findParent((p) => 'right' in p.node)?.node;
29
- path.skip();
30
- }
31
- },
32
- },
33
- };
34
- visit(ast, visitors);
35
- return parent;
36
- };
37
-
38
- export const findAssignmentTo = (
39
- ast: AstNode | Program,
40
- assignTo: string
41
- ): ExpressionStatementNode | undefined => {
42
- let assign: ExpressionStatementNode | undefined;
43
- const visitors: NodeVisitors = {
44
- expression_statement: {
45
- enter: (path) => {
46
- if (path.node.expression?.left?.identifier === assignTo) {
47
- assign = path.node;
48
- }
49
- path.skip();
50
- },
51
- },
52
- };
53
- visit(ast, visitors);
54
- return assign;
55
- };
56
-
57
- export const findDeclarationOf = (
58
- ast: AstNode | Program,
59
- declarationOf: string
60
- ): DeclarationNode | undefined => {
61
- let declaration: DeclarationNode | undefined;
62
- const visitors: NodeVisitors = {
63
- declaration_statement: {
64
- enter: (path) => {
65
- const foundDecl = path.node.declaration?.declarations?.find(
66
- (decl: any) => decl?.identifier?.identifier === declarationOf
67
- );
68
- if (foundDecl) {
69
- declaration = foundDecl;
70
- }
71
- path.skip();
72
- },
73
- },
74
- };
75
- visit(ast, visitors);
76
- return declaration;
77
- };
78
-
79
- export const from2To3 = (ast: Program, stage: ShaderStage) => {
80
- const glOut = 'fragmentColor';
81
- // TODO: add this back in when there's only one after the merge
82
- // ast.program.unshift({
83
- // type: 'preprocessor',
84
- // line: '#version 300 es',
85
- // _: '\n',
86
- // });
87
- if (stage === 'fragment') {
88
- ast.program.unshift({
89
- type: 'declaration_statement',
90
- declaration: {
91
- type: 'declarator_list',
92
- specified_type: {
93
- type: 'fully_specified_type',
94
- qualifiers: [{ type: 'keyword', token: 'out', whitespace: ' ' }],
95
- specifier: {
96
- type: 'type_specifier',
97
- specifier: { type: 'keyword', token: 'vec4', whitespace: ' ' },
98
- quantifier: null,
99
- },
100
- },
101
- declarations: [
102
- {
103
- type: 'declaration',
104
- identifier: {
105
- type: 'identifier',
106
- identifier: glOut,
107
- whitespace: undefined,
108
- },
109
- quantifier: null,
110
- operator: undefined,
111
- initializer: undefined,
112
- },
113
- ],
114
- commas: [],
115
- },
116
- semi: { type: 'literal', literal: ';', whitespace: '\n ' },
117
- });
118
- }
119
- visit(ast, {
120
- function_call: {
121
- enter: (path) => {
122
- const identifier = path.node.identifier;
123
- if (
124
- 'specifier' in identifier &&
125
- identifier.specifier?.identifier === 'texture2D'
126
- ) {
127
- identifier.specifier.identifier = 'texture';
128
- }
129
- },
130
- },
131
- identifier: {
132
- enter: (path) => {
133
- if (path.node.identifier === 'gl_FragColor') {
134
- path.node.identifier = glOut;
135
- }
136
- },
137
- },
138
- keyword: {
139
- enter: (path) => {
140
- if (
141
- (path.node.token === 'attribute' || path.node.token === 'varying') &&
142
- path.findParent((path) => path.node.type === 'declaration_statement')
143
- ) {
144
- path.node.token =
145
- stage === 'vertex' && path.node.token === 'varying' ? 'out' : 'in';
146
- }
147
- },
148
- },
149
- });
150
- };
151
-
152
- export const outDeclaration = (name: string): Object => ({
153
- type: 'declaration_statement',
154
- declaration: {
155
- type: 'declarator_list',
156
- specified_type: {
157
- type: 'fully_specified_type',
158
- qualifiers: [{ type: 'keyword', token: 'out', whitespace: ' ' }],
159
- specifier: {
160
- type: 'type_specifier',
161
- specifier: { type: 'keyword', token: 'vec4', whitespace: ' ' },
162
- quantifier: null,
163
- },
164
- },
165
- declarations: [
166
- {
167
- type: 'declaration',
168
- identifier: {
169
- type: 'identifier',
170
- identifier: name,
171
- whitespace: undefined,
172
- },
173
- quantifier: null,
174
- operator: undefined,
175
- initializer: undefined,
176
- },
177
- ],
178
- commas: [],
179
- },
180
- semi: { type: 'literal', literal: ';', whitespace: '\n ' },
181
- });
182
-
183
- export const makeStatement = (stmt: string): AstNode => {
184
- // console.log(stmt);
185
- let ast;
186
- try {
187
- ast = parser.parse(
188
- `${stmt};
189
- `,
190
- { quiet: true }
191
- );
192
- } catch (error: any) {
193
- console.error({ stmt, error });
194
- throw new Error(`Error parsing stmt "${stmt}": ${error?.message}`);
195
- }
196
- // console.log(util.inspect(ast, false, null, true));
197
- return ast.program[0];
198
- };
199
-
200
- export const makeFnStatement = (fnStmt: string): AstNode => {
201
- let ast;
202
- try {
203
- ast = parser.parse(
204
- `
205
- void main() {
206
- ${fnStmt};
207
- }`,
208
- { quiet: true }
209
- );
210
- } catch (error: any) {
211
- console.error({ fnStmt, error });
212
- throw new Error(`Error parsing fnStmt "${fnStmt}": ${error?.message}`);
213
- }
214
-
215
- // console.log(util.inspect(ast, false, null, true));
216
- return (ast.program[0] as FunctionNode).body.statements[0];
217
- };
218
-
219
- export const makeExpression = (expr: string): AstNode => {
220
- let ast;
221
- try {
222
- ast = parser.parse(
223
- `void main() {
224
- a = ${expr};
225
- }`,
226
- { quiet: true }
227
- );
228
- } catch (error: any) {
229
- console.error({ expr, error });
230
- throw new Error(`Error parsing expr "${expr}": ${error?.message}`);
231
- }
232
-
233
- // console.log(util.inspect(ast, false, null, true));
234
- return (ast.program[0] as FunctionNode).body.statements[0].expression.right;
235
- };
236
-
237
- export const makeExpressionWithScopes = (expr: string): Program => {
238
- let ast: Program;
239
- try {
240
- ast = parser.parse(
241
- `void main() {
242
- ${expr};
243
- }`,
244
- { quiet: true }
245
- );
246
- } catch (error: any) {
247
- console.error({ expr, error });
248
- throw new Error(`Error parsing expr "${expr}": ${error?.message}`);
249
- }
250
-
251
- // console.log(util.inspect(ast, false, null, true));
252
- return {
253
- type: 'program',
254
- // Set the main() fn body scope as the global one
255
- scopes: [ast.scopes[1]],
256
- program: [(ast.program[0] as FunctionNode).body.statements[0].expression],
257
- };
258
- };
259
-
260
- export const findFn = (ast: Program, name: string): FunctionNode | undefined =>
261
- ast.program.find(
262
- (stmt): stmt is FunctionNode =>
263
- stmt.type === 'function' && stmt.prototype.header.name.identifier === name
264
- );
265
-
266
- export const returnGlPosition = (fnName: string, ast: Program): void =>
267
- convertVertexMain(fnName, ast, 'vec4', (assign) => assign.expression.right);
268
-
269
- export const returnGlPositionHardCoded = (
270
- fnName: string,
271
- ast: Program,
272
- returnType: string,
273
- hardCodedReturn: string
274
- ): void =>
275
- convertVertexMain(fnName, ast, returnType, () =>
276
- makeExpression(hardCodedReturn)
277
- );
278
-
279
- export const returnGlPositionVec3Right = (fnName: string, ast: Program): void =>
280
- convertVertexMain(fnName, ast, 'vec3', (assign) => {
281
- let found: AstNode | undefined;
282
- visit(assign, {
283
- function_call: {
284
- enter: (path) => {
285
- const { node } = path;
286
- if (
287
- // @ts-ignore
288
- node?.identifier?.specifier?.token === 'vec4' &&
289
- node?.args?.[2]?.token?.includes('1.')
290
- ) {
291
- found = node.args[0];
292
- }
293
- },
294
- },
295
- });
296
- if (!found) {
297
- console.error(generate(ast));
298
- throw new Error(
299
- 'Could not find position assignment to convert to return!'
300
- );
301
- }
302
- return found;
303
- });
304
-
305
- const convertVertexMain = (
306
- fnName: string,
307
- ast: Program,
308
- returnType: string,
309
- generateRight: (positionAssign: ExpressionStatementNode) => AstNode
310
- ) => {
311
- const mainReturnVar = `frogOut`;
312
-
313
- const main = findFn(ast, fnName);
314
- if (!main) {
315
- throw new Error(`No ${fnName} fn found!`);
316
- }
317
-
318
- // Convert the main function to one that returns
319
- (main.prototype.header.returnType.specifier.specifier as KeywordNode).token =
320
- returnType;
321
-
322
- // Find the gl_position assignment line
323
- const assign = main.body.statements.find(
324
- (stmt: AstNode) =>
325
- stmt.type === 'expression_statement' &&
326
- stmt.expression.left?.identifier === 'gl_Position'
327
- );
328
- if (!assign) {
329
- throw new Error(`No gl position assign found in main fn!`);
330
- }
331
-
332
- const rtnStmt = makeFnStatement(
333
- `${returnType} ${mainReturnVar} = 1.0`
334
- ) as DeclarationStatementNode;
335
- rtnStmt.declaration.declarations[0].initializer = generateRight(assign);
336
-
337
- main.body.statements.splice(main.body.statements.indexOf(assign), 1, rtnStmt);
338
- main.body.statements.push(makeFnStatement(`return ${mainReturnVar}`));
339
- };
340
-
341
- export const convert300MainToReturn = (fnName: string, ast: Program): void => {
342
- const mainReturnVar = `frogOut`;
343
-
344
- // Find the output variable, as in "pc_fragColor" from "out highp vec4 pc_fragColor;"
345
- let outName: string | undefined;
346
- ast.program.find((line, index) => {
347
- if (
348
- line.type === 'declaration_statement' &&
349
- line.declaration?.specified_type?.qualifiers?.find(
350
- (n: KeywordNode) => n.token === 'out'
351
- ) &&
352
- line.declaration.specified_type.specifier.specifier.token === 'vec4'
353
- ) {
354
- // Remove the out declaration
355
- ast.program.splice(index, 1);
356
- outName = line.declaration.declarations[0].identifier.identifier;
357
- return true;
358
- }
359
- });
360
- if (!outName) {
361
- console.error(generate(ast));
362
- throw new Error('No "out vec4" line found in the fragment shader');
363
- }
364
-
365
- visit(ast, {
366
- identifier: {
367
- enter: (path) => {
368
- if (path.node.identifier === outName) {
369
- path.node.identifier = mainReturnVar;
370
- // @ts-ignore
371
- path.node.doNotDescope = true; // hack because this var is in the scope which gets renamed later
372
- }
373
- },
374
- },
375
- function: {
376
- enter: (path) => {
377
- if (path.node.prototype.header.name.identifier === fnName) {
378
- (
379
- path.node.prototype.header.returnType.specifier
380
- .specifier as KeywordNode
381
- ).token = 'vec4';
382
- path.node.body.statements.unshift(
383
- makeFnStatement(`vec4 ${mainReturnVar}`)
384
- );
385
- path.node.body.statements.push(
386
- makeFnStatement(`return ${mainReturnVar}`)
387
- );
388
- }
389
- },
390
- },
391
- });
392
- };