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);
这些是我知道的方法。如果您知道以上内容,请在下方发布。