前言#
首先,构成三维模型的基本单位是三角形,那意味着一张画布中会会充满着许多三角形,并有多个顶点构成。
在上一篇章中,我们只能绘制一个点,为了一次性进行多点绘制,WebGL 提供一种 缓冲区机制(buffer object) 来向着色器一次性设置多个顶点的能力,并可以绘制出 WebGL 多种基础图形。
另外,我们可以在顶点作色器变量 gl_Position
中通过计算来实现图形的平移和旋转,最后可以通过矩阵来简化操作。
认识缓冲区#
我们通过 示例:「一次性绘制多个点」 来认识缓冲区的使用方式,效果如下:
缓存区调用步骤:
- 准备工作:定义顶点着色器和片段着色器,约定好顶点位置及大小(10px),和点的颜色(红色)
// Vertex shader program
var VSHADER_SOURCE = `
attribute vec4 a_Position;
void main() {
gl_Position = a_Position;
gl_PointSize = 10.0;
}`;
// Fragment shader program
var FSHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}`;
- 通过
initVertexBuffers
方法初始化缓存区逻辑:
function main() {
// 初始化 gl
var canvas = document.getElementById("webgl");
var gl = getWebGLContext(canvas);
// 初始化着色器程序
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
}
// 初始化缓存区
var n = initVertexBuffers(gl);
// 绘制
}
缓冲区的代码逻辑:
首先通过
vertices
约定好三个顶点坐标数组。使用gl.createBuffer
和gl.bindBuffer
创建并绑定缓存区,再通过gl.bufferData
将缓存区对象和vertices
绑定,最后通过gl.vertexAttribPointer
将缓存区的数据按约定的size
多次绘制到顶点变量gl_Position
中。
function initVertexBuffers(gl) {
// prettier-ignore
var vertices = new Float32Array([
0.0, 0.5, // 第一个点
-0.5, -0.5, // 第二个点
0.5, -0.5 // 第三个点
]);
var n = 3; // The number of vertices
/**
* 1. 创建缓存区
* gl.deleteBuffer(buffer) 方法用于删除缓存区对象。
*/
var vertexBuffer = gl.createBuffer();
if (!vertexBuffer) {
// 如果返回 null,表示创建缓存区失败
console.log("Failed to create the buffer object");
return -1;
}
/**
* 2. 将缓存区绑定到目标
* gl.bindBuffer(target, buffer) 方法接受两个参数:
* target:绑定目标,可以是 gl.ARRAY_BUFFER 或 gl.ELEMENT_ARRAY_BUFFER。
* gl.ARRAY_BUFFER:顶点属性数据缓存区(本例子)。
* gl.ELEMENT_ARRAY_BUFFER:顶点索引缓存区。
* buffer:缓存区对象。
*/
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
/**
* 3. 向缓存区写入数据
* gl.bufferData(target, data, usage) 方法接受三个参数:
* target:缓存区目标,可以是 gl.ARRAY_BUFFER 或 gl.ELEMENT_ARRAY_BUFFER。
* data:数据
* usage:使用方法,可以是 gl.STATIC_DRAW、gl.DYNAMIC_DRAW 或 gl.STREAM_DRAW。
* gl.STATIC_DRAW:只会向缓冲区写入一次,但需要绘制多次(本例子)。
* gl.DYNAMIC_DRAW:只会向缓冲区写入一次,但需要绘制若干次。
* gl.STREAM_DRAW:会向缓存区多次写入数据,并绘制多次。
*/
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
var a_Position = gl.getAttribLocation(gl.program, "a_Position");
/**
* 4. 将缓存区数据分配给 a_Position 变量
* gl.vertexAttribPointer(location, size, type, normalized, stride, offset) 方法接受多个参数:
* location:attribute 变量的存储位置,即 gl.getAttribLocation 返回的变量。
* size:顶点属性的大小。(本例为2,表示每个顶点属性有 2 个值 x,y。)
* type:数据类型。
* gl.FLOAT:32 位浮点数。
* gl.BYTE:8 位有符号整数,范围是 [-128, 127]。
* gl.UNSIGNED_BYTE:8 位无符号整数,范围是 [0, 255]。
* gl.SHORT:16 位有符号整数,范围是 [-32768, 32767]。
* gl.UNSIGNED_SHORT:16 位无符号整数,范围是 [0, 65535]。
* gl.INT:32 位有符号整数,范围是 [-2147483648, 2147483647]。
* gl.UNSIGNED_INT:32 位无符号整数,范围是 [0, 4294967295]。
* normalized:是否归一化,WebGL 默认是 false。
* stride:相邻顶点属性之间的字节数。
* offset:顶点属性的起始位置(偏移量)。
*/
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
/**
* 5. 链接 a_Position 变量和缓存区
* gl.enableVertexAttribArray(index) 方法接受一个参数:
* index:顶点属性索引,即 gl.getAttribLocation 返回的变量。
*/
gl.enableVertexAttribArray(a_Position);
return n;
}
- 绘制图形:按照
gl.POINTS
方式绘制图形
var n = initVertexBuffers(gl);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, n);
WebGL 中的基本图形#
在绘制图形步骤中,我们调用了 gl.drawArrays
方法,现在我们将 gl.POINTS
改为 gl.TRIANGLES
看下效果:
gl.drawArrays(gl.POINTS, 0, n);
可以看到这样的效果:
在 WebGL 中的基本图形如下:
mode | 描述 |
---|---|
gl.POINTS | 绘制点 |
gl.LINES | 绘制线段,按 (v0,v1), (v2,v3),(v4,v5) 顺序绘制(2 个 2 个,不连续) |
gl.LINE_STRIP | 绘制线段,并连接所有点。按 (v0,v1), (v1,v2), (v2,v3) 顺序绘制(2 个 2 个,连续) |
gl.LINE_LOOP | 绘制线段,并连接第一个和最后一个点。按 (v0,v1), (v1,v2), (v2,v0) 顺序绘制(2 个 2 个,连续,首尾再连接) |
gl.TRIANGLES | 绘制(多个独立的)三角形。按 (v0,v1,v2), (v3,v4,v5) 顺序绘制(3 个 3 个,不连续) |
gl.TRIANGLE_STRIP | 绘制三角形,并连接所有点(三角形共用一条边)。按 (v0,v1,v2), (v1,v2,v3), (v2,v3,v4) 顺序绘制(3 个 3 个,每次顺延 1 个点) |
gl.TRIANGLE_FAN | 绘制三角形,共享起点。按 (v0,v1,v2), (v0,v2,v3), (v0,v3,v4) 顺序绘制。 |
下面看下它们的样子:
gl.LINES(线段)#
// prettier-ignore
var vertices = new Float32Array(
[
0, 0.5,
-0.5, -0.5,
0.2, -0.5,
0.7, 0.5
]);
gl.LINE_STRIP(线条)#
var vertices = new Float32Array([0, 0.5, -0.5, -0.5, 0.5, -0.5, 1, 0.5]);
gl.LINE_LOOP(回路)#
var vertices = new Float32Array([-0.5, 0.5, -1, -0.5, 0, -0.5, 0.5, 0.5, -0.5, 0.5, -1, -0.5]);
gl.TRIANGLE_STRIP#
var vertices = new Float32Array([-0.5, 0.5, -1, -0.5, 0, -0.5, 0.5, 0]);
gl.TRIANGLE_FAN(三角扇)#
var vertices = new Float32Array([-0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5]);
变化(移动,旋转,缩放)#
平移#
现在尝试如何将 gl.TRIANGLE
例子中的三角形移动到屏幕的右上角。
这里将修改三角形的顶点数据,这样的操作成为 逐顶点操作(per-vertex operation) 。
- 修改顶点着色器:在位置坐标处添加平移变量 u_Translation
var VSHADER_SOURCE = `attribute vec4 a_Position;
uniform vec4 u_Translation;
void main() {
gl_Position = a_Position + u_Translation;
}`;
- 获取平移变量存储位置
var n = initVertexBuffers(gl);
var u_Translation = gl.getUniformLocation(gl.program, "u_Translation");
- 给平移变量赋值(赋予次坐标值)
var Tx = 0.5,
Ty = 0.5,
Tz = 0.0;
gl.uniform4f(u_Translation, Tx, Ty, Tz, 0.0);
// 最终我们新三角形的位置将是 gl_Position = a_Position + u_Translation;
旋转#
旋转示意效果图:
- 通过 三角函数 换算,设置顶点位置坐标:
/**
* 点p坐标为(x,y),p和原点连线为r,则:
* x = r * cos(α)
* y = r * sinα(α)
*
* 旋转后,p1坐标为(x1,y1),但 p1 和原点连线还是为 r 长度,则:
* x1 = r * cos(α + β)
* y1 = r * sin(α + β)
*
* 根据三角函数两角和公式
* x1 = x * cos(α + β) - y * sin(α + β)
* y1 = x * sin(α + β) + y * cos(α + β)
*
* 换算得到,则为顶点坐标写法:
* x1 = x * cos(β) - y * sin(β)
* y1 = x * sin(β) + y * cos(β)
*
*/
var VSHADER_SOURCE = `attribute vec4 a_Position;
uniform float u_CosB, u_SinB;
void main() {
gl_Position.x = a_Position.x * u_CosB - a_Position.y * u_SinB;
gl_Position.y = a_Position.x * u_SinB + a_Position.y * u_CosB;
gl_Position.z = a_Position.z;
gl_Position.w = 1.0;
}`;
- 通过 弧度制 计算三角函数值
var n = initVertexBuffers(gl);
// Pass the data required to rotate the shape to the vertex shader
var radian = (Math.PI * ANGLE) / 180.0; // Convert to radians
var cosB = Math.cos(radian);
var sinB = Math.sin(radian);
- 赋值给将三角函数值传递给顶点着色器:
var u_CosB = gl.getUniformLocation(gl.program, "u_CosB");
var u_SinB = gl.getUniformLocation(gl.program, "u_SinB");
gl.uniform1f(u_CosB, cosB);
gl.uniform1f(u_SinB, sinB);
// draw...
矩阵旋转#
- 引用矩阵工具函数:
<script src="../lib/cuon-matrix.js"></script>
- 在顶点着色器中,使用矩阵旋转来替换三角函数:
var VSHADER_SOURCE = `
attribute vec4 a_Position;
uniform mat4 u_xformMatrix;
void main() {
gl_Position = u_xformMatrix * a_Position;
}`;
- 定义变换矩阵:
var n = initVertexBuffers(gl);
var radian = (Math.PI * ANGLE) / 180.0; // Convert to radians
var cosB = Math.cos(radian),
sinB = Math.sin(radian);
/**
* 上面在旋转示例中,我们已经知道旋转后
* x1 点为:x1 = x * cos(β) - y * sin(β)
* y1 点为:y1 = x * sin(β) + y * cos(β)
* 那么旋转矩阵则为
* [cosB, -sinB, 0.0, 0.0] * x
* [sinB, cosB, 0.0, 0.0] * y
* [0.0, 0.0, 1.0, 0.0] * z
* [0.0, 0.0, 0.0, 1.0] * 1
*/
// u_xformMatrix * a_Position [x,y,z,1]
// prettier-ignore
var xformMatrix = new Float32Array([
cosB, sinB, 0.0, 0.0,
-sinB, cosB, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
]);
- 在绘制之前,将变换矩阵传递给顶点着色器:
var u_xformMatrix = gl.getUniformLocation(gl.program, "u_xformMatrix");\
/**
* gl.uniformMatrix4fv(location, transpose, value)
* location: uniform变量的位置
* transpose: 是否转置矩阵,在 webgl 中为 false
* value: 4x4矩阵,即 xformMatrix 的值
*/
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);
矩阵平移#
类似,我们可以通过矩阵平移来改变三角形的位置:
var Tx = 0.5,
Ty = 0.5,
Tz = 0.0;
// prettier-ignore
var xformMatrix = new Float32Array([
// x轴缩放
1.0, 0.0, 0.0, 0.0,
// y轴缩放
0.0, 1.0, 0.0, 0.0,
// z轴缩放
0.0, 0.0, 1.0, 0.0,
// 平移
Tx, Ty, Tz, 1.0
]);
矩阵缩放#
类似,我们可以通过矩阵缩放来改变三角形的大小:
var Sx = 1.0,
Sy = 1.5,
Sz = 1.0;
// prettier-ignore
var xformMatrix = new Float32Array([
Sx, 0.0, 0.0, 0.0, // x轴缩放
0.0, Sy, 0.0, 0.0, // y轴缩放
0.0, 0.0, Sz, 0.0, // z轴缩放
0.0, 0.0, 0.0, 1.0// 平移
]);