From 8ac5502538a58704eceff1a4534227b199fd20a4 Mon Sep 17 00:00:00 2001 From: Perminder Singh Date: Sat, 18 Apr 2026 21:12:18 +0530 Subject: [PATCH] implementing random function for strands --- src/image/filterRenderer2D.js | 10 ++ src/strands/p5.strands.js | 4 + src/strands/strands_api.js | 85 +++++++++ src/webgl/p5.RendererGL.js | 10 ++ src/webgl/shaders/functions/randomGLSL.glsl | 23 +++ .../shaders/functions/randomVertGLSL.glsl | 21 +++ src/webgpu/p5.RendererWebGPU.js | 166 ++++++++++++++---- .../shaders/functions/randomComputeWGSL.js | 25 +++ .../shaders/functions/randomVertWGSL.js | 24 +++ src/webgpu/shaders/functions/randomWGSL.js | 24 +++ src/webgpu/strands_wgslBackend.js | 12 ++ 11 files changed, 367 insertions(+), 37 deletions(-) create mode 100644 src/webgl/shaders/functions/randomGLSL.glsl create mode 100644 src/webgl/shaders/functions/randomVertGLSL.glsl create mode 100644 src/webgpu/shaders/functions/randomComputeWGSL.js create mode 100644 src/webgpu/shaders/functions/randomVertWGSL.js create mode 100644 src/webgpu/shaders/functions/randomWGSL.js diff --git a/src/image/filterRenderer2D.js b/src/image/filterRenderer2D.js index d6bf72eed3..5f00cb919f 100644 --- a/src/image/filterRenderer2D.js +++ b/src/image/filterRenderer2D.js @@ -18,6 +18,8 @@ import webgl2CompatibilityShader from '../webgl/shaders/webgl2Compatibility.glsl import { glslBackend } from '../webgl/strands_glslBackend'; import { getShaderHookTypes } from '../webgl/shaderHookUtils'; import noiseGLSL from '../webgl/shaders/functions/noise3DGLSL.glsl'; +import randomGLSL from '../webgl/shaders/functions/randomGLSL.glsl'; +import randomVertGLSL from '../webgl/shaders/functions/randomVertGLSL.glsl'; import { makeFilterShader } from '../core/filterShaders'; class FilterRenderer2D { @@ -310,6 +312,14 @@ class FilterRenderer2D { return noiseGLSL; } + getRandomFragmentShaderSnippet() { + return randomGLSL; + } + + getRandomVertexShaderSnippet() { + return randomVertGLSL; + } + /** * Set the current filter operation and parameter. If a customShader is provided, * that overrides the operation-based shader. diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index d8c839847e..5b13893880 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -50,6 +50,8 @@ function strands(p5, fn) { ctx.windowOverrides = {}; ctx.fnOverrides = {}; ctx.graphicsOverrides = {}; + ctx._randomCallCount = 0; + ctx._randomSeed = null; if (active) { p5.disableFriendlyErrors = true; } @@ -65,6 +67,8 @@ function strands(p5, fn) { ctx.computeDeclarations = new Set(); ctx.hooks = []; ctx.active = false; + ctx._randomCallCount = 0; + ctx._randomSeed = null; p5.disableFriendlyErrors = ctx.previousFES; for (const key in ctx.windowOverrides) { window[key] = ctx.windowOverrides[key]; diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index ab1453eb1c..2f36372d0a 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -318,6 +318,8 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { // Add noise function with backend-agnostic implementation const originalNoise = fn.noise; const originalNoiseDetail = fn.noiseDetail; + const originalRandom = fn.random; + const originalRandomSeed = fn.randomSeed; const originalMillis = fn.millis; strandsContext._noiseOctaves = null; @@ -382,6 +384,89 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { return createStrandsNode(id, dimension, strandsContext); }); + strandsContext._randomSeed = null; + strandsContext._randomCallCount = 0; + + augmentFn(fn, p5, 'randomSeed', function (seed) { + if (!strandsContext.active) { + return originalRandomSeed.apply(this, arguments); + } + strandsContext._randomSeed = seed; + }); + + augmentFn(fn, p5, 'random', function (...args) { + if (!strandsContext.active) { + return originalRandom.apply(this, args); + } + + const randomVertSnippet = this._renderer.getRandomVertexShaderSnippet(); + const randomFragSnippet = this._renderer.getRandomFragmentShaderSnippet(); + + strandsContext.vertexDeclarations.add(randomVertSnippet); + strandsContext.fragmentDeclarations.add(randomFragSnippet); + + if (this._renderer.getRandomComputeShaderSnippet) { + const randomComputeSnippet = this._renderer.getRandomComputeShaderSnippet(); + strandsContext.computeDeclarations.add(randomComputeSnippet); + } + + let seedNode; + if (strandsContext._randomSeed !== null && strandsContext._randomSeed.isStrandsNode) { + seedNode = strandsContext._randomSeed; + } else { + const userSeed = strandsContext._randomSeed; + seedNode = getOrCreateUniformNode( + strandsContext, + '_p5_randomSeed', + DataType.float1, + userSeed !== null + ? () => userSeed + : () => performance.now(), + ); + } + + const callIndex = strandsContext._randomCallCount++; + + const nodeArgs = [seedNode, callIndex]; + + if (args.length === 0) { + const { id, dimension } = build.functionCallNode(strandsContext, 'random', nodeArgs, { + overloads: [{ + params: [DataType.float1, DataType.float1], + returnType: DataType.float1, + }] + }); + return createStrandsNode(id, dimension, strandsContext); + } else if (args.length === 1) { + // random(max) → [0, max) + const rawNode = build.functionCallNode(strandsContext, 'random', nodeArgs, { + overloads: [{ + params: [DataType.float1, DataType.float1], + returnType: DataType.float1, + }] + }); + const rawStrandsNode = createStrandsNode(rawNode.id, rawNode.dimension, strandsContext); + return rawStrandsNode.mult(p5.strandsNode(args[0])); + } else if (args.length === 2) { + // random(min, max) → [min, max) + const rawNode = build.functionCallNode(strandsContext, 'random', nodeArgs, { + overloads: [{ + params: [DataType.float1, DataType.float1], + returnType: DataType.float1, + }] + }); + const rawStrandsNode = createStrandsNode(rawNode.id, rawNode.dimension, strandsContext); + const minNode = p5.strandsNode(args[0]); + const maxNode = p5.strandsNode(args[1]); + // min + raw * (max - min) + return rawStrandsNode.mult(maxNode.sub(minNode)).add(minNode); + } else { + p5._friendlyError( + `It looks like you've called random() with ${args.length} arguments. In strands, random() supports 0, 1, or 2 numeric arguments.` + ); + } + }); + augmentFn(fn, p5, 'millis', function (...args) { if (!strandsContext.active) { return originalMillis.apply(this, args); diff --git a/src/webgl/p5.RendererGL.js b/src/webgl/p5.RendererGL.js index b744baff19..af85208e80 100644 --- a/src/webgl/p5.RendererGL.js +++ b/src/webgl/p5.RendererGL.js @@ -20,6 +20,8 @@ import { glslBackend } from './strands_glslBackend'; import { TypeInfoFromGLSLName } from '../strands/ir_types.js'; import { getShaderHookTypes } from './shaderHookUtils'; import noiseGLSL from './shaders/functions/noise3DGLSL.glsl'; +import randomGLSL from './shaders/functions/randomGLSL.glsl'; +import randomVertGLSL from './shaders/functions/randomVertGLSL.glsl'; import filterBaseVert from "./shaders/filters/base.vert"; import lightingShader from "./shaders/lighting.glsl"; @@ -1909,6 +1911,14 @@ class RendererGL extends Renderer3D { return noiseGLSL; } + getRandomFragmentShaderSnippet() { + return randomGLSL; + } + + getRandomVertexShaderSnippet() { + return randomVertGLSL; + } + } function rendererGL(p5, fn) { diff --git a/src/webgl/shaders/functions/randomGLSL.glsl b/src/webgl/shaders/functions/randomGLSL.glsl new file mode 100644 index 0000000000..58bdc2fc96 --- /dev/null +++ b/src/webgl/shaders/functions/randomGLSL.glsl @@ -0,0 +1,23 @@ +// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW) +// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) +// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal) +// α₂ = 1/φ₂² = 0.5698402910 +// 1/φ = 0.6180339887 (golden ratio conjugate) + +float _p5_hash(vec3 p) { + p = fract(p * vec3(0.1031, 0.1030, 0.0973)); + p += dot(p, p.yxz + 33.33); + return fract((p.x + p.y) * p.z); +} + +float random(float seed, float callIndex) { + vec2 pixelCoord = gl_FragCoord.xy; + // fract(seed * α₁) normalizes large seeds (e.g. performance.now()) into [0,1) + // and spreads them optimally via the R₂ sequence's plastic constant + float s = fract(seed * 0.7548776662); + return _p5_hash(vec3( + pixelCoord.x + s, + pixelCoord.y + callIndex * 0.5698402910, + s + callIndex * 0.6180339887 + )); +} diff --git a/src/webgl/shaders/functions/randomVertGLSL.glsl b/src/webgl/shaders/functions/randomVertGLSL.glsl new file mode 100644 index 0000000000..2940be1b96 --- /dev/null +++ b/src/webgl/shaders/functions/randomVertGLSL.glsl @@ -0,0 +1,21 @@ +// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW) +// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) +// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal) +// α₂ = 1/φ₂² = 0.5698402910 +// 1/φ = 0.6180339887 (golden ratio conjugate) + +float _p5_hash(vec3 p) { + p = fract(p * vec3(0.1031, 0.1030, 0.0973)); + p += dot(p, p.yxz + 33.33); + return fract((p.x + p.y) * p.z); +} + +float random(float seed, float callIndex) { + float vid = float(gl_VertexID); + float s = fract(seed * 0.7548776662); + return _p5_hash(vec3( + vid + s, + vid * 0.5698402910 + callIndex * 0.6180339887, + s + callIndex * 0.7548776662 + )); +} diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 61f8580ac6..a562ee371a 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -15,6 +15,9 @@ import { fontVertexShader, fontFragmentShader } from './shaders/font'; import { blitVertexShader, blitFragmentShader } from './shaders/blit'; import { wgslBackend } from './strands_wgslBackend'; import noiseWGSL from './shaders/functions/noise3DWGSL'; +import randomWGSL from './shaders/functions/randomWGSL'; +import randomVertWGSL from './shaders/functions/randomVertWGSL'; +import randomComputeWGSL from './shaders/functions/randomComputeWGSL'; import { baseFilterVertexShader, baseFilterFragmentShader } from './shaders/filters/base'; import { imageLightVertexShader, imageLightDiffusedFragmentShader, imageLightSpecularFragmentShader } from './shaders/imageLight'; import { baseComputeShader } from './shaders/compute'; @@ -2533,10 +2536,86 @@ function rendererWebGPU(p5, fn) { ); let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment|compute)\s*(?:@workgroup_size\([^)]+\)\s*)?)?fn main[^{]+\{)/); + + const getBuiltinParamName = (mainSrc, builtinName) => { + const match = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`).exec(mainSrc); + return match ? match[1] : null; + }; + + const ensureBuiltinParam = (mainSrc, builtinName, fallbackName, typeName) => { + const existingName = getBuiltinParamName(mainSrc, builtinName); + if (existingName) { + return { mainSrc, argName: existingName }; + } + + const hasParams = /\(\s*\S/.test(mainSrc); + const injectedMain = mainSrc.replace( + /\)\s*(->|\{)/, + `${hasParams ? ', ' : ''}@builtin(${builtinName}) ${fallbackName}: ${typeName}) $1` + ); + + return { mainSrc: injectedMain, argName: fallbackName }; + }; + + const getMainStructParameter = (mainSrc) => { + const match = /fn main\s*\(\s*(\w+)\s*:\s*(\w+)/.exec(mainSrc); + if (!match) return null; + return { inputName: match[1], structName: match[2] }; + }; + + const getStructBuiltinFieldName = (structName, builtinName) => { + const structMatch = new RegExp(`struct\\s+${structName}\\s*\\{([^}]*)\\}`, 's').exec(preMain); + if (!structMatch) return null; + const fieldMatch = new RegExp(`@builtin\\s*\\(\\s*${builtinName}\\s*\\)\\s*(\\w+)\\s*:`, 's').exec(structMatch[1]); + return fieldMatch ? fieldMatch[1] : null; + }; + + const appendHookParams = (params, additionalParams) => { + if (additionalParams.length === 0) return params; + const hasParams = !/^\(\s*\)$/.test(params); + return `${params.slice(0, -1)}${hasParams ? ', ' : ''}${additionalParams.join(', ')})`; + }; + + let hookExtraParams = []; + let hookExtraArgs = []; + if (shaderType === 'vertex') { - if (!main.match(/\@builtin\s*\(\s*instance_index\s*\)/)) { - main = main.replace(/\)\s*(->|\{)/, ', @builtin(instance_index) instanceID: u32) $1'); + const ensuredInstance = ensureBuiltinParam(main, 'instance_index', 'instanceID', 'u32'); + main = ensuredInstance.mainSrc; + + const ensuredVertex = ensureBuiltinParam(main, 'vertex_index', '_p5VertexId', 'u32'); + main = ensuredVertex.mainSrc; + + hookExtraParams = ['instanceID: u32', '_p5VertexId: u32']; + hookExtraArgs = [ensuredInstance.argName, ensuredVertex.argName]; + } else if (shaderType === 'fragment') { + const directPositionArg = getBuiltinParamName(main, 'position'); + let fragmentPositionArg = directPositionArg; + + if (!fragmentPositionArg) { + const mainStructParam = getMainStructParameter(main); + if (mainStructParam) { + const positionField = getStructBuiltinFieldName(mainStructParam.structName, 'position'); + if (positionField) { + fragmentPositionArg = `${mainStructParam.inputName}.${positionField}`; + } + } } + + if (!fragmentPositionArg) { + const ensuredPosition = ensureBuiltinParam(main, 'position', '_p5FragPos', 'vec4'); + main = ensuredPosition.mainSrc; + fragmentPositionArg = ensuredPosition.argName; + } + + hookExtraParams = ['_p5FragPos: vec4']; + hookExtraArgs = [fragmentPositionArg]; + } else if (shaderType === 'compute') { + const ensuredGlobalId = ensureBuiltinParam(main, 'global_invocation_id', '_p5GlobalId', 'vec3'); + main = ensuredGlobalId.mainSrc; + + hookExtraParams = ['_p5GlobalId: vec3']; + hookExtraArgs = [ensuredGlobalId.argName]; } // Inject hook uniforms as a separate struct at a new binding @@ -2724,11 +2803,7 @@ ${hookUniformFields}} let [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]); - if (shaderType === 'vertex') { - // Splice the instance ID in as a final parameter to every WGSL hook function - let hasParams = !!params.match(/^\(\s*\S+.*\)$/); - params = params.slice(0, -1) + (hasParams ? ', ' : '') + 'instanceID: u32)'; - } + params = appendHookParams(params, hookExtraParams); if (hookType === 'void') { hooks += `fn HOOK_${hookName}${params}${body}\n`; @@ -2737,40 +2812,45 @@ ${hookUniformFields}} } } - // Add the instance ID as a final parameter to each hook call - if (shaderType === 'vertex') { - const addInstanceIDParam = (src) => { - let result = src; - let idx = 0; - let match; - do { - match = /HOOK_\w+\(/.exec(result.slice(idx)); - if (match) { - idx += match.index + match[0].length - 1; - let nesting = 0; - let hasParams = false; - while (idx < result.length) { - if (result[idx] === '(') { - nesting++; - } else if (result[idx] === ')') { - nesting--; - } else if (result[idx].match(/\S/)) { - hasParams = true; - } - idx++; - if (nesting === 0) { - break; - } + // Pass stage-specific builtins from main to each hook call. + // Collect ALL HOOK_ calls (including nested ones) then insert + // extra args from right to left so position shifts don't + // invalidate earlier insertion points. + if (hookExtraArgs.length > 0) { + const addHookArgs = (src) => { + const insertions = []; + let searchIdx = 0; + let m; + while ((m = /HOOK_\w+\(/.exec(src.slice(searchIdx))) !== null) { + const openParen = searchIdx + m.index + m[0].length - 1; + let pos = openParen + 1; + let nesting = 1; + let hasParams = false; + while (pos < src.length && nesting > 0) { + if (src[pos] === '(') nesting++; + else if (src[pos] === ')') { + nesting--; + if (nesting === 0) break; + } else if (/\S/.test(src[pos])) { + hasParams = true; } - const insertion = (hasParams ? ', ' : '') + 'instanceID'; - result = result.slice(0, idx-1) + insertion + result.slice(idx-1); - idx += insertion.length; + pos++; } - } while (match); + insertions.push({ pos, hasParams }); + searchIdx = openParen + 1; + } + + insertions.sort((a, b) => b.pos - a.pos); + + let result = src; + for (const { pos, hasParams } of insertions) { + const insertion = (hasParams ? ', ' : '') + hookExtraArgs.join(', '); + result = result.slice(0, pos) + insertion + result.slice(pos); + } return result; }; - preMain = addInstanceIDParam(preMain); - postMain = addInstanceIDParam(postMain); + preMain = addHookArgs(preMain); + postMain = addHookArgs(postMain); } return preMain + '\n' + defines + hooks + main + postMain; @@ -3654,6 +3734,18 @@ ${hookUniformFields}} return noiseWGSL; } + getRandomFragmentShaderSnippet() { + return randomWGSL; + } + + getRandomVertexShaderSnippet() { + return randomVertWGSL; + } + + getRandomComputeShaderSnippet() { + return randomComputeWGSL; + } + baseFilterShader() { if (!this._baseFilterShader) { diff --git a/src/webgpu/shaders/functions/randomComputeWGSL.js b/src/webgpu/shaders/functions/randomComputeWGSL.js new file mode 100644 index 0000000000..9b9b17379c --- /dev/null +++ b/src/webgpu/shaders/functions/randomComputeWGSL.js @@ -0,0 +1,25 @@ +// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW) +// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) +// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal) +// α₂ = 1/φ₂² = 0.5698402910 +// 1/φ = 0.6180339887 (golden ratio conjugate) +// +// Compute shader version: invocationId is passed in from main via @builtin(global_invocation_id) + +export default ` +fn _p5_hash(p: vec3) -> f32 { + var p3 = fract(p * vec3(0.1031, 0.1030, 0.0973)); + p3 = p3 + dot(p3, p3.yxz + 33.33); + return fract((p3.x + p3.y) * p3.z); +} + +fn random(seed: f32, callIndex: f32, invocationId: vec3) -> f32 { + let id = vec3(invocationId); + let s = fract(seed * 0.7548776662); + return _p5_hash(vec3( + id.x + s, + id.y + callIndex * 0.5698402910, + id.z + s + callIndex * 0.6180339887 + )); +} +`; diff --git a/src/webgpu/shaders/functions/randomVertWGSL.js b/src/webgpu/shaders/functions/randomVertWGSL.js new file mode 100644 index 0000000000..008a5448cd --- /dev/null +++ b/src/webgpu/shaders/functions/randomVertWGSL.js @@ -0,0 +1,24 @@ +// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW) +// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) +// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal) +// α₂ = 1/φ₂² = 0.5698402910 +// 1/φ = 0.6180339887 (golden ratio conjugate) +// +// Vertex shader version: vertexId is passed in from main via @builtin(vertex_index) + +export default ` +fn _p5_hash(p: vec3) -> f32 { + var p3 = fract(p * vec3(0.1031, 0.1030, 0.0973)); + p3 = p3 + dot(p3, p3.yxz + 33.33); + return fract((p3.x + p3.y) * p3.z); +} + +fn random(seed: f32, callIndex: f32, vertexId: f32) -> f32 { + let s = fract(seed * 0.7548776662); + return _p5_hash(vec3( + vertexId + s, + vertexId * 0.5698402910 + callIndex * 0.6180339887, + s + callIndex * 0.7548776662 + )); +} +`; diff --git a/src/webgpu/shaders/functions/randomWGSL.js b/src/webgpu/shaders/functions/randomWGSL.js new file mode 100644 index 0000000000..3e0b69d289 --- /dev/null +++ b/src/webgpu/shaders/functions/randomWGSL.js @@ -0,0 +1,24 @@ +// _p5_hash: "Hash without Sine" by Dave Hoskins (https://www.shadertoy.com/view/4djSRW) +// Mixing constants: R₂ sequence by Martin Roberts (https://extremelearning.com.au/unreasonable-effectiveness-of-quasirandom-sequences/) +// α₁ = 1/φ₂ = 0.7548776662 (plastic constant reciprocal) +// α₂ = 1/φ₂² = 0.5698402910 +// 1/φ = 0.6180339887 (golden ratio conjugate) +// +// Fragment shader version: pixelCoord is passed in from main via @builtin(position) + +export default ` +fn _p5_hash(p: vec3) -> f32 { + var p3 = fract(p * vec3(0.1031, 0.1030, 0.0973)); + p3 = p3 + dot(p3, p3.yxz + 33.33); + return fract((p3.x + p3.y) * p3.z); +} + +fn random(seed: f32, callIndex: f32, pixelCoord: vec2) -> f32 { + let s = fract(seed * 0.7548776662); + return _p5_hash(vec3( + pixelCoord.x + s, + pixelCoord.y + callIndex * 0.5698402910, + s + callIndex * 0.6180339887 + )); +} +`; diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index 32226fa83d..63366d900a 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -482,6 +482,18 @@ export const wgslBackend = { } const functionArgs = node.dependsOn.map(arg =>this.generateExpression(generationContext, dag, arg)); + + if (node.identifier === 'random') { + const ctx = generationContext.shaderContext; + if (ctx === 'fragment') { + functionArgs.push('_p5FragPos.xy'); + } else if (ctx === 'vertex') { + functionArgs.push('f32(_p5VertexId)'); + } else if (ctx === 'compute') { + functionArgs.push('_p5GlobalId'); + } + } + return `${node.identifier}(${functionArgs.join(', ')})`; } if (node.opCode === OpCode.Binary.MEMBER_ACCESS) {