[译]WebGL 基础系列:WebGL着色器和GLSL
原文地址:http://webglfundamentals.org/webgl/lessons/webgl-shaders-and-glsl.html
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 | void main() { |
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.
- Attributes (data pulled from buffers)
- Uniforms (values that stay the same during for all vertices of a single draw call)
- Textures (data from pixels/texels)
顶点着色器需要一些必须的数据,它们一般有3种获取方式。
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 | gl.bindBuffer(gl.ARRAY_BUFFER, buf); |
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 | // 从缓冲区对象中获取数据并传递给这个 attribute 变量 |
In WebGL fundamentals we showed that we can do no math
in the shader and just pass the data directly through.
我们已经在WebGL 基本原理中说明,在着色器中不做数学运算,只是直接传递数据值而已:
1 | attribute vec4 a_position; |
If we put clipspace vertices into our buffers it will work.
当我们将空间坐标数据放入缓冲区对象时,程序就正常运行了。
Attributes can use
float
,vec2
,vec3
,vec4
,mat2
,mat3
, andmat4
as types.
attribute 变量可以使用的数据类型有:float
、vec2
、vec3
、vec4
、mat2
、mat3
和 mat4
。
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 | attribute vec4 a_position; |
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 | gl.uniform1f (floatUniformLoc, v); // for float |
There’s also types
bool
,bvec2
,bvec3
, andbvec4
. They use either thegl.uniform?f?
orgl.uniform?i?
functions.
其它的还有bool
、bvec2
、bvec3
和 bvec4
的数据类型。它们可以使用形如 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 | // in shader |
But if you want to set individual elements of the array you must look up the location of
each element individually.
但是,如果你想要单独设置数组中的元素值,那你必须要单独查询每一个元素的地址。然后,再针对每一个元素分别赋值。
1 | // in JavaScript at init time |
Similarly if you create a struct
类似的,如果你要创建一个结构体:
1 | struct SomeStruct { |
you have to look up each field individually
你需要单独去获取每一个属性的地址:
1 | var someThingActiveLoc = gl.getUniformLocation(someProgram, "u_someThing.active"); |
Textures in Vertex Shaders
顶点着色器中的纹理(texture)
可参考片元着色器中的纹理。
Fragment Shader
片元着色器
A Fragment Shader’s job is to provide a color for the current pixel being rasterized.
It always takes the form
片元着色器的工作主要是为当前进行光栅化的像素提供颜色值。它一般如下面这样的形式:
1 | precision mediump float; |
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种获取方式:
Uniforms in Fragment Shaders
片元着色器中的 uniform 变量
可参考上文顶点着色器中关于 uniform 变量的介绍。
Textures in Fragment Shaders
片元着色器中的纹理
Getting a value from a texture in a shader we create a
sampler2D
uniform and use the GLSL
functiontexture2D
to extract a value from it.
为了从纹理中获取数据,我们需要创建一个 uniform 类型的 sampler2D
变量,然后使用 GLSL 中的 texture2D
方法来导出数据。
1 | precision mediump float; |
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 | var tex = gl.createTexture(); |
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 | var unit = 5; // Pick some texture unit |
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 | attribute vec4 a_position; |
Fragment shader
片元着色器
1 | precision mediump float; |
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 likevec2
,vec3
, andvec4
which
represent 2 values, 3 values, and 4 values respectively. Similarly it hasmat2
,mat3
andmat4
which represent 2x2, 3x3, and 4x4 matrices. You can do things like multiply
avec
by a scalar.
GLSL 的全称为 Graphics Library Shader Language。它是编写着色器的编程语言。在 GLSL 中,有很多特性与 JavaScript 有很大区别。GLSL 在设计的时候,主要是想用它来解决图形光栅化过程中的数学运算的。因此,你在例子中可以看到分别携带了2个、3个甚至4个值的vec2
、vec3
和vec4
的数据类型。同样,它还有mat2
、mat3
和mat4
的类型,分别表示2x2、3x3和4x4的矩阵。你甚至可以方便的对一个vec
和一个标量做乘法计算。
1 | vec4 a = vec4(1, 2, 3, 4); |
Similarly it can do matrix multiplication and vector to matrix multiplication
同样的,它也可以做矩阵和矩阵、矢量和矩阵的乘法运算
译者注:原文例子中,a、b、v 的值均已丢失,没办法知道具体值了,大家只要理解是矩阵和向量即可。
1 | mat4 a = ??? |
It also has various selectors for the parts of a vec. For a vec4
它也包含了很多选择器,让你可以方便的选取矢量部分维度上的值。例如定义一个 vec4
1 | vec4 v; |
v.x
is the same asv.s
andv.r
andv[0]
.v.y
is the same asv.t
andv.g
andv[1]
.v.z
is the same asv.p
andv.b
andv[2]
.v.w
is the same asv.q
andv.a
andv[3]
.
v.x
得到的值同v.s
、v.r
和v[0]
得到的值是完全一样的。v.y
得到的值同v.t
、v.g
和v[1]
得到的值是完全一样的。v.z
得到的值同v.p
、v.b
和v[2]
得到的值是完全一样的。v.w
得到的值同v.q
、v.a
和v[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 | float f = 1.0; // 使用 float |
The example above of
vec4(v.rgb, 1)
doesn’t complain about the1
becausevec4
is
casting the things inside just likefloat(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
orvec4
. If you pass invec4
you getvec4
back
which the sine of each of the components. In other words ifv
is avec4
then
T 可以使用 float
、vec2
、vec3
或者 vec4
的类型。如果你传给函数的参数为 vec4
类型,那么你得到的操作结果就是一个在每个分量上执行了 sin 操作的 vec4
。换句话说,假设 v
是 vec4
类型,那么
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 ifv1
andv2
arevec4
andf
is a float then
有些时候,可能一个参数是 float 类型,其他部分的参数为 T
类型。那么时候,float 类型的参数会参与各个分量的计算。例如,假设 v1
和 v2
都是 vec4
类型,而 f
是 float 类型,然后执行下面的操作:
1 | vec4 m = mix(v1, v2, f); |
is the same as
操作结果同下面的形式是等价的
1 | vec4 m = vec4( |
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 callinggl.drawArrays
orgl.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。如果对变换、旋转和缩放感兴趣,那么你可以去这里学习。