-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Branching and looping for p5.strands #8120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Co-authored-by: Perminder Singh <[email protected]>
Co-authored-by: Perminder Singh <[email protected]>
Co-authored-by: Perminder Singh <[email protected]>
case '>': return 'greaterThan'; | ||
case '>=': return 'greaterThanEqualTo'; | ||
case '>=': return 'greaterEqual'; | ||
case '<': return 'lessThan'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just wondering that we don't need to have !=
, !==
cases right? Like if user uses any of the other operations maybe (**
) which are not in the case, can we throw an error message saying Unsupported operator
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch, that's probably what will happen!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
**
might have to be handled differently, because unlike the rest, which get turned into a method in js but then back into an operator in GLSL, **
becomes pow
and needs to stay pow
in GLSL, which will need some special casing in the code. I'll just leave a TODO for that one for the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
contributor_docs/p5.strands.md
Outdated
baseMaterialShader().modify(() => { | ||
const t = uniformFloat('t', () => millis()) | ||
getWorldInputs((inputs) => { | ||
inputs.position = inputs.position.add(dynamicNode([20, 25, 20]).mult(sin(inputs.position.y.mult(0.05).add(dynamicNode(t).mult(0.004))))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
dynamicNode
has been changed to strandsNode
since the refactor
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah yes good catch! updated
const rightPixel = myp5.get(75, 25); | ||
assert.approximately(rightPixel[0], 127, 5); // 0.5 * 255 ≈ 127 | ||
}); | ||
test('handle if-else-if chains', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't get the black to show. Maybe it has something to do with the codegen, rather than the graph though? Take a look at the outputted code. I changed the colours from this test example to make it clearer for me. I guesss that the nested if statement doesn't know that it should be updating T0
and creates its own output variable instead?
let testShader;
async function setup() {
createCanvas(windowWidth, windowHeight, WEBGL);
testShader = baseMaterialShader().modify(() => {
const value = uniformFloat(() => 0.5); // middle value
getPixelInputs(inputs => {
let color = vec3(0.0);
if (value > 0.8) {
color = vec3(1.0, 0, 0); // white for high values
} else if (value > 0.3) {
color = vec3(0, 1, 0); // gray for medium values
} else {
color = vec3(0, 0, 1); // black for low values
}
inputs.color = [color, 1.0];
return inputs;
});
});
}
function draw() {
background(0);
shader(testShader);
testShader.setUniform('value', mouseX/width)
sphere(100)
}
(Inputs inputs) {
vec3 T0;
if (value > float(0.8000))
{
vec3 T1 = vec3(1.0000, 0.0000, 0.0000);
T0 = T1;
}
else
{
T0 = vec3(0.0000, 1.0000, 0.0000);
vec3 T2;
if (value > float(0.3000))
{
vec3 T3 = vec3(0.0000, 1.0000, 0.0000);
T2 = T3;
}
else
{
vec3 T4 = vec3(0.0000, 0.0000, 1.0000);
T2 = T4;
}
}
inputs.normal = inputs.normal;
inputs.texCoord = inputs.texCoord;
inputs.ambientLight = inputs.ambientLight;
inputs.ambientMaterial = inputs.ambientMaterial;
inputs.specularMaterial = inputs.specularMaterial;
inputs.emissiveMaterial = inputs.emissiveMaterial;
inputs.color = vec4(T0, 1.0000);
inputs.shininess = inputs.shininess;
inputs.metalness = inputs.metalness;
return inputs;
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Did some digging tonight, the problem seems to be that we add assignment to the phi node in the branch body block, but there may also be sub-blocks that happen after. To accommodate, I've made it add a new block just for phi assignments that happens right at the end of a branch to ensure it never ends up accidentally generated abrove sub-branches.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is probably also an issue for for loops so I need to take a look at that too still
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok I think I fixed the issues in for loops -- I also had to update local variable detection there which I had only done for if statements before and added some more tests.
Live here: https://editor.p5js.org/davepagurek/sketches/jeOynvqNG
There are some other issues I'm running into though, I'll make an update to fix those
} | ||
} | ||
} | ||
// Second pass: find assignments to non-local variables |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible that the assignments arent being wrapped in strandsNode
calls because of the pass order? It's causing an error if you do
if (condition > 0.5) {
col = 1.0;
}
Instead of col = float(1)
on the second line above.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good catch! just added a test for this and it failed, so I pushed an update to wrap all assignments in branches in strandsNode. then it works!
There was an additional bug where |
This is really exciting to see! Just a small note on debugging with branches and loops, which create a lot more opportunity to use variables inside strands. Variables that exist in global scope can still be referenced inside strands, though they will not work. From ex below, I can write instead of let testShader;
let pixelate;
let state_;
async function setup() {
state_ = 0;
createCanvas(windowWidth, windowHeight, WEBGL);
testShader = baseMaterialShader().modify(() => {
const value = uniformFloat(() => mouseX/windowWidth);
const state = uniformFloat(() => state_);
let time = uniformFloat(() => frameCount/5);
getPixelInputs((inputs, canvasContent) => {
let color = inputs.color;
if (value < 0.3) {
color = vec3(1.0, state*0.1, 0);
} else if (value < 0.8) {
color = vec3(state*0.1, 1, state*0.1);
} else {
color = vec3(state*0.1, 0, 1);
}
inputs.color = [color, 1.0];
return inputs;
});
});
}
function draw() {
background(0);
shader(testShader);
testShader.setUniform('value', mouseX/width)
sphere(100);
}
function mousePressed(){
state_ += 1;
console.log(state_)
} |
// End chunk 3 | ||
``` | ||
|
||
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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This whole documentation is wonderful! Very minor and not necessary for this iteration: DAG illustration / connecting it to code block below not only in text
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1, I'll see if I can make an SVG to put in. Minor note, in the past I've used Memaid code blocks to do inline diagrams which is helpful for writing and also pretty easily editable, but we never got around to making the p5.js website render it correctly, so it currently shows up as the Mermaid source code there. At some point I'd love to get that working so I can keep using that format, but for now probably svg is still the way to go.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great point, as we just discussed on Discord, I'll do a bit more research on it and open processing/p5.js-website#168 for work. So please feel free to add only the mermaid code here if you prefer, though of course an SVG would be more helpful sooner
case '>': return 'greaterThan'; | ||
case '>=': return 'greaterThanEqualTo'; | ||
case '>=': return 'greaterEqual'; | ||
case '<': return 'lessThan'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
|
||
# p5.strands Overview | ||
|
||
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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be really good to more directly express the intended audience. There's parts of this that would be useful to anyone encountering strands, and parts only useful to people who want to contribute to strands (though this is the contribution section on the site, the intro reads like a general motivation for strands usage). Ie, it's important all users of p5.strands are aware this is a "different mode" of javascript; and that there is also GLSL and OpenGL. However, I don't think terminology of transpiler is generally necessary unless people are interested. Maybe at the end of the intro, direct readers to the tutorials on strands and specify that the rest of this document is for contribution?
I think in the future there are two things I'd like to do to address usage of outside variables:
I could make follow-up issues for both of those if that makes sense! |
Those follow-ups sound great! Just some thoughts below.
Ah, that would be nice. Not plausible to go 1 step further and somehow infer that |
So all the limitations come from the fact that we're not running the user's callback directly, just parsing it to a string, and then making our own function with it. By reconstructing a function like that, it loses all scope that the original function had. So while this works: let val = 10;
let cb = () => console.log(val);
cb(); ...this doesn't, because the runtime creation of the function makes a new, empty scope for it, and this is essentially what we're doing after transpilation: let val = 10;
let cb = new Function('console.log(val)');
cb(); // `val` is not in scope The object thing that we pass in is a little hack to take in a value from outside and pass it in, so that we still get access: let val = 10;
let cb = new Function('val', 'console.log(val)');
cb(val); // `val` is now just a parameter to the function, so it's in scope So that's not just a change inside of the callback's source code, we also need to change something outside of the callback in order to pass in The difference between just passing in let color = [1, 0, 0, 1];
let colorShader = baseMaterialShader().modify(() => {
getFinalColor(() => {
return color;
});
}, { color });
color = [0, 0, 1, 1]; // Set to blue, but the original red is already captured
shader(colorShader);
sphere(30); ...whereas a uniform can be updated live: function setup() {
createCanvas(400, 400, WEBGL);
let color = [1, 0, 0, 1];
let colorShader = baseMaterialShader().modify(function() {
const colorUniform = uniformVector4()
getFinalColor(() => {
return colorUniform;
});
});
color = [0, 0, 1, 1];
shader(colorShader);
colorShader.setUniform('colorUniform', color)
sphere(30);
} |
Resolves #7868
Changes
i++
again>=
operator properlyI refactored a bit in order to handle nested blocks more easily: there's now a
SCOPE_START
andSCOPE_END
block for{
and}
respectively, and I added anASSIGNMENT
statement that is currently only used to assign to phi variables in each branch.Details
The main challenge here is that we have to fully replace control flow (
if
/for
) in users' code into function calls so that these structures can generate nodes in our program graph. 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.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
orfor
and not the original value. 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.If statements
You write an if statement like this:
It gets transpiled into this function call. Each branch callback gets a copy of the node so that it can modify it without affecting the original, and each branch callback then returns an object with modified versions of those variables. The whole if/else structure returns an object with nodes representing the output of the branch, and we then assign back values from that to the original variables.
This then gets compiled to the following GLSL:
For loops
You write a loop like this:
This gets transpiled into this format, where we use a strands function call, and have a callback function for each part of the for loop. The loop body is structured more like a
reduce
, taking in current state + loop iteration and returning next state. At the end of the for loop, the properties of the final state are assigned back to their original variables.This then gets turned into the following GLSL:
PR Checklist
npm run lint
passes