上一节通过一个简单的案例对比了 Canvas 2D 和 WebGL 在绘制一个点上的差异,初步介绍了 WebGL 的初始化、着色器语法、着色器代码加载以及坐标系差异。接下来再介绍点线面的绘制,进一步了解上一节中使用的相关代码。
众所周知,点是图形的基础,线是两个点的连线组成的,面是三个点以上的连线组成的。为了在着色器中添加更多的点,这个时候就涉及到 JavaScript 和着色器代码之间的通信了。
接下来我们定义 6 个点,来组成我们想要的点线面(注意坐标系的中心点是在画布中央,所以 X 轴的范围是 -1.0~1.0,Y 轴同理,这里只展示二维图形,故 Z 统一为 0):
var points = [
-0.7, 0.0, 0.0, // v1 |
-0.2, 0.4, 0.0, // v2 * | * *
0.0, -0.4, 0.0, // v3 --*-------|----------
0.7, 0.4, 0.0, // v4 * *
0.4, 0.4, 0.0, // v5 |
0.7, -0.4, 0.0 // v6
]
如何把这些顶点信息传到顶点着色器呢,首先在顶点着色器中声明变量:
<!-- 定义顶点着色器 -->
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec3 a_Position;
void main() {
gl_Position = vec4(a_Position, 1.0); // 设置点的位置
gl_PointSize = 5.0; // 设置点的大小
}
</script>
通过定义一个 vec3 的向量来表示 xyz 坐标,同时将其作为前三个值传入 vec4 变量中(这是 Shader 语法的便捷之处),实现对输入数据的定义。
attribute 被称为存储限定符(storage qualifier),它具备以下特点:
接下来借助以下函数将顶点数据传输到着色器中:
// 定义6个点
var points = [
-0.7, 0.0, 0.0, // v1
-0.2, 0.4, 0.0, // v2
0.0, -0.4, 0.0, // v3
0.7, 0.4, 0.0, // v4
0.4, 0.4, 0.0, // v5
0.7, -0.4, 0.0 // v6
]
// 获取 attribute 变量的存储位置
// 一般我们习惯在 attribute 变量命名里加一个前缀 a_
var a_Position = gl.getAttribLocation(gl.program, 'a_Position')
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position')
return
}
// 将顶点位置传输给 attribute 变量
// vertexAttrib3f 的同族函数有:vertexAttrib1f/vertexAttrib2f/vertexAttrib4f
// 数字表示其传值的数量,f 表示传值类型(浮点型)
for(var i=0; i<points.length; i+=3) {
gl.vertexAttrib3f(a_Position, points[i], points[i+1], points[i+2])
gl.drawArrays(gl.POINTS, 0, 1)
}
See the Pen 4. Draw Point & Line & Triangle (part1) - WebGL by ShaderFans (@shaderfans) on CodePen.
看完源码的你一定会觉得这种循环传值的方式太繁琐了,现实中往往需要一次性传入非常多的顶点(如加载 3D 模型),所以需要批量导入。WebGL 提供了一种很方便的机制,即「缓冲区对象(buffer object)」来让我们可以一次性向着色器传入多个顶点数据:
// Float32Array:单精度 32 位浮点数
// WebGL 引入的一种类型化数组,通常用于存储顶点坐标和颜色数据
// 通过明确数据类型来提升 WebGL 的处理效率和性能,与 Array 不同的是:不支持 push() 和 pop()
var points = new Float32Array([
-0.7, 0.0, 0.0, // v1
-0.2, 0.4, 0.0, // v2
0.0, -0.4, 0.0, // v3
0.7, 0.4, 0.0, // v4
0.4, 0.4, 0.0, // v5
0.7, -0.4, 0.0 // v6
]);
// 获取 attribute 变量的存储位置
var a_Position = gl.getAttribLocation(gl.program, 'a_Position')
if (a_Position < 0) {
console.log('Failed to get the storage location of a_Position')
return
}
// 创建缓冲区对象
var pointsBuffer = gl.createBuffer()
if (!pointsBuffer) {
console.log('Failed to create the buffer object')
return -1;
}
// 绑定缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, pointsBuffer)
// 写入缓冲区对象
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
// 将缓冲区对象赋值于 attribute 变量中
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0)
// 开启 attribute 变量(使顶点着色器能够访问缓冲区内的数据)
gl.enableVertexAttribArray(a_Position)
// 一次性绘制6个点
gl.drawArrays(gl.POINTS, 0, 6)
See the Pen 5. Draw Point & Line & Triangle (part2) - WebGL by ShaderFans (@shaderfans) on CodePen.
点绘制完了,接下来就将它们连线,这个时候就需要特别讲讲这个强大的函数 drawArrays()
了:
// 函数说明来自《WebGL 编程指南》
// mode:绘制模式(详见下表格)
// first:指定从哪个顶点开始绘制
// count:执行绘制需要用到多少顶点
void gl.drawArrays(mode, first, count);
基本图形 | 参数 mode | 描述 |
---|---|---|
点 | gl.POINTS | 一系列点,绘制在 v0, v1, v2...处 |
线段 | gl.LINES | 一系列单独的线段,绘制在 (v0,v1)、(v2,v3)...处,如果点的个数是奇数,最后一个点将被忽略 |
线条 | gl.LINE_STRIP | 一系列连接的线段,绘制在 (v0,v1)、(v1,v2)...处,第1个点是第一条线段的起点,第二个点是第1条线段的终点和第二条线段的起点...以此类推。最后一个点事最后一条线段的终点 |
回路 | gl.LINE_LOOP | 一系列连接的线段,与 gl.LINE_STRIP 绘制的线相比,增加了一条从最后一个点到第一个点的线段 |
三角形 | gl.TRIANGLES | 一系列单独的三角形,绘制在 (v0,v1,v2)、(v3,v4,v5)...处,如果点的个数不是3的整数倍,则忽略剩下的1个或2个点 |
三角带 | gl.TRIANGLE_STRIP | 一系列条带状三角形,前三个点构成了第一个三角形,从第2个点开始的三个点构成了第二个三角形,该三角形与前一个三角形共享一条边,以此类推。注意绘制顺序:(v0,v1,v2)、(v2,v1,v3)、(v3,v2,v4)... |
三角扇 | gl.TRIANGLE_FAN | 一系列三角形组成的类似于扇形的图形。前三个点构成了第一个三角形,接下来的一个点和前一个三角形的最后一条边 组成接下来的三角形。这些三角形被绘制在 (v0,v1,v2)、(v0,v2,v3)、(v0,v3,v4)...处 |
文字总是苍白的,下面通过一个 Demo 让你更好地理解 mode 参数可以绘制什么图形(切换左下角下拉菜单可见区别):
See the Pen 7. Understand drawArrays - WebGL by ShaderFans (@shaderfans) on CodePen.
通过对 drawArrays() 的灵活应用,我们可以很轻松地绘制出想要的形状:
// 点
var point = new Float32Array([
-0.7, 0.0, 0.0 // v1
]);
// 线
var line = new Float32Array([
-0.2, 0.4, 0.0, // v2
0.0, -0.4, 0.0 // v3
]);
// 面
var triangle = new Float32Array([
0.7, 0.4, 0.0, // v4
0.4, 0.4, 0.0, // v5
0.7, -0.4, 0.0 // v6
]);
...
// 绘制点
gl.bufferData(gl.ARRAY_BUFFER, point, gl.STATIC_DRAW)
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(a_Position)
gl.drawArrays(gl.POINT, 0, 1)
// 绘制线
gl.bufferData(gl.ARRAY_BUFFER, line, gl.STATIC_DRAW)
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(a_Position)
gl.drawArrays(gl.LINES, 0, 2)
// 绘制面
gl.bufferData(gl.ARRAY_BUFFER, triangle, gl.STATIC_DRAW)
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0)
gl.enableVertexAttribArray(a_Position)
gl.drawArrays(gl.TRIANGLES, 0, 3)
See the Pen 6. Draw Point & Line & Triangle (part3) - WebGL by ShaderFans (@shaderfans) on CodePen.
接下来我们尝试给这些图形赋予颜色,之前我们的片元着色器统一设置为白色: gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
。 假设希望每个图形都有各自的颜色,那就需要对每个顶点定义一个颜色,这就意味着同样的需要批量导入颜色数组。
首先分析着色器有什么变化:
<!-- 定义顶点着色器 -->
<script id="vertexShader" type="x-shader/x-vertex">
attribute vec3 a_Position;
attribute vec3 a_Color;
varying vec3 v_Color;
void main() {
gl_Position = vec4(a_Position, 1.0);
gl_PointSize = 5.0;
v_Color = a_Color;
}
</script>
<!-- 定义片元着色器 -->
<script id="fragmentShader" type="x-shader/x-fragment">
varying vec3 v_Color;
void main() {
gl_FragColor = vec4(v_Color, 1.0); // 设置顶点颜色
}
</script>
除了新增一个 attribute 变量 a_Color 来表示顶点的 rgb 颜色之外,还新增了一个 varying 变量 v_Color (正如 attribute 的变量以 a_ 开头,varying 变量则以 v_ 开头)。varying 和 attribute 有和区别?
varying 变量是顶点和片元着色器之间做数据传递用的。一般在顶点着色器中修改 varying 变量的值,然后在片元着色器中使用该 varying 变量的值。因此 varying 变量在着色器之间的声明必须是一致的。
所以顶点着色器和片元着色器中都需要有该定义语句:varying vec3 v_Color;
。 顶点着色器中赋值:v_Color = a_Color;
,片元着色器中使用:gl_FragColor = vec4(v_Color, 1.0);
。
既然 attribute 专属于顶点着色器,varying 用户顶点和片元着色器通信,那自然而然需要有另外一个存储限定符用于片元着色器,这个就是 uniform(注意 uniform 并非片元着色器独占,顶点着色器中也可以使用)。
uniform 变量是外部程序传递给顶点和片元着色器的变量,注意在两个着色器中都可以使用 uniform。而且如果 uniform 变量在着色器两者之间声明方式完全一样,则它可以被共享使用。同样的,我们习惯给 uniform 变量一个 u_ 前缀。
对于 a_Color,我们可以像 a_Position 一样创建缓冲区对象进行赋值,当然也可以直接复用同一个缓冲区,具体做法如下:
// 分别在每个点后面增加了 rgb 色值
// 点
var point = new Float32Array([
-0.7, 0.0, 0.0, 1.0, 0.5, 0.5 // v1: position + color
]);
// 线
var line = new Float32Array([
-0.2, 0.4, 0.0, 0.0, 1.0, 1.0, // v2
0.0, -0.4, 0.0, 1.0, 1.0, 0.0 // v3
]);
// 面
var triangle = new Float32Array([
0.7, 0.4, 0.0, 1.0, 0.0, 1.0, // v4
0.4, 0.4, 0.0, 1.0, 1.0, 0.0, // v5
0.7, -0.4, 0.0, 0.0, 1.0, 0.0 // v6
]);
...
// 获取 attribute 变量的存储位置(顶点颜色)
var a_Color = gl.getAttribLocation(gl.program, 'a_Color')
if (a_Color < 0) {
console.log('Failed to get the storage location of a_Color')
return
}
...
// 获取数组中每个数据元素占用字节数
var FSIZE = point.BYTES_PER_ELEMENT;
// 绘制点(线、面同理)
gl.bufferData(gl.ARRAY_BUFFER, point, gl.STATIC_DRAW)
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0)
gl.enableVertexAttribArray(a_Position)
gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3)
gl.enableVertexAttribArray(a_Color)
gl.drawArrays(gl.POINT, 0, 1)
这里需要特别讲讲 vertexAttribPointer() 这个函数:
// index:指定待分配 attribute 变量的存储位置
// size:指定缓冲区中每个顶点的分量个数
// type:指定数据格式类型:gl.SHORT、gl.INT、gl.FLOAT 等
// normalized:是否将非浮点型的数量归一化
// stride:指定相邻两个顶点间的字节数,默认为0
// offset:指定缓冲区对象中的偏移量(以字节为单位)
void gl.vertexAttribPointer(index, size, type, normalized, stride, offset);
通过上图相信大家对每个参数的含义都有一定的了解了,在 WebGL 中这种组织数据的方式被称作:交替组织数据(Interleaved Array)。所谓交替,指的是 buffer 中可以包含顶点多个不同的属性,不同属性交替在 buffer 中放置。
See the Pen 8. Draw Point & Line & Triangle (part4) - WebGL by ShaderFans (@shaderfans) on CodePen.
看完 Demo 大家可能会疑问:我明明只对顶点设置了颜色,为什么会存在渐变?欲深入了解内部机制可查看下一小节扩展阅读。