Skip to content

Commit 451d4b9

Browse files
authored
Merge pull request #8372 from processing/fix/minified-strands
More fixes for minified filters
2 parents a213e53 + 497b23c commit 451d4b9

File tree

5 files changed

+179
-43
lines changed

5 files changed

+179
-43
lines changed

src/core/filterShaders.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
import * as constants from './constants';
22

33
/*
4-
* Creates p5.strands filter shaders for cross-platform compatibility
4+
* Creates p5.strands filter shaders for cross-platform compatibility.
5+
*
6+
* NOTE: These work a little differently than p5.js web editor shaders work!
7+
* Firstly, it uses instance mode, so we have to explicitly pass in context
8+
* variables in an argument to your callback and as a second argument to `modify`.
9+
* Secondly, always manually specify uniform names, as variable names will change
10+
* in minified builds.
511
*/
612
export function makeFilterShader(renderer, operation, p5) {
713
switch (operation) {
@@ -26,7 +32,7 @@ export function makeFilterShader(renderer, operation, p5) {
2632

2733
case constants.THRESHOLD:
2834
return renderer.baseFilterShader().modify(({ p5 }) => {
29-
const filterParameter = p5.uniformFloat();
35+
const filterParameter = p5.uniformFloat('filterParameter');
3036
p5.getColor((inputs, canvasContent) => {
3137
const color = p5.getTexture(canvasContent, inputs.texCoord);
3238
// weighted grayscale with luminance values
@@ -39,7 +45,7 @@ export function makeFilterShader(renderer, operation, p5) {
3945

4046
case constants.POSTERIZE:
4147
return renderer.baseFilterShader().modify(({ p5 }) => {
42-
const filterParameter = p5.uniformFloat();
48+
const filterParameter = p5.uniformFloat('filterParameter');
4349
const quantize = (color, n) => {
4450
// restrict values to N options/bins
4551
// and floor each channel to nearest value
@@ -61,8 +67,8 @@ export function makeFilterShader(renderer, operation, p5) {
6167

6268
case constants.BLUR:
6369
return renderer.baseFilterShader().modify(({ p5 }) => {
64-
const radius = p5.uniformFloat();
65-
const direction = p5.uniformVec2();
70+
const radius = p5.uniformFloat('radius');
71+
const direction = p5.uniformVec2('direction');
6672

6773
// This isn't a real Gaussian weight, it's a quadratic weight
6874
const quadWeight = (x, e) => {

src/core/p5.Renderer3D.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1912,13 +1912,13 @@ export class Renderer3D extends Renderer {
19121912
if (!this.sphereMapping) {
19131913
const p5 = this._pInst;
19141914
this.sphereMapping = this.baseFilterShader().modify(({ p5 }) => {
1915-
const uEnvMap = p5.uniformTexture();
1916-
const uFovY = p5.uniformFloat();
1917-
const uAspect = p5.uniformFloat();
1915+
const uEnvMap = p5.uniformTexture('uEnvMap');
1916+
const uFovY = p5.uniformFloat('uFovY');
1917+
const uAspect = p5.uniformFloat('uAspect');
19181918
// Hack: we don't have matrix uniforms yet; use three vectors
1919-
const uN1 = p5.uniformVec3();
1920-
const uN2 = p5.uniformVec3();
1921-
const uN3 = p5.uniformVec3();
1919+
const uN1 = p5.uniformVec3('uN1');
1920+
const uN2 = p5.uniformVec3('uN2');
1921+
const uN3 = p5.uniformVec3('uN3');
19221922
p5.getColor((inputs) => {
19231923
const uFovX = uFovY * uAspect;
19241924
const angleY = p5.mix(uFovY/2.0, -uFovY/2.0, inputs.texCoord.y);

src/strands/strands_transpiler.js

Lines changed: 76 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -111,19 +111,34 @@ const ASTCallbacks = {
111111
VariableDeclarator(node, _state, ancestors) {
112112
if (ancestors.some(nodeIsUniform)) { return; }
113113
if (nodeIsUniform(node.init)) {
114-
const uniformNameLiteral = {
115-
type: 'Literal',
116-
value: node.id.name
114+
// Only inject the variable name if the first argument isn't already a string
115+
if (node.init.arguments.length === 0 ||
116+
node.init.arguments[0].type !== 'Literal' ||
117+
typeof node.init.arguments[0].value !== 'string') {
118+
const uniformNameLiteral = {
119+
type: 'Literal',
120+
value: node.id.name
121+
}
122+
node.init.arguments.unshift(uniformNameLiteral);
117123
}
118-
node.init.arguments.unshift(uniformNameLiteral);
119124
}
120125
if (nodeIsVarying(node.init)) {
121-
const varyingNameLiteral = {
122-
type: 'Literal',
123-
value: node.id.name
126+
// Only inject the variable name if the first argument isn't already a string
127+
if (
128+
node.init.arguments.length === 0 ||
129+
node.init.arguments[0].type !== 'Literal' ||
130+
typeof node.init.arguments[0].value !== 'string'
131+
) {
132+
const varyingNameLiteral = {
133+
type: 'Literal',
134+
value: node.id.name
135+
}
136+
node.init.arguments.unshift(varyingNameLiteral);
137+
_state.varyings[node.id.name] = varyingNameLiteral;
138+
} else {
139+
// Still track it as a varying even if name wasn't injected
140+
_state.varyings[node.id.name] = node.init.arguments[0];
124141
}
125-
node.init.arguments.unshift(varyingNameLiteral);
126-
_state.varyings[node.id.name] = varyingNameLiteral;
127142
}
128143
},
129144
Identifier(node, _state, ancestors) {
@@ -362,45 +377,75 @@ const ASTCallbacks = {
362377
};
363378
// Analyze which outer scope variables are assigned in any branch
364379
const assignedVars = new Set();
365-
const analyzeBlock = (body) => {
366-
if (body.type !== 'BlockStatement') return;
367-
// First pass: collect variable declarations within this block
368-
const localVars = new Set();
369-
for (const stmt of body.body) {
380+
381+
// Helper function to check if a block contains strands control flow calls as immediate children
382+
const blockContainsStrandsControlFlow = (node) => {
383+
if (node.type !== 'BlockStatement') return false;
384+
return node.body.some(stmt => {
385+
// Check for variable declarations with strands control flow init
370386
if (stmt.type === 'VariableDeclaration') {
371-
for (const decl of stmt.declarations) {
372-
if (decl.id.type === 'Identifier') {
373-
localVars.add(decl.id.name);
374-
}
387+
const match = stmt.declarations.some(decl =>
388+
decl.init?.type === 'CallExpression' &&
389+
(
390+
(
391+
decl.init?.callee?.type === 'MemberExpression' &&
392+
decl.init?.callee?.object?.type === 'Identifier' &&
393+
decl.init?.callee?.object?.name === '__p5' &&
394+
(decl.init?.callee?.property?.name === 'strandsFor' ||
395+
decl.init?.callee?.property?.name === 'strandsIf')
396+
) ||
397+
(
398+
decl.init?.callee?.type === 'Identifier' &&
399+
(decl.init?.callee?.name === '__p5.strandsFor' ||
400+
decl.init?.callee?.name === '__p5.strandsIf')
401+
)
402+
)
403+
);
404+
return match
405+
}
406+
return false;
407+
});
408+
};
409+
410+
const analyzeBranch = (functionBody) => {
411+
// First pass: collect all variable declarations in the branch
412+
const localVars = new Set();
413+
ancestor(functionBody, {
414+
VariableDeclarator(node, ancestors) {
415+
// Skip if we're inside a block that contains strands control flow
416+
if (ancestors.some(blockContainsStrandsControlFlow)) return;
417+
if (node.id.type === 'Identifier') {
418+
localVars.add(node.id.name);
375419
}
376420
}
377-
}
378-
// Second pass: find assignments to non-local variables
379-
for (const stmt of body.body) {
380-
if (stmt.type === 'ExpressionStatement' &&
381-
stmt.expression.type === 'AssignmentExpression') {
382-
const left = stmt.expression.left;
421+
});
422+
423+
// Second pass: find assignments to non-local variables using acorn-walk
424+
ancestor(functionBody, {
425+
AssignmentExpression(node, ancestors) {
426+
// Skip if we're inside a block that contains strands control flow
427+
if (ancestors.some(blockContainsStrandsControlFlow)) return;
428+
429+
const left = node.left;
383430
if (left.type === 'Identifier') {
384431
// Direct variable assignment: x = value
385432
if (!localVars.has(left.name)) {
386433
assignedVars.add(left.name);
387434
}
388435
} else if (left.type === 'MemberExpression' &&
389436
left.object.type === 'Identifier') {
390-
// Property assignment: obj.prop = value
437+
// Property assignment: obj.prop = value (includes swizzles)
391438
if (!localVars.has(left.object.name)) {
392439
assignedVars.add(left.object.name);
393440
}
394441
}
395-
} else if (stmt.type === 'BlockStatement') {
396-
// Recursively analyze nested block statements
397-
analyzeBlock(stmt);
398442
}
399-
}
443+
});
400444
};
445+
401446
// Analyze all branches for assignments to outer scope variables
402-
analyzeBlock(thenFunction.body);
403-
analyzeBlock(elseFunction.body);
447+
analyzeBranch(thenFunction.body);
448+
analyzeBranch(elseFunction.body);
404449
if (assignedVars.size > 0) {
405450
// Add copying, reference replacement, and return statements to branch functions
406451
const addCopyingAndReturn = (functionBody, varsToReturn) => {

src/webgl/p5.Shader.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,15 +243,20 @@ class Shader {
243243
* then be passed into your function as an argument. If you are
244244
* using instance mode, you will need to pass your sketch object in this way.
245245
*
246+
* If you are also using a build system for your sketch, variable names may be changed as
247+
* part of minification. When creating a uniform, you can pass the name of the uniform in
248+
* as a first parameter to ensure it doesn't get changed.
249+
*
246250
* ```js example
247251
* new p5((sketch) => {
248252
* let myShader;
249253
*
250254
* sketch.setup = function() {
251255
* sketch.createCanvas(200, 200, sketch.WEBGL);
252256
* myShader = sketch.baseMaterialShader().modify(({ sketch }) => {
257+
* let b = uniformFloat('b');
253258
* sketch.getPixelInputs((inputs) => {
254-
* inputs.color = [inputs.texCoord, 0, 1];
259+
* inputs.color = [inputs.texCoord, b, 1];
255260
* return inputs;
256261
* });
257262
* }, { sketch });
@@ -260,6 +265,7 @@ class Shader {
260265
* sketch.draw = function() {
261266
* sketch.background(255);
262267
* sketch.noStroke();
268+
* myShader.setUniform('b', 0.5);
263269
* sketch.shader(myShader); // Apply the custom shader
264270
* sketch.plane(sketch.width, sketch.height); // Draw a plane with the shader applied
265271
* }

test/unit/webgl/p5.Shader.js

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,54 @@ suite('p5.Shader', function() {
426426
myp5.plane(myp5.width, myp5.height);
427427
}).not.toThrowError();
428428
});
429+
430+
test('handle custom uniform names with automatic values', () => {
431+
myp5.createCanvas(50, 50, myp5.WEBGL);
432+
const testShader = myp5.baseMaterialShader().modify(() => {
433+
// Variable name is 'brightness' but uniform name is 'customBrightness'
434+
const brightness = myp5.uniformFloat('customBrightness', () => 0.8);
435+
myp5.getPixelInputs(inputs => {
436+
inputs.color = [brightness, brightness, brightness, 1.0];
437+
return inputs;
438+
});
439+
}, { myp5 });
440+
441+
myp5.noStroke();
442+
myp5.shader(testShader);
443+
myp5.plane(myp5.width, myp5.height);
444+
445+
// Check that the shader uses the automatic value (0.8)
446+
const pixelColor = myp5.get(25, 25);
447+
assert.approximately(pixelColor[0], 204, 5); // 0.8 * 255 = 204
448+
assert.approximately(pixelColor[1], 204, 5);
449+
assert.approximately(pixelColor[2], 204, 5);
450+
});
451+
452+
test('handle custom uniform names with manual setUniform', () => {
453+
myp5.createCanvas(50, 50, myp5.WEBGL);
454+
const testShader = myp5.baseMaterialShader().modify(() => {
455+
// Variable name is 'brightness' but uniform name is 'customBrightness'
456+
const brightness = myp5.uniformFloat('customBrightness');
457+
myp5.getPixelInputs(inputs => {
458+
inputs.color = [brightness, brightness, brightness, 1.0];
459+
return inputs;
460+
});
461+
}, { myp5 });
462+
463+
// Set the uniform using the custom name
464+
testShader.setUniform('customBrightness', 0.6);
465+
466+
myp5.noStroke();
467+
myp5.shader(testShader);
468+
myp5.plane(myp5.width, myp5.height);
469+
470+
// Check that the shader uses the manual value (0.6)
471+
const pixelColor = myp5.get(25, 25);
472+
assert.approximately(pixelColor[0], 153, 5); // 0.6 * 255 = 153
473+
assert.approximately(pixelColor[1], 153, 5);
474+
assert.approximately(pixelColor[2], 153, 5);
475+
});
476+
429477
suite('if statement conditionals', () => {
430478
test('handle simple if statement with true condition', () => {
431479
myp5.createCanvas(50, 50, myp5.WEBGL);
@@ -844,6 +892,37 @@ suite('p5.Shader', function() {
844892
assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127
845893
assert.approximately(pixelColor[2], 127, 5); // Blue channel should be ~127
846894
});
895+
896+
test('handle comma-separated expressions with assignments', () => {
897+
myp5.createCanvas(50, 50, myp5.WEBGL);
898+
const testShader = myp5.baseMaterialShader().modify(() => {
899+
const condition = myp5.uniformFloat(() => 1.0); // true condition
900+
myp5.getPixelInputs(inputs => {
901+
let red = myp5.float(0.0);
902+
let green = myp5.float(0.0);
903+
let blue = myp5.float(0.0);
904+
905+
if (condition > 0.5) {
906+
// Use comma-separated expressions with assignments
907+
red = myp5.float(1.0), green = myp5.float(0.5), blue = myp5.float(0.2);
908+
} else {
909+
red = myp5.float(0.0), green = myp5.float(0.0), blue = myp5.float(0.0);
910+
}
911+
912+
inputs.color = [red, green, blue, 1.0];
913+
return inputs;
914+
});
915+
}, { myp5 });
916+
myp5.noStroke();
917+
myp5.shader(testShader);
918+
myp5.plane(myp5.width, myp5.height);
919+
920+
// Check that the center pixel has the expected color (condition was true)
921+
const pixelColor = myp5.get(25, 25);
922+
assert.approximately(pixelColor[0], 255, 5); // Red channel should be 255
923+
assert.approximately(pixelColor[1], 127, 5); // Green channel should be ~127
924+
assert.approximately(pixelColor[2], 51, 5); // Blue channel should be ~51
925+
});
847926
});
848927

849928
suite('for loop statements', () => {

0 commit comments

Comments
 (0)