在刚接触图形学,看games101课程时,观察变换与投影变换就给我了相当大的麻烦,同样的(l, r, t, b, n, f)参数,网上每个人给出来的矩阵形式有所不同,让我永远分不清。随着学习资源的丰富以及自己稍微有了些成长,这篇文章终于解决了这个困惑。这个博客稍微特色点的地方就在与介绍了正交投影矩阵与透视投影矩阵的常见的两种形式的区别和联系吧
为什么要进行坐标空间的变换
在日常生活中,当我们给其他人指明一个地点的位置时经常使用东南西北,但另一个人可能根本就分不清方向;如果我们使用另一种表达方式,比如面向某某建筑物大门时的右侧,就很容易理解,这就是坐标空间的转换。特定情况下的一些事物难以描述,此时需要抽象出一个新坐标系,使得这些事物的描述更容易理解。
坐标空间变换
参考Unity Shader入门精要
所以要如何实现将一个坐标从一个坐标空间转换到另一个坐标空间?
诶注意,既然都提到了从一个转换到另一个,那么必然至少涉及到两个坐标系。坐标空间转换一定涉及到一个相对的父坐标系与子坐标系,坐标变换就是在父空间与子空间之间对点和矢量进行变换。
假设有父空间P以及一个子空间C,我们知道在父坐标空间中子坐标空间的原点以及构成坐标轴的标准正交基。
我们一般会将某个点或者向量Ac从子坐标空间转换到父坐标空间表示的Ap,或者反过来转换。这是通过一个变换矩阵实现的:
Ap = Mc → pAc
Ac = Mp → cAp
现在的问题就是,如何求解变换矩阵。其实我们只需要求出其中一个即可,另一个就是变换矩阵的逆矩阵。
假设我们已知子坐标空间C的三个坐标轴P下的表示是:[xc,yc,zc],以及C的原点在P中的表示是Oc。
在二维笛卡尔坐标系中,(3, −5)我们为什么能找到它呢,首先坐标系原点是(0, 0),构成这个坐标系的标准正交基是(1, 1),那么(3, −5)就是向x轴移动3个单位1,向y轴移动−5个单位1。
同理,我们就可以得到子坐标空间中的任意一点Ac = (a, b, c)在父坐标空间的位置:
Ap=Oc+axc+byc+czc=(xoc,yoc,zoc)+a(xxc,yxc,zxc)+b(xyc,yyc,zyc)+c(xzc,yzc,zzc)=(xoc,yoc,zoc)+xxcyxczxcxycyyczycxzcyzczzcabc=(xoc,yoc,zoc)+∣xc∣∣yc∣∣zc∣abc
这个加法像是一个平移操作,而我们正好学过如何将平移变换与线性变换合成为一个变换:
Ap=(xOc,yOc,zOc,1)+∣xc∣0∣yc∣0∣zc∣00001abc1=100001000010xOcyOczOc1∣xc∣0∣yc∣0∣zc∣00001abc1=∣xc∣0∣yc∣0∣zc∣0xOcyoczOc1abc1=∣xc∣0∣yc∣0∣zc∣0∣Oc∣1abc1
而这个变换矩阵已经很明显了:
Mc→p = ∣xc∣0∣yc∣0∣zc∣0∣Oc∣1
这里并没有要求用于表示子坐标空间坐标轴的向量[xc,yc,zc]一定是单位向量,因为如果存在缩放变换,这三个向量不一定是单位向量。
反推:如果我们知道Mc → p,将矩阵的前三列提取出来并归一化,我们就能够得到子坐标空间的x, y, z轴。或者从另一方面考虑, 假如要提取x轴,它在子坐标空间是(1, 0, 0),要求出它在父坐标空间表示,也就是Mc → p[1, 0, 0, 0]T,得到的也是同样的结果。
因为对向量做平移变换没有意义,所以需要对向量进行坐标变换时,仅需要提取坐标变换矩阵地前三行前三列即可,也就是:
Mc→p = ∣xc∣∣yc∣∣zc∣
我们常用这个矩阵对法线方向、光照方向进行空间变换。
当Mc→p是一个正交矩阵时,我们不需要计算其逆矩阵来求得Mp→c,只需要计算Mc→p的转置矩阵,也就是:
Mp→c=∣xp∣∣yp∣∣zp∣=Mc→p−1=Mc→pT=Mc→pT=−−−xcyczc−−−
从模型空间到屏幕空间
在我之前的博客中介绍了渲染管线,其中提到了顶点着色器涉及到了多个坐标空间的转换,下面会详细这个过程。
模型空间
模型空间(model space),有时被称为对象空间(object space)或局部空间(lacal space)。每个模型都有自己独立的坐标空间,当它移动或旋转的时候,模型空间也会跟着它移动和旋转。
模型空间的原点和坐标轴通常是由美术人员在建模软件里确定好的,我们可以在顶点着色器中访问到这些模型顶点坐标信息,这些坐标都是相对于模型空间中的原点定义的。
世界空间
世界空间(world space)是一个特殊的坐标系,它建立了一个“最大”的空间,也就是所有坐标空间的最外层,用来描述场景中所有对象的“绝对”位置和方向。
顶点变换的第一步,就是将顶点坐标从模型空间转换到世界空间中,这个变换通常叫做模型变换(model transform),模型坐标到世界坐标的转换非常类似于设置3D模型在世界空间中摆放的位置和姿态,也就是使用旋转、平移、缩放来放置模型。
而模型变换的变换矩阵就表示如何对模型进行旋转、缩放、平移。
Pworld=MmodelPmodel
观察空间
参考虎书的推导
观察空间(view space)也被称为相机空间(camera space)。这个坐标系以摄像机为中心,方便计算图形的可见性、视角变换和投影等。观察空间可以被认为是模型空间的特例,因为它涉及到了一个非常特殊的模型——摄像机。
要将物体从世界坐标转换到观察坐标,需要进行视图变换(view transform)。构建变换矩阵需要我们知道原点坐标轴等信息。

摄像机的位置e决定了观察坐标系的原点
摄像机的朝向w确定了观察坐标系的z轴方向,左手系则是z轴正方向而右手系是z轴负方向。
而摄像机的上方向v,也就是观察坐标系的y轴正方向。
右方向u,也就是x轴正方向,是通过y轴和z轴叉乘计算得出。
完成上面的计算后,我们已经知道观察坐标系在世界空间的原点和坐标轴,那么我们就可以得到观察坐标系转换到世界坐标系的变换矩阵,那么它的逆矩阵就是视图变换的矩阵。
观察坐标系转换到世界坐标系的变换矩阵不一定是正交矩阵,所以不能用转置直接求出逆矩阵,那么就用老方法,从观察坐标系转换到世界坐标系实际上就是一个平移和旋转摄像机的模型变换,那么只需要逆着操作,比如反向平移和反向旋转就可以得到。 而这个旋转就是一个正交矩阵,所以直接转置即可,也就是:
如果对 观察空间中坐标轴在世界空间的表示是uvw,那么将世界空间坐标轴旋转成观察空间的矩阵是[u v w]感到困惑的话,请再次阅读本章节最上面的 坐标空间变换
Mview =[u0v0w0e1]−1=xuxvxw0yuyvyw0zuzvzw00001100001000010−xe−ye−ze1
Pview=MviewMmodelPmodel
在我功力尚浅的时候,对一些东西感到困惑,这里顺便给出。
这里的困惑其实就是games101给出的公式:
Mview=RviewTview=xg^×t^xtx−g0yg^×t^yty−g0zg^×t^ztz−g00001100001000010−xe−ye−ze1=xr^xtx−g0yr^yty−g0zr^ztz−g0−r^⋅e−t⋅eg⋅e1
其中,g表示gaze,相机看向哪里。t就是top,上向量。为什么这里的公式是−g呢?因为这是一个右手系公式,右手系相机看向−z轴,那么z轴就是−g。
那么左手系公式,也能很好理解了:
Mview=RviewTview=xg^×t^xtxg0yg^×t^ytyg0zg^×t^ztzg00001100001000010−xe−ye−ze1=xr^xtxg0yr^ytyg0zr^ztzg0−r^⋅e−t⋅e−g⋅e1
齐次裁剪空间/裁剪空间
顶点接下来要从观察空间转换到裁剪空间(clip space),使用的变换矩阵叫做裁剪矩阵(clip matrix),也被称为投影矩阵(projection matrix)
裁剪空间的目标是能够方便地对渲染图元进行裁剪,完全在这个空间内的图元被保留,完全在空间外的图元被剔除,部分在空间内部分在空间外的图元就会被裁剪。而这个空间的形状则根据使用的投影方式,如果使用正交投影(orthographic projection),那么这个空间就会是一个长方体,如果使用透视投影(perspective projection),那么这个空间就会是一个视锥体(view frustum),这是一个截头四棱锥。

不管是正交投影还是透视投影的可视空间,都有两个比较重要的面,离摄像机近的叫做近裁剪平面(near clip plane),离摄像机最远的叫做远裁剪平面(far clip plane)
对应的,生成的图像也会有所不同。

左图是透视投影,右图是正交投影。
透视投影后,我们发现平行的地板缝隙并没有就像保持平行,离摄像机越近的物体反映到图像上就越大,这种投影方式模拟了我们人眼看世界的方式,近大远小。
正交投影中,大小并不会随着距离发生改变,平行线仍然平行,完全保留了物体的距离和角度。
在可视空间内部的物体才会被保留,部分在可视空间内部的物体将被裁剪,但透视投影的可视空间相对复杂一些,要进行物体的判断和裁剪将会相当困难,因此进行裁剪工作之前,我们要想办法将可视空间变换到方便我们进行裁剪工作的空间,这就是投影变换。
投影变换并没有进行投影工作,它只是为了投影做准备,真正的投影在后续的屏幕映射中。
正交投影
参考Real-time Rendering第四章的形式 fov与aspect radio的形式参考unity shader入门精要
正交投影的过程:

这里假设了可视空间不在原点。正交投影的过程可以分为平移和缩放两个变换。
对于可视空间,我们可以用一个六元组(l,r,b,t,n,f)来描述,对应的就是左侧、右侧、底部、顶部怒、近裁剪平面以及远裁剪平面。
首先平移到原点,缩放到[-1, -1, 1]与[1, 1, -1]的AABB中。那这个矩阵就很好写出来了:
MOrthographic=r−l20000t−b20000f−n200001∗100001000010−2l+r−2b+t−2n+f1
考虑考虑为什么这个矩阵乘法不用齐次坐标写成一个?不是有齐次坐标吗? 注意变换顺序!
要注意,正交投影后仍然是右手系,z轴代表离摄像机的远近,此时仍然是离摄像机越近z越大。
下面仍然是我功力尚浅时很困惑的地方,当初查各种公式发现有好几种类型的写法给我整疯了
但有时候我们可能使用左手系,也就是说z轴越大离摄像机越远(方便计算与理解啥的),那么此时只需要在缩放时将z轴翻转过来,也就是:
MOrthographic=r−l20000t−b20000n−f200001∗100001000010−2l+r−2b+t−2n+f1
唯一的区别在于缩放矩阵的z从f−n2变为了n−f2,这是反转z轴的关键。
在各种引擎以及自己设计小的系统时,不会用这6个参数来表示正交投影的可视空间

正交投影的可视空间相比于透视投影要简单得多,要定义一个需要依赖摄像机以及三个参数:
- 相机与近裁剪平面的距离Near
- 相机与远裁剪平面的距离Far
- 近裁剪平面高长度的一半Size
- 近裁剪平面的宽高比Aspect
对于右手系来说,正交投影矩阵如下(变换后看向z正轴):
Morth =Aspect⋅Size10000Size10000−Far−Near0000−Far−NearFar+Near1
Pclip=MorthPview =Aspect⋅Size10000Size10000−Far−Near2000−Far−NearFar+Near1xyz1=Aspect⋅SizexSizeyFar−Near2z−Far−NearFar+Near1
应用正交投影矩阵后,视锥体的变化,注意这里的矩阵已经使用了反转z轴的方式,所以得到的结果是一个左手系。

现在已经将坐标空间转换到了齐次裁剪空间,下面就是裁剪操作。首先我们要进行标准齐次除法(homogeneous division),也被称为透视除法,实际上就是用坐标的w分量去除x,y,z。对于正交投影来说,实际上没有变化。
经过透视除法后,我们就得到了归一化的设备坐标(Normalized Device Coordinates, NDC)。这个立方体的x轴和y轴都很好确定,就是[−1,1],但z轴不同的API会有不同的范围。例如OpenGL中z轴的范围是[-1, 1],但DirectX中z轴的范围是[0, 1]。
诶此时我们上面推导的正交投影矩阵似乎就不适用了,但其实变换一下非常简单。可以先对z轴除以2,范围变成了[-0.5, 0.5],再加个平移就得到[0,1]。此时我们得到矩阵,将上面的正交投影公式再去左乘下面这个矩阵即可:
Mst=10000100000.50000.51
得到的DirectX的正交投影矩阵如下:
Mo[0,1]=r−l20000t−b20000f−n10−r−lr+l−t−bt+b−f−nn1
别忘了目的是为了裁剪,那么对于所有片元来说,坐标转换后不在这个NDC范围内的就可以剔除掉了。
透视投影
参考Real-time Rendering第四章的推导 fov与aspect radio的形式参考unity shader入门精要

透视投影的整个过程,可以理解为首先按一定的规则将视锥体缩放成一个长方体,长方体再使用正交投影的方式,就可以得到NDC。

我发现透视投影矩阵仍然有多种形式相当麻烦,其实找对方法理解它们还是比较简单的。(如果有错误请指出)
最核心永远不变的,就是将视锥体压缩成长方体的矩阵(它的推导很多地方都有了),我也感觉这是透视实现的核心,离摄像机越远的物体在x和y轴上压缩的越厉害,就形成了近大远小的效果。
Mpers→orth=n0000n0000n+f100−fn0
下面再应用正交投影矩阵就可以得到观察空间应用透视投影到裁剪空间:
Mpers=MorthMpers→orth=r−l2n0000t−b2n00−r−lr+l−t−bt+bf−nf+n100−f−n2fn0
但是有时候我们会经常看到另一种形式,最明显的特征是第四行第三列是-1。当初这个问题还困扰了我挺久的,其实如果解决了正交投影的z轴反转(也就是从右手系变为左手系,深度值越大离相机越远),这个问题也就很好解决了。也就是说跟正交投影矩阵一样,透视投影矩阵也有将z轴反转(深度值越大离相机越远)的形式:
Mpers=r−l2n′0000t−b2n′00r−lr+lt−bt+b−f′−n′f′+n′−100−f′−n′2f′n′0
此时,n′f′的含义是距离摄像机的距离,0<n′<f′。
在DirectX中,投影后z轴不会被映射到[-1, 1]而是[0, 1]上(跟正交矩阵一样),并且DirectX在观察空间和裁剪空间都是左手系,一直看向z正轴,其n和f值是正数:
Mper[0,1]=r−l2n′0000t−b2n′00−r−lr+l−t−bt+bf′−n′f′100−f′−n′f′n′0
同样地,单纯使用坐标来定义它似乎有些复杂,但不论如何,它与摄像机的位置绝对是有很大关系的。

我们通常通过摄像机,并定义4个参数来确定一个视锥体:
- 摄像机张开的角度,称为FOV(Field of View),图中是一个Y轴方向的FOV;横向FOV在3D游戏尤其是第一人称游戏中很常见
- 摄像机与近裁剪平面的距离Near
- 摄像机与远裁剪平面的距离Far
- 简单来说,近裁剪平面的宽高比aspect(包括Far在内的这些平面的aspect其实是一样的)
对于右手系来说,这个透视投影矩阵如下:
Mpersp = Aspect cot2FOV0000cot2 FOV 0000− Far − Near Far + Near −100− Far − Near 2⋅ Near ⋅ Far 0
经过投影变换后,视锥体的变化

应用齐次除法后,就会变为熟悉的NDC的形式(也就是我们最开始给出六元组形式透视投影矩阵时直接得到的结果。)其实在应用齐次除法之前,这种形式也可以进行裁剪,满足:
−w⩽x⩽w−w⩽x⩽y−w≤z⩽w
条件的都在视锥体内。

投影变换的目的仍然是方面裁剪,那么对于所有片元来说,坐标转换后不在这个NDC范围内的就可以剔除掉了。
屏幕空间
屏幕空间主要进行屏幕映射(screen mapping)。其中x和y是屏幕坐标,与z坐标一起,被称作窗口坐标。z坐标值不需要处理。主要是将NDC的x坐标和y坐标映射到一个窗口中,假设窗口左下角为(x1,y1),右上角为(x2,y2)。

那很容易想象,这其实就是个拉伸的过程。
xs=2(x+1)×(x2−x1)+x1ys=2(y+1)×(y2−y1)+y1
注意要先将NDC的x和y从[-1, 1]转换到[0, 1]。
至此,从模型变换到屏幕空间,实现了将场景合理地投影到了2D画面上。
References
- 《Unity Shader 入门精要》
- Real-Time Rendering 4th edition
- 虎书 Fundamentals of computer graphics