WEBGL之光栅化

1,363 阅读12分钟

前言

周末没事的学习了光栅化进一步理解, 从底层去学习,遇到问题才会从容不迫, 并同时把这些知识分享给大家。 这里先做一个概念的铺垫在3D即将渲染到我们屏幕当中来的时候。而接下来我们要做的是把这个标准立方体绘制到屏幕上,这样才能最终被我们所看见。

我们简单看下这张图:

变换过程

变换过程

而光栅化的过程发生在哪里 ,其实 就是物体通过MVP变换,把摄像机观测的空间压缩成了一个标准立方体。 然后将标准的立方体【-,1,】绘制到屏幕上的这些过程

转换

转换

在做这步操作之前,我们首先要知道如下一些定义:

屏幕(Screen)

在图形学中,我们把屏幕抽象为一个二维数组,数组中的每个元素称之为像素(pixel,picture element的缩写)。这个数组的大小,就是屏幕的分辨率(Resolution),例如我们常说的屏幕分辨率为1920*1080,就是说有这么些个像素。屏幕是一个典型的光栅成像设备(Raster display)。

光栅设备

示波器(Oscilloscope),阴极射线管(Cathode Ray Tube,早期的显示器),液晶显示器(Liquid Crystal Display,LCD,利用了光的波动性,液晶的扭曲),发光二极管(Light Emitting Diode,LED),墨水屏(Electrophoretic Display,刷新率很低)。

设备

设备

光栅与光栅化

光栅(Raster)在德语中就是屏幕的意思,光栅化(Rasterize)就是把东西画在屏幕上的过程。这个过程非常的复杂,也是本文的重点,会在后面进行介绍。

像素

像素,最小单位,我们可以把它理解为一个个小方块,像素内的颜色可以用rgba来定义,一个像素(小方块)内只存在一种颜色。

当然随着科技的发展,像素的内容以及分布也越来越复杂,例如下图为两个手机屏幕的像素分布:

image-20220116153836643

image-20220116153836643

左图的苹果手机屏幕中,一个像素拥有三种颜色。

右图三星手机的这种分布我们称之为Bayer pattern,从中可以发现绿色的点比红蓝更多,这是因为人眼对绿色最敏感,人眼看上去可以更舒服。

在文本中依旧认为每个像素中只存在一种颜色。

定义屏幕空间

前面说了,我们要把标准立方体绘制到屏幕上,那么首先我们要定义一下屏幕空间,然后把标准立方体变换到这个空间上。

定义屏幕空间相当于在屏幕上建立一个坐标系,这里我们以屏幕的左下角为原点,向右为x轴方向,向上是y轴方向(注,定义的方法有很多种,例如我们也可以左上角为原点,在后续的操作中遵循自己的定义即可)。

screen

screen

前面说到屏幕是有一个个像素所组成的,例如我们像素的二维数组为 wh,那么就表示在x轴方向有w列,在y轴方向有h行。这里我们设每个像素的大小为11,那么整个屏幕的大小即为 w*h。如上图,一个个小方块即代表一个像素。

这样我们就可以通过坐标的方式来定义每个像素的位置了,即(x, y),可以当做是图中每个小方块左下角点的坐标。例如坐标(0, 0)就表示屏幕最左下角的那个像素,由于坐标从0开始,因此最右上角的那个像素坐标为(w-1, h-1)。

此外我们说过像素是一个个小方块,那么自然有它的中点(即图中小方块中间的点),因此像素(x, y)的中点即为(x+0.5, y+0.5)。

视口变换(Viewport Transform)

在前面我们定义好了屏幕空间,那么我们要把MVP变换后的标准立方体绘制到屏幕上,首先要做的就是把其变换到这个w*h的空间上,这个变换我们称之为视口变换。

该变换我们主要分为如下两步:

  1. 将x轴和y轴长度为2的标准立方体缩放为x轴和y轴长度分别为w和h长方体。
  2. 将该立方体从原点平移到 w /2 , h/ 2 中 注:此处我们先不考虑z轴的变换,后续会有它的作用。

这个变换矩阵很简单,就不过多推导了,其结果如下:

矩阵

矩阵

光栅化

在上面视口变换后,我们的标准立方体虽然变成了一个xy方向和屏幕一样大的空间,但是空间中依旧还是我们的三维物体,例如人,建筑,植物等。前面我们知道屏幕是由一个个像素组成的,也就是说我们通过屏幕看见的二维画面其实都是由无数个像素构成的。因此接下来我们要做的就是把空间中的那些三维物体全部打散成像素,而这个过程,我们就可以称之为光栅化。

Mesh与三角形

在生活中我们知道,不管是相机拍照还是人眼看,我们仅仅只能看见物体的表面,因此我们要显示在屏幕上的,也仅仅是这些三维物体的表面。而对于表面,我们可以把它理解成由多个不同平面所组成,例如长方体即是六个长方形所组成的。在图形学中,我们需要把表面分解成无数个不同的小三角形(Triangle),这些三角形像网一样编织在一起,就可以形成任何我们想要的三维物体表面,这些由三角形所构成的表面我们称之为Mesh。例如长方体就是有12个三角形组成,因为一个长方形可以分解成两个三角形。更复杂的物体表面分解可见下面几个示意图:

mesh

mesh

为什么选择三角形呢?因为它的优点如下:

三角形是最基础的多边形,任何其他不同的多边形都可以拆成若干个三角形。 我们可以通过向量的叉积来判断一个点是在三角形内或者外,但是对于有凹凸的多边形不行。 我们可以给定三个顶点不同的属性,在三角形内做出渐变效果,即可根据插值算出三角形内任意一点的属性。

单个三角形光栅化

通过上面的解释,我们又把问题进行了简单化,也就是把三维物体光栅化即是把无数个三角形进行光栅化。那么同样由繁化简,我们先来看看如何把一个xy平面上的三角形(忽视z轴)进行光栅化,如下图,背景中的黑色实线所围成的小格子即是我们的像素,灰色虚线为辅助线,方便看像素的中心点:

三角形

三角形

首先可以肯定的是,光栅化后,肯定不是像上图那样的显示了。因为前面我们说过一个像素中只会存在一个颜色,而上图明显不符合这个要求,例如我们看下标为(1, 2)的像素点,里面只有一部分是红色的。那么这个像素到底应该是没有颜色还是全部红色呢?在图形学中,我们定义若一个像素的中心点在三角形的内部,那么这个像素就属于该三角形。例如例子中下标为(2, 2)的像素点,我们可以从图中明确的看出其中心点在三角形内部,那么这个像素就应该全部显示红色。

当然了,我们肯定不可能通过肉眼来观察是否在三角形内部,因此光栅化过程中很重要的一步便是:判断像素的中心点与三角形的内外关系。这里也就体现了使用三角形的好处 ,因为前面我们说了使用叉积的方法可以判断点和三角形的内外关系(原理就不多赘述了,主要是通过叉乘去判断 三个 z轴的方向,方向是否一致,来判断点是不是在三角形内部) 。那么我们就可以定义一个函数用来判断,如下:

bool isInside(t, x, y){}

函数体内即使用叉积来判断,若在三角形内则返回true,不在则返回false。输入的参数 t 代表三角形的信息集合(三个顶点的x,y信息),输入的参数 x 和 y 即点的位置信息。

知道了屏幕中任何一个点和三角形的关系后,我们遍历每个像素,取其中心点的x和y带入该函数中即可,而这步操作,我们称之为采样。

这张图可能看的更明显一点:

采样

采样

采样(sampling)

何为采样?是指从总体中抽取个体或样品的过程。用程序的思维来解释的话,就是给定一个连续的函数,然后我们通过不同的输入来获取函数的值。例如有个函数 ,我们分别求 x = 1,x = 2,x = 3...时y的值。采样即把一个函数给离散化(Discretize)的过程,在图形学中有广泛的应用。

采样频率:采样频率可以理解为抽取样本的间隔,例如上面我们采样的是x = 1,x = 2,x = 3...,间隔为1。如果改成x = 1,x = 3,x = 5... ,间隔为2,那么就代表频率变慢。而改成x = 0.5,x = 1,x = 1.5... ,间隔为0.5,那么就代表频率变快。

应用到我们现在所说的屏幕中的话,我们前面已经把摄像机所观测的空间通过一系列变换,变成了在xy方向上和屏幕一样大的空间了。而这个空间内的可以想象成是由无数个连续的点所组成(忽略z)。而我们的屏幕又由一个个的像素组成,这些像素的中心点对应到空间中的点,就是我们从空间中所有点中所抽取出的样本。那么采样的间隔自然是我们像素的实际物理大小。因此若屏幕大小不变,分辨率越高(即像素越多,像素的中心点物理间隔越小),采样的频率越高。

上面介绍的属于采样二维空间中的位置信息,此外我们还可以采样时间,例如下图,便是采样了不同时间人挥球的动作,进行了显示

img

img

前面我们说了屏幕是由 width * height 个像素点组成的,因此要采样每个像素的中心点,只需要遍历所有像素,然后将其中心点的x和y带入上面定义好的函数中即可:

for(int x = 0; x < width; x++){
    for(int y = 0; y < height; y++){
        pixel[x][y] = isInside(t, x + 0.5, y + 0.5);//前面提到像素中心点是像素坐标x,y的值+0.5
    }
}

这样我们就可以知道在三角形内的所有像素了,如下图,顶点标记黑色的即为在三角形内的顶点。

test

test

思考:如果我们修改下上面的代码,改成如下,那是做了什么事情?

for(int x = 0; x < width; x++){
    for(int y = 0; y < height; y++){
        pixel1[x][y] = isInside(t, x + 0.25, y + 0.25);
        pixel2[x][y] = isInside(t, x + 0.25, y + 0.75);
        pixel3[x][y] = isInside(t, x + 0.75, y + 0.25);
        pixel4[x][y] = isInside(t, x + 0.75, y + 0.75);
    }
}

其实很简单,做的就不再是采样每个像素的中心点,而是讲一个像素分成了如下图的四块,然后采样每个像素这四块的中心点。

四个

四个

Bounding Box

在上面的采样中,我们需要采样屏幕中所有的像素中心点,但是实际上我们的三角形可能非常的小,就占了几个像素,那么这种做法就会造成很大的性能消耗。因此我们可以使用Bounding Box来缩小我们的采样范围。例如下图,可能在三角形内的像素肯定是在蓝色区域的范围内,而这个蓝色区域我们就称之为Bounding Box。也可称之为轴向的包围盒,即Aixe align bounding box,也就是常说的AABB。

包围盒

包围盒

因此,给定三角形的三个顶点,我们只需要求出三个顶点的在x轴的最大最小值,在y轴的最大最小值,即可定义出一个Bounding Box,然后只需要在这个Bounding Box中进行采样即可。

for(int x = xmin; x < xmax; x++){
    for(int y = ymin; y < ymax; y++){
        pixel[x][y] = isInside(t, x + 0.5, y + 0.5);
    }
}

这样我们已经可以减少很多的采样了,但是还有些特殊的情况,导致三角形本身依旧不大,但是Bounding Box特别大,例如下图这种情况:

special

special

针对这种情况,我们也可做特殊处理,例如每行做一个Bounding Box,然后从左到右遍历,如下。

逐行boundingBox

逐行boundingBox

至于怎么判断每行的Bounding Box的左右边界,以及怎么判断这个三角形属于这种特殊情况,等后续的学习再进行补充。

通过上面的知识,我们可以找到屏幕中在三角形内部的像素,接着我们对这些像素进行着色,就可得到如下结果

image-20220116161928711

image-20220116161928711

上图也就是我们单个三角形进行光栅化后的结果,也就是屏幕中真正显示的样子。

很显然,这个效果看着和原本的三角形差距很大,三角形的边缘处都是凹凸不平的,也就是所谓的锯齿。这个等后面我继续学习,并整理一下,敬请关注哦

最后给大家做了一个简单的总结如下面图片所示:

总结