diff --git a/src/core/p5.Renderer3D.js b/src/core/p5.Renderer3D.js index 4799a5cc1b..4241a56157 100644 --- a/src/core/p5.Renderer3D.js +++ b/src/core/p5.Renderer3D.js @@ -1448,10 +1448,6 @@ export class Renderer3D extends Renderer { this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix); shader.setUniform("uCameraNormalMatrix", this.scratchMat3.mat3); } - if (shader.uniforms.uCameraRotation) { - this.scratchMat3.inverseTranspose4x4(this.states.uViewMatrix); - shader.setUniform("uCameraRotation", this.scratchMat3.mat3); - } shader.setUniform("uViewport", this._viewport); } diff --git a/src/webgl/p5.Shader.js b/src/webgl/p5.Shader.js index 1e835fe95c..42d3df0c89 100644 --- a/src/webgl/p5.Shader.js +++ b/src/webgl/p5.Shader.js @@ -1028,8 +1028,12 @@ class Shader { return; } else { if (Array.isArray(data)) { + if (uniform._cachedData && this._renderer._arraysEqual(uniform._cachedData, data)) { + return; + } uniform._cachedData = data.slice(0); } else { + if (uniform._cachedData === data) return; uniform._cachedData = data; } } diff --git a/src/webgl/shaders/lighting.glsl b/src/webgl/shaders/lighting.glsl index 85a4c79684..43343d07b4 100644 --- a/src/webgl/shaders/lighting.glsl +++ b/src/webgl/shaders/lighting.glsl @@ -4,10 +4,10 @@ precision highp float; precision highp int; uniform mat4 uViewMatrix; +uniform mat3 uCameraNormalMatrix; uniform bool uUseLighting; -uniform mat3 uCameraRotation; uniform int uDirectionalLightCount; uniform vec3 uLightingDirection[5]; uniform vec3 uDirectionalDiffuseColors[5]; @@ -108,7 +108,7 @@ vec2 mapTextureToNormal( vec3 v ){ vec3 calculateImageDiffuse(vec3 vNormal, vec3 vViewPosition, float metallic){ // make 2 seperate builds vec3 worldCameraPosition = vec3(0.0, 0.0, 0.0); // hardcoded world camera position - vec3 worldNormal = normalize(vNormal * uCameraRotation); + vec3 worldNormal = normalize(vNormal * uCameraNormalMatrix); vec2 newTexCoor = mapTextureToNormal( worldNormal ); vec4 texture = TEXTURE( environmentMapDiffused, newTexCoor ); // this is to make the darker sections more dark @@ -120,7 +120,7 @@ vec3 calculateImageSpecular(vec3 vNormal, vec3 vViewPosition, float shininess, f vec3 worldCameraPosition = vec3(0.0, 0.0, 0.0); vec3 worldNormal = normalize(vNormal); vec3 lightDirection = normalize( vViewPosition - worldCameraPosition ); - vec3 R = reflect(lightDirection, worldNormal) * uCameraRotation; + vec3 R = reflect(lightDirection, worldNormal) * uCameraNormalMatrix; vec2 newTexCoor = mapTextureToNormal( R ); #ifdef WEBGL2 // In p5js the range of shininess is >= 1, diff --git a/src/webgpu/p5.RendererWebGPU.js b/src/webgpu/p5.RendererWebGPU.js index 46db2b3925..389afe9b27 100644 --- a/src/webgpu/p5.RendererWebGPU.js +++ b/src/webgpu/p5.RendererWebGPU.js @@ -37,10 +37,21 @@ function rendererWebGPU(p5, fn) { constructor(pInst, w, h, isMainCanvas, elt) { super(pInst, w, h, isMainCanvas, elt) - this.renderPass = {}; + // Used to group draws into one big render pass + this.activeRenderPass = null; + this.activeRenderPassEncoder = null; this.samplers = new Map(); + // Some uniforms update every frame, like model matrices and sometimes colors. + // The fastest way to handle these is to use mapped memory. We'll batch those + // into bigger buffers with dynamic offsets, separate from the usual system + // where bind groups have their own little buffers that get cached when they + // are unchanged + this.uniformBufferAlignment = 256; + this.activeUniformBuffers = []; + this.currentUniformBuffer = undefined; + // Cache for current frame's canvas texture view this.currentCanvasColorTexture = null; this.currentCanvasColorTextureView = null; @@ -57,6 +68,9 @@ function rendererWebGPU(p5, fn) { // Registry to track geometries with buffer pools this._geometriesWithPools = []; + // Reusable Map for uniform buffer bindings to avoid GC + this._uniformBuffersForBinding = new Map(); + // Flag to track if any draws have happened that need queue submission this._hasPendingDraws = false; this._pendingCommandEncoders = []; @@ -204,6 +218,66 @@ function rendererWebGPU(p5, fn) { return this.currentCanvasColorTextureView; } + _beginActiveRenderPass() { + if (this.activeRenderPass) return; + + // Use framebuffer texture if active, otherwise use canvas texture + const activeFramebuffer = this.activeFramebuffer(); + + const colorAttachment = { + view: activeFramebuffer + ? (activeFramebuffer.aaColorTexture + ? activeFramebuffer.aaColorTextureView + : activeFramebuffer.colorTextureView) + : this._getCanvasColorTextureView(), + loadOp: "load", + storeOp: "store", + // If using multisampled texture, resolve to non-multisampled texture + resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture + ? activeFramebuffer.colorTextureView + : undefined, + }; + + // Use framebuffer depth texture if active, otherwise use canvas depth texture + const depthTextureView = activeFramebuffer + ? (activeFramebuffer.aaDepthTexture + ? activeFramebuffer.aaDepthTextureView + : activeFramebuffer.depthTextureView) + : this.depthTextureView; + const renderPassDescriptor = { + colorAttachments: [colorAttachment], + depthStencilAttachment: depthTextureView + ? { + view: depthTextureView, + depthLoadOp: "load", + depthStoreOp: "store", + depthClearValue: 1.0, + stencilLoadOp: "load", + stencilStoreOp: "store", + depthReadOnly: false, + stencilReadOnly: false, + } + : undefined, + }; + const commandEncoder = this.device.createCommandEncoder(); + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + this.activeRenderPassEncoder = commandEncoder; + this.activeRenderPass = passEncoder; + } + + _finishActiveRenderPass() { + if (!this.activeRenderPass) return; + + const commandEncoder = this.activeRenderPassEncoder; + const passEncoder = this.activeRenderPass; + passEncoder.end(); + + // Store the command encoder for later submission + this._pendingCommandEncoders.push(commandEncoder.finish()); + this.activeRenderPassEncoder = null; + this.activeRenderPass = null; + } + clear(...args) { const _r = args[0] || 0; const _g = args[1] || 0; @@ -215,6 +289,8 @@ function rendererWebGPU(p5, fn) { this._frameState = FRAME_STATE.UNPROMOTED; } + this._finishActiveRenderPass(); + const commandEncoder = this.device.createCommandEncoder(); // Use framebuffer texture if active, otherwise use canvas texture @@ -269,6 +345,7 @@ function rendererWebGPU(p5, fn) { * occlude anything subsequently drawn. */ clearDepth(depth = 1) { + this._finishActiveRenderPass(); const commandEncoder = this.device.createCommandEncoder(); // Use framebuffer texture if active, otherwise use canvas texture @@ -359,7 +436,6 @@ function rendererWebGPU(p5, fn) { const loc = attr.location; if (!this.registerEnabled.has(loc)) { // TODO - // this.renderPass.setVertexBuffer(loc, buffer); this.registerEnabled.add(loc); } } @@ -496,39 +572,37 @@ function rendererWebGPU(p5, fn) { } _finalizeShader(shader) { - const rawSize = Math.max( - 0, - ...Object.values(shader.uniforms).filter(u => !u.isSampler).map(u => u.offsetEnd) - ); - const alignedSize = Math.ceil(rawSize / 16) * 16; - shader._uniformData = new Float32Array(alignedSize / 4); - shader._uniformDataView = new DataView(shader._uniformData.buffer); - - // Create pools for uniform buffers (both GPU buffers and data arrays.) This - // is so that we can queue up multiple things to be able to be drawn and have - // the GPU go through them as fast as possible. If we're overwriting the same - // data again and again, we would have to wait for the GPU after each primitive - // that we draw. - shader._uniformBufferPool = []; - shader._uniformBuffersInUse = []; - shader._uniformBufferSize = alignedSize; - - // Create the first buffer for the pool - const firstGPUBuffer = this.device.createBuffer({ - size: alignedSize, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, - }); - const firstData = new Float32Array(alignedSize / 4); - const firstDataView = new DataView(firstData.buffer); - - shader._uniformBufferPool.push({ - buffer: firstGPUBuffer, - data: firstData, - dataView: firstDataView - }); - - // Keep backward compatibility reference - shader._uniformBuffer = firstGPUBuffer; + // Per-group buffer pools. We will pull from these when we draw multiple + // times using the shader in a render pass. These are per group instead of + // global so that we can reuse the last used buffer when uniform values + // don't change. + shader._uniformBufferGroups = []; + + for (const group of shader._uniformGroups) { + // Calculate the size needed for this group's uniforms + const groupUniforms = Object.values(group.uniforms); + const rawSize = Math.max( + 0, + ...groupUniforms.map(u => u.offsetEnd) + ); + const alignedSize = Math.ceil(rawSize / 16) * 16; + + shader._uniformBufferGroups.push({ + group: group.group, + binding: group.binding, + varName: group.varName, + structType: group.structType, + uniforms: groupUniforms, + size: alignedSize, + + bufferPool: [], + nextBufferPool: [], + + dynamic: groupUniforms.some(u => u.name.startsWith('uModel')), + buffersInUse: new Set(), + currentBuffer: null, // For caching + }); + } // Register this shader in our registry for pool cleanup this._shadersWithPools.push(shader); @@ -536,12 +610,18 @@ function rendererWebGPU(p5, fn) { const bindGroupLayouts = new Map(); // group index -> bindGroupLayout const groupEntries = new Map(); // group index -> array of entries - // We're enforcing that every shader have a single uniform struct in binding 0 - groupEntries.set(0, [{ - binding: 0, - visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, - buffer: { type: 'uniform' }, - }]); + // Add all uniform group bindings to group 0 + const group0Entries = []; + for (const bufferGroup of shader._uniformBufferGroups) { + group0Entries.push({ + bufferGroup, + binding: bufferGroup.binding, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + buffer: { type: 'uniform', hasDynamicOffset: bufferGroup.dynamic }, + }); + } + group0Entries.sort((a, b) => a.binding - b.binding); + groupEntries.set(0, group0Entries); // Add the variable amount of samplers and texture bindings that can come after for (const sampler of shader.samplers) { @@ -563,6 +643,7 @@ function rendererWebGPU(p5, fn) { uniform: sampler, }); + entries.sort((a, b) => a.binding - b.binding); groupEntries.set(group, entries); } @@ -819,6 +900,7 @@ function rendererWebGPU(p5, fn) { } _resetBuffersBeforeDraw() { + this._finishActiveRenderPass(); // Set state to PENDING - we'll decide on first draw this._frameState = FRAME_STATE.PENDING; @@ -1072,50 +1154,95 @@ function rendererWebGPU(p5, fn) { // Uniform buffer pool management ////////////////////////////////////////////// - _getUniformBufferFromPool(shader) { + _getUniformBufferFromPool(bufferGroup) { // Try to get a buffer from the pool - if (shader._uniformBufferPool.length > 0) { - const bufferInfo = shader._uniformBufferPool.pop(); - shader._uniformBuffersInUse.push(bufferInfo); + if (bufferGroup.bufferPool.length > 0) { + const bufferInfo = bufferGroup.bufferPool.pop(); + bufferGroup.buffersInUse.add(bufferInfo); return bufferInfo; } // No buffers available, create a new one const newBuffer = this.device.createBuffer({ - size: shader._uniformBufferSize, + size: bufferGroup.size, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, }); - const newData = new Float32Array(shader._uniformBufferSize / 4); + const newData = new Float32Array(bufferGroup.size / 4); const newDataView = new DataView(newData.buffer); - const bufferInfo = { buffer: newBuffer, data: newData, dataView: newDataView }; - shader._uniformBuffersInUse.push(bufferInfo); + bufferGroup.buffersInUse.add(bufferInfo); return bufferInfo; } + _getDynamicUniformBufferFromPool(bufferGroup) { + // + let buffer; + if ( + this.currentUniformBuffer && + this.currentUniformBuffer.offset + bufferGroup.size < this.currentUniformBuffer.size + ) { + // We can fit this next block of uniforms into the current active memory chunk + buffer = this.currentUniformBuffer; + } else { + // Kinda arbitrary. Each dynamic offset has to be in groups of 256, but then + // we can choose how many things we want to be able to fit into a block. + // There's some overhead to each block so if we're drawing a lot of stuff, + // bigger is better. But it's also a lot of wasted memory if we AREN'T drawing + // a lot of stuff. So.... right now it's 40. Feel free to update this if + // a better balance can be achieved. + const size = 256 * 40; + buffer = { + dynamic: true, + lastOffset: 0, + offset: 0, + size, + buffer: this.device.createBuffer({ + size: size, + usage: GPUBufferUsage.UNIFORM, + mappedAtCreation: true, + }), + } + + buffer.data = new Float32Array(buffer.buffer.getMappedRange()); + buffer.dataView = new DataView(buffer.data.buffer); + + this.activeUniformBuffers.push(buffer); + } + + this.currentUniformBuffer = buffer; + + return buffer; + } + _returnUniformBuffersToPool() { // Return all used buffers back to their pools for all registered shaders for (const shader of this._shadersWithPools) { - if (shader._uniformBuffersInUse && shader._uniformBuffersInUse.length > 0) { - this._returnShaderBuffersToPool(shader); - } + this._returnShaderBuffersToPool(shader); } } _returnShaderBuffersToPool(shader) { - // Move all buffers from inUse back to pool - while (shader._uniformBuffersInUse.length > 0) { - const bufferInfo = shader._uniformBuffersInUse.pop(); - shader._uniformBufferPool.push(bufferInfo); + if (shader._uniformBufferGroups) { + for (const bufferGroup of shader._uniformBufferGroups) { + while (bufferGroup.nextBufferPool.length > 0) { + bufferGroup.bufferPool.push(bufferGroup.nextBufferPool.pop()); + } + for (const bufferInfo of bufferGroup.buffersInUse.keys()) { + bufferGroup.nextBufferPool.push(bufferInfo); + } + bufferGroup.currentBuffer = null; + bufferGroup.buffersInUse.clear(); + } } } flushDraw() { + this._finishActiveRenderPass(); // Only submit if we actually had any draws if (this._hasPendingDraws) { // Create a copy of pending command encoders @@ -1123,9 +1250,16 @@ function rendererWebGPU(p5, fn) { this._pendingCommandEncoders = []; this._hasPendingDraws = false; + for (const bufferInfo of this.activeUniformBuffers) { + bufferInfo.buffer.unmap(); + } + // Submit the commands this.queue.submit(commandsToSubmit); + this.activeUniformBuffers = []; + this.currentUniformBuffer = undefined; + // Execute post-submit callbacks after GPU work completes if (this._postSubmitCallbacks.length > 0) { const callbacks = this._postSubmitCallbacks; @@ -1203,11 +1337,14 @@ function rendererWebGPU(p5, fn) { this._returnVertexBuffersToPool(); // Destroy all retired buffers - for (const buffer of this._retiredBuffers) { - if (buffer && buffer.destroy) { - buffer.destroy(); + const retired = this._retiredBuffers; + this._postSubmitCallbacks.push(() => { + for (const buffer of retired) { + if (buffer && buffer.destroy) { + buffer.destroy(); + } } - } + }); this._retiredBuffers = []; if (this._frameState === FRAME_STATE.PROMOTED) { @@ -1235,48 +1372,8 @@ function rendererWebGPU(p5, fn) { this._promoteToFramebufferWithoutCopy(); } - const commandEncoder = this.device.createCommandEncoder(); - - // Use framebuffer texture if active, otherwise use canvas texture - const activeFramebuffer = this.activeFramebuffer(); - - const colorAttachment = { - view: activeFramebuffer - ? (activeFramebuffer.aaColorTexture - ? activeFramebuffer.aaColorTextureView - : activeFramebuffer.colorTextureView) - : this._getCanvasColorTextureView(), - loadOp: "load", - storeOp: "store", - // If using multisampled texture, resolve to non-multisampled texture - resolveTarget: activeFramebuffer && activeFramebuffer.aaColorTexture - ? activeFramebuffer.colorTextureView - : undefined, - }; - - // Use framebuffer depth texture if active, otherwise use canvas depth texture - const depthTextureView = activeFramebuffer - ? (activeFramebuffer.aaDepthTexture - ? activeFramebuffer.aaDepthTextureView - : activeFramebuffer.depthTextureView) - : this.depthTextureView; - const renderPassDescriptor = { - colorAttachments: [colorAttachment], - depthStencilAttachment: depthTextureView - ? { - view: depthTextureView, - depthLoadOp: "load", - depthStoreOp: "store", - depthClearValue: 1.0, - stencilLoadOp: "load", - stencilStoreOp: "store", - depthReadOnly: false, - stencilReadOnly: false, - } - : undefined, - }; - - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + this._beginActiveRenderPass(); + const passEncoder = this.activeRenderPass; const currentShader = this._curShader; passEncoder.setPipeline(currentShader.getPipeline(this._shaderOptions({ mode }))); @@ -1297,33 +1394,71 @@ function rendererWebGPU(p5, fn) { const gpuBuffer = buffers[buffer.dst]; passEncoder.setVertexBuffer(location, gpuBuffer, 0); } - // Bind uniforms - get a buffer from the pool - const uniformBufferInfo = this._getUniformBufferFromPool(currentShader); - this._packUniforms(currentShader, uniformBufferInfo); - this.device.queue.writeBuffer( - uniformBufferInfo.buffer, - 0, - uniformBufferInfo.data.buffer, - uniformBufferInfo.data.byteOffset, - uniformBufferInfo.data.byteLength - ); - // Bind sampler/texture uniforms + // Clear and reuse the map to avoid GC + this._uniformBuffersForBinding.clear(); + + for (const bufferGroup of currentShader._uniformBufferGroups) { + if (bufferGroup.dynamic) { + // Bind uniforms into a part of a big dynamic memory block because + // the group changes often + const uniformBufferInfo = this._getDynamicUniformBufferFromPool(bufferGroup); + this._packUniformGroup(currentShader, bufferGroup.uniforms, uniformBufferInfo); + uniformBufferInfo.lastOffset = uniformBufferInfo.offset; + uniformBufferInfo.offset += Math.ceil(bufferGroup.size / this.uniformBufferAlignment) * this.uniformBufferAlignment; + + // Make a shallow copy so that we keep track of the last offset for this uniform + this._uniformBuffersForBinding.set(bufferGroup.binding, { ...uniformBufferInfo }); + } else { + // Bind uniforms to a binding-specific buffer, which may be cached for performance + let bufferInfo; + const dataChanged = this._hasGroupDataChanged(currentShader, bufferGroup); + + if (!dataChanged && bufferGroup.currentBuffer) { + // Reuse the cached buffer - no need to pack or write + bufferInfo = bufferGroup.currentBuffer; + bufferGroup.buffersInUse.add(bufferInfo); + } else { + // Data changed - get a new buffer and write to it + bufferInfo = this._getUniformBufferFromPool(bufferGroup); + this._packUniformGroup(currentShader, bufferGroup.uniforms, bufferInfo); + this.device.queue.writeBuffer( + bufferInfo.buffer, + 0, + bufferInfo.data.buffer, + bufferInfo.data.byteOffset, + bufferInfo.data.byteLength + ); + + currentShader.buffersDirty = currentShader.buffersDirty || {}; + currentShader.buffersDirty[bufferGroup.group + ',' + bufferGroup.binding] = false; + + // Cache this buffer and data for next frame + bufferGroup.currentBuffer = bufferInfo; + } + + this._uniformBuffersForBinding.set(bufferGroup.binding, bufferInfo); + } + } + + // Bind sampler/texture uniforms and uniform buffers for (const [group, entries] of currentShader._groupEntries) { const bgEntries = entries.map(entry => { - if (group === 0 && entry.binding === 0) { + // Check if this is a uniform buffer binding + const uniformBufferInfo = this._uniformBuffersForBinding.get(entry.binding); + if (uniformBufferInfo && entry.bufferGroup) { return { - binding: 0, - resource: { buffer: uniformBufferInfo.buffer }, + binding: entry.binding, + resource: entry.bufferGroup.dynamic + ? { + buffer: uniformBufferInfo.buffer, + offset: 0, + size: Math.ceil(entry.bufferGroup.size / this.uniformBufferAlignment) * this.uniformBufferAlignment, + } + : { buffer: uniformBufferInfo.buffer }, }; } - if (!entry.uniform.isSampler) { - throw new Error( - 'All non-texture/sampler uniforms should be in the uniform struct!' - ); - } - return { binding: entry.binding, resource: entry.uniform.type === 'sampler' @@ -1337,7 +1472,13 @@ function rendererWebGPU(p5, fn) { layout, entries: bgEntries, }); - passEncoder.setBindGroup(group, bindGroup); + passEncoder.setBindGroup( + group, + bindGroup, + entries.map(e => e.bufferGroup && this._uniformBuffersForBinding.get(e.binding)) + .filter(b => b?.dynamic) + .map(b => b.lastOffset) + ); } if (currentShader.shaderType === "fill") { @@ -1362,11 +1503,6 @@ function rendererWebGPU(p5, fn) { passEncoder.draw(geometry.lineVertices.length / 3, count, 0, 0); } - passEncoder.end(); - - // Store the command encoder for later submission - this._pendingCommandEncoders.push(commandEncoder.finish()); - // Mark that we have pending draws that need submission this._hasPendingDraws = true; } @@ -1375,46 +1511,48 @@ function rendererWebGPU(p5, fn) { // SHADER ////////////////////////////////////////////// - _packUniforms(shader, bufferInfo) { + _packUniformGroup(shader, groupUniforms, bufferInfo) { + // Pack a single group's uniforms into a buffer const data = bufferInfo.data; const dataView = bufferInfo.dataView; - for (const name in shader.uniforms) { - const uniform = shader.uniforms[name]; - if (uniform.isSampler) continue; + const offset = bufferInfo.offset || 0; + for (const uniform of groupUniforms) { + const fullUniform = shader.uniforms[uniform.name]; + if (!fullUniform || fullUniform.isSampler) continue; - if (uniform.baseType === 'u32') { - if (uniform.size === 4) { - // Single u32 - dataView.setUint32(uniform.offset, uniform._cachedData, true); + if (fullUniform.baseType === 'u32') { + if (fullUniform.size === 4) { + dataView.setUint32(offset + fullUniform.offset, fullUniform._cachedData, true); } else { - // Vector of u32s - const uniformData = uniform._cachedData; + const uniformData = fullUniform._cachedData; for (let i = 0; i < uniformData.length; i++) { - dataView.setUint32(uniform.offset + i * 4, uniformData[i], true); + dataView.setUint32(offset + fullUniform.offset + i * 4, uniformData[i], true); } } - } else if (uniform.baseType === 'i32') { - if (uniform.size === 4) { - // Single i32 - dataView.setInt32(uniform.offset, uniform._cachedData, true); + } else if (fullUniform.baseType === 'i32') { + if (fullUniform.size === 4) { + dataView.setInt32(offset + fullUniform.offset, fullUniform._cachedData, true); } else { - // Vector of i32s - const uniformData = uniform._cachedData; + const uniformData = fullUniform._cachedData; for (let i = 0; i < uniformData.length; i++) { - dataView.setInt32(uniform.offset + i * 4, uniformData[i], true); + dataView.setInt32(offset + fullUniform.offset + i * 4, uniformData[i], true); } } - } else if (uniform.size === 4) { - // Single float value - data.set([uniform._cachedData], uniform.offset / 4); - } else if (uniform._cachedData !== undefined) { - // Float array (including vec2, vec3, vec4, mat4x4) - data.set(uniform._cachedData, uniform.offset / 4); + } else if (fullUniform.size === 4) { + data.set([fullUniform._cachedData], (offset + fullUniform.offset) / 4); + } else if (fullUniform._cachedData !== undefined) { + data.set(fullUniform._cachedData, (offset + fullUniform.offset) / 4); } } } + _hasGroupDataChanged(shader, bufferGroup) { + // First time + if (!bufferGroup.currentBuffer) return true; + return shader.buffersDirty?.[bufferGroup.group + ',' + bufferGroup.binding]; + } + _parseStruct(shaderSource, structName) { const structMatch = shaderSource.match( new RegExp(`struct\\s+${structName}\\s*\\{([^\\}]+)\\}`) @@ -1542,22 +1680,64 @@ function rendererWebGPU(p5, fn) { } getUniformMetadata(shader) { - // Currently, for ease of parsing, we enforce that the first bind group is a - // struct, which contains all non-sampler uniforms. Then, any subsequent - // groups contain samplers. - - // Extract the struct name from the uniform variable declaration - const uniformVarRegex = /@group\(0\)\s+@binding\(0\)\s+var\s+(\w+)\s*:\s*(\w+);/; - const uniformVarMatch = uniformVarRegex.exec(shader.vertSrc()); - if (!uniformVarMatch) { - throw new Error('Expected a uniform struct bound to @group(0) @binding(0)'); - } - const structType = uniformVarMatch[2]; - const uniforms = this._parseStruct(shader.vertSrc(), structType); + // Parse all uniform struct bindings in group 0. + // TODO: support non-sampler uniforms being in other groups + + // Each binding represents a logical group of uniforms, since they get + // updated or cached all at once. + + const uniformGroups = []; + const uniformVarRegex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var\s+(\w+)\s*:\s*(\w+);/g; + + let match; + while ((match = uniformVarRegex.exec(shader.vertSrc())) !== null) { + const [_, groupNum, binding, varName, structType] = match; + const bindingIndex = parseInt(binding); + const uniforms = this._parseStruct(shader.vertSrc(), structType); + + uniformGroups.push({ + group: parseInt(groupNum), + binding: bindingIndex, + varName, + structType, + uniforms + }); + } + + if (uniformGroups.length === 0) { + throw new Error('Expected at least one uniform struct bound to @group(0)'); + } + + // While we're also keeping track of the groups, the API we expose + // to users of p5 is just a flat list of uniforms (which can be the + // individual struct items in the group.) + const allUniforms = {}; + for (const group of uniformGroups) { + for (const [uniformName, uniformData] of Object.entries(group.uniforms)) { + allUniforms[uniformName] = { + ...uniformData, + group: group.group, + binding: group.binding, + varName: group.varName + }; + } + } + + // Store uniform groups for buffer pooling + shader._uniformGroups = uniformGroups; + // Extract samplers from group bindings const samplers = {}; // TODO: support other texture types const samplerRegex = /@group\((\d+)\)\s*@binding\((\d+)\)\s*var\s+(\w+)\s*:\s*(texture_2d|sampler);/g; + + // Track which bindings are taken by the struct properties we've parsed + // (the rest should be textures/samplers) + const structUniformBindings = {}; + for (const g of uniformGroups) { + structUniformBindings[g.group + ',' + g.binding] = true; + } + for (const [src, visibility] of [ [shader.vertSrc(), GPUShaderStage.VERTEX], [shader.fragSrc(), GPUShaderStage.FRAGMENT] @@ -1567,10 +1747,8 @@ function rendererWebGPU(p5, fn) { const [_, group, binding, name, type] = match; const groupIndex = parseInt(group); const bindingIndex = parseInt(binding); - // We're currently reserving group 0 for non-sampler stuff, which we parse - // above, so we can skip it here while we grab the remaining sampler - // uniforms - if (groupIndex === 0 && bindingIndex === 0) continue; + // Skip struct uniform bindings which we've already parsed + if (structUniformBindings[groupIndex + ',' + bindingIndex]) continue; const key = `${groupIndex},${bindingIndex}`; samplers[key] = { @@ -1599,17 +1777,17 @@ function rendererWebGPU(p5, fn) { } } } - return [...Object.values(uniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)]; + return [...Object.values(allUniforms).sort((a, b) => a.index - b.index), ...Object.values(samplers)]; } - getNextBindingIndex(shader, group = 0) { + 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|sampler|uniform)/g; let maxBindingIndex = -1; for (const [src, visibility] of [ - [shader.vertSrc(), GPUShaderStage.VERTEX], - [shader.fragSrc(), GPUShaderStage.FRAGMENT] + [vert, GPUShaderStage.VERTEX], + [frag, GPUShaderStage.FRAGMENT] ]) { let match; while ((match = samplerRegex.exec(src)) !== null) { @@ -1623,10 +1801,13 @@ function rendererWebGPU(p5, fn) { return maxBindingIndex + 1; } - updateUniformValue(_shader, uniform, data) { + updateUniformValue(shader, uniform, data) { if (uniform.isSampler) { uniform.texture = data instanceof Texture ? data : this.getTexture(data); + } else { + shader.buffersDirty = shader.buffersDirty || {}; + shader.buffersDirty[uniform.group + ',' + uniform.binding] = true; } } @@ -1864,6 +2045,7 @@ function rendererWebGPU(p5, fn) { } _clearClipBuffer() { + this._finishActiveRenderPass(); const commandEncoder = this.device.createCommandEncoder(); const activeFramebuffer = this.activeFramebuffer(); @@ -1947,12 +2129,33 @@ function rendererWebGPU(p5, fn) { } } - let uniforms = ''; + // Inject hook uniforms as a separate struct at a new binding + let hookUniformFields = ''; for (const key in shader.hooks.uniforms) { // WGSL format: "name: type" - uniforms += `${key},\n`; + hookUniformFields += ` ${key},\n`; + } + + if (hookUniformFields) { + // Find the next available binding in group 0 + // Use the source we're currently building (preMain) which has texture bindings. We can't call `fragSrc()` + // or `vertSrc()` because we may be in one of those calls already, and might infinite loop + const nextBinding = this.getNextBindingIndex({ + vert: shaderType === 'vertex' ? preMain + (shader.hooks.vertex?.declarations ?? '') + shader.hooks.declarations : shader._vertSrc, + frag: shaderType === 'fragment' ? preMain + (shader.hooks.fragment?.declarations ?? '') + shader.hooks.declarations : shader._fragSrc, + }, 0); + + // Create HookUniforms struct and binding + const hookUniformsDecl = ` +// Hook Uniforms (from .modify()) +struct HookUniforms { +${hookUniformFields}} + +@group(0) @binding(${nextBinding}) var hooks: HookUniforms; +`; + // Insert before the first @group binding + preMain = preMain.replace(/(@group\(0\)\s+@binding)/, `${hookUniformsDecl}\n$1`); } - preMain = preMain.replace(/struct\s+Uniforms\s+\{/, `$&\n${uniforms}`); // Handle varying variables by injecting them into VertexOutput and FragmentInput structs if (shader.hooks.varyingVariables && shader.hooks.varyingVariables.length > 0) { @@ -2423,6 +2626,7 @@ function rendererWebGPU(p5, fn) { } _clearFramebufferTextures(framebuffer) { + this._finishActiveRenderPass(); const commandEncoder = this.device.createCommandEncoder(); // Clear the color texture (and multisampled texture if it exists) @@ -2863,6 +3067,7 @@ function rendererWebGPU(p5, fn) { * Copy framebuffer content directly to WebGPU texture mip level */ _accumulateMipLevel(framebuffer, mipmapData, mipLevel, width, height) { + this.flushDraw(); // Copy from framebuffer texture to the mip level const commandEncoder = this.device.createCommandEncoder(); diff --git a/src/webgpu/shaders/color.js b/src/webgpu/shaders/color.js index dae7ab4e8d..fc9feace5e 100644 --- a/src/webgpu/shaders/color.js +++ b/src/webgpu/shaders/color.js @@ -1,19 +1,30 @@ const uniforms = ` -struct Uniforms { +// Group 1: Camera and Projection +struct CameraUniforms { + uProjectionMatrix: mat4x4, // @p5 ifdef Vertex getWorldInputs - uModelMatrix: mat4x4, uViewMatrix: mat4x4, - uModelNormalMatrix: mat3x3, +// @p5 endif uCameraNormalMatrix: mat3x3, +} + +// Group 2: Model Transform +struct ModelUniforms { +// @p5 ifdef Vertex getWorldInputs + uModelMatrix: mat4x4, + uModelNormalMatrix: mat3x3, // @p5 endif // @p5 ifndef Vertex getWorldInputs uModelViewMatrix: mat4x4, uNormalMatrix: mat3x3, // @p5 endif - uProjectionMatrix: mat4x4, uMaterialColor: vec4, +} + +// Group 3: Material Properties +struct MaterialUniforms { uUseVertexColor: u32, -}; +} `; export const colorVertexShader = ` @@ -32,7 +43,9 @@ struct VertexOutput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var material: MaterialUniforms; struct Vertex { position: vec3, @@ -46,12 +59,12 @@ fn main(input: VertexInput) -> VertexOutput { HOOK_beforeVertex(); var output: VertexOutput; - let useVertexColor = (uniforms.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0); + let useVertexColor = (material.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0); var inputs = Vertex( input.aPosition, input.aNormal, input.aTexCoord, - select(uniforms.uMaterialColor, input.aVertexColor, useVertexColor) + select(model.uMaterialColor, input.aVertexColor, useVertexColor) ); // @p5 ifdef Vertex getObjectInputs @@ -59,20 +72,20 @@ fn main(input: VertexInput) -> VertexOutput { // @p5 endif // @p5 ifdef Vertex getWorldInputs - inputs.position = (uniforms.uModelMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = uniforms.uModelNormalMatrix * inputs.normal; + inputs.position = (model.uModelMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = model.uModelNormalMatrix * inputs.normal; inputs = HOOK_getWorldInputs(inputs); // @p5 endif // @p5 ifdef Vertex getWorldInputs // Already multiplied by the model matrix, just apply view - inputs.position = (uniforms.uViewMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = uniforms.uCameraNormalMatrix * inputs.normal; + inputs.position = (camera.uViewMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = camera.uCameraNormalMatrix * inputs.normal; // @p5 endif // @p5 ifndef Vertex getWorldInputs // Apply both at once - inputs.position = (uniforms.uModelViewMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = uniforms.uNormalMatrix * inputs.normal; + inputs.position = (model.uModelViewMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = model.uNormalMatrix * inputs.normal; // @p5 endif // @p5 ifdef Vertex getCameraInputs @@ -83,7 +96,7 @@ fn main(input: VertexInput) -> VertexOutput { output.vVertexNormal = normalize(inputs.normal); output.vColor = inputs.color; - output.Position = uniforms.uProjectionMatrix * vec4(inputs.position, 1.0); + output.Position = camera.uProjectionMatrix * vec4(inputs.position, 1.0); HOOK_afterVertex(); return output; @@ -98,7 +111,9 @@ struct FragmentInput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var material: MaterialUniforms; @fragment diff --git a/src/webgpu/shaders/font.js b/src/webgpu/shaders/font.js index 7cd92c6bff..b78550607e 100644 --- a/src/webgpu/shaders/font.js +++ b/src/webgpu/shaders/font.js @@ -1,7 +1,16 @@ const uniforms = ` -struct Uniforms { - uModelViewMatrix: mat4x4, +// Group 1: Camera and Projection +struct CameraUniforms { uProjectionMatrix: mat4x4, +} + +// Group 2: Model Transform +struct ModelUniforms { + uModelViewMatrix: mat4x4, +} + +// Group 3: Font Properties +struct FontUniforms { uStrokeImageSize: vec2, uCellsImageSize: vec2, uGridImageSize: vec2, @@ -10,7 +19,7 @@ struct Uniforms { uGlyphRect: vec4, uGlyphOffset: f32, uMaterialColor: vec4, -}; +} `; export const fontVertexShader = ` @@ -25,7 +34,9 @@ struct VertexOutput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var font: FontUniforms; @vertex fn main(input: VertexInput) -> VertexOutput { @@ -33,35 +44,35 @@ fn main(input: VertexInput) -> VertexOutput { var positionVec4 = vec4(input.aPosition, 1.0); // scale by the size of the glyph's rectangle - positionVec4.x = positionVec4.x * (uniforms.uGlyphRect.z - uniforms.uGlyphRect.x); - positionVec4.y = positionVec4.y * (uniforms.uGlyphRect.w - uniforms.uGlyphRect.y); + positionVec4.x = positionVec4.x * (font.uGlyphRect.z - font.uGlyphRect.x); + positionVec4.y = positionVec4.y * (font.uGlyphRect.w - font.uGlyphRect.y); // Expand glyph bounding boxes by 1px on each side to give a bit of room // for antialiasing - let newOrigin = (uniforms.uModelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz; - let newDX = (uniforms.uModelViewMatrix * vec4(1.0, 0.0, 0.0, 1.0)).xyz; - let newDY = (uniforms.uModelViewMatrix * vec4(0.0, 1.0, 0.0, 1.0)).xyz; + let newOrigin = (model.uModelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz; + let newDX = (model.uModelViewMatrix * vec4(1.0, 0.0, 0.0, 1.0)).xyz; + let newDY = (model.uModelViewMatrix * vec4(0.0, 1.0, 0.0, 1.0)).xyz; let pixelScale = vec2( 1.0 / length(newOrigin - newDX), 1.0 / length(newOrigin - newDY) ); let offset = pixelScale * normalize(input.aTexCoord - vec2(0.5, 0.5)); let textureOffset = offset * (1.0 / vec2( - uniforms.uGlyphRect.z - uniforms.uGlyphRect.x, - uniforms.uGlyphRect.w - uniforms.uGlyphRect.y + font.uGlyphRect.z - font.uGlyphRect.x, + font.uGlyphRect.w - font.uGlyphRect.y )); // move to the corner of the glyph - positionVec4.x = positionVec4.x + uniforms.uGlyphRect.x; - positionVec4.y = positionVec4.y + uniforms.uGlyphRect.y; + positionVec4.x = positionVec4.x + font.uGlyphRect.x; + positionVec4.y = positionVec4.y + font.uGlyphRect.y; // move to the letter's line offset - positionVec4.x = positionVec4.x + uniforms.uGlyphOffset; + positionVec4.x = positionVec4.x + font.uGlyphOffset; positionVec4.x = positionVec4.x + offset.x; positionVec4.y = positionVec4.y + offset.y; - output.Position = uniforms.uProjectionMatrix * uniforms.uModelViewMatrix * positionVec4; + output.Position = camera.uProjectionMatrix * model.uModelViewMatrix * positionVec4; output.vTexCoord = input.aTexCoord + textureOffset; return output; @@ -74,7 +85,9 @@ struct FragmentInput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var font: FontUniforms; @group(1) @binding(0) var uSamplerStrokes: texture_2d; @group(1) @binding(1) var uSamplerStrokes_sampler: sampler; @@ -217,14 +230,14 @@ fn main(input: FragmentInput) -> @location(0) vec4 { let pixelScale = hardness / fwidth(input.vTexCoord); // which grid cell is this pixel in? - let gridCoord = vec2(floor(input.vTexCoord * vec2(uniforms.uGridSize))); + let gridCoord = vec2(floor(input.vTexCoord * vec2(font.uGridSize))); // intersect curves in this row { // the index into the row info bitmap - let rowIndex = gridCoord.y + uniforms.uGridOffset.y; + let rowIndex = gridCoord.y + font.uGridOffset.y; // fetch the info texel - let rowInfo = getTexel(uSamplerRows, uSamplerRows_sampler, rowIndex, uniforms.uGridImageSize); + let rowInfo = getTexel(uSamplerRows, uSamplerRows_sampler, rowIndex, font.uGridImageSize); // unpack the rowInfo let rowStrokeIndex = getInt16(rowInfo.xy); let rowStrokeCount = getInt16(rowInfo.zw); @@ -237,14 +250,14 @@ fn main(input: FragmentInput) -> @location(0) vec4 { // each stroke is made up of 3 points: the start and control point // and the start of the next curve. // fetch the indices of this pair of strokes: - let strokeIndices = getTexel(uSamplerRowStrokes, uSamplerRowStrokes_sampler, rowStrokeIndex + iRowStroke, uniforms.uCellsImageSize); + let strokeIndices = getTexel(uSamplerRowStrokes, uSamplerRowStrokes_sampler, rowStrokeIndex + iRowStroke, font.uCellsImageSize); // unpack the stroke index let strokePos = getInt16(strokeIndices.xy); // fetch the two strokes - let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, uniforms.uStrokeImageSize); - let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, uniforms.uStrokeImageSize); + let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, font.uStrokeImageSize); + let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, font.uStrokeImageSize); // calculate the coverage coverageX(stroke0.xy, stroke0.zw, stroke1.xy, input.vTexCoord, pixelScale, &coverage, &weight); @@ -253,8 +266,8 @@ fn main(input: FragmentInput) -> @location(0) vec4 { // intersect curves in this column { - let colIndex = gridCoord.x + uniforms.uGridOffset.x; - let colInfo = getTexel(uSamplerCols, uSamplerCols_sampler, colIndex, uniforms.uGridImageSize); + let colIndex = gridCoord.x + font.uGridOffset.x; + let colInfo = getTexel(uSamplerCols, uSamplerCols_sampler, colIndex, font.uGridImageSize); let colStrokeIndex = getInt16(colInfo.xy); let colStrokeCount = getInt16(colInfo.zw); @@ -263,11 +276,11 @@ fn main(input: FragmentInput) -> @location(0) vec4 { break; } - let strokeIndices = getTexel(uSamplerColStrokes, uSamplerColStrokes_sampler, colStrokeIndex + iColStroke, uniforms.uCellsImageSize); + let strokeIndices = getTexel(uSamplerColStrokes, uSamplerColStrokes_sampler, colStrokeIndex + iColStroke, font.uCellsImageSize); let strokePos = getInt16(strokeIndices.xy); - let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, uniforms.uStrokeImageSize); - let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, uniforms.uStrokeImageSize); + let stroke0 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 0, font.uStrokeImageSize); + let stroke1 = getTexel(uSamplerStrokes, uSamplerStrokes_sampler, strokePos + 1, font.uStrokeImageSize); coverageY(stroke0.xy, stroke0.zw, stroke1.xy, input.vTexCoord, pixelScale, &coverage, &weight); } } @@ -276,7 +289,7 @@ fn main(input: FragmentInput) -> @location(0) vec4 { let distance = max(weight.x + weight.y, minDistance); // manhattan approx. let antialias = abs(dot(coverage, weight) / distance); let cover = min(abs(coverage.x), abs(coverage.y)); - var outColor = vec4(uniforms.uMaterialColor.rgb, 1.0) * uniforms.uMaterialColor.a; + var outColor = vec4(font.uMaterialColor.rgb, 1.0) * font.uMaterialColor.a; outColor = outColor * saturate_f32(max(antialias, cover)); return outColor; } diff --git a/src/webgpu/shaders/line.js b/src/webgpu/shaders/line.js index 5562d6775c..3b53b0a6e7 100644 --- a/src/webgpu/shaders/line.js +++ b/src/webgpu/shaders/line.js @@ -1,5 +1,13 @@ const uniforms = ` -struct Uniforms { +// Group 1: Camera and Projection +struct CameraUniforms { + uProjectionMatrix: mat4x4, + uViewport: vec4, + uPerspective: u32, +} + +// Group 2: Model Transform +struct ModelUniforms { // @p5 ifdef StrokeVertex getWorldInputs uModelMatrix: mat4x4, uViewMatrix: mat4x4, @@ -8,12 +16,13 @@ struct Uniforms { uModelViewMatrix: mat4x4, // @p5 endif uMaterialColor: vec4, - uProjectionMatrix: mat4x4, +} + +// Group 3: Stroke Properties +struct StrokeUniforms { uStrokeWeight: f32, uUseLineColor: f32, uSimpleLines: f32, - uViewport: vec4, - uPerspective: u32, uStrokeCap: u32, uStrokeJoin: u32, }`; @@ -40,7 +49,9 @@ struct StrokeVertexOutput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var stroke: StrokeUniforms; struct StrokeVertex { position: vec3, @@ -73,7 +84,7 @@ fn lineIntersection(aPoint: vec2f, aDir: vec2f, bPoint: vec2f, bDir: vec2f) -> v fn main(input: StrokeVertexInput) -> StrokeVertexOutput { HOOK_beforeVertex(); var output: StrokeVertexOutput; - let simpleLines = (uniforms.uSimpleLines != 0.); + let simpleLines = (stroke.uSimpleLines != 0.); if (!simpleLines) { if (all(input.aTangentIn == vec3()) != all(input.aTangentOut == vec3())) { output.vCap = 1.; @@ -90,17 +101,17 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { } } var lineColor: vec4; - if (uniforms.uUseLineColor != 0.) { + if (stroke.uUseLineColor != 0.) { lineColor = input.aVertexColor; } else { - lineColor = uniforms.uMaterialColor; + lineColor = model.uMaterialColor; } var inputs = StrokeVertex( input.aPosition.xyz, input.aTangentIn, input.aTangentOut, lineColor, - uniforms.uStrokeWeight + stroke.uStrokeWeight ); // @p5 ifdef StrokeVertex getObjectInputs @@ -108,23 +119,23 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { // @p5 endif // @p5 ifdef StrokeVertex getWorldInputs - inputs.position = (uniforms.uModelMatrix * vec4(inputs.position, 1.)).xyz; - inputs.tangentIn = (uniforms.uModelMatrix * vec4(input.aTangentIn, 1.)).xyz; - inputs.tangentOut = (uniforms.uModelMatrix * vec4(input.aTangentOut, 1.)).xyz; + inputs.position = (model.uModelMatrix * vec4(inputs.position, 1.)).xyz; + inputs.tangentIn = (model.uModelMatrix * vec4(input.aTangentIn, 1.)).xyz; + inputs.tangentOut = (model.uModelMatrix * vec4(input.aTangentOut, 1.)).xyz; inputs = HOOK_getWorldInputs(inputs); // @p5 endif // @p5 ifdef StrokeVertex getWorldInputs // Already multiplied by the model matrix, just apply view - inputs.position = (uniforms.uViewMatrix * vec4(inputs.position, 1.)).xyz; - inputs.tangentIn = (uniforms.uViewMatrix * vec4(input.aTangentIn, 0.)).xyz; - inputs.tangentOut = (uniforms.uViewMatrix * vec4(input.aTangentOut, 0.)).xyz; + inputs.position = (model.uViewMatrix * vec4(inputs.position, 1.)).xyz; + inputs.tangentIn = (model.uViewMatrix * vec4(input.aTangentIn, 0.)).xyz; + inputs.tangentOut = (model.uViewMatrix * vec4(input.aTangentOut, 0.)).xyz; // @p5 endif // @p5 ifndef StrokeVertex getWorldInputs // Apply both at once - inputs.position = (uniforms.uModelViewMatrix * vec4(inputs.position, 1.)).xyz; - inputs.tangentIn = (uniforms.uModelViewMatrix * vec4(input.aTangentIn, 0.)).xyz; - inputs.tangentOut = (uniforms.uModelViewMatrix * vec4(input.aTangentOut, 0.)).xyz; + inputs.position = (model.uModelViewMatrix * vec4(inputs.position, 1.)).xyz; + inputs.tangentIn = (model.uModelViewMatrix * vec4(input.aTangentIn, 0.)).xyz; + inputs.tangentOut = (model.uModelViewMatrix * vec4(input.aTangentOut, 0.)).xyz; // @p5 endif // @p5 ifdef StrokeVertex getCameraInputs inputs = HOOK_getCameraInputs(inputs); @@ -174,27 +185,27 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { posqIn.z -= dynamicZAdjustment; posqOut.z -= dynamicZAdjustment; - var p = uniforms.uProjectionMatrix * posp; - var qIn = uniforms.uProjectionMatrix * posqIn; - var qOut = uniforms.uProjectionMatrix * posqOut; + var p = camera.uProjectionMatrix * posp; + var qIn = camera.uProjectionMatrix * posqIn; + var qOut = camera.uProjectionMatrix * posqOut; - var tangentIn = normalize((qIn.xy * p.w - p.xy * qIn.w) * uniforms.uViewport.zw); - var tangentOut = normalize((qOut.xy * p.w - p.xy * qOut.w) * uniforms.uViewport.zw); + var tangentIn = normalize((qIn.xy * p.w - p.xy * qIn.w) * camera.uViewport.zw); + var tangentOut = normalize((qOut.xy * p.w - p.xy * qOut.w) * camera.uViewport.zw); var curPerspScale = vec2(); - if (uniforms.uPerspective == 1) { + if (camera.uPerspective == 1) { // Perspective --- // convert from world to clip by multiplying with projection scaling factor // to get the right thickness (see https://github.com/processing/processing/issues/5182) // The y value of the projection matrix may be flipped if rendering to a Framebuffer. // Multiplying again by its sign here negates the flip to get just the scale. - curPerspScale = (uniforms.uProjectionMatrix * vec4(1., sign(uniforms.uProjectionMatrix[1][1]), 0., 0.)).xy; + curPerspScale = (camera.uProjectionMatrix * vec4(1., sign(camera.uProjectionMatrix[1][1]), 0., 0.)).xy; } else { // No Perspective --- // multiply by W (to cancel out division by W later in the pipeline) and // convert from screen to clip (derived from clip to screen above) - curPerspScale = p.w / (0.5 * uniforms.uViewport.zw); + curPerspScale = p.w / (0.5 * camera.uViewport.zw); } var offset = vec2(); @@ -217,7 +228,7 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { if (sideEnum == 2.) { // Calculate the position + tangent on either side of the join, and // find where the lines intersect to find the elbow of the join - var c = (posp.xy / posp.w + vec2(1.)) * 0.5 * uniforms.uViewport.zw; + var c = (posp.xy / posp.w + vec2(1.)) * 0.5 * camera.uViewport.zw; var intersection = lineIntersection( c + (side * normalIn * inputs.weight / 2.), @@ -243,7 +254,7 @@ fn main(input: StrokeVertexInput) -> StrokeVertexOutput { offset = side * normalOut * inputs.weight / 2.; } } - if (uniforms.uStrokeJoin == 2) { + if (stroke.uStrokeJoin == 2) { var avgNormal = vec2(-output.vTangent.y, output.vTangent.x); output.vMaxDist = abs(dot(avgNormal, normalIn * inputs.weight / 2.)); } else { @@ -292,7 +303,9 @@ struct StrokeFragmentInput { } ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var stroke: StrokeUniforms; fn distSquared(a: vec2, b: vec2) -> f32 { @@ -321,12 +334,12 @@ fn main(input: StrokeFragmentInput) -> @location(0) vec4 { if (input.vCap > 0.) { if ( - uniforms.uStrokeCap == STROKE_CAP_ROUND && + stroke.uStrokeCap == STROKE_CAP_ROUND && HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25) ) { discard; } else if ( - uniforms.uStrokeCap == STROKE_CAP_SQUARE && + stroke.uStrokeCap == STROKE_CAP_SQUARE && HOOK_shouldDiscard(dot(inputs.position - inputs.center, inputs.tangent) > 0.) ) { discard; @@ -335,11 +348,11 @@ fn main(input: StrokeFragmentInput) -> @location(0) vec4 { } } else if (input.vJoin > 0.) { if ( - uniforms.uStrokeJoin == STROKE_JOIN_ROUND && + stroke.uStrokeJoin == STROKE_JOIN_ROUND && HOOK_shouldDiscard(distSquared(inputs.position, inputs.center) > inputs.strokeWeight * inputs.strokeWeight * 0.25) ) { discard; - } else if (uniforms.uStrokeJoin == STROKE_JOIN_BEVEL) { + } else if (stroke.uStrokeJoin == STROKE_JOIN_BEVEL) { let normal = vec2(-inputs.tangent.y, -inputs.tangent.x); if (HOOK_shouldDiscard(abs(dot(inputs.position - inputs.center, normal)) > input.vMaxDist)) { discard; diff --git a/src/webgpu/shaders/material.js b/src/webgpu/shaders/material.js index aec9a1d292..bac69aa4ff 100644 --- a/src/webgpu/shaders/material.js +++ b/src/webgpu/shaders/material.js @@ -1,40 +1,49 @@ const uniforms = ` -struct Uniforms { +// Group 1: Camera and Projection +struct CameraUniforms { + uViewMatrix: mat4x4, + uProjectionMatrix: mat4x4, + uCameraNormalMatrix: mat3x3, +} + +// Group 2: Model Transform +struct ModelUniforms { // @p5 ifdef Vertex getWorldInputs uModelMatrix: mat4x4, uModelNormalMatrix: mat3x3, - uCameraNormalMatrix: mat3x3, // @p5 endif // @p5 ifndef Vertex getWorldInputs uModelViewMatrix: mat4x4, uNormalMatrix: mat3x3, // @p5 endif - uViewMatrix: mat4x4, - uProjectionMatrix: mat4x4, uMaterialColor: vec4, - uUseVertexColor: u32, +} +// Group 3: Material Properties +struct MaterialUniforms { + uUseVertexColor: u32, uHasSetAmbient: u32, uAmbientColor: vec3, uSpecularMatColor: vec4, uAmbientMatColor: vec4, uEmissiveMatColor: vec4, - uTint: vec4, isTexture: u32, + uSpecular: u32, + uShininess: f32, + uMetallic: f32, +} - uCameraRotation: mat3x3, - +// Group 4: Lighting +struct LightingUniforms { uDirectionalLightCount: i32, uLightingDirection: array, 5>, uDirectionalDiffuseColors: array, 5>, uDirectionalSpecularColors: array, 5>, - uPointLightCount: i32, uPointLightLocation: array, 5>, uPointLightDiffuseColors: array, 5>, uPointLightSpecularColors: array, 5>, - uSpotLightCount: i32, uSpotLightAngle: vec4, uSpotLightConc: vec4, @@ -42,18 +51,12 @@ struct Uniforms { uSpotLightSpecularColors: array, 4>, uSpotLightLocation: array, 4>, uSpotLightDirection: array, 4>, - - uSpecular: u32, - uShininess: f32, - uMetallic: f32, - uConstantAttenuation: f32, uLinearAttenuation: f32, uQuadraticAttenuation: f32, - uUseImageLight: u32, uUseLighting: u32, -}; +} `; export const materialVertexShader = ` @@ -73,7 +76,10 @@ struct VertexOutput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var material: MaterialUniforms; +@group(0) @binding(3) var lighting: LightingUniforms; struct Vertex { position: vec3, @@ -87,12 +93,12 @@ fn main(input: VertexInput) -> VertexOutput { HOOK_beforeVertex(); var output: VertexOutput; - let useVertexColor = (uniforms.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0); + let useVertexColor = (material.uUseVertexColor != 0 && input.aVertexColor.x >= 0.0); var inputs = Vertex( input.aPosition, input.aNormal, input.aTexCoord, - select(uniforms.uMaterialColor, input.aVertexColor, useVertexColor) + select(model.uMaterialColor, input.aVertexColor, useVertexColor) ); // @p5 ifdef Vertex getObjectInputs @@ -100,20 +106,20 @@ fn main(input: VertexInput) -> VertexOutput { // @p5 endif // @p5 ifdef Vertex getWorldInputs - inputs.position = (uniforms.uModelMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = uniforms.uModelNormalMatrix * inputs.normal; + inputs.position = (model.uModelMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = model.uModelNormalMatrix * inputs.normal; inputs = HOOK_getWorldInputs(inputs); // @p5 endif // @p5 ifdef Vertex getWorldInputs // Already multiplied by the model matrix, just apply view - inputs.position = (uniforms.uViewMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = uniforms.uCameraNormalMatrix * inputs.normal; + inputs.position = (camera.uViewMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = camera.uCameraNormalMatrix * inputs.normal; // @p5 endif // @p5 ifndef Vertex getWorldInputs // Apply both at once - inputs.position = (uniforms.uModelViewMatrix * vec4(inputs.position, 1.0)).xyz; - inputs.normal = uniforms.uNormalMatrix * inputs.normal; + inputs.position = (model.uModelViewMatrix * vec4(inputs.position, 1.0)).xyz; + inputs.normal = model.uNormalMatrix * inputs.normal; // @p5 endif // @p5 ifdef Vertex getCameraInputs @@ -125,7 +131,7 @@ fn main(input: VertexInput) -> VertexOutput { output.vNormal = normalize(inputs.normal); output.vColor = inputs.color; - output.Position = uniforms.uProjectionMatrix * vec4(inputs.position, 1.0); + output.Position = camera.uProjectionMatrix * vec4(inputs.position, 1.0); HOOK_afterVertex(); return output; @@ -141,15 +147,18 @@ struct FragmentInput { }; ${uniforms} -@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(0) var camera: CameraUniforms; +@group(0) @binding(1) var model: ModelUniforms; +@group(0) @binding(2) var material: MaterialUniforms; +@group(0) @binding(3) var lighting: LightingUniforms; -@group(0) @binding(1) var uSampler: texture_2d; -@group(0) @binding(2) var uSampler_sampler: sampler; +@group(0) @binding(4) var uSampler: texture_2d; +@group(0) @binding(5) var uSampler_sampler: sampler; -@group(0) @binding(3) var environmentMapDiffused: texture_2d; -@group(0) @binding(4) var environmentMapDiffused_sampler: sampler; -@group(0) @binding(5) var environmentMapSpecular: texture_2d; -@group(0) @binding(6) var environmentMapSpecular_sampler: sampler; +@group(0) @binding(6) var environmentMapDiffused: texture_2d; +@group(0) @binding(7) var environmentMapDiffused_sampler: sampler; +@group(0) @binding(8) var environmentMapSpecular: texture_2d; +@group(0) @binding(9) var environmentMapSpecular_sampler: sampler; struct ColorComponents { baseColor: vec3, @@ -212,7 +221,7 @@ fn mapTextureToNormal(v: vec3) -> vec2 { fn calculateImageDiffuse(vNormal: vec3, vViewPosition: vec3, metallic: f32) -> vec3 { // make 2 seperate builds let worldCameraPosition = vec3(0.0, 0.0, 0.0); // hardcoded world camera position - let worldNormal = normalize(vNormal * uniforms.uCameraRotation); + let worldNormal = normalize(vNormal * camera.uCameraNormalMatrix); let newTexCoord = mapTextureToNormal(worldNormal); let texture = textureSample(environmentMapDiffused, environmentMapDiffused_sampler, newTexCoord); // this is to make the darker sections more dark @@ -224,7 +233,7 @@ fn calculateImageSpecular(vNormal: vec3, vViewPosition: vec3, shinines let worldCameraPosition = vec3(0.0, 0.0, 0.0); let worldNormal = normalize(vNormal); let lightDirection = normalize(vViewPosition - worldCameraPosition); - let R = reflect(lightDirection, worldNormal) * uniforms.uCameraRotation; + let R = reflect(lightDirection, worldNormal) * camera.uCameraNormalMatrix; let newTexCoord = mapTextureToNormal(R); // In p5js the range of shininess is >= 1, @@ -273,7 +282,7 @@ fn singleLight( let specular = select( 0., phongSpecular(lightDir, viewDirection, normal, shininess) * specularIntensity, - uniforms.uSpecular == 1 + material.uSpecular == 1 ); return LightIntensityResult(diffuse, specular); } @@ -287,69 +296,69 @@ fn totalLight( var totalSpecular = vec3(0.0, 0.0, 0.0); var totalDiffuse = vec3(0.0, 0.0, 0.0); - if (uniforms.uUseLighting == 0) { + if (lighting.uUseLighting == 0) { return LightResult(vec3(1.0, 1.0, 1.0), totalSpecular); } let viewDirection = normalize(-modelPosition); for (var j = 0; j < 5; j++) { - if (j < uniforms.uDirectionalLightCount) { - let lightVector = (uniforms.uViewMatrix * vec4( - uniforms.uLightingDirection[j], + if (j < lighting.uDirectionalLightCount) { + let lightVector = (camera.uViewMatrix * vec4( + lighting.uLightingDirection[j], 0.0 )).xyz; - let lightColor = uniforms.uDirectionalDiffuseColors[j]; - let specularColor = uniforms.uDirectionalSpecularColors[j]; + let lightColor = lighting.uDirectionalDiffuseColors[j]; + let specularColor = lighting.uDirectionalSpecularColors[j]; let result = singleLight(viewDirection, normal, lightVector, shininess, metallic); totalDiffuse += result.diffuse * lightColor; totalSpecular += result.specular * specularColor; } - if (j < uniforms.uPointLightCount) { - let lightPosition = (uniforms.uViewMatrix * vec4( - uniforms.uPointLightLocation[j], + if (j < lighting.uPointLightCount) { + let lightPosition = (camera.uViewMatrix * vec4( + lighting.uPointLightLocation[j], 1.0 )).xyz; let lightVector = modelPosition - lightPosition; let lightDistance = length(lightVector); let lightFalloff = 1.0 / ( - uniforms.uConstantAttenuation + - lightDistance * uniforms.uLinearAttenuation + - lightDistance * lightDistance * uniforms.uQuadraticAttenuation + lighting.uConstantAttenuation + + lightDistance * lighting.uLinearAttenuation + + lightDistance * lightDistance * lighting.uQuadraticAttenuation ); - let lightColor = uniforms.uPointLightDiffuseColors[j] * lightFalloff; - let specularColor = uniforms.uPointLightSpecularColors[j] * lightFalloff; + let lightColor = lighting.uPointLightDiffuseColors[j] * lightFalloff; + let specularColor = lighting.uPointLightSpecularColors[j] * lightFalloff; let result = singleLight(viewDirection, normal, lightVector, shininess, metallic); totalDiffuse += result.diffuse * lightColor; totalSpecular += result.specular * specularColor; } - if (j < uniforms.uSpotLightCount) { - let lightPosition = (uniforms.uViewMatrix * vec4( - uniforms.uSpotLightLocation[j], + if (j < lighting.uSpotLightCount) { + let lightPosition = (camera.uViewMatrix * vec4( + lighting.uSpotLightLocation[j], 1.0 )).xyz; let lightVector = modelPosition - lightPosition; let lightDistance = length(lightVector); var lightFalloff = 1.0 / ( - uniforms.uConstantAttenuation + - lightDistance * uniforms.uLinearAttenuation + - lightDistance * lightDistance * uniforms.uQuadraticAttenuation + lighting.uConstantAttenuation + + lightDistance * lighting.uLinearAttenuation + + lightDistance * lightDistance * lighting.uQuadraticAttenuation ); - let lightDirection = (uniforms.uViewMatrix * vec4( - uniforms.uSpotLightDirection[j], + let lightDirection = (camera.uViewMatrix * vec4( + lighting.uSpotLightDirection[j], 0.0 )).xyz; let spotDot = dot(normalize(lightVector), normalize(lightDirection)); let spotFalloff = select( 0.0, - pow(spotDot, uniforms.uSpotLightConc[j]), - spotDot < uniforms.uSpotLightAngle[j] + pow(spotDot, lighting.uSpotLightConc[j]), + spotDot < lighting.uSpotLightAngle[j] ); lightFalloff *= spotFalloff; - let lightColor = uniforms.uSpotLightDiffuseColors[j]; - let specularColor = uniforms.uSpotLightSpecularColors[j]; + let lightColor = lighting.uSpotLightDiffuseColors[j]; + let specularColor = lighting.uSpotLightSpecularColors[j]; let result = singleLight(viewDirection, normal, lightVector, shininess, metallic); totalDiffuse += result.diffuse * lightColor; totalSpecular += result.specular * specularColor; @@ -357,7 +366,7 @@ fn totalLight( } // Image light contribution - if (uniforms.uUseImageLight != 0) { + if (lighting.uUseImageLight != 0) { totalDiffuse += calculateImageDiffuse(normal, modelPosition, metallic); totalSpecular += calculateImageSpecular(normal, modelPosition, shininess, metallic); } @@ -374,19 +383,19 @@ fn main(input: FragmentInput) -> @location(0) vec4 { let color = select( input.vColor, - textureSample(uSampler, uSampler_sampler, input.vTexCoord) * (uniforms.uTint/255.0), - uniforms.isTexture == 1 + textureSample(uSampler, uSampler_sampler, input.vTexCoord) * (material.uTint/255.0), + material.isTexture == 1 ); // TODO: check isTexture and apply tint var inputs = Inputs( normalize(input.vNormal), input.vTexCoord, - uniforms.uAmbientColor, - select(color.rgb, uniforms.uAmbientMatColor.rgb, uniforms.uHasSetAmbient == 1), - uniforms.uSpecularMatColor.rgb, - uniforms.uEmissiveMatColor.rgb, + material.uAmbientColor, + select(color.rgb, material.uAmbientMatColor.rgb, material.uHasSetAmbient == 1), + material.uSpecularMatColor.rgb, + material.uEmissiveMatColor.rgb, color, - uniforms.uShininess, - uniforms.uMetallic + material.uShininess, + material.uMetallic ); inputs = HOOK_getPixelInputs(inputs); diff --git a/src/webgpu/strands_wgslBackend.js b/src/webgpu/strands_wgslBackend.js index a2e432948c..22ae1d62d7 100644 --- a/src/webgpu/strands_wgslBackend.js +++ b/src/webgpu/strands_wgslBackend.js @@ -196,7 +196,10 @@ export const wgslBackend = { if (!strandsContext.renderer || !strandsContext.baseShader) return; // Get the next available binding index from the renderer - let bindingIndex = strandsContext.renderer.getNextBindingIndex(strandsContext.baseShader); + let bindingIndex = strandsContext.renderer.getNextBindingIndex({ + vert: strandsContext.baseShader.vertSrc(), + frag: strandsContext.baseShader.fragSrc(), + }); for (const {name, typeInfo} of strandsContext.uniforms) { if (typeInfo.baseType === 'sampler2D') { @@ -365,7 +368,7 @@ export const wgslBackend = { // Check if this is a uniform variable (but not a texture) const uniform = generationContext.strandsContext?.uniforms?.find(uniform => uniform.name === node.identifier); if (uniform && uniform.typeInfo.baseType !== 'sampler2D') { - return `uniforms.${node.identifier}`; + return `hooks.${node.identifier}`; } return node.identifier;