Conceitos básicos do WebGL

Gregg Tavares
Gregg Tavares

Fundamentos do WebGL

O WebGL permite mostrar gráficos 3D incríveis em tempo real no navegador, mas muitas pessoas não sabem que o WebGL é uma API 2D, não 3D. Vou explicar.

A WebGL só se importa com duas coisas. Coordenadas do espaço de recorte em 2D e cores. Seu trabalho como programador que usa o WebGL é fornecer essas duas coisas ao WebGL. Você fornece dois "shaders" para fazer isso. Um sombreador de vértice que fornece as coordenadas do espaço de recorte e um sombreador de fragmento que fornece a cor. As coordenadas do Clipspace sempre vão de -1 a +1, independentemente do tamanho da tela. Aqui está um exemplo simples que mostra o WebGL em sua forma mais simples.

// Get A WebGL context
var canvas = document.getElementById("canvas");
var gl = canvas.getContext("experimental-webgl");

// setup a GLSL program
var vertexShader = createShaderFromScriptElement(gl, "2d-vertex-shader");
var fragmentShader = createShaderFromScriptElement(gl, "2d-fragment-shader");
var program = createProgram(gl, [vertexShader, fragmentShader]);
gl.useProgram(program);

// look up where the vertex data needs to go.
var positionLocation = gl.getAttribLocation(program, "a_position");

// Create a buffer and put a single clipspace rectangle in
// it (2 triangles)
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(
    gl.ARRAY_BUFFER,
    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]),
    gl.STATIC_DRAW);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

// draw
gl.drawArrays(gl.TRIANGLES, 0, 6);

Estes são os dois sombreadores

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

void main() {
  gl_Position = vec4(a_position, 0, 1);
}
</script>

<script id="2d-fragment-shader" type="x-shader/x-fragment">
void main() {
  gl_FragColor = vec4(0,1,0,1);  // green
}
</script>

Novamente, as coordenadas do espaço de clipe sempre vão de -1 a +1, independentemente do tamanho da tela. No caso acima, você pode ver que não estamos fazendo nada além de transmitir nossos dados de posição diretamente. Como os dados de posição já estão no clipspace, não é necessário fazer nada. Se você quiser 3D, forneça shaders que convertem de 3D para 2D, porque o WebGL É UMA API 2D! Para coisas em 2D, você provavelmente prefere trabalhar em pixels do que em clipspace. Vamos mudar o sombreador para fornecer retângulos em pixels e fazer a conversão para clipspace. Este é o novo shader de vértice

<script id="2d-vertex-shader" type="x-shader/x-vertex">
attribute vec2 a_position;

uniform vec2 u_resolution;

void main() {
   // convert the rectangle from pixels to 0.0 to 1.0
   vec2 zeroToOne = a_position / u_resolution;

   // convert from 0->1 to 0->2
   vec2 zeroToTwo = zeroToOne * 2.0;

   // convert from 0->2 to -1->+1 (clipspace)
   vec2 clipSpace = zeroToTwo - 1.0;

   gl_Position = vec4(clipSpace, 0, 1);
}
</script>

Agora podemos mudar nossos dados de clipspace para pixels.

// set the resolution
var resolutionLocation = gl.getUniformLocation(program, "u_resolution");
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);

// setup a rectangle from 10,20 to 80,30 in pixels
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
    10, 20,
    80, 20,
    10, 30,
    10, 30,
    80, 20,
    80, 30]), gl.STATIC_DRAW);

O retângulo está perto da parte de baixo dessa área. O WebGL considera o canto inferior esquerdo como 0,0. Para fazer com que ele seja o canto superior esquerdo mais tradicional usado em APIs de gráficos 2D, basta inverter a coordenada y.

gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1);

Vamos transformar o código que define um retângulo em uma função para que possamos chamá-lo para retângulos de tamanhos diferentes. Enquanto isso, vamos definir a cor. Primeiro, fazemos com que o sombreador de fragmento receba uma entrada de cor uniforme.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

uniform vec4 u_color;

void main() {
   gl_FragColor = u_color;
}
</script>

Este é o novo código que desenha 50 retângulos em lugares e cores aleatórios.

...

  var colorLocation = gl.getUniformLocation(program, "u_color");
  ...
  // Create a buffer
  var buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.enableVertexAttribArray(positionLocation);
  gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

  // draw 50 random rectangles in random colors
  for (var ii = 0; ii < 50; ++ii) {
    // Setup a random rectangle
    setRectangle(
        gl, randomInt(300), randomInt(300), randomInt(300), randomInt(300));

    // Set a random color.
    gl.uniform4f(colorLocation, Math.random(), Math.random(), Math.random(), 1);

    // Draw the rectangle.
    gl.drawArrays(gl.TRIANGLES, 0, 6);
  }
}

// Returns a random integer from 0 to range - 1.
function randomInt(range) {
  return Math.floor(Math.random() * range);
}

// Fills the buffer with the values that define a rectangle.
function setRectangle(gl, x, y, width, height) {
  var x1 = x;
  var x2 = x + width;
  var y1 = y;
  var y2 = y + height;
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
     x1, y1,
     x2, y1,
     x1, y2,
     x1, y2,
     x2, y1,
     x2, y2]), gl.STATIC_DRAW);
}

Espero que você tenha percebido que a WebGL é, na verdade, uma API bem simples. Embora possa ficar mais complicado fazer 3D, essa complicação é adicionada por você, o programador, na forma de shaders mais complexos. A API WebGL é 2D e bastante simples.

O que significa type="x-shader/x-vertex" e type="x-shader/x-fragment"?

As tags <script> têm JavaScript por padrão. Você pode colocar "no type" ou type="javascript" ou type="text/javascript", e o navegador vai interpretar o conteúdo como JavaScript. Se você colocar qualquer outra coisa, o navegador vai ignorar o conteúdo da tag de script.

Podemos usar esse recurso para armazenar shaders em tags de script. Melhor ainda, podemos criar nosso próprio tipo e, no nosso JavaScript procurá-lo para decidir se vamos compilar o sombreador como um sombreador de vértice ou de fragmento.

Nesse caso, a função createShaderFromScriptElement procura um script com id especificado e, em seguida, analisa o type para decidir que tipo de sombreador criar.

Processamento de imagens do WebGL

O processamento de imagens é fácil no WebGL. Quão fácil? Leia abaixo.

Para desenhar imagens em WebGL, precisamos usar texturas. Da mesma forma que o WebGL espera coordenadas de clipspace ao renderizar em vez de pixels, ele espera coordenadas de textura ao ler uma textura. As coordenadas de textura vão de 0,0 a 1,0, não importa as dimensões da textura. Como estamos desenhando apenas um único retângulo (bem, dois triângulos), precisamos informar ao WebGL a que lugar na textura cada ponto do retângulo corresponde. Vamos transmitir essas informações do sombreador de vértice ao sombreador de fragmento usando um tipo especial de variável chamada de "variada". É chamado de variável porque varia. O WebGL vai interpolar os valores fornecidos no sombreador de vértice ao renderizar cada pixel usando o sombreador de fragmentos. Usando o sombreador de vértice do final da seção anterior, precisamos adicionar um atributo para transmitir coordenadas de textura e, em seguida, transmiti-las para o sombreador de fragmentos.

attribute vec2 a_texCoord;
...
varying vec2 v_texCoord;

void main() {
   ...
   // pass the texCoord to the fragment shader
   // The GPU will interpolate this value between points
   v_texCoord = a_texCoord;
}

Em seguida, fornecemos um sombreador de fragmentos para procurar cores na textura.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   // Look up a color from the texture.
   gl_FragColor = texture2D(u_image, v_texCoord);
}
</script>

Por fim, precisamos carregar uma imagem, criar uma textura e copiar a imagem para a textura. Como estamos em um navegador, as imagens são carregadas de forma assíncrona. Por isso, temos que reorganizar nosso código um pouco para esperar um pouco até que a textura carregue. Quando ele carregar, vamos desenhar.

function main() {
  var image = new Image();
  image.src = "http://someimage/on/our/server";  // MUST BE SAME DOMAIN!!!
  image.onload = function() {
    render(image);
  }
}

function render(image) {
  ...
  // all the code we had before.
  ...
  // look up where the texture coordinates need to go.
  var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");

  // provide texture coordinates for the rectangle.
  var texCoordBuffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
      0.0,  0.0,
      1.0,  0.0,
      0.0,  1.0,
      0.0,  1.0,
      1.0,  0.0,
      1.0,  1.0]), gl.STATIC_DRAW);
  gl.enableVertexAttribArray(texCoordLocation);
  gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

  // Create a texture.
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set the parameters so we can render any size image.
  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);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  // Upload the image into the texture.
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  ...
}

Isso não é demais, então vamos manipular a imagem. Que tal trocar as cores vermelho e azul?

...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...

E se quisermos fazer um processamento de imagem que realmente analise outros pixels? Como o WebGL faz referência a texturas em coordenadas de textura que vão de 0,0 a 1,0, podemos calcular quanto mover para 1 pixel com a matemática simples onePixel = 1.0 / textureSize. Aqui está um sombreador de fragmentos que calcula a média dos pixels à esquerda e à direita de cada pixel na textura.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   // compute 1 pixel in texture coordinates.
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;

   // average the left, middle, and right pixels.
   gl_FragColor = (
       texture2D(u_image, v_texCoord) +
       texture2D(u_image, v_texCoord + vec2(onePixel.x, 0.0)) +
       texture2D(u_image, v_texCoord + vec2(-onePixel.x, 0.0))) / 3.0;
}
</script>

Em seguida, precisamos transmitir o tamanho da textura do JavaScript.

...
var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
...
// set the size of the image
gl.uniform2f(textureSizeLocation, image.width, image.height);
...

Agora que sabemos como referenciar outros pixels, vamos usar um kernel de convolução para fazer vários processamentos de imagem comuns. Neste caso, usaremos um kernel 3x3. Um kernel de convolução é apenas uma matriz 3x3 em que cada entrada na matriz representa quanto multiplicar os 8 pixels ao redor do pixel que estamos renderizando. Em seguida, dividimos o resultado pelo peso do kernel (a soma de todos os valores no kernel) ou 1,0, o que for maior. Confira este artigo sobre o assunto. E este é outro artigo mostrando um código real se você escrevesse isso manualmente em C++. No nosso caso, vamos fazer esse trabalho no sombreador. Confira o novo sombreador de fragmentos.

<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// our texture
uniform sampler2D u_image;
uniform vec2 u_textureSize;
uniform float u_kernel[9];

// the texCoords passed in from the vertex shader.
varying vec2 v_texCoord;

void main() {
   vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;
   vec4 colorSum =
     texture2D(u_image, v_texCoord + onePixel * vec2(-1, -1)) * u_kernel[0] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0, -1)) * u_kernel[1] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1, -1)) * u_kernel[2] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  0)) * u_kernel[3] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  0)) * u_kernel[4] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  0)) * u_kernel[5] +
     texture2D(u_image, v_texCoord + onePixel * vec2(-1,  1)) * u_kernel[6] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 0,  1)) * u_kernel[7] +
     texture2D(u_image, v_texCoord + onePixel * vec2( 1,  1)) * u_kernel[8] ;
   float kernelWeight =
     u_kernel[0] +
     u_kernel[1] +
     u_kernel[2] +
     u_kernel[3] +
     u_kernel[4] +
     u_kernel[5] +
     u_kernel[6] +
     u_kernel[7] +
     u_kernel[8] ;

   if (kernelWeight <= 0.0) {
     kernelWeight = 1.0;
   }

   // Divide the sum by the weight but just use rgb
   // we'll set alpha to 1.0
   gl_FragColor = vec4((colorSum / kernelWeight).rgb, 1.0);
}
</script>

No JavaScript, precisamos fornecer um kernel de convolução.

...
var kernelLocation = gl.getUniformLocation(program, "u_kernel[0]");
...
var edgeDetectKernel = [
    -1, -1, -1,
    -1,  8, -1,
    -1, -1, -1
];
gl.uniform1fv(kernelLocation, edgeDetectKernel);
...

Espero que isso tenha convencido você de que o processamento de imagens no WebGL é bem simples. A seguir, vou explicar como aplicar mais de um efeito à imagem.

O que há com os prefixos a, u e v_ em variáveis em GLSL?

Isso é apenas uma convenção de nomenclatura. a_ para atributos que são os dados fornecidos por buffers. u_ para uniformes que são entradas para os shaders, v_ para variações que são valores transmitidos de um shader de vértice para um shader de fragmento e interpolados (ou variados) entre os vértices de cada pixel desenhado.

Como aplicar vários efeitos

A próxima pergunta mais óbvia para o processamento de imagens é como aplicar vários efeitos.

Você pode tentar gerar shaders dinamicamente. Forneça uma interface que permita ao usuário selecionar os efeitos que ele quer usar e, em seguida, gere um sombreador que faça todos os efeitos. Isso nem sempre é possível, mas essa técnica é usada com frequência para criar efeitos para gráficos em tempo real. Uma maneira mais flexível é usar mais duas texturas e renderizar cada uma delas, fazendo pingue-pongue para frente e para trás e aplicando o próximo efeito cada vez.

Original Image -> [Blur]        -> Texture 1
Texture 1      -> [Sharpen]     -> Texture 2
Texture 2      -> [Edge Detect] -> Texture 1
Texture 1      -> [Blur]        -> Texture 2
Texture 2      -> [Normal]      -> Canvas

Para isso, precisamos criar framebuffers. No WebGL e no OpenGL, um framebuffer é um nome ruim. Um framebuffer WebGL/OpenGL é realmente apenas uma coleção de estados e não um buffer de nenhum tipo. No entanto, ao anexar uma textura a um framebuffer, podemos renderizar essa textura. Primeiro, vamos transformar o código antigo de criação de texturas em uma função.

function createAndSetupTexture(gl) {
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // Set up texture so we can render any size image and so we are
  // working with pixels.
  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);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);

  return texture;
}

// Create a texture and put the image in it.
var originalImageTexture = createAndSetupTexture(gl);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

Agora vamos usar essa função para criar mais duas texturas e anexá-las a dois framebuffers.

// create 2 textures and attach them to framebuffers.
var textures = [];
var framebuffers = [];
for (var ii = 0; ii < 2; ++ii) {
  var texture = createAndSetupTexture(gl);
  textures.push(texture);

  // make the texture the same size as the image
  gl.texImage2D(
      gl.TEXTURE_2D, 0, gl.RGBA, image.width, image.height, 0,
      gl.RGBA, gl.UNSIGNED_BYTE, null);

  // Create a framebuffer
  var fbo = gl.createFramebuffer();
  framebuffers.push(fbo);
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  // Attach a texture to it.
  gl.framebufferTexture2D(
      gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
}

Agora vamos criar um conjunto de kernels e uma lista deles para aplicar.

// Define several convolution kernels
var kernels = {
  normal: [
    0, 0, 0,
    0, 1, 0,
    0, 0, 0
  ],
  gaussianBlur: [
    0.045, 0.122, 0.045,
    0.122, 0.332, 0.122,
    0.045, 0.122, 0.045
  ],
  unsharpen: [
    -1, -1, -1,
    -1,  9, -1,
    -1, -1, -1
  ],
  emboss: [
     -2, -1,  0,
     -1,  1,  1,
      0,  1,  2
  ]
};

// List of effects to apply.
var effectsToApply = [
  "gaussianBlur",
  "emboss",
  "gaussianBlur",
  "unsharpen"
];

E, por fim, vamos aplicar cada uma, alternando a textura que estamos renderizando.

// start with the original image
gl.bindTexture(gl.TEXTURE_2D, originalImageTexture);

// don't y flip images while drawing to the textures
gl.uniform1f(flipYLocation, 1);

// loop through each effect we want to apply.
for (var ii = 0; ii < effectsToApply.length; ++ii) {
  // Setup to draw into one of the framebuffers.
  setFramebuffer(framebuffers[ii % 2], image.width, image.height);

  drawWithKernel(effectsToApply[ii]);

  // for the next draw, use the texture we just rendered to.
  gl.bindTexture(gl.TEXTURE_2D, textures[ii % 2]);
}

// finally draw the result to the canvas.
gl.uniform1f(flipYLocation, -1);  // need to y flip for canvas
setFramebuffer(null, canvas.width, canvas.height);
drawWithKernel("normal");

function setFramebuffer(fbo, width, height) {
  // make this the framebuffer we are rendering to.
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);

  // Tell the shader the resolution of the framebuffer.
  gl.uniform2f(resolutionLocation, width, height);

  // Tell webgl the viewport setting needed for framebuffer.
  gl.viewport(0, 0, width, height);
}

function drawWithKernel(name) {
  // set the kernel
  gl.uniform1fv(kernelLocation, kernels[name]);

  // Draw the rectangle.
  gl.drawArrays(gl.TRIANGLES, 0, 6);
}

Algumas coisas que preciso revisar.

Chamar gl.bindFramebuffer com null informa ao WebGL que você quer renderizar o canvas em vez de um dos framebuffers. O WebGL precisa converter do espaço de recorte de volta para pixels. Ele faz isso com base nas configurações de gl.viewport. O padrão das configurações de gl.viewport é o tamanho da tela ao inicializar o WebGL. Como os framebuffers que estão sendo renderizados têm um tamanho diferente do canvas, precisamos definir a janela de visualização corretamente. Por fim, nos exemplos dos conceitos básicos do WebGL, invertemos a coordenada Y ao renderizar porque o WebGL mostra o canvas com 0,0 sendo o canto inferior esquerdo, em vez do canto superior esquerdo 2D, que é mais tradicional. Isso não é necessário ao renderizar em um framebuffer. Como o framebuffer nunca é mostrado, a parte de cima e de baixo é irrelevante. O que importa é que o pixel 0,0 no framebuffer corresponde a 0,0 nos cálculos. Para lidar com isso, foi possível definir se a inversão seria feita ou não adicionando mais uma entrada ao sombreador.

<script id="2d-vertex-shader" type="x-shader/x-vertex">
...
uniform float u_flipY;
...

void main() {
   ...
   gl_Position = vec4(clipSpace * vec2(1, u_flipY), 0, 1);
   ...
}
</script>

E podemos defini-lo ao renderizar com

...
var flipYLocation = gl.getUniformLocation(program, "u_flipY");
...
// don't flip
gl.uniform1f(flipYLocation, 1);
...
// flip
gl.uniform1f(flipYLocation, -1);

Mantive este exemplo simples usando um único programa GLSL que pode alcançar vários efeitos. Se você quiser fazer o processamento de imagens completo, provavelmente precisará de muitos programas GLSL. Um programa para ajuste de matiz, saturação e luminância. Outro para brilho e contraste. Um para inverter, outro para ajustar níveis etc. Você precisa mudar o código para alternar os programas GLSL e atualizar os parâmetros para esse programa específico. Eu considerei escrever esse exemplo, mas é melhor deixar o exercício para o leitor, porque vários programas GLSL, cada um com as próprias necessidades de parâmetro, provavelmente significam uma grande refatoração para evitar que tudo se torne uma grande bagunça. Esperamos que este e os exemplos anteriores tenham ajudado a tornar o WebGL mais acessível. Além disso, esperamos que começar com o 2D facilite a compreensão do WebGL. Se eu tiver tempo, vou tentar escrever mais alguns artigos sobre como fazer 3D e mais detalhes sobre o que o WebGL realmente faz.

WebGL e Alfa

Percebi que alguns desenvolvedores do OpenGL estão tendo problemas com a forma como a WebGL trata o alfa no backbuffer (ou seja, a tela). Por isso, achei que seria bom discutir algumas das diferenças entre a WebGL e o OpenGL relacionadas ao alfa.

A maior diferença entre o OpenGL e o WebGL é que o OpenGL renderiza para um backbuffer que não é composto com nada, ou seja, não é composto por nada pelo gerenciador de janelas do SO. Portanto, não importa qual é o alfa. O WebGL é composto pelo navegador com a página da Web, e o padrão é usar o alfa pré-multiplicado, da mesma forma que as tags <img> .png com transparência e as tags de tela 2D. O WebGL tem várias maneiras de tornar isso mais parecido com o OpenGL.

#1) Informar ao WebGL que você quer que ele seja composto com alfa não pré-multiplicado

gl = canvas.getContext("experimental-webgl", {premultipliedAlpha: false});

O padrão é verdadeiro. O resultado ainda será composto na página com qualquer cor de plano de fundo que esteja abaixo da tela (a cor de plano de fundo da tela, a cor de plano de fundo do contêiner da tela, a cor de plano de fundo da página, o conteúdo por trás da tela se ela tiver um z-index > 0 etc.). Em outras palavras, o CSS de cor define essa área da página da Web. Uma boa maneira de descobrir se você tem problemas com o Alfa é definir o plano de fundo da tela para uma cor brilhante, como o vermelho. Você verá imediatamente o que está acontecendo.

<canvas style="background: red;"></canvas>

Você também pode definir como preto, o que vai ocultar todos os problemas alfa que você tiver.

2) Informar ao WebGL que você não quer Alfa no backbuffer

gl = canvas.getContext("experimental-webgl", {alpha: false});

Isso vai fazer com que ele se comporte mais como o OpenGL, já que o backbuffer terá apenas RGB. Essa é provavelmente a melhor opção, porque um bom navegador pode detectar que você não tem alfa e otimizar a forma como o WebGL é composto. Isso também significa que não haverá alfa no backbuffer. Portanto, se você estiver usando o alfa no backbuffer para algum propósito, isso pode não funcionar. Poucos apps que eu conheço usam a versão Alfa no backbuffer. Acho que isso deveria ser o padrão.

#3) Limpar alfa no final da renderização

..
renderScene();
..
// Set the backbuffer's alpha to 1.0
gl.clearColor(1, 1, 1, 1);
gl.colorMask(false, false, false, true);
gl.clear(gl.COLOR_BUFFER_BIT);

A limpeza geralmente é muito rápida, porque há um caso especial para isso na maioria dos hardwares. Fiz isso na maioria das demonstrações. Se eu fosse esperto, mudaria para o método 2 acima. Talvez eu faça isso logo depois de postar. Parece que a maioria das bibliotecas do WebGL usa esse método por padrão. Os poucos desenvolvedores que usam a versão Alfa para criar efeitos podem pedir. O restante vai ter o melhor desempenho e menos surpresas.

#4) Limpar o Alfa uma vez e não renderizar mais

// At init time. Clear the back buffer.
gl.clearColor(1,1,1,1);
gl.clear(gl.COLOR_BUFFER_BIT);

// Turn off rendering to alpha
gl.colorMask(true, true, true, false);

Se você estiver renderizando para seus próprios framebuffers, talvez seja necessário reativar a renderização para Alpha e desativá-la novamente quando mudar para a tela.

#5) Como lidar com imagens

Além disso, se você estiver carregando arquivos PNG com alfa em texturas, o padrão é que o alfa seja pré-multiplicado, o que geralmente NÃO é a forma como a maioria dos jogos funciona. Se você quiser impedir esse comportamento, informe o WebGL com

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

6) Usar uma equação de combinação que funcione com alfa pré-multiplicado

Quase todos os apps OpenGL que eu escrevi ou em que trabalhei usam

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Isso funciona para texturas alfa não pré-multiplicadas. Se você realmente quer trabalhar com texturas alfa pré-multiplicadas, então você provavelmente

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Esses são os métodos dos quais eu conheço. Se você souber de mais, poste abaixo.