# 立方体绘制

三维物体也是由二维图形组成的,如下,一个由 12 个三角形组成的立方体:

avatar

在三维世界里,跟二维不同的是,需要在三维世界定义观察者在什么地方、朝哪里看、视野有多宽、能看多远。

# 视点和视线

三维物体与二维图形最显著的区别就是,三维物体具有深度,也就是 z 轴。我们绘制出来的东西还是二维屏幕,只不过观察者可以处在任意位置观察。定义观察者,需要考虑下面两点:

  • 观察方向:从哪个方向看过去,看哪一部分?
  • 可视距离:能看多远。

观察者的位置称为视点,从视点出发沿着观察方向的射线称为视线。

在 webgl 系统中,默认情视点处于原点 (0, 0, 0),视线为 z 轴负半轴(指向屏幕里面)

avatar

# 视点、观察目标点和上方向

为了确定观察者的状态,需要确定三个信息:

  1. 视点:观察者的位置;视线的起点;下面表述都用坐标 (eyeX, eyeY, eyeZ) 表示;
  2. 观察目标点:被观察目标所在的点;他可以同来确定视线。观察目标点是一个点。观察点坐标用 (atX, atY, atZ) 表示;
  3. 上方向:为了将观察到的景色绘制到屏幕上;它是具有三个分量的矢量,用 (upX, upY, upZ) 表示;

avatar

在 webgl 中,可以用上述三个矢量创建一个视图矩阵。然后将该矩阵传给顶点着色器。

封装创建视图矩阵的方法如下:

/**
 * 创建视图矩阵
 * @param eyeX, 指定视点
 * @param atX, 指定观察点
 * @param upX, 指定上方向,如果上方向是 Y 轴正方向,那么传入(0, 1, 0)
 * @return this
 */
Matrix4.prototype.setLookAt = function(eyeX, eyeY, eyeZ, atX, atY, atZ, upX, upY, upZ) {
  var e, fx, fy, fz, rlf, sx, sy, sz, rls, ux, uy, uz;

  fx = atX - eyeX;
  fy = atY - eyeY;
  fz = atZ - eyeZ;

  // Normalize f.
  rlf = 1 / Math.sqrt(fx*fx + fy*fy + fz*fz);
  fx *= rlf;
  fy *= rlf;
  fz *= rlf;

  // Calculate cross product of f and up.
  sx = fy * upZ - fz * upY;
  sy = fz * upX - fx * upZ;
  sz = fx * upY - fy * upX;

  // Normalize s.
  rls = 1 / Math.sqrt(sx*sx + sy*sy + sz*sz);
  sx *= rls;
  sy *= rls;
  sz *= rls;

  // Calculate cross product of s and f.
  ux = sy * fz - sz * fy;
  uy = sz * fx - sx * fz;
  uz = sx * fy - sy * fx;

  e = this.elements;
  e[0] = sx;
  e[1] = ux;
  e[2] = -fx;
  e[3] = 0;

  e[4] = sy;
  e[5] = uy;
  e[6] = -fy;
  e[7] = 0;

  e[8] = sz;
  e[9] = uz;
  e[10] = -fz;
  e[11] = 0;

  e[12] = 0;
  e[13] = 0;
  e[14] = 0;
  e[15] = 1;
  return this.translate(-eyeX, -eyeY, -eyeZ)
};

在 webgl 中,观察者的默认状态是:

  • 视点位于坐标系统圆点 (0, 0, 0);
  • 视线为 z 轴负方向,观察点为 (0, 0, -1),上方向为 y 轴负方向,即 (0, 1, 0)

示例,添加视图矩阵,修改观察点位置:

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_ViewMatrix;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_ViewMatrix * a_Position;\n' +
  '  v_Color = a_Color;\n' +
  '}\n';

// 片元着色器
var FSHADER_SOURCE =
  '#ifdef GL_ES\n' +
  'precision mediump float;\n' +
  '#endif\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_FragColor = v_Color;\n' +
  '}\n';

function main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return;
  }
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to intialize shaders.');
    return;
  }

  // 设置顶点坐标和颜色
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the vertex information');
    return;
  }
  gl.clearColor(0, 0, 0, 1);

  // 获取 u_ViewMatrix 变量的存储地址
  var u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
  if (!u_ViewMatrix) { 
    console.log('Failed to get the storage locations of u_ViewMatrix');
    return;
  }
  // 设置视点、视线和上方向
  var viewMatrix = new Matrix4();
  viewMatrix.setLookAt(0.20, 0.25, 0.25, 0, 0, 0, 0, 1, 0);
  // 将视图矩阵传给 u_ViewMatrix 变量
  gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);
  gl.clear(gl.COLOR_BUFFER_BIT);
  // 绘制三角形
  gl.drawArrays(gl.TRIANGLES, 0, n);
}

function initVertexBuffers(gl) {
  var verticesColors = new Float32Array([
    // 顶点坐标和颜色
     0.0,  0.5,  -0.4,  0.4,  1.0,  0.4, // 绿色三角形在最后面
    -0.5, -0.5,  -0.4,  0.4,  1.0,  0.4,
     0.5, -0.5,  -0.4,  1.0,  0.4,  0.4, 
   
     0.5,  0.4,  -0.2,  1.0,  0.4,  0.4, // T黄色三角形在中间
    -0.5,  0.4,  -0.2,  1.0,  1.0,  0.4,
     0.0, -0.6,  -0.2,  1.0,  1.0,  0.4, 

     0.0,  0.5,   0.0,  0.4,  0.4,  1.0,  // 蓝色三角形在最前面
    -0.5, -0.5,   0.0,  0.4,  0.4,  1.0,
     0.5, -0.5,   0.0,  1.0,  0.4,  0.4, 
  ]);
  var n = 9;

  // 创建缓冲区对象
  var vertexColorbuffer = gl.createBuffer();  
  if (!vertexColorbuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }

  gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorbuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);
  var FSIZE = verticesColors.BYTES_PER_ELEMENT;

  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 -1;
  }
  gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
  gl.enableVertexAttribArray(a_Position);

  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 -1;
  }
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
  gl.enableVertexAttribArray(a_Color);

  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  return n;
}

结果:

avatar

实际上,根据自定义的观察者状态,绘制观察者看到的景象,与使用默认的观察状态,但是对三维对象进行平移、旋转等变换,在绘制观察者看到的景象是一致的。

avatar

# 从指定视点观察旋转后的三角形

完成上面的操作,我们需要两个矩阵:旋转矩阵(三角形旋转操作)和视图矩阵,我们操作的步骤是先旋转三角形,然后从这个视点来观察,就需要先对三角形进行旋转变换,再对旋转后的三角形进行视图矩阵的变换,如下等式:

从视点看去的旋转后的顶点 = 视图矩阵 × 旋转矩阵 × 原始顶点坐标

除了旋转矩阵,还可以对顶点坐标进行基本的平移、旋转等变换矩阵操作,这是这些操作的组合成为模型矩阵。

从视点看去的旋转后的顶点 = 视图矩阵 × 模型矩阵 × 原始顶点坐标

示例代码:

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_ViewMatrix;\n' +
  'uniform mat4 u_ModelMatrix;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_ViewMatrix * u_ModelMatrix * a_Position;\n' +
  '  v_Color = a_Color;\n' +
  '}\n';

// 片元着色器
var FSHADER_SOURCE =
  '#ifdef GL_ES\n' +
  'precision mediump float;\n' +
  '#endif\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_FragColor = v_Color;\n' +
  '}\n';

function main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return;
  }

  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to intialize shaders.');
    return;
  }
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the vertex information');
    return;
  }
  gl.clearColor(0.0, 0.0, 0.0, 1.0);

  // 获取 u_ViewMatrix 和 u_ModelMatrix 的存储地址
  var u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
  var u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix');
  if(!u_ViewMatrix || !u_ModelMatrix) { 
    console.log('Failed to get the storage location of u_viewMatrix or u_ModelMatrix');
    return;
  }

  // 指定视点和视线-视点,观察目标点,上方向
  var viewMatrix = new Matrix4();
  viewMatrix.setLookAt(0.20, 0.25, 0.25, 0, 0, 0, 0, 1, 0);

  // 计算旋转矩阵-绕 z 轴旋转 10°
  var modelMatrix = new Matrix4();
  modelMatrix.setRotate(-10, 0, 0, 1);

  gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);
  gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);

  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.drawArrays(gl.TRIANGLES, 0, n);
}

function initVertexBuffers(gl) {
  var verticesColors = new Float32Array([
     0.0,  0.5,  -0.4,  0.4,  1.0,  0.4,
    -0.5, -0.5,  -0.4,  0.4,  1.0,  0.4,
     0.5, -0.5,  -0.4,  1.0,  0.4,  0.4, 
   
     0.5,  0.4,  -0.2,  1.0,  0.4,  0.4,
    -0.5,  0.4,  -0.2,  1.0,  1.0,  0.4,
     0.0, -0.6,  -0.2,  1.0,  1.0,  0.4, 

     0.0,  0.5,   0.0,  0.4,  0.4,  1.0,
    -0.5, -0.5,   0.0,  0.4,  0.4,  1.0,
     0.5, -0.5,   0.0,  1.0,  0.4,  0.4, 
  ]);
  var n = 9;

  var vertexColorBuffer = gl.createBuffer();  
  if (!vertexColorBuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }

  gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

  var FSIZE = verticesColors.BYTES_PER_ELEMENT;

  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 -1;
  }
  gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
  gl.enableVertexAttribArray(a_Position);

  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 -1;
  }
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
  gl.enableVertexAttribArray(a_Color);

  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  return n;
}

上面例子,在着色器中,我们通过对每个顶点进行计算视图矩阵 × 模型矩阵。如果顶点数量过多,这一步操作就会造成不必要的开销。因为,无论哪个顶点而言,式子中两个矩阵相乘的结果都是一样的。所以我们可以在 JavaScript 中计算好两个矩阵相乘的结果,然后传入到顶点着色器。这两个矩阵相乘得到的结果被称为模型视图矩阵。

模型视图矩阵 = 视图矩阵 × 模型矩阵,所以 从视点看去的旋转后的顶点 = 模型视图矩阵 × 原始顶点坐标;

上面的例子,修改之后如下:

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_ModelViewMatrix;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_ModelViewMatrix * a_Position;\n' +
  '  v_Color = a_Color;\n' +
  '}\n';
// 片元着色器同上例子
function main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return;
  }
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to intialize shaders.');
    return;
  }
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the vertex information');
    return;
  }
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  var u_ModelViewMatrix = gl.getUniformLocation(gl.program, 'u_ModelViewMatrix');
  if(!u_ModelViewMatrix) { 
    console.log('Failed to get the storage location of u_ModelViewMatrix');
    return;
  }
  var viewMatrix = new Matrix4();
  viewMatrix.setLookAt(0.20, 0.25, 0.25, 0, 0, 0, 0, 1, 0);

  var modelMatrix = new Matrix4();
  modelMatrix.setRotate(-10, 0, 0, 1);
  // 获取两个矩阵相乘结果  viewMatrix * modelMatrix
  var modelViewMatrix = viewMatrix.multiply(modelMatrix);
  gl.uniformMatrix4fv(u_ModelViewMatrix, false, modelViewMatrix.elements);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, n);
}

# 使用键盘改变视点

实现通过键盘左右键,修改视点的 x 坐标; 如下代码

// 着色器部分同上代码
function main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return;
  }
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to intialize shaders.');
    return;
  }
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the vertex information');
    return;
  }
  gl.clearColor(0, 0, 0, 1);

  var u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
  if(!u_ViewMatrix) { 
    console.log('Failed to get the storage locations of u_ViewMatrix');
    return;
  }

  var viewMatrix = new Matrix4();
  // 注册键盘事件响应函数
  document.onkeydown = function(ev){ keydown(ev, gl, n, u_ViewMatrix, viewMatrix); };
  draw(gl, n, u_ViewMatrix, viewMatrix);
}

// 视点
var g_eyeX = 0.20, g_eyeY = 0.25, g_eyeZ = 0.25;
function keydown(ev, gl, n, u_ViewMatrix, viewMatrix) {
    // 按下左键
    if(ev.keyCode == 39) {
      g_eyeX += 0.01;
    } else if (ev.keyCode == 37) {
      // 按下左键
      g_eyeX -= 0.01;
    } else { return; }
    draw(gl, n, u_ViewMatrix, viewMatrix);    
}

function draw(gl, n, u_ViewMatrix, viewMatrix) {
  // 设置视点和视线
  viewMatrix.setLookAt(g_eyeX, g_eyeY, g_eyeZ, 0, 0, 0, 0, 1, 0);
  // 传递视图矩阵
  gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, n);
}

# 可视范围(正射类型)

在三维空间中,只有当在可视范围内的时候,webgl 才会去绘制物体,这样可以降低程序的开销。webgl 通过定义水平视角、垂直视角和可视深度(能够看多远)来定义可视空间。

# 可视空间

有两类常用的可视空间:

  • 长方体可视空间,也称盒状空间,由正射投影产生。
  • 四棱锥/金字塔可视空间,由透视投影产生。

我们平时观察的世界用的就是透视投影,相比之下,正射投影的好处是用户可以方便地比较场景中的物体的大小,因为物体看上去的大小与其所在的位置没有关系。

  • 正射投影

如图,可视空间由前后两个矩形表面确定分别称为近裁剪面和远裁剪面,前者四个顶点为 (right, top, -near), (-left, top, -near), (-left, -bottom, -near), (right, -bottom, -near) 后者的四个顶点为 (right, top, far), (-left, -bottom, far), (right, -bottom, far)

avatar

canvas 上显示的就是可视看见中物体在近裁剪平面上的投影。如果裁剪面的宽高比和 canvas 不一样,那么画面就会被按照 canvas 的宽高比进行压缩,物体会被扭曲。近裁剪面与远裁剪面之间的盒型空间就是可视空间,只有在此空间内的物体才会被显示出来。如果有的物体一部分在盒子里一部分在外面,就只显示盒子里面的部分。

# 定义盒状可视空间

封装如下方法,来设置矩阵:这个矩阵被称为正射投影矩阵。

Matrix4.prototype.setOrtho = function(left, right, bottom, top, near, far) {
  var e, rw, rh, rd;

  if (left === right || bottom === top || near === far) {
    throw 'null frustum';
  }

  rw = 1 / (right - left);
  rh = 1 / (top - bottom);
  rd = 1 / (far - near);

  e = this.elements;

  e[0]  = 2 * rw;
  e[1]  = 0;
  e[2]  = 0;
  e[3]  = 0;

  e[4]  = 0;
  e[5]  = 2 * rh;
  e[6]  = 0;
  e[7]  = 0;

  e[8]  = 0;
  e[9]  = 0;
  e[10] = -2 * rd;
  e[11] = 0;

  e[12] = -(right + left) * rw;
  e[13] = -(top + bottom) * rh;
  e[14] = -(far + near) * rd;
  e[15] = 1;

  return this;
};

正射投影矩阵如下:

avatar

下面例子是实现一个将视点设置在原点,视线为 z 轴负方向,可视空间如下图,near = 0.0, far = 0.5, left = -1.0, right = 1.0, bottom = -1.0, top = 1.0, 三角形处于 z 轴 0.0 到 -0.4 区间上。

avatar

代码如下:

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_ProjMatrix;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_ProjMatrix * a_Position;\n' +
  '  v_Color = a_Color;\n' +
  '}\n';
// 片元着色器同上例子
function main() {
  // 初始化同上

  var n =  initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the vertex information');
    return;
  }
  gl.clearColor(0, 0, 0, 1);
  var u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix');
  if (!u_ProjMatrix) { 
    console.log('Failed to get the storage location of u_ProjMatrix');
    return;
  }
  var projMatrix = new Matrix4();
  // 注册键盘事件响应函数
  document.onkeydown = function(ev){ keydown(ev, gl, n, u_ProjMatrix, projMatrix, nf); };

  draw(gl, n, u_ProjMatrix, projMatrix, nf);
}

function initVertexBuffers(gl) {
  var verticesColors = new Float32Array([
     0.0,  0.6,  -0.4,  0.4,  1.0,  0.4,
    -0.5, -0.4,  -0.4,  0.4,  1.0,  0.4,
     0.5, -0.4,  -0.4,  1.0,  0.4,  0.4, 
   
     0.5,  0.4,  -0.2,  1.0,  0.4,  0.4,
    -0.5,  0.4,  -0.2,  1.0,  1.0,  0.4,
     0.0, -0.6,  -0.2,  1.0,  1.0,  0.4, 

     0.0,  0.5,   0.0,  0.4,  0.4,  1.0,
    -0.5, -0.5,   0.0,  0.4,  0.4,  1.0,
     0.5, -0.5,   0.0,  1.0,  0.4,  0.4, 
  ]);
  var n = 9;

  var vertexColorbuffer = gl.createBuffer();  
  if (!vertexColorbuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }

  gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorbuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

  var FSIZE = verticesColors.BYTES_PER_ELEMENT;

  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 -1;
  }
  gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
  gl.enableVertexAttribArray(a_Position);

  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 -1;
  }
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
  gl.enableVertexAttribArray(a_Color);

  return n;
}

// 视点与近、远裁剪面的距离
var g_near = 0.0, g_far = 0.5;
function keydown(ev, gl, n, u_ProjMatrix, projMatrix, nf) {
  switch(ev.keyCode){
    case 39: g_near += 0.01; break;  // 右键
    case 37: g_near -= 0.01; break;  // 左键
    case 38: g_far += 0.01;  break;  // 上键
    case 40: g_far -= 0.01;  break;  // 下键
    default: return;
  }
 
  draw(gl, n, u_ProjMatrix, projMatrix, nf);    
}

function draw(gl, n, u_ProjMatrix, projMatrix, nf) {
  // 设置可视空间
  projMatrix.setOrtho(-1.0, 1.0, -1.0, 1.0, g_near, g_far);

  // 将投影矩阵传递给 u_ProjMatrix 变量
  gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);

  gl.clear(gl.COLOR_BUFFER_BIT);

  // 显示当前的 near 和 far 值
  nf.innerHTML = 'near: ' + Math.round(g_near * 100)/100 + ', far: ' + Math.round(g_far*100)/100;

  gl.drawArrays(gl.TRIANGLES, 0, n);   // Draw the triangles
}

# 结合视图矩阵正射投影矩阵

我们使用视图矩阵以及正射投影矩阵修改视图矩阵那个例子,如下代码:

我们传递给顶点着色器的公式:正射投影矩阵 × 视图矩阵 × 顶点坐标

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_ViewMatrix;\n' +
  'uniform mat4 u_ProjMatrix;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_ProjMatrix * u_ViewMatrix * a_Position;\n' +
  '  v_Color = a_Color;\n' +
  '}\n';

// 片元着色器同上

function main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return;
  }
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to intialize shaders.');
    return;
  }
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to specify the vertex infromation');
    return;
  }
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  var u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
  var u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix');
  if (!u_ViewMatrix || !u_ProjMatrix) { 
    console.log('Failed to get u_ViewMatrix or u_ProjMatrix');
    return;
  }
  var viewMatrix = new Matrix4();
  // 注册键盘事件
 document.onkeydown = function(ev){ keydown(ev, gl, n, u_ViewMatrix, viewMatrix); };
  // 创建指定可视空间的矩阵并传给 u_ProjMatrix 变量
  var projMatrix = new Matrix4();
  projMatrix.setOrtho(-1.0, 1.0, -1.0, 1.0, 0.0, 2.0);
  gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);
  draw(gl, n, u_ViewMatrix, viewMatrix);   // Draw the triangles
}
// 其他代码与视图矩阵例子一致

# 可视空间(透视投影)

在正射投影的可视空间中,不管三角形与视点的距离是远还是近,他又多大,那么画出来的就有多大。可以使用透视投影产生深度感。

# 定义透视投影可视空间

透视投影可视空间如下图,跟盒状可视空间一样,透视投影可视空间也有视点、视线、近裁剪面和远裁剪面,这样可视空间内的物体才会被显示,可视空间外的物体不会显示。

avatar

下面是封装了透视投影矩阵生成方法:该矩阵称为透视投影矩阵

/**
 * 通过个参数计算透视投影矩阵,将其存储在 Matrix4 中,注意 near 的值必须小于 far
 * @param fovy 指定垂直视角,即可视空间顶面和地面间的夹角,必须大于 0
 * @param aspect 指定近裁剪面的宽高比(宽度/高度)
 * @param near 指定近裁剪面和远裁剪面的位置,即可视空间的近边界和远边界(near 和 far 必须都大于 0)
 * @param far 
 * @return this
 */
Matrix4.prototype.setPerspective = function(fovy, aspect, near, far) {
  var e, rd, s, ct;

  if (near === far || aspect === 0) {
    throw 'null frustum';
  }
  if (near <= 0) {
    throw 'near <= 0';
  }
  if (far <= 0) {
    throw 'far <= 0';
  }

  fovy = Math.PI * fovy / 180 / 2;
  s = Math.sin(fovy);
  if (s === 0) {
    throw 'null frustum';
  }

  rd = 1 / (far - near);
  ct = Math.cos(fovy) / s;

  e = this.elements;

  e[0]  = ct / aspect;
  e[1]  = 0;
  e[2]  = 0;
  e[3]  = 0;

  e[4]  = 0;
  e[5]  = ct;
  e[6]  = 0;
  e[7]  = 0;

  e[8]  = 0;
  e[9]  = 0;
  e[10] = -(far + near) * rd;
  e[11] = -1;

  e[12] = 0;
  e[13] = 0;
  e[14] = -2 * near * far * rd;
  e[15] = 0;

  return this;
};

透视投影创建的矩阵如下:

avatar

下图是该例子中三角形的位置, avatar

下图是各个三角形与可视空间的相对位置,我们指定了 near = 1.0, far = 100, aspect = 1.0 宽高相等,以及 fov = 30.0 avatar

代码如下:

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_ViewMatrix;\n' +
  'uniform mat4 u_ProjMatrix;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_ProjMatrix * u_ViewMatrix * a_Position;\n' +
  '  v_Color = a_Color;\n' +
  '}\n';

// 片元着色器同上例子
function main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return;
  }s
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to intialize shaders.');
    return;
  }
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the vertex information');
    return;
  }
  gl.clearColor(0, 0, 0, 1);
  var u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
  var u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix');
  if (!u_ViewMatrix || !u_ProjMatrix) { 
    console.log('Failed to get the storage location of u_ViewMatrix and/or u_ProjMatrix');
    return;
  }

  var viewMatrix = new Matrix4(); // 视图矩阵
  var projMatrix = new Matrix4();  // 投影矩阵

  // 计算视图矩阵与投影矩阵
  viewMatrix.setLookAt(0, 0, 5, 0, 0, -100, 0, 1, 0);
  projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);

  gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);
  gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, n);
}

function initVertexBuffers(gl) {
  var verticesColors = new Float32Array([
    // 右侧三个三角形
    0.75,  1.0,  -4.0,  0.4,  1.0,  0.4,
    0.25, -1.0,  -4.0,  0.4,  1.0,  0.4,
    1.25, -1.0,  -4.0,  1.0,  0.4,  0.4, 

    0.75,  1.0,  -2.0,  1.0,  1.0,  0.4,
    0.25, -1.0,  -2.0,  1.0,  1.0,  0.4,
    1.25, -1.0,  -2.0,  1.0,  0.4,  0.4, 

    0.75,  1.0,   0.0,  0.4,  0.4,  1.0,
    0.25, -1.0,   0.0,  0.4,  0.4,  1.0,
    1.25, -1.0,   0.0,  1.0,  0.4,  0.4, 

    // 左侧三个三角形
   -0.75,  1.0,  -4.0,  0.4,  1.0,  0.4,
   -1.25, -1.0,  -4.0,  0.4,  1.0,  0.4,
   -0.25, -1.0,  -4.0,  1.0,  0.4,  0.4, 

   -0.75,  1.0,  -2.0,  1.0,  1.0,  0.4,
   -1.25, -1.0,  -2.0,  1.0,  1.0,  0.4,
   -0.25, -1.0,  -2.0,  1.0,  0.4,  0.4, 

   -0.75,  1.0,   0.0,  0.4,  0.4,  1.0,
   -1.25, -1.0,   0.0,  0.4,  0.4,  1.0,
   -0.25, -1.0,   0.0,  1.0,  0.4,  0.4, 
  ]);
  var n = 18; // 绘制 6 个三角形
  var vertexColorbuffer = gl.createBuffer();  
  if (!vertexColorbuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorbuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

  var FSIZE = verticesColors.BYTES_PER_ELEMENT;
  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 -1;
  }
  gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
  gl.enableVertexAttribArray(a_Position);
  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 -1;
  }
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
  gl.enableVertexAttribArray(a_Color);
  return n;
}

结果如下:

avatar

其实透视投影矩阵对三角形进行了两次变换:

  1. 根据三角形与视点的距离,按比例对三角形进行了缩小变换。
  2. 对三角形进行平移变换,使其贴近视线。

avatar

透视投影矩阵实际是将金字塔状的可视空间变换为了盒状的可视空间,这个盒状的可视空间又称为规范立方体。正射投影矩阵不能产生深度感,正射投影矩阵的工作仅仅是将顶点从盒状的可视空间映射到规范立方体中。顶点着色器输出的顶点都必须在规范立方体中,这样才会显示在屏幕上。

# 视图矩阵、模型矩阵和投影矩阵

我们使用下面公式来实现一个综合的例子:投影矩阵 × 视图矩阵 × 模型矩阵 × 顶点坐标,

代码如下:

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_ModelMatrix;\n' +
  'uniform mat4 u_ViewMatrix;\n' +
  'uniform mat4 u_ProjMatrix;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_ProjMatrix * u_ViewMatrix * u_ModelMatrix * a_Position;\n' +
  '  v_Color = a_Color;\n' +
  '}\n';

// 片元着色器同上

function main() {
  var n = initVertexBuffers(gl);

  var u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix');
  var u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix');
  var u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix');
  if (!u_ModelMatrix || !u_ViewMatrix || !u_ProjMatrix) { 
    console.log('Failed to Get the storage locations of u_ModelMatrix, u_ViewMatrix, and/or u_ProjMatrix');
    return;
  }

  var modelMatrix = new Matrix4(); // 模型矩阵
  var viewMatrix = new Matrix4();  // 视图矩阵
  var projMatrix = new Matrix4();  // 投影矩阵

  // 计算模型矩阵、视图矩阵和投影矩阵 matrix
  modelMatrix.setTranslate(0.75, 0, 0);  // x 轴平移 0.75 个单位
  viewMatrix.setLookAt(0, 0, 5, 0, 0, -100, 0, 1, 0);
  projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);

  gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);
  gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements);
  gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements);

  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.drawArrays(gl.TRIANGLES, 0, n);   // 绘制右侧的一组三角形

  // 为另一侧的三角形重新计算模型矩阵
  modelMatrix.setTranslate(-0.75, 0, 0); // x 轴平移 -0.75 单位
  // 修改模型矩阵
  gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);

  gl.drawArrays(gl.TRIANGLES, 0, n);   // 绘制左侧的一组三角形
}

function initVertexBuffers(gl) {
  var verticesColors = new Float32Array([
    // 顶点坐标以及颜色
     0.0,  1.0,  -4.0,  0.4,  1.0,  0.4,
    -0.5, -1.0,  -4.0,  0.4,  1.0,  0.4,
     0.5, -1.0,  -4.0,  1.0,  0.4,  0.4, 

     0.0,  1.0,  -2.0,  1.0,  1.0,  0.4,
    -0.5, -1.0,  -2.0,  1.0,  1.0,  0.4,
     0.5, -1.0,  -2.0,  1.0,  0.4,  0.4, 

     0.0,  1.0,   0.0,  0.4,  0.4,  1.0,
    -0.5, -1.0,   0.0,  0.4,  0.4,  1.0,
     0.5, -1.0,   0.0,  1.0,  0.4,  0.4, 
  ]);
  var n = 9;
  var vertexColorbuffer = gl.createBuffer();  
  if (!vertexColorbuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }

  gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorbuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

  var FSIZE = verticesColors.BYTES_PER_ELEMENT;

  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 -1;
  }

  gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
  gl.enableVertexAttribArray(a_Position);

  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 -1;
  }
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
  gl.enableVertexAttribArray(a_Color);

  return n;
}

上面例子中,我们通过在顶点着色器中计算好矩阵进行相乘计算最终的结果,可以在 JavaScript 中计算好结果然后传递给顶点着色器,传入的矩阵被称为模型视图投影矩阵。如下代码:

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_MvpMatrix;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_MvpMatrix * a_Position;\n' +
  '  v_Color = a_Color;\n' +
  '}\n';

// 片元着色器同上

function main() {
  var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
  if (!u_MvpMatrix) { 
    console.log('Failed to get the storage location of u_MvpMatrix');
    return;
  }

  var modelMatrix = new Matrix4(); // 模型矩阵
  var viewMatrix = new Matrix4();  // 视图矩阵
  var projMatrix = new Matrix4();  // 投影矩阵
  var mvpMatrix = new Matrix4();   // 模型视图投影矩阵

  // 计算模型、视图、投影矩阵
  modelMatrix.setTranslate(0.75, 0, 0);
  viewMatrix.setLookAt(0, 0, 5, 0, 0, -100, 0, 1, 0);
  projMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
  // 计算模型视图投影矩阵
  mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);
  // 将模型视图投影矩阵传递给 u_MvpMatrix
  gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

  gl.clear(gl.COLOR_BUFFER_BIT);

  gl.drawArrays(gl.TRIANGLES, 0, n);

  modelMatrix.setTranslate(-0.75, 0, 0);

  mvpMatrix.set(projMatrix).multiply(viewMatrix).multiply(modelMatrix);

  gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

  gl.drawArrays(gl.TRIANGLES, 0, n);
}

# 正确处理对象的前后关系

webgl 在默认情况下会按照缓冲区中的顺序绘制图形,而且后绘制的图形覆盖先绘制的图形,因为这样做很高效。如果场景中的对象不发生运动,观察者的状态也是唯一的,那么这种做法没有问题。但是如果,比如说你希望不断移动视点,从不同的角度看物体,那么你不可能事先决定对象出现的顺序。

# 隐藏面消失

webgl 提供了隐藏面消失功能。可以消除那些被遮挡的表面。

开启隐藏面消除功能需要两步:

  1. 开启隐藏消除功能:gl.enable(gl.DEPTH_TEST)
  2. 在绘制之前,清除深度缓冲区:gl.clear(gl.DEPTH_BUFFER_BIT)

gl.enable() 函数说明如下:

avatar

在第二步中,清除深度缓冲区,深度缓冲区是一个中间对象,其作用是帮助 webgl 进行隐藏;webgl 在颜色缓冲区中绘制几何图形,绘制完成后将颜色缓冲区显示在 canvas 上。如果要将隐藏面消除,那就必须知道每个几何图形的深度信息,而深度缓冲度就是用来存储深度信息的。由于深度方向通常是 z 轴方向,所以有时候也成为 z 轴缓冲区。

在绘制任意一帧之前,都必须清除深度缓冲区,以消除绘制上一帧时在其中留下的痕迹。如果不这样做,就会出现错误的结果,调用 gl.clear() 方法,传入 gl.DEPTH_BUFFER_BIT 参数清除深度缓冲区。

当然,还需要清除颜色缓冲区。用按位或符号(|)连接 gl.DEPTH_BUFFER_BIT 和 gl.COLOR_BUFFER_BIT 并作为参数传入 gl.clear() 方法

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)

同时清除任意两个缓冲区时,都可以使用按位或符号。

与 gl.enable() 函数对应的还有 gl.disable() 函数,使用如下: 他是禁用某个功能 avatar

# 示例代码(清除深度缓冲区)

// 顶点、片元着色器同上例子
function main() {
  // ...
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the vertex information');
    return;
  }

  // 指定清除 canvas 背景色
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  // 开启隐藏面消除
  gl.enable(gl.DEPTH_TEST);

  // ...

  // 清空颜色和深度缓冲区
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

}

function initVertexBuffers(gl) {
  var verticesColors = new Float32Array([
    // 前面的蓝色三角形
     0.0,  1.0,   0.0,  0.4,  0.4,  1.0,
    -0.5, -1.0,   0.0,  0.4,  0.4,  1.0,
     0.5, -1.0,   0.0,  1.0,  0.4,  0.4, 

     0.0,  1.0,  -2.0,  1.0,  1.0,  0.4, // 中间黄色三角形
    -0.5, -1.0,  -2.0,  1.0,  1.0,  0.4,
     0.5, -1.0,  -2.0,  1.0,  0.4,  0.4,

     0.0,  1.0,  -4.0,  0.4,  1.0,  0.4, // 后面绿色三角形
    -0.5, -1.0,  -4.0,  0.4,  1.0,  0.4,
     0.5, -1.0,  -4.0,  1.0,  0.4,  0.4, 
  ]);
  var n = 9;
  // 其他逻辑不变
  return n;
}

如下结果: avatar

这里需要注意的是,隐藏面消除的前提是正确设置可视空间,否则就可能产生错误的结果。不管是盒状的正射投影空间,还是金字塔状的透视投影空间,必须使用其中一个。

# 深度冲突

当几何图形或物体的两个表面极为接近时,就会出现新的问题,使得表面看上去斑斑驳驳。这种现象称为深度冲突。之所以会产生深度冲突,是因为两个表面过于接近,深度缓冲区有限的精度已经不能区分哪个在前,哪个在后了。严格地说,如果创建三维模型阶段就对顶点的深度值加以注意,是能够避免深度冲突的。但是当场景中有多个运动着的物体时,实现这一点几乎是不可能的。

avatar

webgl 提供了一种被称为多边形偏移的机制来解决。该机制将自动在 z 值加上一个偏移量,偏移量的值由物体表面相对于观察者视线的角度来确定。使用如下:

  1. 启用多边形偏移:gl.enable(gl.POLYGON_OFFSET_FILL)
  2. 在绘制之前指定用来计算偏移量的参数:gl.polygonOffset(1.0, 1.0)

gl.polygonOffset() 函数规范如下:

avatar

示例关键代码如下:

var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_ViewProjMatrix;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_ViewProjMatrix * a_Position;\n' +
  '  v_Color = a_Color;\n' +
  '}\n';

var FSHADER_SOURCE =
  '#ifdef GL_ES\n' +
  'precision mediump float;\n' +
  '#endif\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_FragColor = v_Color;\n' +
  '}\n';

function main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return;
  }
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to intialize shaders.');
    return;
  }
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the vertex information');
    return;
  }
  gl.clearColor(0, 0, 0, 1);
  gl.enable(gl.DEPTH_TEST);
  var u_ViewProjMatrix = gl.getUniformLocation(gl.program, 'u_ViewProjMatrix');
  if (!u_ViewProjMatrix) { 
    console.log('Failed to get the storage locations of u_ViewProjMatrix');
    return;
  }
  var viewProjMatrix = new Matrix4();
  viewProjMatrix.setPerspective(30, canvas.width/canvas.height, 1, 100);
  viewProjMatrix.lookAt(3.06, 2.5, 10.0, 0, 0, -2, 0, 1, 0);
  gl.uniformMatrix4fv(u_ViewProjMatrix, false, viewProjMatrix.elements);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  // 启用多边形偏移
  gl.enable(gl.POLYGON_OFFSET_FILL);
  gl.drawArrays(gl.TRIANGLES, 0, n/2);   // 绘制绿色三角形
  gl.polygonOffset(1.0, 1.0);          // 设置多边形偏移量---使得以后的绘制受到多边形偏移机制影响
  gl.drawArrays(gl.TRIANGLES, n/2, n/2); // 绘制黄色三角形
}

function initVertexBuffers(gl) {
  var verticesColors = new Float32Array([
     0.0,  2.5,  -5.0,  0.4,  1.0,  0.4,
    -2.5, -2.5,  -5.0,  0.4,  1.0,  0.4,
     2.5, -2.5,  -5.0,  1.0,  0.4,  0.4, 

     0.0,  3.0,  -5.0,  1.0,  0.4,  0.4,
    -3.0, -3.0,  -5.0,  1.0,  1.0,  0.4,
     3.0, -3.0,  -5.0,  1.0,  1.0,  0.4, 
  ]);
  var n = 6;
  var vertexColorbuffer = gl.createBuffer();  
  if (!vertexColorbuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorbuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

  var FSIZE = verticesColors.BYTES_PER_ELEMENT;
  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 -1;
  }
  gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
  gl.enableVertexAttribArray(a_Position);
  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 -1;
  }
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
  gl.enableVertexAttribArray(a_Color);

  return n;
}

结果: avatar

# 立方体

绘制一个如下的立方体: avatar

如果我们继续使用前面的方式绘制,立方体的每个面需要两个三角形,需要 6 个顶点坐标,那么绘制完所有的需要 36 个顶点坐标,这样会很麻烦,webgl 体用了一种完美的方案:gl.drawElements() ,使用该函数替代 gl.drawArrays() 函数进行绘制,能够避免重复定义。通过索引去获取坐标:

avatar

顶点列表中共有 8 个顶点,索引值为从 0 到 7;这样用一个数据结构就可以描述立方体是怎样由顶点坐标和颜色构成的了。

# 通过顶点索引绘制物体

我们需要在 gl.ELEMENT_ARRAY_BUFFER (不是之前的 gl.ARRAY_BUFFER) 中指定顶点的索引值,所以 gl.drawArrays() 和 gl.drawElements() 的区别就在于 gl.ELEMENT_ARRAY_BUFFER,它管理者具有索引结构的三维模型数据。

gl.drawElements() 方法说明如下:

avatar

数据输出格式:ArrayBuffer;其他使用 drawArrays 传入顶点数据。

avatar

代码如下:

// 顶点着色器
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_MvpMatrix;\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_Position = u_MvpMatrix * a_Position;\n' +
  '  v_Color = a_Color;\n' +
  '}\n';

// 片元着色器
var FSHADER_SOURCE =
  '#ifdef GL_ES\n' +
  'precision mediump float;\n' +
  '#endif\n' +
  'varying vec4 v_Color;\n' +
  'void main() {\n' +
  '  gl_FragColor = v_Color;\n' +
  '}\n';

function main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return;
  }
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to intialize shaders.');
    return;
  }
  // 设置顶点坐标和颜色
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the vertex information');
    return;
  }
  // 设置背景色并开启隐藏面消除
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.enable(gl.DEPTH_TEST);
  var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
  if (!u_MvpMatrix) { 
    console.log('Failed to get the storage location of u_MvpMatrix');
    return;
  }

  // 设置顶点和可视空间
  var mvpMatrix = new Matrix4();
  mvpMatrix.setPerspective(30, 1, 1, 100);
  mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);
  gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

  // 清空颜色缓冲区和视图缓冲区
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

  // 绘制立方体-n 表示顶点索引数组的长度,也就是顶点着色器的执行次数
  gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

function initVertexBuffers(gl) {
  //    v6----- v5
  //   /|      /|
  //  v1------v0|
  //  | |     | |
  //  | |v7---|-|v4
  //  |/      |/
  //  v2------v3
  var verticesColors = new Float32Array([
      // 顶点坐标和颜色
     1.0,  1.0,  1.0,     1.0,  1.0,  1.0,  // v0 White
    -1.0,  1.0,  1.0,     1.0,  0.0,  1.0,  // v1 Magenta
    -1.0, -1.0,  1.0,     1.0,  0.0,  0.0,  // v2 Red
     1.0, -1.0,  1.0,     1.0,  1.0,  0.0,  // v3 Yellow
     1.0, -1.0, -1.0,     0.0,  1.0,  0.0,  // v4 Green
     1.0,  1.0, -1.0,     0.0,  1.0,  1.0,  // v5 Cyan
    -1.0,  1.0, -1.0,     0.0,  0.0,  1.0,  // v6 Blue
    -1.0, -1.0, -1.0,     0.0,  0.0,  0.0   // v7 Black
  ]);

  // 顶点和颜色
  var indices = new Uint8Array([
    0, 1, 2,   0, 2, 3,    // front
    0, 3, 4,   0, 4, 5,    // right
    0, 5, 6,   0, 6, 1,    // up
    1, 6, 7,   1, 7, 2,    // left
    7, 4, 3,   7, 3, 2,    // down
    4, 7, 6,   4, 6, 5     // back
 ]);

  // 创建缓冲区对象
  var vertexColorBuffer = gl.createBuffer();
  var indexBuffer = gl.createBuffer();
  if (!vertexColorBuffer || !indexBuffer) {
    return -1;
  }

  // 将顶点坐标和颜色写入缓冲区对象
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexColorBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verticesColors, gl.STATIC_DRAW);

  var FSIZE = verticesColors.BYTES_PER_ELEMENT;
  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 -1;
  }
  // 获取顶点坐标传给 a_Position
  gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0);
  gl.enableVertexAttribArray(a_Position);
  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 -1;
  }
  // 将缓冲区内顶点颜色数据分配给 a_Color
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3);
  gl.enableVertexAttribArray(a_Color);

  // 将顶点索引数据写入缓冲区对象
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

  return indices.length;
}

通过两个缓冲区对象将顶点坐标以及颜色数据跟顶点索引数据传递给了着色器,此时,webgl 系统的内部状态如图:

avatar

调用 gl.drawElements() 时,webgl 首先从绑定到 gl.ELEMENT_ARRAY_BUFFER 的缓冲区中获取顶点的索引值,然后根据索引值,从绑定到 gl.ARRAY_BUFFER 的缓冲区中获取顶点的坐标、颜色等信息,然后传递给 attribute 变量并执行顶点着色器。

# 为立方体的每个面指定颜色

顶点着色器进行的是逐顶点的计算,接收的是逐顶点的信息,这说明,如果想指定表面的信息,也需要将颜色定义为逐顶点的信息,并传给顶点着色器。但是每个顶点不仅仅在一个面上出现。为了解决这个问题,可以创建多个相同顶点坐标的顶点。

avatar

此时的三角形列表,也就是顶点索引序列,对每个面都指向一组不同的顶点,不再有重复。

代码如下: 上面例子将顶点的坐标和颜色存储在同一个缓冲区中,下面的例子是将顶点的坐标和颜色分别存储在不同的两个缓冲区中。

// 着色器部分同上
function main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  if (!gl) {
    console.log('Failed to get the rendering context for WebGL');
    return;
  }
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
    console.log('Failed to intialize shaders.');
    return;
  }
  // 设置顶点信息
  var n = initVertexBuffers(gl);
  if (n < 0) {
    console.log('Failed to set the vertex information');
    return;
  }

  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.enable(gl.DEPTH_TEST);
  var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
  if (!u_MvpMatrix) {
    console.log('Failed to get the storage location of u_MvpMatrix');
    return;
  }
  var mvpMatrix = new Matrix4();
  mvpMatrix.setPerspective(30, 1, 1, 100);
  mvpMatrix.lookAt(3, 3, 7, 0, 0, 0, 0, 1, 0);
  gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

function initVertexBuffers(gl) {
  //    v6----- v5
  //   /|      /|
  //  v1------v0|
  //  | |     | |
  //  | |v7---|-|v4
  //  |/      |/
  //  v2------v3

  var vertices = new Float32Array([   // 顶点坐标
     1.0, 1.0, 1.0,  -1.0, 1.0, 1.0,  -1.0,-1.0, 1.0,   1.0,-1.0, 1.0,  // v0-v1-v2-v3 front
     1.0, 1.0, 1.0,   1.0,-1.0, 1.0,   1.0,-1.0,-1.0,   1.0, 1.0,-1.0,  // v0-v3-v4-v5 right
     1.0, 1.0, 1.0,   1.0, 1.0,-1.0,  -1.0, 1.0,-1.0,  -1.0, 1.0, 1.0,  // v0-v5-v6-v1 up
    -1.0, 1.0, 1.0,  -1.0, 1.0,-1.0,  -1.0,-1.0,-1.0,  -1.0,-1.0, 1.0,  // v1-v6-v7-v2 left
    -1.0,-1.0,-1.0,   1.0,-1.0,-1.0,   1.0,-1.0, 1.0,  -1.0,-1.0, 1.0,  // v7-v4-v3-v2 down
     1.0,-1.0,-1.0,  -1.0,-1.0,-1.0,  -1.0, 1.0,-1.0,   1.0, 1.0,-1.0   // v4-v7-v6-v5 back
  ]);

  var colors = new Float32Array([     // 颜色
    0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  // v0-v1-v2-v3 front(blue)
    0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  // v0-v3-v4-v5 right(green)
    1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  // v0-v5-v6-v1 up(red)
    1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  // v1-v6-v7-v2 left
    1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  // v7-v4-v3-v2 down
    0.4, 1.0, 1.0,  0.4, 1.0, 1.0,  0.4, 1.0, 1.0,  0.4, 1.0, 1.0   // v4-v7-v6-v5 back
  ]);

  var indices = new Uint8Array([       // 顶点索引
     0, 1, 2,   0, 2, 3,    // front
     4, 5, 6,   4, 6, 7,    // right
     8, 9,10,   8,10,11,    // up
    12,13,14,  12,14,15,    // left
    16,17,18,  16,18,19,    // down
    20,21,22,  20,22,23     // back
  ]);

  // 创建缓冲区对象
  var indexBuffer = gl.createBuffer();
  if (!indexBuffer) 
    return -1;
  // 将顶点坐标和颜色写入缓冲区对象
  if (!initArrayBuffer(gl, vertices, 3, gl.FLOAT, 'a_Position'))
    return -1;

  if (!initArrayBuffer(gl, colors, 3, gl.FLOAT, 'a_Color'))
    return -1;

  // 将顶点索引写入缓冲区对象
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

  return indices.length;
}

function initArrayBuffer(gl, data, num, type, attribute) {
  var buffer = gl.createBuffer();   // 创建缓冲区对象
  if (!buffer) {
    console.log('Failed to create the buffer object');
    return false;
  }
  // 将数据写入缓冲区对象
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
  var a_attribute = gl.getAttribLocation(gl.program, attribute);
  if (a_attribute < 0) {
    console.log('Failed to get the storage location of ' + attribute);
    return false;
  }
  gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
  // 将缓冲区对象分配给 attribute 变量
  gl.enableVertexAttribArray(a_attribute);

  return true;
}

评 论:

更新: 11/21/2020, 7:00:56 PM