webGL坐标系变换

前言

由于webGL存在着几个不同的坐标系:模型坐标系、世界坐标系、相机坐标系和裁剪坐标系;因此将物体的同一坐标表示在不同的坐标系时,就需要对坐标进行转换,而根据不同坐标系之间的特性转换的具体参数可能有不同之处。

img

坐标系变换矩阵

将坐标从坐标系变换到坐标系中,有以下通用变换矩阵:

(UxxUyxUzxOxUxyUyyUzyOyUxzUyzUzzOz0001)1Ux=(Uxx, Uxy, Uxz), x轴基向量Uy=(Uyx, Uyy, Uyz), y轴基向量Uz=(Uzx, Uzy, Uzz), z轴基向量O=(Ox, Oy, Oz), 原点\begin{pmatrix} U_{xx} & U_{yx} & U_{zx} & O_{x}\\ U_{xy} & U_{yy} & U_{zy} & O_{y}\\ U_{xz} & U_{yz} & U_{zz} & O_{z}\\ 0 & 0 & 0 & 1 \end{pmatrix}^{-1} \\[2em] \begin{aligned} \overrightarrow{U_{x}} &= (U_{xx},\ U_{xy},\ U_{xz}),\ \text{x轴基向量} \\ \overrightarrow{U_{y}} &= (U_{yx},\ U_{yy},\ U_{yz}),\ \text{y轴基向量} \\ \overrightarrow{U_{z}} &= (U_{zx},\ U_{zy},\ U_{zz}),\ \text{z轴基向量} \\ \overrightarrow{O} &= (O_{x},\ O_{y},\ O_{z}),\ \text{原点} \end{aligned}

其中,Ux,Uy,Uz,O\overrightarrow{U_{x}},\overrightarrow{U_{y}},\overrightarrow{U_{z}},\overrightarrow{O}分别是新坐标系的基向量及原点坐标系中的表示。只需要将变换矩阵左乘一个列向量,即可得到该向量在新坐标系中的表示。

通过通用的坐标系变换矩阵可以推导出其它变换(平移旋转缩放等)的矩阵;

:左乘是因为得到的变换矩阵是列主序的,为啥矩阵要列主序?因为glsl中的矩阵就是列主序的

思考:为啥矩阵要取逆矩阵?

模型变换矩阵

模型变换指的是将坐标从模型坐标系转换到世界坐标系;一个模型内的所有顶点坐标都是相对于该模型的中心点(也就是模型坐标系的原点)而确定的,在实际应用中,一般都只能确定模型中心点在世界坐标系中的坐标,以及平移、旋转、缩放等操作,除此之外,一般模型的中心点的初始位置就是世界坐标系的原点位置(0, 0, 0),所以模型变换矩阵可以简化为:

M=TRST:平移矩阵R:旋转矩阵S:缩放矩阵M = T * R * S \\[1em] \begin{aligned} T&: \text{平移矩阵} \\ R&: \text{旋转矩阵} \\ S&: \text{缩放矩阵} \end{aligned}

思考:为什么能够简化?

注意顺序一定不能弄错,因为矩阵不满足交换律……那么为啥矩阵顺序必须是这样的?原因如下:

  1. 平移后会影响旋转,因此这里的旋转指的是是绕模型中心进行的旋转操作;所以平移要后于旋转进行。
  2. 同样,旋转后再进行缩放时,可能会造成缩放的效果不对(主要是旋转后x, y, z坐标发生改变,而缩放x, y, z系数不一致时,就可能对应不上);因此缩放要先于旋转进行。

视图变换矩阵

视图变换指的是将坐标从世界坐标系转换到相机坐标系(也叫观察坐标系);根据通用坐标系变换矩阵公式可知,只需要得到相机坐标系的原点及三个基向量在世界坐标系中的表示,即可得到视图变换矩阵。

需要注意的是相机坐标系是右手系!!!

相机空间

  • VRP(View Reference Point):观察参考点,也叫视点(eyepoint);相机坐标系的原点。
  • VPN(View Plane Normal):观察平面法向量,也叫观察/相机方向;相机坐标系的z轴。
  • VUV(View Up Vector):相机顶部正朝向;相机坐标系的y轴大概方向。

img

推导过程

  1. 首先,相机坐标系原点(即人眼或相机位置)在世界坐标系中的位置是已知的;相机的关注点(lookat)的位置也是已知的;VUV也是已知的;
  2. 根据关注点和相机位置可以得到VPN(即相机坐标系的z轴基向量,N\overrightarrow{N});
  3. 根据VUVVPN叉乘可以得到相机坐标系的x轴基向量,U\overrightarrow{U}
  4. 同理,根据N\overrightarrow{N}U\overrightarrow{U}的叉乘可以得到真正y轴基向量,V\overrightarrow{V}
  5. U\overrightarrow{U}V\overrightarrow{V}N\overrightarrow{N}以及VRP的坐标代入到通用变换公式即可得到视图变换矩阵VV

V=(uxvxnxOxuyvynyOyuzvznzOz0001)1=(TR)1=R1T1=(uxuyuzUOvxvyvzVOnxnynzNO0001)R=(uxvxnx0uyvyny0uzvznz00001)T=(100Ox010Oy001Oz0001)R1=RT=(uxuyuz0vxvyvz0nxnynz00001)T1=(100Ox010Oy001Oz0001)\begin{aligned} V &= \begin{pmatrix} u_{x} & v_{x} & n_{x} & O_{x}\\ u_{y} & v_{y} & n_{y} & O_{y}\\ u_{z} & v_{z} & n_{z} & O_{z}\\ 0 & 0 & 0 & 1 \end{pmatrix}^{-1} \\[2em] &= (T * R)^{-1} \\ &= R^{-1} * T^{-1} \\ &= \begin{pmatrix} u_{x} & u_{y} & u_{z} & -\overrightarrow{U}\cdot O\\ v_{x} & v_{y} & v_{z} & -\overrightarrow{V}\cdot O\\ n_{x} & n_{y} & n_{z} & -\overrightarrow{N}\cdot O\\ 0 & 0 & 0 & 1 \end{pmatrix} \end{aligned} \\[2em] \begin{aligned} R &= \begin{pmatrix} u_{x} & v_{x} & n_{x} & 0\\ u_{y} & v_{y} & n_{y} & 0\\ u_{z} & v_{z} & n_{z} & 0\\ 0 & 0 & 0 & 1 \end{pmatrix} \\[2em] T &= \begin{pmatrix} 1 & 0 & 0 & O_{x}\\ 0 & 1 & 0 & O_{y}\\ 0 & 0 & 1 & O_{z}\\ 0 & 0 & 0 & 1 \end{pmatrix} \\[2em] R^{-1} = R^T &= \begin{pmatrix} u_{x} & u_{y} & u_{z} & 0\\ v_{x} & v_{y} & v_{z} & 0\\ n_{x} & n_{y} & n_{z} & 0\\ 0 & 0 & 0 & 1 \end{pmatrix} \\[2em] T^{-1} &= \begin{pmatrix} 1 & 0 & 0 & -O_{x}\\ 0 & 1 & 0 & -O_{y}\\ 0 & 0 & 1 & -O_{z}\\ 0 & 0 & 0 & 1 \end{pmatrix} \end{aligned}

:当一个矩阵为正交矩阵时(即行向量和列向量两两正交),其转置矩阵等于其逆矩阵[1]

投影变换矩阵

投影变换指的是将坐标从相机坐标系转换到裁剪坐标系,最终投影变换将得到顶点投影后的标准化设备坐标Normalized Device Coordinate, NDC),可以输入到顶点着色器中进行使用。

所谓的标准化设备坐标指的是x, y , z坐标值的范围都在[-1, 1]之间。

投影变换跟前面的坐标系之间的变换方法有些不同,裁剪坐标系并不是指定一个坐标原点和三个基向量而形成的,而是通过一些观察参数得到的平截头体Frustum,一个空间范围)形成的,这个空间范围除了跟观察参数有关以外还和投影方式有关!

相关概念

实际上在设定观察参数时,用到的参考坐标系仍然是相机坐标系,也就是相当于平截头体是相机空间的子空间,只不过变换后坐标进行了转换。

  1. 投影点:相机坐标系的原点。
  2. 近平面:通过设置近平面距离DnearD_{near}可得,所以近平面指的是相机坐标系z=Dnearz = -D_{near}平面。
  3. 远平面:同理,设置一个远平面距离DfarD_{far}可得,近平面指的是相机坐标系z=Dfarz = -D_{far}平面。
  4. 平截头体的宽高:指的是平截头体在近平面一端上的矩形宽高(远平面一端矩形的宽高可以根据参数推算)。

正交投影

正交投影是平行投影的一种,其投影线是垂直于投影平面(这里的投影平面值的就是近平面)的。因此可以得到正交投影的平截头体为一个长方体:

img

:正交投影平截头体的宽高一般是通过设置xleftx_{left}xrightx_{right}ytopy_{top}ybottomy_{bottom}得到的。

由于投影平面垂直于相机坐标系的z轴,因此相机坐标系内的一点在投影平面的xyz坐标只需要对原坐标做一个范围缩放处理即可:

x=xxleftxrightxleft21=2(xxleft)xright+xleftxrightxleft=2x(xleft+xright)xrightxleft=2xxrightxleftxleft+xrightxrightxleft\begin{aligned} x' &= \frac{x - x_{left}}{x_{right} - x_{left}} * 2 - 1 \\ &= \frac{2 * (x - x_{left}) - x_{right} + x_{left}}{x_{right} - x_{left}} \\ &= \frac{2x - (x_{left} + x_{right})}{x_{right} - x_{left}} \\ &= \frac{2x}{x_{right} - x_{left}} - \frac{x_{left} + x_{right}}{x_{right} - x_{left}} \end{aligned}

从上面x坐标的变换可以看到,这种变换实际上就是[xleft,xright][x_{left}, x_{right}][1,1][-1, 1]的变换。同理,y坐标就是[ybottom,ytop][1,1][y_{bottom}, y_{top}]\rightarrow[-1, 1];而z坐标则是[zfar,znear][1,1][z_{far}, z_{near}] \rightarrow [-1, 1]

y=2yytopybottomybottom+ytopytopybottomz=2zznearzfarzfar+znearznearzfar\begin{aligned} y' &= \frac{2y}{y_{top} - y_{bottom}} - \frac{y_{bottom} + y_{top}}{y_{top} - y_{bottom}} \\ z' &= \frac{2z}{z_{near} - z_{far}} - \frac{z_{far} + z_{near}}{z_{near} - z_{far}} \end{aligned}

:由于相机坐标系是右手系,因此znear>zfarz_{near} > z_{far}

从投影变换结果(x,y,z)(x', y', z')可以得到正交投影的投影变换矩阵

P=(2xrightxleft00xleft+xrightxrightxleft02ytopybottom0ybottom+ytopytopybottom002znearzfarzfar+znearznearzfar0001)P_{正交} = \begin{pmatrix} \large \frac{2}{\large x_{right} - x_{left}} & 0 & 0 & - \large \frac{\large x_{left} + x_{right}}{\large x_{right} - x_{left}} \\[1em] 0 & \large \frac{2}{\large y_{top} - y_{bottom}} & 0 & - \large \frac{\large y_{bottom} + y_{top}}{\large y_{top} - y_{bottom}} \\[1em] 0 & 0 & \large \frac{2}{\large z_{near} - z_{far}} & -\large \frac{\large z_{far} + z_{near}}{\large z_{near} - z_{far}} \\[1em] 0 & 0 & 0 & 1 \end{pmatrix}

透视投影

投射投影则是模拟真实世界中看见物体的模式进行的投影,具有立体效果。其平截头体如下所示:

img

可以看到点和投影点有一组三角相似关系:

img

进而根据三角相似原理可以得到一组等式:

xx=yy=znearz{x=znearxzy=znearyzz=znear\frac{x'}{x} = \frac{y'}{y} = \frac{z_{near}}{z} \\[1em] \Rightarrow \large\begin{cases} x' &= \frac{z_{near} * x}{z} \\[1em] y' &= \frac{z_{near} * y}{z} \\[1em] z' &= z_{near} \end{cases}

需要注意的是,这里的xx'yy'并不是最终我们想要的标准化设备坐标,而是在近平面的投影坐标,实际上还是处于相机坐标系中;因此还需要进行一下范围变换:

x=xxleftxrightxleft21=x(width/2)width21=2xwidth=znear2xzwidthy=znear2yzheight\begin{aligned} x'' &= \frac{x' - x_{left}}{x_{right} - x_{left}} * 2 - 1 \\ &= \frac{x' - (-width / 2)}{width} * 2 - 1 \\ &= \frac{2x'}{width} \\[2em] &= \frac{z_{near} * 2x}{z * width} \\[2em] y''&= \frac{z_{near} * 2y}{z * height} \end{aligned}

不过由于投影坐标系是左手系的,因此z轴方向与相机坐标系刚好相反,需要取负值;其次,由于ww分量为zz,所以各分量需要先乘以zz

x=znear2xwidthy=znear2yheight\begin{aligned} x'' &= - \frac{z_{near} * 2x}{width} \\[2em] y''&= - \frac{z_{near} * 2y}{height} \end{aligned}

由于投影到平面的点是2D的(因为投影后所有点的zz都相同),不过由于深度测试需要用到zz值来判断点在空间中前后顺序,因此需要对原有的zz坐标进行线性插值得到一个符合标准化设备坐标系的坐标值;不过,由于齐次坐标的ww分量为zz,因此实际上应该对1z\large\frac{1}{\large z}进行插值[2]

思考:我在想如果使w=1w=1,那么是不是可以用普通的一次线性函数插值?还有就是为啥一定要利用ww的特性来除以x,y,zx, y, z坐标,而不是直接让其为目标值?这样看起来有点多此一举……

事实上,使用普通一次线性函数z=az+bz'' = az + b得到的参数似乎对不上公认的那个透视投影矩阵。

z=a1z+bz'' = a\cdot\frac{1}{\large z} + b

同时,在近平面z=1z'' = -1和远平面z=1z'' = 1处有确定的投影关系:

{a1znear+b=1a1zfar+b=1{a=2znearzfarznearzfarb=znear+zfarznearzfar\begin{cases} a * \large\frac{1}{\large z_{near}} + b &= -1 \\[1em] a * \large\frac{1}{\large z_{far}} + b &= 1 \end{cases} \\[1em] \Rightarrow \begin{cases} a &= \large \frac{\large 2 * z_{near} * z_{far}}{\large z_{near} - z_{far}} \\[1em] b &= - \large \frac{\large {\large z_{near} + z_{far}}}{\large z_{near} - z_{far}} \end{cases} \\

经过这么多步转换后,可以得到透视投影矩阵为:

P=(2znearwidth00002znearheight0000znear+zfarznearzfar2znearzfarznearzfar0010)P_{透视} = \begin{pmatrix} \large \frac{2 * \large z_{near}}{width} & 0 & 0 & 0 \\[1em] 0 & \large \frac{2 * \large z_{near}}{height} & 0 & 0 \\[1em] 0 & 0 & \large \frac{\large {\large z_{near} + z_{far}}}{\large z_{near} - z_{far}} & \large \frac{2 * \large z_{near} * z_{far}}{\large z_{near} - z_{far}} \\[1em] 0 & 0 & -1 & 0 \end{pmatrix}

不过上述参数只是从数学上假设的,实际应用中计算透视投影矩阵时通常使用视场角fovy)、宽高比aspect)、近平面和远平面这四个值来计算透视投影矩阵:

img

很明显,通过视场角、近平面和宽高比能够算出近平面的宽高;

{tan(fovy/2)=0.5heightznearaspect=widthheight记 θ=fovy/2P=(cot(θ)aspect0000cot(θ)0000znear+zfarznearzfar2znearzfarznearzfar0010)\begin{cases} tan(fovy / 2) &= \large \frac{\large 0.5 * height}{\large z_{near}} \\[1em] aspect &= \large \frac{\large width}{\large height} \end{cases} \\[1em] \text{记 }\theta = fovy / 2 \\[1em] \Rightarrow P_{透视} = \begin{pmatrix} \large \frac{cot(\theta)}{aspect} & 0 & 0 & 0 \\[1em] 0 & cot(\theta) & 0 & 0 \\[1em] 0 & 0 & \large \frac{\large {\large z_{near} + z_{far}}}{\large z_{near} - z_{far}} & \large \frac{2 * \large z_{near} * z_{far}}{\large z_{near} - z_{far}} \\[1em] 0 & 0 & -1 & 0 \end{pmatrix}

相关文档


  1. 正交矩阵 - 维基百科,自由的百科全书 ↩︎

  2. 透视投影详解 - 翰墨小生 - 博客园 ↩︎