Skip to content

Commit 4201812

Browse files
authored
Merge branch 'dev-2.0' into deprecate-array-vector
2 parents 6a5d0aa + aa73cea commit 4201812

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

71 files changed

+8843
-5615
lines changed

.github/workflows/ci-test.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,36 @@ jobs:
2424
env:
2525
CI: true
2626
- name: build and test
27+
id: test
2728
run: npm test
29+
continue-on-error: true
30+
env:
31+
CI: true
32+
- name: Generate Visual Test Report
33+
if: always()
34+
run: node visual-report.js
35+
env:
36+
CI: true
37+
- name: Upload Visual Test Report
38+
if: always()
39+
uses: actions/upload-artifact@v4
40+
with:
41+
name: visual-test-report
42+
path: test/unit/visual/visual-report.html
43+
retention-days: 14
44+
- name: generate TypeScript types
45+
run: npm run generate-types
46+
env:
47+
CI: true
48+
- name: test TypeScript types
49+
run: npm run test:types
2850
env:
2951
CI: true
3052
- name: report test coverage
53+
if: steps.test.outcome == 'success'
3154
run: bash <(curl -s https://codecov.io/bash) -f coverage/coverage-final.json
3255
env:
3356
CI: true
57+
- name: fail job if tests failed
58+
if: steps.test.outcome != 'success'
59+
run: exit 1

.gitignore

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,17 @@ __screenshots__/
2424
*.d.ts
2525
p5.zip
2626
yarn.lock
27+
28+
docs/data.json
29+
analyzer/
30+
preview/
31+
__screenshots__/
32+
actual-screenshots/
33+
visual-report.html
34+
2735
todo.md
2836

2937
*.DS_Store
3038
.idea
31-
.project
39+
.project
40+

contributor_docs/p5.strands.md

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
<!-- How p5.strands JS-to-GLSL compilation works. -->
2+
3+
# p5.strands Overview
4+
5+
Shader programming is an area of creative coding that can feel like a dark art to many. People share lots of stunning visuals that are created with shaders, but shaders feel like a completely different way of coding, requiring you to learn a new language, pipeline, and paradigm.
6+
7+
p5.strands hopes to address all of those issues by letting you write shader snippets in JavaScript and compiling it to OpenGL Shading Language (GLSL) for you!
8+
9+
If you're looking to start writing p5.strands shaders yourself, take a look at <a href="https://beta.p5js.org/tutorials/intro-to-p5-strands/">our p5.strands tutorial</a> or the <a href="https://beta.p5js.org/reference/p5/basematerialshader/">examples in the reference for the p5.js base shaders.</a> The rest of this document will describe how p5.strands works behind the scenes. If you are interested in contributing to the p5.strands codebase, read on!
10+
11+
## Code processing pipeline
12+
13+
At its core, p5.strands works in four steps:
14+
1. The user writes a function in pseudo-JavaScript.
15+
2. p5.strands transpiles that into actual JavaScript and rewrites aspects of your code.
16+
3. The transpiled code is run. Variable modification function calls are tracked in a graph data structure.
17+
4. p5.strands generates GLSL code from that graph.
18+
19+
## Why pseudo-JavaScript?
20+
21+
The code the user writes when using p5.strands is mostly JavaScript, with some extensions. Shader code heavily encourages use of vectors, and the extensions all make this as easy in JavaScript as in GLSL.
22+
- In JavaScript, there is not a vector data type. In p5.strands, you create vectors by creating array, e.g. `myVec = [1, 0, 0]`. You can't use actual arrays in p5.strands; all arrays are fixed-size vectors.
23+
- In JavaScript, you can only use mathematical operators like `+` between numbers and strings, not with vectors. In p5.strands, we allow use of these operators between vectors.
24+
- In GLSL, you can do something called *swizzling*, where you can create new vectors out of the components of an existing vector, e.g. `myvec.xy`, `myvec.bgr`, or even `myvec.zzzz`. p5.strands adds support for this on its vectors.
25+
26+
When we transpile the input code, we rewrite these into valid JavaScript. Array literals are turned into function calls like `vec3(1, 0, 0)` which return vector class instances. These instances are wrapped in a `Proxy` that handles property accesses that look like swizzles, and converts them into sub-vector references. Operators between vectors like `a + b` are rewritten into method calls, like `a.add(b)`.
27+
28+
If a user writes something like this:
29+
30+
```js
31+
baseMaterialShader().modify(() => {
32+
const t = uniformFloat(() => millis())
33+
getWorldInputs((inputs) => {
34+
inputs.position += [20, 25, 20] * sin(inputs.position.y * 0.05 + t * 0.004)
35+
return inputs
36+
})
37+
})
38+
```
39+
40+
...it gets transpiled to something like this:
41+
```js
42+
baseMaterialShader().modify(() => {
43+
const t = uniformFloat('t', () => millis())
44+
getWorldInputs((inputs) => {
45+
inputs.position = inputs.position.add(strandsNode([20, 25, 20]).mult(sin(inputs.position.y.mult(0.05).add(strandsNode(t).mult(0.004)))))
46+
return inputs
47+
})
48+
})
49+
```
50+
51+
## The program graph
52+
53+
The overall structure of a shader program is represented by a **control-flow graph (CFG)**. This divides up a program into chunks that need to be outputted in linear order based on control flow. A program like the one below would get chunked up around the if statement:
54+
55+
```js
56+
// Start chunk 1
57+
let a = 0;
58+
let b = 1;
59+
// End chunk 1
60+
61+
// Start chunk 2
62+
if (a < 2) {
63+
b = 10;
64+
}
65+
// End chunk 2
66+
67+
// Start chunk 3
68+
b += 2;
69+
return b;
70+
// End chunk 3
71+
```
72+
73+
```mermaid
74+
flowchart TD
75+
76+
subgraph chunk1
77+
a0[let a = 0]
78+
b0[let b = 1]
79+
end
80+
81+
subgraph chunk2
82+
ifstart[if a < 2]
83+
b1[b = 10]
84+
ifend[end if]
85+
end
86+
87+
subgraph chunk3
88+
b2[b += 2]
89+
ret[return b]
90+
end
91+
92+
chunk1-->chunk2
93+
chunk2-->chunk3
94+
```
95+
96+
We store the individual states that variables can be in as nodes in a **directed acyclic graph (DAG)**. This is a fancy name that basically means each of these variable states may depend on previous variable states, and outputs can't feed back into inputs. Each time you modify a variable, that represents a new state of that variable. For example, below, it is not sufficient to know that `c` depends on `a` and `b`; you also need to know *which version of `b`* it branched off from:
97+
98+
```js
99+
let a = 0;
100+
let b = 1;
101+
b += 1;
102+
let c = a + b;
103+
return c;
104+
```
105+
106+
We can imagine giving each of these states a separate name to make it clearer. In fact, that's what we do when we output GLSL, because we don't need to preserve variable names.
107+
```js
108+
let a_0 = 0;
109+
let b_0 = 1;
110+
let b_1 = b_0 + 1;
111+
let c_0 = b_1 + a_0;
112+
return c_0;
113+
```
114+
115+
When we generate GLSL from the graph, we start from the variables we need to output, the return values of the function (e.g. `c_0` in the example above.) From there, we can track dependencies through the DAG (in this case, `b_1` and `a_1`). Each dependency has their own dependencies. We make sure we output the dependencies for a node before the node itself.
116+
117+
```mermaid
118+
flowchart TB
119+
120+
c_0-->c_0_plus((+))
121+
c_0_plus-->b_1
122+
c_0_plus-->a_0
123+
b_1-->b_1_plus((+))
124+
b_1_plus-->b_0
125+
b_1_plus-->n1_0[1]
126+
b_0-->b1_1[1]
127+
a_0-->n0[0]
128+
```
129+
130+
Each node in the DAG belongs to a chunk in the CFG. This helps us keep track of key points in the code. If we need to, for example, generate a temporary variable at the end of an if statement, we can refer to that CFG chunk rather than whatever the last value node in the if statement happens to be.
131+
132+
## Control flow
133+
134+
p5.strands has to convert any control flow that should show up in GLSL into function calls instead of JavaScript keywords. If we don't, they run in JavaScript, and are invisible to GLSL generation. For example, if you had a loop that runs 10 times that adds 1 each time, it would output the add 1 line 10 times rather than outputting a for loop.
135+
136+
<table>
137+
<tr>
138+
<th>Input</th>
139+
<th>Output without converting control flow</th>
140+
</tr>
141+
<tr>
142+
<td>
143+
144+
```js
145+
let a = 0;
146+
for (let i = 0; i < 10; i++) {
147+
a += 2;
148+
}
149+
return a;
150+
```
151+
152+
</td>
153+
<td>
154+
155+
```glsl
156+
float a = 0.0;
157+
a += 2.0;
158+
a += 2.0;
159+
a += 2.0;
160+
a += 2.0;
161+
a += 2.0;
162+
a += 2.0;
163+
a += 2.0;
164+
a += 2.0;
165+
a += 2.0;
166+
a += 2.0;
167+
return a;
168+
```
169+
170+
</td>
171+
</tr>
172+
</table>
173+
174+
However, once we have a function call instead of real control flow, we also need a way to make sure that when the users' javascript subsequently references nodes that were updated in the control flow, they properly reference the modified value after the `if` or `for` and not the original value.
175+
176+
<table>
177+
<tr>
178+
<th>Input</th>
179+
<th>Transpiled without updating references</th>
180+
<th>States without updating references</th>
181+
</tr>
182+
<tr>
183+
<td>
184+
185+
```js
186+
let a = 0;
187+
for (let i = 0; i < 10; i++) {
188+
a += 2;
189+
}
190+
let b = a + 1;
191+
return b;
192+
```
193+
194+
</td>
195+
<td>
196+
197+
```js
198+
let a = 0;
199+
p5.strandsFor(
200+
() => 0,
201+
(i) => i.lessThan(10),
202+
(i) => i.add(1),
203+
204+
() => {
205+
a = a.add(2);
206+
}
207+
);
208+
let b = a.add(1);
209+
return b;
210+
```
211+
212+
</td>
213+
<td>
214+
215+
```js
216+
let a_0 = 0;
217+
218+
p5.strandsFor(
219+
// ...
220+
)
221+
// At this point, the final state of a is a_n
222+
223+
// ...but since we didn't actually run the loop,
224+
// b still refers to the initial state of a!
225+
let b_0 = a_0.add(1);
226+
return b;
227+
```
228+
229+
</td>
230+
</tr>
231+
</table>
232+
233+
For that, we make the function calls return updated values, and we generate JS code that assigns these updated values back to the original JS variables. So for loops end up transpiled to something like this, inspired by the JavaScript `reduce` function:
234+
235+
<table>
236+
<tr>
237+
<th>Input</th>
238+
<th>Transpiled with updated references</th>
239+
</tr>
240+
<tr>
241+
<td>
242+
243+
```js
244+
let a = 0;
245+
for (let i = 0; i < 10; i++) {
246+
a += 2;
247+
}
248+
let b = a + 1;
249+
return b;
250+
```
251+
252+
</td>
253+
<td>
254+
255+
```js
256+
let a = 0;
257+
258+
const outputState = p5.strandsFor(
259+
() => 0,
260+
(i) => i.lessThan(10),
261+
(i) => i.add(1),
262+
263+
// Explicitly output new state based on prev state
264+
(i, prevState) => {
265+
return { a: prevState.a.add(2) };
266+
},
267+
268+
{ a } // Pass in initial state
269+
);
270+
a = outputState.a; // Update reference
271+
272+
// b now correctly is based off of the final state of a
273+
let b = a.add(1);
274+
return b;
275+
```
276+
277+
</td>
278+
</tr>
279+
</table>
280+
281+
We use a special kind of node in the DAG called a **phi node**, something used in compilers to refer to the result of some conditional execution. In the example above, the state of `a` in the output state is represented by a phi node.
282+
283+
In the CFG, we surround chunks producing phi nodes by a `BRANCH` and a `MERGE` chunk. In the `BRANCH` chunk, we can initialize phi nodes, sometimes giving them initial values. In the `MERGE` chunk, the value of the phi node has stabilized, and other nodes can use them as a dependency.
284+
285+
## GLSL generation
286+
287+
GLSL is currently the only output format we support, but p5.strands is designed to be able to generate multiple formats. Specifically, in WebGPU, they use the WebGPU Shading Language (WGSL). Our goal is that your same JavaScript p5.strands code can be used in WebGL or WebGPU without you having to do any modifications.
288+
289+
To support this, p5.strands separates out code generation into **backends.** A backend is responsible for converting each type of CFG chunk into a string of shader source code. We currently have a GLSL backend, but in the future we'll have a WGSL backend too!

0 commit comments

Comments
 (0)