1.Viewport Transformation视口变换:
1.1Canonical Cube
之前我们通过MVP矩阵把物体坐标变换到正方体中(每个顶点的x,y,z坐标都应该在-1.0到1.0之间)也被称为裁剪空间clip space,接下来我们需要将该空间映射到屏幕中
描述观察位置的视角范围可以用FOVY、FOYX来表示,又因该位置到near plane的距离|n|和aspect ratio宽 / 高比是已知量,所以FOVY和FOYX只需要知道其中一个就可以根据公式推得其他信息(注:r、t分别是是右、上端到平面原点的距离)
1.2 Screen Space
既然要画在屏幕上,那先要给屏幕一个定义。对于图形学来说就是一个二维数组,数组中的每个元素都是一个像素,1920×1080是代表有那么多个像素点
目前认为一个像素内不会出现第二种颜色,每一种颜色可以用RGB三个量表示,可以认为像素已经是最小单位了在里面不会有任何变化(当然这是不对的,且实际红蓝绿中绿色会偏多一点因为人眼对其最敏感)
左下角是原点,虎书中左上角是原点,根据坐标系的不同y做坐标变换就行
1.3 Canonical Cube to Screen
中心本来在屏幕空间中心,但由于我们定义屏幕空间的原点在左下角因此要平移。不考虑Z坐标
这个变化就称为Viewport Transformation视口变换:裁剪空间变成屏幕空间
2.Raterization 光栅化
空间中的立体图形在经过MVP和视口变换屏幕空间中描述的一堆三角形,如图中的老虎,可以看到屏幕上有各种多边形,但这样还不够,还需要进一步将三角形打散成像素点,告诉成像设备每个像素的值应该是什么,将几何信息转换成一个个的栅格组成的图像(屏幕像素)的过程。这一步的过程就是光栅化。raster在德语中就是屏幕的意思,变成动词,就是“把东西画在屏幕上”即rasterization:Drawing to Raster Display
2.1 为什么选择三角形
为什么使用三角形作为基本形状单元(Fundamental Shape Primitives)呢
- 最基础的多边形:没有比三角学边数更低的多边形了,任何多边形都可以拆成三角形,这就好比你定义了一个基向量,就可以表示任意坐标了。
- 独特性质:
- 三角形内部一定是个平面,在三角形内部给定三个点连成一个三角形一定是一个平面。
- 三角形内外定义非常清晰,可以通过向量叉乘来判断点是否在三角形内部。凹多边形的·内外就不好界定
- 定义良好的三角形顶点插值方法,实现颜色的平滑过渡。
下一步需要将三角形光栅化,采样是实现光栅化的简单的一种方式
2.2 Sampling采样
所谓采样就是一个连续的函数f(x)在不同x值拿到这个函数的值是多少,相当于把一个函数离散化的过程。采样在图形学说是非常重要的概念,会有各种各样的采样,这里说的采样是指利用像素的中心对屏幕空间进行一个采样。在经过MVP和视口变换后,就可以拿到每一个顶点在屏幕空间下的坐标。
此外还可以采样时间、位置、法线,反射光等...
2.2.1 三角形采样
在光栅化阶段,GPU会依次处理每个像素,应该显示哪个三角面的数据,如果不在就显示默认色,如果在就采样三角形对应的纹理色值。判断像素和三角形之间的关系,才能决定哪个像素该不该亮,亮什么颜色,更确切地说是判断像素中心点和三角形的位置关系
定义一个二进制函数inside(t,x,y)其作用是输入屏幕空间中的任意一个点Point(x,y),判断这个点是否在三角形内部,如果在三角形内返回1,不在则返回0。
- 求Inside函数 - 通过叉积的意义
p1p2叉乘p1Q可以知道Q在p1p2的左侧还是右侧
选定一个方向构建向量,如P1P2,P2P3,P3P1,这样就不会乱。
- 如果点在两个三角形的公共边上:
要么不处理,要么规定如何处理。我们不处理
但在openGL和d无序列表irectX,规定像素中心落在三角形上面和左边都认为在三角形内,如果落在右边和下边就不算
2.2.2 优化采样逻辑
但为了一小块三角形就对函数进行采样从而对屏幕空间上所有点进行计算真的有必要吗?
AABB包围盒
根据三角形三个顶点的位置确定一个包围盒,只对AABB包围盒(Axis-Aligned Bounding Box)内的像素点进行采样
逐行检测
取含三角形点的每一行的最左和最右,等价于每一行找了一个包围盒。
适合窄长又旋转的三角形。对于这种情况,使用AABB包围盒检测方式有些浪费,这时候可以进行逐行检测,就是从三角形左下角开始往右检测,检测完一行再检测上一行,直到检测完最上一行。
2.2.3 总结与问题
Summery
我们认为像素是有均匀颜色的小方块,将像素的中心应用采样 - inside函数离散化,来检测像素是否在三角形内,如果在就将像素涂红否则为白
Sampling Artifacts (Errors / Mistakes / Inaccuracies) in Computer Graphics
采样的频率低于信号的频率即采样的速度跟不上信号的速度会产生Jaggis锯齿(学名是走样),产生的原因从图中可知是因为像素本身有大小。因此下面我们要抗锯齿!反走样!
3. Antialiasing 抗锯齿
3.1 Frequency domain 频域
傅里叶级数展开
任何一个周期函数都可以写成一系列正弦和余弦函数和一个常数项的和,这些正弦波和余弦波有着各自的频率,A是振幅。
函数本身有一个频率,采样也应该有一个频率,由图可知我们可以用高频率的采样频率恢复出高频率的函数,因此当采样频率跟不上信号的频率时就会丢失很多原信号的信息,如f5。这样一来这里就解释了之前讲的走样本质是信号变化太快导致采样跟不上这句话。
可以看到由于采样频率过低导致两个不同频率信号的采样结果是完全相同的,即在两个不同频率在频域上太过相近,这便是走样正规的定义
先对图像进行模糊化(利用傅里叶级数变换)把结果的的高频信息拿掉(滤波),然后再做采样,实质上是降低的信号的速度。
效果:
PS:如果先采样后进行模糊化是错误的,这种操作相当于信号频率不变,采样频率变更慢
3.2 Filter 滤波
去除特定频率的信息,频率有可以视为卷积、平均 - 模糊
在图像处理中,频率是指图像中像素值变化的速度或图案变化的速率,它衡量的是单位空间内亮度变化的程度
去除特定频段的信号,傅里叶变换可以把一个函数从时域表示变为频域表示。对一个图片进行傅里叶变换,将时域中某一刻的所有信息转化为频域 :从中间向外频率不断变高,在不同频率的位置上有多少信息,用亮度表示。此例中中心亮度很高可以看出信息主要集中在低频,高频信息相比低频信息少了很多,事实上对自然的图片来说大部分都是如此
水平和竖直分割线产生原因:
分析信号时默认信号是周期性重复信号,但对于不重复周期信号比如比如图片,我们就认为到右边界之后重复左边界的内容(如果时函数就进行左延拓和右延拓)。就好比水平和竖直方向都重复放置了很多图片。而很少有图片左边界与有边界是一致的,这就导致在图片右到左的过程中,在这条边界上会发生剧烈的信号变化,也就会产生一个及其高的高频,这就是做傅里叶变换会看到两条线
把声音波形文件切一刀,比如之前完全静音,突然间有声音,就会有一声嚓的噪声。 做过剪辑的人应该都有这个体会。 现在想来,这段极短的阶跃如果编辑傅立叶变换的话,囊括了非常宽的频域,所以这也是变成噪声的原因吧……
进行操作,用filter对图像的低频信息进行过滤
其实这里我有一个疑问,很明显这是一个极坐标 / 复平面的坐标空间,那么亮度表示信号的振幅(强度),r - 距原点的距离表示频率的高低,由低到高,那这个图里的角度代表什么信息?
可以发现高频信号表示了图像的边界,图像信息变化快的地方频率就高,比如团的边界,纹理,褶皱。只留下低频信息
所以去掉高频可以图像的边界就变得模糊了
图中的水波纹是由于不完美的低通滤波造成的
可知内部细节越多,频率就越应该保留低频部分。中通滤波器保留的是图像中间的部分
convolution 卷积(滤波 = 卷积)
实现卷积的两种方法
方法一
- 直接用滤波器对图进行卷积操作
方法二
- 先用傅里叶变换将图变换到频域上,卷积的滤波器也变换到频域上
- 将两者相乘,得到频域的结果
- 再把结果逆傅里叶变换到时域上
下图为两种方法实现低通滤波。
在时域是在做卷积操作,但在频域是在做点乘操作,背后的原理:
卷积定理:两个信号(连续不连续都可)时域卷积(乘积)= 频域乘积(卷积)
4. 反走样 Antialiasing
4.1 采样稀疏造成走样的原因
想了半天想通了,时域上采样频率是指周期小,转化到频域f = 2Π /T,T越小,f不就越大,即采样的f之间距离变大不就不会重合了嘛。所以是反过来的
从频率的角度上,采样如果较为稀疏,在该信号的频谱重复时会发生混叠,就会发生走样现象
采样定理,如果采样频率 fsfs(其倒数为采样周期 TT)大于信号中的最高频率成分的两倍(即 fs>2Bfs>2B,其中 BB 是信号的带宽),则原始信号的信息可以从其采样值中完全恢复。
4.2 如何减少反走样现象
方法一:增加采样率 sampling rate本质上增加了采样时频谱中副本之间的距离
使用更高分辨率的显示器、传感器、更高规格的帧缓冲区
方法二:反走样 Antialiasing
,即在采样前滤除原始信号的高频信息(注意图像中高频意味变化快的部分),在采样重复频谱前使其中的副本内容”更窄“,让不容易重叠
通过低通滤波器来实现这一点,低通滤波器是(kernal),将其与每一个像素点进行卷积以实现平滑效果。 最简单的kernal就是一个1×1大小的,对每单个像素点filter - Convolution - averaging(smooth)
但我们要如何知道有颜色部分到底占有像素的百分比呢?一种很笨的方法是超采样
4.3 Antialiasing By Supersampling (MSAA)
我们认为把一个像素可以再细分为很多个次像素,判断这些小像素是否在三角形内,然后再把判断的结果进行平均,就能得到一个近似结果。如下图所示,每个像素被再细分为2*2的次像素,根据三角形与每4个小像素的覆盖情况,可以得到0、25%、50%、75%、1的平均,以上是模糊操作。然后才是采样,所以MSAA并没有提高分辨率(别看似乎将1分为4),只是增加采样点来近似覆盖率
4.4 其它方法
- FXAA(Fast Approximate AA),生成有锯齿的图片之后,对锯齿进行处理,替换成没有锯齿的边界
- TAA(Temporal AA),将上一帧感知到的结果应用到当前帧,相当于把MSAA对应的这些样本分布在时间上,并且在当前帧没有引入任何额外的操作。
5.超分辨率
可以理解为2k的片源放到4k的显示器上,将其恢复成更高分辨率从低分辨率到高分辨率本质还是采样率不足的问题对于高分辨率下未知的像素点,使用深度学习的方法进行猜测,DLSS (Deep Learning Super Sampling)
6. Visibility / occlusion可见度/遮挡
经过上面的步骤我们已经可以把一个三角形画在屏幕上了,但是通常一个场景里会有很多个物体并且会有互相遮挡的关系,那么我们要怎么确认他们的遮挡关系呢
6.1 画家算法 Painter’s Algorithm
- 由远到近绘制物体,先绘制完远处物体接着再绘制近处的物体覆盖掉之前的物体 Inspired by how painters paint, paint from back to front, overwrite in the framebuffer
- 一定程度上可行,排序n个三角形的时间复杂度为O(nlogn) 基于比较的排序最快就是这个复杂度 Requires sorting in depth O(nlogn) for n triangles)
- 有互相遮挡关系时,无法定义物体间的深度关系,画家算法无法解决 Can have unresolvable depth orde
6.2 深度缓冲 Z-Buffer
所以我们不再针对物体而是每一个像素
- 存储当前每个采样点(像素)的距离相机最近距离的深度信息 Store current min. z-value for each sample (pixel)
- 每个像素最后输出的颜色信息储存为帧缓冲,深度信息储存为深度缓冲 Frame buffer stores color values, depth buffer (z-buffer) stores depth
这两张图是同步生成的,深度缓存中越近颜色越深(黑白是一维的,所以用黑白来代替强度,在图形学里非常常见)
6.3 深度缓冲的算法 Z-Buffer Algorithm
之前视图转换时相机是朝Z的负方向看的,以下为了简化,深度值z永远是正的,越小表示越近,越大表示越远
将深度缓冲中全部每个像素(采样点)的深度值初始化为无限远,在每个三角形光栅化时,将它的每个像素的深度值和深度缓冲进行比较,如果该像素的深度值小于深度缓冲,则更新深度缓冲,否则舍弃
for (each triangle T) for (each sample (x,y,z) in T) if (z < zbuffer[x,y]) // closest sample so far framebuffer[x,y] = rgb; // update color zbuffer[x,y] = z; // update depth else ; // do nothing, this sample is occluded
- 排序n个三角形(假设覆盖常数个像素,没有特别大或小的三角形)的时间复杂度为 O(n) for n triangles (assuming constant coverage),此处不是排序而是比较取最小值
- 三角形的进入深度缓冲的顺序和结果无关(优点)
- 深度缓冲算法是一个最重要的算法,在几乎所有的GPU硬件中都有应用 Most important visibility algorithm, implemented in hardware for all GPUs
- Z-Buffer不能处理透明物体,透明物体需要特殊处理
- 如果是MSAA这种处理方法,可能不是对每个像素点而是对每个采样点应用深度缓存Z - buffer