[译]WebGL 基础系列:图像处理

原文地址:http://webglfundamentals.org/webgl/lessons/webgl-image-processing.html

优胜美地

Image processing is easy in WebGL. How easy? Read below.

在 WebGL 中进行图像处理非常简单。有多简单?请接着往下看。

This is a continuation from WebGL Fundamentals. If you haven’t read that I’d suggest webgl-fundamentals.html going there first.

本文接WebGL 基本原理的内容进行描述。如果还没有读过,那么你最好先去看下WebGL 基本原理相关内容。

To draw images in WebGL we need to use textures. Similarly to the way WebGL expects clipspace coordinates when rendering instead of pixels, WebGL expects texture coordinates when reading a texture. Texture coordinates go from 0.0 to 1.0 no matter the dimensions of the texture.

我们需要使用纹理在 WebGL 中绘制图像。WebGL 在渲染时使用的是裁剪空间坐标,同样道理,它在读取纹理的时候使用的是纹理坐标。无论纹理尺寸多大,纹理坐标的范围都是从 0.0 到 1.0。

Since we are only drawing a single rectangle (well, 2 triangles) we need to tell WebGL which place in the texture each point in the rectangle corresponds to. We’ll pass this information from the vertex shader to the fragment shader using a special kind of variable called a ‘varying’. It’s called a varying because it varies. WebGL will interpolate the values we provide in the vertex shader as it draws each pixel using the fragment shader.

当我们在绘制一个矩形(恩,需要两个三角形来构成)的时候,我们需要将矩形上每个点对应的纹理位置信息提供给 WebGL。这时,需要使用一种特殊的变量来将这些信息从顶点着色器传递到片元着色器。这个特殊的变量就是 ‘varying’,它之所以这么命名就是因为它本身是变化的。当 WebGL 使用片元着色器来绘制像素时,它需要对顶点着色器提供的信息进行内插计算,从而得到正确的值来完成绘制。

译者注:WebGL 会将顶点着色器和片元着色器中的同名 varying 变量建立一种“特殊”的连接,通过对顶点着色器中的 varying 变量进行内插计算得到结果,并提供给片元着色器中的同名 varying 变量。


Using the vertex shader from the end of the previous post we need to add an attribute to pass in texture coordinates and then pass those on to the fragment shader.

这里我们依然使用前面文章中提到的顶点着色器代码,但是我们需要额外添加一个 attribute 变量,用来将纹理坐标提供给片元着色器。

1
2
3
4
5
6
7
8
9
10
attribute vec2 a_texCoord;
...
varying vec2 v_texCoord;

void main() {
...
// 将纹理坐标传递给片元着色器
// GPU 会在这些值之间做内插计算
v_texCoord = a_texCoord;
}

Then we supply a fragment shader to look up colors from the texture.

然后,我们就可以使用下面的片元着色器代码,从纹理中查找指定位置的颜色了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// 我们的纹理
uniform sampler2D u_image;

// 从顶点着色器传递过来的纹理坐标
varying vec2 v_texCoord;

void main() {
// 在纹理中查找颜色值
gl_FragColor = texture2D(u_image, v_texCoord);
}
</script>

Finally we need to load an image, create a texture and copy the image into the texture. Because we are in a browser images load asynchronously so we have to re-arrange our code a little to wait for the texture to load. Once it loads we’ll draw it.

最后我们还需要加载图片、创建纹理,并把图片放入纹理中。因为浏览器中的图片是异步加载的,所以我们还需要对代码重新组织下。纹理加载完毕后我们就可以进行绘制了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function main() {
var image = new Image();
image.src = "http://someimage/on/our/server"; // 必须要同域!!!
image.onload = function() {
render(image);
}
}

function render(image) {
...
// 省略了前文出现过的一些代码
...
// 获取 attribute 变量的存储位置
var texCoordLocation = gl.getAttribLocation(program, "a_texCoord");

// 为矩形提供纹理坐标
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);

// 创建纹理
var texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);

// 设置一些参数从而让我们可以处理任何大小的图片
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);

// 将图片上传到纹理中
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
...
}

And here’s the image rendered in WebGL.

下面是一个 WebGL 渲染图片的例子:

你可以在新窗口打开这个例子。

Not too exciting so let’s manipulate that image. How about just swapping red and blue?

是不是觉得上面的例子挺无聊的?好,那我们接下来做些处理,让事情变得有趣起来。你觉得我们将红色和蓝色交换下会发生什么事情?

1
2
3
...
gl_FragColor = texture2D(u_image, v_texCoord).bgra;
...

And now red and blue are swapped.

现在红色和蓝色交换了,效果如下:

你可以在新窗口打开这个例子。

What if we want to do image processing that actually looks at other pixels? Since WebGL references textures in texture coordinates which go from 0.0 to 1.0 then we can calculate how much to move for 1 pixel with the simple math onePixel = 1.0 / textureSize.

如果在图像处理的过程中需要参考其他像素该如何操作呢?因为 WebGL 处理纹理使用的是纹理坐标(范围[0.0,1.0]),所以我们可以通过计算来得到 WebGL 绘制一个像素所需要移动的距离,公式如下:

1
onePixel = 1.0 / textureSize

Here’s a fragment shader that averages the left and right pixels of each pixel in the texture.

在下面的例子中,片元着色器对纹理中的每个像素做了处理,通过对当前像素、左边相邻像素、右边相邻像素求平均来获取当前像素颜色值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// 纹理
uniform sampler2D u_image;
uniform vec2 u_textureSize;

// 从顶点着色器传递过来的纹理坐标
varying vec2 v_texCoord;

void main() {
// 计算纹理坐标中的 1 像素
vec2 onePixel = vec2(1.0, 1.0) / u_textureSize;

// 对左、中、右三个像素求平均值
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>

We then need to pass in the size of the texture from JavaScript.

然后,我们需要通过 JavaScript 将纹理大小传递进去。

1
2
3
4
5
6
...
var textureSizeLocation = gl.getUniformLocation(program, "u_textureSize");
...
// set the size of the image
gl.uniform2f(textureSizeLocation, image.width, image.height);
...

Compare to the un-blurred image above.

效果如下,可以同上面未进行模糊处理的图片对比下。

你可以在新窗口打开这个例子。

Now that we know how to reference other pixels let’s use a convolution kernel to do a bunch of common image processing. In this case we’ll use a 3x3 kernel. A convolution kernel is just a 3x3 matrix where each entry in the matrix represents how much to multiply the 8 pixels around the pixel we are rendering. We then divide the result by the weight of the kernel Here’s a pretty good article on it or 1.0, whichever is greater. (http://docs.gimp.org/en/plug-in-convmatrix.html). And here’s another article showing some actual code if you were to write this by hand in C++.

现在我们已经了解了如何获取其他像素信息,接下来我们将使用一个卷积核(convolution kernel)来做一些更通用的图像处理。在下面的例子中,我们将使用一个 3x3 的核。这里使用的卷积核是一个 3x3 的矩阵,里面的每一个元素的值将会与当前元素周围对应的 8 个像素值相乘。然后,我们将结果除以核的权重(核中所有值的总和)或 1.0。这篇文章很好的介绍了相关内容。另一篇文章介绍了一些具体的代码(C++ 写的)。

In our case we’re going to do that work in the shader so here’s the new fragment shader.

在例子中,我们准备在着色器中进行上面的操作。新的片元着色器代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<script id="2d-fragment-shader" type="x-shader/x-fragment">
precision mediump float;

// 纹理
uniform sampler2D u_image;
uniform vec2 u_textureSize;
uniform float u_kernel[9];
uniform float u_kernelWeight;

// 顶点着色器传递来的纹理坐标信息
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] ;

// 将总和除以权重,但是只处理 rgb 值
// 将 alpha 设置为 1.0
gl_FragColor = vec4((colorSum / u_kernelWeight).rgb, 1.0);
}
</script>

In JavaScript we need to supply a convolution kernel and its weight

在 JavaScript 中,我们需要提供卷积核和它的权重。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function computeKernelWeight(kernel) {
var weight = kernel.reduce(function(prev, curr) {
return prev + curr;
});
return weight <= 0 ? 1 : weight;
}

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

And voila… Use the drop down list to select different kernels.

好,大功告成!示例如下,可以在下拉菜单中选择不同的核来试试效果。

你可以在新窗口打开这个例子。

I hope this article has convinced you image processing in WebGL is pretty simple. Next up I’ll go over how to apply more than one effect to the image.

我希望通过这篇文章可以让你明白:在 WebGL 中处理图像非常简单。下一章我将介绍如何在图片上应用多种效果


你问我答环节

‘u_image’ is never set. How does that work?

1. u_image 一直没赋值,那它是如何工作的呢?

Uniforms default to 0 so u_image defaults to using texture unit 0. Texture unit 0 is also the default active texture so calling bindTexture will bind the texture to texture unit 0.

uniform 变量默认值为 0,所以与之类似,u_image 默认使用索引值为 0 的纹理单元。它同时也是默认激活的纹理,因此当调用 bindTexture 时,会把纹理绑定到该纹理单元。

WebGL has an array of texture units. Which texture unit each sampler uniform references is set by looking up the location of that sampler uniform and then setting the index of the texture unit you want it to reference.

WebGL 有一组纹理单元。需要通过查找 uniform 变量位置、设置纹理单元索引值的操作来将纹理传递给着色器中的取样器变量。

For example:

1
2
3
4
var textureUnitIndex = 6; // 使用 6 号纹理单元
var u_imageLoc = gl.getUniformLocation(
program, "u_image");
gl.uniform1i(u_imageLoc, textureUnitIndex);

To set textures on different units you call gl.activeTexture and then bind the texture you want on that unit. Example

如果要在不同单元上设置纹理,需要调用 gl.activeTexture ,然后再将纹理绑定到你期望的单元上。例如:

1
2
3
// 将 someTexture 绑定到 6 号纹理单元
gl.activeTexture(gl.TEXTURE6);
gl.bindTexture(gl.TEXTURE_2D, someTexture);

This works too

下面的代码有同样的效果:

1
2
3
4
var textureUnitIndex = 6; // 使用 6 号纹理单元
// 将 someTexture 绑定到 6 号纹理单元
gl.activeTexture(gl.TEXTURE0 + textureUnitIndex);
gl.bindTexture(gl.TEXTURE_2D, someTexture);

What’s with the a, u, and v_ prefixes in from of variables in GLSL?

2. 为什么 GLSL 中的变量都要添加形如 a_u_v_ 的前缀呢?

That’s just a naming convention. They are not required but for me it makes it easier to see at a glance
where the values are coming from. a for attributes which is the data provided by buffers. u for uniforms which are inputs to the shaders, v_ for varyings which are values passed from a vertex shader to a fragment shader and interpolated (or varied) between the vertices for each pixel drawn.
See How it works for more details.

这只是一种命名的规范而已。它不是强制性的规定,但是这么做可以让我们轻松识别该变量的来源。例如,a_ 前缀的是 attribute 变量,由缓存对象提供变量值。u_ 前缀的是 uniform 变量,包含了着色器的输入信息。v_ 前缀的是 varying 变量,表示该变量值从顶点着色器传递给片元着色器,并在像素的绘制中需要进行内插运算。

可以在WebGL 工作机制中查看详细介绍。