diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 087da8a8eb..c459374c74 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -21,6 +21,86 @@ import * as FES from './strands_FES' import { getNodeDataFromID } from './ir_dag' import { StrandsNode, createStrandsNode } from './strands_node' +const BUILTIN_GLOBAL_SPECS = { + width: { typeInfo: DataType.float1, get: (p) => p.width }, + height: { typeInfo: DataType.float1, get: (p) => p.height }, + mouseX: { typeInfo: DataType.float1, get: (p) => p.mouseX }, + mouseY: { typeInfo: DataType.float1, get: (p) => p.mouseY }, + pmouseX: { typeInfo: DataType.float1, get: (p) => p.pmouseX }, + pmouseY: { typeInfo: DataType.float1, get: (p) => p.pmouseY }, + winMouseX: { typeInfo: DataType.float1, get: (p) => p.winMouseX }, + winMouseY: { typeInfo: DataType.float1, get: (p) => p.winMouseY }, + pwinMouseX: { typeInfo: DataType.float1, get: (p) => p.pwinMouseX }, + pwinMouseY: { typeInfo: DataType.float1, get: (p) => p.pwinMouseY }, + frameCount: { typeInfo: DataType.float1, get: (p) => p.frameCount }, + deltaTime: { typeInfo: DataType.float1, get: (p) => p.deltaTime }, + displayWidth: { typeInfo: DataType.float1, get: (p) => p.displayWidth }, + displayHeight: { typeInfo: DataType.float1, get: (p) => p.displayHeight }, + windowWidth: { typeInfo: DataType.float1, get: (p) => p.windowWidth }, + windowHeight: { typeInfo: DataType.float1, get: (p) => p.windowHeight }, + mouseIsPressed: { typeInfo: DataType.bool1, get: (p) => p.mouseIsPressed }, +} + +function _getBuiltinGlobalsCache(strandsContext) { + if (!strandsContext._builtinGlobals || strandsContext._builtinGlobals.dag !== strandsContext.dag) { + strandsContext._builtinGlobals = { + dag: strandsContext.dag, + nodes: new Map(), + uniformsAdded: new Set(), + } + } + // return the cache + return strandsContext._builtinGlobals +} + +function getBuiltinGlobalNode(strandsContext, name) { + const spec = BUILTIN_GLOBAL_SPECS[name] + if (!spec) return null + + const cache = _getBuiltinGlobalsCache(strandsContext) + const uniformName = `_p5_global_${name}` + const cached = cache.nodes.get(uniformName) + if (cached) return cached + + if (!cache.uniformsAdded.has(uniformName)) { + cache.uniformsAdded.add(uniformName) + strandsContext.uniforms.push({ + name: uniformName, + typeInfo: spec.typeInfo, + defaultValue: () => { + const p5Instance = strandsContext.renderer?._pInst || strandsContext.p5?.instance + return p5Instance ? spec.get(p5Instance) : undefined + }, + }) + } + + const { id, dimension } = build.variableNode(strandsContext, spec.typeInfo, uniformName) + const node = createStrandsNode(id, dimension, strandsContext) + node._originalBuiltinName = name + cache.nodes.set(uniformName, node) + return node +} + +function installBuiltinGlobalAccessors(strandsContext) { + if (strandsContext._builtinGlobalsAccessorsInstalled) return + + const getRuntimeP5Instance = () => strandsContext.renderer?._pInst || strandsContext.p5?.instance + + for (const name of Object.keys(BUILTIN_GLOBAL_SPECS)) { + const spec = BUILTIN_GLOBAL_SPECS[name] + Object.defineProperty(window, name, { + get: () => { + if (strandsContext.active) { + return getBuiltinGlobalNode(strandsContext, name); + } + const inst = getRuntimeP5Instance() + return spec.get(inst); + }, + }) + } + strandsContext._builtinGlobalsAccessorsInstalled = true +} + ////////////////////////////////////////////// // User nodes ////////////////////////////////////////////// @@ -419,6 +499,8 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName return returnedNodeID; } export function createShaderHooksFunctions(strandsContext, fn, shader) { + installBuiltinGlobalAccessors(strandsContext) + // Add shader context to hooks before spreading const vertexHooksWithContext = Object.fromEntries( Object.entries(shader.hooks.vertex).map(([name, hook]) => [name, { ...hook, shaderContext: 'vertex' }]) diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index e4c0c5f4b9..1b644a27a0 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -1022,6 +1022,24 @@ visualSuite('WebGL', function() { screenshot(); }); + visualTest('uses width/height in getFinalColor', (p5, screenshot) => { + let firstShader; + function firstShaderCallback() { + getFinalColor((color) => { + color = [width / 60, height / 60, 0, 1]; + return color; + }); + } + p5.createCanvas(60, 60, p5.WEBGL); + p5.pixelDensity(1); + firstShader = p5.baseColorShader().modify(firstShaderCallback); + p5.background(0); + p5.shader(firstShader); + p5.noStroke(); + p5.plane(20, 20); + screenshot(); + }); + visualSuite('auto-return for shader hooks', () => { visualTest('auto-returns input struct when return is omitted', (p5, screenshot) => { p5.createCanvas(50, 50, p5.WEBGL); diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/uses width%2Fheight in getFinalColor/000.png b/test/unit/visual/screenshots/WebGL/p5.strands/uses width%2Fheight in getFinalColor/000.png new file mode 100644 index 0000000000..668b80e93e Binary files /dev/null and b/test/unit/visual/screenshots/WebGL/p5.strands/uses width%2Fheight in getFinalColor/000.png differ diff --git a/test/unit/visual/screenshots/WebGL/p5.strands/uses width%2Fheight in getFinalColor/metadata.json b/test/unit/visual/screenshots/WebGL/p5.strands/uses width%2Fheight in getFinalColor/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGL/p5.strands/uses width%2Fheight in getFinalColor/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index f520101be8..75e68257f2 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -447,6 +447,27 @@ suite('p5.Shader', function() { }).not.toThrowError(); }); +test('returns numbers for builtin globals outside hooks and a strandNode when called inside hooks', () => { + myp5.createCanvas(5, 5, myp5.WEBGL); + myp5.baseMaterialShader().modify(() => { + myp5.getPixelInputs(inputs => { + const mxInHook = window.mouseX; + const wInHook = window.width; + assert.isTrue(mxInHook.isStrandsNode); + assert.isTrue(wInHook.isStrandsNode); + inputs.color = [1, 0, 0, 1]; + return inputs; + }); + }, { myp5 }); + + const mx = window.mouseX; + const w = window.width; + assert.isNumber(mx); + assert.isNumber(w); + assert.strictEqual(w, myp5.width); +}); + + test('handle custom uniform names with automatic values', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => {