WebGL 坐标系

在 Three.js、Oak3D、PhiloGL 等一批图形库的"引诱"下,很多人放弃了基础知识,直接开始操控这些成熟的 WebGL 3D 引擎。那些一直坚持学习 WebGL 原生 API 的人,在经历了一开始的艰苦岁月,战胜了面对他人突飞猛进进而自己仍在画三角形的挫败感之后,现在已经成为了 HiWebGL 社区中的中流砥柱。

---《WebGL 编程指南》

为什么需要坐标系

坐标系的存在是为了定量描述物体之间的相对位置,物体上每个点在不同坐标系的表达可能不一样,但是多个点构成的方向向量是固定的。

  • 相对坐标系:以某物体为坐标原点,描述其他物体相对于该物体的空间位置
  • 绝对坐标系:以"地心"为唯一参考描述物体在地球及近地空间的位置的坐标参考系统。坐标原点为地心,也就是每个物体相对于"地心"在某一时刻都有且只有唯一的位置。

坐标系划分

世界坐标系

三维直角坐标系(也是个假象坐标系),在环境中选择一个基准坐标系来描述摄像机的位置,并描述环境中任何物体的位置。被指定后不变且唯一(即:绝对坐标系)

相机坐标系

三维直角坐标系,以相机光心(小孔)为原点,x、y 轴分别与相面的两边平行,z轴为镜头光轴,与像平面垂直,且其随着相机移动而变化(即:相对坐标系)

裁剪坐标系

相机坐标系经过投影矩阵转换后得到的空间坐标系,投影矩阵约定了视角的上下左右前后边界(对应的是相机的 Frustum 范围),后面会将处于边界之外的数据直接 Clip 到边界上。(相对坐标系)

Vertex Shader 输出的顶点坐标就处于这个坐标系中,后面硬件会自动根据裁剪坐标剔除掉越界部分

NDC(归一化设备坐标系)

与设备平台无关的一套三维坐标系,NDC 是有边界的,其 XYZ 方向上的边界为[-1, 1](例如:OpenGL)(相对坐标系)

PS:同一个物件,无论设备使用什么样的分辨率,在这个坐标系中的数值都是相同的

屏幕坐标系

二维平面坐标系,这个坐标系的坐标也是 Fragment Shader、Pixel Shader的输入。(相对坐标系)

将 NDC 转换到屏幕空间,得到与屏幕分辨率相一致的2D整数坐标系(即:坐标范围与 Viewport 分辨率一致)

注:裁剪坐标系、NDC、屏幕坐标系是以相机为主体定义的相对坐标系,在相机的空间位置与旋转角度确定后,这三个坐标系随即不变且唯一

数学基础

坐标系变换需要一些数据基础,这里小小给大家补充一下~

向量

通用的概念是: 一个有大小和方向的量。在几何或者代数中,就是指一个坐标或者数值,有长度或者位置信息,体现某个量的特征。

物理学的角度: 有方向和大小的量,比如"光线"、"力"、"速度"、"磁场"这些同时具有方向和大小两个属性。

编程的角度: 向量只是一列有序的数字,这列数字可以用纵列(column)表示,在代码往往用数组来表示。

在 WebGL 中通常用向量来表示有方向的坐标,向量可以用一行来表示,也可以用一列来表示,WebGL 中使用的是列向量, 也就是说WebGL中的vec* 对象是 列向量

矩阵

矩阵是一个数字阵列,一个二维数组,mn列的阵列称为m*n矩阵。行列式相等的矩阵也叫方阵。对角线为1,其他位置都是 0 的矩阵,类似乘法中的1。

在代码中,矩阵往往用一个数组来表示,要根据行主序还是列主序来确认数组的实际排列,WebGL 中的矩阵是列主序的。

例:

<math xmlns="http://www.w3.org/1998/Math/MathML"> [ 1 2 3 4 5 6 ] \begin{bmatrix}1&2&3\\4&5&6\end{bmatrix} </math>[142536]

JS 数组表示即: [1, 4, 2, 5, 3, 6]

两条平行线可以相交于一点么?

欧式空间(笛卡尔空间):同一个平面的两条平行线不能相交

透视空间:两条平行线可以相交(火车轨道在视线的无穷远处交汇)

PS:欧式几何是透视几何的一个子集

"齐次坐标表示是计算机图形学的重要手段之一,它既能够用来明确区分向量和点,同时也更易用于进行仿射(线性)几何变换。"------ F.S. Hill, JR

齐次坐标

  1. 本质:用 N+1 维来代表 N 维坐标(向量)

    对于三维坐标点 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y , z ) (x,y,z) </math>(x,y,z),增加一维 <math xmlns="http://www.w3.org/1998/Math/MathML"> w ! = 0 w!=0 </math>w!=0,并对原三维坐标进行缩放形成四维坐标 > > <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y , z ) (x,y,z) </math>(x,y,z) ------> <math xmlns="http://www.w3.org/1998/Math/MathML"> ( w x , w y , w z , w ) (wx,wy,wz,w) </math>(wx,wy,wz,w) > > 特别地:如果 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y , z ) (x,y,z) </math>(x,y,z)移动到无穷远处则可用 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( w x , w y , w z , 0 ) (wx,wy,wz,0) </math>(wx,wy,wz,0)表示,即 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( w x / 0 , w y / 0 , w z / 0 ) (wx/0,wy/0,wz/0) </math>(wx/0,wy/0,wz/0) = <math xmlns="http://www.w3.org/1998/Math/MathML"> ( ∞ , ∞ , ∞ ) (\infty,\infty,\infty) </math>(∞,∞,∞) ,此刻 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y , z ) (x,y,z) </math>(x,y,z)可表示为向量

  2. 形式:

    • 普通坐标 ------> 齐次坐标

      • 坐标点: <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y , z ) (x,y,z) </math>(x,y,z) 变为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y , z , 1 ) (x,y,z,1) </math>(x,y,z,1)
      • 向量: <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y , z ) (x,y,z) </math>(x,y,z) 变为 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y , z , 0 ) (x,y,z,0) </math>(x,y,z,0)
    • 齐次坐标 ------> 普通坐标

      • 如果是齐次坐标 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y , z , 1 ) (x,y,z,1) </math>(x,y,z,1),则判断为其是坐标点 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y , z ) (x,y,z) </math>(x,y,z)

      • 如果是 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y , z , 0 ) (x,y,z,0) </math>(x,y,z,0) ,则判断其是个向量 <math xmlns="http://www.w3.org/1998/Math/MathML"> ( x , y , z ) (x,y,z) </math>(x,y,z)

    最常见的仿射变换:平移T、旋转R、缩放S: > > 平移变换只对于点才有意义,因为普通向量没有位置概念,只有大小和方向 > > 旋转和缩放对于向量和点都有意义

  3. 特点:规模不变形,一般用在坐标之间的比例比实际的数值重要的情形下(透视效果)。

为什么齐次坐标用于仿射变换?

对一个三维坐标 <math xmlns="http://www.w3.org/1998/Math/MathML"> p = ( x , y , z ) p=(x,y,z) </math>p=(x,y,z),对其进行仿射变换,表示如下:

<math xmlns="http://www.w3.org/1998/Math/MathML"> [ x 2 y 2 z 2 ] = [ m 0 0 m 0 1 m 0 2 m 1 0 m 1 1 m 1 2 m 2 0 m 2 1 m 2 2 ] ∗ [ x 1 y 1 z 1 ] + [ C o n s t x C o n s t y C o n s t z ] \begin{bmatrix}x_2\\y_2\\z_2\end{bmatrix}=\begin{bmatrix}m_0 _0&m_0 _1&m_0 _2\\m_1 _0&m_1 _1&m_1 _2\\m_2 _0&m_2 _1&m_2 _2\end{bmatrix}*\begin{bmatrix}x_1\\y_1\\z_1\end{bmatrix}+\begin{bmatrix}Const_x\\Const_y\\Const_z\end{bmatrix} </math>⎣ ⎡x2y2z2⎦ ⎤=⎣ ⎡m0 0m1 0m2 0m0 1m1 1m2 1m0 2m1 2m2 2⎦ ⎤∗⎣ ⎡x1y1z1⎦ ⎤+⎣ ⎡ConstxConstyConstz⎦ ⎤

简化为:

<math xmlns="http://www.w3.org/1998/Math/MathML"> P 2 = M ∗ P 1 + C P_2=M*P_1+C </math>P2=M∗P1+C

问题:每次放射变换需要 乘法加法 计算两次才能完成,当变换的次数很多时,这样的计算就会很复杂且不直观。

使用齐次坐标:

<math xmlns="http://www.w3.org/1998/Math/MathML"> [ x 2 y 2 z 2 1 ] = [ m 0 0 m 0 1 m 0 2 C o n s t x m 1 0 m 1 1 m 1 2 C o n s t y m 2 0 m 2 1 m 2 2 C o n s t z 0 0 0 1 ] ∗ [ x 1 y 1 z 1 1 ] \begin{bmatrix}x_2\\y_2\\z_2\\1\end{bmatrix}=\begin{bmatrix}m_0 _0&m_0 _1&m_0 _2&Const_x\\m_1 _0&m_1 _1&m_1 _2&Const_y\\m_2 _0&m_2 _1&m_2 _2&Const_z\\0&0&0&1\end{bmatrix}*\begin{bmatrix}x_1\\y_1\\z_1\\1\end{bmatrix} </math>⎣ ⎡x2y2z21⎦ ⎤=⎣ ⎡m0 0m1 0m2 00m0 1m1 1m2 10m0 2m1 2m2 20ConstxConstyConstz1⎦ ⎤∗⎣ ⎡x1y1z11⎦ ⎤

简化为:

<math xmlns="http://www.w3.org/1998/Math/MathML"> P 2 ' = M ' ∗ P 1 ' P_2^'=M^'*P_1^' </math>P2'=M'∗P1'

结论:仿射变换的进行更加方便

由于图形硬件已经普遍地支持齐次坐标与矩阵乘法,因此更加促进了齐次坐标使用,使得它似乎成为图形学中的一个标准。

坐标系变换

渲染管线

模型数据-顶点着色 - 曲面细分 - 几何着色器 - 裁剪 - NDC空间 - 屏幕空间 - 光栅阶段 - 帧缓存

底层封装:裁剪空间---NDC空间---屏幕空间

顶点着色器:把顶点转换到齐次裁剪空间(就是一般所说的输出的 positionCS )空间

CPU:模型变换、视图变换、投影变换(又称:MVP 变换)

GPU:齐次除法、视口变换

模型变换(物体坐标系 -> 世界坐标系)

  1. 实质:物体坐标系 & 世界坐标系存在映射关系(主要是平移、缩放、旋转)

  2. 坐标系设定

    1. 都是右手坐标系,单位 1(两个坐标系比例尺不大一样)
    2. 模型变换主要涉及:glTranslae (平移)、glScale (缩放)、glRotate (旋转)
    3. 物体坐标系下坐标值设定:glVertex3f设置
    4. 世界坐标系:不受glScale (缩放)影响
  3. 模型变换矩阵推导

从物体坐标系到世界坐标系:

<math xmlns="http://www.w3.org/1998/Math/MathML"> P 1 = M ∗ P 0 P_1=M*P_0 </math>P1=M∗P0

模型变换矩阵 <math xmlns="http://www.w3.org/1998/Math/MathML"> M M </math>M可参考数据基础得出结果

我的理解:世界坐标系就是给定的(类似于地球经纬度,规定起止点和单位),物体和相机基于世界坐标系放置

视图变换(世界坐标系 -> 相机坐标系)

  1. 实质

调用 gluLookAt(eye, center, up),相机放置在位置 eye、视线看向 center、指定上方向 up(eye、center、up 均为世界坐标系下的三维向量),右手坐标系

在世界坐标系中,Camera 并一定在坐标原点,观察方向不一定指向 Z 轴正方向(这俩问题会复杂化后续的投影变换等操作),所以视图变换的作用:

  • 移动 Camera,使其位于世界坐标系原点

  • 旋转 Camera,使其朝向 z 轴正方向(视线由原点指向 z 轴正方向)

  1. 坐标系设定

    • 原点:相机位置(eye)
    • z 轴:从 center 点 指向 eye 方向
    • x 轴: <math xmlns="http://www.w3.org/1998/Math/MathML"> X = u p × Z X=up×Z </math>X=up×Z
    • Y 轴: <math xmlns="http://www.w3.org/1998/Math/MathML"> Y = Z × X Y=Z×X </math>Y=Z×X
    • 单位:1
  2. 视图变换矩阵推导

根据上个小结公示,则从世界坐标系到相机坐标系,相机坐标系下的点 <math xmlns="http://www.w3.org/1998/Math/MathML"> P 2 = V ∗ P 1 P_2=V*P_1 </math>P2=V∗P1,那么:

<math xmlns="http://www.w3.org/1998/Math/MathML"> P 1 = T ∗ R ∗ P 2 P_1=T*R*P_2 </math>P1=T∗R∗P2

<math xmlns="http://www.w3.org/1998/Math/MathML"> P 1 P_1 </math>P1:世界坐标系的坐标

<math xmlns="http://www.w3.org/1998/Math/MathML"> P 2 P_2 </math>P2:相机坐标系的坐标

根据矩阵性质:

  1. 矩阵左乘逆矩阵等于单位矩阵
  2. 单位矩阵左乘矩阵等于原矩阵

推导过程:

<math xmlns="http://www.w3.org/1998/Math/MathML"> I P 1 = T ∗ R ∗ P 2 IP_1=T*R*P_2 </math>IP1=T∗R∗P2

<math xmlns="http://www.w3.org/1998/Math/MathML"> ( T R ) ( T R ) − 1 P 1 = ( T R ) ∗ P 2 (TR){(TR)^-}^1P_1=(TR)*P_2 </math>(TR)(TR)−1P1=(TR)∗P2

<math xmlns="http://www.w3.org/1998/Math/MathML"> ( T R ) − 1 P 1 = P 2 {(TR)^-}^1P_1=P_2 </math>(TR)−1P1=P2

<math xmlns="http://www.w3.org/1998/Math/MathML"> P 2 = ( R ) − 1 ( T ) − 1 P 1 P_2={(R)^-}^1{(T)^-}^1P_1 </math>P2=(R)−1(T)−1P1

因此求得相机的平移矩阵和旋转矩阵的逆矩阵,即可得到视图变换矩阵(此矩阵可讲世界坐标中的点 P1 转换为相机坐标系中的 P2)

视图变换矩阵 <math xmlns="http://www.w3.org/1998/Math/MathML"> V V </math>V的确定:相机平移矩阵和旋转矩阵的逆矩阵

在 OpenGL 中,并没有专门的视点矩阵和模型矩阵,两者是合一的,称为 GL_MODELVIEW 。

投影变换(相机坐标系 -> 裁剪坐标系)

  • Frustum(平截头体):由投影矩阵创建的观察箱,观察箱范围内的坐标最终展示到屏幕
  • Projection(投影):将特定范围内的坐标转化到标准化设备坐标系的过程

将顶点坐标从相机坐标系到裁剪坐标系,需要一个投影矩阵,保留观察箱内的数据。

如果只是图元(Primitive),例如三角形,的一部分超出了裁剪体积(Clipping Volume),则 OpenGL 会重新构建这个三角形为一个或多个三角形让其能够适合这个裁剪范围。

  1. 实质

    1. 投影就是将相机在相机坐标系中的所见,映射到一张二维的投影平面(照片)上的过程。
    2. 投影坐标系仍然是三维,为了方便深度测试等操作
  2. 投影变换矩阵(GL_PROJECTION)

    <math xmlns="http://www.w3.org/1998/Math/MathML"> P 3 = M p ∗ P 2 P_3=M_p*P_2 </math>P3=Mp∗P2
    <math xmlns="http://www.w3.org/1998/Math/MathML"> P 2 P_2 </math>P2:相机坐标系的坐标
    <math xmlns="http://www.w3.org/1998/Math/MathML"> P 3 P_3 </math>P3:投影坐标系的坐标

正射投影

  • 观察箱:一个类似立方体,不改变每个向量的 w 分量
  • 正射投影矩阵:指定观察箱的宽、高和长度(只会保留观察箱内的坐标)
  • 用途:主要用于二维渲染以及一些建筑或工程的程序

创建一个正射投影矩阵

ini 复制代码
// 左侧位置、右侧位置、底部位置、顶部位置,近平面、远平面
  const zNear = 3;
  const zFar = 100.0;
  const projectionMatrix = mat4.create();
  mat4.ortho(projectionMatrix, -1, 1, -1, 1, zNear, zFar);

正射投影矩阵直接将坐标映射到 2D 平面中

透视投影

  • 观察箱:是一个棱柱,近大远小
  • 修改每个顶点坐标的 w 值,从而使得离观察者越远的顶点坐标 w 分量越大
  • 裁剪坐标的坐标都会在 -w 到 w 的范围之间(任何大于这个范围的坐标都会被裁剪掉)

坐标都落在-1.0到1.0范围内,作为顶点着色器最后的输出,因此,一旦坐标在裁剪空间内之后,透视除法就会被应用到裁剪空间坐标上:

创建一个正投影矩阵

ini 复制代码
// fov:视野、宽高比(视口的宽/高)、near、far
 const fov = 45 * Math.PI / 180;   // in radians
  const aspect = gl.canvas.clientWidth / gl.canvas.clientHeight;
  const zNear = 3;
  const zFar = 100.0;
  const projectionMatrix = mat4.create();
  mat4.perspective(projectionMatrix,
       fov,
       aspect,
       zNear,
       zFar);

齐次除法(投影坐标系 -> NDC 坐标系)

将裁剪坐标系的坐标变换为标准化坐标范围(-1.0, 1.0)(DirectX、Vulkan、Metal API 略有差异),将 4D 裁剪空间坐标变换为 3D 标准化设备坐标的过程。

将位置向量的x,y,z分量分别除以向量的齐次w分量(也称透视除法)

如果是正射投影,由于 w 分量都是 1,其坐标没有变化

一篇文章彻底弄懂齐次裁剪

注:

webgl 的 ndc 是一个位于中心点(0, 0, 0)xyz 为 2x2x2 的左手坐标系空间;webgpu 的 ndc 是中心点位于(0, 0, 0.5)的 xyz 为 2x2x1 的左手坐标系空间。这两者的区别也就是 OpenGL 和Metal的区别。

视口变换(glViewport)

拿到 NDC 坐标后,后续就要将顶点绘制到屏幕视窗上。但是由于 NDC 坐标系和屏幕坐标系并不一致,需要我们再进行一次坐标转化,也就是视口转化。

视口转化算法如下:

  • 屏幕 canvas 坐标系 x 轴坐标:NDC 坐标系 x 轴坐标 * canvas 视口宽度 / 2
  • 屏幕 canvas 坐标系 y 轴坐标:NDC 坐标系 y 轴坐标 * canvas 视口高度 / 2

<math xmlns="http://www.w3.org/1998/Math/MathML"> M = [ w i d t h / 2 0 0 w i d t h / 2 0 h e i g h t / 2 0 h e i g h t / 2 0 0 1 0 0 0 0 1 ] M=\begin{bmatrix}width/2&0&0&width/2\\0&height/2&0&height/2\\0&0&1&0\\0&0&0&1\end{bmatrix} </math>M=⎣ ⎡width/20000height/2000010width/2height/201⎦ ⎤

width:屏幕 canvas 视口宽度

height:屏幕 canvas 视口高度

总结

视图变换矩阵: <math xmlns="http://www.w3.org/1998/Math/MathML"> M = M viewport ∗ M per ∗ M cam ∗ M model \mathrm {M}=\mathrm {M}{\text {viewport}}*\mathrm {M}{\text {per}*\mathrm {M} \text {cam }}*\mathrm {M}{\text {model}} </math>M=Mviewport∗Mper∗Mcam ∗Mmodel

相关推荐
喵叔哟8 分钟前
重构代码之取消临时字段
java·前端·重构
还是大剑师兰特1 小时前
D3的竞品有哪些,D3的优势,D3和echarts的对比
前端·javascript·echarts
王解1 小时前
【深度解析】CSS工程化全攻略(1)
前端·css
一只小白菜~1 小时前
web浏览器环境下使用window.open()打开PDF文件不是预览,而是下载文件?
前端·javascript·pdf·windowopen预览pdf
方才coding1 小时前
1小时构建Vue3知识体系之vue的生命周期函数
前端·javascript·vue.js
阿征学IT1 小时前
vue过滤器初步使用
前端·javascript·vue.js
王哲晓1 小时前
第四十五章 Vue之Vuex模块化创建(module)
前端·javascript·vue.js
丶21361 小时前
【WEB】深入理解 CORS(跨域资源共享):原理、配置与常见问题
前端·架构·web
发现你走远了1 小时前
『VUE』25. 组件事件与v-model(详细图文注释)
前端·javascript·vue.js
Mr.咕咕1 小时前
Django 搭建数据管理web——商品管理
前端·python·django