From 9b900c033c6f1f978affeaa86e01a2e0615e5cba Mon Sep 17 00:00:00 2001 From: Gregg Tavares Date: Tue, 14 May 2024 16:52:17 +0900 Subject: [PATCH] add examples --- README.md | 2 + examples/stencil-cube.html | 13 ++ examples/stencil-cube.js | 448 +++++++++++++++++++++++++++++++++++++ examples/stencil.html | 13 ++ examples/stencil.js | 382 +++++++++++++++++++++++++++++++ 5 files changed, 858 insertions(+) create mode 100644 examples/stencil-cube.html create mode 100644 examples/stencil-cube.js create mode 100644 examples/stencil.html create mode 100644 examples/stencil.js diff --git a/README.md b/README.md index 8e89a65..c51b3c3 100644 --- a/README.md +++ b/README.md @@ -437,6 +437,8 @@ import { createTextureFromImage } from 'webgpu-utils'; * [cube-map](examples/cube-map.html) * [instancing](examples/instancing.html) * [primitives](examples/primitives.html) +* [stencil](examples/stencil.html) +* [stencil-cube](examples/stencil-cube.html) ## Development diff --git a/examples/stencil-cube.html b/examples/stencil-cube.html new file mode 100644 index 0000000..e8553c0 --- /dev/null +++ b/examples/stencil-cube.html @@ -0,0 +1,13 @@ + + + + webgpu-utils - stencil cube + + + + + +
webgpu-utils - stencil cube
+ + + \ No newline at end of file diff --git a/examples/stencil-cube.js b/examples/stencil-cube.js new file mode 100644 index 0000000..c36fce0 --- /dev/null +++ b/examples/stencil-cube.js @@ -0,0 +1,448 @@ +/* global GPUBufferUsage */ +/* global GPUTextureUsage */ +import { mat4, vec3 } from 'https://wgpu-matrix.org/dist/2.x/wgpu-matrix.module.js'; +import * as wgh from '../dist/1.x/webgpu-utils.module.js'; + +// note: There is nothing special about webgpu-utils with relation to stencils +async function main() { + const adapter = await navigator.gpu?.requestAdapter(); + const device = await adapter?.requestDevice(); + if (!device) { + fail('need a browser that supports WebGPU'); + return; + } + + const canvas = document.querySelector('canvas'); + const context = canvas.getContext('webgpu'); + const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + context.configure({ + device, + format: presentationFormat, + alphaMode: 'premultiplied', + }); + + function facet(arrays) { + const newArrays = wgh.primitives.deindex(arrays); + newArrays.normal = wgh.primitives.generateTriangleNormals(wgh.makeTypedArrayFromArrayUnion(newArrays.position, 'position')); + return newArrays; + } + + const planeVerts = wgh.primitives.createPlaneVertices(); + for (let i = 1; i < planeVerts.position.length; i += 3) { + planeVerts.position[i] = 0.5; + } + const planeGeo = wgh.createBuffersAndAttributesFromArrays(device, planeVerts); + const sphereGeo = wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createSphereVertices()); + const torusGeo = wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createTorusVertices({thickness: 0.5})); + const cubeGeo = wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createCubeVertices()); + const coneGeo = wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createTruncatedConeVertices()); + const cylinderGeo = wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createCylinderVertices()); + const jemGeo = wgh.createBuffersAndAttributesFromArrays(device, facet(wgh.primitives.createSphereVertices({subdivisionsAxis: 6, subdivisionsHeight: 5}))); + const diceGeo = wgh.createBuffersAndAttributesFromArrays(device, facet(wgh.primitives.createTorusVertices({thickness: 0.5, radialSubdivisions: 8, bodySubdivisions: 8}))); + + const code = ` + struct Uniforms { + world: mat4x4f, + color: vec4f, + }; + + struct SharedUniforms { + viewProjection: mat4x4f, + lightDirection: vec3f, + }; + + @group(0) @binding(0) var uni: Uniforms; + @group(0) @binding(1) var sharedUni: SharedUniforms; + + struct MyVSInput { + @location(0) position: vec4f, + @location(1) normal: vec3f, + @location(2) texcoord: vec2f, + }; + + struct MyVSOutput { + @builtin(position) position: vec4f, + @location(0) normal: vec3f, + @location(1) texcoord: vec2f, + }; + + @vertex + fn myVSMain(v: MyVSInput) -> MyVSOutput { + var vsOut: MyVSOutput; + vsOut.position = sharedUni.viewProjection * uni.world * v.position; + vsOut.normal = (uni.world * vec4f(v.normal, 0.0)).xyz; + vsOut.texcoord = v.texcoord; + return vsOut; + } + + @fragment + fn myFSMain(v: MyVSOutput) -> @location(0) vec4f { + let diffuseColor = uni.color; + let a_normal = normalize(v.normal); + let l = dot(a_normal, sharedUni.lightDirection) * 0.5 + 0.5; + return vec4f(diffuseColor.rgb * l, diffuseColor.a); + } + `; + + const module = device.createShaderModule({code}); + const defs = wgh.makeShaderDataDefinitions(code); + const pipelineDesc = { + vertex: { + module, + buffers: [ + ...sphereGeo.bufferLayouts, + ], + }, + fragment: { + module, + targets: [ + {format: presentationFormat}, + ], + }, + primitive: { + topology: 'triangle-list', + cullMode: 'back', + }, + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + stencilFront: { passOp: 'replace' }, + format: 'depth24plus-stencil8', + }, + }; + const descriptors = wgh.makeBindGroupLayoutDescriptors(defs, pipelineDesc); + const bindGroupLayouts = descriptors.map(desc => device.createBindGroupLayout(desc)); + const layout = device.createPipelineLayout({ bindGroupLayouts }); + + pipelineDesc.layout = layout; + const stencilSetPipeline = device.createRenderPipeline(pipelineDesc); + pipelineDesc.depthStencil.stencilFront.passOp = 'keep'; + pipelineDesc.depthStencil.stencilFront.compare = 'equal'; + const stencilMaskPipeline = device.createRenderPipeline(pipelineDesc); + + function r(min, max) { + if (typeof max === 'undefined') { + max = min; + min = 0; + } + return Math.random() * (max - min) + min; + } + + const hsl = (h, s, l) => `hsl(${h * 360 | 0}, ${s * 100}%, ${l * 100 | 0}%)`; + + const cssColorToRGBA8 = (() => { + const canvas = new OffscreenCanvas(1, 1); + const ctx = canvas.getContext('2d', {willReadFrequently: true}); + return cssColor => { + ctx.clearRect(0, 0, 1, 1); + ctx.fillStyle = cssColor; + ctx.fillRect(0, 0, 1, 1); + return Array.from(ctx.getImageData(0, 0, 1, 1).data); + }; + })(); + + const cssColorToRGBA = cssColor => cssColorToRGBA8(cssColor).map(v => v / 255); + const hslToRGBA = (h, s, l) => cssColorToRGBA(hsl(h, s, l)); + + const randElem = arr => arr[r(arr.length) | 0]; + + function makeScene(numInstances, hue, geometries) { + const sharedUniformValues = wgh.makeStructuredView(defs.uniforms.sharedUni); + + const sharedUniformBuffer = device.createBuffer({ + size: sharedUniformValues.arrayBuffer.byteLength, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + const objectInfos = []; + for (let i = 0; i < numInstances; ++i) { + const uniformView = wgh.makeStructuredView(defs.uniforms.uni); + const uniformBuffer = device.createBuffer({ + size: uniformView.arrayBuffer.byteLength, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + uniformView.views.color.set(hslToRGBA(hue + r(0.2), r(0.7, 1), r(0.5, 0.8))); + + const matrix = uniformView.views.world; + const t = vec3.mulScalar(vec3.normalize([r(-1, 1), r(-1, 1), r(-1, 1)]), r(10)); + mat4.translation(t, matrix); + mat4.rotateX(matrix, r(Math.PI * 2), matrix); + mat4.rotateY(matrix, r(Math.PI), matrix); + const s = r(0.25, 1); + mat4.scale(matrix, [s, s, s], matrix); + + device.queue.writeBuffer(uniformBuffer, 0, uniformView.arrayBuffer); + + const bindGroup = device.createBindGroup({ + layout: bindGroupLayouts[0], + entries: [ + { binding: 0, resource: { buffer: uniformBuffer } }, + { binding: 1, resource: { buffer: sharedUniformBuffer } }, + ], + }); + + objectInfos.push({ + uniformView, + uniformBuffer, + bindGroup, + geometry: randElem(geometries), + }); + } + return { + objectInfos, + sharedUniformBuffer, + sharedUniformValues, + }; + } + + const maskScenes = [ + makeScene(1, 0 / 6 + 0.5, [planeGeo]), + makeScene(1, 1 / 6 + 0.5, [planeGeo]), + makeScene(1, 2 / 6 + 0.5, [planeGeo]), + makeScene(1, 3 / 6 + 0.5, [planeGeo]), + makeScene(1, 4 / 6 + 0.5, [planeGeo]), + makeScene(1, 5 / 6 + 0.5, [planeGeo]), + ]; + const scene0 = makeScene(100, 0 / 7, [sphereGeo]); + const scene1 = makeScene(100, 1 / 7, [cubeGeo]); + const scene2 = makeScene(100, 2 / 7, [torusGeo]); + const scene3 = makeScene(100, 3 / 7, [coneGeo]); + const scene4 = makeScene(100, 4 / 7, [cylinderGeo]); + const scene5 = makeScene(100, 5 / 7, [jemGeo]); + const scene6 = makeScene(100, 6 / 7, [diceGeo]); + + let depthTexture; + let canvasTexture; + + function updateMask(time, {objectInfos, sharedUniformBuffer, sharedUniformValues}, rotation) { + const projection = mat4.perspective(30 * Math.PI / 180, canvas.clientWidth / canvas.clientHeight, 0.5, 100); + const eye = [0, 0, 45]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + + const view = mat4.lookAt(eye, target, up); + mat4.multiply(projection, view, sharedUniformValues.views.viewProjection); + + sharedUniformValues.set({ + lightDirection: vec3.normalize([1, 8, 10]), + }); + + device.queue.writeBuffer(sharedUniformBuffer, 0, sharedUniformValues.arrayBuffer); + + objectInfos.forEach(({ + uniformBuffer, + uniformView, + }) => { + const world = uniformView.views.world; + mat4.identity(world); + mat4.rotateX(world, time * 0.25, world); + mat4.rotateY(world, time * 0.25, world); + mat4.rotateX(world, rotation[0] * Math.PI, world); + mat4.rotateZ(world, rotation[2] * Math.PI, world); + mat4.scale(world, [10, 10, 10], world); + device.queue.writeBuffer(uniformBuffer, 0, uniformView.arrayBuffer); + }); + } + + function updateScene0(time, {objectInfos, sharedUniformBuffer, sharedUniformValues}) { + const projection = mat4.perspective(30 * Math.PI / 180, canvas.clientWidth / canvas.clientHeight, 0.5, 100); + const eye = [0, 0, 35]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + + const view = mat4.lookAt(eye, target, up); + mat4.multiply(projection, view, sharedUniformValues.views.viewProjection); + + sharedUniformValues.set({ + lightDirection: vec3.normalize([1, 8, 10]), + }); + + device.queue.writeBuffer(sharedUniformBuffer, 0, sharedUniformValues.arrayBuffer); + + objectInfos.forEach(({ + uniformBuffer, + uniformView, + }, i) => { + const world = uniformView.views.world; + mat4.identity(world); + mat4.translate(world, [0, 0, Math.sin(i * 3.721 + time * 0.1) * 10], world); + mat4.rotateX(world, i * 4.567, world); + mat4.rotateY(world, i * 2.967, world); + mat4.translate(world, [0, 0, Math.sin(i * 9.721 + time * 0.1) * 10], world); + mat4.rotateX(world, time * 0.53 + i, world); + device.queue.writeBuffer(uniformBuffer, 0, uniformView.arrayBuffer); + }); + } + + function updateScene1(time, {objectInfos, sharedUniformBuffer, sharedUniformValues}) { + const projection = mat4.perspective(30 * Math.PI / 180, canvas.clientWidth / canvas.clientHeight, 0.5, 100); + const radius = 35; + const t = time * 0.1; + const eye = [Math.cos(t) * radius, 4, Math.sin(t) * radius]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + + const view = mat4.lookAt(eye, target, up); + mat4.multiply(projection, view, sharedUniformValues.views.viewProjection); + + sharedUniformValues.set({ + lightDirection: vec3.normalize([1, 8, 10]), + }); + + device.queue.writeBuffer(sharedUniformBuffer, 0, sharedUniformValues.arrayBuffer); + + objectInfos.forEach(({ + uniformBuffer, + uniformView, + }, i) => { + const world = uniformView.views.world; + mat4.identity(world); + mat4.translate(world, [0, 0, Math.sin(i * 3.721 + time * 0.1) * 10], world); + mat4.rotateX(world, i * 4.567, world); + mat4.rotateY(world, i * 2.967, world); + mat4.translate(world, [0, 0, Math.sin(i * 9.721 + time * 0.1) * 10], world); + mat4.rotateX(world, time * 1.53 + i, world); + device.queue.writeBuffer(uniformBuffer, 0, uniformView.arrayBuffer); + }); + } + + function drawScene(encoder, renderPassDescriptor, pipeline, scene, stencilRef) { + const { + objectInfos, + } = scene; + + renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView(); + renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView(); + + const pass = encoder.beginRenderPass(renderPassDescriptor); + pass.setPipeline(pipeline); + pass.setStencilReference(stencilRef); + + objectInfos.forEach(({ + bindGroup, + geometry, + }) => { + pass.setBindGroup(0, bindGroup); + pass.setVertexBuffer(0, geometry.buffers[0]); + if (geometry.indexBuffer) { + pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat); + pass.drawIndexed(geometry.numElements); + } else { + pass.draw(geometry.numElements); + } + }); + + pass.end(); + } + + const clearPassDesc = { + colorAttachments: [ + { + // view: undefined, // Assigned later + clearValue: [ 0.2, 0.2, 0.2, 1.0 ], + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + // view: undefined, // Assigned later + depthClearValue: 1, + depthLoadOp: 'clear', + depthStoreOp: 'store', + stencilLoadOp: 'clear', + stencilStoreOp: 'store', + }, + }; + + const loadPassDesc = { + colorAttachments: [ + { + // view: undefined, // Assigned later + loadOp: 'load', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + // view: undefined, // Assigned later + depthClearValue: 1, + depthLoadOp: 'clear', + depthStoreOp: 'store', + stencilLoadOp: 'load', + stencilStoreOp: 'store', + }, + }; + + function render(time) { + time *= 0.001; + + canvasTexture = context.getCurrentTexture(); + // If we don't have a depth texture OR if its size is different + // from the canvasTexture when make a new depth texture + if (!depthTexture || + depthTexture.width !== canvasTexture.width || + depthTexture.height !== canvasTexture.height) { + if (depthTexture) { + depthTexture.destroy(); + } + depthTexture = device.createTexture({ + size: [canvasTexture.width, canvasTexture.height], + format: 'depth24plus-stencil8', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + } + + updateMask(time, maskScenes[0], [ 0 , 0, 0 ]); + updateMask(time, maskScenes[1], [ 1 , 0, 0 ]); + updateMask(time, maskScenes[2], [ 0 , 0, 0.5]); + updateMask(time, maskScenes[3], [ 0 , 0, -0.5]); + updateMask(time, maskScenes[4], [-0.5, 0, 0 ]); + updateMask(time, maskScenes[5], [ 0.5, 0, 0 ]); + + updateScene0(time, scene0); + updateScene1(time, scene1); + updateScene0(time, scene2); + updateScene1(time, scene3); + updateScene0(time, scene4); + updateScene1(time, scene5); + updateScene0(time, scene6); + + const encoder = device.createCommandEncoder(); + drawScene(encoder, clearPassDesc, stencilSetPipeline, maskScenes[0], 1); + drawScene(encoder, loadPassDesc, stencilSetPipeline, maskScenes[1], 2); + drawScene(encoder, loadPassDesc, stencilSetPipeline, maskScenes[2], 3); + drawScene(encoder, loadPassDesc, stencilSetPipeline, maskScenes[3], 4); + drawScene(encoder, loadPassDesc, stencilSetPipeline, maskScenes[4], 5); + drawScene(encoder, loadPassDesc, stencilSetPipeline, maskScenes[5], 6); + drawScene(encoder, loadPassDesc, stencilMaskPipeline, scene0, 0); + drawScene(encoder, loadPassDesc, stencilMaskPipeline, scene1, 1); + drawScene(encoder, loadPassDesc, stencilMaskPipeline, scene2, 2); + drawScene(encoder, loadPassDesc, stencilMaskPipeline, scene3, 3); + drawScene(encoder, loadPassDesc, stencilMaskPipeline, scene4, 4); + drawScene(encoder, loadPassDesc, stencilMaskPipeline, scene5, 5); + drawScene(encoder, loadPassDesc, stencilMaskPipeline, scene6, 6); + device.queue.submit([encoder.finish()]); + + requestAnimationFrame(render); + } + requestAnimationFrame(render); + + const observer = new ResizeObserver(entries => { + for (const entry of entries) { + const canvas = entry.target; + const width = entry.contentBoxSize[0].inlineSize; + const height = entry.contentBoxSize[0].blockSize; + canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D)); + canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D)); + } + }); + observer.observe(canvas); +} + +function fail(msg) { + const elem = document.createElement('p'); + elem.textContent = msg; + elem.style.color = 'red'; + document.body.appendChild(elem); +} + +main(); diff --git a/examples/stencil.html b/examples/stencil.html new file mode 100644 index 0000000..53bfbe7 --- /dev/null +++ b/examples/stencil.html @@ -0,0 +1,13 @@ + + + + webgpu-utils - stencil + + + + + +
webgpu-utils - stencil
+ + + \ No newline at end of file diff --git a/examples/stencil.js b/examples/stencil.js new file mode 100644 index 0000000..cec3dec --- /dev/null +++ b/examples/stencil.js @@ -0,0 +1,382 @@ +/* global GPUBufferUsage */ +/* global GPUTextureUsage */ +import { mat4, vec3 } from 'https://wgpu-matrix.org/dist/2.x/wgpu-matrix.module.js'; +import * as wgh from '../dist/1.x/webgpu-utils.module.js'; + +// note: There is nothing special about webgpu-utils with relation to stencils +async function main() { + const adapter = await navigator.gpu?.requestAdapter(); + const device = await adapter?.requestDevice(); + if (!device) { + fail('need a browser that supports WebGPU'); + return; + } + + const canvas = document.querySelector('canvas'); + const context = canvas.getContext('webgpu'); + const presentationFormat = navigator.gpu.getPreferredCanvasFormat(); + context.configure({ + device, + format: presentationFormat, + alphaMode: 'premultiplied', + }); + + const sphereGeo = wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createSphereVertices()); + const torusGeo = wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createTorusVertices({thickness: 0.5})); + const cubeGeo = wgh.createBuffersAndAttributesFromArrays(device, wgh.primitives.createCubeVertices()); + + const code = ` + struct Uniforms { + world: mat4x4f, + color: vec4f, + }; + + struct SharedUniforms { + viewProjection: mat4x4f, + lightDirection: vec3f, + }; + + @group(0) @binding(0) var uni: Uniforms; + @group(0) @binding(1) var sharedUni: SharedUniforms; + + struct MyVSInput { + @location(0) position: vec4f, + @location(1) normal: vec3f, + @location(2) texcoord: vec2f, + }; + + struct MyVSOutput { + @builtin(position) position: vec4f, + @location(0) normal: vec3f, + @location(1) texcoord: vec2f, + }; + + @vertex + fn myVSMain(v: MyVSInput) -> MyVSOutput { + var vsOut: MyVSOutput; + vsOut.position = sharedUni.viewProjection * uni.world * v.position; + vsOut.normal = (uni.world * vec4f(v.normal, 0.0)).xyz; + vsOut.texcoord = v.texcoord; + return vsOut; + } + + @fragment + fn myFSMain(v: MyVSOutput) -> @location(0) vec4f { + let diffuseColor = uni.color; + let a_normal = normalize(v.normal); + let l = dot(a_normal, sharedUni.lightDirection) * 0.5 + 0.5; + return vec4f(diffuseColor.rgb * l, diffuseColor.a); + } + `; + + const module = device.createShaderModule({code}); + const defs = wgh.makeShaderDataDefinitions(code); + const pipelineDesc = { + vertex: { + module, + buffers: [ + ...sphereGeo.bufferLayouts, + ], + }, + fragment: { + module, + targets: [ + {format: presentationFormat}, + ], + }, + primitive: { + topology: 'triangle-list', + cullMode: 'back', + }, + depthStencil: { + depthWriteEnabled: true, + depthCompare: 'less', + stencilFront: { passOp: 'replace' }, + format: 'depth24plus-stencil8', + }, + }; + const descriptors = wgh.makeBindGroupLayoutDescriptors(defs, pipelineDesc); + const bindGroupLayouts = descriptors.map(desc => device.createBindGroupLayout(desc)); + const layout = device.createPipelineLayout({ bindGroupLayouts }); + + pipelineDesc.layout = layout; + const stencilSetPipeline = device.createRenderPipeline(pipelineDesc); + pipelineDesc.depthStencil.stencilFront.passOp = 'keep'; + pipelineDesc.depthStencil.stencilFront.compare = 'equal'; + const stencilMaskPipeline = device.createRenderPipeline(pipelineDesc); + + function r(min, max) { + if (typeof max === 'undefined') { + max = min; + min = 0; + } + return Math.random() * (max - min) + min; + } + + const randElem = arr => arr[r(arr.length) | 0]; + + function makeScene(numInstances, geometries) { + const sharedUniformValues = wgh.makeStructuredView(defs.uniforms.sharedUni); + + const sharedUniformBuffer = device.createBuffer({ + size: sharedUniformValues.arrayBuffer.byteLength, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + const objectInfos = []; + for (let i = 0; i < numInstances; ++i) { + const uniformView = wgh.makeStructuredView(defs.uniforms.uni); + const uniformBuffer = device.createBuffer({ + size: uniformView.arrayBuffer.byteLength, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + uniformView.views.color.set([r(1), r(1), r(1), 1]); + + const matrix = uniformView.views.world; + const t = vec3.mulScalar(vec3.normalize([r(-1, 1), r(-1, 1), r(-1, 1)]), r(10)); + mat4.translation(t, matrix); + mat4.rotateX(matrix, r(Math.PI * 2), matrix); + mat4.rotateY(matrix, r(Math.PI), matrix); + const s = r(0.25, 1); + mat4.scale(matrix, [s, s, s], matrix); + + device.queue.writeBuffer(uniformBuffer, 0, uniformView.arrayBuffer); + + const bindGroup = device.createBindGroup({ + layout: bindGroupLayouts[0], + entries: [ + { binding: 0, resource: { buffer: uniformBuffer } }, + { binding: 1, resource: { buffer: sharedUniformBuffer } }, + ], + }); + + objectInfos.push({ + uniformView, + uniformBuffer, + bindGroup, + geometry: randElem(geometries), + }); + } + return { + objectInfos, + sharedUniformBuffer, + sharedUniformValues, + }; + } + + const maskScene = makeScene(1, [torusGeo]); + const scene0 = makeScene(100, [sphereGeo]); + const scene1 = makeScene(100, [cubeGeo]); + + let depthTexture; + let canvasTexture; + + function updateMask(time, {objectInfos, sharedUniformBuffer, sharedUniformValues}) { + const projection = mat4.perspective(30 * Math.PI / 180, canvas.clientWidth / canvas.clientHeight, 0.5, 100); + const eye = [0, 0, 45]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + + const view = mat4.lookAt(eye, target, up); + mat4.multiply(projection, view, sharedUniformValues.views.viewProjection); + + sharedUniformValues.set({ + lightDirection: vec3.normalize([1, 8, 10]), + }); + + device.queue.writeBuffer(sharedUniformBuffer, 0, sharedUniformValues.arrayBuffer); + + objectInfos.forEach(({ + uniformBuffer, + uniformView, + }) => { + const world = uniformView.views.world; + mat4.identity(world); + mat4.rotateX(world, time * 0.3, world); + mat4.rotateY(world, time * 0.51, world); + mat4.scale(world, [4, 4, 4], world); + device.queue.writeBuffer(uniformBuffer, 0, uniformView.arrayBuffer); + }); + } + + function updateScene0(time, {objectInfos, sharedUniformBuffer, sharedUniformValues}) { + const projection = mat4.perspective(30 * Math.PI / 180, canvas.clientWidth / canvas.clientHeight, 0.5, 100); + const eye = [0, 0, 35]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + + const view = mat4.lookAt(eye, target, up); + mat4.multiply(projection, view, sharedUniformValues.views.viewProjection); + + sharedUniformValues.set({ + lightDirection: vec3.normalize([1, 8, 10]), + }); + + device.queue.writeBuffer(sharedUniformBuffer, 0, sharedUniformValues.arrayBuffer); + + objectInfos.forEach(({ + uniformBuffer, + uniformView, + }, i) => { + const world = uniformView.views.world; + mat4.identity(world); + mat4.translate(world, [0, 0, Math.sin(i * 3.721 + time * 0.1) * 10], world); + mat4.rotateX(world, i * 4.567, world); + mat4.rotateY(world, i * 2.967, world); + mat4.translate(world, [0, 0, Math.sin(i * 9.721 + time * 0.1) * 10], world); + mat4.rotateX(world, time * 0.53 + i, world); + device.queue.writeBuffer(uniformBuffer, 0, uniformView.arrayBuffer); + }); + } + + function updateScene1(time, {objectInfos, sharedUniformBuffer, sharedUniformValues}) { + const projection = mat4.perspective(30 * Math.PI / 180, canvas.clientWidth / canvas.clientHeight, 0.5, 100); + const radius = 35; + const t = time * 0.1; + const eye = [Math.cos(t) * radius, 4, Math.sin(t) * radius]; + const target = [0, 0, 0]; + const up = [0, 1, 0]; + + const view = mat4.lookAt(eye, target, up); + mat4.multiply(projection, view, sharedUniformValues.views.viewProjection); + + sharedUniformValues.set({ + lightDirection: vec3.normalize([1, 8, 10]), + }); + + device.queue.writeBuffer(sharedUniformBuffer, 0, sharedUniformValues.arrayBuffer); + + objectInfos.forEach(({ + uniformBuffer, + uniformView, + }, i) => { + const world = uniformView.views.world; + mat4.identity(world); + mat4.translate(world, [0, 0, Math.sin(i * 3.721 + time * 0.1) * 10], world); + mat4.rotateX(world, i * 4.567, world); + mat4.rotateY(world, i * 2.967, world); + mat4.translate(world, [0, 0, Math.sin(i * 9.721 + time * 0.1) * 10], world); + mat4.rotateX(world, time * 1.53 + i, world); + device.queue.writeBuffer(uniformBuffer, 0, uniformView.arrayBuffer); + }); + } + + function drawScene(encoder, renderPassDescriptor, pipeline, scene, stencilRef) { + const { + objectInfos, + } = scene; + + renderPassDescriptor.colorAttachments[0].view = canvasTexture.createView(); + renderPassDescriptor.depthStencilAttachment.view = depthTexture.createView(); + + const pass = encoder.beginRenderPass(renderPassDescriptor); + pass.setPipeline(pipeline); + pass.setStencilReference(stencilRef); + + objectInfos.forEach(({ + bindGroup, + geometry, + }) => { + pass.setBindGroup(0, bindGroup); + pass.setVertexBuffer(0, geometry.buffers[0]); + if (geometry.indexBuffer) { + pass.setIndexBuffer(geometry.indexBuffer, geometry.indexFormat); + pass.drawIndexed(geometry.numElements); + } else { + pass.draw(geometry.numElements); + } + }); + + pass.end(); + } + + const clearPassDesc = { + colorAttachments: [ + { + // view: undefined, // Assigned later + clearValue: [ 0.2, 0.2, 0.2, 1.0 ], + loadOp: 'clear', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + // view: undefined, // Assigned later + depthClearValue: 1, + depthLoadOp: 'clear', + depthStoreOp: 'store', + stencilLoadOp: 'clear', + stencilStoreOp: 'store', + }, + }; + + const loadPassDesc = { + colorAttachments: [ + { + // view: undefined, // Assigned later + loadOp: 'load', + storeOp: 'store', + }, + ], + depthStencilAttachment: { + // view: undefined, // Assigned later + depthClearValue: 1, + depthLoadOp: 'clear', + depthStoreOp: 'store', + stencilLoadOp: 'load', + stencilStoreOp: 'store', + }, + }; + + function render(time) { + time *= 0.001; + + canvasTexture = context.getCurrentTexture(); + // If we don't have a depth texture OR if its size is different + // from the canvasTexture when make a new depth texture + if (!depthTexture || + depthTexture.width !== canvasTexture.width || + depthTexture.height !== canvasTexture.height) { + if (depthTexture) { + depthTexture.destroy(); + } + depthTexture = device.createTexture({ + size: [canvasTexture.width, canvasTexture.height], + format: 'depth24plus-stencil8', + usage: GPUTextureUsage.RENDER_ATTACHMENT, + }); + } + + updateMask(time, maskScene); + updateScene0(time, scene0); + updateScene1(time, scene1); + + const encoder = device.createCommandEncoder(); + drawScene(encoder, clearPassDesc, stencilSetPipeline, maskScene, 1); + drawScene(encoder, loadPassDesc, stencilMaskPipeline, scene0, 0); + drawScene(encoder, loadPassDesc, stencilMaskPipeline, scene1, 1); + device.queue.submit([encoder.finish()]); + + requestAnimationFrame(render); + } + requestAnimationFrame(render); + + const observer = new ResizeObserver(entries => { + for (const entry of entries) { + const canvas = entry.target; + const width = entry.contentBoxSize[0].inlineSize; + const height = entry.contentBoxSize[0].blockSize; + canvas.width = Math.max(1, Math.min(width, device.limits.maxTextureDimension2D)); + canvas.height = Math.max(1, Math.min(height, device.limits.maxTextureDimension2D)); + } + }); + observer.observe(canvas); +} + +function fail(msg) { + const elem = document.createElement('p'); + elem.textContent = msg; + elem.style.color = 'red'; + document.body.appendChild(elem); +} + +main();