antialiasingwebgl2msaa

WebGL2 render to texture with MSAA anti-aliasing and transparent background


I'm trying to render a set of triangles with multi-sample-anti-aliasing MSAA enabled in WebGL2. Therefore, I'm setting up rendering pipeline with a multisample renderbuffer to render to a target texture. Anti-aliasing seems to work, however if I try to render the scene to a transparent renderbuffer, the anti-aliasing seems gradually blend to the opaque background color despite it being fully transparent.

In the example image below, a set of green rgb(0,1,0,1) triangles is drawn: first with background clear color set to gl.clearColor(0, 0, 0, 0) - second with clear color set to gl.clearColor(1, 0, 0, 0) - (The resulting texture is blended on a white background to show the results).

How can I render the scene to a transparent texture with anti-aliasing going gradually from rgba(0,0,255,1) to rgba(0,0,0,0)?

//initialization code
gl.frameBufferAA = gl.createFramebuffer();
//render code
let renderBufferAA = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, renderBufferAA);
gl.renderbufferStorageMultisample(gl.RENDERBUFFER, gl.getParameter(gl.MAX_SAMPLES), gl.RGBA8, texDst.width, texDst.height);

//attach renderBufferAA to frameBufferRenderBuffer
gl.bindFramebuffer(gl.FRAMEBUFFER, gl.frameBufferAA);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, renderBufferAA);
gl.clearColor(0, 0, 0, 0);  //<--- transparent color affects anti-aliasing 
gl.colorMask(true, true, true, true);
gl.clear(gl.COLOR_BUFFER_BIT);

twgl.drawBufferInfo(gl, gl.TRIANGLES, bufferInfo);

//blit renderBuffe
gl.bindFramebuffer(gl.READ_FRAMEBUFFER, gl.frameBufferAA);
gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, gl.frameBuffer1);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texDst, 0);
gl.blitFramebuffer(
  0, 0, texDst.width, texDst.height,
  0, 0, texDst.width, texDst.height,
  gl.COLOR_BUFFER_BIT, gl.NEAREST
);

gl.deleteRenderbuffer(renderBufferAA);

Update:

I've created a stack overflow snippet to isolate the problem. The fiddle draws an anti-aliased red circle. The pixels created by anti-aliasing are fading to green which is the clear-color of the multisample renderbuffer. The problem seems to be related to alpha=false creation parameter of the webgl2 context.

(function () {
        'use strict';

        var canvas = document.createElement('canvas');
        canvas.width = Math.min(window.innerWidth, window.innerHeight);
        canvas.height = canvas.width;
        document.body.appendChild(canvas);

        var gl = canvas.getContext( 'webgl2', { antialias: false, alpha: false } );
        var isWebGL2 = !!gl;
        if(!isWebGL2) {
            document.getElementById('info').innerHTML = 'WebGL 2 is not available.  See <a href="https://www.khronos.org/webgl/wiki/Getting_a_WebGL_Implementation">How to get a WebGL 2 implementation</a>';
            return;
        }

        // -- Init program
        var PROGRAM = {
            TEXTURE: 0,
            SPLASH: 1,
            MAX: 2
        };

        var programs = [
            createProgram(gl, getShaderSource('vs-render'), getShaderSource('fs-render')),
            createProgram(gl, getShaderSource('vs-splash'), getShaderSource('fs-splash'))
        ];
        var mvpLocationTexture = gl.getUniformLocation(programs[PROGRAM.TEXTURE], 'MVP');
        var mvpLocation = gl.getUniformLocation(programs[PROGRAM.SPLASH], 'MVP');
        var diffuseLocation = gl.getUniformLocation(programs[PROGRAM.SPLASH], 'diffuse');

        // -- Init primitive data
        var vertexCount = 18;
        var data = new Float32Array(vertexCount * 2);
        var angle;
        var radius = 0.1;
        for(var i = 0; i < vertexCount; i++ )
        {
            angle = Math.PI * 2 * i / vertexCount;
            data[2 * i] = radius * Math.sin(angle);
            data[2 * i + 1] = radius * Math.cos(angle);
        }

        // -- Init buffers
        var vertexDataBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexDataBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);

        var positions = new Float32Array([
            -1.0, -1.0,
             1.0, -1.0,
             1.0,  1.0,
             1.0,  1.0,
            -1.0,  1.0,
            -1.0, -1.0
        ]);
        var vertexPosBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);

        var texCoords = new Float32Array([
            0.0, 1.0,
            1.0, 1.0,
            1.0, 0.0,
            1.0, 0.0,
            0.0, 0.0,
            0.0, 1.0
        ]);
        var vertexTexBuffer = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, texCoords, gl.STATIC_DRAW);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);

        // -- Init Texture
        // used for draw framebuffer storage
        var FRAMEBUFFER_SIZE = {
            x: canvas.width,
            y: canvas.height
        };
        var texture = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, FRAMEBUFFER_SIZE.x, FRAMEBUFFER_SIZE.y, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
        gl.bindTexture(gl.TEXTURE_2D, null);

        // -- Init Frame Buffers
        var FRAMEBUFFER = {
            RENDERBUFFER: 0,
            COLORBUFFER: 1
        };
        var framebuffers = [
            gl.createFramebuffer(),
            gl.createFramebuffer()
        ];
        var colorRenderbuffer = gl.createRenderbuffer();
        gl.bindRenderbuffer(gl.RENDERBUFFER, colorRenderbuffer);
        gl.renderbufferStorageMultisample(gl.RENDERBUFFER, 4, gl.RGBA8, FRAMEBUFFER_SIZE.x, FRAMEBUFFER_SIZE.y);

        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[FRAMEBUFFER.RENDERBUFFER]);
        gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, colorRenderbuffer);
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);

        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[FRAMEBUFFER.COLORBUFFER]);
        gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);

        // -- Init VertexArray
        var vertexArrays = [
            gl.createVertexArray(),
            gl.createVertexArray()
        ];

        var vertexPosLocation = 0; // set with GLSL layout qualifier

        gl.bindVertexArray(vertexArrays[PROGRAM.TEXTURE]);
        gl.enableVertexAttribArray(vertexPosLocation);
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexDataBuffer);
        gl.vertexAttribPointer(vertexPosLocation, 2, gl.FLOAT, false, 0, 0);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);
        gl.bindVertexArray(null);

        gl.bindVertexArray(vertexArrays[PROGRAM.SPLASH]);

        gl.enableVertexAttribArray(vertexPosLocation);
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer);
        gl.vertexAttribPointer(vertexPosLocation, 2, gl.FLOAT, false, 0, 0);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);

        var vertexTexLocation = 1; // set with GLSL layout qualifier
        gl.enableVertexAttribArray(vertexTexLocation);
        gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexBuffer);
        gl.vertexAttribPointer(vertexTexLocation, 2, gl.FLOAT, false, 0, 0);
        gl.bindBuffer(gl.ARRAY_BUFFER, null);

        gl.bindVertexArray(null);

        // -- Render

        // Pass 1
        gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffers[FRAMEBUFFER.RENDERBUFFER]);
        gl.clearBufferfv(gl.COLOR, 0, [0.0, 1.0, 0.0, 0.0]);
        gl.useProgram(programs[PROGRAM.TEXTURE]);
        gl.bindVertexArray(vertexArrays[PROGRAM.TEXTURE]);

        var IDENTITY = mat4.create();
        gl.uniformMatrix4fv(mvpLocationTexture, false, IDENTITY);

        gl.enable(gl.blend);
        gl.blendFunc(gl.SRC_COLOR, gl.ONE_MINUS_SRC_ALPHA);
        gl.drawArrays(gl.LINE_LOOP, 0, vertexCount);

        // Blit framebuffers, no Multisample texture 2d in WebGL 2
        gl.bindFramebuffer(gl.READ_FRAMEBUFFER, framebuffers[FRAMEBUFFER.RENDERBUFFER]);
        gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, framebuffers[FRAMEBUFFER.COLORBUFFER]);
        gl.blitFramebuffer(
            0, 0, FRAMEBUFFER_SIZE.x, FRAMEBUFFER_SIZE.y,
            0, 0, FRAMEBUFFER_SIZE.x, FRAMEBUFFER_SIZE.y,
            gl.COLOR_BUFFER_BIT, gl.NEAREST
        );

        // Pass 2
        gl.bindFramebuffer(gl.FRAMEBUFFER, null);
        gl.useProgram(programs[PROGRAM.SPLASH]);
        gl.uniform1i(diffuseLocation, 0);
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, texture);
        gl.bindVertexArray(vertexArrays[PROGRAM.SPLASH]);

        gl.clearColor(1.0, 1.0, 1.0, 1.0);
        gl.clear(gl.COLOR_BUFFER_BIT);

        var scaleVector3 = vec3.create();
        vec3.set(scaleVector3, 8.0, 8.0, 8.0);
        var mvp = mat4.create();
        mat4.scale(mvp, IDENTITY, scaleVector3);

        gl.uniformMatrix4fv(mvpLocation, false, mvp);

        gl.enable(gl.BLEND);
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
        gl.drawArrays(gl.TRIANGLES, 0, 6);

        // -- Delete WebGL resources
        gl.deleteBuffer(vertexPosBuffer);
        gl.deleteBuffer(vertexTexBuffer);
        gl.deleteTexture(texture);
        gl.deleteRenderbuffer(colorRenderbuffer);
        gl.deleteFramebuffer(framebuffers[FRAMEBUFFER.RENDERBUFFER]);
        gl.deleteFramebuffer(framebuffers[FRAMEBUFFER.COLORBUFFER]);
        gl.deleteVertexArray(vertexArrays[PROGRAM.TEXTURE]);
        gl.deleteVertexArray(vertexArrays[PROGRAM.SPLASH]);
        gl.deleteProgram(programs[PROGRAM.TEXTURE]);
        gl.deleteProgram(programs[PROGRAM.SPLASH]);

    })();
body { margin: 0; }
canvas { width: 100vw; height: 100vh; display: block }
<script src="https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/2.8.1/gl-matrix-min.js" integrity="sha256-+09xst+d1zIS41eAvRDCXOf0MH993E4cS40hKBIJj8Q=" crossorigin="anonymous"></script>
<script>
(function () {
    'use strict';

    window.getShaderSource = function(id) {
        return document.getElementById(id).textContent.replace(/^\s+|\s+$/g, '');
    };

    function createShader(gl, source, type) {
        var shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);
        return shader;
    }

    window.createProgram = function(gl, vertexShaderSource, fragmentShaderSource) {
        var program = gl.createProgram();
        var vshader = createShader(gl, vertexShaderSource, gl.VERTEX_SHADER);
        var fshader = createShader(gl, fragmentShaderSource, gl.FRAGMENT_SHADER);
        gl.attachShader(program, vshader);
        gl.deleteShader(vshader);
        gl.attachShader(program, fshader);
        gl.deleteShader(fshader);
        gl.linkProgram(program);

        var log = gl.getProgramInfoLog(program);
        if (log) {
            console.log(log);
        }

        log = gl.getShaderInfoLog(vshader);
        if (log) {
            console.log(log);
        }

        log = gl.getShaderInfoLog(fshader);
        if (log) {
            console.log(log);
        }

        return program;
    };        
})();
</script>
<!-- vertex shader -->
<!-- WebGL 2 shaders -->
<script id="vs-render" type="x-shader/x-vertex">
        #version 300 es
        #define POSITION_LOCATION 0

        precision highp float;
        precision highp int;

        uniform mat4 MVP;

        layout(location = POSITION_LOCATION) in vec2 position;

        void main()
        {
            gl_Position = MVP * vec4(position, 0.0, 1.0);
        }
</script>

<script id="fs-render" type="x-shader/x-fragment">
        #version 300 es
        precision highp float;
        precision highp int;

        out vec4 color;

        void main()
        {
            color = vec4(1.0, 0.0, 0.0, 1.0);
        }
</script>

<script id="vs-splash" type="x-shader/x-vertex">
        #version 300 es
        precision highp float;
        precision highp int;

        uniform mat4 MVP;

        layout(location = 0) in vec2 position;
        layout(location = 1) in vec2 texcoord;

        out vec2 uv;

        void main()
        {
            uv = texcoord;
            gl_Position = MVP * vec4(position, 0.0, 1.0);
        }
</script>

<script id="fs-splash" type="x-shader/x-fragment">
        #version 300 es
        precision highp float;
        precision highp int;

        uniform sampler2D diffuse;

        in vec2 uv;

        out vec4 color;

        void main()
        {
            color = texture(diffuse, uv);
        }
</script>

<script>
    
    </script>

background color set to transparent black background color set to transparent red expected result, created in image editor


Solution

  • Saying the problem seems to be related to 'alpha=false creation parameter of the webgl2 context.' suggests the issue is how the canvas is blended with the webpage itself?

    A canvas is composited (blended) with the rest of the HTML in the page, whatever is behind the canvas. The canvas element itself can have a CSS background, the page <body> can have a background. The canvas can be over other elements. Regardless it's composited with the page.

    The default is it's effectively blended using blendFunc(ONE, ONE_MINUS_SRC_ALPHA) so the colors in canvas need to be premultiplied alpha values.

    You can set the canvas to have no alpha getContext("webgl", {alpha: false}) in which case the alpha is effectivity 1.0 for

    You can also tell the browser your pixel values are un-premultipled alpha in which case it will effectively use blendFunc(SRC_ALPHA, ONE_MINUS_SRC_ALPHA) to composite the canvas in the page. You do this with getContext("webgl, {premultipliedAlpha: false}.

    I'd suggest you set the the canvas's background color to something that will make it clear what's going on. For example.

    canvas {
      background-color: #FF0;
      background-image: 
          linear-gradient(45deg, #F0F 25%, transparent 25%), 
          linear-gradient(-45deg, #F0F 25%, transparent 25%), 
          linear-gradient(45deg, transparent 75%, #F0F 75%), 
          linear-gradient(-45deg, transparent 75%, #F0F 75%);
      background-size: 20px 20px;
      background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
    }
    <canvas></canvas>

    Then draw stuff

    function main(attribs = {}) {
      const canvas = document.createElement('canvas');
      canvas.width = 150;
      canvas.height = 50;
      document.body.appendChild(canvas);
      const gl = canvas.getContext('webgl2', attribs);
      if (!gl) {
        return alert('need webgl2');
      }
      log('attribs:', JSON.stringify(gl.getContextAttributes()));
      
      const vs = `
      attribute vec4 position;
      void main() {
        gl_Position = position;
      }
      `;
      const fs = `
      precision highp float;
      uniform vec4 color;
      void main() {
        gl_FragColor = color;
      }
      `;
      const programInfo = twgl.createProgramInfo(gl, [vs, fs]);
      const bufferInfo = twgl.createBufferInfoFromArrays(gl, {
        position: [
          -0.8, 0.5, 0,
           0.8, 0.4, 0,
           0.0,-0.5, 0,
        ],
      });
      gl.useProgram(programInfo.program);
      twgl.setBuffersAndAttributes(gl, programInfo, bufferInfo);
      twgl.setUniforms(programInfo, {color: [0, 1, 0, 1]});
      twgl.drawBufferInfo(gl, bufferInfo);
    }
    
    function log(...args) {
      const elem = document.createElement('pre');
      elem.textContent = [...args].join(' ');
      document.body.appendChild(elem);
    }
    
    main({});
    main({premultipliedAlpha: false});
    main({antialias: false});
    canvas {
      image-rendering: pixelated;
      background-color: #FF0;
      background-image: 
          linear-gradient(45deg, #F0F 25%, transparent 25%), 
          linear-gradient(-45deg, #F0F 25%, transparent 25%), 
          linear-gradient(45deg, transparent 75%, #F0F 75%), 
          linear-gradient(-45deg, transparent 75%, #F0F 75%);
      background-size: 20px 20px;
      background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
    }
    <script src="https://twgljs.org/dist/4.x/twgl-full.min.js"></script>

    If I blow those up you can see the difference. You can also see it's being blended over the background from the HTML.

    Note: I didn't setup an MSAA renderbuffer because in general the canvas itself is an MSAA renderbuffer by default. It's actually up to the browser but it's clear my browser on my GPU is use MSAA by default for the canvas.

    enter image description here

    If that wasn't clear, what I'm suggesting is that your results are that you didn't set {alpha: false} so if there are any non 1.0 alpha values in your canvas then you'll see the results of blending with the HTML colors behind the canvas.

    Even with a white backround we can see issues depending on the settings

    enter image description here

    Also note that you mentioned clearing to (1, 0, 0, 0). 1, 0, 0, 0 is an invalid color on a default "premultipliedAlpha: true" canvas. R can not be > A if the colors are premultiplied.

    What happens in that case is undefined. It used to be that firefox and chrome behaved very different for such colors.