WebGL 基础知识

Gregg Tavares
Gregg Tavares

WebGL 基础知识

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

WebGL 只关注 2 件事。2D 和颜色的裁剪空间坐标。作为使用 WebGL 的程序员,您的任务是为 WebGL 提供这 2 件东西。为此,您需要提供 2 个“着色器”。一个 Vertex 着色器提供裁剪空间坐标和一个提供颜色的 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。由于我们只绘制一个矩形(有两个三角形),因此需要告知 WebGL 矩形中的每个点对应于纹理中的哪个位置。我们将使用一种称为“转换”的特殊变量,将此信息从顶点着色器传递到 fragment 着色器。之所以称为变化项,是因为它会不断变化。WebGL 会在使用 fragment 着色器绘制每个像素时插入我们在顶点着色器中提供的值。使用上一部分末尾的顶点着色器,我们需要添加一个属性以传入纹理坐标,然后将其传递给 fragment 着色器。

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

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

<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 个像素的移动幅度。以下 Fragment 着色器会平均分配纹理中每个像素的左右像素。

<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++ 中手动编写这些代码,请参阅这篇文章,其中会说明一些实际代码。在本例中,我们将在着色器中执行该操作,因此,以下是新的 fragment 着色器。

<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_ 表示变化值,变化值是从顶点着色器传递到 fragment 着色器,并在绘制的每个像素的顶点之间插入(或变化)。

应用多种效果

对于图像处理,下一个最显而易见的问题是如何应用多种效果?

您可以尝试即时生成着色器。提供一个界面,让用户能够选择他想要使用的效果,然后生成执行所有效果的着色器。但这并非总是可行,尽管这项技术常常用于为实时图形创建效果。 更灵活的方法是使用另外 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 由浏览器与网页合成,默认使用与带有 canvas 的 <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 进行合成效果的少数开发者可能会请求使用 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 值是预乘的,这通常不是大多数游戏的处理方式。如果您想阻止这种行为,则需要使用

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

这些是我知道的方法。如果您知道以上内容,请在下方发布。