diff --git a/preview/index.html b/preview/index.html index db3963b211..3584e5e251 100644 --- a/preview/index.html +++ b/preview/index.html @@ -24,12 +24,13 @@ const sketch = function (p) { let fbo; - let sh; + let sh, sh2; let ssh; let tex; let font; let redFilter; let env; + let instance; p.setup = async function () { await p.createCanvas(400, 400, p.WEBGPU); @@ -39,6 +40,8 @@ ); fbo = p.createFramebuffer(); + instance = p.buildGeometry(() => p.sphere(5)); + redFilter = p.baseFilterShader().modify(() => { p.getColor((inputs, canvasContent) => { let col = p.getTexture(canvasContent, inputs.texCoord); @@ -84,6 +87,12 @@ return inputs; }); }, { p }) + sh2 = p.baseMaterialShader().modify(() => { + p.getWorldInputs((inputs) => { + inputs.position.x += 20 * p.instanceID(); + return inputs; + }); + }, { p }) /*ssh = p.baseStrokeShader().modify({ uniforms: { 'f32 time': () => p.millis(), @@ -151,6 +160,13 @@ } p.pop(); + p.push(); + p.shader(sh2); + p.noStroke(); + p.fill('red'); + p.model(instance, 10); + p.pop(); + // Test beginShape/endShape with immediate mode shapes p.push(); p.translate(0, 100, 0); diff --git a/src/strands/strands_api.js b/src/strands/strands_api.js index 7ffc6e9a2b..05ae0ec5ea 100644 --- a/src/strands/strands_api.js +++ b/src/strands/strands_api.js @@ -50,7 +50,7 @@ export function initGlobalStrandsAPI(p5, fn, strandsContext) { }; p5.break = fn.break; fn.instanceID = function() { - const node = build.variableNode(strandsContext, { baseType: BaseType.INT, dimension: 1 }, 'gl_InstanceID'); + const node = build.variableNode(strandsContext, { baseType: BaseType.INT, dimension: 1 }, strandsContext.backend.instanceIdReference()); return createStrandsNode(node.id, node.dimension, strandsContext); } // Internal methods use p5 static methods; user-facing methods use fn. diff --git a/src/webgl/strands_glslBackend.js b/src/webgl/strands_glslBackend.js index 42f4f60fb3..ebb2c5a263 100644 --- a/src/webgl/strands_glslBackend.js +++ b/src/webgl/strands_glslBackend.js @@ -379,5 +379,9 @@ export const glslBackend = { }] }); return { id, dimension }; - } + }, + + instanceIdReference() { + return 'gl_InstanceID'; + }, } diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 5dda796bdd..5445a0aa80 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -289,7 +289,7 @@ function rendererWebGPU(p5, fn) { // Check if we already have a buffer for this data let existingBuffer = buffers[dst]; const needsNewBuffer = !existingBuffer; - + // Only create new buffer and write data if buffer doesn't exist or data is dirty if (needsNewBuffer || geometry.dirtyFlags[src] !== false) { const raw = map ? map(srcData) : srcData; @@ -1349,7 +1349,7 @@ function rendererWebGPU(p5, fn) { } _getShaderAttributes(shader) { - const mainMatch = /fn main\(.+:\s*(\S+)\s*\)/.exec(shader._vertSrc); + const mainMatch = /fn main\(.+:\s*([^\s\)]+)/.exec(shader._vertSrc); if (!mainMatch) throw new Error("Can't find `fn main` in vertex shader source"); const inputType = mainMatch[1]; @@ -1740,7 +1740,12 @@ function rendererWebGPU(p5, fn) { } ); - let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment)\s*)?fn main)/); + let [preMain, main, postMain] = src.split(/((?:@(?:vertex|fragment)\s*)?fn main[^{]+\{)/); + if (shaderType !== 'fragment') { + if (!main.match(/\@builtin\s*\(\s*instance_index\s*\)/)) { + main = main.replace(/\)\s*(->|\{)/, ', @builtin(instance_index) instanceID: u32) $1'); + } + } let uniforms = ''; for (const key in shader.hooks.uniforms) { @@ -1850,7 +1855,14 @@ function rendererWebGPU(p5, fn) { shader.hooks.modified[shaderType][hookDef] ? 'true' : 'false' };\n`; - const [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]); + let [_, params, body] = /^(\([^\)]*\))((?:.|\n)*)$/.exec(shader.hooks[shaderType][hookDef]); + + if (shaderType !== 'fragment') { + // 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)'; + } + if (hookType === 'void') { hooks += `fn HOOK_${hookName}${params}${body}\n`; } else { @@ -1858,6 +1870,42 @@ function rendererWebGPU(p5, fn) { } } + // Add the instance ID as a final parameter to each hook call + if (shaderType !== 'fragment') { + 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; + } + } + const insertion = (hasParams ? ', ' : '') + 'instanceID'; + result = result.slice(0, idx-1) + insertion + result.slice(idx-1); + idx += insertion.length; + } + } while (match); + return result; + }; + preMain = addInstanceIDParam(preMain); + postMain = addInstanceIDParam(postMain); + } + return preMain + '\n' + defines + hooks + main + postMain; } diff --git a/src/webgpu/shaders/color.js b/src/webgpu/shaders/color.js index 99a5c12b3b..dae7ab4e8d 100644 --- a/src/webgpu/shaders/color.js +++ b/src/webgpu/shaders/color.js @@ -46,7 +46,7 @@ fn main(input: VertexInput) -> VertexOutput { HOOK_beforeVertex(); var output: VertexOutput; - let useVertexColor = (uniforms.uUseVertexColor != 0); + let useVertexColor = (uniforms.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0); var inputs = Vertex( input.aPosition, input.aNormal, diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js index 49b601dc7d..aec9a1d292 100644 --- a/src/webgpu/shaders/material.js +++ b/src/webgpu/shaders/material.js @@ -87,7 +87,7 @@ fn main(input: VertexInput) -> VertexOutput { HOOK_beforeVertex(); var output: VertexOutput; - let useVertexColor = (uniforms.uUseVertexColor != 0); + let useVertexColor = (uniforms.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0); var inputs = Vertex( input.aPosition, input.aNormal, diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index c56820bf2a..95bdd66cf3 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -503,5 +503,9 @@ export const wgslBackend = { }] }); return { id, dimension }; - } + }, + + instanceIdReference() { + return 'instanceID'; + }, } diff --git a/test/unit/visual/cases/webgpu.js b/test/unit/visual/cases/webgpu.js index 0a16815a64..45c793a322 100644 --- a/test/unit/visual/cases/webgpu.js +++ b/test/unit/visual/cases/webgpu.js @@ -114,6 +114,22 @@ visualSuite("WebGPU", function () { await screenshot(); }, ); + + visualTest('Instanced rendering', async function(p5, screenshot) { + await p5.createCanvas(50, 50, p5.WEBGPU); + const model = p5.buildGeometry(() => p5.sphere(5)); + const shader = p5.baseMaterialShader().modify(() => { + p5.getWorldInputs((inputs) => { + inputs.position += (p5.instanceID() - 1) * 15 + return inputs; + }); + }, { p5 }); + p5.noStroke(); + p5.fill(0); + p5.shader(shader); + p5.model(model, 3); + await screenshot(); + }); }); visualSuite('filters', function() { diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/Instanced rendering/000.png b/test/unit/visual/screenshots/WebGPU/Shaders/Instanced rendering/000.png new file mode 100644 index 0000000000..ff804f8d7c Binary files /dev/null and b/test/unit/visual/screenshots/WebGPU/Shaders/Instanced rendering/000.png differ diff --git a/test/unit/visual/screenshots/WebGPU/Shaders/Instanced rendering/metadata.json b/test/unit/visual/screenshots/WebGPU/Shaders/Instanced rendering/metadata.json new file mode 100644 index 0000000000..2d4bfe30da --- /dev/null +++ b/test/unit/visual/screenshots/WebGPU/Shaders/Instanced rendering/metadata.json @@ -0,0 +1,3 @@ +{ + "numScreenshots": 1 +} \ No newline at end of file