Search code examples
javascriptgoogle-chromefirefoxwebglstencil-buffer

Support for Stencil Buffer with WebGL


Initializing webgl with canvas.getContext("webgl", {stencil : true}) requests a stencil buffer, but not all browsers will actually give you one (for me, Firefox 79.0 on Ubuntu 20.04 LTS doesn't works but Chrome 84.0.4147.89 does. My graphics card is NVIDIA RTX 2060, I'm using the nvidia-driver-440-server driver).

I would like to know how widely supported stencil buffers are, but I can't find information about what browsers are supported. The functions like glStencilOp, which are the only things I can find support information for, can still be used, they just don't do anything with 0 stencil bits.

Is there a list of browsers that support this feature?


Solution

  • Honestly that sounds like a bug in firefox although given the spec lets an implementation fail to provide a stencil buffer on the canvas for any reason whatsoever it's not technically a bug. I would consider filling one. Test with a Chromium browser just to check that this is Firefox choosing not to provide a stencil buffer and not a driver issue or something else.

    You should be able to always make a DEPTH_STENCIL renderbuffer. There is no version of WebGL that allows an implementation to not support that. So, you can work around the bug by rendering to a texture + depth stencil renderbuffer attached to a framebuffer and then render the framebuffer color texture to the canvas.

    Here's a test. you should see a red square with the bottom right corner green. that will be inside a blue square which is inside a purple square.

    The blue square is to show the extents of the framebuffer texture. If the green square was not being masked by the stencil buffer it would bleed into the blue.

    The purple square is to show the size of the canvas and that we are drawing the framebuffer texture smaller than the full canvas. This is all just to show that stencil buffers work on your machine. For your own solution you'd want to draw a quad made out of vertices instead of using points like below, and you'd want to make the texture and renderbuffer attached to the frame buffer the same size as your canvas.

    "use strict";
    
    function main() {
      const gl = document.querySelector("canvas").getContext("webgl");
      
      const vs = `
      attribute vec4 position;
    
      void main() {
        gl_Position = position;
        gl_PointSize = 64.0;
      }
      `;
    
      const fs = `
      precision mediump float;
    
      uniform sampler2D tex;
    
      void main() {
         gl_FragColor = texture2D(tex, gl_PointCoord.xy);
      }
      `;
    
      const program = twgl.createProgram(gl, [vs, fs]);
      const posLoc = gl.getAttribLocation(program, "position");
    
      // Create a texture to render to
      const targetTextureWidth = 128;
      const targetTextureHeight = 128;
      const targetTexture = createTexture(gl);
    
      {
        // define size and format of level 0
        const level = 0;
        const internalFormat = gl.RGBA;
        const border = 0;
        const format = gl.RGBA;
        const type = gl.UNSIGNED_BYTE;
        const data = null;
        gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
                      targetTextureWidth, targetTextureHeight, border,
                      format, type, data);
    
      }
    
      // Create and bind the framebuffer
      const fb = gl.createFramebuffer();
      gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
    
      // attach the texture as the first color attachment
      const attachmentPoint = gl.COLOR_ATTACHMENT0;
      const level = 0;
      gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, targetTexture, level);
    
      // create a depth-stencil renderbuffer
      const depthStencilBuffer = gl.createRenderbuffer();
      gl.bindRenderbuffer(gl.RENDERBUFFER, depthStencilBuffer);
    
      // make a depth-stencil buffer and the same size as the targetTexture
      gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_STENCIL, targetTextureWidth, targetTextureHeight);
      gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_STENCIL_ATTACHMENT, gl.RENDERBUFFER, depthStencilBuffer);
      
      function createTexture(gl, color) {
        const tex = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, tex);
        // set the filtering so we don't need mips
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        if (color) {
          gl.texImage2D(
              gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0,
              gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(color));
        }
        return tex;
      }
      
      // create a red texture and a green texture
      const redTex = createTexture(gl, [255, 0, 0, 255]);
      const greenTex = createTexture(gl, [0, 255, 0, 255]);
    
      gl.enable(gl.STENCIL_TEST);
    
      gl.useProgram(program);
      gl.clearColor(0, 0, 1, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      
      gl.bindTexture(gl.TEXTURE_2D, redTex);
      gl.stencilFunc(
           gl.ALWAYS,    // the test
           1,            // reference value
           0xFF,         // mask
        );  
      gl.stencilOp(
         gl.KEEP,     // what to do if the stencil test fails
         gl.KEEP,     // what to do if the depth test fails
         gl.REPLACE,  // what to do if both tests pass
      );
      
      // draw a 64x64 pixel red rect in middle
      gl.drawArrays(gl.POINTS, 0, 1);
      
    
      gl.stencilFunc(
           gl.EQUAL,     // the test
           1,            // reference value
           0xFF,         // mask
        );  
      gl.stencilOp(
         gl.KEEP,     // what to do if the stencil test fails
         gl.KEEP,     // what to do if the depth test fails
         gl.KEEP,  // what to do if both tests pass
      );
    
    
      // draw a green 64x64 pixel square in the
      // upper right corner. The stencil will make
      // it not go outside the red square
      gl.vertexAttrib2f(posLoc, 0.5, 0.5);
      gl.bindTexture(gl.TEXTURE_2D, greenTex);
      gl.drawArrays(gl.POINTS, 0, 1);
    
      // draw the framebuffer's texture to
      // the canvas. we should see a 32x32
      // red square with the bottom right corner
      // green showing the stencil worked. That will
      // be surrounded by blue to show the texture
      // we were rendering to is larger than the
      // red square. And that will be surrounded
      // by purple since we're drawing a 64x64
      // point on a 128x128 canvas which we clear 
      // purple.
      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
      gl.clearColor(1, 0, 1, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      
      gl.vertexAttrib2f(posLoc, 0.0, 0.0);
      gl.bindTexture(gl.TEXTURE_2D, targetTexture);
      gl.drawArrays(gl.POINTS, 0, 1);  
    
    }
    
    main();
    canvas { border: 1px solid black; }
    <script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
    <canvas width="128" height="128"></canvas>

    If you change the renderbuffer format to DEPTH_COMPONENT16 and the attachment point to DEPTH_ATTACHMENT then you'll see green square is no longer masked by the stencil

    "use strict";
    
    function main() {
      const gl = document.querySelector("canvas").getContext("webgl");
      
      const vs = `
      attribute vec4 position;
    
      void main() {
        gl_Position = position;
        gl_PointSize = 64.0;
      }
      `;
    
      const fs = `
      precision mediump float;
    
      uniform sampler2D tex;
    
      void main() {
         gl_FragColor = texture2D(tex, gl_PointCoord.xy);
      }
      `;
    
      const program = twgl.createProgram(gl, [vs, fs]);
      const posLoc = gl.getAttribLocation(program, "position");
    
      // Create a texture to render to
      const targetTextureWidth = 128;
      const targetTextureHeight = 128;
      const targetTexture = createTexture(gl);
    
      {
        // define size and format of level 0
        const level = 0;
        const internalFormat = gl.RGBA;
        const border = 0;
        const format = gl.RGBA;
        const type = gl.UNSIGNED_BYTE;
        const data = null;
        gl.texImage2D(gl.TEXTURE_2D, level, internalFormat,
                      targetTextureWidth, targetTextureHeight, border,
                      format, type, data);
    
      }
    
      // Create and bind the framebuffer
      const fb = gl.createFramebuffer();
      gl.bindFramebuffer(gl.FRAMEBUFFER, fb);
    
      // attach the texture as the first color attachment
      const attachmentPoint = gl.COLOR_ATTACHMENT0;
      const level = 0;
      gl.framebufferTexture2D(gl.FRAMEBUFFER, attachmentPoint, gl.TEXTURE_2D, targetTexture, level);
    
      // create a depth-stencil renderbuffer
      const depthStencilBuffer = gl.createRenderbuffer();
      gl.bindRenderbuffer(gl.RENDERBUFFER, depthStencilBuffer);
    
      // make a depth-stencil buffer and the same size as the targetTexture
      gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, targetTextureWidth, targetTextureHeight);
      gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthStencilBuffer);
      
      function createTexture(gl, color) {
        const tex = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, tex);
        // set the filtering so we don't need mips
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        if (color) {
          gl.texImage2D(
              gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0,
              gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(color));
        }
        return tex;
      }
      
      // create a red texture and a green texture
      const redTex = createTexture(gl, [255, 0, 0, 255]);
      const greenTex = createTexture(gl, [0, 255, 0, 255]);
    
      gl.enable(gl.STENCIL_TEST);
    
      gl.useProgram(program);
      gl.clearColor(0, 0, 1, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      
      gl.bindTexture(gl.TEXTURE_2D, redTex);
      gl.stencilFunc(
           gl.ALWAYS,    // the test
           1,            // reference value
           0xFF,         // mask
        );  
      gl.stencilOp(
         gl.KEEP,     // what to do if the stencil test fails
         gl.KEEP,     // what to do if the depth test fails
         gl.REPLACE,  // what to do if both tests pass
      );
      
      // draw a 64x64 pixel red rect in middle
      gl.drawArrays(gl.POINTS, 0, 1);
      
    
      gl.stencilFunc(
           gl.EQUAL,     // the test
           1,            // reference value
           0xFF,         // mask
        );  
      gl.stencilOp(
         gl.KEEP,     // what to do if the stencil test fails
         gl.KEEP,     // what to do if the depth test fails
         gl.KEEP,  // what to do if both tests pass
      );
    
    
      // draw a green 64x64 pixel square in the
      // upper right corner. The stencil will make
      // it not go outside the red square
      gl.vertexAttrib2f(posLoc, 0.5, 0.5);
      gl.bindTexture(gl.TEXTURE_2D, greenTex);
      gl.drawArrays(gl.POINTS, 0, 1);
    
      // draw the framebuffer's texture to
      // the canvas. we should see a 32x32
      // red square with the bottom right corner
      // green showing the stencil worked. That will
      // be surrounded by blue to show the texture
      // we were rendering to is larger than the
      // red square. And that will be surrounded
      // by purple since we're drawing a 64x64
      // point on a 128x128 canvas which we clear 
      // purple.
      gl.bindFramebuffer(gl.FRAMEBUFFER, null);
      gl.clearColor(1, 0, 1, 1);
      gl.clear(gl.COLOR_BUFFER_BIT);
      
      gl.vertexAttrib2f(posLoc, 0.0, 0.0);
      gl.bindTexture(gl.TEXTURE_2D, targetTexture);
      gl.drawArrays(gl.POINTS, 0, 1);  
    
    }
    
    main();
    canvas { border: 1px solid black; }
    <script src="https://twgljs.org/dist/4.x/twgl.min.js"></script>
    <canvas width="128" height="128"></canvas>

    You're supposed to be able to call gl.getContextAttributes to check if you got a stencil buffer or not so you can use the suggested solution if it tells you you didn't get a stencil buffer on the canvas.