[译]WebGL 基础系列:WebGL着色器和GLSL

原文地址:http://webglfundamentals.org/webgl/lessons/webgl-shaders-and-glsl.html

Pearl Boy

This is a continuation from WebGL Fundamentals.
If you haven’t read about how WebGL works you might want to read this first.

本章内容紧接WebGL 基本原理来进行介绍。如果你还不了解 WebGL 的工作机制,那么你可能需要先来读读这篇文章WebGL 工作机制

We’ve talked about shaders and GLSL but haven’t really given them any specific details.
I think I was hoping it would be clear by example but let’s try to make it clearer just in case.

在前面的几篇文章中,我们已经提到了着色器和 GLSL 的相关知识,但是并没有对它们进行更多的解释和详细说明。我一直认为举例来介绍相关概念可以让你更容易接受,但是为了让你有更清晰的认识,我准备在这里对它们做更加详细的说明。

As mentioned in how it works WebGL requires 2 shaders every time you
draw something. A vertex shader and a fragment shader. Each shader is a function. A vertex
shader and fragment shader are linked together into a shader program (or just program). A typical
WebGL app will have many shader programs.

正如在WebGL 工作机制中提到的那样,当我们需要绘制图形的时候,WebGL 需要使用2种着色器:顶点着色器片元着色器。着色器就是一个函数方法。顶点着色器和片元着色器在着色器程序(或者程序)中被连接到一起。一个典型的 WebGL 应用包含多个着色器程序。

译者注:这里的着色器程序应该是指 shader program 对象。program 对象有且仅有一个顶点着色器对象和一个片元着色器对象连接到它。链接 program 对象后,可以产生最终的可执行程序,它包含最后可以在硬件上执行的硬件指令。

Vertex Shader

顶点着色器

A Vertex Shader’s job is to generate clipspace coordinates. It always takes the form

顶点着色器的工作是用来生成空间位置坐标。它一般像下面这样来使用

1
2
3
void main() {
gl_Position = doMathToMakeClipspaceCoordinates
}

Your shader is called once per vertex. Each time it’s called you are required to set the
the special global variable, gl_Position to some clipspace coordinates.

每个顶点都会调用一次着色器。当它每次被调用的时候,你需要将一些空间坐标设置给一个特殊的全局变量 gl_Position

Vertex shaders need data. They can get that data in 3 ways.

  1. Attributes (data pulled from buffers)
  2. Uniforms (values that stay the same during for all vertices of a single draw call)
  3. Textures (data from pixels/texels)

顶点着色器需要一些必须的数据,它们一般有3种获取方式。

  1. Attribute (从缓冲区对象中拉取数据)
  2. Uniform (在绘制过程中所有顶点都需要的、固定的数据值)
  3. Texture (像素/纹理元素数据)

Attributes

Attribute

The most common way is through buffers and attributes.
How it works covered buffers and
attributes. You create buffers,

给顶点着色器传递数据,最常用的方式是通过缓冲区对象和 attribute工作机制中已经介绍了缓冲区对象和 attribute 相关内容。我们可以像下面这样来创建缓冲区对象:

1
var buf = gl.createBuffer();

put data in those buffers

将数据放入缓冲区对象:

1
2
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, someData, gl.STATIC_DRAW);

Then, given a shader program you made you look up the location of its attributes,

然后,获取 attribute 变量的地址:

1
var positionLoc = gl.getAttribLocation(someShaderProgram, "a_position");

then tell WebGL how to pull data out of those buffers and into the attribute

然后,告诉 WebGL 如何从缓冲区对象中获取数据,并把这些数据放入 attribute 变量中。

1
2
3
4
5
6
7
8
9
10
11
// 从缓冲区对象中获取数据并传递给这个 attribute 变量
gl.enableVertexAttribArray(positionLoc);

var numComponents = 3; // (x, y, z)
var type = gl.FLOAT;
var normalize = false; // 保持值不变
var offset = 0; // 从缓冲区起始位置开始
var stride = 0; // 到下一个顶点需要移动的字节数
// 0表示使用符合类型大小的 stride

gl.vertexAttribPointer(positionLoc, numComponents, type, false, stride, offset);

In WebGL fundamentals we showed that we can do no math
in the shader and just pass the data directly through.

我们已经在WebGL 基本原理中说明,在着色器中不做数学运算,只是直接传递数据值而已:

1
2
3
4
5
attribute vec4 a_position;

void main() {
gl_Position = a_position;
}

If we put clipspace vertices into our buffers it will work.

当我们将空间坐标数据放入缓冲区对象时,程序就正常运行了。

Attributes can use float, vec2, vec3, vec4, mat2, mat3, and mat4 as types.

attribute 变量可以使用的数据类型有:floatvec2vec3vec4mat2mat3mat4

Uniforms

Uniform

For a vertex shader uniforms are values passed to the vertex shader that stay the same
for all vertices in a draw call. As a very simple example we could add an offset to
the vertex shader above

对于顶点着色器而言,uniform 变量用来存储那些在绘制过程中所有顶点都共用的数据值。举个简单的例子,我们给上文的顶点着色器添加一个 offset 参数:

1
2
3
4
5
6
attribute vec4 a_position;
uniform vec4 u_offset;

void main() {
gl_Position = a_position + u_offset;
}

And now we could offset every vertex by a certain amount. First we’d look up the
location of the uniform

现在我们可以给每个顶点都设置一个具体的偏移量了。首先,我们来获取下 uniform 变量的地址:

1
var offsetLoc = gl.getUniformLocation(someProgram, "u_offset");

And then before drawing we’d set the uniform

然后,在绘制前我们需要先设置下 uniform 变量的值:

1
gl.uniform4fv(offsetLoc, [1, 0, 0, 0]);  // 将它移动到视图的右半边

Uniforms can be many types. For each type you have to call the corresponding function to set it.

uniform 变量有多种类型。对于不同的类型,你只有调用正确的方法才可以设置变量值。

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
gl.uniform1f (floatUniformLoc, v);                 // for float
gl.uniform1fv(floatUniformLoc, [v]); // for float or float array
gl.uniform2f (vec2UniformLoc, v0, v1); // for vec2
gl.uniform2fv(vec2UniformLoc, [v0, v1]); // for vec2 or vec2 array
gl.uniform3f (vec3UniformLoc, v0, v1, v2); // for vec3
gl.uniform3fv(vec3UniformLoc, [v0, v1, v2]); // for vec3 or vec3 array
gl.uniform4f (vec4UniformLoc, v0, v1, v2, v4); // for vec4
gl.uniform4fv(vec4UniformLoc, [v0, v1, v2, v4]); // for vec4 or vec4 array

gl.uniformMatrix2fv(mat2UniformLoc, false, [ 4x element array ]) // for mat2 or mat2 array
gl.uniformMatrix3fv(mat3UniformLoc, false, [ 9x element array ]) // for mat3 or mat3 array
gl.uniformMatrix4fv(mat4UniformLoc, false, [ 16x element array ]) // for mat4 or mat4 array

gl.uniform1i (intUniformLoc, v); // for int
gl.uniform1iv(intUniformLoc, [v]); // for int or int array
gl.uniform2i (ivec2UniformLoc, v0, v1); // for ivec2
gl.uniform2iv(ivec2UniformLoc, [v0, v1]); // for ivec2 or ivec2 array
gl.uniform3i (ivec3UniformLoc, v0, v1, v2); // for ivec3
gl.uniform3iv(ivec3UniformLoc, [v0, v1, v2]); // for ivec3 or ivec3 array
gl.uniform4i (ivec4UniformLoc, v0, v1, v2, v4); // for ivec4
gl.uniform4iv(ivec4UniformLoc, [v0, v1, v2, v4]); // for ivec4 or ivec4 array

gl.uniform1i (sampler2DUniformLoc, v); // for sampler2D (textures)
gl.uniform1iv(sampler2DUniformLoc, [v]); // for sampler2D or sampler2D array

gl.uniform1i (samplerCubeUniformLoc, v); // for samplerCube (textures)
gl.uniform1iv(samplerCubeUniformLoc, [v]); // for samplerCube or samplerCube array

There’s also types bool, bvec2, bvec3, and bvec4. They use either the gl.uniform?f? or gl.uniform?i?
functions.

其它的还有boolbvec2bvec3bvec4 的数据类型。它们可以使用形如 gl.uniform?f?gl.uniform?i? 的方法即可。

Note that for an array you can set all the uniforms of the array at once. For example

注意,你可以使用数组一次性给 uniform 数组形式的变量赋值。举例如下:

1
2
3
4
5
6
7
8
// in shader
uniform vec2 u_someVec2[3];

// in JavaScript at init time
var someVec2Loc = gl.getUniformLocation(someProgram, "u_someVec2");

// at render time
gl.uniform2fv(someVec2Loc, [1, 2, 3, 4, 5, 6]); // set the entire array of u_someVec3

But if you want to set individual elements of the array you must look up the location of
each element individually.

但是,如果你想要单独设置数组中的元素值,那你必须要单独查询每一个元素的地址。然后,再针对每一个元素分别赋值。

1
2
3
4
5
6
7
8
9
// in JavaScript at init time
var someVec2Element0Loc = gl.getUniformLocation(someProgram, "u_someVec2[0]");
var someVec2Element1Loc = gl.getUniformLocation(someProgram, "u_someVec2[1]");
var someVec2Element2Loc = gl.getUniformLocation(someProgram, "u_someVec2[2]");

// at render time
gl.uniform2fv(someVec2Element0Loc, [1, 2]); // set element 0
gl.uniform2fv(someVec2Element1Loc, [3, 4]); // set element 1
gl.uniform2fv(someVec2Element2Loc, [5, 6]); // set element 2

Similarly if you create a struct

类似的,如果你要创建一个结构体:

1
2
3
4
5
struct SomeStruct {
bool active;
vec2 someVec2;
};
uniform SomeStruct u_someThing;

you have to look up each field individually

你需要单独去获取每一个属性的地址:

1
2
var someThingActiveLoc = gl.getUniformLocation(someProgram, "u_someThing.active");
var someThingSomeVec2Loc = gl.getUniformLocation(someProgram, "u_someThing.someVec2");

Textures in Vertex Shaders

顶点着色器中的纹理(texture)

See Textures in Fragment Shaders.

可参考片元着色器中的纹理

Fragment Shader

片元着色器

A Fragment Shader’s job is to provide a color for the current pixel being rasterized.
It always takes the form

片元着色器的工作主要是为当前进行光栅化的像素提供颜色值。它一般如下面这样的形式:

1
2
3
4
5
precision mediump float;

void main() {
gl_FragColor = doMathToMakeAColor;
}

Your fragment shader is called once per pixel. Each time it’s called you are required
to set the special global variable, gl_FragColor to some color.

每个像素都会调用一次片元着色器。每当它被调用的时候,你都需要给一个特殊的全局变量 gl_FragColor 设置颜色值。

Fragment shaders need data. They can get data in 3 ways

片元着色器也需要数据,它们通常有3种获取方式:

  1. Uniforms (values that stay the same for every pixel of a single draw call)
  2. Textures (data from pixels/texels)
  3. Varyings (data passed from the vertex shader and interpolated)
  1. Uniform (在绘制过程中每个像素都需要的、固定的变量值)
  2. Texture (像素/纹理元素数据)
  3. Varying (从顶点着色器传递过来并且经过内插过程的数据)

Uniforms in Fragment Shaders

片元着色器中的 uniform 变量

See Uniforms in Vertex Shaders.

可参考上文顶点着色器中关于 uniform 变量的介绍。

Textures in Fragment Shaders

片元着色器中的纹理

Getting a value from a texture in a shader we create a sampler2D uniform and use the GLSL
function texture2D to extract a value from it.

为了从纹理中获取数据,我们需要创建一个 uniform 类型的 sampler2D 变量,然后使用 GLSL 中的 texture2D 方法来导出数据。

1
2
3
4
5
6
7
8
precision mediump float;

uniform sampler2D u_texture;

void main() {
vec2 texcoord = vec2(0.5, 0.5) // get a value from the middle of the texture
gl_FragColor = texture2D(u_texture, texcoord);
}

What data comes out of the texture is dependent on many settings.
At a minimum we need to create and put data in the texture, for example

纹理会导出什么样的数据呢?这依赖于很多配置项
我们至少要创建纹理并把数据传递过来,如:

1
2
3
4
5
6
7
var tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
var level = 0;
var width = 2;
var height = 1;
var data = new Uint8Array([255, 0, 0, 255, 0, 255, 0, 255]);
gl.texImage2D(gl.TEXTURE_2D, level, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);

Then look up the uniform location in the shader program

然后,获取 uniform 变量的地址:

1
var someSamplerLoc = gl.getUniformLocation(someProgram, "u_texture");

WebGL then requires you to bind it to a texture unit

然后,我们需要将它绑定到一个纹理单元:

1
2
3
var unit = 5;  // Pick some texture unit
gl.activeTexture(gl.TEXTURE0 + unit);
gl.bindTexture(gl.TEXTURE_2D, tex);

And tell the shader which unit you bound the texture to

并且要告诉着色器你刚才绑定的纹理单元:

1
gl.uniform1i(someSamplerLoc, unit);

Varyings

varying

A varying is a way to pass a value from a vertex shader to a fragment shader which we
covered in how it works.

我们已经在WebGL 工作机制中介绍过了,varying 变量主要用来从顶点着色器向片元着色器传递数值。

To use a varying we need to declare matching varyings in both a vertex and fragment shader.
We set the varying in the vertex shader with some value per vertex. When WebGL draws pixels
it will interpolate between those values and pass them to the corresponding varying in
the fragment shader

为了使用它,我们需要在顶点着色器和片元着色器中声明同名的 varying 变量。在顶点着色器中,我们根据每个顶点来设置 varying 变量值。当 WebGL 绘制像素的时候,它会对这些 varying 变量值执行内插过程,然后把处理后得到的相关值传递给片元着色器中的同名 varying 变量。

Vertex shader

顶点着色器

1
2
3
4
5
6
7
8
9
10
attribute vec4 a_position;

uniform vec4 u_offset;

varying vec4 v_positionWithOffset;

void main() {
gl_Position = a_position + u_offset;
v_positionWithOffset = a_position + u_offset;
}

Fragment shader

片元着色器

1
2
3
4
5
6
7
8
9
precision mediump float;

varying vec4 v_positionWithOffset;

void main() {
// convert from clipsapce (-1 <-> +1) to color space (0 -> 1).
vec4 color = v_positionWithOffset * 0.5 + 0.5
gl_FragColor = color;
}

The example above is a mostly nonsense example. It doesn’t generally make sense to
directly copy the clipspace values to the fragment shader and use them as colors. Nevertheless
it will work and produce colors.

上面的例子并不太明智。直接将空间坐标值传递给片元着色器并用作颜色值看起来是非常不合理的做法。然而,这确实能让程序运行起来,并渲染出了颜色。

GLSL

GLSL stands for Graphics Library Shader Language. It’s the language shaders are written
in. It has some special semi unique features that are certainly not common in JavaScript.
It’s designed to do the math that is commonly needed to compute things for rasterizing
graphics. So for example it has built in types like vec2, vec3, and vec4 which
represent 2 values, 3 values, and 4 values respectively. Similarly it has mat2, mat3
and mat4 which represent 2x2, 3x3, and 4x4 matrices. You can do things like multiply
a vec by a scalar.

GLSL 的全称为 Graphics Library Shader Language。它是编写着色器的编程语言。在 GLSL 中,有很多特性与 JavaScript 有很大区别。GLSL 在设计的时候,主要是想用它来解决图形光栅化过程中的数学运算的。因此,你在例子中可以看到分别携带了2个、3个甚至4个值的vec2vec3vec4的数据类型。同样,它还有mat2mat3mat4的类型,分别表示2x2、3x3和4x4的矩阵。你甚至可以方便的对一个vec和一个标量做乘法计算。

1
2
3
vec4 a = vec4(1, 2, 3, 4);
vec4 b = a * 2.0;
// b is now vec4(2, 4, 6, 8);

Similarly it can do matrix multiplication and vector to matrix multiplication

同样的,它也可以做矩阵和矩阵、矢量和矩阵的乘法运算

译者注:原文例子中,a、b、v 的值均已丢失,没办法知道具体值了,大家只要理解是矩阵和向量即可。

1
2
3
4
5
6
mat4 a = ???
mat4 b = ???
mat4 c = a * b;

vec4 v = ???
vec4 y = c * v;

It also has various selectors for the parts of a vec. For a vec4

它也包含了很多选择器,让你可以方便的选取矢量部分维度上的值。例如定义一个 vec4

1
vec4 v;
  • v.x is the same as v.s and v.r and v[0].
  • v.y is the same as v.t and v.g and v[1].
  • v.z is the same as v.p and v.b and v[2].
  • v.w is the same as v.q and v.a and v[3].
  • v.x 得到的值同 v.sv.rv[0]得到的值是完全一样的。
  • v.y 得到的值同 v.tv.gv[1]得到的值是完全一样的。
  • v.z 得到的值同 v.pv.bv[2]得到的值是完全一样的。
  • v.w 得到的值同 v.qv.av[3]得到的值是完全一样的。

译者注:因为矢量可以用来存储顶点的坐标、颜色和纹理坐标,所以 GLSL ES 支持以上三种分量名称,从而增加程序可读性。

It it able to swizzle vec components which means you can swap or repeat components.

你还可以通过对矢量同时抽取多个分量,从而实现 混合 的过程。

1
v.yyyy

is the same as

它等价于下面这种形式

1
vec4(v.y, v.y, v.y, v.y)

Similarly

同样可以使用

1
v.bgra

is the same as

或者这种

1
vec4(v.b, v.g, v.r, v.a)

when constructing a vec or a mat you can supply multiple parts at once. So for example

当构造矢量或者矩阵的时候,你也可以一次性提供多个部分来完成构造。例如:

1
vec4(v.rgb, 1)

Is the same as

等价于

1
vec4(v.r, v.g, v.b, 1)

One thing you’ll likely get caught up on is that GLSL is very type strict.

编程中时刻要注意 GLSL 是一种强类型语言,不然极易导致程序报错。如:

1
float f = 1;  // ERROR 1 is an int. You can't assign an int to a float

The correct way is one of these

正确的方法是像下面这么写:

1
2
float f = 1.0;      // 使用 float
float f = float(1) // 把 integer 转换为 float

The example above of vec4(v.rgb, 1) doesn’t complain about the 1 because vec4 is
casting the things inside just like float(1).

在上面 vec4(v.rgb, 1) 的例子中也使用了 1,但是它并没有报错。这是为什么呢?因为 vec4 在内部已经对这些参数做了类似 float(1) 的转换操作。

GLSL has a bunch of built in functions. Many of them operate on multiple components at once.
So for example

GLSL 包含了很多内置功能函数。其中很多函数操作都是一次操作多个分量。例如:

1
T sin(T angle)

Means T can be float, vec2, vec3 or vec4. If you pass in vec4 you get vec4 back
which the sine of each of the components. In other words if v is a vec4 then

T 可以使用 floatvec2vec3 或者 vec4 的类型。如果你传给函数的参数为 vec4 类型,那么你得到的操作结果就是一个在每个分量上执行了 sin 操作的 vec4。换句话说,假设 vvec4 类型,那么

1
vec4 s = sin(v);

is the same as

得到的结果同下面的操作是一样的

1
vec4 s = vec4(sin(v.x), sin(v.y), sin(v.z), sin(v.w));

Sometimes one argument is a float and the rest is T. That means that float will be applied
to all the components. For example if v1 and v2 are vec4 and f is a float then

有些时候,可能一个参数是 float 类型,其他部分的参数为 T 类型。那么时候,float 类型的参数会参与各个分量的计算。例如,假设 v1v2 都是 vec4 类型,而 f 是 float 类型,然后执行下面的操作:

1
vec4 m = mix(v1, v2, f);

is the same as

操作结果同下面的形式是等价的

1
2
3
4
5
vec4 m = vec4(
mix(v1.x, v2.x, f),
mix(v1.y, v2.y, f),
mix(v1.z, v2.z, f),
mix(v1.w, v2.w, f));

You can see a list of all the GLSL functions on the last page of the WebGL
Reference Card
.
If you like really dry and verbose stuff you can try
the GLSL spec.

你可以在the WebGL
Reference Card
中查看 GLSL 的全部函数列表。如果喜欢更详细的介绍,那你可以参考the GLSL spec

Putting it all togehter

总结一下

That’s the point of this entire series of posts. WebGL is all about creating various shaders, supplying
the data to those shaders and then calling gl.drawArrays or gl.drawElements to have WebGL process
the vertices by calling the current vertex shader for each vertex and then render pixels by calling the
the current fragment shader for each pixel.

这里介绍的内容是整个系列文章中的重点内容。其实,WebGL 可以总结如下:创建各种着色器,给着色器提供数据,然后调用 gl.drawArrays 或者 gl.drawElements 方法来让 WebGL 处理器根据顶点着色器对每个顶点进行处理,最后使用片元着色器对每个像素进行渲染。

Actually creating the shaders requires several lines of code. Since those lines are the same in
most WebGL programs and since once written you can pretty much ignore them how to compile GLSL shaders
and link them into a shader program is covered here
.

实际上,创建着色器需要大量代码。然而,在大多数 WebGL 程序中这些代码都是相同的。因此,这些代码介绍一次就可以了,后续学习你可以完全忽略它们了。更多内容可以参考how to compile GLSL shaders
and link them into a shader program is covered here

If you’re just starting from here you can go in 2 directions. If you are interested in image procesing
I’ll show you how to do some 2D image processing.
If you are interesting in learning about translation,
rotation and scale then start here.

从这里开始,你有两个方向可以去深入学习。如果对图像处理感兴趣,你可以去学习how to do some 2D image processing。如果对变换、旋转和缩放感兴趣,那么你可以去这里学习。