Conceitos básicos do WebGL

Gregg Tavares
Gregg Tavares

Conceitos básicos do WebGL

A WebGL torna possível exibir gráficos 3D em tempo real incríveis no seu navegador, mas o que muitas pessoas não sabem é que a WebGL é, na verdade, uma API 2D, não uma API 3D. Vou explicar.

O WebGL se importa apenas com duas coisas. Coordenadas de Clipspace em 2D e cores. Seu trabalho como programador usando o WebGL é oferecer esses dois itens à WebGL. Você fornece dois "sombreadores" para fazer isso. Um sombreador de vértice que fornece as coordenadas do espaço de corte 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. Veja um exemplo simples de WebGL que mostra o WebGL na 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 clipspace 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 passar nossos dados de posição diretamente. Como os dados de posição já estão no clipspace, não há trabalho a ser feito. Se quiser usar o 3D, você decide fornecer sombreadores que convertem de 3D para 2D, porque o WebGL É uma API 2D. Para coisas 2D, você provavelmente prefere trabalhar em pixels em vez de clipspace. Vamos mudar o sombreador para podermos fornecer retângulos em pixels e convertê-los em clipspace. Este é o novo sombreador 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);

Você pode notar que o retângulo está perto da parte inferior dessa área. O WebGL considera o canto inferior esquerdo como 0,0. Para que ele seja o canto superior esquerdo mais tradicional usado para APIs de gráficos 2D, basta girar 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 tornar as cores configuráveis. Primeiro, fazemos com que o sombreador de fragmento receba uma entrada de uniforme de cores.

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

uniform vec4 u_color;

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

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

...

  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 é uma API bem simples. Embora fazer 3D seja mais complicado, você, o programador, adiciona a complicação na forma de sombreadores mais complexos. A API WebGL em si é 2D e bastante simples.

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

O padrão das tags <script> é ter JavaScript. Você pode colocar nenhum tipo ou colocar type="javascript" ou type="text/javascript" e o navegador interpretará o conteúdo como JavaScript. Se você colocar mais alguma coisa, o navegador ignorará o conteúdo da tag do script.

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

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

Processamento de imagens 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, o WebGL espera coordenadas de textura ao ler uma textura. As coordenadas de textura vão de 0,0 a 1,0, independentemente das dimensões da textura. Como estamos apenas desenhando 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 para o sombreador de fragmento usando um tipo especial de variável chamada "variável". É chamada de variável porque ela varia. O WebGL interpola os valores fornecidos no sombreador de vértice à medida que desenha cada pixel usando o sombreador de fragmento. Usando o sombreador de vértice do final da seção anterior, precisamos adicionar um atributo para transmitir as coordenadas de textura e, em seguida, transmiti-las ao sombreador de fragmento.

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 fragmento para procurar cores da 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 copiá-la nela. Como estamos em um navegador, as imagens são carregadas de forma assíncrona. Portanto, precisamos reorganizar nosso código um pouco para aguardar o carregamento da textura. Depois que ele for carregado, será desenhado.

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);
  ...
}

Não é muito emocionante, então vamos manipular essa imagem. Que tal apenas trocar vermelho e azul?

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

E se quisermos fazer um processamento de imagem que realmente analisa 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 usando o cálculo simples onePixel = 1.0 / textureSize. Aqui está um sombreador de fragmento que faz a média dos pixels esquerdo e direito 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 realizar vários processamentos comuns de imagens. Neste caso, usaremos um kernel 3x3. Um kernel de convolução é apenas uma matriz 3x3 em que cada entrada na matriz representa o quanto os 8 pixels ao redor do pixel que estamos renderizando devem ser multiplicados. Depois, dividimos o resultado pelo peso do kernel (a soma de todos os valores nele) ou 1,0, que sempre for maior. Confira um artigo excelente sobre isso. Confira outro artigo que mostra um código real para escrever manualmente em C++. No nosso caso, vamos fazer esse trabalho no sombreador. Aqui está o novo sombreador de fragmento.

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

Em JavaScript, precisamos de 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ê que o processamento de imagens no WebGL é muito simples. A seguir, veremos como aplicar mais de um efeito à imagem.

O que são os prefixos a, u e v_ das variáveis na GLSL?

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

Aplicar vários efeitos

A próxima pergunta mais óbvia sobre o processamento de imagens é: como aplicar vários efeitos?

Você pode tentar gerar sombreadores rapidamente. 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 eles. Isso nem sempre é possível, embora essa técnica seja 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 o pingue-pongue para frente e para trás e aplicando o próximo efeito a 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. Em WebGL e OpenGL, um Framebuffer é, na verdade, um nome ruim. Um framebuffer WebGL/OpenGL é realmente apenas uma coleção de estados, e não um buffer de qualquer tipo. Porém, anexando uma textura a um framebuffer, podemos renderizar essa textura. Primeiro, vamos transformar o antigo código de criação de textura 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);

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"
];

Por fim, vamos aplicar cada uma delas, pincelando 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 rever.

Chamar gl.bindFramebuffer com null informa ao WebGL que você quer renderizar na tela e não em um dos framebuffers. O WebGL precisa converter o clipspace em pixels. Isso é feito com base nas configurações do gl.viewport. As configurações de gl.viewport assumem como padrão o tamanho da tela quando inicializamos o WebGL. Como os framebuffers que estamos renderizando têm um tamanho diferente, precisamos definir a janela de visualização corretamente. Por fim, nos exemplos de fundamentos do WebGL, invertemos a coordenada Y ao renderizar porque o WebGL exibe a tela com 0,0 sendo 0,0 no canto inferior esquerdo, em vez do mais tradicional para o 2D no canto superior esquerdo. Isso não é necessário ao renderizar para um framebuffer. Como o framebuffer nunca é exibido, qual parte é superior e inferior é irrelevante. O importante é que o pixel 0,0 no framebuffer corresponda a 0,0 em nossos cálculos. Para lidar com isso, adicionei mais uma entrada ao sombreador para definir se a tela será invertida ou não.

<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 então podemos defini-lo ao renderizarmos com

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

Mantenhai esse exemplo simples usando um único programa GLSL que pode atingir vários efeitos. Para processar todas as imagens, você provavelmente precisa de muitos programas GLSL. Um programa para ajuste de matiz, saturação e luminância. Outra para brilho e contraste. um para inverter, outro para ajustar níveis etc. Você precisaria mudar o código para alternar entre programas GLSL e atualizar os parâmetros desse programa específico. Pensei em escrever esse exemplo, mas é melhor deixar para o leitor, porque vários programas GLSL, cada um com suas próprias necessidades de parâmetro, provavelmente significam uma grande refatoração para evitar que tudo se torne uma grande confusão. Espero que isso e os exemplos anteriores tenham feito com que o WebGL pareça um pouco mais acessível e espero que começar com 2D ajude a tornar o WebGL um pouco mais fácil de entender. Se eu tiver tempo, vou tentar escrever mais alguns artigos sobre como fazer 3D, além de mais detalhes sobre o que o WebGL está realmente fazendo nos bastidores.

WebGL e Alfa

Notei que alguns desenvolvedores OpenGL tendo problemas com a forma como o WebGL trata alfa no backbuffer (ou seja, a tela), então achei que seria uma boa ideia examinar algumas das diferenças entre WebGL e OpenGL relacionadas à Alfa.

A maior diferença entre OpenGL e WebGL é que o OpenGL renderiza em um backbuffer que não é composto com nada, ou que efetivamente não é composto com nada pelo gerenciador de janelas do SO, não importa qual é seu alfa. O WebGL é composto pelo navegador com a página da Web, e o padrão é usar tags alfa pré-multiplicadas da mesma forma que tags .png <img> com transparência e telas 2d. O WebGL tem várias maneiras de deixar isso mais parecido com o OpenGL.

1) Informar ao WebGL que você quer que ele seja composto com Alfa não pré-multiplicada

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

O padrão é "true". É claro que o resultado ainda será composto na página, com qualquer cor de fundo que esteja abaixo da tela (a cor de plano de fundo do canvas, a cor de fundo do contêiner da tela, a cor de fundo da página, o que está atrás da tela se a tela tiver um Z-index > 0 etc.). Em outras palavras, a cor que o CSS define para essa área da página da Web. Uma boa maneira de descobrir se você tem algum problema com a versão Alfa é definir o plano de fundo da tela com uma cor brilhante, como vermelho. Assim, você vai entender o que está acontecendo.

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

Também é possível definir como preto, o que vai ocultar os problemas Alfa que você tenha.

2) Informar à WebGL que você não quer a versão Alfa no buffer

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

Isso fará com que ele funcione mais como o OpenGL, já que o backbuffer só terá RGB. Essa é provavelmente a melhor opção porque um bom navegador poderia perceber que você não tem alfa e, na verdade, otimizar a forma como o WebGL é composto. Isso também significa que ele não terá Alfa no backbuffer. Portanto, se você estiver usando Alfa no backbuffer para algum propósito que pode não funcionar para você. Poucos apps que conheço usam a versão Alfa no buffer. Sem dúvida, esse deveria ter sido o padrão.

3) Apagar o Alfa ao 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, pois há um caso especial para ela na maioria dos hardwares. Fiz isso na maioria das minhas demonstrações. Se eu fosse inteligente, mudaria para o método no 2 acima. Talvez eu faça isso logo depois de postar. Parece que a maioria das bibliotecas WebGL deveria usar esse método por padrão. Esses poucos desenvolvedores que realmente usam a versão alfa para criar efeitos podem pedir isso. O restante terá apenas o melhor desempenho e as menos surpresas.

4) Limpe o Alfa uma vez e não renderize mais para ele

// 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);

Obviamente, se você estiver renderizando em seus próprios framebuffers, pode ser necessário reativar a renderização para alfa e, em seguida, desativá-la quando alternar para a renderização no canvas.

5) Gerenciamento de imagens

Além disso, se você carregar arquivos PNG com alpha em texturas, o padrão é que o alfa deles seja pré-multiplicado, o que geralmente NÃO é a maneira como a maioria dos jogos funciona. Se quiser evitar esse comportamento, você precisa informar à WebGL

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

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

Quase todos os apps OpenGL que escrevi ou trabalhei usam

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

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

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Esses são os métodos que conheço. Se você souber de mais, poste-as abaixo.