diff --git a/src/strands/p5.strands.js b/src/strands/p5.strands.js index e9d4cd7143..d783c77f0a 100644 --- a/src/strands/p5.strands.js +++ b/src/strands/p5.strands.js @@ -338,18 +338,20 @@ if (typeof p5 !== "undefined") { */ /** - * @method instanceID + * @property instanceIndex * @beta * @description * Returns the index of the current instance when drawing multiple copies of a * shape with `model(count)`. The first instance has an - * ID of `0`, the second has `1`, and so on. + * index of `0`, the second has `1`, and so on. * * This lets each copy of a shape behave differently. For example, you can use - * the ID to place instances at different positions, give them different colors, + * the index to place instances at different positions, give them different colors, * or animate them at different speeds. * - * `instanceID()` can only be used inside a p5.strands shader callback. + * `instanceIndex` can only be used inside a p5.strands shader callback. + * + * (Note: `instanceID()` is also available as a function for compatibility.) * * ```js example * let instancesShader; @@ -372,7 +374,7 @@ if (typeof p5 !== "undefined") { * // Spread spheres evenly across the canvas based on their index * let spacing = width / count; * worldInputs.position.x += - * (instanceID() - (count - 1) / 2) * spacing; + * (instanceIndex - (count - 1) / 2) * spacing; * worldInputs.end(); * } * @@ -386,7 +388,7 @@ if (typeof p5 !== "undefined") { * } * ``` * - * If you are using WebGPU mode, a common pattern is to use `instanceID()` to look up data made with + * If you are using WebGPU mode, a common pattern is to use `instanceIndex` to look up data made with * `createStorage()`. * This lets you give each instance different properties. * @@ -429,7 +431,7 @@ if (typeof p5 !== "undefined") { * let itemColor = sharedVec4(); * * worldInputs.begin(); - * let item = data[instanceID()]; + * let item = data[instanceIndex]; * itemColor = item.color; * worldInputs.position += item.position; * worldInputs.end(); @@ -451,8 +453,20 @@ if (typeof p5 !== "undefined") { * This can be paired with `buildComputeShader` * to update the data being read. * - * @webgpu - * @returns {*} The index of the current instance. + * @type {StrandsNode} + */ + +/** + * @method instanceID + * @beta + * @description + * A function alias for `instanceIndex`, kept for compatibility. + * Returns the index of the current instance when drawing multiple copies of a + * shape with `model(count)`. + * + * `instanceID()` can only be used inside a p5.strands shader callback. + * + * @returns {StrandsNode} The index of the current instance. */ /** diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 85af78f3c7..4294958cdb 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -176,6 +176,43 @@ function installBuiltinGlobalAccessors(strandsContext) { strandsContext._builtinGlobalsAccessorsInstalled = true } +function installInstanceIndexAccessor(strandsContext) { + if (strandsContext._instanceIndexAccessorInstalled) return; + + const getRuntimeP5Instance = () => strandsContext.renderer?._pInst || strandsContext.p5?.instance; + + const instanceIndexGetter = function() { + if (strandsContext.active) { + const node = build.variableNode(strandsContext, { baseType: BaseType.INT, dimension: 1 }, strandsContext.backend.instanceIdReference()); + return createStrandsNode(node.id, node.dimension, strandsContext); + } + return undefined; + }; + + const inst = getRuntimeP5Instance(); + if (inst?._isGlobal) { + Object.defineProperty(window, 'instanceIndex', { + get: instanceIndexGetter, + configurable: true, + }); + } + + Object.defineProperty(strandsContext.p5.prototype, 'instanceIndex', { + get: instanceIndexGetter, + configurable: true, + }); + + const GraphicsProto = strandsContext.p5?.Graphics?.prototype; + if (GraphicsProto) { + Object.defineProperty(GraphicsProto, 'instanceIndex', { + get: instanceIndexGetter, + configurable: true, + }); + } + + strandsContext._instanceIndexAccessorInstalled = true; +} + ////////////////////////////////////////////// // Prototype mirroring helpers ////////////////////////////////////////////// @@ -956,6 +993,7 @@ function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName } export function createShaderHooksFunctions(strandsContext, fn, shader) { installBuiltinGlobalAccessors(strandsContext) + installInstanceIndexAccessor(strandsContext) // Add shader context to hooks before spreading const vertexHooksWithContext = Object.fromEntries( diff --git a/test/types/strands.ts b/test/types/strands.ts index c9af27c05a..726fd57de4 100644 --- a/test/types/strands.ts +++ b/test/types/strands.ts @@ -42,6 +42,7 @@ function starShaderCallback() { function semiSphere() { let id = instanceID(); + let idx = instanceIndex(); let theta = rand2([id, 0.1234]) * TWO_PI + time / 100000; let phi = rand2([id, 3.321]) * PI + time / 50000; diff --git a/test/unit/visual/cases/webgl.js b/test/unit/visual/cases/webgl.js index 5d6b19e609..40dd436481 100644 --- a/test/unit/visual/cases/webgl.js +++ b/test/unit/visual/cases/webgl.js @@ -972,7 +972,7 @@ visualSuite('WebGL', function() { const sh = p5.baseMaterialShader().modify(() => { const data = p5.uniformTexture(() => positionData); p5.getWorldInputs((inputs) => { - const angle = p5.getTexture(data, [p5.instanceID()/3, 0]).r * p5.TWO_PI; + const angle = p5.getTexture(data, [p5.instanceIndex/3, 0]).r * p5.TWO_PI; inputs.position.xy += [p5.cos(angle) * 10, p5.sin(angle) * 10]; return inputs; }); @@ -1031,7 +1031,7 @@ visualSuite('WebGL', function() { p5.createCanvas(50, 50, p5.WEBGL); const shader = p5.baseMaterialShader().modify(() => { p5.getWorldInputs((inputs) => { - const id = p5.instanceID(); + const id = p5.instanceIndex; const gridSize = 5; const row = p5.floor(id / gridSize); const col = id - row * gridSize; @@ -1055,7 +1055,7 @@ visualSuite('WebGL', function() { p5.createCanvas(50, 50, p5.WEBGL); const shader = p5.baseMaterialShader().modify(() => { p5.getWorldInputs((inputs) => { - const id = p5.instanceID(); + const id = p5.instanceIndex; const gridSize = 5; const row = p5.int(id / gridSize); const col = id - row * gridSize; @@ -1080,7 +1080,7 @@ visualSuite('WebGL', function() { const shader = p5.baseMaterialShader().modify(() => { // Vertex hook: position instances in a horizontal row p5.getWorldInputs((inputs) => { - const id = p5.instanceID(); + const id = p5.instanceIndex; const spacing = 12; const offset = (id - (numInstances - 1) / 2.0) * spacing; inputs.position.x += offset; @@ -1088,7 +1088,7 @@ visualSuite('WebGL', function() { }); // Fragment hook: color each instance based on instanceID p5.getFinalColor((color) => { - const id = p5.instanceID(); + const id = p5.instanceIndex; const t = id / (numInstances - 1.0); color = [t, t, t, 1]; return color; @@ -1359,7 +1359,7 @@ visualTest('randomGaussian() in a fragment loop averages to the mean', (p5, scre } function semiSphere() { - let id = p5.instanceID(); + let id = p5.instanceIndex; let theta = rand2([id, 0.1234]) * p5.TWO_PI + time / 100000; let phi = rand2([id, 3.321]) * p5.PI + time / 50000; @@ -1377,7 +1377,7 @@ visualTest('randomGaussian() in a fragment loop averages to the mean', (p5, scre }); p5.getObjectInputs((inputs) => { - let size = 1 + 0.5 * p5.sin(time * 0.002 + p5.instanceID()); + let size = 1 + 0.5 * p5.sin(time * 0.002 + p5.instanceIndex); inputs.position *= size; return inputs; }); diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index ee9679f45c..0e84359858 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -120,7 +120,7 @@ visualSuite("WebGPU", function () { const model = p5.buildGeometry(() => p5.sphere(5)); const shader = p5.baseMaterialShader().modify(() => { p5.getWorldInputs((inputs) => { - inputs.position += (p5.instanceID() - 1) * 15 + inputs.position += (p5.instanceIndex - 1) * 15 return inputs; }); }, { p5 }); @@ -144,7 +144,7 @@ visualSuite("WebGPU", function () { } function semiSphere() { - let id = p5.instanceID(); + let id = p5.instanceIndex; let theta = rand2([id, 0.1234]) * p5.TWO_PI + time / 100000; let phi = rand2([id, 3.321]) * p5.PI + time / 50000; @@ -162,7 +162,7 @@ visualSuite("WebGPU", function () { }); p5.getObjectInputs((inputs) => { - let size = 1 + 0.5 * p5.sin(time * 0.002 + p5.instanceID()); + let size = 1 + 0.5 * p5.sin(time * 0.002 + p5.instanceIndex); inputs.position *= size; return inputs; }); @@ -304,7 +304,7 @@ visualSuite("WebGPU", function () { const shader = p5.baseMaterialShader().modify(() => { // Vertex hook: position instances in a horizontal row p5.getWorldInputs((inputs) => { - const id = p5.instanceID(); + const id = p5.instanceIndex; const spacing = 12; const offset = (id - (numInstances - 1) / 2.0) * spacing; inputs.position.x += offset; @@ -312,7 +312,7 @@ visualSuite("WebGPU", function () { }); // Fragment hook: color each instance based on instanceID p5.getFinalColor((color) => { - const id = p5.instanceID(); + const id = p5.instanceIndex; const t = id / (numInstances - 1.0); color = [t, t, t, 1]; return color; @@ -1177,7 +1177,7 @@ visualTest('randomGaussian() in a fragment loop averages to the mean (WebGPU)', const sphereShader = p5.baseMaterialShader().modify(() => { const posData = p5.uniformStorage(); p5.getWorldInputs((inputs) => { - const idx = p5.instanceID(); + const idx = p5.instanceIndex; inputs.position.x += posData[idx * 2]; inputs.position.y += posData[idx * 2 + 1]; return inputs; @@ -1285,7 +1285,7 @@ visualTest('randomGaussian() in a fragment loop averages to the mean (WebGPU)', const sphereShader = p5.baseMaterialShader().modify(() => { const buf = p5.uniformStorage('buf', particles); p5.getWorldInputs((inputs) => { - const p = buf[p5.instanceID()].position; + const p = buf[p5.instanceIndex].position; inputs.position.x += p.x; inputs.position.y += p.y; return inputs; @@ -1318,7 +1318,7 @@ visualTest('randomGaussian() in a fragment loop averages to the mean (WebGPU)', const sphereShader = p5.baseMaterialShader().modify(() => { const buf = p5.uniformStorage('buf', particles); p5.getWorldInputs((inputs) => { - const p = buf[p5.instanceID()].position; + const p = buf[p5.instanceIndex].position; inputs.position.x += p.x; inputs.position.y += p.y; return inputs; @@ -1351,7 +1351,7 @@ visualTest('randomGaussian() in a fragment loop averages to the mean (WebGPU)', const sphereShader = p5.baseMaterialShader().modify(() => { const buf = p5.uniformStorage('buf', { position: [0, 0] }); p5.getWorldInputs((inputs) => { - const p = buf[p5.instanceID()].position; + const p = buf[p5.instanceIndex].position; inputs.position.x += p.x; inputs.position.y += p.y; return inputs; diff --git a/test/unit/webgl/p5.Shader.js b/test/unit/webgl/p5.Shader.js index 6434642c1d..b5f97cb6bc 100644 --- a/test/unit/webgl/p5.Shader.js +++ b/test/unit/webgl/p5.Shader.js @@ -532,6 +532,22 @@ test('returns numbers for builtin globals outside hooks and a strandNode when ca assert.strictEqual(w, myp5.width); }); +test('instanceIndex is a value and instanceID() is a compatibility alias', () => { + myp5.createCanvas(5, 5, myp5.WEBGL); + myp5.baseMaterialShader().modify(() => { + myp5.getWorldInputs(inputs => { + // instanceIndex is a property — no parentheses + const idx = myp5.instanceIndex; + assert.isTrue(idx.isStrandsNode); + + // instanceID() is a function kept for compatibility + const idxCompat = myp5.instanceID(); + assert.isTrue(idxCompat.isStrandsNode); + + return inputs; + }); + }, { myp5 }); +}); test('map() works inside a strands modify callback', () => { myp5.createCanvas(50, 50, myp5.WEBGL); const testShader = myp5.baseMaterialShader().modify(() => {