Skip to content

Add p5.strands support for randomGaussian()#8800

Open
harshiltewari2004 wants to merge 3 commits into
processing:dev-2.0from
harshiltewari2004:strands-random-gaussian
Open

Add p5.strands support for randomGaussian()#8800
harshiltewari2004 wants to merge 3 commits into
processing:dev-2.0from
harshiltewari2004:strands-random-gaussian

Conversation

@harshiltewari2004
Copy link
Copy Markdown
Contributor

Resolves #8775

Changes

Adds randomGaussian() support for p5.strands, following @davepagurek's guidance in #8775. The implementation is a JS-side wrapper over the existing random() strands function, applying a Box-Muller transform to two uniform samples to produce a normal-distributed sample.

  • New augmentFn entry in src/strands/strands_api.js next to the existing random() registration.
  • Falls through to the original p5 randomGaussian() when called outside a strands shader context.
  • Inside a strands context: builds Box-Muller via two this.random() calls + chained node math.

Implementation note

Uses this.x() rather than fn.x() per @davepagurek's note in the issue thread.

Testing

No unit tests included — there's no existing test/unit/strands/ directory and no clear precedent for unit-testing strands functions (which produce IR nodes rather than directly-testable values). Happy to add tests in whichever direction the maintainers prefer.

Local npm test shows the same 4 flaky visual regression test failures (test/unit/visual/cases/typography.js) that I observed on dev-2.0 before my changes — unrelated to this PR.

PR Checklist

  • npm run lint passes
  • [Inline reference] is included / updated
  • [Unit tests] are included / updated

@perminder-17 perminder-17 self-requested a review May 20, 2026 12:02
@p5-bot
Copy link
Copy Markdown

p5-bot Bot commented May 20, 2026

Continuous Release

CDN link

Published Packages

Commit hash: dabfd6d

Previous deployments

055554d


This is an automated message.

Comment thread src/strands/strands_api.js Outdated
Comment on lines +419 to +431
augmentFn(fn, p5, 'randomGaussian', function(...args){
if(!strandsContext.active){
return originalRandomGaussian.apply(this, args);
}
const mean = args.length >= 1 ? args[0] : 0;
const stdDev = args.length>=2 ? args[1] : 1;

const u1 = this.random();
const u2 = this.random();
const z = this.sqrt(this.log(u1).mult(-2)).mult(this.cos(u2.mult(2*Math.PI)));

return z.mult(p5.strandsNode(stdDev)).add(p5.strandsNode(mean));
});
Copy link
Copy Markdown
Collaborator

@perminder-17 perminder-17 May 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thanks for the work @harshiltewari2004 , just one concern I have: are you defining randomGaussian inside the random function intentionally? Do you think it should be outside, otherwise randomGaussian won't exist until the user calls random() at least once, and it'll get redefined on every random() call.

Also, can you ad tests for this implementation?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks @perminder-17 — fixed in the latest commit. The randomGaussian augmentation is now a sibling of random, not nested inside it.

@harshiltewari2004
Copy link
Copy Markdown
Contributor Author

Happy to add tests @perminder-17 — I noticed there's no test/unit/strands/ directory yet, so I held off. Should I establish that pattern here, or is there an existing test approach for strands I missed?

@perminder-17
Copy link
Copy Markdown
Collaborator

off

You can go with the visual tests, that would make more sense. Thanks for the update :)

@harshiltewari2004
Copy link
Copy Markdown
Contributor Author

Thanks @perminder-17! Here's the plan — would appreciate a thumbs-up before I write the tests.

Location: inside the existing visualSuite('p5.strands', ...) in both test/unit/visual/cases/webgl.js and test/unit/visual/cases/webgpu.js. Mirrors the random() colors a basic shader pattern (webgl L1119 / webgpu L329). No new file or suite.

Tests (two per file, four total):

  1. randomGaussian() colors a basic shader — direct analogue of the existing random() shader-coloring test. randomSeed(12), then randomGaussian(0.5, 0.1) per pixel mapped to a grayscale color channel. Mean of 0.5 + small sd keeps ~99.7% of samples in [0.2, 0.8] (3σ), so out-of-range tails are rare enough not to dominate the snapshot — no clamping needed.

  2. randomGaussian() in a fragment loop averages to the mean — analogue of the existing random() in a fragment loop averages to gray test (webgpu L346). randomSeed(7), sum 20 samples of randomGaussian(0.5, 0.2) per pixel, divide by 20. Averaging shrinks the spread by a factor of √20, so the canvas should appear nearly-uniform gray near 0.5. A single-sample test can't catch a wrong distribution mean; this one can.

WebGPU versions will be async with await createCanvas / await screenshot(); WebGL versions sync — per existing convention.

Reference screenshots will be generated on first run, sanity-checked (test 1: Gaussian noise around mid-gray; test 2: near-uniform gray), then committed alongside the test code.

Does this look reasonable? Happy to adjust means/sd/sample counts, or add a third test for a different property.

@perminder-17
Copy link
Copy Markdown
Collaborator

  • randomGaussian() colors a basic shader — direct analogue of the existing random() shader-coloring test. randomSeed(12), then randomGaussian(0.5, 0.1) per pixel mapped to a grayscale color channel. Mean of 0.5 + small sd keeps ~99.7% of samples in [0.2, 0.8] (3σ), so out-of-range tails are rare enough not to dominate the snapshot — no clamping needed.

  • randomGaussian() in a fragment loop averages to the mean — analogue of the existing random() in a fragment loop averages to gray test (webgpu L346). randomSeed(7), sum 20 samples of randomGaussian(0.5, 0.2) per pixel, divide by 20. Averaging shrinks the spread by a factor of √20, so the canvas should appear nearly-uniform gray near 0.5. A single-sample test can't catch a wrong distribution mean; this one can.

Yeah, everything sounds good to me. You can go with adding that. Thanks

The averaging test (loop of 20 samples) revealed that randomGaussian was returning values from N(0.5, 1.0) instead of N(0.5, stdDev), causing heavy clipping visible as outlier pixels throughout the canvas. Root cause: unnecessary p5.strandsNode() wrapping around the stdDev and mean scalars in the final scale-and-shift expression. Removing the wrapping applies the multiplication correctly, mirroring how scalar literals (-2, 2*PI) are already passed to .mult() elsewhere in the same body.

Tests added in both webgl and webgpu suites:
- randomGaussian() colors a basic shader (single-sample distribution)
- randomGaussian() in a fragment loop averages to the mean (variance reduction)
@harshiltewari2004
Copy link
Copy Markdown
Contributor Author

Tests added per discussion — randomGaussian() colors a basic shader and randomGaussian() in a fragment loop averages to the mean, in both webgl and webgpu visual suites. Following the existing patterns from the random() tests in the same files.
One thing worth flagging: writing the averaging test surfaced a bug in the implementation. randomGaussian(0.5, 0.2) was producing samples from N(0.5, 1.0) — the stdDev argument wasn't being applied. Traced it to unnecessary p5.strandsNode() wrapping around the scalar in the final scale-and-shift line. Removed the wrapping (mirroring how -2 and 2*PI are passed raw to .mult() elsewhere in the same expression), and Test 2 now shows the expected variance-reduced output.
Small caveat on Test 2's snapshot: there are a few scattered outlier pixels that don't appear in the analogous random() in a fragment loop averages to gray reference. The math suggests they aren't simple Box-Muller tail behavior (a single extreme sample averaged with 19 normal ones can't push the result outside [0,1] at this stdDev). Could indicate non-independence in strands' random() at certain seeds, but I haven't traced it conclusively. The snapshot is deterministic and stable across runs, so the test catches regressions either way — flagging it here for visibility in case it points at something worth a follow-up.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants