【计算机图形学】【GAMES101学习笔记】Rasterization 光栅化

1. 光栅化概述

光栅化是对象顺序图形的中心操作,而光栅化器是任何图形管道的中心。对于输入的每个基元,光栅化器有两个任务:枚举基元覆盖的像素,并在基元上插值,称为属性。这些属性的用途将在后面的示例中明确。光栅化器的输出是一组片段,每个片段对应于基本体覆盖的像素。每个片段“存在”于特定像素处,并携带其自己的一组属性值。

通俗的理解就是,前面所讲的MVP操作先将物体呈现在 [ − 1 , 1 ] 3 [-1, 1]^3 [1,1]3的标准空间内,然后经过视口变换映射到 [ 0 , w i d t h ] ∗ [ 0 , h e i g h t ] [0, width] * [0, height] [0,width][0,height]的屏幕上,最后要通过光栅化才能真正在屏幕上展示出物体。换句话说,光栅化的目的就是将物体画在屏幕上。

2. 认识像素和屏幕

像素是指在由一个数字序列表示的图像中的一个最小单位,通常可以将一幅图像看成一个二维矩阵,比如800 x 800像素的图像其实就是一个800 x 800的矩阵,矩阵中的每个元素其实就是一个像素。

巧妙的是,在计算机图形学中,屏幕也是像素的二维数组,我们平常所说的分辨率(如1920*1080/1080p)就是像素数组的大小。通常来说,像素点越多,像素数组越大,屏幕分辨率越高。屏幕就是典型的光栅显示。具体如下图:

屏幕定义

  • 每个像素以方块形式呈现,坐标范围为 ( 0 , 0 ) (0, 0) (0,0) ( w i d t h − 1 , h e i g h t − 1 ) (width-1, height-1) (width1,height1)
  • 像素中心点坐标即为像素坐标,数值表示为 ( x + 0.5 , y + 0.5 ) (x+0.5, y+0.5) (x+0.5,y+0.5)
  • 整个屏幕表示范围为 ( 0 , 0 ) (0, 0) (0,0) ( w i d t h , h e i g h t ) (width, height) (width,height)

3. 线性光栅化(Linear Rasterization)

3.1 DDA数值微分算法

DDA(数值微分算法)算法是一个增量算法。增量算法:在一个迭代算法中,每一步的x、y值是用前一步的值加上一个增量来获得。通过各行各列象素中心构造一组虚拟网格线。按直线从起点到终点的顺序计算直线与各垂直网格线的交点,然后根据误差项的符号确定该列象素中与此交点最近的象素。
DDA需要考虑所画直线的斜率k:

  • ∣ k ∣ < 1 |k| < 1 k<1 x x x每增加1, y y y增加 k k k
  • ∣ k ∣ > 1 |k| > 1 k>1 y y y每增加1, x x x增加 1 / k 1/k 1/k
void CMyView::OnDdaline()
{
    
    
    CDC *pDC=GetDC();              // 获得设备指针
    intx0=100,y0=100,x1=300,y1=200,c=RGB(255,0,0);  //定义直线两端点和直线颜色(红色)
    float x,y,i;
    float dx,dy,k;
    dx=(float)(x1-x0);
    dy=(float)(y1-y0);
    k=dy/dx;//计算斜率
    y=y0; x=x0;
    if(abs(k)<1)
    {
    
       for(;x<=x1;x++)
        {
    
    pDC->SetPixel(x,int(y+0.5),c);
        y=y+k;}//x自增,y=y+k
    }
    if(abs(k)>=1)
    {
    
    
      for(;y<=y1;y++)
      {
    
    pDC->SetPixel(int(x+0.5),y,c);
      x=x+1/k;}
    }
    ReleaseDC(pDC);      //释放设备指针
}

3.2 中点Bresenham算法

Bresenham算法是计算机图形学领域使用最广泛的直线扫描转换方法。 其原理是:过各行、各列像素中心构造一组虚拟网格线,按直线从起点到终点的顺序计算直线各垂直网格线的交点,然后确定该列像素中与此交点最近的像素。 该算法的优点在于可以采用增量计算,使得对于每一列,只要检查一个误差项的符号,就可以确定该列所求的像素。

void CTestView::OnBresenhamline()
{
    
    
    CDC*pDC=GetDC();
    intx1=100,y1=200,x2=600,y2=800,color=RGB(0,0,255);
    inti,x,y,dx,dy;
    float k,e;
    dx=x2-x1;
    dy=y2-y1;
    k=dy/dx;
    e=-0.5;  x=x1; y=y1;//e初值d0-0.5
    for(i=0;i<=dx;i++)
    {
    
       pDC->SetPixel(x,y,color);
       x++;
       e=e+k;
       if(e>=0)  {
    
     y++; e=e-1;}
    }
}

4. 三角形光栅化(Triangle Rasterization)

4.1 三角形的好处

  • 是最基础的多边形,任意多边形都可以拆分成三角形;
  • 可以保证三个顶点在同一平面;
  • 三角形内部和外部定义明确,有利于像素的着色;
  • 用于在三角形顶点处插值的明确定义方法(重心插值)。

4.2 怎么进行光栅化

要实现三角形的光栅化,需要对像素点进行采样,即遍历所有的像素点,判断像素点是否在三角形内。其对应的代码如下:

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函数是用于判断像素点是否在三角形内部的函数,其最经典的实现方法是利用叉乘,如下图:
判断内外
我们从 P 2 P_2 P2按顺时针顺序来看,直线 P 2 P 1 P_2P_1 P2P1与直线 P 2 Q P_2Q P2Q的叉乘,利用右手定则,指向屏幕内,说明点 Q Q Q在直线 P 2 P 1 P_2P_1 P2P1的右侧。同理直线 P 1 P 0 P_1P_0 P1P0与直线 P 1 Q P_1Q P1Q的叉乘,推出点 Q Q Q在直线 P 1 P 0 P_1P_0 P1P0的右侧;而最后又推出 Q Q Q在直线 P 0 P 2 P_0P_2 P0P2的左侧。说明点 Q Q Q在三角形外部。

因此,三角形光栅化只需要遍历每一个点,判断是否位于其内部即可。当然我们还可以进一步进行优化,因为显然并没有必要去测试屏幕中的每一个点,一个三角形面可能只占屏幕很小的部分,可以利用一个bounding box包围住想要测试的三角形,只对该bounding box内的点进行采样测试,如下图:
三角形优化

5. 锯齿与走样(Aliasing)

5.1 光栅化带来的锯齿与走样

利用上述三角形光栅化的方法,我们最后得到的图形是下图这样的:
锯齿
可以看到,本来是三角形的,但是经过采样之后根本就不是三角形,边缘部分非常不平整光滑,这种现象在图形学中称为走样。

为什么会走样呢?其本质是采样的频率过低,导致采样频率无法跟上图像的频率。通俗地讲就是采样数过少,可以想象一下,像素点足够多,采样数足够大,那么精细度就会越高,一个个的锯齿将会变得十分小至肉眼无法分辨,这样看上去就是平整光滑的了。

5.2 如何抗锯齿/反走样

抗锯齿/反走样的基本思路就是模糊,例如,我们可以先对三角形进行模糊,然后再进行采样,比如下图:
模糊
要想从深层次去理解为什么模糊能够抗锯齿,就需要涉及到时域和频域的一些知识了。如下图,采样对应的时域像素点的乘积等于图像对应的频域与低频滤波的卷积,二者都可以达到模糊的效果:
时域与频域
总之,解决问题的方法是用有限离散的像素点去逼近连续的三角形,下面介绍两种工业界采用的方法。

5.2.1 超采样反走样(Super Sampling Anti-Aliasing, SSAA)

这是最基本的抗锯齿模式,实现原理是渲染时把画面按照显示器分辨率的若干倍放大,如在1024 x 768分辨率上开启2xSSAA,GPU会先渲染2048 x 1536图像,再“塞进”1024 x 768的边框里成型,将画面精细度提升一倍,毫无疑问会改善边缘锯齿的情况。

但是众所周知,高分辨率图形的渲染会极大地消耗GPU运算资源和显存容量及带宽,因此SSAA资源消耗极大,即使是最低的2x也未必能够轻易承受。

此方法无非就是提高分辨率,也就是增加采样点,如下图,将每个像素点细分成了4个采样点:
在这里插入图片描述
在这里插入图片描述
之后根据每个采样点进行着色,每有一个采样点被覆盖就着色一次。这样得到了每个采样点的颜色之后,我们将每个像素点内部所细分的采样点的颜色值全部加起来求平均,作为像素点的抗走样之后的颜色值。结果如下:
在这里插入图片描述

5.2.2 多采样反走样(Multi Sampling Anti-Aliasing, MSAA)

MSAA是SSAA的改进版,SSAA仅仅为了边缘平滑,而不得不重新以数倍的分辨率渲染整个画面,造成宝贵显卡处理资源的极大浪费,因此MSAA正是为了改善这种情况而生。MSAA实现方式类似于SSAA,不同之处在于MSAA仅仅将3D建模的边缘部分放大处理,而不是整个画面,简单说就是:3D模型是由大量多边形所组成,MSAA仅仅处理模型最外层的多边形,因此显卡的负担大幅减轻。

MSAA虽然趋于易用化,十分流行,但是缺点也很明显

  • 如果画面中单位物体较多,需要处理的边缘多边形数量也自然增多,此时MSAA性能也会下降的十分厉害;
  • 同样倍数的MSAA,理论上边缘平滑效果与SSAA相同,但是由于仅仅处理边缘部分的多边形,因此非边缘部分的纹理锐度远不如SSAA。

同样利用上述的例子说明MSAA和SSAA区别,MSAA仍将像素分为多个采样点,不同的是不再需要每有一个采样点被覆盖就着色一次,而是统计被覆盖采样点的个数,例如有两个采样点被覆盖,那么只需要用改像素中心计算出来的颜色值乘以50%即可,这样大大减少了计算量,如上述:
在这里插入图片描述

6. 深度缓冲(Z-Buffer)

在计算机图形学中,深度缓冲是在三维图形中处理图像深度坐标的过程,这个过程通常在硬件中完成,它也可以在软件中完成,它是可见性问题的一个解决方法。可见性问题是确定渲染场景中哪部分可见、哪部分不可见的问题。

接着上文,我们在处理完图形光栅化之后,还要考虑物体先后的关系?这十分重要,更直白的说法是,要搞清楚物体的图层,哪个物体会被哪个物体遮挡,哪个物体会遮挡哪个物体。具体的说每个像素点所对应的可能不止一个三角形面上的点,该选择哪个三角形面上的点来显示呢?

当然是离摄像头最近的像素点显示,这就需要用到深度缓冲。(3D物体的远近通过 Z ZZ 轴表示,故又称Z-Buffer),如下图,深度图如右所示:
在这里插入图片描述
具体步骤如下:

  • Z-Buffer算法需要为每个像素点维持一个深度数组记为zbuffer,其每个位置初始值置为无穷大(即离摄像机无穷远);
  • 随后我们遍历每个三角形面上的每一个像素点[x,y],如果该像素点的深度值z,小于zbuffer[x,y]中的值,则更新zbuffer[x,y]值为该点深度值z,并同时更新该像素点[x,y]的颜色为该三角形面上的该点的颜色。
Initialize depth buffer to ∞
During rasterization:
	for(each triangle T)
		for(each sample(x,y,z) in T)
			if(z < zbuffer[x,y])              //目前为止离摄像机最近的
				framebuffer[x,y] = rgb;       //更新颜色
				zbuffer[x,y] = z;             //更新深度

举个形象的例子,其中数字代表深度,越小代表离相机越近,我们可以使用以上算法实现像素颜色的更新。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/charenCsdn/article/details/125854534
今日推荐