一、视图矩阵(View)矩阵
首先明确视图矩阵的作用:在OpenGL的众多坐标系中,存在一个世界坐标系和一个摄像机坐标系,视图矩阵的作用就是将世界坐标系内的坐标转换成摄像机坐标系内的坐标。
如图,空间中存在一个点 PPP,它在世界坐标系内的坐标为(Xw,Yw,Zw)(X_w,Y_w,Z_w)(Xw,Yw,Zw),在摄像机坐标系内的坐标为(Xc,Yc,Zc)(X_c,Y_c,Z_c)(Xc,Yc,Zc),在视图矩阵的转换下,存在如下等式:
[XcYcZc1]=View[XwYwZw1]\begin{bmatrix} X_c \\ Y_c \\ Z_c \\ 1 \\ \end{bmatrix} =View \begin{bmatrix} X_w \\ Y_w \\ Z_w \\ 1 \\ \end{bmatrix} ⎣⎢⎢⎡XcYcZc1⎦⎥⎥⎤=View⎣⎢⎢⎡XwYwZw1⎦⎥⎥⎤
二、坐标系的变换与坐标的变换
而上面提到的负责坐标的变换的ViewViewView矩阵需要明确一下到底是
从世界坐标系变换到摄像机坐标系的旋转平移矩阵
从摄像机坐标系变换到世界坐标系的旋转平移矩阵
两个其中哪一个,两个互为逆矩阵
举个简单的例子,如下图所示,世界坐标系centerXwYwZwcenterX_wY_wZ_wcenterXwYwZw内有一点Pw(0,0,−2)P_w(0,0,-2)Pw(0,0,−2),摄像机坐标系eyeXcYcZceyeX_cY_cZ_ceyeXcYcZc各坐标轴与世界坐标系各坐标轴平行,即没有旋转变化,摄像机坐标系原点位于世界坐标系(0,0,1)(0,0,1)(0,0,1)处。
可以知道,PPP点在摄像机坐标系内的坐标应该为Pc(0,0,−3)P_c(0,0,-3)Pc(0,0,−3)
从世界坐标系变换到摄像机坐标系的平移矩阵:
Tw2c=[1000010000110001]T_{w2c}=\begin{bmatrix}1&0&0&0\\ 0&1&0&0\\ 0&0&1&1\\ 0&0&0&1 \end{bmatrix} Tw2c=⎣⎢⎢⎡1000010000100011⎦⎥⎥⎤
从摄像机坐标系变换到世界坐标系的平移矩阵:
Tc2w=[10000100001−10001]T_{c2w}=\begin{bmatrix}1&0&0&0\\ 0&1&0&0\\ 0&0&1&-1\\ 0&0&0&1 \end{bmatrix} Tc2w=⎣⎢⎢⎡10000100001000−11⎦⎥⎥⎤
可以看到,PwP_wPw到PcP_cPc的坐标变换矩阵ViewViewView:
Pc=ViewPw=[10000100001−10001]Pw=Tc2wPwP_c=ViewP_w=\begin{bmatrix}1&0&0&0\\ 0&1&0&0\\ 0&0&1&-1\\ 0&0&0&1 \end{bmatrix}P_w=T_{c2w}P_w Pc=ViewPw=⎣⎢⎢⎡10000100001000−11⎦⎥⎥⎤Pw=Tc2wPw
简便记忆方法:两个相邻的w抵消,剩下个c
通俗地来说,坐标变化过程可以这样理解:将摄像机坐标系经过旋转平移矩阵(Rc2wTc2w)(R_{c2w}T_{c2w})(Rc2wTc2w)变化到与世界坐标系重合,并且将世界坐标系内原有的顶点坐标做出同样的变换,得到的就是那些顶点位于摄像机坐标系内的坐标
因此:
View=Rc2wTc2w=(Rw2cTw2c)−1View=R_{c2w}T_{c2w}=(R_{w2c}T_{w2c})^{-1} View=Rc2wTc2w=(Rw2cTw2c)−1
三、视图矩阵推导
由摄像机坐标系变换到世界坐标系的旋转平移矩阵比较难求,但由世界坐标系变换到摄像机坐标系的旋转平移矩阵是非常好求的,而这两个矩阵又是互为逆矩阵的关系,所以从求解由世界坐标系变换到摄像机坐标系的旋转平移矩阵入手
如图所示,摄像机位于世界坐标系中 eyeeyeeye 位置,并在该位置形成了自己的坐标系,要推导视图矩阵,需要知道世界坐标系centerXwYwZwcenterX_wY_wZ_wcenterXwYwZw是如何变换(经过怎样的平移和旋转)成为摄像机坐标系eyesu(−f)eye~s~u~(-f)eyesu(−f)的。
首先是比较简单的旋转变换,由于所有向量都是单位向量的形式,将世界坐标系旋转到与摄像机坐标系的角度相同,所用到的旋转矩阵Rw2cR_{w2c}Rw2c可以比较直接地写出来:
Rw2c=[sxux−fx0syuy−fy0szuz−fz00001]R_{w2c}=\begin{bmatrix} s_x&u_x&-f_x&0 \\ s_y&u_y&-f_y&0 \\ s_z&u_z&-f_z&0 \\ 0&0&0&1 \\ \end{bmatrix} Rw2c=⎣⎢⎢⎡sxsysz0uxuyuz0−fx−fy−fz00001⎦⎥⎥⎤
记录一下为什么可以直接写出来吧,世界坐标系由三个单位向量组成,可以看作一组基,旋转后得到的摄像机坐标系可以看作由另外三个单位向量组成,即另外一组基。
空间中同一个向量PPP,可以由一组基[e1,e2,e3][e_1,e_2,e_3][e1,e2,e3]的线性组合表示,也可以由另一组基[e1′,e2′,e3′][e_1',e_2',e_3'][e1′,e2′,e3′]的线性组合表示:
P=a1e1+a2e2+a3e3=a1′e1′+a2′e2′+a3′e3′P=a_1e_1+a_2e_2+a_3e_3=a_1'e_1'+a_2'e_2'+a_3'e_3' P=a1e1+a2e2+a3e3=a1′e1′+a2′e2′+a3′e3′
其中,a1,a2,a3a_1,a_2,a_3a1,a2,a3便可以认为是向量PPP在由基[e1,e2,e3][e_1,e_2,e_3][e1,e2,e3]组成的坐标系内的坐标,a1′,a2′,a3′a_1',a_2',a_3'a1′,a2′,a3′同理。
写成矩阵形式:
[e1e2e3][a1a2a3]=[e1′e2′e3′][a1′a2′a3′]\begin{bmatrix} e_1&e_2&e_3 \end{bmatrix}\begin{bmatrix} a_1\\a_2\\a_3 \end{bmatrix}=\begin{bmatrix} e_1'&e_2'&e_3' \end{bmatrix}\begin{bmatrix} a_1'\\a_2'\\a_3' \end{bmatrix} [e1e2e3]⎣⎡a1a2a3⎦⎤=[e1′e2′e3′]⎣⎡a1′a2′a3′⎦⎤
假设由基[e1,e2,e3][e_1,e_2,e_3][e1,e2,e3]组成的坐标系就是世界坐标系,其中:
e1=(1,0,0)Te2=(0,1,0)Te3=(0,0,1)Te_1=(1,0,0)^T\\ e_2=(0,1,0)^T\\ e_3=(0,0,1)^T e1=(1,0,0)Te2=(0,1,0)Te3=(0,0,1)T
由基[e1′,e2′,e3′][e_1',e_2',e_3'][e1′,e2′,e3′]组成的坐标系就是摄像机坐标系,其中:
e1′=(sx,sy,sz)Te2′=(ux,uy,uz)Te3′=(−fx,−fy,−fz)Te_1'=(s_x,s_y,s_z)^T\\ e_2'=(u_x,u_y,u_z)^T\\ e_3'=(-f_x,-f_y,-f_z)^T e1′=(sx,sy,sz)Te2′=(ux,uy,uz)Te3′=(−fx,−fy,−fz)T
于是:
[100010001][a1a2a3]=[sxux−fxsyuy−fyszuz−fz][a1′a2′a3′]\begin{bmatrix} 1&0&0\\ 0&1&0\\ 0&0&1 \end{bmatrix} \begin{bmatrix} a_1\\ a_2\\ a_3 \end{bmatrix}=\begin{bmatrix} s_x&u_x&-f_x\\ s_y&u_y&-f_y\\ s_z&u_z&-f_z \end{bmatrix} \begin{bmatrix} a_1'\\ a_2'\\ a_3' \end{bmatrix} ⎣⎡100010001⎦⎤⎣⎡a1a2a3⎦⎤=⎣⎡sxsyszuxuyuz−fx−fy−fz⎦⎤⎣⎡a1′a2′a3′⎦⎤
[a1a2a3]=[100010001]T[sxux−fxsyuy−fyszuz−fz][a1′a2′a3′]\begin{bmatrix} a_1\\ a_2\\ a_3 \end{bmatrix}=\begin{bmatrix} 1&0&0\\ 0&1&0\\ 0&0&1 \end{bmatrix}^T\begin{bmatrix} s_x&u_x&-f_x\\ s_y&u_y&-f_y\\ s_z&u_z&-f_z \end{bmatrix} \begin{bmatrix} a_1'\\ a_2'\\ a_3' \end{bmatrix} ⎣⎡a1a2a3⎦⎤=⎣⎡100010001⎦⎤T⎣⎡sxsyszuxuyuz−fx−fy−fz⎦⎤⎣⎡a1′a2′a3′⎦⎤
[a1a2a3]=[sxux−fxsyuy−fyszuz−fz][a1′a2′a3′]\begin{bmatrix} a_1\\ a_2\\ a_3 \end{bmatrix}=\begin{bmatrix} s_x&u_x&-f_x\\ s_y&u_y&-f_y\\ s_z&u_z&-f_z \end{bmatrix} \begin{bmatrix} a_1'\\ a_2'\\ a_3' \end{bmatrix} ⎣⎡a1a2a3⎦⎤=⎣⎡sxsyszuxuyuz−fx−fy−fz⎦⎤⎣⎡a1′a2′a3′⎦⎤
齐次坐标形式:
[a1a2a31]=[sxux−fx0syuy−fy0szuz−fz00001][a1′a2′a3′1]\begin{bmatrix} a_1\\ a_2\\ a_3\\ 1 \end{bmatrix}=\begin{bmatrix} s_x&u_x&-f_x&0\\ s_y&u_y&-f_y&0\\ s_z&u_z&-f_z&0\\ 0&0&0&1 \end{bmatrix} \begin{bmatrix} a_1'\\ a_2'\\ a_3'\\ 1 \end{bmatrix} ⎣⎢⎢⎡a1a2a31⎦⎥⎥⎤=⎣⎢⎢⎡sxsysz0uxuyuz0−fx−fy−fz00001⎦⎥⎥⎤⎣⎢⎢⎡a1′a2′a3′1⎦⎥⎥⎤
Pw=Rw2cPcP_w=R_{w2c}P_c Pw=Rw2cPc
至于平移变换,由于摄像机位置eyeeyeeye的坐标是(ex,ey,ez)(e_x,e_y,e_z)(ex,ey,ez),所以将世界坐标系原点(0,0,0)(0,0,0)(0,0,0)平移到摄像机坐标系原点eyeeyeeye所处的位置,所用到的平移矩阵Tw2cT_{w2c}Tw2c如下:
Tw2c=[100ex010ey001ez0001]T_{w2c}=\begin{bmatrix} 1&0&0&e_x \\ 0&1&0&e_y \\ 0&0&1&e_z \\ 0&0&0&1 \\ \end{bmatrix} Tw2c=⎣⎢⎢⎡100001000010exeyez1⎦⎥⎥⎤
旋转矩阵是正交矩阵,因此它的逆矩阵就是它的转置矩阵:
Rc2w=(Rw2c)−1=(Rw2c)T=[sxsysz0uxuyuz0−fx−fy−fz00001]R_{c2w}=(R_{w2c})^{-1}=(R_{w2c})^T=\begin{bmatrix} s_x&s_y&s_z&0 \\ u_x&u_y&u_z&0 \\ -f_x&-f_y&-f_z&0 \\ 0&0&0&1 \\ \end{bmatrix} Rc2w=(Rw2c)−1=(Rw2c)T=⎣⎢⎢⎡sxux−fx0syuy−fy0szuz−fz00001⎦⎥⎥⎤
平移矩阵的逆矩阵就是将平移过的量平移回去:
Tc2w=(Tw2c)−1=[100−ex010−ey001−ez0001]T_{c2w}=(T_{w2c})^{-1}=\begin{bmatrix} 1&0&0&-e_x \\ 0&1&0&-e_y \\ 0&0&1&-e_z \\ 0&0&0&1 \\ \end{bmatrix} Tc2w=(Tw2c)−1=⎣⎢⎢⎡100001000010−ex−ey−ez1⎦⎥⎥⎤
求得摄像机坐标系变换到世界坐标系的旋转平移矩阵后,根据上文的推导,求出ViewViewView矩阵:
View=Rc2wTc2w=[sxsysz0uxuyuz0−fx−fy−fz00001][100−ex010−ey001−ez0001]=[sxsysz−(s⋅eye)uxuyuz−(u⋅eye)−fx−fy−fzf⋅eye0001]View=R_{c2w}T_{c2w}=\begin{bmatrix} s_x&s_y&s_z&0 \\ u_x&u_y&u_z&0 \\ -f_x&-f_y&-f_z&0 \\ 0&0&0&1 \\ \end{bmatrix}\begin{bmatrix} 1&0&0&-e_x \\ 0&1&0&-e_y \\ 0&0&1&-e_z \\ 0&0&0&1 \\ \end{bmatrix}=\begin{bmatrix} s_x&s_y&s_z&-(s·eye) \\ u_x&u_y&u_z&-(u·eye) \\ -f_x&-f_y&-f_z&f·eye \\ 0&0&0&1 \\ \end{bmatrix} View=Rc2wTc2w=⎣⎢⎢⎡sxux−fx0syuy−fy0szuz−fz00001⎦⎥⎥⎤⎣⎢⎢⎡100001000010−ex−ey−ez1⎦⎥⎥⎤=⎣⎢⎢⎡sxux−fx0syuy−fy0szuz−fz0−(s⋅eye)−(u⋅eye)f⋅eye1⎦⎥⎥⎤
四、glm::lookAt
经过上述的推导后,相信glm::lookAt函数源码是怎么实现的就很清楚了,上文的字母特意选用了与源码内相一致的字母。
glm::lookAt(eye, center, up);
glm::lookAt函数有三个参数,eye表示摄像机所在位置,center表示摄像机要看向的中心点的位置,在本文中是世界坐标系原点,up表示摄像机的三个方位向量中的up向量。
函数返回一个4×44\times 44×4的视图矩阵(view矩阵)。
glm::lookAt其实会先判断是左手坐标系还是右手坐标系,因为左手坐标系和右手坐标系z轴的指向不同,因而最终的运算结果也有差异,OpenGL是右手坐标系,因此我们来看看lookAtRH函数
GLM_FUNC_QUALIFIER mat<4, 4, T, Q> lookAtRH(vec<3, T, Q> const& eye, vec<3, T, Q> const& center, vec<3, T, Q> const& up){vec<3, T, Q> const f(normalize(center - eye));vec<3, T, Q> const s(normalize(cross(f, up)));vec<3, T, Q> const u(cross(s, f));mat<4, 4, T, Q> Result(1);Result[0][0] = s.x;Result[1][0] = s.y;Result[2][0] = s.z;Result[0][1] = u.x;Result[1][1] = u.y;Result[2][1] = u.z;Result[0][2] =-f.x;Result[1][2] =-f.y;Result[2][2] =-f.z;Result[3][0] =-dot(s, eye);Result[3][1] =-dot(u, eye);Result[3][2] = dot(f, eye);return Result;}
函数会先求出f向量,即摄像机朝向。
vec<3, T, Q> const f(normalize(center - eye));
再通过 f×upf\times upf×up 叉乘的方式求出s向量
vec<3, T, Q> const s(normalize(cross(f, up)));
最后通过 s×fs\times fs×f 叉乘的方式,求出u向量
vec<3, T, Q> const u(cross(s, f));
函数最终得到的ResultResultResult也与我们在上文中推导出的ViewViewView矩阵完全一样,由于上文的字母特意选用了与源码内相一致的符号,所以两个矩阵直接就可以看出是完全一样的。
View=[sxsysz−(s⋅eye)uxuyuz−(u⋅eye)−fx−fy−fzf⋅eye0001]View=\begin{bmatrix} s_x&s_y&s_z&-(s·eye) \\ u_x&u_y&u_z&-(u·eye) \\ -f_x&-f_y&-f_z&f·eye \\ 0&0&0&1 \\ \end{bmatrix} View=⎣⎢⎢⎡sxux−fx0syuy−fy0szuz−fz0−(s⋅eye)−(u⋅eye)f⋅eye1⎦⎥⎥⎤
Result=[sxsysz−(s⋅eye)uxuyuz−(u⋅eye)−fx−fy−fzf⋅eye0001]Result=\begin{bmatrix} s_x&s_y&s_z&-(s·eye) \\ u_x&u_y&u_z&-(u·eye) \\ -f_x&-f_y&-f_z&f·eye \\ 0&0&0&1 \\ \end{bmatrix} Result=⎣⎢⎢⎡sxux−fx0syuy−fy0szuz−fz0−(s⋅eye)−(u⋅eye)f⋅eye1⎦⎥⎥⎤
注意!注意!注意!这是一个比较重要的小细节
glm库储存矩阵元素采用的是列优先的储存方式
所以mat[ i ][ j ]表示的是第i列,第j行元素
这块代码的元素位置以及本文推导的矩阵内的元素位置并没有问题,是完全一样的
Result[0][0] = s.x;//0列0行Result[1][0] = s.y;//1列0行Result[2][0] = s.z;//2列0行Result[0][1] = u.x;//0列1行Result[1][1] = u.y;//1列1行Result[2][1] = u.z;//2列1行Result[0][2] =-f.x;//0列2行Result[1][2] =-f.y;//1列2行Result[2][2] =-f.z;//2列2行Result[3][0] =-dot(s, eye);//3列0行Result[3][1] =-dot(u, eye);//3列1行Result[3][2] = dot(f, eye);//3列2行
这里创建的是4×44\times44×4的单位矩阵
mat<4, 4, T, Q> Result(1);
至此,视图矩阵的推导以及glm::lookAt函数的实现源码分析完毕。
如果觉得《[OpenGL] 视图矩阵(View)矩阵与glm::lookAt函数源码解析》对你有帮助,请点赞、收藏,并留下你的观点哦!