Skip to content
Merged
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
36 changes: 34 additions & 2 deletions src/strands/ir_builders.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function unaryOpNode(strandsContext, nodeOrValue, opCode) {
const { dag, cfg } = strandsContext;
let dependsOn;
let node;
if (nodeOrValue instanceof StrandsNode) {
if (nodeOrValue?.isStrandsNode) {
node = nodeOrValue;
} else {
const { id, dimension } = primitiveConstructorNode(strandsContext, { baseType: BaseType.FLOAT, dimension: null }, nodeOrValue);
Expand Down Expand Up @@ -257,6 +257,20 @@ export function constructTypeFromIDs(strandsContext, typeInfo, strandsNodesArray

export function primitiveConstructorNode(strandsContext, typeInfo, dependsOn) {
const cfg = strandsContext.cfg;
dependsOn = (Array.isArray(dependsOn) ? dependsOn : [dependsOn])
.flat(Infinity)
.map(a => {
if (
a.isStrandsNode &&
a.typeInfo().baseType === BaseType.INT &&
// TODO: handle ivec inputs instead of just int scalars
a.typeInfo().dimension === 1
) {
return castToFloat(strandsContext, a);
} else {
return a;
}
});
const { mappedDependencies, inferredTypeInfo } = mapPrimitiveDepsToIDs(strandsContext, typeInfo, dependsOn);

const finalType = {
Expand All @@ -272,6 +286,24 @@ export function primitiveConstructorNode(strandsContext, typeInfo, dependsOn) {
return { id, dimension: finalType.dimension, components: mappedDependencies };
}

export function castToFloat(strandsContext, dep) {
const { id, dimension } = functionCallNode(
strandsContext,
strandsContext.backend.getTypeName('float', dep.typeInfo().dimension),
[dep],
{
overloads: [{
params: [dep.typeInfo()],
returnType: {
...dep.typeInfo(),
baseType: BaseType.FLOAT,
},
}],
}
);
return createStrandsNode(id, dimension, strandsContext);
}

export function structConstructorNode(strandsContext, structTypeInfo, rawUserArgs) {
const { cfg, dag } = strandsContext;
const { identifer, properties } = structTypeInfo;
Expand Down Expand Up @@ -491,7 +523,7 @@ export function swizzleTrap(id, dimension, strandsContext, onRebind) {
// This may not be the most efficient way, as we swizzle each component individually,
// so that .xyz becomes .x, .y, .z
let scalars = [];
if (value instanceof StrandsNode) {
if (value?.isStrandsNode) {
if (value.dimension === 1) {
scalars = Array(chars.length).fill(value);
} else if (value.dimension === chars.length) {
Expand Down
27 changes: 16 additions & 11 deletions src/strands/strands_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ function _getBuiltinGlobalsCache(strandsContext) {
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)
Expand Down Expand Up @@ -154,7 +154,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
}

// Convert value to a StrandsNode if it isn't already
const valueNode = value instanceof StrandsNode ? value : p5.strandsNode(value);
const valueNode = value?.isStrandsNode ? value : p5.strandsNode(value);

// Create a new CFG block for the early return
const earlyReturnBlockID = CFG.createBasicBlock(cfg, BlockType.DEFAULT);
Expand Down Expand Up @@ -369,12 +369,17 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) {
fn[typeInfo.fnName] = function(...args) {
if (strandsContext.active) {
if (args.length === 1 && args[0].dimension && args[0].dimension === typeInfo.dimension) {
const { id, dimension } = build.functionCallNode(strandsContext, typeInfo.fnName, args, {
overloads: [{
params: [args[0].typeInfo()],
returnType: typeInfo,
}]
});
const { id, dimension } = build.functionCallNode(
strandsContext,
strandsContext.backend.getTypeName(typeInfo.baseType, typeInfo.dimension),
args,
{
overloads: [{
params: [args[0].typeInfo()],
returnType: typeInfo,
}]
}
);
return createStrandsNode(id, dimension, strandsContext);
} else {
// For vector types with a single argument, repeat it for each component
Expand Down Expand Up @@ -431,7 +436,7 @@ function createHookArguments(strandsContext, parameters){
const oldDependsOn = dag.dependsOn[structNode.id];
const newDependsOn = [...oldDependsOn];
let newValueID;
if (val instanceof StrandsNode) {
if (val?.isStrandsNode) {
newValueID = val.id;
}
else {
Expand Down Expand Up @@ -463,7 +468,7 @@ function createHookArguments(strandsContext, parameters){
return args;
}
function enforceReturnTypeMatch(strandsContext, expectedType, returned, hookName) {
if (!(returned instanceof StrandsNode)) {
if (!(returned?.isStrandsNode)) {
// try {
const result = build.primitiveConstructorNode(strandsContext, expectedType, returned);
return result.id;
Expand Down Expand Up @@ -578,7 +583,7 @@ export function createShaderHooksFunctions(strandsContext, fn, shader) {
const handleRetVal = (retNode) => {
if(isStructType(expectedReturnType)) {
const expectedStructType = structType(expectedReturnType);
if (retNode instanceof StrandsNode) {
if (retNode?.isStrandsNode) {
const returnedNode = getNodeDataFromID(strandsContext.dag, retNode.id);
if (returnedNode.baseType !== expectedStructType.typeName) {
const receivedTypeName = returnedNode.baseType || 'undefined';
Expand Down
2 changes: 1 addition & 1 deletion src/strands/strands_for.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ export class StrandsFor {
let initialVar = this.initialCb();

// Convert to StrandsNode if it's not already one
if (!(initialVar instanceof StrandsNode)) {
if (!(initialVar?.isStrandsNode)) {
const { id, dimension } = primitiveConstructorNode(this.strandsContext, { baseType: BaseType.FLOAT, dimension: 1 }, initialVar);
initialVar = createStrandsNode(id, dimension, this.strandsContext);
}
Expand Down
4 changes: 2 additions & 2 deletions src/strands/strands_node.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export class StrandsNode {
const baseType = orig?.baseType ?? BaseType.FLOAT;

let newValueID;
if (value instanceof StrandsNode) {
if (value?.isStrandsNode) {
newValueID = value.id;
} else {
const newVal = primitiveConstructorNode(
Expand Down Expand Up @@ -95,7 +95,7 @@ export class StrandsNode {
const baseType = orig?.baseType ?? BaseType.FLOAT;

let newValueID;
if (value instanceof StrandsNode) {
if (value?.isStrandsNode) {
newValueID = value.id;
} else {
const newVal = primitiveConstructorNode(
Expand Down
5 changes: 4 additions & 1 deletion src/webgpu/p5.RendererWebGPU.js
Original file line number Diff line number Diff line change
Expand Up @@ -1904,7 +1904,7 @@ function rendererWebGPU(p5, fn) {

getNextBindingIndex({ vert, frag }, group = 0) {
// Get the highest binding index in the specified group and return the next available
const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d<f32>|sampler|uniform)/g;
const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var(?:<uniform>)?\s+(\w+)\s*:\s*(texture_2d<f32>|sampler|uniform|\w+)/g;
let maxBindingIndex = -1;

for (const [src, visibility] of [
Expand Down Expand Up @@ -2254,6 +2254,9 @@ function rendererWebGPU(p5, fn) {
// Inject hook uniforms as a separate struct at a new binding
let hookUniformFields = '';
for (const key in shader.hooks.uniforms) {
// Skip textures, they don't get added to structs
if (key.endsWith(': sampler2D')) continue;

// WGSL format: "name: type"
hookUniformFields += ` ${key},\n`;
}
Expand Down
7 changes: 4 additions & 3 deletions src/webgpu/strands_wgslBackend.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,11 @@ export const wgslBackend = {
return primitiveTypeName;
},
generateHookUniformKey(name, typeInfo) {
// For sampler2D types, we don't add them to the uniform struct
// Instead, they become separate texture and sampler bindings
// For sampler2D types, we don't add them to the uniform struct,
// but we still need them in the shader's hooks object so that
// they can be set by users.
if (typeInfo.baseType === 'sampler2D') {
return null; // Signal that this should not be added to uniform struct
return `${name}: sampler2D`; // Signal that this should not be added to uniform struct
}
return `${name}: ${this.getTypeName(typeInfo.baseType, typeInfo.dimension)}`;
},
Expand Down
126 changes: 124 additions & 2 deletions test/unit/visual/cases/webgl.js
Original file line number Diff line number Diff line change
Expand Up @@ -1144,6 +1144,128 @@ visualSuite('WebGL', function() {
screenshot();
});
});

visualTest('Strands tutorial', function(p5, screenshot) {
// From Luke Plowden's Intro to Strands tutorial
// https://beta.p5js.org/tutorials/intro-to-p5-strands/

function starShaderCallback({ p5 }) {
const time = p5.uniformFloat(() => p5.millis());
const skyRadius = p5.uniformFloat(90);

function rand2(st) {
return p5.sin((st.x + st.y) * 123.456);
}

function semiSphere() {
let id = p5.instanceID();
let theta = rand2([id, 0.1234]) * p5.TWO_PI + time / 100000;
let phi = rand2([id, 3.321]) * p5.PI + time / 50000;

let r = skyRadius;
r *= p5.sin(phi);
let x = r * p5.sin(phi) * p5.cos(theta);
let y = r * 1.5 * p5.cos(phi);
let z = r * p5.sin(phi) * p5.sin(theta);
return [x, y, z];
}

p5.getWorldInputs((inputs) => {
inputs.position += semiSphere();
return inputs;
});

p5.getObjectInputs((inputs) => {
let size = 1 + 0.5 * p5.sin(time * 0.002 + p5.instanceID());
inputs.position *= size;
return inputs;
});
}

function pixelateShaderCallback({ p5 }) {
const pixelCountX = p5.uniformFloat(() => 100);

p5.getColor((inputs, canvasContent) => {
const aspectRatio = inputs.canvasSize.x / inputs.canvasSize.y;
const pixelSize = [pixelCountX, pixelCountX / aspectRatio];

let coord = inputs.texCoord;
coord = p5.floor(coord * pixelSize) / pixelSize;

let col = p5.getTexture(canvasContent, coord);
return col//[coord, 0, 1];
});
}

function bloomShaderCallback({ p5, originalImage }) {
const preBlur = p5.uniformTexture(() => originalImage);

getColor((input, canvasContent) => {
const blurredCol = p5.getTexture(canvasContent, input.texCoord);
const originalCol = p5.getTexture(preBlur, input.texCoord);

const intensity = p5.max(originalCol, 0.1) * 12.2;

const bloom = originalCol + blurredCol * intensity;
return [bloom.rgb, 1];
});
}

p5.createCanvas(200, 200, p5.WEBGL);
const stars = p5.buildGeometry(() => p5.sphere(4, 4, 2))
const originalImage = p5.createFramebuffer();

function fresnelShaderCallback({ p5 }) {
const fresnelPower = p5.uniformFloat(2);
const fresnelBias = p5.uniformFloat(-0.1);
const fresnelScale = p5.uniformFloat(2);

p5.getCameraInputs((inputs) => {
let n = p5.normalize(inputs.normal);
let v = p5.normalize(-inputs.position);
let base = 1.0 - p5.dot(n, v);
let fresnel = fresnelScale * p5.pow(base, fresnelPower) + fresnelBias;
let col = p5.mix([0, 0, 0], [1, .5, .7], fresnel);
inputs.color = [col, 1];
return inputs;
});
}

const starShader = p5.baseMaterialShader().modify(starShaderCallback, { p5 });
const starStrokeShader = p5.baseStrokeShader().modify(starShaderCallback, { p5 })
const fresnelShader = p5.baseColorShader().modify(fresnelShaderCallback, { p5 });
const bloomShader = p5.baseFilterShader().modify(bloomShaderCallback, { p5, originalImage });
const pixelateShader = p5.baseFilterShader().modify(pixelateShaderCallback, { p5 });

originalImage.begin();
p5.background(0);

p5.push()
p5.strokeWeight(2)
p5.stroke(255,0,0)
p5.fill(255,100, 150)
p5.strokeShader(starStrokeShader)
p5.shader(starShader);
p5.model(stars, 100);
p5.pop()

p5.push()
p5.shader(fresnelShader)
p5.noStroke()
p5.sphere(30);
p5.filter(pixelateShader);
p5.pop()

originalImage.end();

p5.imageMode(p5.CENTER)
p5.image(originalImage, 0, 0)

p5.filter(p5.BLUR, 5)
p5.filter(bloomShader);

screenshot();
});
});

visualSuite('background()', function () {
Expand Down Expand Up @@ -1316,7 +1438,7 @@ visualSuite('WebGL', function() {
visualSuite('Tessellation', function() {
visualTest('Handles nearly identical consecutive vertices', function(p5, screenshot) {
p5.createCanvas(400, 400, p5.WEBGL);

const contours = [
[
[-3.8642425537109375, -6.120738636363637, 0],
Expand Down Expand Up @@ -1355,7 +1477,7 @@ visualSuite('WebGL', function() {
[-1.8045834628018462, 4.177556818181818, 0]
]
];

p5.background('red');
p5.push();
p5.stroke(0);
Expand Down
Loading
Loading