Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions src/strands/p5.strands.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <a href="#/p5/model">`model(count)`</a>. 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;
Expand All @@ -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();
* }
*
Expand All @@ -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
* <a href="#/p5/createStorage">`createStorage()`</a>.
* This lets you give each instance different properties.
*
Expand Down Expand Up @@ -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();
Expand All @@ -451,8 +453,20 @@ if (typeof p5 !== "undefined") {
* This can be paired with <a href="#/p5/buildComputeShader">`buildComputeShader`</a>
* to update the data being read.
*
* @webgpu
* @returns {*} The index of the current instance.
* @type {StrandsNode}
*/

/**
* @method instanceID
* @beta
* @description
* A function alias for <a href="#/p5/instanceIndex">`instanceIndex`</a>, kept for compatibility.
* Returns the index of the current instance when drawing multiple copies of a
* shape with <a href="#/p5/model">`model(count)`</a>.
*
* `instanceID()` can only be used inside a p5.strands shader callback.
*
* @returns {StrandsNode} The index of the current instance.
*/

/**
Expand Down
38 changes: 38 additions & 0 deletions src/strands/strands_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
//////////////////////////////////////////////
Expand Down Expand Up @@ -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(
Expand Down
1 change: 1 addition & 0 deletions test/types/strands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
14 changes: 7 additions & 7 deletions test/unit/visual/cases/webgl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -1080,15 +1080,15 @@ 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;
return inputs;
});
// 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;
Expand Down Expand Up @@ -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;

Expand All @@ -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;
});
Expand Down
18 changes: 9 additions & 9 deletions test/unit/visual/cases/webgpu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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;

Expand All @@ -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;
});
Expand Down Expand Up @@ -304,15 +304,15 @@ 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;
return inputs;
});
// 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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
16 changes: 16 additions & 0 deletions test/unit/webgl/p5.Shader.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down