From dbca8ed7f947156cb143bfac2584a692363c50f8 Mon Sep 17 00:00:00 2001 From: cmhhelgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:14:45 -0700 Subject: [PATCH 1/7] Squashed commit of the following: commit 8882d406224507d198fc7285cfa842e3ae1397de Author: cmhhelgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Tue Apr 2 19:48:16 2024 -0700 Basic functionality implemented commit 6fee17cf1200d80e390d7dcf84a0101fa104373f Author: cmhhelgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Tue Apr 2 18:40:35 2024 -0700 added uniforms commit 470263b05259498298102952d1624f9e361520af Author: cmhhelgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Tue Apr 2 18:19:31 2024 -0700 Stencil Mask example --- sample/stencilMask/fullscreenQuad.vert.wgsl | 43 ++ sample/stencilMask/index.html | 30 ++ sample/stencilMask/instanced.vert.wgsl | 24 ++ sample/stencilMask/main.ts | 442 ++++++++++++++++++++ sample/stencilMask/meta.ts | 17 + sample/stencilMask/sdf.frag.wgsl | 48 +++ sample/stencilMask/starSDF.frag.wgsl | 0 src/samples.ts | 2 + 8 files changed, 606 insertions(+) create mode 100644 sample/stencilMask/fullscreenQuad.vert.wgsl create mode 100644 sample/stencilMask/index.html create mode 100644 sample/stencilMask/instanced.vert.wgsl create mode 100644 sample/stencilMask/main.ts create mode 100644 sample/stencilMask/meta.ts create mode 100644 sample/stencilMask/sdf.frag.wgsl create mode 100644 sample/stencilMask/starSDF.frag.wgsl diff --git a/sample/stencilMask/fullscreenQuad.vert.wgsl b/sample/stencilMask/fullscreenQuad.vert.wgsl new file mode 100644 index 00000000..97e27417 --- /dev/null +++ b/sample/stencilMask/fullscreenQuad.vert.wgsl @@ -0,0 +1,43 @@ +struct VertexOutput { + @builtin(position) Position : vec4f, + @location(0) fragUV : vec2f, +} + +struct Uniforms { + offset_x: f32, + offset_y: f32, + radius_scale: f32, +} + +@group(0) @binding(0) var uniforms: Uniforms; + +@vertex +fn vertexMain(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { + const pos = array( + vec2( 1.0, 1.0), + vec2(-1.0, -1.0), + vec2(-1.0, 1.0), + vec2( 1.0, 1.0), + vec2( 1.0, -1.0), + vec2(-1.0, -1.0), + ); + + const uv = array( + vec2( 1.0, -1.0), + vec2(-1.0, 1.0), + vec2(-1.0, -1.0), + vec2( 1.0, -1.0), + vec2( 1.0, 1.0), + vec2(-1.0, 1.0), + ); + + var output : VertexOutput; + let mask_offset = vec2f(uniforms.offset_x, uniforms.offset_y); + output.Position = vec4( + pos[VertexIndex] / uniforms.radius_scale + mask_offset, + 0.0, + 1.0 + ); + output.fragUV = uv[VertexIndex]; + return output; +} diff --git a/sample/stencilMask/index.html b/sample/stencilMask/index.html new file mode 100644 index 00000000..6957e09f --- /dev/null +++ b/sample/stencilMask/index.html @@ -0,0 +1,30 @@ + + + + + + webgpu-samples: instancedCube + + + + + + + + diff --git a/sample/stencilMask/instanced.vert.wgsl b/sample/stencilMask/instanced.vert.wgsl new file mode 100644 index 00000000..c1ccb98b --- /dev/null +++ b/sample/stencilMask/instanced.vert.wgsl @@ -0,0 +1,24 @@ +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 new file mode 100644 index 00000000..7ae5e61e --- /dev/null +++ b/sample/stencilMask/main.ts @@ -0,0 +1,442 @@ +import { mat4, vec3 } from 'wgpu-matrix'; + +import { + cubeVertexArray, + cubeVertexSize, + cubeUVOffset, + cubePositionOffset, + cubeVertexCount, +} from '../../meshes/cube'; + +// Stencil Mask Shader +import fullscreenQuadWGSL from './fullscreenQuad.vert.wgsl'; +import sdfWGSL from './sdf.frag.wgsl'; + +// Cube render shader +import instancedVertWGSL from './instanced.vert.wgsl'; +import vertexPositionColorWGSL from '../../shaders/vertexPositionColor.frag.wgsl'; +import { GUI } from 'dat.gui'; + +const canvas = document.querySelector('canvas') as HTMLCanvasElement; +const adapter = await navigator.gpu.requestAdapter(); +const device = await adapter.requestDevice(); + +const context = canvas.getContext('webgpu') as GPUCanvasContext; + +const devicePixelRatio = window.devicePixelRatio; +canvas.width = canvas.clientWidth * devicePixelRatio; +canvas.height = canvas.clientHeight * devicePixelRatio; +const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + +context.configure({ + device, + format: presentationFormat, + alphaMode: 'premultiplied', +}); + +enum SDFEnum { + circle, + triangle, +} + +const settings = { + sdf: 'circle', + invertMask: false, + // Offset mask in x direction + offsetX: 0.0, + // Offset mask in y direction + offsetY: 0.0, + scaleRadius: 2.0, +}; + +// Add pinch to scale mask functionality +canvas.addEventListener('wheel', (e) => { + e.preventDefault(); + const scaleFactor = 0.1; + if (e.deltaY < 0) { + settings.scaleRadius += scaleFactor; + } else { + settings.scaleRadius -= scaleFactor; + } + settings.scaleRadius = Math.max(1.5, Math.min(10.0, settings.scaleRadius)); +}); + +canvas.addEventListener('mousemove', (e) => { + const halfCanvasWidth = canvas.clientWidth / 2; + const halfCanvasHeight = canvas.clientHeight / 2; + settings.offsetX = (e.clientX - halfCanvasWidth) / halfCanvasWidth; + settings.offsetY = (e.clientY - halfCanvasHeight) / halfCanvasHeight; +}); + +const gui = new GUI(); +gui.add(settings, 'sdf', ['circle', 'triangle']); +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. + +const depthTexture = device.createTexture({ + size: [canvas.width, canvas.height], + format: 'depth24plus-stencil8', + usage: GPUTextureUsage.RENDER_ATTACHMENT, +}); + +const maskUniformBuffer = device.createBuffer({ + label: 'StencilMask.uniformBuffer', + // offsetX, offsetY, radius, SDF Enum + size: Float32Array.BYTES_PER_ELEMENT * 4, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, +}); + +const maskUniformBGLayout = device.createBindGroupLayout({ + label: 'StencilMask.bindGroupLayout', + entries: [ + { + binding: 0, + buffer: { + type: 'uniform', + }, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + }, + ], +}); + +const maskUniformBindGroup = device.createBindGroup({ + label: 'StencilMask.bindGroup', + layout: maskUniformBGLayout, + entries: [ + { + binding: 0, + resource: { + buffer: maskUniformBuffer, + }, + }, + ], +}); + +// Create our mask shader, which will only write to the stencil buffer. +const stencilMaskPipeline = device.createRenderPipeline({ + layout: device.createPipelineLayout({ + label: 'StencilMask.pipelineLayout', + bindGroupLayouts: [maskUniformBGLayout], + }), + label: 'StencilMask.renderPipeline', + vertex: { + module: device.createShaderModule({ + label: 'StencilMask.vertexShader', + code: fullscreenQuadWGSL, + }), + }, + fragment: { + module: device.createShaderModule({ + label: 'StencilMask.fragmentShader', + code: sdfWGSL, + }), + targets: [ + { + format: presentationFormat, + // Write mask specifices which channel our shader will write to. + // 0 is effectively equivalent to GPUColorWrite.NONE. + writeMask: 0, + }, + ], + }, + // We will write to our depth/stencil texture, but only with stencil values + 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. + stencilFront: { + // IE, 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 + // This value is set per frame via the a GPURenderPassEncoders setStencilReference() function + passOp: 'replace', + }, + stencilBack: { + compare: 'always', + passOp: 'replace', + }, + stencilWriteMask: 0xff, + }, +}); + +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 + colorAttachments: [ + { + view: undefined, + clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: depthTexture.createView(), + // Depth load and store ops still need to be defined with a 'depth24plus-stencil8' texture + // With just 'stencil8', only the stencil ops are necessary + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + // Clear any extant stencil values within the depth texture. + // If stencilLoadOp is clear, clear it to stencilClearValue. + stencilLoadOp: 'clear', + stencilClearValue: 0, + stencilStoreOp: 'store', + }, +}; + +// Create a vertex buffer from the cube data. +const verticesBuffer = device.createBuffer({ + size: cubeVertexArray.byteLength, + usage: GPUBufferUsage.VERTEX, + mappedAtCreation: true, +}); +new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray); +verticesBuffer.unmap(); + +const createRenderPipeline = (invertMask: boolean) => { + return device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: device.createShaderModule({ + code: instancedVertWGSL, + }), + buffers: [ + { + arrayStride: cubeVertexSize, + attributes: [ + { + // position + shaderLocation: 0, + offset: cubePositionOffset, + format: 'float32x4', + }, + { + // uv + shaderLocation: 1, + offset: cubeUVOffset, + format: 'float32x2', + }, + ], + }, + ], + }, + fragment: { + module: device.createShaderModule({ + code: vertexPositionColorWGSL, + }), + targets: [ + { + format: presentationFormat, + }, + ], + }, + primitive: { + topology: 'triangle-list', + // Backface culling since the cube is solid piece of geometry. + // Faces pointing away from the camera will be occluded by faces + // pointing toward the camera. + cullMode: 'back', + }, + // Enable depth testing so that the fragment closest to the camera + // is rendered in front. + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + format: 'depth24plus-stencil8', + stencilFront: { + compare: invertMask ? 'not-equal' : 'equal', + passOp: 'keep', + failOp: 'zero', + }, + stencilBack: { + compare: invertMask ? 'not-equal' : 'equal', + passOp: 'keep', + failOp: 'zero', + }, + 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, + }, + }, + ], +}); + +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 step = 4.0; + +// Initialize the matrix data for every instance. +let m = 0; +for (let x = 0; x < xCount; x++) { + for (let y = 0; y < yCount; y++) { + modelMatrices[m] = mat4.translation( + vec3.fromValues( + step * (x - xCount / 2 + 0.5), + step * (y - yCount / 2 + 0.5), + 0 + ) + ); + m++; + } +} + +const viewMatrix = mat4.translation(vec3.fromValues(0, 0, -12)); + +const tmpMat4 = mat4.create(); + +// Update the transformation matrix data for each instance. +function updateTransformationMatrix() { + const now = Date.now() / 1000; + + let m = 0, + i = 0; + for (let x = 0; x < xCount; x++) { + for (let y = 0; y < yCount; y++) { + mat4.rotate( + modelMatrices[i], + vec3.fromValues( + Math.sin((x + 0.5) * now), + Math.cos((y + 0.5) * now), + 0 + ), + 1, + tmpMat4 + ); + + mat4.multiply(viewMatrix, tmpMat4, tmpMat4); + mat4.multiply(projectionMatrix, tmpMat4, tmpMat4); + + mvpMatricesData.set(tmpMat4, m); + + i++; + m += matrixFloatCount; + } + } +} + +const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + view: undefined, // Assigned later + + clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 }, + loadOp: 'load', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: depthTexture.createView(), + + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + // Load the stencil values from the previous pass + stencilLoadOp: 'load', + stencilStoreOp: 'store', + }, +}; + +function frame() { + // Update mask position and size + device.queue.writeBuffer( + maskUniformBuffer, + 0, + new Float32Array([ + settings.offsetX, + -settings.offsetY, + settings.scaleRadius, + ]) + ); + // Update mask shape + console.log(SDFEnum[settings.sdf]); + device.queue.writeBuffer( + maskUniformBuffer, + 12, + new Uint32Array([SDFEnum[settings.sdf]]) + ); + // Update the cube matrix data. + updateTransformationMatrix(); + device.queue.writeBuffer( + uniformBuffer, + 0, + mvpMatricesData.buffer, + mvpMatricesData.byteOffset, + mvpMatricesData.byteLength + ); + + // Does nothing but make the compiler happy + stencilMaskPassDescriptor.colorAttachments[0].view = context + .getCurrentTexture() + .createView(); + // Actually used as render target here + renderPassDescriptor.colorAttachments[0].view = context + .getCurrentTexture() + .createView(); + + const commandEncoder = device.createCommandEncoder(); + const stencilPassEncoder = commandEncoder.beginRenderPass( + stencilMaskPassDescriptor + ); + stencilPassEncoder.setPipeline(stencilMaskPipeline); + stencilPassEncoder.setBindGroup(0, maskUniformBindGroup); + // Value that will be placed at pixel in stencil buffer when comparison succeeds + stencilPassEncoder.setStencilReference(1); + stencilPassEncoder.draw(6, 1); + stencilPassEncoder.end(); + + // Cube pass + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + if (settings.invertMask) { + passEncoder.setPipeline(invertMaskPipeline); + } else { + passEncoder.setPipeline(maskPipeline); + } + passEncoder.setBindGroup(0, uniformBindGroup); + passEncoder.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(); + device.queue.submit([commandEncoder.finish()]); + + requestAnimationFrame(frame); +} +requestAnimationFrame(frame); diff --git a/sample/stencilMask/meta.ts b/sample/stencilMask/meta.ts new file mode 100644 index 00000000..ab447964 --- /dev/null +++ b/sample/stencilMask/meta.ts @@ -0,0 +1,17 @@ +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, and the properties of the mask are adjusted by changing the stencil test properties in the render pipeline.', + filename: __DIRNAME__, + sources: [ + // Ts files + { path: 'main.ts' }, + // Stencil Mask shaders + { path: 'fullscreenQuad.vert.wgsl' }, + { path: 'sdf.frag.wgsl' }, + // Instanced Cube verts + { path: '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 new file mode 100644 index 00000000..71be89d9 --- /dev/null +++ b/sample/stencilMask/sdf.frag.wgsl @@ -0,0 +1,48 @@ +fn sdfCircle(p: vec2, r: f32) -> f32 { + return length(p)-r; +} + +fn sdfTriangle(p_temp: vec2, r: f32) -> f32 { + var p = p_temp; + let k = sqrt(3.0); + p.x = abs(p.x) - r; + p.y = p.y + r/k; + if( p.x+k*p.y>0.0 ) { + p = vec2(p.x-k*p.y,-k*p.x-p.y)/2.0; + } + p.x -= clamp( p.x, -2.0*r, 0.0 ); + return -length(p)*sign(p.y); +} + +struct VertexOutput { + @builtin(position) Position: vec4f, + @location(0) v_uv: vec2, +} + +struct Uniforms { + offset_x: f32, + offset_y: f32, + radius_scale: f32, + sdf_id: u32, +} + +@group(0) @binding(0) var uniforms: Uniforms; + +@fragment +fn fragmentMain(input: VertexOutput) -> @location(0) vec4 { + var d = 0.0; + switch uniforms.sdf_id { + case 1: { // Local Flip + d = sdfTriangle(input.v_uv, 1.0); + break; + } + default: { + d = sdfCircle(input.v_uv, 1.0); + } + } + var blue = vec3(0.65, 0.85, 1.0); + if (d > 0.0) { + discard; + } + return vec4(blue, 1.0); +} \ No newline at end of file diff --git a/sample/stencilMask/starSDF.frag.wgsl b/sample/stencilMask/starSDF.frag.wgsl new file mode 100644 index 00000000..e69de29b diff --git a/src/samples.ts b/src/samples.ts index 378bcf49..6f78301a 100644 --- a/src/samples.ts +++ b/src/samples.ts @@ -29,6 +29,7 @@ import samplerParameters from '../sample/samplerParameters/meta'; import shadowMapping from '../sample/shadowMapping/meta'; import skinnedMesh from '../sample/skinnedMesh/meta'; import spookyball from '../sample/spookyball/meta'; +import stencilMask from '../sample/stencilMask/meta'; import textRenderingMsdf from '../sample/textRenderingMsdf/meta'; import texturedCube from '../sample/texturedCube/meta'; import twoCubes from '../sample/twoCubes/meta'; @@ -123,6 +124,7 @@ export const pageCategories: PageCategory[] = [ skinnedMesh, textRenderingMsdf, volumeRenderingTexture3D, + stencilMask, }, }, From bfad0385a42c5aa48e4a328c0a7dfe61369a3646 Mon Sep 17 00:00:00 2001 From: cmhhelgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:14:45 -0700 Subject: [PATCH 2/7] Squashed commit of the following: commit 8882d406224507d198fc7285cfa842e3ae1397de Author: cmhhelgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Tue Apr 2 19:48:16 2024 -0700 Basic functionality implemented commit 6fee17cf1200d80e390d7dcf84a0101fa104373f Author: cmhhelgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Tue Apr 2 18:40:35 2024 -0700 added uniforms commit 470263b05259498298102952d1624f9e361520af Author: cmhhelgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Tue Apr 2 18:19:31 2024 -0700 Stencil Mask example --- sample/stencilMask/fullscreenQuad.vert.wgsl | 43 ++ sample/stencilMask/index.html | 30 ++ sample/stencilMask/instanced.vert.wgsl | 24 ++ sample/stencilMask/main.ts | 442 ++++++++++++++++++++ sample/stencilMask/meta.ts | 17 + sample/stencilMask/sdf.frag.wgsl | 48 +++ sample/stencilMask/starSDF.frag.wgsl | 0 src/samples.ts | 2 + 8 files changed, 606 insertions(+) create mode 100644 sample/stencilMask/fullscreenQuad.vert.wgsl create mode 100644 sample/stencilMask/index.html create mode 100644 sample/stencilMask/instanced.vert.wgsl create mode 100644 sample/stencilMask/main.ts create mode 100644 sample/stencilMask/meta.ts create mode 100644 sample/stencilMask/sdf.frag.wgsl create mode 100644 sample/stencilMask/starSDF.frag.wgsl diff --git a/sample/stencilMask/fullscreenQuad.vert.wgsl b/sample/stencilMask/fullscreenQuad.vert.wgsl new file mode 100644 index 00000000..97e27417 --- /dev/null +++ b/sample/stencilMask/fullscreenQuad.vert.wgsl @@ -0,0 +1,43 @@ +struct VertexOutput { + @builtin(position) Position : vec4f, + @location(0) fragUV : vec2f, +} + +struct Uniforms { + offset_x: f32, + offset_y: f32, + radius_scale: f32, +} + +@group(0) @binding(0) var uniforms: Uniforms; + +@vertex +fn vertexMain(@builtin(vertex_index) VertexIndex : u32) -> VertexOutput { + const pos = array( + vec2( 1.0, 1.0), + vec2(-1.0, -1.0), + vec2(-1.0, 1.0), + vec2( 1.0, 1.0), + vec2( 1.0, -1.0), + vec2(-1.0, -1.0), + ); + + const uv = array( + vec2( 1.0, -1.0), + vec2(-1.0, 1.0), + vec2(-1.0, -1.0), + vec2( 1.0, -1.0), + vec2( 1.0, 1.0), + vec2(-1.0, 1.0), + ); + + var output : VertexOutput; + let mask_offset = vec2f(uniforms.offset_x, uniforms.offset_y); + output.Position = vec4( + pos[VertexIndex] / uniforms.radius_scale + mask_offset, + 0.0, + 1.0 + ); + output.fragUV = uv[VertexIndex]; + return output; +} diff --git a/sample/stencilMask/index.html b/sample/stencilMask/index.html new file mode 100644 index 00000000..6957e09f --- /dev/null +++ b/sample/stencilMask/index.html @@ -0,0 +1,30 @@ + + + + + + webgpu-samples: instancedCube + + + + + + + + diff --git a/sample/stencilMask/instanced.vert.wgsl b/sample/stencilMask/instanced.vert.wgsl new file mode 100644 index 00000000..c1ccb98b --- /dev/null +++ b/sample/stencilMask/instanced.vert.wgsl @@ -0,0 +1,24 @@ +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 new file mode 100644 index 00000000..7ae5e61e --- /dev/null +++ b/sample/stencilMask/main.ts @@ -0,0 +1,442 @@ +import { mat4, vec3 } from 'wgpu-matrix'; + +import { + cubeVertexArray, + cubeVertexSize, + cubeUVOffset, + cubePositionOffset, + cubeVertexCount, +} from '../../meshes/cube'; + +// Stencil Mask Shader +import fullscreenQuadWGSL from './fullscreenQuad.vert.wgsl'; +import sdfWGSL from './sdf.frag.wgsl'; + +// Cube render shader +import instancedVertWGSL from './instanced.vert.wgsl'; +import vertexPositionColorWGSL from '../../shaders/vertexPositionColor.frag.wgsl'; +import { GUI } from 'dat.gui'; + +const canvas = document.querySelector('canvas') as HTMLCanvasElement; +const adapter = await navigator.gpu.requestAdapter(); +const device = await adapter.requestDevice(); + +const context = canvas.getContext('webgpu') as GPUCanvasContext; + +const devicePixelRatio = window.devicePixelRatio; +canvas.width = canvas.clientWidth * devicePixelRatio; +canvas.height = canvas.clientHeight * devicePixelRatio; +const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + +context.configure({ + device, + format: presentationFormat, + alphaMode: 'premultiplied', +}); + +enum SDFEnum { + circle, + triangle, +} + +const settings = { + sdf: 'circle', + invertMask: false, + // Offset mask in x direction + offsetX: 0.0, + // Offset mask in y direction + offsetY: 0.0, + scaleRadius: 2.0, +}; + +// Add pinch to scale mask functionality +canvas.addEventListener('wheel', (e) => { + e.preventDefault(); + const scaleFactor = 0.1; + if (e.deltaY < 0) { + settings.scaleRadius += scaleFactor; + } else { + settings.scaleRadius -= scaleFactor; + } + settings.scaleRadius = Math.max(1.5, Math.min(10.0, settings.scaleRadius)); +}); + +canvas.addEventListener('mousemove', (e) => { + const halfCanvasWidth = canvas.clientWidth / 2; + const halfCanvasHeight = canvas.clientHeight / 2; + settings.offsetX = (e.clientX - halfCanvasWidth) / halfCanvasWidth; + settings.offsetY = (e.clientY - halfCanvasHeight) / halfCanvasHeight; +}); + +const gui = new GUI(); +gui.add(settings, 'sdf', ['circle', 'triangle']); +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. + +const depthTexture = device.createTexture({ + size: [canvas.width, canvas.height], + format: 'depth24plus-stencil8', + usage: GPUTextureUsage.RENDER_ATTACHMENT, +}); + +const maskUniformBuffer = device.createBuffer({ + label: 'StencilMask.uniformBuffer', + // offsetX, offsetY, radius, SDF Enum + size: Float32Array.BYTES_PER_ELEMENT * 4, + usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.UNIFORM, +}); + +const maskUniformBGLayout = device.createBindGroupLayout({ + label: 'StencilMask.bindGroupLayout', + entries: [ + { + binding: 0, + buffer: { + type: 'uniform', + }, + visibility: GPUShaderStage.VERTEX | GPUShaderStage.FRAGMENT, + }, + ], +}); + +const maskUniformBindGroup = device.createBindGroup({ + label: 'StencilMask.bindGroup', + layout: maskUniformBGLayout, + entries: [ + { + binding: 0, + resource: { + buffer: maskUniformBuffer, + }, + }, + ], +}); + +// Create our mask shader, which will only write to the stencil buffer. +const stencilMaskPipeline = device.createRenderPipeline({ + layout: device.createPipelineLayout({ + label: 'StencilMask.pipelineLayout', + bindGroupLayouts: [maskUniformBGLayout], + }), + label: 'StencilMask.renderPipeline', + vertex: { + module: device.createShaderModule({ + label: 'StencilMask.vertexShader', + code: fullscreenQuadWGSL, + }), + }, + fragment: { + module: device.createShaderModule({ + label: 'StencilMask.fragmentShader', + code: sdfWGSL, + }), + targets: [ + { + format: presentationFormat, + // Write mask specifices which channel our shader will write to. + // 0 is effectively equivalent to GPUColorWrite.NONE. + writeMask: 0, + }, + ], + }, + // We will write to our depth/stencil texture, but only with stencil values + 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. + stencilFront: { + // IE, 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 + // This value is set per frame via the a GPURenderPassEncoders setStencilReference() function + passOp: 'replace', + }, + stencilBack: { + compare: 'always', + passOp: 'replace', + }, + stencilWriteMask: 0xff, + }, +}); + +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 + colorAttachments: [ + { + view: undefined, + clearValue: { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }, + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: depthTexture.createView(), + // Depth load and store ops still need to be defined with a 'depth24plus-stencil8' texture + // With just 'stencil8', only the stencil ops are necessary + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + // Clear any extant stencil values within the depth texture. + // If stencilLoadOp is clear, clear it to stencilClearValue. + stencilLoadOp: 'clear', + stencilClearValue: 0, + stencilStoreOp: 'store', + }, +}; + +// Create a vertex buffer from the cube data. +const verticesBuffer = device.createBuffer({ + size: cubeVertexArray.byteLength, + usage: GPUBufferUsage.VERTEX, + mappedAtCreation: true, +}); +new Float32Array(verticesBuffer.getMappedRange()).set(cubeVertexArray); +verticesBuffer.unmap(); + +const createRenderPipeline = (invertMask: boolean) => { + return device.createRenderPipeline({ + layout: 'auto', + vertex: { + module: device.createShaderModule({ + code: instancedVertWGSL, + }), + buffers: [ + { + arrayStride: cubeVertexSize, + attributes: [ + { + // position + shaderLocation: 0, + offset: cubePositionOffset, + format: 'float32x4', + }, + { + // uv + shaderLocation: 1, + offset: cubeUVOffset, + format: 'float32x2', + }, + ], + }, + ], + }, + fragment: { + module: device.createShaderModule({ + code: vertexPositionColorWGSL, + }), + targets: [ + { + format: presentationFormat, + }, + ], + }, + primitive: { + topology: 'triangle-list', + // Backface culling since the cube is solid piece of geometry. + // Faces pointing away from the camera will be occluded by faces + // pointing toward the camera. + cullMode: 'back', + }, + // Enable depth testing so that the fragment closest to the camera + // is rendered in front. + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + format: 'depth24plus-stencil8', + stencilFront: { + compare: invertMask ? 'not-equal' : 'equal', + passOp: 'keep', + failOp: 'zero', + }, + stencilBack: { + compare: invertMask ? 'not-equal' : 'equal', + passOp: 'keep', + failOp: 'zero', + }, + 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, + }, + }, + ], +}); + +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 step = 4.0; + +// Initialize the matrix data for every instance. +let m = 0; +for (let x = 0; x < xCount; x++) { + for (let y = 0; y < yCount; y++) { + modelMatrices[m] = mat4.translation( + vec3.fromValues( + step * (x - xCount / 2 + 0.5), + step * (y - yCount / 2 + 0.5), + 0 + ) + ); + m++; + } +} + +const viewMatrix = mat4.translation(vec3.fromValues(0, 0, -12)); + +const tmpMat4 = mat4.create(); + +// Update the transformation matrix data for each instance. +function updateTransformationMatrix() { + const now = Date.now() / 1000; + + let m = 0, + i = 0; + for (let x = 0; x < xCount; x++) { + for (let y = 0; y < yCount; y++) { + mat4.rotate( + modelMatrices[i], + vec3.fromValues( + Math.sin((x + 0.5) * now), + Math.cos((y + 0.5) * now), + 0 + ), + 1, + tmpMat4 + ); + + mat4.multiply(viewMatrix, tmpMat4, tmpMat4); + mat4.multiply(projectionMatrix, tmpMat4, tmpMat4); + + mvpMatricesData.set(tmpMat4, m); + + i++; + m += matrixFloatCount; + } + } +} + +const renderPassDescriptor: GPURenderPassDescriptor = { + colorAttachments: [ + { + view: undefined, // Assigned later + + clearValue: { r: 0.5, g: 0.5, b: 0.5, a: 1.0 }, + loadOp: 'load', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + view: depthTexture.createView(), + + depthClearValue: 1.0, + depthLoadOp: 'clear', + depthStoreOp: 'store', + // Load the stencil values from the previous pass + stencilLoadOp: 'load', + stencilStoreOp: 'store', + }, +}; + +function frame() { + // Update mask position and size + device.queue.writeBuffer( + maskUniformBuffer, + 0, + new Float32Array([ + settings.offsetX, + -settings.offsetY, + settings.scaleRadius, + ]) + ); + // Update mask shape + console.log(SDFEnum[settings.sdf]); + device.queue.writeBuffer( + maskUniformBuffer, + 12, + new Uint32Array([SDFEnum[settings.sdf]]) + ); + // Update the cube matrix data. + updateTransformationMatrix(); + device.queue.writeBuffer( + uniformBuffer, + 0, + mvpMatricesData.buffer, + mvpMatricesData.byteOffset, + mvpMatricesData.byteLength + ); + + // Does nothing but make the compiler happy + stencilMaskPassDescriptor.colorAttachments[0].view = context + .getCurrentTexture() + .createView(); + // Actually used as render target here + renderPassDescriptor.colorAttachments[0].view = context + .getCurrentTexture() + .createView(); + + const commandEncoder = device.createCommandEncoder(); + const stencilPassEncoder = commandEncoder.beginRenderPass( + stencilMaskPassDescriptor + ); + stencilPassEncoder.setPipeline(stencilMaskPipeline); + stencilPassEncoder.setBindGroup(0, maskUniformBindGroup); + // Value that will be placed at pixel in stencil buffer when comparison succeeds + stencilPassEncoder.setStencilReference(1); + stencilPassEncoder.draw(6, 1); + stencilPassEncoder.end(); + + // Cube pass + const passEncoder = commandEncoder.beginRenderPass(renderPassDescriptor); + if (settings.invertMask) { + passEncoder.setPipeline(invertMaskPipeline); + } else { + passEncoder.setPipeline(maskPipeline); + } + passEncoder.setBindGroup(0, uniformBindGroup); + passEncoder.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(); + device.queue.submit([commandEncoder.finish()]); + + requestAnimationFrame(frame); +} +requestAnimationFrame(frame); diff --git a/sample/stencilMask/meta.ts b/sample/stencilMask/meta.ts new file mode 100644 index 00000000..ab447964 --- /dev/null +++ b/sample/stencilMask/meta.ts @@ -0,0 +1,17 @@ +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, and the properties of the mask are adjusted by changing the stencil test properties in the render pipeline.', + filename: __DIRNAME__, + sources: [ + // Ts files + { path: 'main.ts' }, + // Stencil Mask shaders + { path: 'fullscreenQuad.vert.wgsl' }, + { path: 'sdf.frag.wgsl' }, + // Instanced Cube verts + { path: '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 new file mode 100644 index 00000000..71be89d9 --- /dev/null +++ b/sample/stencilMask/sdf.frag.wgsl @@ -0,0 +1,48 @@ +fn sdfCircle(p: vec2, r: f32) -> f32 { + return length(p)-r; +} + +fn sdfTriangle(p_temp: vec2, r: f32) -> f32 { + var p = p_temp; + let k = sqrt(3.0); + p.x = abs(p.x) - r; + p.y = p.y + r/k; + if( p.x+k*p.y>0.0 ) { + p = vec2(p.x-k*p.y,-k*p.x-p.y)/2.0; + } + p.x -= clamp( p.x, -2.0*r, 0.0 ); + return -length(p)*sign(p.y); +} + +struct VertexOutput { + @builtin(position) Position: vec4f, + @location(0) v_uv: vec2, +} + +struct Uniforms { + offset_x: f32, + offset_y: f32, + radius_scale: f32, + sdf_id: u32, +} + +@group(0) @binding(0) var uniforms: Uniforms; + +@fragment +fn fragmentMain(input: VertexOutput) -> @location(0) vec4 { + var d = 0.0; + switch uniforms.sdf_id { + case 1: { // Local Flip + d = sdfTriangle(input.v_uv, 1.0); + break; + } + default: { + d = sdfCircle(input.v_uv, 1.0); + } + } + var blue = vec3(0.65, 0.85, 1.0); + if (d > 0.0) { + discard; + } + return vec4(blue, 1.0); +} \ No newline at end of file diff --git a/sample/stencilMask/starSDF.frag.wgsl b/sample/stencilMask/starSDF.frag.wgsl new file mode 100644 index 00000000..e69de29b diff --git a/src/samples.ts b/src/samples.ts index 378bcf49..6f78301a 100644 --- a/src/samples.ts +++ b/src/samples.ts @@ -29,6 +29,7 @@ import samplerParameters from '../sample/samplerParameters/meta'; import shadowMapping from '../sample/shadowMapping/meta'; import skinnedMesh from '../sample/skinnedMesh/meta'; import spookyball from '../sample/spookyball/meta'; +import stencilMask from '../sample/stencilMask/meta'; import textRenderingMsdf from '../sample/textRenderingMsdf/meta'; import texturedCube from '../sample/texturedCube/meta'; import twoCubes from '../sample/twoCubes/meta'; @@ -123,6 +124,7 @@ export const pageCategories: PageCategory[] = [ skinnedMesh, textRenderingMsdf, volumeRenderingTexture3D, + stencilMask, }, }, From c12e40ff89b3d8e5d1d9d2ecb314e4eba979f250 Mon Sep 17 00:00:00 2001 From: cmhhelgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:40:33 -0700 Subject: [PATCH 3/7] mouse adjustment --- sample/stencilMask/main.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sample/stencilMask/main.ts b/sample/stencilMask/main.ts index 7ae5e61e..fdf0fbfb 100644 --- a/sample/stencilMask/main.ts +++ b/sample/stencilMask/main.ts @@ -58,14 +58,18 @@ canvas.addEventListener('wheel', (e) => { } else { settings.scaleRadius -= scaleFactor; } - settings.scaleRadius = Math.max(1.5, Math.min(10.0, settings.scaleRadius)); + settings.scaleRadius = Math.max(1.0, Math.min(10.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 - halfCanvasWidth) / halfCanvasWidth; - settings.offsetY = (e.clientY - halfCanvasHeight) / halfCanvasHeight; + settings.offsetX = (e.clientX - 3 * quarterCanvasWidth) / halfCanvasWidth + settings.offsetY = (e.clientY - 3 * quarterCanvasHeight) / halfCanvasHeight; }); const gui = new GUI(); From 6750c544649f8d6550420fe9b1529f1fba302595 Mon Sep 17 00:00:00 2001 From: cmhhelgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:48:02 -0700 Subject: [PATCH 4/7] minor comment addition --- sample/stencilMask/main.ts | 2 ++ sample/stencilMask/meta.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/sample/stencilMask/main.ts b/sample/stencilMask/main.ts index fdf0fbfb..fb9f18b8 100644 --- a/sample/stencilMask/main.ts +++ b/sample/stencilMask/main.ts @@ -255,6 +255,8 @@ const createRenderPipeline = (invertMask: boolean) => { depthCompare: 'less', format: 'depth24plus-stencil8', 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. compare: invertMask ? 'not-equal' : 'equal', passOp: 'keep', failOp: 'zero', diff --git a/sample/stencilMask/meta.ts b/sample/stencilMask/meta.ts index ab447964..5ebd649d 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, and the properties of the mask are adjusted by changing the stencil test properties in the render pipeline.', + "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.", filename: __DIRNAME__, sources: [ // Ts files From a7f9b26dbfea421bae72bf59c404f9186d217615 Mon Sep 17 00:00:00 2001 From: cmhhelgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Tue, 2 Apr 2024 20:52:03 -0700 Subject: [PATCH 5/7] npm run fix --- sample/stencilMask/main.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sample/stencilMask/main.ts b/sample/stencilMask/main.ts index fb9f18b8..61f40a59 100644 --- a/sample/stencilMask/main.ts +++ b/sample/stencilMask/main.ts @@ -64,11 +64,11 @@ canvas.addEventListener('wheel', (e) => { canvas.addEventListener('mousemove', (e) => { const halfCanvasWidth = canvas.clientWidth / 2; const quarterCanvasWidth = canvas.clientWidth / 4; - const halfCanvasHeight = canvas.clientHeight / 2 + const halfCanvasHeight = canvas.clientHeight / 2; const quarterCanvasHeight = canvas.clientHeight / 4; const halfCanvasHeight = canvas.clientHeight / 2; - settings.offsetX = (e.clientX - 3 * quarterCanvasWidth) / halfCanvasWidth + settings.offsetX = (e.clientX - 3 * quarterCanvasWidth) / halfCanvasWidth; settings.offsetY = (e.clientY - 3 * quarterCanvasHeight) / halfCanvasHeight; }); 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 6/7] 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 From 61e2416f4a7b3d9bed3ac0b3f2bf3e8b06aaded0 Mon Sep 17 00:00:00 2001 From: cmhhelgeson <62450112+cmhhelgeson@users.noreply.github.com> Date: Thu, 4 Apr 2024 14:06:51 -0700 Subject: [PATCH 7/7] Changed fullscreenQuad name to positionQuad to better reflect the shader's functionality, edited the meta.ts and index.html naming accordingly --- sample/stencilMask/index.html | 2 +- sample/stencilMask/main.ts | 2 +- sample/stencilMask/meta.ts | 2 +- .../{fullscreenQuad.vert.wgsl => positionQuad.wgsl} | 6 ++++-- 4 files changed, 7 insertions(+), 5 deletions(-) rename sample/stencilMask/{fullscreenQuad.vert.wgsl => positionQuad.wgsl} (88%) diff --git a/sample/stencilMask/index.html b/sample/stencilMask/index.html index 6957e09f..28a283ea 100644 --- a/sample/stencilMask/index.html +++ b/sample/stencilMask/index.html @@ -3,7 +3,7 @@ - webgpu-samples: instancedCube + webgpu-samples: stencilMask