WebGL 基础知识

Gregg Tavares
Gregg Tavares

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

WebGL 只关心 2 件事。2D 中的剪裁空间坐标和颜色。作为使用 WebGL 的程序员,您的工作就是为 WebGL 提供这两项内容。您需要提供 2 个“着色器”来实现此目的。一个提供剪裁空间坐标的顶点着色器和一个提供颜色的 Fragment 着色器。 无论您的画布尺寸如何,裁剪空间坐标始终都会从 -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);

以下是 2 种着色器

<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 着色器接受颜色均匀的输入。

<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”的特殊变量,将此信息从顶点着色器传递给片段着色器。之所以称之为 变化,是因为它不尽相同。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 个额外的纹理,并依次渲染到每个纹理,来回 ping pong,并每次应用下一个效果。

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 中,Framebuffer 实际上是一个不恰当的名称。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 的设置执行此操作。gl.viewport 的设置默认为初始化 WebGL 时画布的大小。由于我们要渲染到的帧缓冲区与画布大小不同,因此我们需要适当地设置视口。最后,在 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 由浏览器与网页合成,默认使用预乘 Alpha 值,与具有透明度的 .png <img> 标记和 2d 画布标记相同。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 一次,然后不再渲染到它

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

这些是我知道的方法。如果您知道更多信息,请在下方留言。