本文是阅读笔记第七章的上半部分 总目录链接:https://blog.csdn.net/floating_heart/article/details/124001572 由于章节内容较多,分为上下两部分。 上部包括以下内容:
- 对三维呈现方式的初步了解:视点、观察点、上方向。
- 了解WebGL可视空间采用投影矩阵投影物体,实现盒状可视空间和正射投影。本书补充了正射投影矩阵的数学原理。
- 透视投影可视空间、投影矩阵和模型视图投影矩阵的相关操作提到了立方体的标准化。
第7章 进入三维世界(上)
旧规本章的前言部分记录如下:
前几章的示例程序呈现二维图形。通过这些示例程序,我们了解了它们 WebGL系统的工作原理、着色器的作用、矩阵变换(平移和旋转)、动画和纹理映射等等。事实上,这些知识不仅适用于绘制二维图形,也适用于绘制三维图形。在这一章中,我们将进入三维的世界,探索如何把这些知识用到三维世界中。具体地,我们将研究:
- 从用户的角度进入三维世界
- 控制三维视觉空间
- 裁剪
- 处理物体的前后关系
- 画三维立方体
以上内容对于如何绘制三维场景以及如何向用户展示场景非常重要。只有理解了这些内容,我们才能创建一个复杂的三维场景。我们将一步一步地学习。本章将首先帮助您快速掌握绘制三维物体的基本技能。下一章将涉及一些更复杂的问题,如实现照明效果等先进技术。
立方体由三角形组成
三维图形也由二维图形(尤其是三角形)组成,如下图所示,一个立方体由12个三角形组成:
因此,我们只需要像前几章一样,逐一绘制构成物体的每个三角形,就可以绘制整个三维物体。
此外,与二维相比,三维增加了(depth information)。
先从视角到视觉空间,一步步了解三维模式。
视点和视线
相关内容:1. 引入视图矩阵(
LookAtTriangles.js
),视图矩阵的原理;Matrix.setLookAt()细节(处理非垂直视线和上方向输入);2. 同时使用视图矩阵和模型矩阵(LookAtRotatedTriangels.js
);3. 添加键盘控制(LookAtTrianglesWithKeys.js
)。 相关函数:Matrix.setLookAt(), Matrix.multiply()小结:
在呈现时,我们最终必须在二维屏幕上绘制三维场景,即绘制观察者看到的世界,观察者可以在任何位置观察。为了定义观察者,我们需要考虑以下两点:
- 观察方向,即观察者自己在哪里,的哪一部分?
- 可视距离,即观察者能看多远?
本节主要讨论观察方向,导致视点和视线两个概念。
:观察者的位置;
:沿观察方向的射线从视角出发。
本节,我们创建了一个新的示例程序LookAtTriangles.js
,视点位于(0.20,0.25,0.25),当视线向原点(0、0、0)方向时,可以看到原点附近有三个三角形,前后错落有致,有助于理解三维场景中的深度概念。
在编写代码之前,我们需要了解一些三维图形的基本知识。
为了确定观察者的状态,我们需要获取三个信息:
- (eye point):三维空间的位置,视线的起点。在这里使用(eyeX, eyeY, eyeZ)表示,OpenGL常称相机。
- (look-at point):观察目标所在的点。视线从视角出发,通过观察目标点并继续延伸。观察目标点是一个点,而不是视线方向。只有同时了解观察目标点和视点,才能计算视线方向。观察目标点(atX, atY, atZ)表示
- (up direction):最后,屏幕上图像的向上方向。为了固定观察者,我们还需要指定三个重量的上方向 矢量,用(upX,upY,upZ)表示。
在WebGL我们可以用以上三个矢量创建一个,然后将矩阵传输到顶点着色器。
视图矩阵可以表示观察者的状态,包含观察者的视图、目标点、上方向等信息,最终影响屏幕上显示的视图,即观察者观察到的场景。
本例中使用cuon-matrix.js
库中提供的Matrix4.setLookAt()函数,根据三个信息创建视图矩阵:
根据视点(eyeX, eyeY, eyeZ)、观察点(atX, atY, atZ)、上方向(upX, upY, upZ)创建视图矩阵。视图矩阵的类型是Matrix四、其观察点映射到<canvas>
的中心点。 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默认状态,对物体进行相同的变换,以获得所需的图形。这就是视图矩阵的原理。
相关变换无疑是仿射变换。所需的视图矩阵可以通过之前模型矩阵一节中解释的内容获得。这里不再重复细节。
// 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)
在上一个示例中,我们看到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) // 视线和上方向可以不垂直么
上一个示例展示了视图矩阵的添加方式,如果我们在变换视角的同时也需要对图形进行旋转平移等变换,如何处理视图矩阵和模型矩阵的顺序,就是此处讨论的问题。答案也比较简单: < “ 从 视 点 看 上 去 ” 的 旋 转 后 顶 点 坐 标 > = < 视 图 矩 阵 > × < 模 型 矩 阵 > × < 原 始 顶 点 坐 标 > <“从视点看上去”的旋转后顶点坐标>=<视图矩阵>\times<模型矩阵>\times<原始顶点坐标> <“从视点看上去”的旋转后顶点坐标>=<视图矩阵>×<模型矩阵>×<原始顶点坐标> 相关解释如下:
视图矩阵也可以认为是将顶点坐标变换到合适的位置,使得观察者(以默认状态)观察新位置的顶点,就好像观察者处在(视图矩阵描述的)视点上观察原始顶点一样,具体在上面的补充内容————中有所呈现。 从便于理解的角度来说,我们需要先对三角形进行旋转,再从固定的视角观察它。或者说,我们需要先对三角形进行基本变换,再对变换后的三角形进行与“移动视点”等效的变换。(否则,就需要考虑旋转轴在“移动视点”等效变换后的空间位置了,变得复杂。)
了解了上述原理后,实现就变得简单。示例LookAtRotatedTriangles.js
展示了从某一视点观察旋转后物体的方法,示例效果如下:
该示例相比于上一个示例,变动的地方如下:
- 顶点着色器中设置模型矩阵
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
标签: 电感磁珠upz1005d121