目录html
经过以前的教程,对WebGL中可编程渲染管线的流程有了必定的认识。可是只有前面的知识还不足以绘制真正的三维场景,能够发现以前咱们绘制的点、三角形的坐标都是[-1,1]之间,Z值的坐标都是采用的默认0值,而通常的三维场景都是很复杂的三维坐标。为了在二维视图中绘制复杂的三维场景,须要进行相应的的图形变换;这一篇教程,就是详细讲解WebGL的图形变换的过程,这个过程一样也适合OpenGL/OpenGL ES,甚至其余3D图形接口。编程
能够用照相机拍摄照片来模拟这个图形变换的过程,若是要对某个物体拍摄照片,大体过程以下:函数
而在WebGL/OpenGL中,具体的图形变换流程以下所示[3]:
其中模型变换、视图变换、投影变换是咱们本身在着色器里定义和实现的,而视口变换通常是WebGL/OpenGL自动完成的。这就好像咱们拍照的时候,须要本身去调整位置,相机镜头焦距,而成像的过程就交给相机。因此模型变换、视图变换、投影变换这三者特别重要,另外附一张WebGL/OpenGL矩阵变换的流程图[4]:
学习
从上两图中能够发现,场景中的物体老是从一个坐标系空间转换到另一个坐标系空间。spa
在参考文献[2]中描述的WebGL/OpenGL整个图形变换过程的坐标系和单位:
.net
其流程与前文论述的基本一致,能够看到投影变换以后的过程不是那么简单,还须要将获得的齐次裁剪坐标作透视除法(除以w),作剪切和视口/深度范围变换,光栅化等。设计
其中,用户/着色器变换(也就是教程要具体详述的模型变换、视图变换和投影变换)包含坐标系和单位以下所示:
3d
在一个三维软件中浏览一个三维物体时候,老是会提供给用户平移、缩放和旋转的交互操做,而这正是模型变换的内容。在图形学的范畴当中,平移变换、旋转变换属于刚体变换,缩放和旋转属于线性变换,刚体变换和线性变换又属于仿射变换,而仿射变换也能够当作投影变换的一种[5]。
code
也就是说这些图形变换,本质上能够当作是同一种变换;在数学上,可使用矩阵来描述这种变换。而且,为了兼容各类变换的特殊性,会在3维的基础上再加一维,使用4维的向量和矩阵。4维向量表述一个点(x,y,z,w)等价于三维向量(x/w,y/w,z/w),这就是前面提到的齐次坐标。orm
具体来讲,对于空间某个点v0(x0,y0,z0,1),通过空间图像变换后获得新的点v1(x1,y1,z1,1),那么存在这样一个4行4列的矩阵M:
\[ M= \left[ \begin{matrix} a & b & c & d \\ e & f & g & h\\ i & j & k & l\\ m & n & o & p\\ \end{matrix} \right] \]
模型变换包括平移变换、缩放变换和旋转变换。从内容上来说,这几种变换正好应对的三维交互操做的平移、变换和缩放。经过鼠标操做调整模型变换矩阵就能够实现一种简单三维交互操做。
对于一个点(x,y,z,1),平移以后,获得的点就是(x+Tx,y+Ty,z+Tz,1),其中Tx、Ty、Tz分别表示点在X轴、Y轴、Z轴方向上移动的距离。那么将其代入方程组式(2)的两边,有:
\[\begin{cases} a*x +b*y +c*z + d =x+Tx\\ e*x +f*y +g*z +h =y+Ty\\ i*x +j*y +k*z + l =z+Tz\\ m*x +n*y +o*z + p =1 \end{cases} \]
对于一个点(x,y,z,1),以原点为中心缩放,在X方向缩放Sx倍,在Y方向缩放Sy倍,在Z方向缩放Sz倍,那么新的坐标值为(x*Sx,y*Sy,z*Sz,1)。将其代入方程组式(2)的两边,有:
\[\begin{cases} a*x +b*y +c*z + d =x*Sx\\ e*x +f*y +g*z +h =y*Sy\\ i*x +j*y +k*z + l =z*Sz\\ m*x +n*y +o*z + p =1 \end{cases} \]
旋转变换就稍微复杂一点,对旋转变换而言,必须知道旋转轴、旋转方向和旋转角度。能够绕X轴,Y轴和Z轴旋转,因此通常都会有三个旋转矩阵。以绕Z轴旋转为例,在Z轴正半轴沿着Z轴负方向进行观察,若是看到的物体是逆时针旋转的,那么就是正旋转,旋转方向就是正的,旋转值就是正数;反之若是旋转值为负数,说明旋转方向就是负的,沿着顺时针旋转。用更加通用的说法来讲,正旋转就是右手法则旋转:右手握拳,大拇指伸直并使其指向旋转轴的正方向,那么右手其他几个手指就指明了旋转的方向。
对于一个点p(x,y,z,1),绕Z轴旋转,由于旋转后的Z值不变,因此能够忽略Z值的变换,只考虑XY空间的变化。此时设r为原点到点p的距离,α是X轴旋转到该点的角度。如图所示:
那么p点的坐标表示为式(3):
\[\begin{cases} x=r*cosα\\ y=r*sinα\\ \end{cases} \tag{3} \]
使用矩阵来描述图形变换的好处之一就是可以将以上全部的变换组合起来,例如以下式(6):
\[ v1=S*(R*(T*v0)) \tag{6} \]
表达的图形变换是对于点v0,首先通过平移变换,再通过旋转变换,最后再进行缩放,获得新的点v1。
根据矩阵乘法的结合律,式(6)能够写成:
\[ v1=(S*R*T)*v0 \]
那么模型矩阵M就能够表示为:
\[ M=S*R*T \]
注意上述模型矩阵的SRT顺序并非固定的,须要根据实际的状况采起合适的矩阵,不然会达不到想要的效果。一个重要的原则就是记住缩放变换老是基于原点的,旋转变换老是基于旋转轴的,在进行缩放变换和旋转变换以前每每须要先平移变换至原点位置(不是绝对)。
视图变换其实就是模型变换的逆变换。试想一下,拿一个物体给相机拍摄,其实也就是拿相机去拍摄一个物体,视图变换和模型变换的结果并无显著的区别,有些状况下二者甚至能够合并成一个模型-视图变换(model-view transform)。二者之因此须要分开进行彻底是由实际的交互操做决定的:旋转、缩放到合适的位置实际上是很难设置的,不少交互操做须要在视空间/摄像机空间中设置才比较合适,这个时候就须要视图变换了。
视图变换其实就是构建一个视空间/摄像机空间,须要三个条件量:
经过上述三个条件量,就能够构建一个视图矩阵。这个矩阵通常能够经过图形矩阵库的LookAt()函数进行设置,例如在WebGL的cuon-matrix.js中,其设置函数为:
由前文得知,视图变换构建了一个视空间/摄像机空间坐标系,为了对应于世界坐标系的XYZ,能够将其命名为UVN坐标系,它由以前提到的三个条件量构建而成:
如图所示[7]:
因为视图变换是模型变换的逆变换,以上视图变换的效果,等价于进行一个旋转变换,再进行一个平移变换。故有视图矩阵V:
\[ V=M^{-1}=(TR)^{-1}=R^{-1}T^{-1} \]
根据以前平移矩阵的定义,那么有:
\[ T^{-1}= \left[ \begin{matrix} 1 & 0 & 0 & -Tx \\ 0 & 1 & 0 & -Ty\\ 0 & 0 & 1 & -Tz\\ 0 & 0 & 0 & 1\\ \end{matrix} \right] \]
这里的(Tx,Ty,Tz)就是视点eye(eyeX, eyeY, eyeZ)。通过平移变换以后,相机的原点就和世界原点重合,剩下的操做就是经过旋转矩阵R,将世界坐标系XYZ的点转换到成UVN坐标系上的点。令:
\[ X=(1,0,0),Y=(0,1,0),Z=(0,0,1)\\ U=(Ux,Uy,Uz),V=(Vx,Vy,Vz),N=(Nx,Ny,Nz) \]
则有:
\[ \left[ \begin{matrix} U & V & N \\ \end{matrix} \right] = \left[ \begin{matrix} X & Y & Z \\ \end{matrix} \right] * R = \left[ \begin{matrix} X & Y & Z \\ \end{matrix} \right] * \left[ \begin{matrix} Ux & Vx & Nx \\ Uy & Vy & Ny \\ Uz & Vz & Nz \\ \end{matrix} \right] \]
又由旋转矩阵R为正交矩阵,因此有:
\[ R^{-1} = \left[ \begin{matrix} Ux & Uy & Uz \\ Vx & Vy & Vz \\ Nx & Ny & Nz \\ \end{matrix} \right] \]
最后便可得视图矩阵:
\[ V=R^{-1} T^{-1}= \left[ \begin{matrix} Ux & Uy & Uz & 0 \\ Vx & Vy & Vz & 0 \\ Nx & Ny & Nz & 0 \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] * \left[ \begin{matrix} 1 & 0 & 0 & -Tx \\ 0 & 1 & 0 & -Ty\\ 0 & 0 & 1 & -Tz\\ 0 & 0 & 0 & 1\\ \end{matrix} \right] = \left[ \begin{matrix} Ux & Uy & Uz & -U·T \\ Vx & Vy & Vz & -V·T \\ Nx & Ny & Nz & -N·T \\ 0 & 0 & 0 & 1 \\ \end{matrix} \right] \]
投影变换定义的是一个可视空间,决定了哪些物体显示,哪些物体不显示,以及物体如何显示。经常使用的可视空间有两种:
投影投影模拟的就是人眼成像或者摄像机成像的过程,试想一下,摄像机拍摄的老是取景器方位内的物体,而且呈现近大远小的效果。在WebGL/OpenGL中,透视投影就决定了一个视点、视线、近裁剪面、远裁剪面组成的四棱椎可视空间。如图所示:
在实际使用中,图形矩阵库(我这里用的WebGL的cuon-matrix.js)通常都会提供相似setPerspective()的函数,具体定义以下:
如图所示,已知视空间坐标系XYZ,坐标系原点(视点)为O,视椎体近截面与视点距离为n,远平面与视点的距离为f。已知视椎体空间中有一点为P(x0,y0,z0),那么要求的就是射线OP与近截面的投影点P1(x1,y1,z1)。如图所示:
近截面与平面XOY平行,那么z1 = -near,那么问题能够简化为:已知空间上点P的坐标,存在点P与坐标O连线上一点P1,P1的Z值已知,求P1坐标。如图所示:
显然这是一个三角形类似的问题,P1点在视空间坐标系的XY坐标为:
\[ \begin{cases} x1'=-n/z0*x0\\ y1'=-n/z0*y0\\ \end{cases} \]
根据前文论述,投影变换获得的4维度齐次坐标(x1,y1,z1,w1),会除以w1使得x1和y1的值归一化到-1到1之间。那么可设l和r分别为近截面左、右边框的x坐标,那么就是l映射到-1,r映射到1。这是一个线性变换问题:存在两组点(l,-1)(r,1)知足方程y=kx+b。
\[ \begin{cases} kl+b=-1\\ kr+b=1\\ \end{cases} \]
解方程组:
\[ \begin{cases} k=\frac{2}{r-l}\\ b=-\frac{r+l}{r-l}\\ \end{cases} \]
那么P1归一化后的x坐标xn为:
\[ xn=\frac{2}{r-l}*x1'-\frac{r+l}{r-l}=-\frac{1}{z0}*(\frac{2n}{r-l}*x0+\frac{r+l}{r-l}*z0) \]
同理可得,P1归一化以后y 坐标yn为:
\[ yn=-\frac{1}{z0}*(\frac{2n}{t-b}*y0+\frac{t+b}{t-b}*z0) \]
能够发现,归一化的坐标xn、yn都存在一个乘数因子(-1/z0),那么能够令投影变换后的w1=-z0,这样就能够知足归一化以后的wn=1,而且知足上面xn、yn的表达式。即有裁剪坐标系的点P1(x1,y1,z1,w1):
\[ \begin{cases} x1= \frac{2n}{r-l}*x0+\frac{r+l}{r-l}*z0 \\ y1= \frac{2n}{t-b}*y0+\frac{t+b}{t-b}*z0 \\ w1= -z0 \\ \end{cases} \]
代入到式(2)中,得:
\[ \left[ \begin{matrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ I & J & K & L \\ 0 & 0 & -1 & 0 \\ \end{matrix} \right] * \left[ \begin{matrix} x0 \\ y0 \\ z0 \\ 1 \\ \end{matrix} \right] = \left[ \begin{matrix} x1 \\ y1 \\ z1 \\ w1 \\ \end{matrix} \right] \]
继续求上式的投影矩阵的第三行。投影转换后获得的z1是一个深度值,它是一个与x0,y0无关的值,因此I=0,J=0。而且在归一化以后,z1会成为一个-1到1之间的值:当z0=-n时(近截面),z1=-1;当 z0=-f时(远截面),z1=1。代入上式,有:
\[ \begin{cases} (K*(-n)+L)/n=-1 \\ (K*(-f)+L)/f=1 \\ \end{cases} \]
获得:
\[ \begin{cases} K=(f+n)/(n-f) \\ L=2fn/(n-f) \\ \end{cases} \]
综合,可得透视投影矩阵P:
\[ P= \left[ \begin{matrix} \frac{2n}{r-l} & 0 & \frac{r+l}{r-l} & 0 \\ 0 & \frac{2n}{t-b} & \frac{t+b}{t-b} & 0 \\ 0 & 0 & \frac{f+n}{n-f} & \frac{2fn}{n-f} \\ 0 & 0 & -1 & 0 \\ \end{matrix} \right] \]
注意,经过相似setPerspective()的函数定义的矩阵是对称的视锥体,视点在近截面的投影点为近截面的中心,于是有:
\[ \begin{cases} r=-l \\ t=-b \\ t-b=height \\ width= height*aspect \\ tan(\frac{fovy}{2})=\frac{height/2}{n} \end{cases} \]
代入透视投影矩阵P,获得对称透视投影矩阵P:
\[ P= \left[ \begin{matrix} \frac{1}{aspect*tan(\frac{fovy}{2})} & 0 & 0 & 0 \\ 0 & \frac{1}{tan(\frac{fovy}{2})} & 0 & 0 \\ 0 & 0 & \frac{f+n}{n-f} & \frac{2fn}{n-f} \\ 0 & 0 & -1 & 0 \\ \end{matrix} \right] \]
正射投影一个很常见的应用就是地图。不管是纸质地图仍是谷歌地图,甚至于室内设计的户型图、工程设计的工程图,无一例外所有都是正射投影。正射投影可以很方便的比较场景中物体的大小,而且每一个地方的所表明的大小都是同样的(分辨率一致)。固然,在这种投影下是没有深度感的,就像你在卫星地图上是看不出一座山有多高的。
正射投影一样也是近裁剪面和远裁剪面组成的可视空间,只不过这个可视空间是个长方体,如图所示:
一样的,可使用相似setOrtho()函数来设置正射投影:
在正射投影的盒状可视空间中,XYZ三个方向上都是等比例的。设盒状可视空间中某一物体点P(x0,y0,z0),那么P点在近截面的投影点为P1(x0,y0,z0’),仅仅只是Z值不一样。
同透视变换的推导同样,将P1的X、Y坐标(x0,y0)映射到-1到1的范围(xn,yn)。即有两组点(l,-1)和(r,1)知足式子(线性关系y=kx+b):
\[ Xn=Kx*x0+Bx \]
有两组点(b,−1)和(t,1)知足式子(线性关系y=kx+b):
\[ Yn=Ky*y0+By \]
分别代入解方程组,可得:
\[ \begin{cases} xn=2/(r-l)*x0-(r+l)/(r-l) \\ yn=2/(t-b)*y0-(t+b)/(t-b) \\ \end{cases} \]
一样的,在Z方向上,将z0映射成-1到1直接的值:当点在近截面时,映射成-1;当点在远截面时,映射成1。故也有两组点(-n,-1)和(-f,1)知足线性关系y=kx+b,同理可求得:
\[ zn=(-2)/(f-n)*z0-(f+n)/(f-n) \]
对于正射变换而言,w变量是没必要要的,可直接令w=1。那么裁剪坐标P1(x1,y1,z1,w1)就是通过透视除法的标准化设备坐标(xn,yn,zn,1)。故有:
\[ \begin{cases} x1=2/(r-l)*x0-(r+l)/(r-l) \\ y1=2/(t-b)*y0-(t+b)/(t-b) \\ z1=(-2)/(f-n)*z0-(f+n)/(f-n) \\ w1=1 \\ \end{cases} \]
代入到式(2)的两边,可得正射投影矩阵:
\[ O = \left[ \begin{matrix} \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 & 0 & 1 \\ \end{matrix} \right] \]
综上所述,模型矩阵M,视图矩阵V,投影矩阵P,同时做用于物体的顶点,使得最终的物体能后被看见或者进行UI操做。根据以前教程内容,逐顶点的操做能够将其放入到顶点着色器。通常而言,先进行模型变换,再进行视图变换,最后进行投影变换:
\[ v1=P*V*M*v0 \]
根据矩阵乘法的结合律:
\[ v1=(P*V*M)*v0 \]
这个P*V*M矩阵合并获得的模型视图投影矩阵(model view projection matrix),简称为MVP矩阵。在实际使用过程当中,只须要将这个MVP矩阵传入到顶点着色器,就能根据设置的矩阵获得想要的渲染效果:
gl_Position = u_MvpMatrix * a_Position;
这一篇教程是纯理论知识,相对来讲不太容易理解。若是是初次接触,至少应该先作大体的了解,后续会大量用到这里的知识。
[1]《WebGL编程指南》
[2]《OpenGL编程指南》第八版
[3] OpenGL学习脚印: 投影矩阵和视口变换矩阵(math-projection and viewport matrix)
[4] OpenGL矩阵变换的数学推导
[5] 基本图像变换:线性变换,仿射变换,投影变换
[6] 旋转变换(一)旋转矩阵
[7] 视图矩阵的推导