锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

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

时间:2022-09-27 10:30:01 电感磁珠upz1005d121

本文是阅读笔记第七章的上半部分
总目录链接: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()

小结:

在呈现时,我们最终必须在二维屏幕上绘制三维场景,即绘制观察者看到的世界,观察者可以在任何位置观察。为了定义观察者,我们需要考虑以下两点:

  • 观察方向,即观察者自己在哪里,的哪一部分?
  • 可视距离,即观察者能看多远?

本节主要讨论观察方向,导致视点和视线两个概念。

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

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

本节,我们创建了一个新的示例程序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我们可以用以上三个矢量创建一个视图矩阵(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)创建视图矩阵。视图矩阵的类型是Matrix四、其观察点映射到的中心点。
参数:
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默认状态,对物体进行相同的变换,以获得所需的图形。这就是视图矩阵的原理。

相关变换无疑是仿射变换。所需的视图矩阵可以通过之前模型矩阵一节中解释的内容获得。这里不再重复细节。


视图矩阵的应用-示例程序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展示了从某一视点观察旋转后物体的方法,示例效果如下:

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

  • 顶点着色器中设置模型矩阵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元器件数据手册、IC替代型号,打造电子元器件IC百科大全!
          

相关文章