软光栅从零开始——MVP变换
前言
在上一篇中,我们以正交投影的方式学习了如何绘制三角形,但在生活中我们眼睛看到的现象用正交投影无法解释。比如如果是正交投影,我们看到的铁道两个铁轨在视角上并不会交于一点,也就是两个铁轨的间距并不会随着离我们的位置越远,而变小,实际上这正是透视投影。因此,对于渲染摄像机理解为我们的眼睛,用透视投影来表示;而正交投影则主要用于主要用于二维渲染以及建筑工程软件
如下右边是正交投影,左边是透视投影
2d变换
开始之前,需要提醒一下,以下向量表示都是列向量
缩放 沿主轴缩放
反射 2维中围绕直线翻转对象
错切(剪切) 不均匀地拉伸坐标空间。思路是将一个坐标的倍数添加到另一个坐标上
旋转 二维中围绕一点旋转
平移 前面这几种变化都是线性变换,因为它们是原点不会改变的变换。对于平移来说,他的原点改变了,因此无法用线性变换(矩阵)来表示平移
可以看到这样对于平移我们需要在线性变换的基础上,在后面跟上个加法表示位移,但这样表示并不方便,因此我们需要想办法将其用一个矩阵来表示。方法是引入齐次坐标w,也就是说多加一个坐标
注解:我们如何理解齐次坐标的含义呢?w是摄像机离2维平面的距离。想象一下,我们有一台摄影机将画面投射到一个屏幕上,我们移动摄像机,离屏幕越近,w越小,画面肯定会被压缩,这样这些画面上的物体的坐标是不是缩小了,离屏幕越远,w放大,画面被放大,画面上的物体的坐标肯定是放大了,可以看出齐次坐标控制着生成2维图像的大小
如何理解point添加的是1,vector是0呢?因为向量只描述方向和大小,与位置毫无关联,所以对于向量来说,平移是毫无意义的,因此为0。又或者点减点表示一个向量。点加上点表示中点
变为齐次坐标后来表示平移的矩阵
仿射变换 线性变化 + 平移。缩放旋转平移组合在一起。值得注意的是,矩阵乘法并没有交换律,因此需要按照顺序,从右到左
3d变换
齐次坐标 同样是多加一个数
缩放
平移
旋转 围绕一个主轴旋转,根据左右坐标系,分别伸出左手或右手,大拇指方向指向旋转轴的正方向,四指弯曲方向为旋转方向
仿射变换
值得注意的是,旋转矩阵、反射矩阵都有正交变换,且矩阵乘以其逆矩阵是单位矩阵,正交矩阵乘以转置矩阵是单位矩阵,因此我们若要求这些矩阵的逆矩阵,只需求其转置矩阵。这有什么用呢?逆矩阵运算量大,而转置十分简单,避免了昂贵的计算成本
坐标空间与空间变换
我们已经学会如何用矩阵对顶点进行变换,现在我们将他应用在渲染中。所谓渲染的艺术,就是将一个3d场景转变为2d图形,这一变化如何实现呢?接下来我们将进一步探讨各个坐标空间
首先我们来看渲染需要经过哪些坐标系统:
- 局部空间
- 世界空间
- 观察空间
- 裁剪空间
- 屏幕空间
从局部空间转变到世界空间被称为Model矩阵,;从世界空间转变到观察空间称为View矩阵;从观察空间转变到裁剪空间称为projection矩阵;这就是我们常说的MVP矩阵
局部空间、世界空间 试想一下,许多人做一个大项目,我们并不可能直接在整个项目中直接进行创作,否则很有可能一不小心破防场景中的其他物体,而是将其拆分成一个个子项目,待大功告成后,才会将它们合并在一个整体的大项目中。因此我们定义,局部坐标系是一种以目标物体的中心为原点,且坐标轴与该物体对齐的简单便用的坐标系。只要在局部空间中定义3D模型的各顶点,我们就可以将其变换至全局场景(世界空间)中;而世界空间则是一个宏观的特殊坐标系,其代表了我们关心的最大坐标系,我们合并的一个整体的项目就在世界空间中
局部空间的优点:
- 易于使用。物体的中心通常位于局部空间的原点,且关于主轴对称
- 物体可以横跨多个场景且重复使用
- 我们很可能需要在一个场景中绘制同一个但位置、方向、大小不同的物体,如果每次创建物体实例时都要复制其顶点和索引,那将消耗大量资源。因此,通常的做法是存储一个几何体,按照世界矩阵来指定物体在世界空间中的位置、方向、大小
从局部空间变换到世界空间需要经历平移缩放旋转三个步骤
观察空间 相当于给人拍照,摆好姿势,摄像机调好位置和朝向。摄像机也就是观察空间,而调整摄影机的过程,就是观察矩阵。观察空间又被称作视图空间、视觉空间
从我们描述的观察空间可得出,观察空间需要定义三个向量:
- 相机位置(Position) $$\vec{e}$$
- 观察方向(Look-at / gaze direction) $$\vec{g}$$
- 世界空间中向上方向的向量(Up direction)$$\vec{t}$$
我们规定变换后的相机在原点,看向-z方向,t为y正半轴
View矩阵 从世界空间变化到观察空间,它们大小是相同,我们只需变换位置和朝向。我们需要(平移)将相机位置移回世界坐标原点,(旋转)观察方向朝向-Z轴,向上方向为y轴正半轴,最后平移回去。这时会发现将相机坐标轴旋转到世界坐标轴很复杂,但是从世界坐标轴旋转到相机坐标轴很简单,因此我们只需要求逆,又因为旋转是正交矩阵,因此我们只需要求其转置即可
但其实从相机坐标轴旋转到世界坐标轴一步也可以完成,在这里我们需要先了解坐标变换:不同坐标系下的坐标转换
我们先来看向量坐标变换
现有坐标系A,在这个坐标系中有一向量v(x,y),现在我们想要把向量v转换到坐标系B中,我们设转换后的向量为v' = (x', y')。如下图中可得知,v = ux + vy,其中u和v分别是x轴和y轴的单位向量;我们再用坐标系B来表示v' = u\(b\)x + v\(b\)y,因此如果给定v = (x,y),也知道u向量和v向量在坐标系b中的坐标u\(b\) = (u\(x\),u\(y\)),v\(b\) = (v\(x\),v\(y\)),我们就可以求出v'了.推理到三维道理一样,v = (x,y,z),v' = u\(b\)x + v\(b\)y + w\(b\)z,u、v、w为坐标系A中x、y、z轴正方向上的单位向量
点的坐标变换
点的坐标变换与向量稍有不同,这是因为位置是点的一个重要属性。如下图所示,我可以求得在坐标系A中p\(A\) = ux + uy + Q\(A\),其中u和v分别是x轴和y轴的单位向量,Q\(A\)为坐标系A的原点,而我们在坐标系中表示则是p\(B\) = Q\(B\) + u\(b\)x + v\(b\)y,其中QB是坐标系A的原点在坐标系B的坐标,u\(b\)是u在坐标系B中的坐标,v\(b\)是v在坐标系B中的坐标
因此从局部空间到世界空间得变换矩阵如下,这个矩阵对之前局部到世界同样适用
而从世界空间到观察空间如下,也就是求逆
裁剪空间 对着人拍照,按下快门,将三维的场景变成了2维图片,同时摄影机屏幕外的部分不要。这一投影过程用到的就是透视投影矩阵,摄影机屏幕也就是裁剪空间。为什么叫裁剪空间呢?因为我们期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪。被裁剪的坐标就会被忽略,不纳入计算,这部分最终也看不到,所以剩下的坐标就将变为屏幕上可见的片段。为了将顶点坐标从观察空间变换到裁剪空间,我们需要定义一个投影矩阵,它指定了一个范围内的坐标,如[-100, 100],接着投影矩阵便会将这个范围内的坐标变换为标准化设备坐标[-1.0, 1.0],这被称为视口变化(因为将所有坐标都指定在[-1.0,1.0]内不是很直观,所以我们指定自己的坐标集并将它变换回标准化设备坐标系),因此投影就是将特定范围内的坐标转化到标准化设备坐标系的过程;我们前面交过齐次坐标的含义,因此可以得出透视是通过改变w的分量实现的.那么是怎样实现的呢?一个物体离相机越远,也就是z越大,那么向量会变小,没错是通过z来影响w的
正交投影的过程很简单,坐标的相对位置都不会改变,我们只需要将物体变换到[-1.0,1.0]的范围即可,过程就是平移至原点,缩放,最后平移回去,不过创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度
由于这里渲染器主要用透视投影,因此我们重点讲解透视投影,下图中左边是透视投影,右边是正交投影
开头我们讲过,透视投影类似人眼,看到的方式是近大远小,随后我们,除此以外它还包含一个组成要素摄像机可观察到的空间体积,这个空间体积的范围我们用一个四棱锥截取的平截头体来表示,也就是上图中的Frustum,我们将顶点到观察点的连线称为顶点的投影线,透视投影的步骤是将远平面拉成和近平面一样的大小,组成一个方体,再进行正交投影
在观察空间中,近平面n是已知的,远平面f是未知的,观察点为原点,z = -n为投影平面,我们约定经过透视投影后近平面的所有点(x,y)不会改变,远平面上的点的z值不变,根据相似三角形的原理可以求得x'和y'
我们需要求得一个矩阵完成以下变换,因为z是未知的,所以是Unknown。我们可以看到w为z,这印证了我们之前说的z的值在影响w的
即
转换成矩阵形式如下,但我们并不知道第三行
如何求得?很简单,现在我们先考虑近平面,近平面上的任何一个点进行透视投影后必定不会改变,用n取代z,结果如下
又因为之前求出的结果前两项是nx和ny,因此我们可以确定unknow这一行前两个位置肯定都是0,然后我们将后面两个位置设为A和B,结果如下
不难推出
在用远平面f取代z,推出如下
求解
最后只需要再进行正交投影即可求得结果
最终矩阵如下
视口变换 将[-1,1]的立方体变换到[0,width]和[0,height],因为这是2d平面,因此我们只需要计算x和y.因为标准立方体中心在原点,而屏幕原点在左下角,所以还需要经过一个平移使得原点坐标对齐
实现
//观测矩阵
mat4 MatrixLookAtLH( vec3 eyePosition, vec3 lookAt, vec3 up )
{
mat4 view = mat4::identity();
vec3 z = unit_vector( eyePosition - lookAt ); //因为是对物体进行旋转,所以是方向是到摄像机
vec3 x = unit_vector( cross( up, z ) );
vec3 y = unit_vector( cross( z, x ) );
mat4 translation = mat4::identity();
translation[0][3] = -eyePosition.x();
translation[1][3] = -eyePosition.y();
translation[2][3] = -eyePosition.z();
mat4 rotate = mat4::identity();
rotate[0][0] = x[0];
rotate[0][1] = x[1];
rotate[0][2] = x[2];
rotate[1][0] = y[0];
rotate[1][1] = y[1];
rotate[1][2] = y[2];
rotate[2][0] = z[0];
rotate[2][1] = z[1];
rotate[2][2] = z[2];
view = rotate * translation * view;
return view;
}
//正交矩阵
mat4 MatrixOrtho( float top, float bottom, float left, float right, float Nearz, float Farz )
{
mat4 ortho = mat4::identity();
ortho[0][0] = 2 / ( right - left );
ortho[1][1] = 2 / ( top - bottom );
ortho[2][2] = 2 / ( Farz - Nearz );
ortho[0][3] = -( right + left ) / ( right - left );
ortho[1][3] = -( top + bottom ) / ( top - bottom );
ortho[2][3] = -( Nearz + Farz ) / ( Nearz - Farz );
return ortho;
}
//透视投影
mat4 MatrixPerspectiveFovLH( float FovAngleY, float Aspect, float Nearz, float Farz )
{
mat4 m = mat4::identity();
FovAngleY = FovAngleY / 180.0 * PI;
m[0][0] = Nearz / 2 * ( Aspect * tan(FovAngleY) * Nearz ) ;
m[1][1] = Nearz / 2 * ( tan(FovAngleY) * Nearz );
m[2][2] = ( Nearz + Farz ) / ( Nearz - Farz );
m[2][3] = -2 * Nearz * Farz / ( Farz - Nearz );
m[3][2] = -1;
m[3][3] = 0;
return m;
}
//视口变换
mat4 viewPort( const int width, const int height )
{
mat4 m = mat4::identity();
m[0][0] = width / 2;
m[0][3] = width / 2;
m[1][1] = height / 2;
m[1][3] = height / 2;
return m;
}
//绕z轴旋转
mat4 mat4_rotate_z(float angle)
{
mat4 m = mat4::identity();
angle = angle / 180.0 * PI;
float c = cos(angle);
float s = sin(angle);
m[0][0] = c;
m[0][1] = -s;
m[1][0] = s;
m[1][1] = c;
return m;
}
//绕y轴旋转
mat4 mat4_rotate_y(float angle)
{
mat4 m = mat4::identity();
angle = angle / 180.0 * PI;
float c = cos(angle);
float s = sin(angle);
m[0][0] = c;
m[0][2] = s;
m[2][0] = -s;
m[2][2] = c;
return m;
}
//绕x轴旋转
mat4 mat4_rotate_x(float angle)
{
mat4 m = mat4::identity();
angle = angle / 180.0 * PI;
float c = cos(angle);
float s = sin(angle);
m[1][1] = c;
m[1][2] = -s;
m[2][1] = s;
m[2][2] = c;
return m;
}
//缩放
mat4 mat4_scale(float sx, float sy, float sz)
{
mat4 m = mat4::identity();
m[0][0] = sx;
m[1][1] = sy;
m[2][2] = sz;
return m;
}
//平移
mat4 mat4_translate(float tx, float ty, float tz)
{
mat4 m = mat4::identity();
m[0][3] = tx;
m[1][3] = ty;
m[2][3] = tz;
return m;
}
reference
Fundamentals of Computer Graphics 5th
GAMES101
Directx12