From f1ffdce5ab6a3c99d8d962ebc9515492952f4694 Mon Sep 17 00:00:00 2001 From: cmhhelgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:48:31 -0700 Subject: [PATCH] Removed unused code, modified mask to render into a separate scene, added additional comments, cleaned up description --- sample/stencilMask/fullscreenQuad.vert.wgsl | 9 +- sample/stencilMask/instanced.vert.wgsl | 24 -- sample/stencilMask/main.ts | 285 ++++++++++++++------ sample/stencilMask/meta.ts | 4 +- sample/stencilMask/sdf.frag.wgsl | 25 ++ sample/stencilMask/starSDF.frag.wgsl | 0 6 files changed, 235 insertions(+), 112 deletions(-) delete mode 100644 sample/stencilMask/instanced.vert.wgsl delete mode 100644 sample/stencilMask/starSDF.frag.wgsl diff --git a/sample/stencilMask/fullscreenQuad.vert.wgsl b/sample/stencilMask/fullscreenQuad.vert.wgsl index 97e27417..b2330043 100644 --- a/sample/stencilMask/fullscreenQuad.vert.wgsl +++ b/sample/stencilMask/fullscreenQuad.vert.wgsl @@ -7,6 +7,9 @@ struct Uniforms { offset_x: f32, offset_y: f32, radius_scale: f32, + sdf_id: u32, + scale_to_canvas_x: f32, + scale_to_canvas_y:f32, } @group(0) @binding(0) var uniforms: Uniforms; @@ -33,8 +36,12 @@ fn vertexMain(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { var output : VertexOutput; let mask_offset = vec2f(uniforms.offset_x, uniforms.offset_y); + // scale_to_canvas is effectively a transformation that takes our quad positions from NDC space to canvas space. + let scale_to_canvas = vec2(uniforms.scale_to_canvas_x, uniforms.scale_to_canvas_y); + let posCS = pos[VertexIndex] * scale_to_canvas; + let offsetCS = mask_offset * scale_to_canvas; output.Position = vec4( - pos[VertexIndex] / uniforms.radius_scale + mask_offset, + posCS * uniforms.radius_scale + offsetCS, 0.0, 1.0 ); diff --git a/sample/stencilMask/instanced.vert.wgsl b/sample/stencilMask/instanced.vert.wgsl deleted file mode 100644 index c1ccb98b..00000000 --- a/sample/stencilMask/instanced.vert.wgsl +++ /dev/null @@ -1,24 +0,0 @@ -struct Uniforms { - modelViewProjectionMatrix : array, -} - -@binding(0) @group(0) var uniforms : Uniforms; - -struct VertexOutput { - @builtin(position) Position : vec4f, - @location(0) fragUV : vec2f, - @location(1) fragPosition: vec4f, -} - -@vertex -fn main( - @builtin(instance_index) instanceIdx : u32, - @location(0) position : vec4f, - @location(1) uv : vec2f -) -> VertexOutput { - var output : VertexOutput; - output.Position = uniforms.modelViewProjectionMatrix[instanceIdx] * position; - output.fragUV = uv; - output.fragPosition = 0.5 * (position + vec4(1.0)); - return output; -} diff --git a/sample/stencilMask/main.ts b/sample/stencilMask/main.ts index 61f40a59..692ad04a 100644 --- a/sample/stencilMask/main.ts +++ b/sample/stencilMask/main.ts @@ -13,7 +13,7 @@ import fullscreenQuadWGSL from './fullscreenQuad.vert.wgsl'; import sdfWGSL from './sdf.frag.wgsl'; // Cube render shader -import instancedVertWGSL from './instanced.vert.wgsl'; +import instancedVertWGSL from '../instancedCube/instanced.vert.wgsl'; import vertexPositionColorWGSL from '../../shaders/vertexPositionColor.frag.wgsl'; import { GUI } from 'dat.gui'; @@ -37,6 +37,7 @@ context.configure({ enum SDFEnum { circle, triangle, + coolS, } const settings = { @@ -46,50 +47,47 @@ const settings = { offsetX: 0.0, // Offset mask in y direction offsetY: 0.0, - scaleRadius: 2.0, + scaleRadius: 200.0, }; -// Add pinch to scale mask functionality +// Add wheel to change mask size canvas.addEventListener('wheel', (e) => { e.preventDefault(); - const scaleFactor = 0.1; + const scaleFactor = 2.0; if (e.deltaY < 0) { settings.scaleRadius += scaleFactor; } else { settings.scaleRadius -= scaleFactor; } - settings.scaleRadius = Math.max(1.0, Math.min(10.0, settings.scaleRadius)); + settings.scaleRadius = Math.max(50.0, Math.min(400.0, settings.scaleRadius)); }); canvas.addEventListener('mousemove', (e) => { - const halfCanvasWidth = canvas.clientWidth / 2; - const quarterCanvasWidth = canvas.clientWidth / 4; - const halfCanvasHeight = canvas.clientHeight / 2; - const quarterCanvasHeight = canvas.clientHeight / 4; - - const halfCanvasHeight = canvas.clientHeight / 2; - settings.offsetX = (e.clientX - 3 * quarterCanvasWidth) / halfCanvasWidth; - settings.offsetY = (e.clientY - 3 * quarterCanvasHeight) / halfCanvasHeight; + const halfCanvasWidth = canvas.width / 2; + const halfCanvasHeight = canvas.height / 2; + + settings.offsetX = e.offsetX - halfCanvasWidth; //-width / 2, width / 2 + settings.offsetY = e.offsetY - halfCanvasHeight; //-height / 2, height / 2 }); const gui = new GUI(); -gui.add(settings, 'sdf', ['circle', 'triangle']); +gui.add(settings, 'sdf', ['circle', 'triangle', 'coolS']); gui.add(settings, 'invertMask'); // A good portion of this code is shared with the 'Instanced Cube' sample, but // pay attention to the different ways in which pipelines and renderDescriptors -// are set up to account for the new stencil pass. - +// are set up to account for the stencil component of our depth texture. const depthTexture = device.createTexture({ size: [canvas.width, canvas.height], format: 'depth24plus-stencil8', usage: GPUTextureUsage.RENDER_ATTACHMENT, }); +// Uniforms passed to stencilMask pass, which writes the mask to the depth texture's stencil buffer. const maskUniformBuffer = device.createBuffer({ label: 'StencilMask.uniformBuffer', - // offsetX, offsetY, radius, SDF Enum - size: Float32Array.BYTES_PER_ELEMENT * 4, + // offsetX, offsetY, radius, SDF Enum, scaleToCanvasX, scaleToCanvasY + size: Float32Array.BYTES_PER_ELEMENT * 6, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, }); @@ -129,6 +127,8 @@ const stencilMaskPipeline = device.createRenderPipeline({ vertex: { module: device.createShaderModule({ label: 'StencilMask.vertexShader', + // Not the same as fullscreenTexturedQuad. Rather than rendering a texture to a quad that covers the whole screen + // This shader takes a quad, then positions it within 2D space. code: fullscreenQuadWGSL, }), }, @@ -140,21 +140,22 @@ const stencilMaskPipeline = device.createRenderPipeline({ targets: [ { format: presentationFormat, - // Write mask specifices which channel our shader will write to. - // 0 is effectively equivalent to GPUColorWrite.NONE. + // Write mask specifices which color channel our shader will write to. + // Since we only want to write to the stencil buffer, we set this value to 0, + // indicating that we would not like to write to the color channel. writeMask: 0, }, ], }, - // We will write to our depth/stencil texture, but only with stencil values + // We will write to our depth-stencil texture, but only modify the stencil component. depthStencil: { format: 'depth24plus-stencil8', depthWriteEnabled: false, - // Stencil front and stencil back define state of stencil comparisons for - // front-facing and back-facing primitives respectively. For the sake of - // this example, they are treated the same. + // Stencil front and stencil back define the state of stencil comparisons for + // front-facing and back-facing primitives respectively. For this 2D mask, + // both kinds of primitives can basically be treated the same way. stencilFront: { - // IE, if we write to this pixel in our shader, the stencil test will always succeed + // 'Always': If we write to this pixel in our shader, the stencil test will always succeed // And the corresponding pixel will be written to in the stencil buffer. compare: 'always', // The value at this pixel in the stencil buffer will be replaced by a reference value @@ -170,8 +171,8 @@ const stencilMaskPipeline = device.createRenderPipeline({ }); const stencilMaskPassDescriptor: GPURenderPassDescriptor = { - // Although we are not writing to color in the stencil mask, it's still necessary - // to pass our renderDescriptor a color attachment + // Although we are not writing to color in the stencil mask pass, it's still necessary + // to pass our stencil mask pipeline's renderDescriptor a color attachment. colorAttachments: [ { view: undefined, @@ -187,10 +188,11 @@ const stencilMaskPassDescriptor: GPURenderPassDescriptor = { depthClearValue: 1.0, depthLoadOp: 'clear', depthStoreOp: 'store', - // Clear any extant stencil values within the depth texture. - // If stencilLoadOp is clear, clear it to stencilClearValue. + // Clear any extant stencil values within the depth-stencil texture. + // When stencilLoadOp is set to clear, the values in the stencil buffer are cleared to stencilClearValue. stencilLoadOp: 'clear', stencilClearValue: 0, + // Store the stencil values for the next pass stencilStoreOp: 'store', }, }; @@ -204,11 +206,72 @@ const verticesBuffer = device.createBuffer({ new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray); verticesBuffer.unmap(); -const createRenderPipeline = (invertMask: boolean) => { +// Create instanced cube uniforms +const xCount = 4; +const yCount = 4; +const numInstances = xCount * yCount; +const matrixFloatCount = 16; // 4x4 matrix +const matrixSize = 4 * matrixFloatCount; +const instancedCubeUniformBufferSize = numInstances * matrixSize; + +const instancedCubeBGLayout = device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.VERTEX, + buffer: { + type: 'uniform', + }, + }, + ], +}); + +const uniformBufferInfo: GPUBufferDescriptor = { + size: instancedCubeUniformBufferSize, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, +}; + +const scene0UniformBuffer = device.createBuffer(uniformBufferInfo); +const scene1UniformBuffer = device.createBuffer(uniformBufferInfo); + +const scene0BindGroup = device.createBindGroup({ + layout: instancedCubeBGLayout, + entries: [ + { + binding: 0, + resource: { + buffer: scene0UniformBuffer, + }, + }, + ], +}); + +const scene1BindGroup = device.createBindGroup({ + layout: instancedCubeBGLayout, + entries: [ + { + binding: 0, + resource: { + buffer: scene1UniformBuffer, + }, + }, + ], +}); + +const createRenderPipeline = ( + label: string, + colorWrite: number, + invertMask: boolean +) => { return device.createRenderPipeline({ - layout: 'auto', + label: `${label}.renderPipeline`, + layout: device.createPipelineLayout({ + label: `${label}.pipelineLayout`, + bindGroupLayouts: [instancedCubeBGLayout], + }), vertex: { module: device.createShaderModule({ + label: `${label}.vertexShader`, code: instancedVertWGSL, }), buffers: [ @@ -233,11 +296,14 @@ const createRenderPipeline = (invertMask: boolean) => { }, fragment: { module: device.createShaderModule({ + label: `${label}.fragmentShader`, code: vertexPositionColorWGSL, }), targets: [ { format: presentationFormat, + // Scene0 identified by full color, Scene1 by only blue color + writeMask: colorWrite, }, ], }, @@ -257,55 +323,55 @@ const createRenderPipeline = (invertMask: boolean) => { stencilFront: { // 'Equal': If the current value of the stencil is equal to the reference value set by setStencilReference(), keep the pixel. // 'not-equal': Opposite of equal. + // If the comparison operation returns true, then the pixel will be written to the screen. compare: invertMask ? 'not-equal' : 'equal', + // Pass and fail operation DO NOT determine whether a pixel is written to the color target. + // Rather, it only specifies whether and how the stencil buffer should be modified, in the case that it either passes or fails. + // Since we want our stencil mask to remain the same for both render passes, we maintain the values written to our stencil buffer in + // the stencil mask pass, regardless of whether or not the stencil operation fails. passOp: 'keep', - failOp: 'zero', + failOp: 'keep', }, stencilBack: { compare: invertMask ? 'not-equal' : 'equal', passOp: 'keep', - failOp: 'zero', + failOp: 'keep', }, stencilReadMask: 0xff, }, }); }; -const maskPipeline = createRenderPipeline(false); -const invertMaskPipeline = createRenderPipeline(true); - -const xCount = 4; -const yCount = 4; -const numInstances = xCount * yCount; -const matrixFloatCount = 16; // 4x4 matrix -const matrixSize = 4 * matrixFloatCount; -const uniformBufferSize = numInstances * matrixSize; - -// Allocate a buffer large enough to hold transforms for every -// instance. -const uniformBuffer = device.createBuffer({ - size: uniformBufferSize, - usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, -}); - -const uniformBindGroup = device.createBindGroup({ - layout: maskPipeline.getBindGroupLayout(0), - entries: [ - { - binding: 0, - resource: { - buffer: uniformBuffer, - }, - }, - ], -}); +// Scene 0 and Scene 1 delineated by different cube transforms +// as well as different color outputs +const scene0MaskPipeline = createRenderPipeline( + 'Scene0Mask', + GPUColorWrite.ALL, + false +); +const scene0InverseMaskPipeline = createRenderPipeline( + 'Scene0InverseMask', + GPUColorWrite.ALL, + true +); +const scene1MaskPipeline = createRenderPipeline( + 'Scene0Mask', + GPUColorWrite.BLUE, + false +); +const scene1InverseMaskPipeline = createRenderPipeline( + 'Scene1InverseMask', + GPUColorWrite.BLUE, + true +); const aspect = canvas.width / canvas.height; const projectionMatrix = mat4.perspective((2 * Math.PI) / 5, aspect, 1, 100.0); type Mat4 = mat4.default; const modelMatrices = new Array(numInstances); -const mvpMatricesData = new Float32Array(matrixFloatCount * numInstances); +const mvpMatricesDataScene0 = new Float32Array(matrixFloatCount * numInstances); +const mvpMatricesDataScene1 = new Float32Array(matrixFloatCount * numInstances); const step = 4.0; @@ -326,31 +392,52 @@ for (let x = 0; x < xCount; x++) { const viewMatrix = mat4.translation(vec3.fromValues(0, 0, -12)); -const tmpMat4 = mat4.create(); +const tmpMat4Scene0 = mat4.create(); +const tmpMat4Scene1 = mat4.create(); // Update the transformation matrix data for each instance. function updateTransformationMatrix() { - const now = Date.now() / 1000; + const nowScene0 = Date.now() / 1000; + const nowScene1 = nowScene0 + 100.0; let m = 0, i = 0; for (let x = 0; x < xCount; x++) { for (let y = 0; y < yCount; y++) { + // Update matrices for cubes in scene 0 + mat4.rotate( + modelMatrices[i], + vec3.fromValues( + Math.sin((x + 0.5) * nowScene0), + Math.cos((y + 0.5) * nowScene0), + 0 + ), + 1, + tmpMat4Scene0 + ); + // Update matrices for cubes in scene 1 mat4.rotate( modelMatrices[i], vec3.fromValues( - Math.sin((x + 0.5) * now), - Math.cos((y + 0.5) * now), + Math.sin((x + 0.5) * nowScene1), + Math.cos((y + 0.5) * nowScene1), 0 ), 1, - tmpMat4 + tmpMat4Scene1 ); - mat4.multiply(viewMatrix, tmpMat4, tmpMat4); - mat4.multiply(projectionMatrix, tmpMat4, tmpMat4); + mat4.scale(tmpMat4Scene1, vec3.create(1, 1, 1), tmpMat4Scene1); + // Update with view and proj in both scenes + // Scene 0 + mat4.multiply(viewMatrix, tmpMat4Scene0, tmpMat4Scene0); + mat4.multiply(projectionMatrix, tmpMat4Scene0, tmpMat4Scene0); + // Scene 1 + mat4.multiply(viewMatrix, tmpMat4Scene1, tmpMat4Scene1); + mat4.multiply(projectionMatrix, tmpMat4Scene1, tmpMat4Scene1); - mvpMatricesData.set(tmpMat4, m); + mvpMatricesDataScene0.set(tmpMat4Scene0, m); + mvpMatricesDataScene1.set(tmpMat4Scene1, m); i++; m += matrixFloatCount; @@ -380,6 +467,12 @@ const renderPassDescriptor: GPURenderPassDescriptor = { }, }; +device.queue.writeBuffer( + maskUniformBuffer, + 16, + new Float32Array([1 / (canvas.width * 0.5), 1 / (canvas.height * 0.5)]) +); + function frame() { // Update mask position and size device.queue.writeBuffer( @@ -392,20 +485,26 @@ function frame() { ]) ); // Update mask shape - console.log(SDFEnum[settings.sdf]); device.queue.writeBuffer( maskUniformBuffer, 12, new Uint32Array([SDFEnum[settings.sdf]]) ); - // Update the cube matrix data. + // Update the cube matrix data in both scenes updateTransformationMatrix(); device.queue.writeBuffer( - uniformBuffer, + scene0UniformBuffer, 0, - mvpMatricesData.buffer, - mvpMatricesData.byteOffset, - mvpMatricesData.byteLength + mvpMatricesDataScene0.buffer, + mvpMatricesDataScene0.byteOffset, + mvpMatricesDataScene0.byteLength + ); + device.queue.writeBuffer( + scene1UniformBuffer, + 0, + mvpMatricesDataScene1.buffer, + mvpMatricesDataScene1.byteOffset, + mvpMatricesDataScene1.byteLength ); // Does nothing but make the compiler happy @@ -428,19 +527,35 @@ function frame() { stencilPassEncoder.draw(6, 1); stencilPassEncoder.end(); - // Cube pass - const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + // Render Scene 0 + const scene0PassEncoder = + commandEncoder.beginRenderPass(renderPassDescriptor); if (settings.invertMask) { - passEncoder.setPipeline(invertMaskPipeline); + scene0PassEncoder.setPipeline(scene0InverseMaskPipeline); } else { - passEncoder.setPipeline(maskPipeline); + scene0PassEncoder.setPipeline(scene0MaskPipeline); } - passEncoder.setBindGroup(0, uniformBindGroup); - passEncoder.setVertexBuffer(0, verticesBuffer); + scene0PassEncoder.setBindGroup(0, scene0BindGroup); + scene0PassEncoder.setVertexBuffer(0, verticesBuffer); // Keep stencil buffer value if cube intersects with areas where stencil buffer equals 1. - passEncoder.setStencilReference(1); - passEncoder.draw(cubeVertexCount, numInstances, 0, 0); - passEncoder.end(); + scene0PassEncoder.setStencilReference(1); + scene0PassEncoder.draw(cubeVertexCount, numInstances, 0, 0); + scene0PassEncoder.end(); + + // Render Scene 1 + const scene1PassEncoder = + commandEncoder.beginRenderPass(renderPassDescriptor); + // Scene 1 will render inside the area opposite to what scene 0 is rendering + if (settings.invertMask) { + scene1PassEncoder.setPipeline(scene1MaskPipeline); + } else { + scene1PassEncoder.setPipeline(scene1InverseMaskPipeline); + } + scene1PassEncoder.setBindGroup(0, scene1BindGroup); + scene1PassEncoder.setVertexBuffer(0, verticesBuffer); + scene1PassEncoder.setStencilReference(1); + scene1PassEncoder.draw(cubeVertexCount, numInstances, 0, 0); + scene1PassEncoder.end(); device.queue.submit([commandEncoder.finish()]); requestAnimationFrame(frame); diff --git a/sample/stencilMask/meta.ts b/sample/stencilMask/meta.ts index 5ebd649d..eeb81e63 100644 --- a/sample/stencilMask/meta.ts +++ b/sample/stencilMask/meta.ts @@ -1,7 +1,7 @@ export default { name: 'Stencil Mask', description: - "This example demonstrates how to use the stencil buffer to create a simple mask over a scene. The mask itself is rendered using an SDF, with options to invert the mask or change the mask's shape. The position and scale of the mask can also be adjusted via the mouse and scroll wheel respectively.", + "This example demonstrates how to use the stencil buffer to create a dynamic mask over a scene, allowing for the selective rendering of two different instances of the 'Instanced Cube' scene. Each instance of the scene is distinguished by different uniform and color properties. The mask is crafted using a Signed Distance Field (SDF), offering options to invert the mask or switch between shapes like circles and triangles. Users can interactively adjust the mask's position and scale with the mouse and the scroll wheel.", filename: __DIRNAME__, sources: [ // Ts files @@ -10,7 +10,7 @@ export default { { path: 'fullscreenQuad.vert.wgsl' }, { path: 'sdf.frag.wgsl' }, // Instanced Cube verts - { path: 'instanced.vert.wgsl' }, + { path: '../instancedCube/instanced.vert.wgsl' }, { path: '../../shaders/vertexPositionColor.frag.wgsl' }, { path: '../../meshes/cube.ts' }, ], diff --git a/sample/stencilMask/sdf.frag.wgsl b/sample/stencilMask/sdf.frag.wgsl index 71be89d9..48d96db4 100644 --- a/sample/stencilMask/sdf.frag.wgsl +++ b/sample/stencilMask/sdf.frag.wgsl @@ -1,3 +1,4 @@ +// SDFs taken from https://iquilezles.org/articles/distfunctions2d/ fn sdfCircle(p: vec2, r: f32) -> f32 { return length(p)-r; } @@ -14,6 +15,26 @@ fn sdfTriangle(p_temp: vec2, r: f32) -> f32 { return -length(p)*sign(p.y); } +fn dot2(v: vec2) -> f32 { + return dot(v, v); +} + +fn sdfCoolS(p_temp: vec2) -> f32 { + var p = p_temp; + var six: f32 = select(-p.x, p.x, p.y < 0.0); + p.x = abs(p.x); + p.y = abs(p.y) - 0.2; + var rex: f32 = p.x - min(round(p.x / 0.4), 0.4); + var aby: f32 = abs(p.y - 0.2) - 0.6; + + var d: f32 = dot2(vec2(six, -p.y) - clamp(0.5 * (six - p.y), 0.0, 0.2)); + d = min(d, dot2(vec2(p.x, -aby) - clamp(0.5 * (p.x - aby), 0.0, 0.4))); + d = min(d, dot2(vec2(rex, p.y - clamp(p.y, 0.0, 0.4)))); + + var s: f32 = 2.0 * p.x + aby + abs(aby + 0.4) - 0.4; + return sqrt(d) * sign(s); +} + struct VertexOutput { @builtin(position) Position: vec4f, @location(0) v_uv: vec2, @@ -36,6 +57,10 @@ fn fragmentMain(input: VertexOutput) -> @location(0) vec4 { d = sdfTriangle(input.v_uv, 1.0); break; } + case 2: { + d = sdfCoolS(input.v_uv); + break; + } default: { d = sdfCircle(input.v_uv, 1.0); } diff --git a/sample/stencilMask/starSDF.frag.wgsl b/sample/stencilMask/starSDF.frag.wgsl deleted file mode 100644 index e69de29b..00000000