软光栅从零开始——MVP变换

前言

在上一篇中,我们以正交投影的方式学习了如何绘制三角形,但在生活中我们眼睛看到的现象用正交投影无法解释。比如如果是正交投影,我们看到的铁道两个铁轨在视角上并不会交于一点,也就是两个铁轨的间距并不会随着离我们的位置越远,而变小,实际上这正是透视投影。因此,对于渲染摄像机理解为我们的眼睛,用透视投影来表示;而正交投影则主要用于主要用于二维渲染以及建筑工程软件

image-20221108191306381

如下右边是正交投影,左边是透视投影

img

2d变换

​ 开始之前,需要提醒一下,以下向量表示都是列向量

缩放 沿主轴缩放

image-20221108193740350

反射 2维中围绕直线翻转对象

image-20221108193808000

错切(剪切) 不均匀地拉伸坐标空间。思路是将一个坐标的倍数添加到另一个坐标上

image-20221108194128219

旋转 二维中围绕一点旋转

image-20221108194230470

平移 前面这几种变化都是线性变换,因为它们是原点不会改变的变换。对于平移来说,他的原点改变了,因此无法用线性变换(矩阵)来表示平移

image-20221108194736562

image-20221108194754378

可以看到这样对于平移我们需要在线性变换的基础上,在后面跟上个加法表示位移,但这样表示并不方便,因此我们需要想办法将其用一个矩阵来表示。方法是引入齐次坐标w,也就是说多加一个坐标
注解:我们如何理解齐次坐标的含义呢?w是摄像机离2维平面的距离。想象一下,我们有一台摄影机将画面投射到一个屏幕上,我们移动摄像机,离屏幕越近,w越小,画面肯定会被压缩,这样这些画面上的物体的坐标是不是缩小了,离屏幕越远,w放大,画面被放大,画面上的物体的坐标肯定是放大了,可以看出齐次坐标控制着生成2维图像的大小

如何理解point添加的是1,vector是0呢?因为向量只描述方向和大小,与位置毫无关联,所以对于向量来说,平移是毫无意义的,因此为0。又或者点减点表示一个向量。点加上点表示中点

image-20221108195514949

image-20221108200717673

变为齐次坐标后来表示平移的矩阵

image-20221108195626130

仿射变换 线性变化 + 平移。缩放旋转平移组合在一起。值得注意的是,矩阵乘法并没有交换律,因此需要按照顺序,从右到左

image-20221108201349587

3d变换

齐次坐标 同样是多加一个数

image-20221108201936591

缩放

image-20221108202318702

平移

image-20221108202332676

旋转 围绕一个主轴旋转,根据左右坐标系,分别伸出左手或右手,大拇指方向指向旋转轴的正方向,四指弯曲方向为旋转方向

image-20221108202533582

仿射变换

image-20221108202933198

值得注意的是,旋转矩阵、反射矩阵都有正交变换,且矩阵乘以其逆矩阵是单位矩阵,正交矩阵乘以转置矩阵是单位矩阵,因此我们若要求这些矩阵的逆矩阵,只需求其转置矩阵。这有什么用呢?逆矩阵运算量大,而转置十分简单,避免了昂贵的计算成本

坐标空间与空间变换

​ 我们已经学会如何用矩阵对顶点进行变换,现在我们将他应用在渲染中。所谓渲染的艺术,就是将一个3d场景转变为2d图形,这一变化如何实现呢?接下来我们将进一步探讨各个坐标空间

​ 首先我们来看渲染需要经过哪些坐标系统:

  1. 局部空间
  2. 世界空间
  3. 观察空间
  4. 裁剪空间
  5. 屏幕空间

从局部空间转变到世界空间被称为Model矩阵,;从世界空间转变到观察空间称为View矩阵;从观察空间转变到裁剪空间称为projection矩阵;这就是我们常说的MVP矩阵

局部空间、世界空间 试想一下,许多人做一个大项目,我们并不可能直接在整个项目中直接进行创作,否则很有可能一不小心破防场景中的其他物体,而是将其拆分成一个个子项目,待大功告成后,才会将它们合并在一个整体的大项目中。因此我们定义,局部坐标系是一种以目标物体的中心为原点,且坐标轴与该物体对齐的简单便用的坐标系。只要在局部空间中定义3D模型的各顶点,我们就可以将其变换至全局场景(世界空间)中;而世界空间则是一个宏观的特殊坐标系,其代表了我们关心的最大坐标系,我们合并的一个整体的项目就在世界空间中

局部空间的优点:

  • 易于使用。物体的中心通常位于局部空间的原点,且关于主轴对称
  • 物体可以横跨多个场景且重复使用
  • 我们很可能需要在一个场景中绘制同一个但位置、方向、大小不同的物体,如果每次创建物体实例时都要复制其顶点和索引,那将消耗大量资源。因此,通常的做法是存储一个几何体,按照世界矩阵来指定物体在世界空间中的位置、方向、大小

从局部空间变换到世界空间需要经历平移缩放旋转三个步骤

观察空间 相当于给人拍照,摆好姿势,摄像机调好位置和朝向。摄像机也就是观察空间,而调整摄影机的过程,就是观察矩阵。观察空间又被称作视图空间、视觉空间

从我们描述的观察空间可得出,观察空间需要定义三个向量:

  • 相机位置(Position) $$\vec{e}$$
  • 观察方向(Look-at / gaze direction) $$\vec{g}$$
  • 世界空间中向上方向的向量(Up direction)$$\vec{t}$$

我们规定变换后的相机在原点,看向-z方向,t为y正半轴

image-20221108211200984

View矩阵 从世界空间变化到观察空间,它们大小是相同,我们只需变换位置和朝向。我们需要(平移)将相机位置移回世界坐标原点,(旋转)观察方向朝向-Z轴,向上方向为y轴正半轴,最后平移回去。这时会发现将相机坐标轴旋转到世界坐标轴很复杂,但是从世界坐标轴旋转到相机坐标轴很简单,因此我们只需要求逆,又因为旋转是正交矩阵,因此我们只需要求其转置即可

img

image-20221108212729126

但其实从相机坐标轴旋转到世界坐标轴一步也可以完成,在这里我们需要先了解坐标变换:不同坐标系下的坐标转换

我们先来看向量坐标变换

现有坐标系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轴正方向上的单位向量

image-20221109192714293

点的坐标变换

点的坐标变换与向量稍有不同,这是因为位置是点的一个重要属性。如下图所示,我可以求得在坐标系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中的坐标

image-20221109213220889

因此从局部空间到世界空间得变换矩阵如下,这个矩阵对之前局部到世界同样适用

image-20221109222130184

而从世界空间到观察空间如下,也就是求逆
image-20221109214036039

裁剪空间 对着人拍照,按下快门,将三维的场景变成了2维图片,同时摄影机屏幕外的部分不要。这一投影过程用到的就是透视投影矩阵,摄影机屏幕也就是裁剪空间。为什么叫裁剪空间呢?因为我们期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪。被裁剪的坐标就会被忽略,不纳入计算,这部分最终也看不到,所以剩下的坐标就将变为屏幕上可见的片段。为了将顶点坐标从观察空间变换到裁剪空间,我们需要定义一个投影矩阵,它指定了一个范围内的坐标,如[-100, 100],接着投影矩阵便会将这个范围内的坐标变换为标准化设备坐标[-1.0, 1.0],这被称为视口变化(因为将所有坐标都指定在[-1.0,1.0]内不是很直观,所以我们指定自己的坐标集并将它变换回标准化设备坐标系),因此投影就是将特定范围内的坐标转化到标准化设备坐标系的过程;我们前面交过齐次坐标的含义,因此可以得出透视是通过改变w的分量实现的.那么是怎样实现的呢?一个物体离相机越远,也就是z越大,那么向量会变小,没错是通过z来影响w的

​ 正交投影的过程很简单,坐标的相对位置都不会改变,我们只需要将物体变换到[-1.0,1.0]的范围即可,过程就是平移至原点,缩放,最后平移回去,不过创建一个正射投影矩阵需要指定可见平截头体的宽、高和长度

image-20221113153218510

image-20221113153742351

image-20221113154217208

​ 由于这里渲染器主要用透视投影,因此我们重点讲解透视投影,下图中左边是透视投影,右边是正交投影

image-20221110191857332

​ 开头我们讲过,透视投影类似人眼,看到的方式是近大远小,随后我们,除此以外它还包含一个组成要素摄像机可观察到的空间体积,这个空间体积的范围我们用一个四棱锥截取的平截头体来表示,也就是上图中的Frustum,我们将顶点到观察点的连线称为顶点的投影线,透视投影的步骤是将远平面拉成和近平面一样的大小,组成一个方体,再进行正交投影

image-20221110192420398

​ 在观察空间中,近平面n是已知的,远平面f是未知的,观察点为原点,z = -n为投影平面,我们约定经过透视投影后近平面的所有点(x,y)不会改变,远平面上的点的z值不变,根据相似三角形的原理可以求得x'和y'

image-20221110192821108

image-20221110230935872

我们需要求得一个矩阵完成以下变换,因为z是未知的,所以是Unknown。我们可以看到w为z,这印证了我们之前说的z的值在影响w的

image-20221110192951241

image-20221110193129288

转换成矩阵形式如下,但我们并不知道第三行

image-20221110193343398

如何求得?很简单,现在我们先考虑近平面,近平面上的任何一个点进行透视投影后必定不会改变,用n取代z,结果如下

image-20221110231204236

又因为之前求出的结果前两项是nx和ny,因此我们可以确定unknow这一行前两个位置肯定都是0,然后我们将后面两个位置设为A和B,结果如下

image-20221110231449176

不难推出

image-20221110233031511

在用远平面f取代z,推出如下

image-20221110233057797

求解

image-20221110233726994

最后只需要再进行正交投影即可求得结果

最终矩阵如下

image-20221113163220561

视口变换 将[-1,1]的立方体变换到[0,width]和[0,height],因为这是2d平面,因此我们只需要计算x和y.因为标准立方体中心在原点,而屏幕原点在左下角,所以还需要经过一个平移使得原点坐标对齐

image-20221114190956601

实现

//观测矩阵
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

posted @ 2022-11-14 19:23  爱莉希雅  阅读(165)  评论(0编辑  收藏  举报