Skip to content

Commit 5a7a0a8

Browse files
authored
Merge pull request #8730 from perminder-17/random-strands
implementing random function for strands
2 parents f1e3da6 + 3fb5509 commit 5a7a0a8

19 files changed

Lines changed: 447 additions & 45 deletions

File tree

src/image/filterRenderer2D.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import filterBaseVert from '../webgl/shaders/filters/base.vert';
1717
import webgl2CompatibilityShader from '../webgl/shaders/webgl2Compatibility.glsl';
1818
import { glslBackend } from '../webgl/strands_glslBackend';
1919
import { getShaderHookTypes } from '../webgl/shaderHookUtils';
20+
import randomGLSL from '../webgl/shaders/functions/randomGLSL.glsl';
21+
import randomVertGLSL from '../webgl/shaders/functions/randomVertGLSL.glsl';
2022
import { makeFilterShader } from '../core/filterShaders';
2123

2224
class FilterRenderer2D {
@@ -309,6 +311,14 @@ class FilterRenderer2D {
309311
}
310312

311313

314+
getRandomFragmentShaderSnippet() {
315+
return randomGLSL;
316+
}
317+
318+
getRandomVertexShaderSnippet() {
319+
return randomVertGLSL;
320+
}
321+
312322
/**
313323
* Set the current filter operation and parameter. If a customShader is provided,
314324
* that overrides the operation-based shader.

src/strands/p5.strands.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ function strands(p5, fn) {
4949
ctx.windowOverrides = {};
5050
ctx.fnOverrides = {};
5151
ctx.graphicsOverrides = {};
52+
ctx._randomSeed = null;
5253
if (active) {
5354
p5.disableFriendlyErrors = true;
5455
}
@@ -64,6 +65,7 @@ function strands(p5, fn) {
6465
ctx.computeDeclarations = new Set();
6566
ctx.hooks = [];
6667
ctx.active = false;
68+
ctx._randomSeed = null;
6769
p5.disableFriendlyErrors = ctx.previousFES;
6870
for (const key in ctx.windowOverrides) {
6971
window[key] = ctx.windowOverrides[key];

src/strands/strands_api.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
318318
// Add noise function with backend-agnostic implementation
319319
const originalNoise = fn.noise;
320320
const originalNoiseDetail = fn.noiseDetail;
321+
const originalRandom = fn.random;
322+
const originalRandomSeed = fn.randomSeed;
321323
const originalMillis = fn.millis;
322324

323325
strandsContext._noiseOctaves = null;
@@ -382,6 +384,84 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
382384
return createStrandsNode(id, dimension, strandsContext);
383385
});
384386

387+
strandsContext._randomSeed = null;
388+
389+
augmentFn(fn, p5, 'randomSeed', function (seed) {
390+
if (!strandsContext.active) {
391+
return originalRandomSeed.apply(this, arguments);
392+
}
393+
strandsContext._randomSeed = seed;
394+
});
395+
396+
augmentFn(fn, p5, 'random', function (...args) {
397+
if (!strandsContext.active) {
398+
return originalRandom.apply(this, args);
399+
}
400+
401+
const randomVertSnippet = strandsContext.backend.getRandomVertexShaderSnippet();
402+
const randomFragSnippet = strandsContext.backend.getRandomFragmentShaderSnippet();
403+
404+
strandsContext.vertexDeclarations.add(randomVertSnippet);
405+
strandsContext.fragmentDeclarations.add(randomFragSnippet);
406+
407+
if (strandsContext.backend.getRandomComputeShaderSnippet) {
408+
const randomComputeSnippet = strandsContext.backend.getRandomComputeShaderSnippet();
409+
strandsContext.computeDeclarations.add(randomComputeSnippet);
410+
}
411+
412+
let seedNode;
413+
if (strandsContext._randomSeed !== null && strandsContext._randomSeed.isStrandsNode) {
414+
seedNode = strandsContext._randomSeed;
415+
} else {
416+
const userSeed = strandsContext._randomSeed;
417+
seedNode = getOrCreateUniformNode(
418+
strandsContext,
419+
'_p5_randomSeed',
420+
DataType.float1,
421+
userSeed !== null
422+
? () => userSeed
423+
: () => performance.now(),
424+
);
425+
}
426+
427+
// The shader-side random() owns a private per-invocation counter, so a
428+
// single AST node still produces distinct values across runtime loop
429+
// iterations. We just pass the seed.
430+
const nodeArgs = [seedNode];
431+
const randomOverloads = [{
432+
params: [DataType.float1],
433+
returnType: DataType.float1,
434+
}];
435+
436+
if (args.length === 0) {
437+
const { id, dimension } = build.functionCallNode(strandsContext, 'random', nodeArgs, {
438+
overloads: randomOverloads,
439+
});
440+
return createStrandsNode(id, dimension, strandsContext);
441+
} else if (args.length === 1) {
442+
// random(max) → [0, max)
443+
const rawNode = build.functionCallNode(strandsContext, 'random', nodeArgs, {
444+
overloads: randomOverloads,
445+
});
446+
const rawStrandsNode = createStrandsNode(rawNode.id, rawNode.dimension, strandsContext);
447+
return rawStrandsNode.mult(p5.strandsNode(args[0]));
448+
} else if (args.length === 2) {
449+
// random(min, max) → [min, max)
450+
const rawNode = build.functionCallNode(strandsContext, 'random', nodeArgs, {
451+
overloads: randomOverloads,
452+
});
453+
const rawStrandsNode = createStrandsNode(rawNode.id, rawNode.dimension, strandsContext);
454+
const minNode = p5.strandsNode(args[0]);
455+
const maxNode = p5.strandsNode(args[1]);
456+
// min + raw * (max - min)
457+
return rawStrandsNode.mult(maxNode.sub(minNode)).add(minNode);
458+
} else {
459+
p5._friendlyError(
460+
`It looks like you've called random() with ${args.length} arguments. In strands, random() supports 0, 1, or 2 numeric arguments.`
461+
);
462+
}
463+
});
464+
385465
augmentFn(fn, p5, 'millis', function (...args) {
386466
if (!strandsContext.active) {
387467
return originalMillis.apply(this, args);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW)
2+
// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/)
3+
// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal)
4+
// α₂ = 1/φ₂² = 0.5698402910
5+
// 1/φ = 0.6180339887 (golden ratio conjugate)
6+
7+
int _p5_randomCallIndex = 0;
8+
9+
float _p5_hash(vec3 p) {
10+
p = fract(p * vec3(0.1031, 0.1030, 0.0973));
11+
p += dot(p, p.yxz + 33.33);
12+
return fract((p.x + p.y) * p.z);
13+
}
14+
15+
float random(float seed) {
16+
vec2 pixelCoord = gl_FragCoord.xy;
17+
float callIndex = float(_p5_randomCallIndex);
18+
_p5_randomCallIndex += 1;
19+
// fract(seed * α₁) normalizes large seeds (e.g. performance.now()) into [0,1)
20+
// and spreads them optimally via the R₂ sequence's plastic constant
21+
float s = fract(seed * 0.7548776662);
22+
return _p5_hash(vec3(
23+
pixelCoord.x + s,
24+
pixelCoord.y + callIndex * 0.5698402910,
25+
s + callIndex * 0.6180339887
26+
));
27+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW)
2+
// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/)
3+
// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal)
4+
// α₂ = 1/φ₂² = 0.5698402910
5+
// 1/φ = 0.6180339887 (golden ratio conjugate)
6+
7+
int _p5_randomCallIndex = 0;
8+
9+
float _p5_hash(vec3 p) {
10+
p = fract(p * vec3(0.1031, 0.1030, 0.0973));
11+
p += dot(p, p.yxz + 33.33);
12+
return fract((p.x + p.y) * p.z);
13+
}
14+
15+
float random(float seed) {
16+
float vid = float(gl_VertexID);
17+
float callIndex = float(_p5_randomCallIndex);
18+
_p5_randomCallIndex += 1;
19+
float s = fract(seed * 0.7548776662);
20+
return _p5_hash(vec3(
21+
vid + s,
22+
vid * 0.5698402910 + callIndex * 0.6180339887,
23+
s + callIndex * 0.7548776662
24+
));
25+
}

src/webgl/strands_glslBackend.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import noiseGLSL from './shaders/functions/noise3DGLSL.glsl';
2+
import randomGLSL from './shaders/functions/randomGLSL.glsl';
3+
import randomVertGLSL from './shaders/functions/randomVertGLSL.glsl';
24
import { NodeType, OpCodeToSymbol, BlockType, OpCode, NodeTypeToName, isStructType, BaseType, StatementType, DataType, INSTANCE_ID_VARYING_NAME } from "../strands/ir_types";
35
import { getNodeDataFromID, extractNodeTypeInfo } from "../strands/ir_dag";
46
import * as FES from '../strands/strands_FES';
@@ -173,6 +175,12 @@ export const glslBackend = {
173175
getNoiseShaderSnippet() {
174176
return noiseGLSL;
175177
},
178+
getRandomFragmentShaderSnippet() {
179+
return randomGLSL;
180+
},
181+
getRandomVertexShaderSnippet() {
182+
return randomVertGLSL;
183+
},
176184
getTypeName(baseType, dimension) {
177185
const primitiveTypeName = TypeNames[baseType + dimension]
178186
if (!primitiveTypeName) {

0 commit comments

Comments
 (0)