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/dist/1.x/webgpu-utils.js b/dist/1.x/webgpu-utils.js index d682d3d..3e557b3 100644 --- a/dist/1.x/webgpu-utils.js +++ b/dist/1.x/webgpu-utils.js @@ -1,4 +1,4 @@ -/* webgpu-utils@1.7.1, license MIT */ +/* webgpu-utils@1.7.2, license MIT */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : diff --git a/dist/1.x/webgpu-utils.module.js b/dist/1.x/webgpu-utils.module.js index d9d79b9..e08949e 100644 --- a/dist/1.x/webgpu-utils.module.js +++ b/dist/1.x/webgpu-utils.module.js @@ -1,4 +1,4 @@ -/* webgpu-utils@1.7.1, license MIT */ +/* webgpu-utils@1.7.2, license MIT */ const roundUpToMultipleOf = (v, multiple) => (((v + multiple - 1) / multiple) | 0) * multiple; function keysOf(obj) { return Object.keys(obj); diff --git a/docs/index.html b/docs/index.html index 0247a55..c43c090 100644 --- a/docs/index.html +++ b/docs/index.html @@ -106,6 +106,8 @@

webgpu-utils

  • cube-map
  • instancing
  • primitives
  • +
  • stencil
  • +
  • stencil-cube
  • Development

    git clone https://github.com/greggman/webgpu-utils.git
    cd webgpu-utils
    npm ci
    npm start
    diff --git a/examples/2d-array.html b/examples/2d-array.html index ba09d39..edac047 100644 --- a/examples/2d-array.html +++ b/examples/2d-array.html @@ -8,6 +8,7 @@
    webgpu-utils - 2d-array texture
    + \ No newline at end of file diff --git a/examples/2d-array.js b/examples/2d-array.js index 99a7bd4..9d7cc40 100644 --- a/examples/2d-array.js +++ b/examples/2d-array.js @@ -257,10 +257,10 @@ async function main() { } function fail(msg) { - const elem = document.createElement('p'); - elem.textContent = msg; - elem.style.color = 'red'; - document.body.appendChild(elem); + const elem = document.querySelector('#fail'); + elem.style.display = ''; + elem.children[0].textContent = msg; } + main(); diff --git a/examples/background.html b/examples/background.html index 432301c..4c85988 100644 --- a/examples/background.html +++ b/examples/background.html @@ -7,6 +7,7 @@ + \ No newline at end of file diff --git a/examples/background.js b/examples/background.js index 774f9e5..e840a18 100644 --- a/examples/background.js +++ b/examples/background.js @@ -404,10 +404,10 @@ async function main() { } function fail(msg) { - const elem = document.createElement('p'); - elem.textContent = msg; - elem.style.color = 'red'; - document.body.appendChild(elem); + const elem = document.querySelector('#fail'); + elem.style.display = ''; + elem.children[0].textContent = msg; } + main(); diff --git a/examples/bind-group-layouts.html b/examples/bind-group-layouts.html index d805a6e..57746bd 100644 --- a/examples/bind-group-layouts.html +++ b/examples/bind-group-layouts.html @@ -8,6 +8,7 @@
    webgpu-utils - 2d-array texture
    + \ No newline at end of file diff --git a/examples/bind-group-layouts.js b/examples/bind-group-layouts.js index 92279ac..81a5b4d 100644 --- a/examples/bind-group-layouts.js +++ b/examples/bind-group-layouts.js @@ -241,10 +241,10 @@ async function main() { } function fail(msg) { - const elem = document.createElement('p'); - elem.textContent = msg; - elem.style.color = 'red'; - document.body.appendChild(elem); + const elem = document.querySelector('#fail'); + elem.style.display = ''; + elem.children[0].textContent = msg; } + main(); diff --git a/examples/cube-map.html b/examples/cube-map.html index ad1fdbb..08a9feb 100644 --- a/examples/cube-map.html +++ b/examples/cube-map.html @@ -8,6 +8,7 @@
    webgpu-utils - cube-map
    + \ No newline at end of file diff --git a/examples/cube-map.js b/examples/cube-map.js index 1dbca81..bcaf0e3 100644 --- a/examples/cube-map.js +++ b/examples/cube-map.js @@ -190,10 +190,10 @@ async function main() { } function fail(msg) { - const elem = document.createElement('p'); - elem.textContent = msg; - elem.style.color = 'red'; - document.body.appendChild(elem); + const elem = document.querySelector('#fail'); + elem.style.display = ''; + elem.children[0].textContent = msg; } + main(); diff --git a/examples/cube.html b/examples/cube.html index 04985d8..5d7e5b3 100644 --- a/examples/cube.html +++ b/examples/cube.html @@ -8,6 +8,7 @@
    webgpu-utils - cube
    + \ No newline at end of file diff --git a/examples/cube.js b/examples/cube.js index 98ccee4..82d812e 100644 --- a/examples/cube.js +++ b/examples/cube.js @@ -225,10 +225,10 @@ async function main() { } function fail(msg) { - const elem = document.createElement('p'); - elem.textContent = msg; - elem.style.color = 'red'; - document.body.appendChild(elem); + const elem = document.querySelector('#fail'); + elem.style.display = ''; + elem.children[0].textContent = msg; } + main(); diff --git a/examples/examples.css b/examples/examples.css index 87b52cc..2cc1acb 100644 --- a/examples/examples.css +++ b/examples/examples.css @@ -20,4 +20,16 @@ canvas { width: 100%; top: 1em; left: 0; +} + +#fail { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + color: red; } \ No newline at end of file diff --git a/examples/instancing-size-only.html b/examples/instancing-size-only.html index 4655fee..3dc9132 100644 --- a/examples/instancing-size-only.html +++ b/examples/instancing-size-only.html @@ -8,6 +8,7 @@
    webgpu-utils - instancing (size only)
    + \ No newline at end of file diff --git a/examples/instancing-size-only.js b/examples/instancing-size-only.js index 26e2fd8..576fcd7 100644 --- a/examples/instancing-size-only.js +++ b/examples/instancing-size-only.js @@ -253,10 +253,10 @@ async function main() { } function fail(msg) { - const elem = document.createElement('p'); - elem.textContent = msg; - elem.style.color = 'red'; - document.body.appendChild(elem); + const elem = document.querySelector('#fail'); + elem.style.display = ''; + elem.children[0].textContent = msg; } + main(); diff --git a/examples/instancing.html b/examples/instancing.html index d6bd709..00783bd 100644 --- a/examples/instancing.html +++ b/examples/instancing.html @@ -8,6 +8,7 @@
    webgpu-utils - instancing
    + \ No newline at end of file diff --git a/examples/instancing.js b/examples/instancing.js index 290d042..f408de7 100644 --- a/examples/instancing.js +++ b/examples/instancing.js @@ -251,10 +251,9 @@ async function main() { } function fail(msg) { - const elem = document.createElement('p'); - elem.textContent = msg; - elem.style.color = 'red'; - document.body.appendChild(elem); + const elem = document.querySelector('#fail'); + elem.style.display = ''; + elem.children[0].textContent = msg; } main(); diff --git a/examples/primitives.html b/examples/primitives.html index 5ae854c..d1de102 100644 --- a/examples/primitives.html +++ b/examples/primitives.html @@ -8,6 +8,7 @@
    webgpu-utils - primitives
    + \ No newline at end of file diff --git a/examples/primitives.js b/examples/primitives.js index ecacfda..dff57de 100644 --- a/examples/primitives.js +++ b/examples/primitives.js @@ -271,10 +271,9 @@ async function main() { } function fail(msg) { - const elem = document.createElement('p'); - elem.textContent = msg; - elem.style.color = 'red'; - document.body.appendChild(elem); + const elem = document.querySelector('#fail'); + elem.style.display = ''; + elem.children[0].textContent = msg; } main(); 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(); diff --git a/index.html b/index.html index a27253e..d6f29d8 100644 --- a/index.html +++ b/index.html @@ -1,7 +1,7 @@ @@ -416,6 +416,8 @@

    Examples

  • cube-map
  • instancing
  • primitives
  • +
  • stencil
  • +
  • stencil-cube
  • Development

    git clone https://github.com/greggman/webgpu-utils.git
    diff --git a/package-lock.json b/package-lock.json
    index 046ce09..6533f1a 100644
    --- a/package-lock.json
    +++ b/package-lock.json
    @@ -1,12 +1,12 @@
     {
       "name": "webgpu-utils",
    -  "version": "1.7.1",
    +  "version": "1.7.2",
       "lockfileVersion": 3,
       "requires": true,
       "packages": {
         "": {
           "name": "webgpu-utils",
    -      "version": "1.7.1",
    +      "version": "1.7.2",
           "license": "MIT",
           "devDependencies": {
             "@rollup/plugin-node-resolve": "^15.2.3",
    diff --git a/package.json b/package.json
    index 59c807c..2441ef9 100644
    --- a/package.json
    +++ b/package.json
    @@ -1,6 +1,6 @@
     {
       "name": "webgpu-utils",
    -  "version": "1.7.1",
    +  "version": "1.7.2",
       "description": "webgpu utilities",
       "main": "dist/1.x/webgpu-utils.module.js",
       "module": "dist/1.x/webgpu-utils.module.js",
    diff --git a/test/tests/umd-min-test.html b/test/tests/umd-min-test.html
    index b605ac3..e12b6a1 100644
    --- a/test/tests/umd-min-test.html
    +++ b/test/tests/umd-min-test.html
    @@ -5,6 +5,7 @@
       
       
         
    +    
         
         
         
    diff --git a/test/tests/umd-test.html b/test/tests/umd-test.html
    index 2b2274a..aaa19b9 100644
    --- a/test/tests/umd-test.html
    +++ b/test/tests/umd-test.html
    @@ -5,6 +5,7 @@
       
       
         
    +