Skip to main content
  1. Blogs/
  2. 前端开发/
  3. 可视化/
  4. WebGL/

WebGL编程指南03:缓存区和图形变换

·996 words·5 mins
Table of Contents

前言
#

首先,构成三维模型的基本单位是三角形,那意味着一张画布中会会充满着许多三角形,并有多个顶点构成。

在上一篇章中,我们只能绘制一个点,为了一次性进行多点绘制,WebGL 提供一种 缓冲区机制(buffer object) 来向着色器一次性设置多个顶点的能力,并可以绘制出 WebGL 多种基础图形。

另外,我们可以在顶点作色器变量 gl_Position 中通过计算来实现图形的平移和旋转,最后可以通过矩阵来简化操作。

认识缓冲区
#

我们通过 示例:「一次性绘制多个点」 来认识缓冲区的使用方式,效果如下:

|300

缓存区调用步骤:

  1. 准备工作:定义顶点着色器和片段着色器,约定好顶点位置及大小(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);
	}`;
  1. 通过 initVertexBuffers 方法初始化缓存区逻辑:
function main() {
  // 初始化 gl
  var canvas = document.getElementById("webgl");
  var gl = getWebGLContext(canvas);

  // 初始化着色器程序
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
  }

  // 初始化缓存区
  var n = initVertexBuffers(gl);

  // 绘制
}
  1. 缓冲区的代码逻辑:

    首先通过 vertices 约定好三个顶点坐标数组。使用 gl.createBuffergl.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;
}
  1. 绘制图形:按照 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);

可以看到这样的效果:

|300

在 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
  ]);

|300

gl.LINE_STRIP(线条)
#

var vertices = new Float32Array([0, 0.5, -0.5, -0.5, 0.5, -0.5, 1, 0.5]);

|300

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]);

|300

gl.TRIANGLE_STRIP
#

var vertices = new Float32Array([-0.5, 0.5, -1, -0.5, 0, -0.5, 0.5, 0]);

|300

gl.TRIANGLE_FAN(三角扇)
#

var vertices = new Float32Array([-0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5]);

|300

变化(移动,旋转,缩放)
#

平移
#

现在尝试如何将 gl.TRIANGLE 例子中的三角形移动到屏幕的右上角。

这里将修改三角形的顶点数据,这样的操作成为 逐顶点操作(per-vertex operation)

|300

  1. 修改顶点着色器:在位置坐标处添加平移变量 u_Translation
var VSHADER_SOURCE = `attribute vec4 a_Position;
  uniform vec4 u_Translation;
  void main() {
    gl_Position = a_Position + u_Translation;
  }`;
  1. 获取平移变量存储位置
var n = initVertexBuffers(gl);
var u_Translation = gl.getUniformLocation(gl.program, "u_Translation");
  1. 给平移变量赋值(赋予次坐标值)
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;

旋转
#

旋转示意效果图:

|300

  1. 通过 三角函数 换算,设置顶点位置坐标:
/**
 * 点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;
  }`;
  1. 通过 弧度制 计算三角函数值
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);
  1. 赋值给将三角函数值传递给顶点着色器:
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...

矩阵旋转
#

  1. 引用矩阵工具函数:
<script src="../lib/cuon-matrix.js"></script>
  1. 在顶点着色器中,使用矩阵旋转来替换三角函数:
var VSHADER_SOURCE = `
  attribute vec4 a_Position;
  uniform mat4 u_xformMatrix;
  void main() {
    gl_Position = u_xformMatrix * a_Position;
  }`;
  1. 定义变换矩阵:
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
]);
  1. 在绘制之前,将变换矩阵传递给顶点着色器:
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// 平移
]);

|300