Skip to content

Commit 746a481

Browse files
authored
Merge pull request #7961 from processing/fix/strands-uniforms-instance
Fix p5.strands uniform calls, add instance mode construct
2 parents e4ad5ee + 46842d0 commit 746a481

File tree

3 files changed

+185
-47
lines changed

3 files changed

+185
-47
lines changed

src/webgl/ShaderGenerator.js

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ function shadergenerator(p5, fn) {
1616

1717
const oldModify = p5.Shader.prototype.modify
1818

19-
p5.Shader.prototype.modify = function(shaderModifier, options = { parser: true, srcLocations: false }) {
19+
p5.Shader.prototype.modify = function(shaderModifier, scope = {}) {
2020
if (shaderModifier instanceof Function) {
21+
// TODO make this public. Currently for debugging only.
22+
const options = { parser: true, srcLocations: false };
2123
let generatorFunction;
2224
if (options.parser) {
2325
// #7955 Wrap function declaration code in brackets so anonymous functions are not top level statements, which causes an error in acorn when parsing
@@ -29,13 +31,17 @@ function shadergenerator(p5, fn) {
2931
});
3032
ancestor(ast, ASTCallbacks, undefined, { varyings: {} });
3133
const transpiledSource = escodegen.generate(ast);
32-
generatorFunction = new Function(
34+
const scopeKeys = Object.keys(scope);
35+
const internalGeneratorFunction = new Function(
36+
'p5',
37+
...scopeKeys,
3338
transpiledSource
3439
.slice(
3540
transpiledSource.indexOf('{') + 1,
3641
transpiledSource.lastIndexOf('}')
3742
).replaceAll(';', '')
3843
);
44+
generatorFunction = () => internalGeneratorFunction(p5, ...scopeKeys.map(key => scope[key]));
3945
} else {
4046
generatorFunction = shaderModifier;
4147
}
@@ -66,15 +72,24 @@ function shadergenerator(p5, fn) {
6672
}
6773
}
6874

69-
function ancestorIsUniform(ancestor) {
75+
function nodeIsUniform(ancestor) {
7076
return ancestor.type === 'CallExpression'
71-
&& ancestor.callee?.type === 'Identifier'
72-
&& ancestor.callee?.name.startsWith('uniform');
77+
&& (
78+
(
79+
// Global mode
80+
ancestor.callee?.type === 'Identifier' &&
81+
ancestor.callee?.name.startsWith('uniform')
82+
) || (
83+
// Instance mode
84+
ancestor.callee?.type === 'MemberExpression' &&
85+
ancestor.callee?.property.name.startsWith('uniform')
86+
)
87+
);
7388
}
7489

7590
const ASTCallbacks = {
76-
UnaryExpression(node, _state, _ancestors) {
77-
if (_ancestors.some(ancestorIsUniform)) { return; }
91+
UnaryExpression(node, _state, ancestors) {
92+
if (ancestors.some(nodeIsUniform)) { return; }
7893

7994
const signNode = {
8095
type: 'Literal',
@@ -85,7 +100,7 @@ function shadergenerator(p5, fn) {
85100
node.type = 'CallExpression'
86101
node.callee = {
87102
type: 'Identifier',
88-
name: 'unaryNode',
103+
name: 'p5.unaryNode',
89104
}
90105
node.arguments = [node.argument, signNode]
91106
}
@@ -108,7 +123,7 @@ function shadergenerator(p5, fn) {
108123
type: 'CallExpression',
109124
callee: {
110125
type: 'Identifier',
111-
name: 'unaryNode'
126+
name: 'p5.unaryNode'
112127
},
113128
arguments: [node.argument.object, signNode],
114129
};
@@ -125,8 +140,9 @@ function shadergenerator(p5, fn) {
125140
delete node.argument;
126141
delete node.operator;
127142
},
128-
VariableDeclarator(node, _state, _ancestors) {
129-
if (node.init.callee && node.init.callee.name?.startsWith('uniform')) {
143+
VariableDeclarator(node, _state, ancestors) {
144+
if (ancestors.some(nodeIsUniform)) { return; }
145+
if (nodeIsUniform(node.init)) {
130146
const uniformNameLiteral = {
131147
type: 'Literal',
132148
value: node.id.name
@@ -142,7 +158,8 @@ function shadergenerator(p5, fn) {
142158
_state.varyings[node.id.name] = varyingNameLiteral;
143159
}
144160
},
145-
Identifier(node, _state, _ancestors) {
161+
Identifier(node, _state, ancestors) {
162+
if (ancestors.some(nodeIsUniform)) { return; }
146163
if (_state.varyings[node.name]
147164
&& !_ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) {
148165
node.type = 'ExpressionStatement';
@@ -165,16 +182,18 @@ function shadergenerator(p5, fn) {
165182
},
166183
// The callbacks for AssignmentExpression and BinaryExpression handle
167184
// operator overloading including +=, *= assignment expressions
168-
ArrayExpression(node, _state, _ancestors) {
185+
ArrayExpression(node, _state, ancestors) {
186+
if (ancestors.some(nodeIsUniform)) { return; }
169187
const original = JSON.parse(JSON.stringify(node));
170188
node.type = 'CallExpression';
171189
node.callee = {
172190
type: 'Identifier',
173-
name: 'dynamicNode',
191+
name: 'p5.dynamicNode',
174192
};
175193
node.arguments = [original];
176194
},
177-
AssignmentExpression(node, _state, _ancestors) {
195+
AssignmentExpression(node, _state, ancestors) {
196+
if (ancestors.some(nodeIsUniform)) { return; }
178197
if (node.operator !== '=') {
179198
const methodName = replaceBinaryOperator(node.operator.replace('=',''));
180199
const rightReplacementNode = {
@@ -211,10 +230,10 @@ function shadergenerator(p5, fn) {
211230
}
212231
}
213232
},
214-
BinaryExpression(node, _state, _ancestors) {
233+
BinaryExpression(node, _state, ancestors) {
215234
// Don't convert uniform default values to node methods, as
216235
// they should be evaluated at runtime, not compiled.
217-
if (_ancestors.some(ancestorIsUniform)) { return; }
236+
if (ancestors.some(nodeIsUniform)) { return; }
218237
// If the left hand side of an expression is one of these types,
219238
// we should construct a node from it.
220239
const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
@@ -223,7 +242,7 @@ function shadergenerator(p5, fn) {
223242
type: 'CallExpression',
224243
callee: {
225244
type: 'Identifier',
226-
name: 'dynamicNode',
245+
name: 'p5.dynamicNode',
227246
},
228247
arguments: [node.left]
229248
}
@@ -1012,7 +1031,7 @@ function shadergenerator(p5, fn) {
10121031
return length
10131032
}
10141033

1015-
fn.dynamicNode = function (input) {
1034+
p5.dynamicNode = function (input) {
10161035
if (isShaderNode(input)) {
10171036
return input;
10181037
}
@@ -1025,8 +1044,8 @@ function shadergenerator(p5, fn) {
10251044
}
10261045

10271046
// For replacing unary expressions
1028-
fn.unaryNode = function(input, sign) {
1029-
input = dynamicNode(input);
1047+
p5.unaryNode = function(input, sign) {
1048+
input = p5.dynamicNode(input);
10301049
return dynamicAddSwizzleTrap(new UnaryExpressionNode(input, sign));
10311050
}
10321051

@@ -1133,6 +1152,7 @@ function shadergenerator(p5, fn) {
11331152
}
11341153

11351154
const windowOverrides = {};
1155+
const fnOverrides = {};
11361156

11371157
Object.keys(availableHooks).forEach((hookName) => {
11381158
const hookTypes = originalShader.hookTypes(hookName);
@@ -1168,7 +1188,7 @@ function shadergenerator(p5, fn) {
11681188
// If the expected return type is a struct we need to evaluate each of its properties
11691189
if (!isGLSLNativeType(expectedReturnType.typeName)) {
11701190
Object.entries(returnedValue).forEach(([propertyName, propertyNode]) => {
1171-
propertyNode = dynamicNode(propertyNode);
1191+
propertyNode = p5.dynamicNode(propertyNode);
11721192
toGLSLResults[propertyName] = propertyNode.toGLSLBase(this.context);
11731193
this.context.updateComponents(propertyNode);
11741194
});
@@ -1218,18 +1238,25 @@ function shadergenerator(p5, fn) {
12181238
this.resetGLSLContext();
12191239
}
12201240
windowOverrides[hookTypes.name] = window[hookTypes.name];
1241+
fnOverrides[hookTypes.name] = fn[hookTypes.name];
12211242

12221243
// Expose the Functions to global scope for users to use
12231244
window[hookTypes.name] = function(userOverride) {
12241245
GLOBAL_SHADER[hookTypes.name](userOverride);
12251246
};
1247+
fn[hookTypes.name] = function(userOverride) {
1248+
GLOBAL_SHADER[hookTypes.name](userOverride);
1249+
};
12261250
});
12271251

12281252

12291253
this.cleanup = () => {
12301254
for (const key in windowOverrides) {
12311255
window[key] = windowOverrides[key];
12321256
}
1257+
for (const key in fnOverrides) {
1258+
fn[key] = fnOverrides[key];
1259+
}
12331260
};
12341261
}
12351262

@@ -1638,14 +1665,14 @@ function shadergenerator(p5, fn) {
16381665
if (!GLOBAL_SHADER?.isGenerating) {
16391666
return originalNoise.apply(this, args); // fallback to regular p5.js noise
16401667
}
1641-
1668+
16421669
GLOBAL_SHADER.output.vertexDeclarations.add(noiseGLSL);
16431670
GLOBAL_SHADER.output.fragmentDeclarations.add(noiseGLSL);
16441671
return fnNodeConstructor('noise', args, { args: ['vec2'], returnType: 'float' });
16451672
};
16461673
}
1647-
1648-
1674+
1675+
16491676
export default shadergenerator;
16501677

16511678
if (typeof p5 !== 'undefined') {

src/webgl/p5.Shader.js

Lines changed: 112 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -279,13 +279,103 @@ class Shader {
279279
* of shader code replacing default behaviour.
280280
*
281281
* Each shader may let you override bits of its behavior. Each bit is called
282-
* a *hook.* A hook is either for the *vertex* shader, if it affects the
283-
* position of vertices, or in the *fragment* shader, if it affects the pixel
284-
* color. You can inspect the different hooks available by calling
282+
* a *hook.* For example, a hook can let you adjust positions of vertices, or
283+
* the color of a pixel. You can inspect the different hooks available by calling
285284
* <a href="#/p5.Shader/inspectHooks">`yourShader.inspectHooks()`</a>. You can
286285
* also read the reference for the default material, normal material, color, line, and point shaders to
287286
* see what hooks they have available.
288287
*
288+
* `modify()` can be passed a function as a parameter. Inside, you can override hooks
289+
* by calling them as functions. Each hook will take in a callback that takes in inputs
290+
* and is expected to return an output. For example, here is a function that changes the
291+
* material color to red:
292+
*
293+
* ```js example
294+
* let myShader;
295+
*
296+
* function setup() {
297+
* createCanvas(200, 200, WEBGL);
298+
* myShader = baseMaterialShader().modify(() => {
299+
* getPixelInputs((inputs) => {
300+
* inputs.color = [inputs.texCoord, 0, 1];
301+
* return inputs;
302+
* });
303+
* });
304+
* }
305+
*
306+
* function draw() {
307+
* background(255);
308+
* noStroke();
309+
* shader(myShader); // Apply the custom shader
310+
* plane(width, height); // Draw a plane with the shader applied
311+
* }
312+
* ```
313+
*
314+
* In addition to calling hooks, you can create uniforms, which are special variables
315+
* used to pass data from p5.js into the shader. They can be created by calling `uniform` + the
316+
* type of the data, such as `uniformFloat` for a number of `uniformVector2` for a two-component vector.
317+
* They take in a function that returns the data for the variable. You can then reference these
318+
* variables in your hooks, and their values will update every time you apply
319+
* the shader with the result of your function.
320+
*
321+
* ```js example
322+
* let myShader;
323+
*
324+
* function setup() {
325+
* createCanvas(200, 200, WEBGL);
326+
* myShader = baseMaterialShader().modify(() => {
327+
* // Get the current time from p5.js
328+
* let t = uniformFloat(() => millis());
329+
*
330+
* getPixelInputs((inputs) => {
331+
* inputs.color = [
332+
* inputs.texCoord,
333+
* sin(t * 0.01) / 2 + 0.5,
334+
* 1,
335+
* ];
336+
* return inputs;
337+
* });
338+
* });
339+
* }
340+
*
341+
* function draw() {
342+
* background(255);
343+
* noStroke(255);
344+
* shader(myShader); // Apply the custom shader
345+
* plane(width, height); // Draw a plane with the shader applied
346+
* }
347+
* ```
348+
*
349+
* p5.strands functions are special, since they get turned into a shader instead of being
350+
* run like the rest of your code. They only have access to p5.js functions, and variables
351+
* you declare inside the `modify` callback. If you need access to local variables, you
352+
* can pass them into `modify` with an optional second parameter, `variables`. If you are
353+
* using instance mode, you will need to pass your sketch object in this way.
354+
*
355+
* ```js example
356+
* new p5((sketch) => {
357+
* let myShader;
358+
*
359+
* sketch.setup = function() {
360+
* sketch.createCanvas(200, 200, sketch.WEBGL);
361+
* myShader = sketch.baseMaterialShader().modify(() => {
362+
* sketch.getPixelInputs((inputs) => {
363+
* inputs.color = [inputs.texCoord, 0, 1];
364+
* return inputs;
365+
* });
366+
* }, { sketch });
367+
* }
368+
*
369+
* sketch.draw = function() {
370+
* sketch.background(255);
371+
* sketch.noStroke();
372+
* sketch.shader(myShader); // Apply the custom shader
373+
* sketch.plane(sketch.width, sketch.height); // Draw a plane with the shader applied
374+
* }
375+
* });
376+
* ```
377+
*
378+
* You can also write GLSL directly in `modify` if you need direct access. To do so,
289379
* `modify()` takes one parameter, `hooks`, an object with the hooks you want
290380
* to override. Each key of the `hooks` object is the name
291381
* of a hook, and the value is a string with the GLSL code for your hook.
@@ -298,18 +388,7 @@ class Shader {
298388
* a default value as its value. These will be automatically set when the shader is set
299389
* with `shader(yourShader)`.
300390
*
301-
* You can also add a `declarations` key, where the value is a GLSL string declaring
302-
* custom uniform variables, globals, and functions shared
303-
* between hooks. To add declarations just in a vertex or fragment shader, add
304-
* `vertexDeclarations` and `fragmentDeclarations` keys.
305-
*
306-
* @beta
307-
* @param {Object} [hooks] The hooks in the shader to replace.
308-
* @returns {p5.Shader}
309-
*
310-
* @example
311-
* <div modernizr='webgl'>
312-
* <code>
391+
* ```js example
313392
* let myShader;
314393
*
315394
* function setup() {
@@ -334,12 +413,14 @@ class Shader {
334413
* fill('red'); // Set fill color to red
335414
* sphere(50); // Draw a sphere with the shader applied
336415
* }
337-
* </code>
338-
* </div>
416+
* ```
339417
*
340-
* @example
341-
* <div modernizr='webgl'>
342-
* <code>
418+
* You can also add a `declarations` key, where the value is a GLSL string declaring
419+
* custom uniform variables, globals, and functions shared
420+
* between hooks. To add declarations just in a vertex or fragment shader, add
421+
* `vertexDeclarations` and `fragmentDeclarations` keys.
422+
*
423+
* ```js example
343424
* let myShader;
344425
*
345426
* function setup() {
@@ -364,8 +445,17 @@ class Shader {
364445
* fill('red');
365446
* sphere(50);
366447
* }
367-
* </code>
368-
* </div>
448+
* ```
449+
*
450+
* @beta
451+
* @param {Function} callback A function with p5.strands code to modify the shader.
452+
* @param {Object} [variables] An optional object with local variables p5.strands
453+
* should have access to.
454+
* @returns {p5.Shader}
455+
*/
456+
/**
457+
* @param {Object} [hooks] The hooks in the shader to replace.
458+
* @returns {p5.Shader}
369459
*/
370460
modify(hooks) {
371461
// p5._validateParameters('p5.Shader.modify', arguments);

0 commit comments

Comments
 (0)