Search code examples
maskstencil-bufferwebgpu

Webgpu text rendering with 2d clipping mask


I am new to WebGPU, and I am trying to combine those two examples and put the text behind a mask: WebGPU Text Rendering and How to create a 2d clipping mask in webgpu? This is what I have tried so far, and the text is not masked or completely invisible:

<canvas></canvas>

<script type="module">
  // WebGPU Simple Textured Quad - Import Canvas
  // from https://webgpufundamentals.org/webgpu/webgpu-simple-textured-quad-import-canvas.html


  import { mat4 } from 'https://webgpufundamentals.org/3rdparty/wgpu-matrix.module.js';

  const glyphWidth = 32;
  const glyphHeight = 40;
  const glyphsAcrossTexture = 16;
  function genreateGlyphTextureAtlas() {
    const ctx = document.createElement('canvas').getContext('2d');
    ctx.canvas.width = 512;
    ctx.canvas.height = 256;

    let x = 0;
    let y = 0;
    ctx.font = '32px monospace';
    ctx.textBaseline = 'middle';
    ctx.textAlign = 'center';
    ctx.fillStyle = 'white';
    for (let c = 33; c < 128; ++c) {
      ctx.fillText(String.fromCodePoint(c), x + glyphWidth / 2, y + glyphHeight / 2);
      x += glyphWidth;
      if (x >= ctx.canvas.width) {
        x = 0;
        y += glyphHeight;
      }
    }

    return ctx.canvas;
  }

  async function main() {
    const adapter = await navigator.gpu?.requestAdapter();
    const device = await adapter?.requestDevice();
    if (!device) {
      fail('need a browser that supports WebGPU');
      return;
    }

    // Get a WebGPU context from the canvas and configure it
    const canvas = document.querySelector('canvas');
    const context = canvas.getContext('webgpu');
    const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
    context.configure({
      device,
      format: presentationFormat,
    });






    const module2 = device.createShaderModule({
      code:
        `struct VSIn {
  @location(0) pos: vec4f,
  };

  struct VSOut {
    @builtin(position) pos: vec4f,
  };

  @vertex fn vs(vsIn: VSIn) -> VSOut {
    var vsOut: VSOut;
    vsOut.pos = vsIn.pos;
    return vsOut;
  }

  @fragment fn fs(vin: VSOut) -> @location(0) vec4f {
    return vec4f(1, 0, 0, 1);
  }`});

    const maskMakingPipeline = device.createRenderPipeline({
      label: 'pipeline for rendering the mask',
      layout: 'auto',
      vertex: {
        module: module2,
        entryPoint: 'vs',
        buffers: [
          // position
          {
            arrayStride: 2 * 4, // 2 floats, 4 bytes each
            attributes: [
              { shaderLocation: 0, offset: 0, format: 'float32x2' },
            ],
          },
        ],
      },
      fragment: {
        module: module2,
        entryPoint: 'fs',
        targets: [],
      },
      // replace the stencil value when we draw
      depthStencil: {
        format: 'stencil8',
        depthCompare: 'always',
        depthWriteEnabled: false,
        stencilFront: {
          passOp: 'replace',
        },
      },
    });









    const module = device.createShaderModule({
      label: 'our hardcoded textured quad shaders',
      code: `
      struct VSInput {
        @location(0) position: vec4f,
        @location(1) texcoord: vec2f,
        @location(2) color: vec4f,
      };

      struct VSOutput {
        @builtin(position) position: vec4f,
        @location(0) texcoord: vec2f,
        @location(1) color: vec4f,
      };

      struct Uniforms {
        matrix: mat4x4f,
      };

      @group(0) @binding(2) var<uniform> uni: Uniforms;

      @vertex fn vs(vin: VSInput) -> VSOutput {
        var vsOutput: VSOutput;
        vsOutput.position = uni.matrix * vin.position;
        vsOutput.texcoord = vin.texcoord;
        vsOutput.color = vin.color;
        return vsOutput;
      }

      @group(0) @binding(0) var ourSampler: sampler;
      @group(0) @binding(1) var ourTexture: texture_2d<f32>;

      @fragment fn fs(fsInput: VSOutput) -> @location(0) vec4f {
        return textureSample(ourTexture, ourSampler, fsInput.texcoord) * fsInput.color;
      }

    `,
    });

    const glyphCanvas = genreateGlyphTextureAtlas();
    // so we can see it
    document.body.appendChild(glyphCanvas);
    glyphCanvas.style.backgroundColor = '#222';

    const maxGlyphs = 100;
    const floatsPerVertex = 2 + 2 + 4; // 2(pos) + 2(texcoord) + 4(color)
    const vertexSize = floatsPerVertex * 4; // 4 bytes each float
    const vertsPerGlyph = 6;
    const vertexBufferSize = maxGlyphs * vertsPerGlyph * vertexSize;
    const vertexBuffer = device.createBuffer({
      label: 'vertices',
      size: vertexBufferSize,
      usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    });
    const indexBuffer = device.createBuffer({
      label: 'indices',
      size: maxGlyphs * vertsPerGlyph * 4,
      usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
    });
    // pre fill index buffer with quad indices
    {
      const indices = [];
      for (let i = 0; i < maxGlyphs; ++i) {
        const ndx = i * 4;
        indices.push(ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3);
      }
      device.queue.writeBuffer(indexBuffer, 0, new Uint32Array(indices));
    }

    function generateGlyphVerticesForText(s, colors = [[1, 1, 1, 1]]) {
      const vertexData = new Float32Array(maxGlyphs * floatsPerVertex * vertsPerGlyph);
      const glyphUVWidth = glyphWidth / glyphCanvas.width;
      const glyphUVheight = glyphHeight / glyphCanvas.height;
      let offset = 0;
      let x0 = 0;
      let x1 = 1;
      let y0 = 0;
      let y1 = 1;
      let width = 0;

      const addVertex = (x, y, u, v, r, g, b, a) => {
        vertexData[offset++] = x;
        vertexData[offset++] = y;
        vertexData[offset++] = u;
        vertexData[offset++] = v;
        vertexData[offset++] = r;
        vertexData[offset++] = g;
        vertexData[offset++] = b;
        vertexData[offset++] = a;
      };

      const spacing = 0.55;
      let colorNdx = 0;
      for (let i = 0; i < s.length; ++i) {
        // convert char code to texcoords for glyph texture
        const c = s.charCodeAt(i);
        if (c >= 33) {
          const cNdx = c - 33;
          const glyphX = cNdx % glyphsAcrossTexture;
          const glyphY = Math.floor(cNdx / glyphsAcrossTexture);
          const u0 = (glyphX * glyphWidth) / glyphCanvas.width;
          const v1 = (glyphY * glyphHeight) / glyphCanvas.height;
          const u1 = u0 + glyphUVWidth;
          const v0 = v1 + glyphUVheight;
          width = Math.max(x1, width);

          addVertex(x0, y0, u0, v0, ...colors[colorNdx]);
          addVertex(x1, y0, u1, v0, ...colors[colorNdx]);
          addVertex(x0, y1, u0, v1, ...colors[colorNdx]);
          addVertex(x1, y1, u1, v1, ...colors[colorNdx]);
        } else {
          colorNdx = (colorNdx + 1) % colors.length;
          if (c === 10) {
            x0 = 0;
            x1 = 1;
            y0 = y0 - 1;
            y1 = y0 + 1;
            continue;
          }
        }
        x0 = x0 + spacing;
        x1 = x0 + 1;
      }

      return {
        vertexData,
        numGlyphs: offset / floatsPerVertex,
        width,
        height: y1,
      };
    }

    const { vertexData, numGlyphs, width, height } = generateGlyphVerticesForText(
      'Hello\nworld!\nText in\nWebGPU!', [
      [1, 1, 0, 1],
      [0, 1, 1, 1],
      [1, 0, 1, 1],
      [1, 0, 0, 1],
      [0, .5, 1, 1],
    ]);
    device.queue.writeBuffer(vertexBuffer, 0, vertexData);

    const pipeline = device.createRenderPipeline({
      label: 'hardcoded textured quad pipeline',
      layout: 'auto',
      vertex: {
        module,
        entryPoint: 'vs',
        buffers: [
          {
            arrayStride: vertexSize,
            attributes: [
              { shaderLocation: 0, offset: 0, format: 'float32x2' },  // position
              { shaderLocation: 1, offset: 8, format: 'float32x2' },  // texcoord
              { shaderLocation: 2, offset: 16, format: 'float32x4' },  // color
            ],
          },
        ],
      },
      depthStencil: {
        depthCompare: 'always',
        depthWriteEnabled: false,
        format: 'stencil8',
        stencilFront: {
          compare: 'equal',
        },
      },
      fragment: {
        module,
        entryPoint: 'fs',
        targets: [
          {
            format: presentationFormat,
            blend: {
              color: {
                srcFactor: 'one',
                dstFactor: 'one-minus-src-alpha',
                operation: 'add',
              },
              alpha: {
                srcFactor: 'one',
                dstFactor: 'one-minus-src-alpha',
                operation: 'add',
              },
            },
          },
        ],
      },
    });

    function copySourceToTexture(device, texture, source, { flipY } = {}) {
      device.queue.copyExternalImageToTexture(
        { source, flipY, },
        { texture, premultipliedAlpha: true },
        { width: source.width, height: source.height },
      );
    }

    function createTextureFromSource(device, source, options = {}) {
      const texture = device.createTexture({
        format: 'rgba8unorm',
        size: [source.width, source.height],
        usage: GPUTextureUsage.TEXTURE_BINDING |
          GPUTextureUsage.COPY_DST |
          GPUTextureUsage.RENDER_ATTACHMENT,
      });
      copySourceToTexture(device, texture, source, options);
      return texture;
    }

    const texture = createTextureFromSource(device, glyphCanvas, { mips: true });
    const sampler = device.createSampler({
      minFilter: 'linear',
      magFilter: 'linear',
    });

    // create a buffer for the uniform values
    const uniformBufferSize =
      16 * 4; // matrix is 16 32bit floats (4bytes each)
    const uniformBuffer = device.createBuffer({
      label: 'uniforms for quad',
      size: uniformBufferSize,
      usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
    });

    // create a typedarray to hold the values for the uniforms in JavaScript
    const kMatrixOffset = 0;
    const uniformValues = new Float32Array(uniformBufferSize / 4);
    const matrix = uniformValues.subarray(kMatrixOffset, 16);

    const bindGroup = device.createBindGroup({
      layout: pipeline.getBindGroupLayout(0),
      entries: [
        { binding: 0, resource: sampler },
        { binding: 1, resource: texture.createView() },
        { binding: 2, resource: { buffer: uniformBuffer } },
      ],
    });




    const maskVerts = new Float32Array([-1, -1, 1, -1, 1, 1]);
    const maskVertexBuffer = device.createBuffer({
      size: maskVerts.byteLength,
      usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
    });
    const stencilTexture = device.createTexture({
      format: 'stencil8',
      size: [canvas.width, canvas.height],
      usage: GPUTextureUsage.RENDER_ATTACHMENT,
    });


    const renderPassDescriptor = {
      label: 'our basic canvas renderPass',
      /*colorAttachments: [
        {
          // view: <- to be filled out when we render
          clearValue: [0.3, 0.3, 0.3, 1],
          loadOp: 'clear',
          storeOp: 'store',
        },
      ],*/
      colorAttachments: [{
        view: context.getCurrentTexture().createView(),
        clearValue: [0, 0, 0, 1],
        loadOp: 'clear',
        storeOp: 'store',
      }],
      depthStencilAttachment: {
        view: stencilTexture.createView(),
        stencilLoadOp: 'load',
        stencilStoreOp: 'store',
      }
    };










    function render(time) {
      time *= 0.001;

      const fov = 60 * Math.PI / 180;  // 60 degrees in radians
      const aspect = canvas.clientWidth / canvas.clientHeight;
      const zNear = 0.001;
      const zFar = 50;
      const projectionMatrix = mat4.perspective(fov, aspect, zNear, zFar);

      const cameraPosition = [0, 0, 5];
      const up = [0, 1, 0];
      const target = [0, 0, 0];
      const viewMatrix = mat4.lookAt(cameraPosition, target, up);
      const viewProjectionMatrix = mat4.multiply(projectionMatrix, viewMatrix);

      // Get the current texture from the canvas context and
      // set it as the texture to render to.
      renderPassDescriptor.colorAttachments[0].view =
        context.getCurrentTexture().createView();

      const encoder = device.createCommandEncoder({
        label: 'render quad encoder',
      });



      {
        const pass = encoder.beginRenderPass({
          colorAttachments: [],
          depthStencilAttachment: {
            view: stencilTexture.createView(),
            stencilClearValue: 0,
            stencilLoadOp: 'clear',
            stencilStoreOp: 'store',
          }
        });
        // draw the mask
        pass.setPipeline(maskMakingPipeline);
        pass.setVertexBuffer(0, maskVertexBuffer);
        pass.setStencilReference(1);
        pass.draw(3);
        pass.end();
      }


      {
        const pass = encoder.beginRenderPass(renderPassDescriptor);
        pass.setPipeline(pipeline);

        mat4.rotateY(viewProjectionMatrix, 0, matrix);
        mat4.translate(matrix, [-width / 2, -height / 2, 0], matrix);

        // copy the values from JavaScript to the GPU
        device.queue.writeBuffer(uniformBuffer, 0, uniformValues);

        pass.setBindGroup(0, bindGroup);
        pass.setVertexBuffer(0, vertexBuffer);
        pass.setIndexBuffer(indexBuffer, 'uint32');
        //pass.setStencilReference(1);
        pass.drawIndexed(numGlyphs * 6);

        pass.end();
      }

      const commandBuffer = encoder.finish();
      device.queue.submit([commandBuffer]);

      requestAnimationFrame(render);
    }
    requestAnimationFrame(render);
  }

  function fail(msg) {
    // eslint-disable-next-line no-alert
    alert(msg);
  }

  main();
</script>

If I uncomment pass.setStencilReference(1); for the text pipeline, the text disappears


Solution

  • It's because you didn't put your mask vertex data into the mask vertex buffer. Adding this line

            device.queue.writeBuffer(maskVertexBuffer, 0, maskVerts);
    

    fixed it.

    When I run into this kind of issue in my own code, which happens too often 😅, I usually try to render the mask all by itself. In other words, update the mask rendering shader to write to a canvas texture (and not render the text) or something so I can see the mask is actually rendering what I expect it to render.

    In this case I saw that if I cleared the mask texture to 1 instead of 0 it also worked making it pretty clear there was nothing but 0s in the mask texture. That led to checking why, which led to noticing no data in the mask vertex buffer.

    <canvas></canvas>
    
    <script type="module">
      // WebGPU Simple Textured Quad - Import Canvas
      // from https://webgpufundamentals.org/webgpu/webgpu-simple-textured-quad-import-canvas.html
    
    
      import { mat4 } from 'https://webgpufundamentals.org/3rdparty/wgpu-matrix.module.js';
    
      const glyphWidth = 32;
      const glyphHeight = 40;
      const glyphsAcrossTexture = 16;
      function genreateGlyphTextureAtlas() {
        const ctx = document.createElement('canvas').getContext('2d');
        ctx.canvas.width = 512;
        ctx.canvas.height = 256;
    
        let x = 0;
        let y = 0;
        ctx.font = '32px monospace';
        ctx.textBaseline = 'middle';
        ctx.textAlign = 'center';
        ctx.fillStyle = 'white';
        for (let c = 33; c < 128; ++c) {
          ctx.fillText(String.fromCodePoint(c), x + glyphWidth / 2, y + glyphHeight / 2);
          x += glyphWidth;
          if (x >= ctx.canvas.width) {
            x = 0;
            y += glyphHeight;
          }
        }
    
        return ctx.canvas;
      }
    
      async function main() {
        const adapter = await navigator.gpu?.requestAdapter();
        const device = await adapter?.requestDevice();
        if (!device) {
          fail('need a browser that supports WebGPU');
          return;
        }
    
        // Get a WebGPU context from the canvas and configure it
        const canvas = document.querySelector('canvas');
        const context = canvas.getContext('webgpu');
        const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
        context.configure({
          device,
          format: presentationFormat,
        });
    
    
    
    
    
    
        const module2 = device.createShaderModule({
          code:
            `struct VSIn {
      @location(0) pos: vec4f,
      };
    
      struct VSOut {
        @builtin(position) pos: vec4f,
      };
    
      @vertex fn vs(vsIn: VSIn) -> VSOut {
        var vsOut: VSOut;
        vsOut.pos = vsIn.pos;
        return vsOut;
      }
    
      @fragment fn fs(vin: VSOut) -> @location(0) vec4f {
        return vec4f(1, 0, 0, 1);
      }`});
    
        const maskMakingPipeline = device.createRenderPipeline({
          label: 'pipeline for rendering the mask',
          layout: 'auto',
          vertex: {
            module: module2,
            entryPoint: 'vs',
            buffers: [
              // position
              {
                arrayStride: 2 * 4, // 2 floats, 4 bytes each
                attributes: [
                  { shaderLocation: 0, offset: 0, format: 'float32x2' },
                ],
              },
            ],
          },
          fragment: {
            module: module2,
            entryPoint: 'fs',
            targets: [],
          },
          // replace the stencil value when we draw
          depthStencil: {
            format: 'stencil8',
            depthCompare: 'always',
            depthWriteEnabled: false,
            stencilFront: {
              passOp: 'replace',
            },
          },
        });
    
    
    
    
    
    
    
    
    
        const module = device.createShaderModule({
          label: 'our hardcoded textured quad shaders',
          code: `
          struct VSInput {
            @location(0) position: vec4f,
            @location(1) texcoord: vec2f,
            @location(2) color: vec4f,
          };
    
          struct VSOutput {
            @builtin(position) position: vec4f,
            @location(0) texcoord: vec2f,
            @location(1) color: vec4f,
          };
    
          struct Uniforms {
            matrix: mat4x4f,
          };
    
          @group(0) @binding(2) var<uniform> uni: Uniforms;
    
          @vertex fn vs(vin: VSInput) -> VSOutput {
            var vsOutput: VSOutput;
            vsOutput.position = uni.matrix * vin.position;
            vsOutput.texcoord = vin.texcoord;
            vsOutput.color = vin.color;
            return vsOutput;
          }
    
          @group(0) @binding(0) var ourSampler: sampler;
          @group(0) @binding(1) var ourTexture: texture_2d<f32>;
    
          @fragment fn fs(fsInput: VSOutput) -> @location(0) vec4f {
            return textureSample(ourTexture, ourSampler, fsInput.texcoord) * fsInput.color;
          }
    
        `,
        });
    
        const glyphCanvas = genreateGlyphTextureAtlas();
        // so we can see it
        document.body.appendChild(glyphCanvas);
        glyphCanvas.style.backgroundColor = '#222';
    
        const maxGlyphs = 100;
        const floatsPerVertex = 2 + 2 + 4; // 2(pos) + 2(texcoord) + 4(color)
        const vertexSize = floatsPerVertex * 4; // 4 bytes each float
        const vertsPerGlyph = 6;
        const vertexBufferSize = maxGlyphs * vertsPerGlyph * vertexSize;
        const vertexBuffer = device.createBuffer({
          label: 'vertices',
          size: vertexBufferSize,
          usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
        });
        const indexBuffer = device.createBuffer({
          label: 'indices',
          size: maxGlyphs * vertsPerGlyph * 4,
          usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST,
        });
        // pre fill index buffer with quad indices
        {
          const indices = [];
          for (let i = 0; i < maxGlyphs; ++i) {
            const ndx = i * 4;
            indices.push(ndx, ndx + 1, ndx + 2, ndx + 2, ndx + 1, ndx + 3);
          }
          device.queue.writeBuffer(indexBuffer, 0, new Uint32Array(indices));
        }
    
        function generateGlyphVerticesForText(s, colors = [[1, 1, 1, 1]]) {
          const vertexData = new Float32Array(maxGlyphs * floatsPerVertex * vertsPerGlyph);
          const glyphUVWidth = glyphWidth / glyphCanvas.width;
          const glyphUVheight = glyphHeight / glyphCanvas.height;
          let offset = 0;
          let x0 = 0;
          let x1 = 1;
          let y0 = 0;
          let y1 = 1;
          let width = 0;
    
          const addVertex = (x, y, u, v, r, g, b, a) => {
            vertexData[offset++] = x;
            vertexData[offset++] = y;
            vertexData[offset++] = u;
            vertexData[offset++] = v;
            vertexData[offset++] = r;
            vertexData[offset++] = g;
            vertexData[offset++] = b;
            vertexData[offset++] = a;
          };
    
          const spacing = 0.55;
          let colorNdx = 0;
          for (let i = 0; i < s.length; ++i) {
            // convert char code to texcoords for glyph texture
            const c = s.charCodeAt(i);
            if (c >= 33) {
              const cNdx = c - 33;
              const glyphX = cNdx % glyphsAcrossTexture;
              const glyphY = Math.floor(cNdx / glyphsAcrossTexture);
              const u0 = (glyphX * glyphWidth) / glyphCanvas.width;
              const v1 = (glyphY * glyphHeight) / glyphCanvas.height;
              const u1 = u0 + glyphUVWidth;
              const v0 = v1 + glyphUVheight;
              width = Math.max(x1, width);
    
              addVertex(x0, y0, u0, v0, ...colors[colorNdx]);
              addVertex(x1, y0, u1, v0, ...colors[colorNdx]);
              addVertex(x0, y1, u0, v1, ...colors[colorNdx]);
              addVertex(x1, y1, u1, v1, ...colors[colorNdx]);
            } else {
              colorNdx = (colorNdx + 1) % colors.length;
              if (c === 10) {
                x0 = 0;
                x1 = 1;
                y0 = y0 - 1;
                y1 = y0 + 1;
                continue;
              }
            }
            x0 = x0 + spacing;
            x1 = x0 + 1;
          }
    
          return {
            vertexData,
            numGlyphs: offset / floatsPerVertex,
            width,
            height: y1,
          };
        }
    
        const { vertexData, numGlyphs, width, height } = generateGlyphVerticesForText(
          'Hello\nworld!\nText in\nWebGPU!', [
          [1, 1, 0, 1],
          [0, 1, 1, 1],
          [1, 0, 1, 1],
          [1, 0, 0, 1],
          [0, .5, 1, 1],
        ]);
        device.queue.writeBuffer(vertexBuffer, 0, vertexData);
    
        const pipeline = device.createRenderPipeline({
          label: 'hardcoded textured quad pipeline',
          layout: 'auto',
          vertex: {
            module,
            entryPoint: 'vs',
            buffers: [
              {
                arrayStride: vertexSize,
                attributes: [
                  { shaderLocation: 0, offset: 0, format: 'float32x2' },  // position
                  { shaderLocation: 1, offset: 8, format: 'float32x2' },  // texcoord
                  { shaderLocation: 2, offset: 16, format: 'float32x4' },  // color
                ],
              },
            ],
          },
          depthStencil: {
            depthCompare: 'always',
            depthWriteEnabled: false,
            format: 'stencil8',
            stencilFront: {
              compare: 'equal',
            },
          },
          fragment: {
            module,
            entryPoint: 'fs',
            targets: [
              {
                format: presentationFormat,
                blend: {
                  color: {
                    srcFactor: 'one',
                    dstFactor: 'one-minus-src-alpha',
                    operation: 'add',
                  },
                  alpha: {
                    srcFactor: 'one',
                    dstFactor: 'one-minus-src-alpha',
                    operation: 'add',
                  },
                },
              },
            ],
          },
        });
    
        function copySourceToTexture(device, texture, source, { flipY } = {}) {
          device.queue.copyExternalImageToTexture(
            { source, flipY, },
            { texture, premultipliedAlpha: true },
            { width: source.width, height: source.height },
          );
        }
    
        function createTextureFromSource(device, source, options = {}) {
          const texture = device.createTexture({
            format: 'rgba8unorm',
            size: [source.width, source.height],
            usage: GPUTextureUsage.TEXTURE_BINDING |
              GPUTextureUsage.COPY_DST |
              GPUTextureUsage.RENDER_ATTACHMENT,
          });
          copySourceToTexture(device, texture, source, options);
          return texture;
        }
    
        const texture = createTextureFromSource(device, glyphCanvas, { mips: true });
        const sampler = device.createSampler({
          minFilter: 'linear',
          magFilter: 'linear',
        });
    
        // create a buffer for the uniform values
        const uniformBufferSize =
          16 * 4; // matrix is 16 32bit floats (4bytes each)
        const uniformBuffer = device.createBuffer({
          label: 'uniforms for quad',
          size: uniformBufferSize,
          usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
        });
    
        // create a typedarray to hold the values for the uniforms in JavaScript
        const kMatrixOffset = 0;
        const uniformValues = new Float32Array(uniformBufferSize / 4);
        const matrix = uniformValues.subarray(kMatrixOffset, 16);
    
        const bindGroup = device.createBindGroup({
          layout: pipeline.getBindGroupLayout(0),
          entries: [
            { binding: 0, resource: sampler },
            { binding: 1, resource: texture.createView() },
            { binding: 2, resource: { buffer: uniformBuffer } },
          ],
        });
    
    
    
    
        const maskVerts = new Float32Array([-1, -1, 1, -1, 1, 1]);
        const maskVertexBuffer = device.createBuffer({
          size: maskVerts.byteLength,
          usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST,
        });
        device.queue.writeBuffer(maskVertexBuffer, 0, maskVerts);
        const stencilTexture = device.createTexture({
          format: 'stencil8',
          size: [canvas.width, canvas.height],
          usage: GPUTextureUsage.RENDER_ATTACHMENT,
        });
    
    
        const renderPassDescriptor = {
          label: 'our basic canvas renderPass',
          /*colorAttachments: [
            {
              // view: <- to be filled out when we render
              clearValue: [0.3, 0.3, 0.3, 1],
              loadOp: 'clear',
              storeOp: 'store',
            },
          ],*/
          colorAttachments: [{
            view: context.getCurrentTexture().createView(),
            clearValue: [0, 0, 0, 1],
            loadOp: 'clear',
            storeOp: 'store',
          }],
          depthStencilAttachment: {
            view: stencilTexture.createView(),
            stencilLoadOp: 'load',
            stencilStoreOp: 'store',
          }
        };
    
    
    
    
    
    
    
    
    
    
        function render(time) {
          time *= 0.001;
    
          const fov = 60 * Math.PI / 180;  // 60 degrees in radians
          const aspect = canvas.clientWidth / canvas.clientHeight;
          const zNear = 0.001;
          const zFar = 50;
          const projectionMatrix = mat4.perspective(fov, aspect, zNear, zFar);
    
          const cameraPosition = [0, 0, 5];
          const up = [0, 1, 0];
          const target = [0, 0, 0];
          const viewMatrix = mat4.lookAt(cameraPosition, target, up);
          const viewProjectionMatrix = mat4.multiply(projectionMatrix, viewMatrix);
    
          // Get the current texture from the canvas context and
          // set it as the texture to render to.
          renderPassDescriptor.colorAttachments[0].view =
            context.getCurrentTexture().createView();
    
          const encoder = device.createCommandEncoder({
            label: 'render quad encoder',
          });
    
    
    
          {
            const pass = encoder.beginRenderPass({
              colorAttachments: [],
              depthStencilAttachment: {
                view: stencilTexture.createView(),
                stencilClearValue: 0,
                stencilLoadOp: 'clear',
                stencilStoreOp: 'store',
              }
            });
            // draw the mask
            pass.setPipeline(maskMakingPipeline);
            pass.setVertexBuffer(0, maskVertexBuffer);
            pass.setStencilReference(1);
            pass.draw(3);
            pass.end();
          }
    
    
          {
            const pass = encoder.beginRenderPass(renderPassDescriptor);
            pass.setPipeline(pipeline);
    
            mat4.rotateY(viewProjectionMatrix, 0, matrix);
            mat4.translate(matrix, [-width / 2, -height / 2, 0], matrix);
    
            // copy the values from JavaScript to the GPU
            device.queue.writeBuffer(uniformBuffer, 0, uniformValues);
    
            pass.setBindGroup(0, bindGroup);
            pass.setVertexBuffer(0, vertexBuffer);
            pass.setIndexBuffer(indexBuffer, 'uint32');
            pass.setStencilReference(1);
            pass.drawIndexed(numGlyphs * 6);
    
            pass.end();
          }
    
          const commandBuffer = encoder.finish();
          device.queue.submit([commandBuffer]);
    
          requestAnimationFrame(render);
        }
        requestAnimationFrame(render);
      }
    
      function fail(msg) {
        // eslint-disable-next-line no-alert
        alert(msg);
      }
    
      main();
    </script>