【《WebGL编程指南》读书笔记-进入三维世界(上)】

本文为读书笔记第七章上半部分
总目录链接:https://blog.csdn.net/floating_heart/article/details/124001572
因为章节内容较多,所以分为上下两部分。
上部分包括以下内容:

  • 初步了解三维呈现方式:视点、观察点、上方向。
  • 了解WebGL可视空间,采用投影矩阵对物体进行投影操作,实现盒状可视空间和正射投影。对于正射投影矩阵的数学原理进行了本书之外的补充说明。
  • 透视投影可视空间、投影矩阵和模型视图投影矩阵的相关操作,提及规范立方体。

第7章 进入三维世界(上)

老规矩,将书中此章的前言部分记录如下:

前几章的示例程序渲染的都是二维图形。通过这些示例程序,我们了解了 WebGL系统的工作原理、着色器的作用、矩阵变换(平移和旋转)、动画和纹理映射等等。事实上,这些知识不仅适用于绘制二维图形,也适用于绘制三维图形。在这一章中,我们将进入三维的世界,探索如何把这些知识用到三维世界中。具体地,我们将研究:

  • 以用户视角而进入三维世界
  • 控制三维可视空间
  • 裁剪
  • 处理物体的前后关系
  • 绘制三维的立方体

以上这些内容对于如何绘制三维场景,如何将场景展现给用户非常重要。只有理解了这些内容,才能够去创建复杂的三维场景。我们将一步一步地学习,本章先帮助你快速掌握绘制三维物体的基本技能,后面几章再涉及更加复杂的问题,比如实现光照效果等一些高级技术。


立方体由三角形组成

三维图形也是由二维图形(特别是三角形)组成的,如下图所示,一个立方体由12个三角形组成:

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第1张图片

所以我们只需要像前几章那样,逐个绘制组成物体的每个三角形,就可以绘制出整个三维物体了。

除此之外,三维相对于二维增加了深度信息(depth information)。

下面我们先从视角,到可视空间等等,一步步了解三维的模式。


视点和视线

相关内容:1. 引入视图矩阵(LookAtTriangles.js),补充内容-视图矩阵的原理;Matrix.setLookAt()的细节(处理非垂直的视线和上方向输入);2. 同时使用视图矩阵和模型矩阵(LookAtRotatedTriangels.js);3. 加入键盘控制(LookAtTrianglesWithKeys.js)。
相关函数:Matrix.setLookAt(), Matrix.multiply()

小结:

在呈现的时候,我们最后还是得把三维场景绘制到二维屏幕上,即绘制观察者看到的世界,而观察者可以处在任意位置观察。为了定义观察者,我们需要考虑以下两点:

  • 观察方向,即观察者自己在什么位置,在看场景的哪一部分?
  • 可视距离,即观察者能够看多远?

本节主要讨论观察方向的问题,由此引出了两个概念:视点和视线。

视点(eye point):观察者所处的位置;

视线(viewing direction):从视点出发沿着观察方向的射线。

本节,我们创建了一个新的示例程序LookAtTriangles.js,程序中视点位于(0.20,0.25,0.25),视线向着原点(0,0,0)方向,可以看到原点附近有三个三角形,三角形前后错落摆放,以帮助理解三维场景中深度的概念。

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第2张图片


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

在编写代码之前,我们需要了解一些三维图形的基本知识。

为了确定观察者的状态,我们需要获取三项信息:

  • 视点(eye point):观察者所在的三维空间中位置,视线的起点。此处使用(eyeX, eyeY, eyeZ)表示,OpenGL中常称作相机。
  • 观察目标点(look-at point):被观察目标所在的点。视线从视点出发,穿过观察目标点并继续延伸。观察目标点是一个点而不是视线方向,只有同时知道观察目标点和视点,才能算出视线方向。观察目标点用(atX, atY, atZ)表示
  • 上方向(up direction):最终绘制在屏幕上的影像中的向上的方向。为了将观察者固定住,我们还需要指定上方向,上方向是具有三个分量的 矢量,用(upX,upY,upZ)表示。

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第3张图片

在WebGL中,我们可以用上述三个矢量创建一个视图矩阵(view matrix),然后将矩阵传给顶点着色器。

视图矩阵可以表示观察者的状态,含有观察者的视点、观察目标点、上方向等信息,最终影响显示在屏幕上的视图,也就是观察者观察到的场景。

本示例中使用cuon-matrix.js库中提供的Matrix4.setLookAt()函数,根据三项信息创建视图矩阵:

Matrix.setLookAt(eyeX, eyeY, eyeZ, atX, atY, atZ, upX, upY, upZ)
根据视点(eyeX, eyeY, eyeZ)、观察点(atX, atY, atZ)、上方向(upX, upY, upZ)创建视图矩阵。视图矩阵的类型是Matrix4,其观察点映射到的中心点。
参数:
eyeX, eyeY, eyeZ: 指定视点
atX, atY, atZ: 指定观察点
upX, upY, upZ: 指定上方向,如果上方向是Y轴正方向,那么(upX, upY, upZ)就是(0, 1, 0)
返回值:

在WebGL中,观察者的默认状态如下:

  • 视点位于坐标系统原点(0,0,0)
  • 视线为Z轴负方向,观察点为(0,0,-1),上方向为Y轴正方向,即(0,1,0)

补充:视图矩阵的原理

视图矩阵只是变换次数较多的模型矩阵。要认识这一点,只需要明确两个基本信息:

信息一: 在WebGL中,观察者的默认状态如下:

  • 视点位于坐标系统原点(0,0,0)
  • 视线为Z轴负方向,观察点为(0,0,-1),上方向为Y轴正方向,即(0,1,0)

我们之前所做的所有二维示例,都是在这一条件下绘制的。

信息二: 视点、视线和上方向与被观察物体之间的关系是相对的。

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第4张图片

如上图所示,左图和右图在观察者的角度呈现的图形是一样的。

所以,我们只需要把现有的视点、视线和上方向变换到WebGL默认状态,对物体做同样的变换,即可得到需要的图形。这就是视图矩阵的原理。

相关变换无疑属于仿射变换,通过之前模型矩阵一节中讲解的内容,采用矩阵和矩阵乘法即可获得所需视图矩阵。细节此处不再赘述。


视图矩阵的应用-示例程序LookAtTriangels.js

// LookAtTriangles.js
// 顶点着色器
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 =
  'precision mediump float;\n' +
  'varying vec4 v_Color;\n' +
  'void main(){\n' +
  ' gl_FragColor = v_Color;\n' +
  '}\n'
// 主函数
function main() {
  // 获取canvas元素
  let canvas = document.getElementById('webgl')
  // 获取webgl上下文
  let 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 initialize shaders')
    return
  }
  // 设置顶点坐标和颜色
  let n = initVertexBuffers(gl)
  if (n < 0) {
    console.log('Failed to set the positions of the vertices')
    return
  }
  // 获取u_ViewMatrix存储地址
  let u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix')
  if (!u_ViewMatrix) {
    console.log('Failed to get the storage loaction of u_ViewMatrix')
    return
  }
  // 设置视点、视线和上方向
  let viewMatrix = new Matrix4()
  viewMatrix.setLookAt(0.25, 0.25, 0.25, 0, 0, 0, 0, 1, 0)
  // viewMatrix.setLookAt(0.25, 0.25, 0.25, 0, 0, 0, -0.25, 0.75, -0.25) // 视线和上方向可以不垂直么
  // 将视图矩阵传递给u_ViewMatrix
  gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements)
  // 绘制三角形
  gl.clearColor(0.0, 0.0, 0.0, 1.0)
  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.drawArrays(gl.TRIANGLES, 0, n)
}
// 设置顶点坐标和颜色
function initVertexBuffers(gl) {
  // 准备数据
  let 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,
  ])
  let n = 9
  // 创建缓冲区对象
  let 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)

  let FSIZE = verticesColors.BYTES_PER_ELEMENT
  // a_Position配置
  let 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)
  // a_Color配置
  let 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
}

本例基于第五章ColoredTriangle.js示例改编,片元着色器、传入数据的方式等二者相同,区别主要为如下三点:

  • 视图矩阵被传给顶点着色器,并与顶点坐标相乘;
// 顶点着色器
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'
...
  • initVertexBuffers()函数创建了3个三角形的顶点坐标和颜色数据,并在main()函数中被调用;
...
  // 准备数据
  let 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,
  ])
...
   gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, FSIZE * 6, 0)
...
  gl.vertexAttribPointer(a_Color, 3, gl.FLOAT, false, FSIZE * 6, FSIZE * 3)
...
  • main()函数计算了视图矩阵并传给顶点着色器中的uniform变量u_viewMatrix。视点坐标为(0.25,0.25,0.25),观察点坐标为(0,0,0),上方向为(0,1,0)。
  // 设置视点、视线和上方向
  let viewMatrix = new Matrix4()
  viewMatrix.setLookAt(0.25, 0.25, 0.25, 0, 0, 0, 0, 1, 0)
  // 将视图矩阵传递给u_ViewMatrix
  gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements)

补充:Matrix.setLookAt()的细节——视线和上方向不垂直?

在上一个示例中,我们看到viewMatrix.setLookAt()中给出的视线和上方向并不垂直:视线为 ( 0 , 0 , 0 ) − ( 0.25 , 0.25 , 0.25 ) = ( − 0.25 , − 0.25 , − 0.25 ) (0,0,0)-(0.25,0.25,0.25)=(-0.25,-0.25,-0.25) (0,0,0)(0.25,0.25,0.25)=(0.25,0.25,0.25),上方向为 ( 0 , 1 , 0 ) (0,1,0) (0,1,0)。但在正确的理解中,二者应该互相垂直。实际上,viewMatrix.setLookAt()函数对这种情况进行了处理,此处进行简单介绍,详细过程可见源码。

首先,关于矢量计算有一个计算规则:两个矢量叉乘获得的新矢量垂直于两个矢量构成的平面。viewMatrix.setLookAt()函数对视线和上方向两个矢量及其运算结果进行了两次叉乘运算,将上方向投影到了与视线垂直的平面之上,两次运算过程如下:

  // 第一次运算:Calculate cross product of f and up.
  // f为视线矢量
  sx = fy * upZ - fz * upY;
  sy = fz * upX - fx * upZ;
  sz = fx * upY - fy * upX;
  // 第二次运算:Calculate cross product of s and f.
  // u为投影后的上方向矢量
  ux = sy * fz - sz * fy;
  uy = sz * fx - sx * fz;
  uz = sx * fy - sy * fx;

之后根据新的上方向、视线矢量和视点坐标,构建视图矩阵即可。

根据以上说明,我们如果对上方向矢量加n倍的视线矢量(n为任意数值),绘图的结果不变,如下面的代码所示:

  // viewMatrix.setLookAt(0.25, 0.25, 0.25, 0, 0, 0, -0.25, 0.75, -0.25) // 视线和上方向可以不垂直么

视图矩阵+旋转矩阵—示例程序LookAtRotatedTriangles.js

上一个示例展示了视图矩阵的添加方式,如果我们在变换视角的同时也需要对图形进行旋转平移等变换,如何处理视图矩阵和模型矩阵的顺序,就是此处讨论的问题。答案也比较简单:
< “ 从 视 点 看 上 去 ” 的 旋 转 后 顶 点 坐 标 > = < 视 图 矩 阵 > × < 模 型 矩 阵 > × < 原 始 顶 点 坐 标 > <“从视点看上去”的旋转后顶点坐标>=<视图矩阵>\times<模型矩阵>\times<原始顶点坐标> <>=<>×<>×<>
相关解释如下:

视图矩阵也可以认为是将顶点坐标变换到合适的位置,使得观察者(以默认状态)观察新位置的顶点,就好像观察者处在(视图矩阵描述的)视点上观察原始顶点一样,具体在上面的补充内容——补充:视图矩阵的原理——中有所呈现。
从便于理解的角度来说,我们需要先对三角形进行旋转,再从固定的视角观察它。或者说,我们需要先对三角形进行基本变换,再对变换后的三角形进行与“移动视点”等效的变换。(否则,就需要考虑旋转轴在“移动视点”等效变换后的空间位置了,变得复杂。)

了解了上述原理后,实现就变得简单。示例LookAtRotatedTriangles.js展示了从某一视点观察旋转后物体的方法,示例效果如下:

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第5张图片

该示例相比于上一个示例,变动的地方如下:

  • 顶点着色器中设置模型矩阵u_ModelMatrix
// 顶点着色器
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'
  • 主函数中配置模型矩阵
  // 获取u_ModelMatrix的存储地址
  let u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix')
  if (!u_ModelMatrix) {
    console.log('Failed to get the storage loaction of u_ModelMatrix')
    return
  }
  // 计算旋转矩阵
  let modelMatrix = new Matrix4()
  modelMatrix.setRotate(-90, 0, 0, 1)
  // 将旋转矩阵传递给u_ModelMatrix
  gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)

简化顶点着色器的计算

如果顶点的数量很多,在顶点着色器中的 < 视 图 矩 阵 > × < 模 型 矩 阵 > <视图矩阵>\times<模型矩阵> <>×<>操作会造成不必要的开销,此处可以模仿 < 模 型 矩 阵 > = < 旋 转 矩 阵 > × < 平 移 矩 阵 > <模型矩阵>=<旋转矩阵>\times<平移矩阵> <>=<>×<>的方式,事先就给出视图矩阵和模型矩阵相乘的结果,该结果称为模型视图矩阵(model view matrix)。
< 模 型 视 图 矩 阵 > = < 视 图 矩 阵 > × < 模 型 矩 阵 > <模型视图矩阵>=<视图矩阵>\times<模型矩阵> <>=<>×<>
据此,可以改写LookAtRotatedTriangles.js的代码如下:

  • 顶点着色器部分:
// 顶点着色器
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'
  • 矩阵计算和传输部分:
  // 获取u_ModelViewMatrix存储地址
  let u_ModelViewMatrix = gl.getUniformLocation(gl.program, 'u_ModelViewMatrix')
  if (!u_ModelViewMatrix) {
    console.log('Failed to get the storage loaction of u_ModelViewMatrix')
    return
  }
  // 设置视点、视线和上方向
  let viewMatrix = new Matrix4()
  viewMatrix.setLookAt(0.25, 0.25, 0.25, 0, 0, 0, 0, 1, 0)

  // 计算旋转矩阵
  let modelMatrix = new Matrix4()
  modelMatrix.setRotate(-90, 0, 0, 1)
  // 两个矩阵相乘
  let modelViewMatrix = viewMatrix.multiply(modelMatrix)
  
  // 直接使用如下方法,不计算旋转矩阵和相乘
  // let modelViewMatrix = viewMatrix.rotate(-90, 0, 0, 1)
  
  // 将模型视图矩阵传递给u_ViewMatrix
  gl.uniformMatrix4fv(u_ModelViewMatrix, false, modelViewMatrix.elements)

此处其实也可以使用Matrix.rotate()方法,在viewMatrix矩阵基础上直接和旋转矩阵相乘。示例中的方法只是方便理解。


利用键盘改变视点—LookAtTrianglesWithKeys.js

LookAtTrianglesWithKeys.js在之前视图矩阵示例LookAtTriangles.js的基础上,加入了键盘响应函数,通过左右方向键控制视点沿X轴移动。效果如下:

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第6张图片

与绘制动画相似,示例使用了浏览器自带的鼠标点击事件来绑定函数,通过不断擦除-绘制形成视点移动效果。主要改动如下:

  • 为了方便每次擦除-绘制操作,将该操作封装为draw()函数,draw()函数在主函数末尾首次调用
// 绘制函数
function draw(gl, n, u_ViewMatrix, viewMatrix) {
  // 设置视点和视线
  viewMatrix.setLookAt(g_eyeX, g_eyeY, g_eyeZ, 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)
}
  • 注册键盘响应事件
  // 注册键盘事件响应函数
  document.onkeydown = function (ev) {
    keydown(ev, gl, n, u_ViewMatrix, viewMatrix)
  }
  • window对象下挂载初始视点,响应事件对视点进行修改并调用draw()函数进行绘制
// 键盘响应事件
var g_eyeX = 0.2,
  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)
}

可视范围与盒状可视空间

相关内容:1. 正射投影、正射投影矩阵与盒状可视空间(OrthoView.js);2. 投影矩阵与视图矩阵的先后顺序(LookAtTrianglesWithKeys_ViewVolume.js);3. 可视空间长宽比与标签窗口大小不一致的问题。
相关函数:Matrix4.setOrtho()

WebGL绘制物体时存在可视范围的概念,只有物体处于可视范围内,WebGL才会绘制它。这种设计一方面符合人眼观察物体的方式,一方面也降低了程序开销。比如上一个键盘响应的示例,当视点处于极右或者极左时,三角形会少一个角,这一个角就超出了可视范围:

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第7张图片

WebGL的可视空间限制包括水平、垂直和深度三个方向,三者——包括水平视角、垂直视角和可视深度——共同定义了可视空间(view volume)。


可视空间

常用的可视空间有两类:

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

正射投影中,用户可以方便地比较场景中物体的大小,因为物体看上去的大小和所在位置无关,多用于精确制图。
透视投影中,物体看上去的大小与所在位置有关,产生的三维场景更有深度感,更加自然,多用于模拟视觉。

此处主要介绍盒状可视空间:
【《WebGL编程指南》读书笔记-进入三维世界(上)】_第8张图片

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

WebGL最终绘制的图形会显示在canvas标签中,上显示的就是可视空间中物体在近裁剪面上的投影。如果裁剪面的宽高比和不一致,那么画面会按照的宽高比进行压缩,物体会被扭曲。


定义盒装可视空间

可视空间采用投影的方法进行定义,投影同样采用矩阵的方式呈现,称为投影矩阵,对于盒状可视空间,需要采用正射投影矩阵(orthographic projection)进行变换。在cuon-matrix,js中设置正射投影矩阵的方法如下:

Matrix4.setOrtho(left, right, bottom, top, near, far)
通过各参数计算正射投影矩阵,将其存储在Matrix4中。注意,left不一定与right相等,bottom不一定与top相等,near与far不相等。
参数:
left, right: 指定近裁剪面(也是可视空间的,下同)的左边界和右边界
bottom, top: 指定近裁剪面的上边界和下边界
near, far: 指定近裁剪面和远裁剪面的位置,即可视空间的近边界和远边界
返回值:


补充:正射投影矩阵的原理

如果不想理解细节的话,可以认为,正射投影矩阵规定了盒装可视空间的范围,只能观察到可视空间内的物体。

正射投影变换,就是已知盒状可视空间内任意点坐标(x,y,z),将其垂直投影到xy平面(视平面)的对应点坐标。

但是,这一操作不能通过简单地舍弃z坐标来实现,原因如下:

  • 在WebGL系统中,xy的坐标都被限制在(-1,1)范围内,超出的不显示,所以需要把盒状可视空间内的坐标投影到该范围内
  • z坐标需要保留,绘制图形时需要z坐标来判断遮挡关系。

所以,正射投影矩阵所做的事情是体对体的投影,同时,为了保证不同渲染系统的适用,选择投影到规范立方体中。

规范立方体:书中提到的规范立方体是xyz范围在-1到1之间的立方体。

这种投影可以采用平移加缩放变换来实现:

  • 方案一:首先平移,将两个体的中心重合,再缩放,不同方向缩放系数可能不同,使二者重合;
  • 方案二:首先缩放,使两个体大小相同,再平移,使二者重合。

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第9张图片

(图片仅供参考)

笔者根据cuon-matrix.js中setOrtho()函数源码,得到正射投影矩阵如下:
[ 2 r − l 0 0 − r + l r − l 0 2 t − b 0 − t + b t − b 0 0 − 2 f − n − f + n f − n 0 0 o 1 ] \begin{bmatrix}\frac{2}{r-l}&0&0&-\frac{r+l}{r-l} \\0&\frac{2}{t-b}&0&-\frac{t+b}{t-b}\\ 0&0&-\frac{2}{f-n}&-\frac{f+n}{f-n}\\0&0&o&1\end{bmatrix} rl20000tb20000fn2orlr+ltbt+bfnf+n1

r = r i g h t , l = l e f t , t = t o p , b = b o t t o m , f = f a r , n = n e a r r=right,l=left,t=top,b=bottom,f=far,n=near r=right,l=left,t=top,b=bottom,f=far,n=near

从图片和矩阵都可以看出,正射投影矩阵是一个叠加平移和缩放操作的矩阵,它根据等比例的线性变换将旧坐标变换到新坐标,旧坐标位于定义的可视空间内,新坐标位于规范立方体内。


示例程序OrthoView.htmlOrthoView.js

该示例程序主要展示可视空间的效果,主要设置如下:

  • 示例程序绘制3个与之前示例相同的三角形,但直接将视点置于原点,视线为Z轴负方向。
  • 可视空间定义为near=0.0, far=0.5, left=-1.0, right=1.0, bottom=-1.0, top=1.0,三角形处于Z轴0.0到-0.4区间上。
  • 允许键盘按键修改可视空间的near和far值:右方向键(39)-near提高0.01,左方向键(37)-near降低0.01,上方向键(38)-far提高0.01,下方向键(40)-far降低0.01。
  • 在html文件中增加了id为nearFar的p标签,标签内显示当前near和far的值。

LookAtTriangles.js为蓝本,OrthoView.js的主要改动如下:

  • 与之前的矩阵变换相同,顶点着色器中加入投影矩阵
// 顶点着色器
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'
  • 常规的获取标签、获取着色器中变量的地址之外,键盘响应事件设置如下:1. 参数增加了p标签nf;2. 根据keyCode对near和far进行改动。
  // 创建投影矩阵的Matrix4对象
  let projMatrix = new Matrix4()
  // 注册键盘事件响应函数
  document.onkeydown = function (ev) {
    keydown(ev, gl, n, u_ProjMatrix, projMatrix, nf)
  }
// 键盘响应事件
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:
      console.log(ev.keyCode)
      return // 按下了其它键
  }
  draw(gl, n, u_ProjMatrix, projMatrix, nf)
}
  • 绘制函数设置如下:1. 投影矩阵的传递;2. nf标签内容设置;3. 在main()函数末尾首次执行(与之前很多示例相同,未记录在下方)
// 绘制函数
function draw(gl, n, u_ProjMatrix, projMatrix, nf) {
  // 设置盒状可视空间投影矩阵
  projMatrix.setOrtho(-1, 1, -1, 1, g_near, g_far)
  // 将投影矩阵传递给u_ProjMatrix
  gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements)
  // 显示当前的near值和far值
  nf.innerHTML =
    'near:' +
    Math.round(g_near * 100) / 100 +
    ',far:' +
    Math.round(g_far * 100) / 100
  // 绘制三角形
  gl.clear(gl.COLOR_BUFFER_BIT)
  gl.drawArrays(gl.TRIANGLES, 0, n)
}

示例程序的思路比较简单,许多思路在前面的流程在前面的示例中有提到,此处就不再详述。

通过这个示例,我们可以看到可视空间的变换对WebGL绘图的影响:

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第10张图片

随着near的增加,三角形从“近”到“远”逐渐消失。


投影矩阵与视图矩阵的顺序—LookAtTrianglesWithKeys_ViewVolume.js

在之前的示例中,LookAtTrianglesWithKeys.js在某些情况下无法显示完整的三角形,如果加入投影矩阵,就能完美地解决此类问题。

如果了解了补充内容:补充:正射投影矩阵的原理,对于投影矩阵和视图矩阵的顺序就没有太多的疑问:首先通过视图矩阵将物体相对位移到合适的坐标,再通过正射矩阵将该坐标投影到规定立方体中。
< 正 射 投 影 矩 阵 > × < 视 图 矩 阵 > × < 顶 点 坐 标 > <正射投影矩阵>\times<视图矩阵>\times<顶点坐标> <>×<>×<>
按照此顺序,加入投影矩阵,得到新的示例LookAtTrianglesWithKeys_ViewVolume.js。主要区别如下:

  • 顶点着色器加入投影矩阵(因为视图矩阵随键盘事件变动,也为了更好展示两个矩阵的关系,此处没有将两个矩阵合并为一个)
// 顶点着色器
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'
  • 主函数中配置投影矩阵
  // 可视空间操作
  let u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix')
  if (!u_ProjMatrix) {
    console.log('Failed to get the storage loaction of u_ProjMatrix')
    return
  }
  let projMatrix = new Matrix4()
  projMatrix.setOrtho(-1.0, 1.0, -1.0, 1.0, 0.0, 2.0)
  gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements)

此时,图片能够完整显示:

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第11张图片


可视空间长宽比与不一致造成的变形

在前文有过说明:如果可视空间近裁剪面的宽高比与不一致,显示出来的物体就会被压缩变形。这一部分很好理解,下面以OrthoView.js为例,展示相关效果。

  • 不做任何修改时,可视空间设置如下:
  projMatrix.setOrtho(-1, 1, -1, 1, 0.0, 0.5)

显示效果如下:

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第12张图片

  • 如果把可视空间xy等比例减小:
  projMatrix.setOrtho(-0.5, 0.5, -0.5, 0.5, 0.0, 0.5)

显示效果如下:

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第13张图片

  • 如果可视空间xy非等比例减小:
  projMatrix.setOrtho(-0.3, 0.3, -1.0, 1.0, 0.0, 0.5)

显示效果如下:

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第14张图片

透视投影可视空间

相关内容:1. 透视投影可视空间的介绍与设计适用;2. 透视投影矩阵的设计原理;3.顶点着色器输出的顶点必须在规范立方体中,才会显示到屏幕上
相关函数:Matrix4.setPerspective()

相比于正射投影,透视投影使景物“远小近大”,更符合人眼观察世界的情况,使用透视投影构建的可视空间在前文被称作四棱锥/金字塔可视空间,下面将对相关可视空间和透视投影的方法进行介绍。


定义透视投影可视空间

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第15张图片

透视投影可视空间如上图所示,主要包括视点、视线、近裁剪面和远裁剪面,结合宽高比和垂直视角来限制空间范围。投影到近裁剪面上的图像显示在WebGL系统中。

Matrix4对象通过setPerspective()方法来生成透视投影矩阵(perspective projection matrix)来定义透视投影可视空间,函数规范如下:

Matrix4.setPerspective(fov, aspect, near, far)
通过各参数计算透视投影矩阵,将其存储在Matrix4中。注意,near的值必须小于far。
参数:
fov: 指定垂直视角,即可视空间顶面和底面间的夹角,必须大于0(degree,单位为度)
aspect: 指定近裁剪面的宽高比(宽度/高度)
near, far: 指定近裁剪面和远裁剪面的位置,即可视空间的近边界和远边界(near和far必须都大于0)
返回值:


补充:透视投影矩阵的原理

与正射投影矩阵相同,透视投影矩阵同样要把可视空间内的物体投影到xyz坐标在-1到1范围内的规范立方体中,即从下方左图到右图建立投影关系(图片源于网络)。

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第16张图片

易知,这种投影可以根据类似相似三角形的等比例关系进行不同坐标轴的投影换算,包括顶点向视线方向的缩放和z轴方向的平移。建立比例关系后,转换过程并不困难,网上有很多介绍,此处给出书的附录中得到的透视投影矩阵:
[ 1 a s p e c t ∗ tan ⁡ f o v 2 0 0 0 0 1 tan ⁡ f o v 2 0 0 0 0 − f a r + n e a r f a r − n e a r − 2 ∗ f a r ∗ n e a r f a r − n e a r 0 0 − 1 0 ] \begin{bmatrix}\frac{1}{aspect*\tan\frac{fov}{2}}&0&0&0 \\0&\frac{1}{\tan\frac{fov}{2}}&0&0\\ 0&0&-\frac{far+near}{far-near}&-\frac{2*far*near}{far-near}\\0&0&-1&0\end{bmatrix} aspecttan2fov10000tan2fov10000farnearfar+near100farnear2farnear0


笔者在此处发现,所有的投影矩阵都是将物体投影到了xyz坐标-1到1之间的规范立方体中。

理论上来说,根据WebGL默认视点,WebGL无法“看到”变换后z坐标大于0的物体,但在实际的试验中看到了。

简单来说,顶点着色器输出的顶点都必须在规范立方体中,才会显示到屏幕上。

这部分解释详见附录D“WebGL/OpenGL:左手或右手”。


透视投影示例Perspectiveview.js

示例Perspectiveview.js展示了透视投影的效果:

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第17张图片

其基本流程和同时使用视图矩阵和投影矩阵的示例LookAtTrianglesWithKeys_ViewVolume.js相近。部分代码如下:

  • 顶点着色器没有变换,依然采用投影矩阵*视图矩阵*顶点坐标的形式
// 顶点着色器
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'
  • 顶点坐标如下:(顶点坐标数量很多,后面会讨论简化输入的方法)
  // 准备数据
  let 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,
  ])
  let n = 18
  • 没有使用键盘响应事件,视图矩阵的配置如下:
  // 获取u_ViewMatrix存储地址
  let u_ViewMatrix = gl.getUniformLocation(gl.program, 'u_ViewMatrix')
  if (!u_ViewMatrix) {
    console.log('Failed to get the storage loaction of u_ViewMatrix')
    return
  }
  // 创建视图矩阵的Matrix4对象
  let viewMatrix = new Matrix4()
  // 计算视图矩阵
  viewMatrix.setLookAt(0, 0, 5, 0, 0, -100, 0, 1, 0)
  // 视图矩阵传值
  gl.uniformMatrix4fv(u_ViewMatrix, false, viewMatrix.elements)
  • 投影矩阵的配置如下:
  // 可视空间操作
  let u_ProjMatrix = gl.getUniformLocation(gl.program, 'u_ProjMatrix')
  if (!u_ProjMatrix) {
    console.log('Failed to get the storage loaction of u_ProjMatrix')
    return
  }
  let projMatrix = new Matrix4()
  projMatrix.setPerspective(30, canvas.width / canvas.clientHeight, 1, 100)
  gl.uniformMatrix4fv(u_ProjMatrix, false, projMatrix.elements)

程序流程没有新的内容,只是用透视投影矩阵取代了正射投影矩阵。


三者结合(模型矩阵、视图矩阵、投影矩阵)PerspectiveView_mvp.js

运用投影矩阵、模型矩阵和视图矩阵,我们能够处理顶点需要经过的所有几何变换(平移、旋转、缩放),最终达到“具有深度感”的视觉效果。

基于之前的示例,此处有两点创新:

  • 同时使用模型矩阵、视图矩阵、投影矩阵

易知 < 最 终 的 坐 标 > = < 投 影 矩 阵 > × < 视 图 矩 阵 > × < 模 型 矩 阵 > × < 顶 点 坐 标 > <最终的坐标>=<投影矩阵>\times<视图矩阵>\times<模型矩阵>\times<顶点坐标> <>=<>×<>×<>×<>

  • 简化顶点的输入

三角形的关系如下图所示:

【《WebGL编程指南》读书笔记-进入三维世界(上)】_第18张图片

所以,此处我们可以采用如下步骤绘制:

  1. 在虚线处沿Z轴准备3个三角形的顶点数据;
  2. 将三角形沿X轴正方向平移0.75单位,绘制三角形;
  3. 将三角形沿X轴负方向平移0.75单位,绘制三角形。

至此,我们已经掌握了示例PerspectiveView_mvp.js的部分重要思路。整个示例代码很长,此处展示重要的部分:

  • 顶点着色器引入三种矩阵
// 顶点着色器
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'
  • 顶点坐标的准备
  // 准备数据
  let 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,
  ])
  let n = 9
  • 关于矩阵操作,通过模型矩阵的重新构建和两次绘制实现效果(省略了创建矩阵对象和获取uniform变量地址)
  // 计算矩阵
  modelMatrix.setTranslate(0.75, 0, 0) // 平移0.75
  viewMatrix.setLookAt(0, 0, 5, 0, 0, -100, 0, 1, 0)
  projMatrix.setPerspective(30, canvas.width / canvas.clientHeight, 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) // 平移0.75
  gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements)
  // 绘制左侧三角形
  gl.drawArrays(gl.TRIANGLES, 0, n)

减少着色器内部计算

同模型矩阵合并了旋转和平移矩阵相同,此处我们使用模型视图投影矩阵(model view projection matrix)整合三个矩阵为一个,在JavaScript中进行矩阵乘法,也可以减少着色器内部的计算,提高效率。
< 模 型 视 图 投 影 矩 阵 > = < 投 影 矩 阵 > × < 视 图 矩 阵 > × < 模 型 矩 阵 > <模型视图投影矩阵>=<投影矩阵>\times<视图矩阵>\times<模型矩阵> <>=<>×<>×<>
这一部分程序思路和之前合并矩阵的思路相同:

  • 着色器中只定义并使用一个矩阵:u_MvpMatrix
  • 通过Matrix4对象的multiply方法进行矩阵乘法,将计算得到的三个矩阵相乘获得模型视图投影矩阵
  • 矩阵传值

因为顶点要经历两种模型矩阵,所以共传值和绘制两次。

程序思路十分清晰,在之前也有很多可以参考的代码(模型矩阵章节示例和上一个示例PerspectiveView_mvp.js),此处不再给出代码。

你可能感兴趣的:(WebGL基础,html5)