上一节我们主要讲解了WebGL 的基础。在开始之前,我们来了解一下WebGL 和你的 GPU 底层的工作原理。GPU 主要做了两件事。第一个是转换顶点(或者说 数据流)到裁剪平面中。第二个就是基于前一部分来渲染图像。
当你调用:
gl.drawArrays(gl.TRIANGLE, 0, 9);
后面的 9 代表着“处理9个点”。所以,这里就有9个点被处理了。
左边是你提供的数据,顶点着色器是你用 GLSL 写出来的函数。它会在每个顶点被处理时调用。该点的值经过相应的数学运算,转换成为裁剪坐标的值,并且赋值给特殊的变量 gl_Position
。GPU 会获得该值并保存。
当你在画三角形时,前一个部分每次会渲染出3个点,GPU 就可以利用这3个点去画一个三角形。GPU 会找到这3个点在图上对应着的像素点,然后渲染出一个三角形。接着,对于每个像素点,GPU 会调用你的片元着色器,给相应的点加上颜色。片元着色器实际上是通过 gl_FragColor 给每个像素点设置颜色。
不过,在我们的例子中,片元着色器并没有区分每一个点的颜色值。当然,办法是有的。我们可以定义 “varyings” 将每一个颜色值通过顶点着色器赋给片元着色器。
看一个简单的例子,我们将从顶点着色器计算出的裁剪坐标值传递给片元着色器。
然后,画一个简单的三角形。我们修改前面的例子中的代码,将画 F
改为画一个三角形。
// 将三角形的像素值存储在 buffer 中
function setGeometry(gl) {
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array([
0,-100,
150, 125,
-175, 100]),
gl.STATIC_DRAW);
}
我们只需要画三个点:
// 画场景
function drawScene() {
...
// 画几何图形
gl.drawArrays(gl.TRIANGLES,0,3);
}
在顶点着色器中,我们声明一个 varying
将数据传递给片元着色器。
varying vec4 v_color;
...
void main() {
// 使用 matrix 乘以该位置
gl_Position = vec4((u_matrix * vec3(a_position,1)).xy,0,1);
// 从 裁剪空间值 转换为 颜色空间值
// 裁剪空间值的范围为 -1.0 到 +1.0
// 颜色空间的值从 0.0 到 1.0
v_color = gl_Position * 0.5 + 0.5;
}
然后,我们在片元着色器中,声明一个一样的 varying
。
precision mediump float;
varying vec4 v_color;
void main() {
gl_FragColor = v_color;
}
WebGL 会自动关联在顶点着色器和片元着色器存在的同名 varying。
Here's the working version.
具体效果,可以查看网页。
上面例子中,我们可以移动,缩放并且旋转该三角形。注意,因为这个颜色是直接根据裁剪空间来的,而不是根据三角形上的点来的。所以,他们不会随三角形一起移动,而是固定在背景中。
想一想,我们只计算了3个点。所以,顶点着色器也只会调用3次,如果这样的话,我们也只能得出3种颜色,但是,我们的三角形却有很多颜色。这就是为什么我们需要一个 varying。
在上面的例子中,我们设置了3个点:
我们的顶点着色器使用一个模型去移动,旋转,放缩 并且转化为 裁剪坐标的值。上述变化的默认值分别是:
- 平移为 200,150
- 旋转为 0
- 放缩为 1,1
所以,只有平移有点不同。前面给出的 backbuffer 是 400×300,我们的顶点着色器会将其传递给模型,然后计算出下面3个裁剪坐标。
它同样会转换上述值到颜色空间,并且赋值给我们刚才声明的 varying v_color 变量中。
这三个值被写入 v_color 之后,会添加并且传递给片元着色器去渲染每个点的颜色。v_color 会插入在 v0,v1和v2 之间。
整个渲染过程,可以查看具体网页。
我们可以传递更多的数据给顶点着色器,同样,也可以传递给片元着色器。 如果我们想画一个矩形,就需要 2 个三角形,2个不同的颜色。在顶点着色器中,我们还需要另外一个 attribute 去传递更多的数据。那么片元着色器也会处理更多的数据。
attribute vec2 a_position;
attribute vec4 a_color;
...
varying vec4 v_color;
void main() {
...
// 把 attribute 中的颜色值复制到 varying 中
v_color = a_color;
}
我们现在向 WebGL 提供需要用到的颜色。
// 找到 vertex data 绑定的位置
var positionLocation = gl.getAttribLocation(program,"a_position");
var colorLocation = gl.getAttribLocation(program,"a_color");
...
// 创建一个 buffer 去加载颜色值
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER,buffer);
gl.enableVertexAttribArray(colorLocation);
gl.vertexAttribPointer(colorLocation,4,gl.FLOAT,false,0,0);
// 设置颜色值
setColors(gl);
...
// 给 buffer 填充2个三角形需要用到的颜色值
function setColors(gl) {
// 随机选取2种颜色
var r1 = Math.random();
var b1 = Math.random();
var g1 = Math.random();
var r2 = Math.random();
var b2 = Math.random();
var g2 = Math.random();
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(
[ r1,b1,g1,1,
r1,b1,g1,1,
r1,b1,g1,1,
r2,b2,g2,1,
r2,b2,g2,1,
r2,b2,g2,1]),
gl.STATIC_DRAW);
}
注意,上面我们使用的是两个固定的颜色值。但是,我们是将值赋给 varying,所以,三角形中的颜色值是可以变化的。我们在上面给每个三角形的3个点设置的是相同的颜色值,如果我们设置不同的值,将会看到具体的差值。
// 给 buffer 填充2个三角形需要用到的颜色值
function setColors(gl) {
// 给每个顶点设置不同的颜色
gl.bufferData(
gl.ARRAY_BUFFER,
new Float32Array(
[ Math.random(),Math.random(),Math.random(),1,
Math.random(),Math.random(),Math.random(),1,
Math.random(),Math.random(),Math.random(),1,
Math.random(),Math.random(),Math.random(),1,
Math.random(),Math.random(),Math.random(),1,
Math.random(),Math.random(),Math.random(),1]),
gl.STATIC_DRAW);
}
现在,我们能看见不一样的地方。
看起来和第一个例子差不多,但是我们了解了使用更多的 attribute 将数据由顶点着色器传递给片元着色器。如果你看了图像处理的例子的话,你会发现那里用了一个额外的 attribute 去传递纹理坐标。
Buffers 用来获取顶点和其他点的数据,并传输给 CPU。 gl.createBuffer
创建一个 buffer。 gl.bindBuffer
绑定需要处理的 buffer。gl.bufferData
将数据拷贝到指定的 buffer。
一旦数据在 buffer 中准备好了,我们就需要告诉 WebGL 如何将数据提取出来并且传递给顶点着色器's attributes。
为了完成上述过程,首先,我们先要了解 WebGL 把哪些位置赋值给了 attributes。比如,在上面的代码中:
// 解析 vertex 数据的流向
var positionLocation = gl.getAttribLocation(program,"a_position");
var colorLocation = gl.getAttribLocation(program,"a_color");
一旦我们知道了 attribute 的相关位置,就需要调用2个命令。
gl.enableVertexAttribArray(location);
这个命令告诉 WebGL,我们通过 buffer 提供的数据。
gl.vertexAttribPointer(
location,
numComponents,
typeOfData,
normalizeFlag,
strideToNextPieceOfData,
offsetIntoBuffer);
这个命令让 WebGL 从刚才通过 gl.bindBuffer
绑定的 buffer 中提取数据。该 buffer 包含了每个顶点的组成部分(1 - 4),具体数据的类型是什么 (BYTE, FLOAT, INT, UNSIGNED_SHORT 等等),每个有效数据之间的步长是多少,真实数据在 buffer 中的偏移量是多少。
组成每个顶点的数据长度一般都是 1 到 4。
如果你使用一个单位大小的 buffer 的单一类型的数据的话,那么步长和偏移量总是 0。步长为 0 意味着 “每个步长包括了数据的类型和大小”。偏移量为 0 意味着数据从 buffer 的起始位开始。将他们设置为其他值而不是 0 来说, 会更复杂,尽管这样做在性能方面有些好处。不过,相对于复杂度来说,这并不值得,除非你是想让 WebGL 受它绝对的限制。
我希望上面解决了 buffers 和 attributes 怎样工作的问题。
下面,让我们接着学习 shaders and GLSL。
补充
normalize flag 主要影响所有的非浮点类型。 如果你将它设置为 false, 那么类型的值还是保持不变。BYTE 的大小还是从 -128 到 127,UNSIGNED_BYTE 的大小从 0 到 255, SHORT 的大小从 -32768 到 32767 等等。
如果 normalize flag 设置为 true, 那么 BYTE (-128 到 127) 的大小变为 -1.0 到 +1.0,UNSIGNED_BYTE (0 到 255) 的大小变为 0.0 到 +1.0。 SHORT 的大小和 BYTE 一样也是 -1.0 到 +1.0, 不过他比 BYTE 的分辨率更高。
归一化数据最常用在颜色值上。颜色值基本上都是从 0.0 到 1.0。 使用浮点数去表达红色,绿色,蓝色和透明度,每个顶点的颜色会花掉 16 bytes 的大小。如果你有更复杂的几何图形的话,那累积下来就很大了。如果,你能用 UNSIGNED_BYTEs 去表达你的颜色值, 比如 0 代表 0.0, 255 代表 1.0。那么,你仅仅只需要 4 bytes 去表达每个点的颜色值,这节省了 75% 的大小。
我们来改写一下代码。当我们告诉 WebGL 怎样去提取颜色时,我们需要使用
gl.vertexAttribPointer(colorLocation, 4, gl.UNSIGNED_BYTE, true, 0, 0);
然后, 当我们将颜色值填充到 buffer 里,我们需要使用
// 填充 2 个三角颜色值到 buffer 里
function setColors(gl) {
// 选取 2 个随机颜色
var r1 = Math.random() * 256; // 0 to 255.99999
var b1 = Math.random() * 256; // these values
var g1 = Math.random() * 256; // will be truncated
var r2 = Math.random() * 256; // when stored in the
var b2 = Math.random() * 256; // Uint8Array
var g2 = Math.random() * 256;
gl.bufferData(
gl.ARRAY_BUFFER,
new Uint8Array( // Uint8Array
[ r1, b1, g1, 255,
r1, b1, g1, 255,
r1, b1, g1, 255,
r2, b2, g2, 255,
r2, b2, g2, 255,
r2, b2, g2, 255]),
gl.STATIC_DRAW);
}
下面是个实例。
有问题? 在 stack overflow 上提问。 Issue/Bug? 在 github 上创建 issue。