WebGL 基础知识
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);
这些是我知道的方法。如果您知道更多信息,请在下方留言。