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>
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.
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
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.