Skip to content

Commit 98fc10c

Browse files
committed
Fix p5.strands uniform calls, add instance mode construct
1 parent f3ab0ce commit 98fc10c

File tree

3 files changed

+178
-47
lines changed

3 files changed

+178
-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
const sourceString = shaderModifier.toString()
@@ -27,13 +29,17 @@ function shadergenerator(p5, fn) {
2729
});
2830
ancestor(ast, ASTCallbacks, undefined, { varyings: {} });
2931
const transpiledSource = escodegen.generate(ast);
30-
generatorFunction = new Function(
32+
const scopeKeys = Object.keys(scope);
33+
const internalGeneratorFunction = new Function(
34+
'p5',
35+
...scopeKeys,
3136
transpiledSource
3237
.slice(
3338
transpiledSource.indexOf('{') + 1,
3439
transpiledSource.lastIndexOf('}')
3540
).replaceAll(';', '')
3641
);
42+
generatorFunction = () => internalGeneratorFunction(p5, ...scopeKeys.map(key => scope[key]));
3743
} else {
3844
generatorFunction = shaderModifier;
3945
}
@@ -64,15 +70,24 @@ function shadergenerator(p5, fn) {
6470
}
6571
}
6672

67-
function ancestorIsUniform(ancestor) {
73+
function nodeIsUniform(ancestor) {
6874
return ancestor.type === 'CallExpression'
69-
&& ancestor.callee?.type === 'Identifier'
70-
&& ancestor.callee?.name.startsWith('uniform');
75+
&& (
76+
(
77+
// Global mode
78+
ancestor.callee?.type === 'Identifier' &&
79+
ancestor.callee?.name.startsWith('uniform')
80+
) || (
81+
// Instance mode
82+
ancestor.callee?.type === 'MemberExpression' &&
83+
ancestor.callee?.property.name.startsWith('uniform')
84+
)
85+
);
7186
}
7287

7388
const ASTCallbacks = {
74-
UnaryExpression(node, _state, _ancestors) {
75-
if (_ancestors.some(ancestorIsUniform)) { return; }
89+
UnaryExpression(node, _state, ancestors) {
90+
if (ancestors.some(nodeIsUniform)) { return; }
7691

7792
const signNode = {
7893
type: 'Literal',
@@ -83,7 +98,7 @@ function shadergenerator(p5, fn) {
8398
node.type = 'CallExpression'
8499
node.callee = {
85100
type: 'Identifier',
86-
name: 'unaryNode',
101+
name: 'p5.unaryNode',
87102
}
88103
node.arguments = [node.argument, signNode]
89104
}
@@ -106,7 +121,7 @@ function shadergenerator(p5, fn) {
106121
type: 'CallExpression',
107122
callee: {
108123
type: 'Identifier',
109-
name: 'unaryNode'
124+
name: 'p5.unaryNode'
110125
},
111126
arguments: [node.argument.object, signNode],
112127
};
@@ -123,8 +138,9 @@ function shadergenerator(p5, fn) {
123138
delete node.argument;
124139
delete node.operator;
125140
},
126-
VariableDeclarator(node, _state, _ancestors) {
127-
if (node.init.callee && node.init.callee.name?.startsWith('uniform')) {
141+
VariableDeclarator(node, _state, ancestors) {
142+
if (ancestors.some(nodeIsUniform)) { return; }
143+
if (nodeIsUniform(node.init)) {
128144
const uniformNameLiteral = {
129145
type: 'Literal',
130146
value: node.id.name
@@ -140,7 +156,8 @@ function shadergenerator(p5, fn) {
140156
_state.varyings[node.id.name] = varyingNameLiteral;
141157
}
142158
},
143-
Identifier(node, _state, _ancestors) {
159+
Identifier(node, _state, ancestors) {
160+
if (ancestors.some(nodeIsUniform)) { return; }
144161
if (_state.varyings[node.name]
145162
&& !_ancestors.some(a => a.type === 'AssignmentExpression' && a.left === node)) {
146163
node.type = 'ExpressionStatement';
@@ -163,16 +180,18 @@ function shadergenerator(p5, fn) {
163180
},
164181
// The callbacks for AssignmentExpression and BinaryExpression handle
165182
// operator overloading including +=, *= assignment expressions
166-
ArrayExpression(node, _state, _ancestors) {
183+
ArrayExpression(node, _state, ancestors) {
184+
if (ancestors.some(nodeIsUniform)) { return; }
167185
const original = JSON.parse(JSON.stringify(node));
168186
node.type = 'CallExpression';
169187
node.callee = {
170188
type: 'Identifier',
171-
name: 'dynamicNode',
189+
name: 'p5.dynamicNode',
172190
};
173191
node.arguments = [original];
174192
},
175-
AssignmentExpression(node, _state, _ancestors) {
193+
AssignmentExpression(node, _state, ancestors) {
194+
if (ancestors.some(nodeIsUniform)) { return; }
176195
if (node.operator !== '=') {
177196
const methodName = replaceBinaryOperator(node.operator.replace('=',''));
178197
const rightReplacementNode = {
@@ -209,10 +228,10 @@ function shadergenerator(p5, fn) {
209228
}
210229
}
211230
},
212-
BinaryExpression(node, _state, _ancestors) {
231+
BinaryExpression(node, _state, ancestors) {
213232
// Don't convert uniform default values to node methods, as
214233
// they should be evaluated at runtime, not compiled.
215-
if (_ancestors.some(ancestorIsUniform)) { return; }
234+
if (ancestors.some(nodeIsUniform)) { return; }
216235
// If the left hand side of an expression is one of these types,
217236
// we should construct a node from it.
218237
const unsafeTypes = ['Literal', 'ArrayExpression', 'Identifier'];
@@ -221,7 +240,7 @@ function shadergenerator(p5, fn) {
221240
type: 'CallExpression',
222241
callee: {
223242
type: 'Identifier',
224-
name: 'dynamicNode',
243+
name: 'p5.dynamicNode',
225244
},
226245
arguments: [node.left]
227246
}
@@ -1010,7 +1029,7 @@ function shadergenerator(p5, fn) {
10101029
return length
10111030
}
10121031

1013-
fn.dynamicNode = function (input) {
1032+
p5.dynamicNode = function (input) {
10141033
if (isShaderNode(input)) {
10151034
return input;
10161035
}
@@ -1023,8 +1042,8 @@ function shadergenerator(p5, fn) {
10231042
}
10241043

10251044
// For replacing unary expressions
1026-
fn.unaryNode = function(input, sign) {
1027-
input = dynamicNode(input);
1045+
p5.unaryNode = function(input, sign) {
1046+
input = p5.dynamicNode(input);
10281047
return dynamicAddSwizzleTrap(new UnaryExpressionNode(input, sign));
10291048
}
10301049

@@ -1131,6 +1150,7 @@ function shadergenerator(p5, fn) {
11311150
}
11321151

11331152
const windowOverrides = {};
1153+
const fnOverrides = {};
11341154

11351155
Object.keys(availableHooks).forEach((hookName) => {
11361156
const hookTypes = originalShader.hookTypes(hookName);
@@ -1166,7 +1186,7 @@ function shadergenerator(p5, fn) {
11661186
// If the expected return type is a struct we need to evaluate each of its properties
11671187
if (!isGLSLNativeType(expectedReturnType.typeName)) {
11681188
Object.entries(returnedValue).forEach(([propertyName, propertyNode]) => {
1169-
propertyNode = dynamicNode(propertyNode);
1189+
propertyNode = p5.dynamicNode(propertyNode);
11701190
toGLSLResults[propertyName] = propertyNode.toGLSLBase(this.context);
11711191
this.context.updateComponents(propertyNode);
11721192
});
@@ -1216,18 +1236,25 @@ function shadergenerator(p5, fn) {
12161236
this.resetGLSLContext();
12171237
}
12181238
windowOverrides[hookTypes.name] = window[hookTypes.name];
1239+
fnOverrides[hookTypes.name] = fn[hookTypes.name];
12191240

12201241
// Expose the Functions to global scope for users to use
12211242
window[hookTypes.name] = function(userOverride) {
12221243
GLOBAL_SHADER[hookTypes.name](userOverride);
12231244
};
1245+
fn[hookTypes.name] = function(userOverride) {
1246+
GLOBAL_SHADER[hookTypes.name](userOverride);
1247+
};
12241248
});
12251249

12261250

12271251
this.cleanup = () => {
12281252
for (const key in windowOverrides) {
12291253
window[key] = windowOverrides[key];
12301254
}
1255+
for (const key in fnOverrides) {
1256+
fn[key] = fnOverrides[key];
1257+
}
12311258
};
12321259
}
12331260

@@ -1636,14 +1663,14 @@ function shadergenerator(p5, fn) {
16361663
if (!GLOBAL_SHADER?.isGenerating) {
16371664
return originalNoise.apply(this, args); // fallback to regular p5.js noise
16381665
}
1639-
1666+
16401667
GLOBAL_SHADER.output.vertexDeclarations.add(noiseGLSL);
16411668
GLOBAL_SHADER.output.fragmentDeclarations.add(noiseGLSL);
16421669
return fnNodeConstructor('noise', args, { args: ['vec2'], returnType: 'float' });
16431670
};
16441671
}
1645-
1646-
1672+
1673+
16471674
export default shadergenerator;
16481675

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

src/webgl/p5.Shader.js

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -279,13 +279,96 @@ 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 = [1, 0, 0, 1];
301+
* return inputs;
302+
* });
303+
* });
304+
* }
305+
*
306+
* function draw() {
307+
* background(255);
308+
* shader(myShader); // Apply the custom shader
309+
* plane(width, height); // Draw a plane with the shader applied
310+
* }
311+
* ```
312+
*
313+
* In addition to calling hooks, you can create uniforms, which are special variables
314+
* used to pass data from p5.js into the shader. They can be created by calling `uniform` + the
315+
* type of the data, such as `uniformFloat` for a number of `uniformVector2` for a two-component vector.
316+
* They take in a function that returns the data for the variable. You can then reference these
317+
* variables in your hooks, and their values will update every time you apply
318+
* the shader with the result of your function.
319+
*
320+
* ```js example
321+
* let myShader;
322+
*
323+
* function setup() {
324+
* createCanvas(200, 200, WEBGL);
325+
* myShader = baseMaterialShader().modify(() => {
326+
* // Get the current time from p5.js
327+
* let t = uniformFloat(() => millis());
328+
*
329+
* getPixelInputs((inputs) => {
330+
* inputs.color = [sin(t * 0.01) / 2 + 0.5, 0, 0, 1];
331+
* return inputs;
332+
* });
333+
* });
334+
* }
335+
*
336+
* function draw() {
337+
* background(255);
338+
* shader(myShader); // Apply the custom shader
339+
* plane(width, height); // Draw a plane with the shader applied
340+
* }
341+
* ```
342+
*
343+
* p5.strands functions are special, since they get turned into a shader instead of being
344+
* run like the rest of your code. They only have access to p5.js functions, and variables
345+
* you declare inside the `modify` callback. If you need access to local variables, you
346+
* can pass them into `modify` with an optional second parameter, `variables`. If you are
347+
* using instance mode, you will need to pass your sketch object in this way.
348+
*
349+
* ```js example
350+
* new p5((sketch) => {
351+
* let myShader;
352+
*
353+
* sketch.setup = function() {
354+
* sketch.createCanvas(200, 200, sketch.WEBGL);
355+
* myShader = sketch.baseMaterialShader().modify(() => {
356+
* sketch.getPixelInputs((inputs) => {
357+
* inputs.color = [1, 0, 0, 1];
358+
* return inputs;
359+
* });
360+
* }, { sketch });
361+
* }
362+
*
363+
* sketch.draw = function() {
364+
* sketch.background(255);
365+
* sketch.shader(myShader); // Apply the custom shader
366+
* sketch.plane(sketch.width, sketch.height); // Draw a plane with the shader applied
367+
* }
368+
* });
369+
* ```
370+
*
371+
* You can also write GLSL directly in `modify` if you need direct access. To do so,
289372
* `modify()` takes one parameter, `hooks`, an object with the hooks you want
290373
* to override. Each key of the `hooks` object is the name
291374
* of a hook, and the value is a string with the GLSL code for your hook.
@@ -298,18 +381,7 @@ class Shader {
298381
* a default value as its value. These will be automatically set when the shader is set
299382
* with `shader(yourShader)`.
300383
*
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>
384+
* ```js example
313385
* let myShader;
314386
*
315387
* function setup() {
@@ -334,12 +406,14 @@ class Shader {
334406
* fill('red'); // Set fill color to red
335407
* sphere(50); // Draw a sphere with the shader applied
336408
* }
337-
* </code>
338-
* </div>
409+
* ```
339410
*
340-
* @example
341-
* <div modernizr='webgl'>
342-
* <code>
411+
* You can also add a `declarations` key, where the value is a GLSL string declaring
412+
* custom uniform variables, globals, and functions shared
413+
* between hooks. To add declarations just in a vertex or fragment shader, add
414+
* `vertexDeclarations` and `fragmentDeclarations` keys.
415+
*
416+
* ```js example
343417
* let myShader;
344418
*
345419
* function setup() {
@@ -364,8 +438,17 @@ class Shader {
364438
* fill('red');
365439
* sphere(50);
366440
* }
367-
* </code>
368-
* </div>
441+
* ```
442+
*
443+
* @beta
444+
* @param {Function} callback A function with p5.strands code to modify the shader.
445+
* @param {Object} [variables] An optional object with local variables p5.strands
446+
* should have access to.
447+
* @returns {p5.Shader}
448+
*/
449+
/**
450+
* @param {Object} [hooks] The hooks in the shader to replace.
451+
* @returns {p5.Shader}
369452
*/
370453
modify(hooks) {
371454
// p5._validateParameters('p5.Shader.modify', arguments);

0 commit comments

Comments
 (0)