从两个图形库看CPU与GPU渲染的差异

Cairo与Skia是两个知名的2D渲染图形库。其中,Cairo使用CPU渲染,Skia已切换为GPU渲染加速。本文通过这两个图形库渲染实现的差异来比较CPU与GPU渲染的差异。

Cairo与CPU渲染

我们使用Cairo绘制一个三角形描边,代码如下所示:

我们通过GTK+3.0创建一个视窗(Window),然后获取一个绑定到该视窗上的Cairo绘制上下文对象cr。接着调用cr的move_to、line_to及stroke等API绘制一个三角形描边。渲染的结果如下图所示。

现在我们通过Cairo的源码研究Cairo是怎么画出这个三角形的。换句话说,Cairo是如何根据一些几何描述,然后在一个空白画布的指定位置上,填充像素。

输入条件是三个点的位置,如下图所示。

生成几何数据结构

首先根据这三个点的位置,及线宽、线段连接类型(Line Join)计算轮廓点的位置。然后再根据轮廓点计算边缘线段,如下图所示。

图中共有12个轮廓点。通过观察可发现,这些轮廓点可分为三类:

  1. 输入点(黑色的点):输入的三个点,即图中序号为8, 5, 2的点。
  2. 线宽点(红色的点):与黑色的点的连线垂直于三角形的一边,如红色的L08垂直于蓝色的L82,且L08的长度为线宽的一半。
  3. 三角形顶点(蓝色的点)。

三角形顶点的位置需要根据另外两类点的位置计算得到,如下Cairo中的源码注释所示:

默认的线段连接类型为miter,且图中的线宽(line_width)已知,角度(psi)易知,从而可以计算出miter_length,得到顶点位置。

在数据结构的分组(chain)上,前两类点为一组轮廓点,第三类点为一组轮廓点。将各组中每两个相邻点及首尾点连接,就可以得到边缘线段(Edges)。边缘线段是Cairo表示多边形(Polygon)的主要几何数据结构(除此外,还有其他一些辅助的数据结构)。

合成(Composite)

接下来,Cairo对多边形的每一行做一个扫描,决定每一行的哪些地方有非透明像素,从而生成一个称为跨度(span)的数据结构。这个过程称为合成。

本例中的三角形高度共有253px,也就是说共有253行。其中,第73行扫描后生成的跨度如下所示:

在上述数据结构中,x表示第几列(像素),cov是色值覆盖度(coverage)的意思。这里以每两个相邻的点表示一个区间。如果区间覆盖度越低,则像素越接近透明。举例来说,如果Xn的覆盖度为0,那么[Xn, Xn+1]区间为透明像素。覆盖度是由抗锯齿(Antialias)算法计算得到的,为了使边缘看起来更光滑。Cairo默认会使用抗锯齿模式。

我们将上述数据结构使用图表来表示,如下图所示。

这里使用边缘线段生成跨度实现的关键是:每次取两条线段,并决定哪条在左边,哪条在右边。Cairo把这个算法称为Glitter Paths,具体可以查看cairo-clip-tor-scan-converter.c文件的注释。

Cairo把生成表示像素的数据结构的过程称为合成(Composite)。

渲染(Render)

最后,通过跨度的数据结构就可以渲染出每一行像素。如下代码所示:

我们可以使用跨度的数据结构,打印一个ASCII的图形。只要有色值就打印一个点“.”,否则打印一个空白字符“ ”。如下代码所示:

结果如下图所示:

 

(由于每个文字不是一个正方形,所以打印出来的三角形看起来上下拉伸了。)

Skia与GPU渲染

我们使用Skia绘制一个相同的三角形,如下代码所示。Skia默认使用GL的渲染后台(GL Backend)。

绘制结果如下图所示:

与Cairo绘制的结果对比,两者几乎没有差异。

背景

GPU渲染的基础元素是三角形。例如,填充一个四边形需要使用两个三角形拼起来:

图中的顶点数组共包含4个顶点。图中三角形数组中的元素,表示在顶点数组中点的索引。

那么Skia是如何利用三角形来实现描边的呢?

顶点数据与三角形

我们可以将本例中Skia最终生成的顶点数据与三角形数据打印出来,如下所示:

上面共有64个顶点、47个三角形。我们将这些顶点及三角形绘制出来,如下图所示:

把一个形状拆分为多个三角形的过程称为曲线细分(Tessellation)或镶嵌化处理。我们将这个过程做成一个动画,如下图所示。它的实现可见Skia的GrAAConvexTessellator::tessellate函数(AA:抗锯齿的缩写 ,Gr:Skia中GL相关的类名都以Gr开头)。

这个过程可分为4步。同样,处理过程的输入条件仍是三个点、线宽及线段连接类型。

外镶嵌

外镶嵌的输入条件是:三个顶点位置,描边宽度为线宽的一半减去抗锯齿半径(kAntialiasingRadius  = 0.5),线段连接类型为miter, 色值覆盖度的标志为1。外镶嵌算法的过程,如下图所示:

外镶嵌抗锯齿

经过上一步外镶嵌处理后,可以得到9个外边缘顶点。以这个9个顶点的位置、描边宽度为抗锯齿直径(kAntialiasingRadius  * 2 = 1),色值覆盖度的标志为0,作为输入条件,重复上一步的操作。结果共增加15个顶点、24个三角形,如下图所示(新增的点及线使用灰色表示)。

通过描边宽度的不同可知,多边形在距离边缘0.5px内的范围是实的,而在距离边缘[-0.5px, 0.5px]的范围,则要作抗锯齿虚化处理。

内镶嵌

接下来,继续以原始的三个顶点作为输入条件,描边宽度为线宽的一半减去抗锯齿半径,作内镶嵌处理,结果如下图所示:

内镶嵌抗锯齿

过程类似于外镶嵌抗锯齿处理,如果如下图所示:

至此,生成顶点与三角形的过程完毕。接下来是生成顶点的颜色,然后将这些数据提交到GPU渲染。假设我们将strokeWidth从8修改为1,那么同样也会生成这么多数据。

对比

我们看到了两个相似又有差异的渲染过程。Cairo的过程是由点生成点,再生成边,最后生成每一行的像素跨度。Skia的过程则是由点生成顶点,顶点再连接为三角形。它们都需要进行一些数学计算,且算法有所不同。

两者最大的不同在于,前者CPU渲染可以在像素级别操作,后者GPU渲染单位是三角形,同时需要有拼接三角形的过程。而拼接三角形可能会存在一些精度问题,可能导致嵌接之后容易出现噪点、不期望的实线等问题,如下图所示:

据了解,皮克斯公司、AfterEffect软件使用的是CPU渲染。影视场景主流是CPU渲染。由于GPU渲染比CPU渲染快,所以实时渲染如网页、游戏的场景主流是GPU渲染。

 

综上,通过Cairo与Skia绘制两个相同的三角形描边的比较,可以比较好地理解CPU渲染与GPU渲染的差异之处。由于GPU的基础元素是三角形,导致使用GPU渲染的算法与CPU渲染的算法存在差异。并且,由于GPU本身一些特性,导致它的渲染品质比传统的CPU渲染差。但GPU价格比CPU便宜,速度快,因此实时场景仍然离不开GPU渲染。

 

发表评论

邮箱地址不会被公开。