一、视图矩阵(View)矩阵
首先明确视图矩阵的作用:在OpenGL的众多坐标系中,存在一个世界坐标系和一个摄像机坐标系,视图矩阵的作用就是将世界坐标系内的坐标转换成摄像机坐标系内的坐标。
如图,空间中存在一个点
P
P
P,它在世界坐标系内的坐标为
(
X
w
,
Y
w
,
Z
w
)
(X_w,Y_w,Z_w)
(Xw,Yw,Zw),在摄像机坐标系内的坐标为
(
X
c
,
Y
c
,
Z
c
)
(X_c,Y_c,Z_c)
(Xc,Yc,Zc),在视图矩阵的转换下,存在如下等式:
[
X
c
Y
c
Z
c
1
]
=
V
i
e
w
[
X
w
Y
w
Z
w
1
]
\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⎦⎥⎥⎤
二、视图矩阵推导
如图所示,摄像机位于世界坐标系中 e y e eye eye 位置,并在该位置形成了自己的坐标系,要推导视图矩阵,需要知道世界坐标系 c e n t e r X w Y w Z w centerX_wY_wZ_w centerXwYwZw是如何变换(经过怎样的平移和旋转)成为摄像机坐标系 e y e s u ( − f ) eye~s~u~(-f) eye s u (−f)的。
首先是比较简单的旋转变换,由于所有向量都是单位向量的形式,将世界坐标系旋转到与摄像机坐标系的角度相同,所用到的旋转矩阵 R w 2 c R_{w2c} Rw2c可以比较直接地写出来:
R w 2 c = [ 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 ] 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⎦⎥⎥⎤
至于平移变换,由于摄像机位置
e
y
e
eye
eye的坐标是
(
e
x
,
e
y
,
e
z
)
(e_x,e_y,e_z)
(ex,ey,ez),所以将世界坐标系原点
(
0
,
0
,
0
)
(0,0,0)
(0,0,0)平移到摄像机坐标系原点
e
y
e
eye
eye所处的位置,所用到的平移矩阵
T
w
2
c
T_{w2c}
Tw2c如下:
T
w
2
c
=
[
1
0
0
e
x
0
1
0
e
y
0
0
1
e
z
0
0
0
1
]
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⎦⎥⎥⎤
注意!注意!注意!这里很关键,许多文章都没有提到这点,导致我苦思冥想了很久才发现问题所在
1、这个视图矩阵是作用在我们输入OpenGL内的物体顶点坐标上的,它实际上的作用是旋转平移这些顶点
2、OpenGL内并没有摄像机实际存在,我们的屏幕始终位于世界坐标系原点处,且世界坐标系 Z w Z_w Zw从屏幕内穿出到屏幕外,而 X w X_w Xw轴从左到右, Y w Y_w Yw轴从上到下。
我们实际上是借用了相对运动关系,通过移动世界坐标系上的那些物体的顶点,来营造出一个存在摄像机,并且摄像机还在移动改变视角的效果。
举个例子,如下图,假设一个顶点P位于
Z
w
Z_w
Zw轴负方向,坐标
(
0
,
0
,
−
2
)
(0,0,-2)
(0,0,−2)处(即我们屏幕正前方,正好可以被看到)。
要营造出一个摄像机沿着 Z w Z_w Zw轴正方向移动了距离1(即摄像机向后退了距离1)的效果,实际上我们是通过让顶点 P P P沿着 Z w Z_w Zw轴负方向移动距离1,来实现的。
也就是说,我们给点
P
P
P作用了一个平移矩阵:
[
1
0
0
0
0
1
0
0
0
0
1
−
1
0
0
0
1
]
\begin{bmatrix} 1&0&0&0 \\ 0&1&0&0 \\ 0&0&1&-1 \\ 0&0&0&1 \\ \end{bmatrix}
⎣⎢⎢⎡10000100001000−11⎦⎥⎥⎤
回到平移变换中来,我们想要营造出一个世界坐标系旋移动了平移矩阵
T
w
2
c
T_{w2c}
Tw2c,旋转了旋转矩阵
R
w
2
c
R_{w2c}
Rw2c来到达摄像机坐标系所在位置的效果,实际上我们需要对空间内的所有顶点坐标作用一个相反的平移矩阵
T
T
T,以及一个相反的旋转矩阵
R
R
R(由于旋转矩阵是正交矩阵,所以它的逆矩阵也就是它的转置矩阵):
T
=
(
T
w
2
c
)
−
1
=
[
1
0
0
−
e
x
0
1
0
−
e
y
0
0
1
−
e
z
0
0
0
1
]
T=(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}
T=(Tw2c)−1=⎣⎢⎢⎡100001000010−ex−ey−ez1⎦⎥⎥⎤
R
=
(
R
w
2
c
)
−
1
=
[
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
]
R=(R_{w2c})^{-1}=\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}
R=(Rw2c)−1=⎣⎢⎢⎡sxux−fx0syuy−fy0szuz−fz00001⎦⎥⎥⎤
最终,我们得到视图矩阵:
V
i
e
w
=
R
T
=
[
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
]
[
1
0
0
−
e
x
0
1
0
−
e
y
0
0
1
−
e
z
0
0
0
1
]
View=RT=\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}
View=RT=⎣⎢⎢⎡sxux−fx0syuy−fy0szuz−fz00001⎦⎥⎥⎤⎣⎢⎢⎡100001000010−ex−ey−ez1⎦⎥⎥⎤
V
i
e
w
=
[
s
x
s
y
s
z
−
(
s
⋅
e
y
e
)
u
x
u
y
u
z
−
(
u
⋅
e
y
e
)
−
f
x
−
f
y
−
f
z
f
⋅
e
y
e
0
0
0
1
]
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⎦⎥⎥⎤
三、glm::lookAt
经过上述的推导后,相信glm::lookAt函数源码是怎么实现的就很清楚了,上文的字母特意选用了与源码内相一致的字母。
glm::lookAt(eye, center, up);
glm::lookAt 函数有三个参数,eye 表示摄像机所在位置,center 表示摄像机要看向的中心点的位置,在本文中是世界坐标系原点,up 表示摄像机的三个方位向量中的up向量。
函数返回一个 4 × 4 4\times 4 4×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 × u p f\times up f×up 叉乘的方式求出 s 向量
vec<3, T, Q> const s(normalize(cross(f, up)));
最后通过 s × f s\times f s×f 叉乘的方式,求出 u 向量
vec<3, T, Q> const u(cross(s, f));
函数最终得到的 R e s u l t Result Result也与我们在上文中推导出的 V i e w View View矩阵完全一样,由于上文的字母特意选用了与源码内相一致的符号,所以两个矩阵直接就可以看出是完全一样的。
注意!注意!注意!这同样是一个比较重要的小细节
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 × 4 4\times4 4×4的单位矩阵
mat<4, 4, T, Q> Result(1);
至此,视图矩阵的推导以及glm::lookAt函数的实现源码分析完毕。