Aspectos básicos de WebGL

Gregg Tavares
Gregg Tavares

Aspectos básicos de WebGL

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

WebGL solo se preocupa por 2 cosas: Coordenadas de espacio de recorte en 2D y colores. Tu trabajo como programador que usa WebGL es proporcionarle a WebGL estas 2 cosas. Para ello, debes proporcionar 2 "sombreadores". Un sombreador de Vertex que proporciona las coordenadas de clipspace y un sombreador de fragmentos que proporciona el color. Las coordenadas del espacio de recorte siempre van de -1 a +1, independientemente del tamaño de tu lienzo. A continuación, te mostramos 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>

Nuevamente, las coordenadas de clipspace 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. Como los datos de posición ya están en el espacio de clips, no hay trabajo que hacer. Si deseas obtener 3D, depende de ti proporcionar sombreadores que conviertan de 3D a 2D porque WebGL ES una API en 2D. Para elementos 2D, probablemente preferirías trabajar en píxeles en lugar de espacio de recorte, así que cambiemos el sombreador para que podamos suministrar rectángulos en píxeles y hacer que se convierta 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 un 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 usada para las APIs de gráficos 2D, solo giramos la coordenada Y.

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

Hagamos el código que define un rectángulo en una función para que podamos llamarlo para rectángulos de diferentes tamaños. Mientras estemos trabajando 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>

Y 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 resultar más complicado crear 3D, el programador agrega esta complicación en forma de sombreadores más complejos. La API de WebGL en sí misma es 2D y es bastante simple.

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

Las etiquetas <script> tienen JavaScript de forma predeterminada. Puedes no escribir ningún tipo, o bien colocar type="javascript" o type="text/javascript", y el navegador interpretará el contenido como JavaScript. Si agregas cualquier otra cosa, 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 buscarlo en nuestro código JavaScript para decidir si compilar el sombreador como sombreador de vértices o de fragmentos.

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

Procesamiento de imágenes con WebGL

Procesar 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 como WebGL espera las coordenadas de clipspace cuando se renderiza en lugar de píxeles, WebGL espera coordenadas de texturas al leer una textura. Las coordenadas de textura van de 0.0 a 1.0 independientemente de las dimensiones de la textura. Dado que solo dibujamos un rectángulo (bueno, 2 triángulos), debemos 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 denomina 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 texturas 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 los colores de 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, tenemos que cargar una imagen, crear una textura y copiar la imagen en la textura. Como las imágenes de un navegador se cargan de forma asíncrona, debemos reorganizar un poco nuestro código 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 manipule esa imagen. ¿Y si solo cambias el rojo por el azul?

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

¿Qué sucede si queremos realizar un procesamiento de imágenes que realmente mire otros píxeles? Dado que WebGL hace referencia a las texturas en coordenadas de texturas que van de 0.0 a 1.0, podemos calcular cuánto se moverá en 1 píxel con una matemática simple onePixel = 1.0 / textureSize. A continuación, se muestra un sombreador de fragmentos que promedia los píxeles izquierdo y derecho de cada píxel de 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 hacer un procesamiento de imágenes común. En este caso, usaremos un kernel de 3x3. Un kernel de convolución es solo una matriz de 3×3 en la que cada entrada de la matriz representa cuánto se deben multiplicar los 8 píxeles alrededor del píxel que estamos renderizando. Luego, dividimos el resultado por el peso del kernel (la suma de todos los valores del kernel) o 1.0, que sea mayor. Este es un buen artículo al respecto. Y este es otro artículo que muestra código real si lo escribiras de forma manual en C++. En nuestro caso, haremos ese trabajo en el sombreador, así que aquí está 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, necesitamos 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?

Esa es solo una convención de nomenclatura. a_ para los atributos, que son los datos proporcionados por los búferes u_ para uniformes que son entradas a los sombreadores, v_ para valores variables que son valores que se pasan de un sombreador de vértices a un sombreador de fragmentos y que se interpolan (o varían) entre los vértices de 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.

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. Aunque no siempre sea posible, esa técnica se suele usar para crear efectos para gráficos en tiempo real. Una forma más flexible es usar 2 texturas más y renderizarlas en cada una de ellas, hacer ping pong de un lado a otro, y aplicar 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, el búfer de fotogramas es un nombre deficiente. Un búfer de fotogramas WebGL/OpenGL en realidad es solo una colección de estados y no un búfer de ningún tipo. Sin embargo, al adjuntar una textura a un búfer de fotogramas, podemos renderizarla en esa textura. Primero, convirtamos el código de creación de texturas 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 usaremos esa función para crear 2 texturas más y adjuntarlas a 2 búferes de fotogramas.

// 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 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 último, apliquemos cada una, ping ponging 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);
}

Algunas cosas que debería repasar.

Llamar a gl.bindFramebuffer con null le indica a WebGL que quieres renderizar el lienzo en el lienzo en lugar de hacerlo en uno de los búferes de fotogramas. 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. Como los búferes de fotogramas en los que renderizaremos tienen un tamaño diferente, el lienzo debe configurarse como el viewport de forma adecuada. Por último, en los ejemplos de los aspectos básicos de WebGL, giramos la coordenada Y durante la renderización porque WebGL muestra el lienzo en el que 0,0 es la esquina inferior izquierda en lugar de la esquina superior izquierda de 2D, que es más tradicional. Esto no es necesario cuando se renderiza en un búfer de fotogramas. Debido a que el búfer de fotogramas nunca se muestra, qué parte de la parte superior e inferior es irrelevante. Lo único que importa es que el píxel 0,0 del búfer de fotogramas corresponde a 0,0 en nuestros cálculos. Para lidiar con esto, hice que fuera posible configurar si girar 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>

Luego, podemos configurarlo cuando lo renderizamos con

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

Mantuve este ejemplo simple usando un único programa GLSL que puede lograr varios efectos. Si quisieras realizar todo el procesamiento de imágenes, probablemente necesitarás muchos programas GLSL. Un programa para el ajuste de matiz, saturación y luminancia. Otra opción para el brillo y el contraste. Uno para invertir, otro para ajustar niveles, etc. Deberás cambiar el código para cambiar los programas GLSL y actualizar los parámetros de ese programa en particular. Creo que había considerado 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 signifique una refactorización importante para evitar que todo se convierta en un gran desorden. Espero que esto 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 comprender. Si encuentro el momento, intentaré escribir algunos artículos más sobre cómo trabajar en 3D y más detalles sobre lo que WebGL realmente hace en segundo plano.

WebGL y Alfa

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

La mayor diferencia entre OpenGL y WebGL es que OpenGL se renderiza en un búfer de reserva que no está compuesto por nada, o bien que no lo compone el administrador de ventanas del SO, por lo que no importa cuál sea tu versión alfa. El navegador compone WebGL con la página web y, de forma predeterminada, usa las etiquetas alfa multiplicadas previamente y las etiquetas <img> de .png2 con transparencia. WebGL tiene varias formas de hacer esto más parecido a OpenGL.

#1) Indica a WebGL que quieres que esté compuesto con alfa no multiplicado previamente

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

El valor predeterminado es verdadero. Por supuesto, el resultado se compone de 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 de la página, el contenido detrás del lienzo si el lienzo tiene un índice z > 0, etc.) es decir, el color CSS define para esa área de la página web. Realmente una buena manera de averiguar si tienes algún problema alfa es establecer el fondo del lienzo en un color brillante, como el rojo. Verás lo que sucede de inmediato.

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

También puedes establecerlo en negro para ocultar los problemas alfa que tengas.

#2) Indica a WebGL que no quieres alfa en el búfer de reserva

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

Esto hará que actúe más como OpenGL, ya que el búfer de reserva solo tendrá RGB. Es probable que esta sea la mejor opción, ya que un buen navegador podría detectar que no tienes una versión alfa y optimizar la forma en que está compuesto WebGL. Por supuesto, eso también significa que en realidad no tendrá alfa en el búfer de reserva, por lo que si estás usando alfa en el búfer de reserva con algún propósito que podría no funcionar para ti. Pocas apps que conozco usan alfa en el búfer de reserva. Creo que podría haber sido la opción predeterminada.

#3) Borra 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);

Por lo general, el borrado es muy rápido, ya que existe un caso especial para él en la mayoría del hardware. Lo hice en la mayoría de mis demostraciones. Si fuera inteligente, cambiaría al método n.o 2 anterior. Tal vez lo haga inmediatamente después de publicar esto. Parece que la mayoría de las bibliotecas de WebGL deberían usar este método de forma predeterminada. Los pocos desarrolladores que están usando alfa para componer efectos pueden solicitarlo. El resto tendrá el mejor rendimiento y las menos sorpresas.

#4) Borra el contenido alfa una vez y no lo renderices más

// 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 realizas la renderización en tus propios búferes de fotogramas, es posible que debas volver a activar la renderización en alfa y, luego, desactivarla cuando cambies a la renderización al lienzo.

#5) Manejo de imágenes

Además, si cargas archivos PNG con alfa en las texturas, el valor predeterminado es que su alfa se multiplica previamente, lo que generalmente NO es la manera en que la mayoría de los juegos realizan las tareas. Si quieres impedir ese comportamiento, debes indicarle a WebGL que

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

#6) Uso de una ecuación de combinación que funcione con alfa ya multiplicado

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

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

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

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

Esos son los métodos que conozco. Si tienes más información, publícala a continuación.