GAMES101 系列总结(二):光栅化

GAMES101 系列总结(一):线性代数与模型变换中我们讲了如何通过MVP矩阵将模型上的点坐标变为[1,1]3[-1,1]^3的一个立方体之中的坐标,这篇文章我们继续介绍,如何将这个立方体中的点绘制到屏幕上。

光栅化主要分为三个部分,首先是将所有的点拆分为一个个的三角形,这个过程叫做Triangles,在这个过程中,可能出现某些三角形覆盖的位置没法用像素来表示而导致的锯齿,所以我们要做抗锯齿,这个过程叫做Antialiasin,还有就是我们从三维映射到二维的过程中,如何进行深度测试,即如何使用Z-Buffer。

Triangles

屏幕是什么

首先我们要明白,屏幕是由什么组成的。这个问题的答案是,屏幕由一个个像素组成,其实我们平时说的屏幕分辨率,如1920*1080,2K,4K指的就是这个屏幕一共有多少个像素点,每个像素点我们可以先简单理解为一个个可以发出不同颜色的小灯。

当然,像素点越多,我们能够模拟的和还原的效果越细腻,像素点多到一定程度,即我们肉眼完全无法识别出有像素点的时候,就是我们常说的视网膜屏了。

我们假设一个块屏幕由以下几个像素点组成;

  • 我们可以用坐标(x,y)来表示像素点,并且x和y都是整数
  • 像素的坐标从(0,0)到(width - 1,height - 1)
  • 像素(x,y)的中心在(x + 0.5, y + 0.5)
  • 屏幕的覆盖范围是(0,0)到(width,height)

我们经过之前的MVP矩阵,其实已经把整个视锥体调整到了一个[1,1]3[-1,1]^3的立方体之中,现在我们要做的事,将这个立方体中的点的坐标调整到[0,width][0,height][0,width] * [0, height]的屏幕之中

具体分为两个步骤:

  1. 暂时忽略z坐标(这里说暂时忽略的意思是,z坐标在这里用不到,但是后面的深度测试还要用到),那么点的坐标就从[1,1]2[-1,1]^2变成了[1,1]3[-1,1]^3
  2. 使用一个矩阵将[1,1]2[-1,1]^2转换到[0,width][0,height][0,width]*[0,height]

Mviewport=[width200width20height2height2000100001]M_{viewport} = \begin{bmatrix} \frac{width}{2} & 0 & 0 & \frac{width}{2} \\ 0 & \frac{height}{2} & \frac{height}{2} & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ \end{bmatrix}

三角面

我们刚才讲了如何将点的坐标变为屏幕上的像素坐标,但是一个个孤立的点并不能表示一个物体,我们需要这些点连接起来形成的面来表示物体。

那么我们应该将这些点连接成几边形呢?

答案是三角形?

那么为什么采用三角形呢?原因有如下几点:

  1. 三角形是最基础的多边形,任意的多边形都可以拆分为几个三角形组成
  2. 独特的属性
    1. 保证是平面的。比如四边形,四个点可能不在同一个平面上
    2. 内部定义明确。比如四边形,有凸多边形和凹多边形,后者比较难判断点是否在四边形内部
    3. 定义良好的插值方法,顶点在三角形上(重心插值)

三角面转换为屏幕像素点

现在,我们已经将模型上的点转化到了二维的屏幕空间[0,0][0,0][width,height][width, height]了,并且变成了一个个的三角面片,现在的问题是,如何确定这些面片覆盖了那些像素点,或者说,如何用这个三角面的颜色来确定屏幕像素的颜色,即如下图所示:

采样

第一种方法就是简单的进行采样,对屏幕上的每个像素点进行一次遍历,对于每个像素点的颜色,我们定义一个函数来通过计算得到,如下:

1
2
for (int x = 0; x < xmax; ++x) 
output[x] = f(x);

那么这个f又如何定义呢?

一种方式是,判断一下这个像素的中心是否在三角形内部,如果在,则将三角形面上盖点的颜色赋值给它,如下图所示:

用公式表达就是:

inside(t,x,y)={1,Point(x,y)intriangle0,otherwiseinside(t,x,y) = \begin{cases} 1, &Point(x,y) &in & triangle \\ 0, & otherwise \end{cases}

1
2
3
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);

那么如何判断一个点是否在三角形内部呢?可以看上一篇博客,讲过这个问提,通过向量的叉乘

那么,这个方法是不是这样就完成了呢?还没有,还有一种情况我们没有考虑,即,如果像素的中心点不在三角形内部也不在三角形外部怎么办,也就是正好在边上,甚至同时在两个三角形的边上怎么办,如下图所示:

解决方法比较简单,可以随便选一个就好

包围盒

除此之外,还有一个问题,我们是否要对屏幕上的每个像素点都执行一遍这个inside函数?

答案是不用的,我们可以先计算出三角形的包围盒,然后遍历包围盒之内的像素点就好,包围盒之外的像素点绝对不会被这个三角形影响到。

采样结果

经过上面的过程,一个三角形在屏幕上的像素点就会被表示为:

可以看出来,如果分辨率不够就会出现明显的锯齿,如下,

采样会出现什么问题

Jaggies(锯齿)

锯齿的效果就是上一张图展示的样子

那么我们有什么方式去作抗锯齿呢?

Blurring (Pre-Filtering) Before

这种方式的核心思想,出现锯齿的原因是所有的像素点都是要不纯粹的红色,要不纯粹的白色

我们可以通过提前的进行一定的模糊来避免这种非红即白的情况,如下图所示:

超采样

所谓超采样的意思是,之前我们是一个像素点的值对应一个坐标去三角形上去做判断,现在我们一个像素点我们拆分成多个采样点去三角形上去采样

基本步骤为:

  1. 选择一个N*N的矩阵去对每个像素点进行采样
  2. 对每个采样点的进行采样,然后取平均值作为该像素点的最终值

摩尔纹

占个位置

Wagon wheel effect – sampling in time

占个位置

z-bufferings

其实这个问题就是,我们虽然刚才光栅化的时候我们虽然暂时忽略了z坐标,但是我们是3D的,在同一个像素点上可能有多个三角形覆盖,这个时候我们应该怎么做呢?

一种方式是我们遍历每个三角形,看看他的z坐标大小,如果遇到了一个距离摄像机更近的三角形,我们就用它的颜色值替代当前像素的颜色。

但是有个问题是,我们只有每个点的坐标,如何判断一个三角形的z坐标,其次是,如果遇到三个三角形互相盖在上面的情况,怎么处理,如下图:

解决方案就是,我们还是遍历每个三角形,不过深度值由每个像素自己记录,我们记录的是像素点当前的颜色对应在三角形上某一点的深度信息,而不是三角形的深度信息

1
2
3
4
5
6
7
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