Aspectos básicos de WebGL

Gregg Tavares
Gregg Tavares

Conceptos básicos de WebGL

WebGL permite mostrar gráficos 3D increíbles en tiempo real en tu navegador, pero lo que muchas personas no saben es que WebGL es en realidad una API 2D, no una API 3D. Lo explicaré ahora.

A WebGL solo le interesan 2 cosas. Coordenadas del espacio de recorte en 2D y colores. Tu trabajo como programador que usa WebGL es proporcionarle esos 2 elementos. Para ello, proporcionas 2 "sombreadores". Un sombreador de vértices que proporciona las coordenadas del espacio de recorte y un sombreador de fragmentos que proporciona el color. Las coordenadas del espacio de clip siempre van de -1 a +1, sin importar el tamaño de tu lienzo. Aquí hay un ejemplo simple de WebGL que muestra WebGL en su forma más sencilla.

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

Estos son los 2 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>

Una vez más, las coordenadas del espacio de recorte siempre van de -1 a +1, independientemente del tamaño del lienzo. En el caso anterior, puedes ver que no hacemos nada más que pasar nuestros datos de posición directamente. Dado que los datos de posición ya están en el espacio de recorte, no hay nada que hacer. Si quieres 3D, depende de ti proporcionar sombreadores que conviertan de 3D a 2D, ya que WebGL ES UNA API DE 2D. Para los elementos 2D, es probable que prefieras trabajar en píxeles en lugar de espacio de recorte, así que cambiemos el sombreador para que podamos proporcionar rectángulos en píxeles y que se conviertan en espacio de recorte. Este es el nuevo sombreador de vértices

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

Ahora podemos cambiar nuestros datos de espacio de recorte a píxeles.

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

Puedes notar que el rectángulo está cerca de la parte inferior de esa área. WebGL considera que la esquina inferior izquierda es 0,0. Para que sea la esquina superior izquierda más tradicional, que se usa para las APIs de gráficos en 2D, simplemente cambiamos la coordenada Y.

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

Hagamos que el código que define un rectángulo sea una función para que podamos llamarlo para rectángulos de diferentes tamaños. Mientras estamos en eso, haremos que el color se pueda configurar. Primero, hacemos que el sombreador de fragmentos tome una entrada de color 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 es el nuevo código que dibuja 50 rectángulos en lugares y colores aleatorios.

...

  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 puedas ver que WebGL es una API bastante simple. Si bien puede complicarse hacer 3D, esa complicación la agregas tú, el programador, en forma de sombreadores más complejos. La API de WebGL es 2D y bastante simple.

¿Qué significan type="x-shader/x-vertex" y type="x-shader/x-fragment"?

Las etiquetas <script> incluyen JavaScript de forma predeterminada. Puedes no colocar ningún tipo o puedes colocar type="javascript" o type="text/javascript", y el navegador interpretará el contenido como JavaScript. Si colocas algo más, el navegador ignorará el contenido de la etiqueta de la secuencia de comandos.

Podemos usar esta función para almacenar sombreadores en etiquetas de secuencias de comandos. Aún mejor, podemos crear nuestro propio tipo y, en nuestro código de JavaScript, podemos buscarlo para decidir si compilar el sombreador como un sombreador de vértices o un sombreador de fragmentos.

En este caso, la función createShaderFromScriptElement busca una secuencia de comandos con id especificada y, luego, observa type para decidir qué tipo de sombreador crear.

Procesamiento de imágenes WebGL

El procesamiento de imágenes es sencillo en WebGL. ¿Qué tan fácil? Lee a continuación.

Para dibujar imágenes en WebGL, debemos usar texturas. De manera similar a la forma en que WebGL espera coordenadas de espacio de recorte cuando renderiza en lugar de píxeles, WebGL espera coordenadas de textura cuando lee una textura. Las coordenadas de textura van de 0.0 a 1.0, independientemente de las dimensiones de la textura. Como solo estamos dibujando un único rectángulo (bueno, 2 triángulos), necesitamos indicarle a WebGL a qué lugar de la textura corresponde cada punto del rectángulo. Pasaremos esta información del sombreador de vértices al sombreador de fragmentos con un tipo especial de variable llamada "varying". Se llama variable porque varía. WebGL interpolará los valores que proporcionamos en el sombreador de vértices a medida que dibuja cada píxel con el sombreador de fragmentos. Con el sombreador de vértices del final de la sección anterior, debemos agregar un atributo para pasar las coordenadas de textura y, luego, pasarlas al 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;
}

Luego, proporcionamos un sombreador de fragmentos para buscar colores en la 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 último, debemos cargar una imagen, crear una textura y copiar la imagen en ella. Como estamos en un navegador, las imágenes se cargan de forma asíncrona, por lo que debemos reorganizar nuestro código un poco para esperar a que se cargue la textura. Una vez que se cargue, lo dibujaremos.

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

No es muy emocionante, así que manipulemos esa imagen. ¿Qué tal si intercambiamos el rojo y el azul?

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

¿Qué sucede si queremos realizar un procesamiento de imágenes que realmente analice otros píxeles? Dado que WebGL hace referencia a texturas en coordenadas de textura que van de 0.0 a 1.0, podemos calcular cuánto mover para 1 píxel con la matemática simple onePixel = 1.0 / textureSize. Este es un sombreador de fragmentos que promedia los píxeles izquierdo y derecho de cada píxel en la 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>

Luego, debemos pasar el tamaño de la textura desde JavaScript.

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

Ahora que sabemos cómo hacer referencia a otros píxeles, usemos un kernel de convolución para realizar una serie de procesamientos de imágenes comunes. En este caso, usaremos un kernel de 3 × 3. Un kernel de convolución es solo una matriz de 3 × 3 en la que cada entrada representa cuánto multiplicar los 8 píxeles alrededor del píxel que renderizamos. Luego, dividimos el resultado por el peso del kernel (la suma de todos los valores del kernel) o 1.0, lo que sea mayor. Aquí hay un buen artículo al respecto. Y este es otro artículo en el que se muestra código real si lo escribieras a mano en C++. En nuestro caso, haremos ese trabajo en el sombreador, así que este es el nuevo 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>

En JavaScript, debemos proporcionar un kernel de convolución.

...
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 esto te haya convencido de que el procesamiento de imágenes en WebGL es bastante simple. A continuación, explicaré cómo aplicar más de un efecto a la imagen.

¿Qué sucede con los prefijos a, u y v_ de las variables en GLSL?

Es solo una convención de nombres. a_ para los atributos, que son los datos que proporcionan los búferes. u_ para los uniformes que son entradas para los sombreadores, v_ para las variaciones que son valores que se pasan de un sombreador de vértices a un sombreador de fragmentos y se interpolan (o varían) entre los vértices para cada píxel dibujado.

Cómo aplicar varios efectos

La siguiente pregunta más obvia para el procesamiento de imágenes es cómo aplicar varios efectos.

Bueno, podrías intentar generar sombreadores sobre la marcha. Proporciona una IU que le permita al usuario seleccionar los efectos que desea usar y, luego, genera un sombreador que realice todos los efectos. Sin embargo, es posible que no siempre sea posible, aunque esa técnica se use con frecuencia para crear efectos para gráficos en tiempo real. Una forma más flexible es usar 2 texturas más y renderizar cada una a su vez, alternando entre ellas y aplicando el siguiente efecto 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 ello, necesitamos crear búferes de fotogramas. En WebGL y OpenGL, un búfer de trama es en realidad un nombre poco adecuado. Un búfer de fotogramas de WebGL/OpenGL es en realidad solo una colección de estados y no un búfer de ningún tipo. Sin embargo, si adjuntamos una textura a un búfer de trama, podemos renderizar en esa textura. Primero, convertiremos el código de creación de textura anterior en una función.

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

Ahora, usemos esa función para crear 2 texturas más y adjuntarlas a 2 búferes de trama.

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

Ahora, hagamos un conjunto de kernels y, luego, una lista de ellos para aplicarlos.

// 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 último, apliquemos cada uno: ping pong, qué textura 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);
}

Hay algunas cosas que debo revisar.

Llamar a gl.bindFramebuffer con null le indica a WebGL que deseas renderizar en el lienzo en lugar de en uno de tus búferes de trama. WebGL tiene que volver a convertir el espacio de recorte en píxeles. Lo hace según la configuración de gl.viewport. La configuración de gl.viewport se establece de forma predeterminada en el tamaño del lienzo cuando inicializamos WebGL. Dado que los búferes de trama en los que renderizamos tienen un tamaño diferente al lienzo, debemos configurar el viewport de forma adecuada. Por último, en los ejemplos de conceptos básicos de WebGL, cambiamos la coordenada Y al renderizar porque WebGL muestra el lienzo en el que 0,0 es la esquina inferior izquierda en lugar de la más tradicional para la parte superior izquierda de 2D. No es necesario cuando se renderiza en un búfer de trama. Dado que el búfer de trama nunca se muestra, es irrelevante qué parte es la superior y la inferior. Lo único que importa es que el píxel 0,0 en el búfer de trama corresponda a 0,0 en nuestros cálculos. Para lidiar con esto, hice posible configurar si se volteaba o no agregando una entrada más al 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>

Y, luego, podemos configurarlo cuando rendericemos con

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

Mantuve la simplicidad de este ejemplo usando un solo programa GLSL que puede lograr varios efectos. Si quieres realizar un procesamiento de imágenes completo, es probable que necesites muchos programas GLSL. Es un programa para ajustar el tono, la saturación y la luminancia. Otro para el brillo y el contraste. Uno para invertir, otro para ajustar niveles, etcétera. Deberás cambiar el código para cambiar los programas GLSL y actualizar los parámetros de ese programa en particular. Consideré escribir ese ejemplo, pero es un ejercicio que es mejor dejarle al lector porque varios programas GLSL, cada uno con sus propias necesidades de parámetros, probablemente impliquen una refactorización importante para evitar que todo se convierta en un gran lío de espagueti. Espero que este y los ejemplos anteriores hayan hecho que WebGL parezca un poco más accesible y que comenzar con 2D ayude a que WebGL sea un poco más fácil de entender. Si tengo tiempo, intentaré escribir algunos artículos más sobre cómo hacer 3D, así como más detalles sobre lo que realmente hace WebGL en segundo plano.

WebGL y Alfa

Noté que algunos desarrolladores de OpenGL tienen problemas con la forma en que WebGL trata el alfa en el búfer de pantalla posterior (es decir, el lienzo), por lo que pensé que sería bueno repasar algunas de las diferencias entre WebGL y OpenGL relacionadas con el alfa.

La mayor diferencia entre OpenGL y WebGL es que OpenGL renderiza en un búfer de pantalla posterior que no se compone con nada, o que el administrador de ventanas del SO no compone con nada, por lo que no importa cuál sea tu alfa. El navegador compone WebGL con la página web y la configuración predeterminada es usar alfa premultiplicado, al igual que las etiquetas <img> de .png con transparencia y etiquetas de lienzo 2D. WebGL tiene varias formas de hacer que esto se parezca más a OpenGL.

#1) Dile a WebGL que quieres que se componga con una alfa no multiplicada

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

El valor predeterminado es verdadero. Por supuesto, el resultado se seguirá componiendo en la página con cualquier color de fondo que quede debajo del lienzo (el color de fondo del lienzo, el color de fondo del contenedor del lienzo, el color de fondo de la página, el contenido detrás del lienzo si el lienzo tiene un índice z > 0, etc.), en otras palabras, el color que define el CSS para esa área de la página web. Una buena manera de saber si tienes algún problema con la transparencia es establecer el fondo del lienzo en un color brillante, como el rojo. Verás de inmediato lo que sucede.

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

También puedes establecerlo en negro, lo que ocultará cualquier problema de alfa que tengas.

#2) Dile a WebGL que no quieres alfa en el búfer de pantalla posterior

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

Esto hará que actúe como OpenGL, ya que el búfer de reserva solo tendrá RGB. Esta es probablemente la mejor opción porque un buen navegador podría ver que no tienes alfa y optimizar la forma en que se compone WebGL. Por supuesto, eso también significa que no tendrá alfa en el búfer de pantalla posterior, por lo que, si usas alfa en el búfer de pantalla posterior para algún propósito, es posible que no funcione. Pocas apps que conozco usan alfa en el búfer de pantalla posterior. Creo que, en realidad, esta debería haber sido la opción predeterminada.

#3) Borra el alfa al final de la renderización

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

El borrado suele ser muy rápido, ya que existe un caso especial para este proceso en la mayoría del hardware. Hice esto en la mayoría de mis demostraciones. Si fuera inteligente, cambiaría al método 2 anterior. Tal vez lo haga justo después de publicar esto. Parece que la mayoría de las bibliotecas de WebGL deberían usar este método de forma predeterminada. Solo los pocos desarrolladores que usan alfa para componer efectos pueden pedirlo. El resto obtendrá el mejor rendimiento y las menos sorpresas.

#4) Borra el canal alfa una vez y deja de renderizarlo

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

Por supuesto, si renderizas en tus propios búferes de trama, es posible que debas volver a activar la renderización en alfa y, luego, desactivarla cuando cambies a la renderización en el lienzo.

#5) Manejo de imágenes

Además, si cargas archivos PNG con alfa en texturas, el valor predeterminado es que su alfa esté multiplicado previamente, que generalmente NO es la forma en que la mayoría de los juegos hacen cosas. Si quieres prevenir ese comportamiento, debes indicarle a WebGL con

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

#6) Usar una ecuación de combinación que funcione con alfa multiplicada previamente

Casi todas las apps de OpenGL que escribí o en las que trabajé usan

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

Eso funciona para texturas alfa no multiplicadas previamente. Si realmente quieres trabajar con texturas alfa multiplicadas previamente, es probable que quieras

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Esos son los métodos que conozco. Si conoces más, publícalos a continuación.