WebGL 基础知识

Gregg Tavares
Gregg Tavares

WebGL 可以在浏览器中显示令人惊叹的实时 3D 图形,但许多人不知道的是,WebGL 实际上是 2D API,而不是 3D API。我来解释一下

WebGL 只关注两件事:2D 裁剪空间坐标和颜色。作为使用 WebGL 的程序员,您的任务是为 WebGL 提供这两样东西。为此,您需要提供 2 个“着色器”。一个提供裁剪空间坐标的顶点着色器和一个提供颜色的片段着色器。 无论画布大小如何,裁剪空间坐标始终介于 -1 到 +1 之间。以下是一个简单的 WebGL 示例,展示了 WebGL 的最简单形式。

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

以下是这两个着色器

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

同样,无论画布大小如何,裁剪空间坐标始终介于 -1 到 +1 之间。在上面的示例中,您可以看到我们只是直接传递位置数据。由于位置数据已经在裁剪空间中,因此无需执行任何操作。如果您想要 3D 效果,则需要自行提供着色器,将 3D 转换为 2D,因为 WebGL 是一个 2D API! 对于 2D 内容,您可能更愿意使用像素而不是裁剪空间,因此我们来更改着色器,以便能够以像素为单位提供矩形,并让着色器为我们转换为裁剪空间。以下是新的顶点着色器

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

现在,我们可以将数据从裁剪空间转换为像素

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

您可能会注意到,矩形位于该区域底部附近。WebGL 将左下角视为 0,0。为了让它成为 2D 图形 API 使用的更传统的左上角,我们只需翻转 y 坐标。

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

我们将定义矩形的代码转换为一个函数,以便针对不同大小的矩形调用该函数。同时,我们还会让颜色可设置。 首先,我们让 fragment 着色器接受颜色 uniform 输入。

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

uniform vec4 u_color;

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

以下是新代码,用于在随机位置绘制 50 个随机颜色的矩形。

...

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

希望您能看出 WebGL 实际上是一个非常简单的 API。虽然 3D 渲染可能会变得更加复杂,但这种复杂性是由您(程序员)以更复杂的着色器的形式添加的。WebGL API 本身是 2D 的,而且相当简单。

type="x-shader/x-vertex" 和 type="x-shader/x-fragment" 是什么意思?

<script> 标记默认包含 JavaScript。您可以不指定类型,也可以指定 type="javascript" 或 type="text/javascript",浏览器会将内容解读为 JavaScript。如果您输入任何其他内容,浏览器都会忽略脚本标记的内容。

我们可以使用此功能将着色器存储在脚本标记中。更棒的是,我们可以自定义类型,并在 JavaScript 中查找该类型,以决定是将着色器编译为顶点着色器还是片段着色器。

在这种情况下,函数 createShaderFromScriptElement 会查找具有指定 id 的脚本,然后查看 type 以决定要创建的着色器类型。

WebGL 图片处理

在 WebGL 中,图像处理非常简单。有多简单?请参阅下文。

如需在 WebGL 中绘制图片,我们需要使用纹理。与 WebGL 在渲染时需要裁剪空间坐标(而非像素)类似,WebGL 在读取纹理时需要纹理坐标。无论纹理尺寸如何,纹理坐标都从 0.0 到 1.0。 由于我们只绘制一个矩形(实际上是 2 个三角形),因此需要告诉 WebGL 矩形中的每个点对应于纹理中的哪个位置。我们将使用一种名为“varying”的特殊变量,将这些信息从顶点着色器传递到片段着色器。之所以称为“varying”,是因为它会变化。WebGL 在使用片段着色器绘制每个像素时,会插值我们在顶点着色器中提供的值。 使用上一部分末尾的顶点着色器,我们需要添加一个属性来传入纹理坐标,然后将这些坐标传递给片段着色器。

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

然后,我们提供一个片段着色器,用于从纹理中查找颜色。

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

最后,我们需要加载图片、创建纹理并将图片复制到纹理中。由于我们在浏览器中,图片会异步加载,因此我们必须稍微重新排列代码,以等待纹理加载。加载完成后,我们会绘制它。

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

这没什么意思,我们来处理一下这张图片。如果只是交换红色和蓝色呢?

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

如果我们想进行实际查看其他像素的图像处理,该怎么办?由于 WebGL 在纹理坐标中引用纹理(范围从 0.0 到 1.0),因此我们可以使用简单的数学公式 onePixel = 1.0 / textureSize 计算 1 个像素的移动量。 以下是一个片段着色器,它会计算纹理中每个像素的左侧和右侧像素的平均值。

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

然后,我们需要从 JavaScript 传入纹理的大小。

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

现在我们已经知道如何引用其他像素,接下来让我们使用卷积核来执行一系列常见的图像处理。在本例中,我们将使用 3x3 核。卷积核只是一个 3x3 矩阵,其中每个条目表示要将正在渲染的像素周围的 8 个像素乘以多少。然后,我们将结果除以核的权重(核中所有值的总和)或 1.0(以较大者为准)。这里有一篇关于此主题的优质文章。这里还有另一篇文章,展示了如果您在 C++ 中手动编写此代码,实际代码会是什么样子。 在我们的例子中,我们将在着色器中完成这项工作,所以这里是新的片段着色器。

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

在 JavaScript 中,我们需要提供一个卷积核。

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

希望这能让您相信,在 WebGL 中进行图片处理非常简单。接下来,我将介绍如何为图片应用多种效果。

GLSL 中变量前面的 a、u 和 v_ 前缀是什么意思?

这只是一个命名惯例。a_,用于表示由缓冲区提供的数据。u_ 用于作为着色器输入的 uniform 变量,v_ 用于从顶点着色器传递到片段着色器并在绘制的每个像素的顶点之间进行插值(或变化)的 varying 变量。

应用多重效果

关于图片处理,下一个最明显的问题是如何应用多种效果?

您可以尝试动态生成着色器。提供一个界面,让用户选择要使用的效果,然后生成一个可实现所有效果的着色器。这可能并不总是可行,但该技术通常用于为实时图形创建效果。 更灵活的方法是再使用 2 个纹理,并依次渲染到每个纹理,来回切换,每次应用下一个效果。

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

为此,我们需要创建帧缓冲区。在 WebGL 和 OpenGL 中,帧缓冲区实际上是一个不恰当的名称。WebGL/OpenGL 帧缓冲区实际上只是状态的集合,而不是任何类型的缓冲区。但是,通过将纹理附加到帧缓冲区,我们可以渲染到该纹理中。 首先,我们将旧的纹理创建代码转换为一个函数

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

现在,我们使用该函数再创建 2 个纹理,并将它们附加到 2 个帧缓冲区。

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

现在,我们来创建一组核,然后创建一个要应用的核列表。

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

最后,让我们应用每个着色器,在渲染时交替使用不同的纹理

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

有些事情我需要说明一下。

使用 null 调用 gl.bindFramebuffer 会告知 WebGL 您要渲染到画布,而不是渲染到其中一个帧缓冲区。 WebGL 必须从裁剪空间转换回像素。它会根据 gl.viewport 的设置执行此操作。初始化 WebGL 时,gl.viewport 的设置默认为画布的大小。由于我们要渲染到的帧缓冲区与画布的大小不同,因此我们需要适当设置视口。 最后,在 WebGL 基础知识示例中,我们在渲染时翻转了 Y 坐标,因为 WebGL 显示画布时,0,0 是左下角,而不是更传统的 2D 左上角。在渲染到帧缓冲区时,不需要这样做。由于帧缓冲区永远不会显示,因此哪部分是顶部和底部并不重要。重要的是,帧缓冲区中的像素 0,0 对应于我们计算中的 0,0。为了解决这个问题,我通过在着色器中添加一个输入,让用户可以设置是否翻转。

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

然后,我们可以在使用

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

为了让这个示例保持简单,我使用了一个可以实现多种效果的 GLSL 程序。如果您想进行全面的图像处理,可能需要许多 GLSL 程序。一个用于调整色调、饱和度和亮度的程序。另一个用于调整亮度和对比度。一个用于反转,另一个用于调整色阶,等等。您需要更改代码以切换 GLSL 程序,并更新该特定程序的参数。我曾考虑过编写该示例,但最好还是留给读者练习,因为多个 GLSL 程序各有其参数需求,这可能意味着需要进行一些重大重构,以避免所有内容变成一团乱麻。 希望本示例和前面的示例能让 WebGL 看起来更易于上手,也希望从 2D 开始有助于您更轻松地理解 WebGL。如果我有时间,我会尝试撰写更多关于如何实现 3D 的文章,以及有关 WebGL 实际运作方式的更多详细信息。

WebGL 和 Alpha

我注意到,一些 OpenGL 开发者在使用 WebGL 处理后缓冲区(即画布)中的 Alpha 通道时遇到了问题,因此我认为有必要探讨一下 WebGL 和 OpenGL 在 Alpha 通道处理方面的一些差异。

OpenGL 和 WebGL 之间的最大区别在于,OpenGL 渲染到不与任何内容合成的后缓冲区,或者实际上不与操作系统的窗口管理器合成任何内容,因此 Alpha 值并不重要。 WebGL 由浏览器与网页合成,默认使用与具有透明度和 2D 画布标记的 .png <img> 标记相同的预乘 Alpha 值。 WebGL 有多种方法可以使其更像 OpenGL。

#1. 告知 WebGL 您希望使用非预乘 Alpha 进行合成

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

默认值为 true。 当然,结果仍会与页面上最终位于画布下方的任何背景颜色(画布的背景颜色、画布容器的背景颜色、页面的背景颜色、画布后面的内容 [如果画布的 z-index > 0] 等)合成,换句话说,就是 CSS 为网页该区域定义的颜色。 要确定是否存在 Alpha 问题,一个非常好的方法是将画布的背景设置为红色等亮色。您会立即看到正在发生的情况。

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

您也可以将其设置为黑色,这样可以隐藏任何 Alpha 问题。

#2. 告知 WebGL 您不希望在后缓冲区中使用 Alpha

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

这将使其行为更像 OpenGL,因为后缓冲区将仅具有 RGB。这可能是最佳选择,因为优秀的浏览器可以检测到您没有 Alpha 通道,并实际优化 WebGL 的合成方式。当然,这也意味着它实际上不会在后台缓冲区中包含 Alpha 通道,因此,如果您出于某种目的在后台缓冲区中使用 Alpha 通道,这可能不适合您。据我所知,很少有应用在后缓冲区中使用 Alpha。我认为这应该成为默认设置。

3. 在渲染结束时清除 Alpha

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

清除操作通常非常快,因为大多数硬件都针对此操作提供了特殊情况。我在大多数演示中都这样做过。如果我足够聪明,就会改用上述方法 2。也许我发完这个帖子后就会这么做。大多数 WebGL 库似乎都应默认使用此方法。少数实际使用 Alpha 通道实现合成效果的开发者可以申请该功能。其余用户将获得最佳性能,并尽可能避免意外情况。

4. 清除 Alpha 一次,然后不再渲染到该 Alpha

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

当然,如果您要渲染到自己的帧缓冲区,则可能需要重新开启渲染到 Alpha 通道,然后在切换到渲染到画布时再次将其关闭。

#5. 处理图片

此外,如果您要将带有 Alpha 通道的 PNG 文件加载到纹理中,默认情况下,其 Alpha 通道会预乘,这通常不是大多数游戏的做法。如果你想阻止这种行为,你需要通过以下方式告知 WebGL

gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);

6. 使用适用于预乘 Alpha 的混合方程

我编写或处理过的几乎所有 OpenGL 应用都使用

gl.blendFunc(gl.SRC_ALPHA, gl_ONE_MINUS_SRC_ALPHA);

这适用于非预乘 Alpha 纹理。 如果您确实想使用预乘 Alpha 纹理,则可能需要

gl.blendFunc(gl.ONE, gl_ONE_MINUS_SRC_ALPHA);

这些是我了解的方法。如果您知道更多,请在下方发布。