1. 概述
GAMES101: 现代计算机图形学入门
链接:GAMES101
1.1 线性代数必备知识
1.1.1 向量
- 向量的计算
- 单位向量,向量的模
- 向量的加法
1.1.2 点乘
- 求取两个向量的夹角:先单位化再点乘再反求ACOS即为夹角。
- 用于投影的计算,如分解。
- 判断两向量方向的关系:同侧、反侧、垂直,接近1则越接近。
1.1.3 叉乘
- 始终规定右手螺旋定则
- 判断多点位置的关系:在内部、在外部,规定点逆时针,对判断点做叉乘,都为同一正负即判断点在规定点内部。
- 相同向量叉乘为零向量。
2. 变换
变换直接通过矩阵的乘法实现。
2.1 变换的实现
2.1.1 放缩矩阵
2.1.2 倾斜矩阵
2.1.3 旋转矩阵
2维的旋转矩阵
3维的旋转矩阵
2.1.4 矩阵的齐次式
为了用一个矩阵清晰地又表示平移又表示变换,我们使用增加一个维度的方法,表示做出的平移变换。
我们规定 点:增加的一维永远是1,向量:增加的一维永远是0
注意:在实际中我们通常先平移,再变换。
2.2 视图变换
视图变换,即相机的变换,把相机移动到规定方向和规定位置的过程。首先规定相机默认位置在原点,默认的向上方向为+Y轴,永远朝向-Z轴的方向。
2.2.1 把相机移动到原点
- position:相机位置e
- look-at direccction:相机的朝向g
- up direcction:向上方向t
2.2.2 把相机转正
- 第一步:平移,很简单
- 第二步:旋转,看做是(x,y,-z)轴转向相机的(g,t)轴,即x轴(1, 0, 0)转向g叉乘t的向量,那么矩阵第一列为g叉乘t的向量,同理可得到剩下的列向量。再对得到的矩阵求逆,而旋转矩阵是正交矩阵,直接转置得到最后结果。
2.3 投影变换
投影变换的过程就是把在一定空间的物体显示在平面上。
投影变换分为正交投影和透视投影。
2.3.1 正交投影
- 从理解上来讲,就是把z轴丢掉
- 先平移
- 再将生成的矩形平移并缩放为[-1, 1]
注意:此处为n-f,是因为我们用右手系,近是大于远的。
2.3.2 透视投影
透视投影其实就是根据z轴距离来做的比例变化。
特殊点的规定:
(1)近平面上的任意一点永远不变。
(2)远平面上的z轴在挤压过程中不会变化
(3)远平面的中心点挤压后不会变化。
注意:对于任何一个深度的z轴,我们都能得到相似三角形的比例变化,即控件内部任何一个点被压到n平面所做的x, y轴变化。
至此,我们已经得到了变换矩阵的一部分。
第一,我们利用近平面的规定,点在近平面不会发生变化,因此我们能得到在近平面上变化后的具体点。
容易观察出z轴的变化一定与x,y轴的值无关,因此变换矩阵的第三行一定是(0 0 A B),A、B的数值未知。
第二,我们利用远平面的中心点的规定,远平面的中心点不会发生变化,我们得到另一组方程。
注意:我们通过两个特殊情况的方程,两个未知数,解出变换矩阵的值。
因此我们得到了压缩变换矩阵,我们做完压缩后,再做正交投影,最终得到透视投影矩阵。
注意:中间的某一个点,在通过挤压后,z轴会变大,即远离视点。具体思路是我们做个z的变换函数,求导即可。
3. 光栅化
经过变换后我们得到了[-1, 1]的立方体,我们需要将立方体画到屏幕上。
屏幕:(1)是一个二维数组;(2)每个元素都是一个像素;(3)一种经典的光栅成像设备。
光栅化:把东西画到屏幕上。
像素:由rgb混合而成。
屏幕空间:(1)像素的范围从(0,0)到(宽度-1,高度-1);(2)以像素块的左下角为坐标;(3)像素的中心在(x+0.5,y+0.5)。
3.1 将[-1, 1]变换到屏幕空间上
我们直接不管z轴,很容易做出到屏幕空间上的变换矩阵。
我们得到了在屏幕空间上的多边形对应的位置,但是这不够,因为之前我们的变换是相对于原来的,现在我们需要相对于屏幕空间,在这些像素上确认我们的显示。所以我们需要把这些多边形打碎成很多三角形,把这些多边形画在屏幕上,这就是我们所说的光栅化。
3.2 采样
3.2.1 三角形的性质
(1)最基础的多边形,任何的多边形都能拆成三角形。
(2)三角形的内外部定义很清楚。
(3)在三角形内部的任意一点可以利用顶点来中心插值。
3.2.2 inside()函数
主要判断像素的中心点与三角形的关系,在三角形内部输出1,在三角形外输出0。
判断是否在三角形内部:利用向量的叉乘即可。
for(int x = 0; x < xmax; ++x)
{
for(int y = 0; y < ymax; ++y)
{
image[x][y] = inside(tri, x + 0.5, y + 0.5);
}
}
上述代码可以使用包围盒优化,只对x,y轴的最大最小所围成的矩形边界进行遍历。
for(int x = min{Px}; x < max{Px} + 1; ++x)
{
for(int y = min{Py}; y < max{Py} + 1; ++y)
{
image[x][y] = inside(tri, x + 0.5, y + 0.5);
}
}
3.2.3 采样带来的问题
- 锯齿(走样,Aliasing)
- 摩尔纹(去掉奇数行或奇数列)
出现问题的原因:信号变化的太快了导致采样跟不上变化的速度。
3.2.4 滤波
傅里叶级数展开:任何的函数都能展开成正弦余弦的加权和的形式。
傅里叶变换:将函数展开的各个频率的函数拿出来。
注意:对于函数本身有一定的频率,对于采样也有一定的频率,因此是采样的频率对于原有函数的频率不一致而导致的问题。
上图,对于蓝线函数和黑线函数,我们用同样的采样方法无法区分,这就叫走样。
滤波:去掉特定的频率。
图片中心亮,是因为信息都基本集中在低频。
(1)我们过滤掉低频信息,只保留高频信息,图片只会显示边界,称为高通滤波。
(2)我们过滤掉高频信息,只保留低频信息,画面会变模糊,即看不见边界,称为低通滤波。
3.2.5 锯齿的解决方案
锯齿产生的主要原因是因为采样的频率跟不上纹理的频率。
使用低通滤波的实现,我们可以通过卷积的方式(平均),因为卷积的矩阵只有低频,所以乘积后得到的就是低通滤波。
因此我们解决锯齿的方法,需要先模糊,再采样。
3.2.6 现代抗锯齿方法
(1)MSAA(MultiSample Anti-Aliasing)
利用更多的采样点进行反走样,将一个像素内部分成很多个采样点,并将采样结果在像素点内部进行平均。
注意:MSAA并没有提高分辨率,只是发生在采样之前,以细分像素的方式将每个像素求平均,因此MSAA就是模糊操作。MSAA之后再进行采样,采样的就是平均后的像素颜色。
(2)FXAA(Fast Approximate AA)
进行图像的后期处理,即在采样之后进行的操作。
输入一张有锯齿的图片,通过方法找到锯齿的边界,并将边界优化成没有锯齿的边界。
注意:这和我们先模糊再采样的解决思想相反。
(3)TAA(Temporal AA)
找上一帧的信息,用相同的点去感知不同时间的图像,再进行优化,即将原像素在时间上做平均。
(4)超分辨率
DLSS(Deep Learning Super Sampling):通过深度学习的方法,把低分辨率的拉成高分辨率的,现在的图像高清修复就是这样做的。
3.3 处理画面的远近
画家算法:先画距离远的再画距离近的。
注意:实际中不能用画家算法,因为画家算法难以确认谁在前谁在后。
Z-Buffer(深度缓冲):我们不用先画远的,而针对于每一个像素,去记录它最浅的结果。
(1)深度图:储存每个像素对应的最浅深度。
(2)结果图:储存最终的结果。
4. 着色
我们将图像画在了屏幕上,但是这与真实情况不同,因为色彩需要考虑明暗、高光等信息,这就是着色处理的问题。
着色定义:对不同物体应用不同的材质。
4.1 光照模型
Blinn-Phong反射模型:高光(Specular highlights)、漫反射(Diffuse reflection)、环境光(Ambient lighting)
规定:着色只考虑着色点的法线(n),点到光源的方向(l),点到观测点的方向(v),其它均不考虑,如阴影等。
4.1.1 漫反射
漫反射:光线打到着色点上,会均匀的反射到各个方向去。
首先,我们定义着色点接收的能量,我们将光线打在着色点上视为一种能量,我们分析该能量的大小,与光线方向与法线方向的夹角有关。夹角越小,那么该点接收到光的能量越大。
定义点光源的强度,在单位1距离上光强度为I,与距离的平方成反比。
我们将光到达了多少能量,并且该着色点接收了多少能量,两者结合,得出了漫反射的表达式。
注意:漫反射各个方面接收的结果应该是一样的,所以与观察方向无关。
漫反射系数越大,表示表面越不吸收能量,所以全部反射出去了,也就越亮。
4.1.2 高光
高光的方向可以理解为镜面反射的方向,因此观察方向越接近镜面反射方向,就越看得到高光。转化成半程向量(光照方向与观察方向的角平分线方向)与法线方向的夹角,这就不用算镜面反射方向了,直接转化成两个向量的加法。
4.1.3 环境光
我们大胆假设,任何一个点接收到来自环境的光永远相同,因此与观察方向、光照方向都无关,即环境光为常数。用来保证场景中没有地方是完全黑暗的。
4.1.4 模型的应用
我们将环境光、漫反射和高光全部叠加,最终即为着色的光照效果。
4.2 着色频率
- 以平面为单位着色
- 以顶点为单位着色
- 以像素为单位着色
注意:这与shader编程类似,只是以不同的单位进行着色。
- 如何求顶点的法线:根据该点的相邻面的法线,平均出该点的法线。
- 如何求面上某一像素的法线:根据面的顶点的法线,用中心插值的方法求出面上某一像素点的法线。
4.3 实时渲染管线(图形管线)
现在的实时渲染管线已经全部GPU内完成好了的,但也可以自定义编程,如顶点处理等。
- Shader 编程:此处不介绍…
- Shader在线编程网站:Shadertoy
4.4 纹理映射
4.4.1 纹理的定义
光照模型中都有光照的系数,而那个系数是根据物体上着色点的属性所决定的,因此我们需要每个点都有自己的基本属性,即纹理映射。注意,任何一个三维物体的表面是可以展开成二维的。
纹理:纹理就是一张uv坐标的图,首先我们不管怎么展成纹理uv坐标的,我们认为每个三维表面的任意点都在uv上对应。
注意:(1)纹理一般可以反复运用,无缝衔接。(2)定义的区别:纹理是希望着色的不同而定义的每个点的基本属性;材质就是着色方式的体现,材质的不同就是着色方式的不同。
4.4.2 重心插值
首先我们属性是定义在顶点上的,因此我们要把各个属性在三角形内部进行插值处理,如纹理坐标、颜色、法向量等。
重心坐标:在三角形内的任意一点都可以表示为三个顶点的线性组合。
注意:如果三个系数都为正,则该点一定在三角形内,重心坐标系数可以通过面积比计算出来。
我们利用向量的叉乘,很容易就能得到每个点的重心坐标。
我们把各个属性都可以用重心插值的方法在各个点上表示出来。
注意:对于投影重心插值无法直接运用,需要现在三维中插值出来,再投影。
4.4.3 纹理太小
纹理图如果很小,在很高分辨率的地方要显示出来,我们采用双线性插值的方法。
双线性插值:取周围的4个点进行插值。
双三线性插值:取周围的16个点进行插值。
4.4.4 纹理太大
纹理太大,会发生走样问题。
- Mipmap:范围查询,只能做近似的正方形的范围查询。
注意:(1)分辨率每减少一层,像素缩减至原来的1/4,也就是利用四个像素平均成一个像素。(2)mipmap存储会增加原来图像1/3的存储量。
我们把每一个像素的区域通过微分的方法近似成纹理坐标uv中的正方形区域。
如果近似出的正方形区域是1×1,mipmap就在原始的图上选取,如果正方形区域是4×4,mipmap就在level2的图上选取,因此是在logL层去选取该区域应该近似的颜色。
越远的地方,一个像素覆盖的纹理很多,所以正方形很大,需要在很高层去查询,越近的地方,一个像素就是一个像素,在原本图像去查询。因此把每一个像素区域都投影到uv坐标上,并且使用mipmap去查询。
层与层之间的三线性插值:在各层上都使用双线性插值,然后对现在的层进行线性插值。即计算小数层上的纹理。
mipmap的缺点:都是正方形的,因此矩形或者斜着的区域会过度模糊,如下图所示。
- 各向异性过滤
如果要查询矩形的纹理,mipmap就很困难了,因此要存储矩形的图形,也就是各向异性过滤。
各向异性过滤即考虑不同方向带来的问题,各向异性过滤的开销是原来的3倍,因此各向异性过滤只与显存有影响。
2X各向异性过滤:考虑各个方向只压缩1次。4X各向异性过滤:考虑各个方向压缩2次。
- EWA过滤
如果要解决斜着的矩阵,我们可以使用EWA过滤,使用圆形进行不断的查询。
4.5 纹理映射的应用
4.5.1 环境映射
Spherical Map:将环境光反射在球上,直接存储在球上。
Cube Map:将球分为六个面,存储在六个面上。
4.5.2 凹凸/法线贴图
通过凹凸贴图来算假的法线,即可达到从视觉上改变高度差的感觉。
对于二维来讲,我们先求切线(求导数),再求法线。对于实际当中,我们分别求出u、v方向的偏导,即可求出法线(梯度)。
4.5.3 位移贴图
位移贴图会真实改变模型的形状。
位移贴图需要模型足够细分,并且需要很高的采样。
在贴图过程中,改变纹理三角形细分的程度,可以通过DirectX的动态曲面细分来完成细分。
4.5.4 三维纹理
- 三维纹理定义了空间中任何一个点的纹理。
- 利用三维空间中的噪声函数来进行纹理映射,比如凹凸等都可以得到。
- 预先使用环境光遮蔽图计算模型阴影。