找回密码
 立即注册
查看: 376|回复: 11

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

[复制链接]
发表于 2023-4-4 07:54 | 显示全部楼层 |阅读模式
Cairo与Skia是两个知名的2D渲染图形库。其中,Cairo使用CPU渲染,Skia已切换为GPU渲染加速。本文通过这两个图形库渲染实现的差异来比较CPU与GPU渲染的差异。
Cairo与CPU渲染

我们使用Cairo绘制一个三角形描边,代码如下所示:
cairo_t * cr = gdk_drawing_context_get_cairo_context (drawingContext);

cairo_set_line_width (cr, 8.0);
cairo_move_to(cr, 50, 10);
cairo_line_to(cr, 90, 90);
cairo_line_to(cr, 10, 120);
cairo_close_path(cr); // 调用close path之后形成一个多边形

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



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



生成几何数据结构

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



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

  • 输入点(黑色的点):输入的三个点,即图中序号为8, 5, 2的点。
  • 线宽点(红色的点):与黑色的点的连线垂直于三角形的一边,如红色的L08垂直于蓝色的L82,且L08的长度为线宽的一半。
  • 三角形顶点(蓝色的点)。
三角形顶点的位置需要根据另外两类点的位置计算得到,如下Cairo中的源码注释所示:



默认的线段连接类型为miter,且图中的线宽(line_width)已知,角度(psi)易知,从而可以计算出miter_length,得到顶点位置。
在数据结构的分组(chain)上,前两类点为一组轮廓点,第三类点为一组轮廓点。将各组中每两个相邻点及首尾点连接,就可以得到边缘线段(Edges)。边缘线段是Cairo表示多边形(Polygon)的主要几何数据结构(除此外,还有其他一些辅助的数据结构)。
合成(Composite)

接下来,Cairo对多边形的每一行做一个扫描,决定每一行的哪些地方有非透明像素,从而生成一个称为跨度(span)的数据结构。这个过程称为合成。
本例中的三角形高度共有253px,也就是说共有253行。其中,第73行扫描后生成的跨度如下所示:
y = 72
x1 = 72, cov = 155
x2 = 73, cov = 255
x3 = 89, cov = 107
x4 = 90, cov = 0
x5 = 117, cov = 177
x6 = 118, cov = 255
x7 = 134, cov = 255
x8 = 135, cov = 53
x9 = 136, cov = 0在上述数据结构中,x表示第几列(像素),cov是色值覆盖度(coverage)的意思。这里以每两个相邻的点表示一个区间。如果区间覆盖度越低,则像素越接近透明。举例来说,如果Xn的覆盖度为0,那么[Xn, Xn+1]区间为透明像素。覆盖度是由抗锯齿(Antialias)算法计算得到的,为了使边缘看起来更光滑。Cairo默认会使用抗锯齿模式。
我们将上述数据结构使用图表来表示,如下图所示。



这里使用边缘线段生成跨度实现的关键是:每次取两条线段,并决定哪条在左边,哪条在右边。Cairo把这个算法称为Glitter Paths,具体可以查看cairo-clip-tor-scan-converter.c文件的注释。
Cairo把生成表示像素的数据结构的过程称为合成(Composite)。
渲染(Render)

最后,通过跨度的数据结构就可以渲染出每一行像素。如下代码所示:
do {
  uint8_t a = spans[0].coverage;
  if (a) { // 覆盖度不为0,说明需要填充
    int len = spans[1].x - spans[0].x;
    uint32_t *d = (uint32_t*)(r->u.fill.data + r->u.fill.stride*y + spans[0].x*4);
    if (a == 0xff) { // 如果覆盖度为255,则直接将画布对应的像素点填充为指定的颜色
      uint32_t *d = (uint32_t*)(r->u.fill.data + r->u.fill.stride*y + spans[0].x*4);
      while (len-- > 0)
        *d++ = r->u.fill.pixel;
    } else while (len-- > 0) { // 否则需要做一个颜色混合处理
      *d = lerp8x4 (r->u.fill.pixel, a, *d);
      d++;
    }
  }
  spans++;
} while (--num_spans > 1);
我们可以使用跨度的数据结构,打印一个ASCII的图形。只要有色值就打印一个点“.”,否则打印一个空白字符“ ”。如下代码所示:
for (unsigned k = 0; k <= spans[0].x; k++) {
  printf(" ");
}

for (unsigned i = 0; i < num_spans - 1; i++) {
  for (unsigned j = spans.x; j < spans[i + 1].x; j++) {
    if (spans.coverage != 0) {
      printf(".");
    } else {
      printf(" ");
    }
  }
}
结果如下图所示:


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

我们使用Skia绘制一个相同的三角形,如下代码所示。Skia默认使用GL的渲染后台(GL Backend)。
SkPaint paint;
paint.setColor(SK_ColorBLACK);
paint.setStyle(SkPaint::kStroke_Style);
paint.setStrokeWidth(8 * 2);

paint.setAntiAlias(true);
SkPath path;
path.moveTo(50 * 2, 10 * 2);
path.lineTo(90 * 2, 90 * 2);
path.lineTo(10 * 2, 120 * 2);
path.close();
canvas->drawPath(path, paint);
//注意到,Skia是一个C++库,Cairo是一个C库。
绘制结果如下图所示:



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

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



图中的顶点数组共包含4个顶点。图中三角形数组中的元素,表示在顶点数组中点的索引。
那么Skia是如何利用三角形来实现描边的呢?
顶点数据与三角形

我们可以将本例中Skia最终生成的顶点数据与三角形数据打印出来,如下所示:
const points = [
    180, 180, 20, 240, 100, 20, 186.708, 176.646, 182.633, 187.022, 190.434, 184.097, 22.6334, 247.022, 12.9515, 237.437, 7.38682, 252.74, 92.9516, 17.4369, 106.708, 16.6459, 98.9104, 1.05021, 187.603, 176.199, 191.328, 183.65, 190.785, 185.034, 191.825, 184.644, 182.985, 187.959, 22.9846, 247.959, 7.73794, 253.676, 6.44702, 252.398, 5.70506, 254.439, 12.0118, 237.095, 92.0118, 17.0952, 97.9706, 0.708462, 99.8048, 0.602991, 98.7651, -1.47643, 107.603, 16.1987, 169.566, 175.903, 32.6132, 227.26, 101.09, 38.9498, 168.175, 175.356, 34.2949, 225.561
];
const tris = [
  0, 3, 5, 0, 5, 4, 1, 6, 8, 1, 8, 7, 0, 6, 1, 0, 4, 6, 2, 9, 11, 2, 11, 10, 1, 9, 2, 1, 7, 9, 2, 3, 0, 2, 10, 3, 5, 13, 15, 5, 15, 14, 3, 13, 5, 3, 12, 13, 5, 16, 4, 5, 14, 16, 4, 17, 6, 4, 16, 17, 8, 18, 20, 8, 20, 19, 6, 18, 8, 6, 17, 18, 8, 21, 7, 8, 19, 21, 7, 22, 9, 7, 21, 22, 11, 23, 25, 11, 25, 24, 9, 23, 11, 9, 22, 23, 11, 26, 10, 11, 24, 26, 10, 12, 3, 10, 26, 12, 0, 1, 28, 0, 28, 27, 1, 2, 29, 1, 29, 28, 2, 0, 27, 2, 27, 29, 27, 28, 31, 27, 31, 30, 28, 29, 32, 28, 32, 31, 29, 27, 30,
];
上面共有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渲染。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2023-4-4 07:57 | 显示全部楼层
看完才知道cairo是用的cpu渲染的,谢谢科普
发表于 2023-4-4 07:57 | 显示全部楼层
cairo 有多种渲染后端吧,除了 opengl 还可以用 skia 做渲染后端。
发表于 2023-4-4 07:57 | 显示全部楼层
查了下说cairo的gl后端一年前删除了https://baijiahao.baidu.com/s?id=1756608866956392094&wfr=spider&for=pc&searchword=Cairo%20%E5%9B%BE%E5%BD%A2%E5%BA%93%E4%B8%8D%E5%86%8D%E6%94%AF%E6%8C%81%20OpenGL
发表于 2023-4-4 08:03 | 显示全部楼层
cairo 的 GL 后端一直没有得到过妥善开发和维护,存在相当多问题,并且从来没有进入过主流的 Linux 发行版。今年1月干脆把代码都删除了。
发表于 2023-4-4 08:10 | 显示全部楼层
感谢科普,第一次听说 skia 这个库[捂脸]
发表于 2023-4-4 08:13 | 显示全部楼层
Flutter,Chrome就用的这个做渲染库
发表于 2023-4-4 08:13 | 显示全部楼层
skia的顶点是怎么打印的?skia需要计算每个三角形的顶点吗?我还以为只要计算很少的几个多边形顶点,然后交给opengl去做就可以了。
发表于 2023-4-4 08:23 | 显示全部楼层
gl的曲线呢?
发表于 2023-4-4 08:27 | 显示全部楼层
skia也是多种后端的吧,可以软件渲染
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-4-29 05:59 , Processed in 0.098083 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表