分享

万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

 新用户0175WbuX 2022-02-01

  随着移动互联网的一路高歌,越来越多的 App 不满足系统原生的 UI 体系。开启了各种花式的玩法。早几年 ReactNative、Weex 等,企图尝试让系统组件可以像浏览器一样动态加载,从而提高发版本的效率。更早几年还有一众通过在系统 Webview 基础上面搭建起来的动态化方案,包括当下诸多的小程序平台等。Flutter 的发布仿佛给业界带来一丝新的生机,通过 Skia 渲染器完美的保证了在诸多平台渲染的一致性。但也带来专属于 Flutter 本身的一些问题。不过多的讨论关于 Flutter 本身,这里只谈关于 Skia 和矢量渲染技术中属于我的理解。

  首先要承认我是彻彻底底的标题党。目前为止我通过官方的编译选项来对 Skia 进行编译裁剪,二进制体积依旧很大。而我的目标就是把 CSS 和排版还有渲染器整体做到 1.5MB 以内,如果选用合理小巧的 JS 引擎整体控制在 2MB 到 2.5MB 左右。

  所以如何把 Skia 裁剪到 1/8? 答案是重写一个(认真严肃)!!!

  目前渲染器已经基本完成,关键节点的性能测试和 Skia 处于同一水平(甚至还要好一些)。但是体积只有 Skia 体积(疯狂裁剪后)的 1/8。

  大概是多大?580KB(x86-64 下构建的产物,Android Armv7a 下还要小许多)。在这基础上又添加了对复杂文本的排版功能,这部分依赖 Freetype(解析字体文件的开源库)和 Harfbuzz(对字模整形的开源库)还有文本的排版引擎,带上这部分功能体积会大一些(目前为止 Skia 还不具备复杂文本排版能力)。

  本文希望可以通过简单通俗的语言和大家探讨渲染器背后的核心技术,如果你也有类似的需求希望能给到足够的启发。

  关于矢量渲染器

  矢量渲染器作为现代 UI 的核心支撑模块,常常被作为内嵌在操作系统内的图形子系统的一部分提供给上层开发者。比如 Windows 下的 GDI/GDI+/Direct2D,Android 下的 Skia/HWUI(HWUI 对一些复杂多边形的处理依旧依赖 Skia 的软绘制,所以不能算完备的矢量渲染器),MacosX/iOS 内置的 CoreGraphics,Linux 下的 Cario 渲染器。

  同样其他的跨平台的库,比如 QT 就自己实现了矢量渲染器,这样可以在不同平台下拥有统一的渲染效果。Flutter,Chrome 和 Android 采用同样的 Skia 渲染器来完成跨平台的能力。

  所以要想在不同平台拥有比较好的渲染一致性,剥离对系统提供渲染器的依赖是很重要的一步。

  同样行业出现了一些类似于包括 NanoVG 在内的一些渲染器,此类渲染器都采用了模板掩码的一种特殊技法(Opengl 红宝书中提到的)来解决复杂多边形的绘制问题,巧妙的规避了复杂的几何运算。

  但是天下没有免费的午餐,它同样也会带来相对应的性能问题。而且天花板很低,后续优化几乎无从下手。对于游戏这类的场景偶尔需要显示一些面板来说无可厚非,但是对于传统的界面程序还是显得捉襟见肘。

  前言

  在探讨之前我觉得有必要定义一下“渲染”这个词。这个词在目前互联网技术上面有诸多含义,带有一定的迷惑性。下文所有提及的“渲染”都和计算机图形学中“渲染”拥有同样的含义,指的是把特定的像素填充对应的颜色,以及围绕这一目的的相关算法。

  鸟瞰渲染器全貌

  时至今日 Google 甚至微软的诸多产品都采用 Skia 作为核心渲染组件。包括但是不限于 Android、Chrome、Flutter、Xamarin 等等。不得不说这是一个伟大的技术产品。

  渲染器本身是一个极其复杂的程序,就拿 Skia 来说核心侧有超过 80w 行的代码。如果算上第三方库甚至达到了惊人的 150w 到 200w 行之巨。

  即使构造的这个轻量的渲染器项目也有超过 25w 行的代码(剔除第三方库,比如图片编解码、字体解析、XML 加载库等等。仍然还有超过 13w 行的核心代码)。

  这么复杂的项目我打算从以下几个方向来依次阐述它的核心技术:

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  GPU 硬件加速能力的抽象

  硬件加速之所以快,在于运算单元多,并行能力强。同样也带来一些限制,参与运算的数据必须符合并行计算的要求。下图描述了大致的流程。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  渲染抽象层的设计

  目前消费电子设备基本都配备了硬件显卡,但是很不凑巧主流设备中的显卡驱动存在较大的差异。因此想要构建完善的硬件加速渲染器,对不同厂商的 GPU 驱动做一层抽象是非常有必要。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  这里叫 RAL(Render Adapter Layer)这只是一个名称。在游戏引擎行业中大家更习惯于叫它 RHI(RHI 一般还涉及线程和异步的相关策略),详细可以参考 Unreal 的设计。

  那么 RAL 到底涉及哪些东西?这就要说到显卡的差异了,就目前来说主流的图形技术在所有显卡中都是相通的。显卡核心组成部件都类似(高级显卡存在一些新的着色器流程,暂时我们不会用到)。只是驱动在所在的平台存在差异,也就是显卡功能性的描述接口存在差异。这部分差异主要存在于 2 个方面(当然还存在一些细微细节不一样,比如窗口坐标系和 NDC 的差异,纹理采样坐标系的差异)。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  其中 API 的差异可以通过对驱动接口的包装来抹平(有点繁琐),编程语言就相对来说非常麻烦了。Skia 内部内置了自己的一套显卡编程语言叫 SKSL,可惜文档比较少。为了达到缩减包体积的效果,设计了一套自己的编程语言。我管它叫 RSL。

  设计一套新的 Shader 编程语言

  为什么要设计一套新的编程语言和语法?为什么不直接使用 glsl 的语法?

  这里有 2 方面的思考(主要为了方便我实现编译器或者叫转化器):

  在这之前我尝试让 OpenglES 运行在 iOS 的 Metal 之上(小游戏引擎的内核项目),手写过 glsl 的编译器。用来转化到 Metal 的 MSL 语法之上。由于 glsl 的 Spec 文档有点多而且复杂,为了测试编译器的稳定性,抓取了 ShaderToy(一个交流 webgl shader 的网站)1w5 千个左右的 shader 进行测试。语法分析通过率只有 95% 多点,总有一些我没有考虑到语法。所以说还是不太稳定,工作量有点大。glsl 规范比较老,缺乏语义的支持。应该还有其他的理由,比如我自己设计的语法。但凡有不太容易实现的部分,我可以选择剔除掉。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  为了缩小包体积,我尝试把 Shader 的编译拆分成离线和在线两个端。和大多编译器的项目类似,或许他们都管这叫编译前端和后端。

  离线端:编译源码,抽取语义得到 AST(抽象语法树),由于这部分离线运行,可以任意选择开源项目配合实现,不用在乎包体积。现在开源的编译器项目很多,比如 Flex 和 Bison 很容易就可以构建出离线端。理论上应该对 AST 进行优化操作,奈何我本人对优化算法知之甚少。所以目前还没有实现任何优化相关的部分。最后对 AST 进行序列化,也可以稍微做点处理,一切以方便设备端翻译工作为目的,然后就可以内嵌或者动态下发到目标端上面了。

  在线端:一般指的就是设备端,接收到 AST 的序列化文件后就可以重构 AST 数据结构,并进行翻译。目前主要搞定了 OpenglES 和 Metal 两个主力平台,Vulkan 正在进行中。

  在开源中找找答案

  Bgfx

  bgfx 是一套对显卡接口的抽象,相对来说比较全面。它通过内置一个开源的 C 语言宏处理器的方式,来利用宏展开的特性把自定义的 Shader 语法实时翻译成目标平台的语法。我没有选择它的原因是我不喜欢它对 Shader 语法的处理方式,就其本身来说是一个非常优秀的项目。

  Spirv

  在 Spirv 字节码(Vulkan 支持的 Shader 字节码,不是编程语言)慢慢变成显卡跨平台字节码的事实标准后,行业也出现了一些利用 Spirv 来进行 Shader 转化的项目。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  我有想过把 RSL 的实现换成微软的 HLSL 实现,这样我就可以不用维护 RSL 的编译器。同时还能享受微软 HLSL 编译器强大的优化能力。

  实际上我也确实这么做了,但是这样会明显增加包体积(会增加十几 MB,我实在没有办法忍受把这么一个巨无霸塞进去)。

  所以目前也是只是对内置的 Shader 在离线编译的时候会使用这个编译方案。如果需要动态下发还是保留 RSL 的方式,互相补充。这也是目前能找到的最好最稳定的办法,重点是不增加二进制体积。

  几何

  从这一节开始涉及渲染器最为核心的灵魂,数学是一切魔法的开始。

  三角形和三角剖分

  在图形学中三角形的重要性已经没有必要去描述了。它的质性简单,可以让显卡的插值器更加简单高效的工作。

  试想一下如果显卡支持的不是三角形而是四边形,那么有四个顶点很有可能不共面,这就会出现很复杂的情况了,而三角形则不会出现这个问题。

  如果只能渲染三角形那就太单调啦,实际情况中通常需要把多边形剖分成一组三角形的网格,我们管这个网格叫 Mesh。只有得到了 Mesh 后才能提交给 GPU 并行计算。我们管这个过程叫三角剖分,可见三角剖分是联系复杂多边形和三角形之间的桥梁。

  复杂的多边形

  如何定义多边形?在计算几何里面也是一个比较麻烦的问题,常见的多边形可以是下图这样的。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  这些还是多边形家族中一小部分。当我们说起多边形,可能第一印象想起的是矩形,矩形是最简单的凸多边形,它也存在一些非常重要的性质。

  region 这类数据结构在表示区域的时候,会使用多个不相交的矩形来进行数学表达。如果存在相交的情况可以利用线扫描快速剔除重叠的区域。这就是利用了他足够简单的特性,运算速度可以飞快。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  如上图所示,看起来杂乱无章实际上也是一个合法的多边形。这样的多边形也应该被算法正确的处理,比如三角化,甚至做一些布尔运算。

  多边形规范

  在图形学中会使用一些关键点序列来描述一个多边形。通常认为沿着关键点序列的顺序行走,左手边代表多边形的内部,相反右手边代表多边形的外部。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  如上图阴影部分代表多边形的区域,它有内外 2 个顺序相反的多边形组成。按照前文的定义很容易就可以得到多边形的区域,同样也很容易用程序的方式存储这个多边形。

  它还是比较简单的,实际运算过程中允许多边形存在父子关系(用来存储含岛多边形),也允许一个多边形的定义存在多个不连通分量,从这个角度多边形是典型的递归定义。

  对上面这个多边形进行硬件加速渲染,就需要对它进行三角剖分,如下图红色虚线构成的三角形网格。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  这里有一个问题,类似于圆这样的“多边形”应该如何处理?对于曲线需要先进行离散化,一般在处理的过程中会传递一个忍受值,当离散相邻的两个点之间的距离小于忍受值就不在进行细分了。所以曲线可以看成由许许多多的“短”的线段围成的多边形。

  时至今日三角剖分算法已经是计算机图形学中一个成熟的话题了。常见的三角剖分算法比如 “Monotone”、“EarCut” 等等。

  其中 Mapbox(一家专注以地图渲染的公司)就开源了一个袖珍精巧的基于“Earcut”的剖分算法。还有一些剖分算法对生成的三角形的形状具有有一定的约束,比如“符合德劳内的三角剖分算法”。在工业领域当然不希望剖分出来的三角形又长又细。因为这样用做零件加工、存储和运输都十分不方便。

  画一条直线

  有了前文的理论支持,现在开始面对一些实际的问题吧,比如从画一条直线开始。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  在几何中只需要 2 个端点的坐标就可以描述一条线,从数学的角度看线是没有宽度的。如果我们需要绘制一条有宽度的线就需要把线转化成面(或者是一个矩形)。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  利用给定的线宽并沿着直线的法线方向(一条直线有两个法线方向,互为相反向量)进行偏移。就可以得到一个矩形,对这个矩形进行剖分就可以得到由 2 个三角形组成的三角网格。GPU 可以高效绘制这个网格,用以表示这条有宽度的线。

  画一条折线

  稍微复杂一些,但是原理和绘制一条直线基本类似。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  如上图所示,最后得到了 4 个三角形的网格,分别是红色、绿色、橙色、紫色 四个三角形。在渲染器中,可能还需要指定线的端点和交点的样式。比如圆角端点,交点的长度限制等等。这些都可以用计算几何的方式得到,这里就不做过多的介绍。

  贝塞尔曲线

  前面我们探讨了一些基础的几何知识。已经可以从面(也就是多边形)和线(甚至是曲线)得到对应的三角形网格。程序可以构建一些简单的多边形(比如矩形,圆形,椭圆等等)然后把这些多边形转化成三角形网格,但是如果想要绘制下图图形则会发现有些难度。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  难度体现在如何得到图形的轮廓,也就是如何构建或者优雅的描述这样一个复杂的多边形。就像美术从业人员会用 Photoshop 这类产品做产品的原型设计,大多会用到一个叫“钢笔工具”的绘图功能。它通过使用分段 3 阶贝塞尔曲线来拟合几乎任何图形的外围轮廓。Skia 的 SkPath 类的功能就和“钢笔工具”类似。

  具体内部原理并不复杂,实现的难度并不大,这里就不过度对其实现原理加以概述。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  上图简单的描述了用分段贝塞尔曲线来拟合椭圆并三角化成 Mesh 的整个流程。

  最后做个科普贝塞尔曲线不是贝塞尔发明的,贝塞尔曲线也不是唯一可以用来做拟合的工控曲线。机械加工有时候要求零件表面曲率平滑,也就是曲线二阶导数平滑那么贝塞尔曲线就无能为力了。但是在图形这个分支下贝塞尔曲线和贝塞尔曲面倒大放异彩。

  建模构形

  尽管通过塞尔曲线有着非常好的拟合的特性。但是在构建复杂多边形轮廓的时候,完全通过贝塞尔曲线来拟合还是不够方便。

  如果把贝塞尔曲线构建的面所围成的区域看成一个集合,如果可以像数学集合一样进行 “并交叉” 运算,就可以更加方便的操作二维空间。在图形学中把这类操作看成多边形的“布尔”运算,操作的过程可以当做多二维多边形的建模或者叫构形。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  多边形减法

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  多边形加法

  就如上图所示,如果直接构建左侧的多边形会有一些困难。但是利用多边形的布尔运算就比较容易了。

  多边形堆叠

  一个复杂多边形的数据定义出现了一部分区域和另一部分区域重叠,这个时候问题就开始变的异常复杂了。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  不仅仅在多边形定义的过程中会出现多边形区域重叠。回想一下绘制折线的过程需要对折线中的子线段进行法线平移,相当于扩大了线段描述的区域。那么扩大了区域的同时难免会出现多边形区域重叠。而渲染器在执行渲染前需要对多边形进行堆叠的剔除。

  布尔运算

  在详细描述如果解决多边形堆叠问题前,先来了解一下多边形布尔运算。Skia 中存在对 SkPath 的 OP 操作就是对这个算法的实现。剔除多边形堆叠就可以简化成对多边形“自己”和“自己”求并集。

  这是一个古老的数学问题。不仅在图形学中存在,在材料科学等领域都有广泛的使用场景。数学家们为了找到这个问题的完美解,历时长达 50 年,直到 1990 年 Vatti bala 发表的博士毕业论文《A Generic Solution to Polygon Clipping》才标志着这一问题被解决。

  值得一提的是中国前计算几何协会的会长、浙江大学前数学系的主任、中国计算几何的泰斗梁友栋教授早在 Vatti 这篇论文发表的 10 年之前给出了一个存在约束条件的解,也就是著名的 Liang-Barskey 算法。

  直到 2009 年时隔 20 年后 Francisco Mart?′nez 发表了《A new algorithm for computing Boolean operations on polygons》貌似给出了一个更快的解决方案。此外从行业的经验来看,Boost 库中的多边形运算的子库被认为是错误的实现。

  由于《A Generic Solution to Polygon Clipping》这篇文章,后续这类算法和衍生算法被简称为 GPC。

  GPC 通用多边形裁剪

  得到 GPC 的过程非常坎坷,但是算法本身却十分容易描述。如下简要的描述下算法的整个过程。为了简单,采用下图 2 个凸多边形的并集运算作为样例。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  算法的关键在于求出边的“交点”和“交点的进出性”。“交点”相对比较容易理解,姑且不表。“进出性”可以用来表达交点和对应多边形的关系。比如下图交点 “C0” 如果从多边形 B 的 B0 点出发,那么“C0”点对于多边形 A 来说是“外部”进入到“内部”,相对应的“C0”点就是多边形 B 的出点。“进出性”对后续的多边形裁剪有着非常重要的意义。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  如上图所示,多边形 A(A0,A1,A2,A3,A4)和多边形 B(B0,B1,B2,B3)。首先计算出所有的边的交点,并计算出交点相对多边形的进出性。然后随机选取一个交点沿多边形一边进行“行进”直到遇到下一个交点。交点代表着分叉口,通过“进出性”来选取对应的路线。递归整个过程,直到全部的交点都被处理掉。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  如上图所示,“C0”作为起点开始处理,直到遇到下一个交点“C1”。考虑到“C1”的“进出性”和当前是求多边形的“并集”,故选取“C1-B2”这条路线,直到所有的交点全部被处理。就能够得到新的多边形(C0,A0,A1,A2,A3,C1,B2,B3,B0),这个多边形就是剔除了堆叠后的并集。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  最后要解决的是如何快速求解多边形边的交点?尤其当多边形异常复杂的情况下。这个可以通过线扫描配合优先队列的方式来完成。此类算法在诸多论文中都有详细的描述,不做详细研究。

  上图只是描述了一个最简单的情况,真实的情况下一般是下面这样:

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  请自行脑补......

  抗锯齿

  抗锯齿本来和几何没有什么关系,比如游戏中常用的抗锯齿技术:

  SSAA:拉大画幅后再线性插值缩小画幅的方式来抗锯齿MSAA:硬件提供的多重采样后再 resolve 的抗锯齿技术FXAA : 通过后处理的算法来来抗锯齿算法

  这些抗锯齿算法在游戏这类全画幅处理中起到了很好的效果,但是在矢量渲染器中就不太合适,由于矢量描述多边形拥有明确的边界。算法只需要处理多边形的边界,像素的过渡中过滤高频跳变就可以达到完美的抗锯齿。所以可以在边界进行低通滤波,也可以通过其他技法来模拟这一过程。这里采用轮廓区域拓展 + 径向渐变的方法来间接模拟低通滤波。就拿绘制斜线的例子来说:

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  上图前三个步骤和前文的描述没有任何区别。在最后一步对轮廓进行了一次扩展,上图所描述的多边形简单,如果对任意复杂度的多边形执行这个过程就非常复杂了。这个过程叫“Polygon Offsetting” 具体实现可以参考 《Polygon Offsetting by Computing Winding Numbers》Paper no. DETC2005-85513 pp. 565-575 这篇论文中描述。

  在正确进行了外轮廓的拓展后,多边形原本的区域被称为“实部”,扩展出来的部分被称为“虚部”。“实部”依旧按照正常的渲染方式进行,此外从“实部”径向渐变过渡到“虚部”的边缘就可以模拟出抗锯齿的效果。

  总结

  如前文所述,从分段贝塞尔曲线到二维构形,从多边形堆叠到通用多边形并交差。已经具备了完善的二维建模的能力,也配备了操作二维图形的手术刀。配合三角剖分算法可以完成和 GPU 的对接。

  硬件加速的必要性

  在计算机显卡还没有普及的年代,UI 依赖的矢量渲染器都是通过 CPU 来实现,CPU 通过线扫描为主的一系列算法来完成像素染色。此后 GPU 得到了广为普及,由于 GPU 的设计天然不适合来进行矢量渲染。故在早期尝试使用 GPU 来加速矢量渲染的尝试中大多得到的都是负优化。

  这是由于为了适应现代 GPU 的运算模式,不得不在提交 GPU 之前做很多预处理。包括但不限于 “三角化” “特殊的边缘抗锯齿算法” 等等,但是在软渲染的流程中则简单的多。

  显卡尽管可以比 CPU 更快速的处理像素,但是像素的成本处理在整个过程中占比不高。随着显卡速度越来越快、屏幕分辨率越来越高、显卡的驱动标准进一步提升,这些问题得到了反转。目前硬件加速矢量渲染已经作为重要的优化手段来使软件界面更加流畅。

  裁剪

  此裁剪和几何部分的多边形裁剪并不一样。特定场景下渲染器需要对渲染的结果做一些限定,比如上层的渲染逻辑只希望部分绘制的结果被用户看到。就像 Android 中父 View 限定子 View 的绘制不能超过父亲指定的区域一样。

  硬件提供的裁剪

  几乎所有的显卡都提供了 scissor 的能力。我们在渲染前给显卡前设置一个矩形区域,如果有像素超过这个窗口就会被显卡丢弃掉。

  但是显卡自带的裁剪能力要求裁剪的区域必须是一个矩形,并且这个矩形还不能够旋转。如果要裁剪一个奇异形状就无能无力了,这极大限制它的使用场景。但是由硬件直接提供的能力性能非常好,对渲染无侵入。

  ClipPath

  Skia 中提供了一个裁剪画布的接口 ClipPath,它可以把一个贝塞尔曲线围成的区域作为裁剪的区域。它的功能很强大,几乎可以涵盖全部的裁剪需求,如果不是性能太差就没有必要提及其他的方式了。

  如果需要通过 ClipPath 来实现对画布的裁剪,需要先构建一个和画布一样尺寸的掩码图。然后把区域绘制到掩码图上,在后续的绘制过程中要逐像素采样掩码图来判断要不要剔除。当然这个过程非常的繁重,体现在三个方面:

  需要对区域做预处理,甚至需要做堆叠剔除需要对贝塞尔曲线包围的区域做三角化需要消耗一次额外的绘制操作

  正如前文描述的那样,复杂曲线围成的区域处理起来都非常复杂而且慢。

  更快的数据结构

  为了解决或者说部分解决 贝塞尔曲线的复杂度带来的性能损耗。可以使用多个矩形来表示一个复杂区域,但是要求矩形之间不能存在堆叠。下图描述了如何剔除矩形之间的堆叠,只需要执行一次线扫描算法即可。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  同样矩形非常容易就可以剖分成三角形,并不需要使用复杂的三角剖分的算法。所以可以快速构建对二维区域的描述。同样基于“矩形集合”的二维区域描述非常容易构建出并交差等运算。而且相关的碰撞检测算法也非常容易实现,但是对于需要使用曲线包围的区域就显的比较乏力了。

  Skia 中使用 SkRegion 这个数据结构来对这个算法进行描述。

  SDF 快速剔除

  SDF(Signed Distance Field,有向距离场),这里用了一种模拟 SDF 的方法来进行快速的裁剪。它发生在光栅化后像素处理的最后阶段。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  比如上图中像素 P 和像素 Q,如果需要保留多边形 (A,B,C,D,E)区域。那么就要找到一个办法来区分像素 P 和 Q 谁落在多边形内,谁落在多边形外。这不是一个很麻烦的事。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  如上图所示,从多边形任意一个点进行“行进”,图中从 C 点开始。那么向量 CB 需要逆时针旋转才可能和向量 CP 重合,也就是 P 点在向量 CB 的左侧,相反像素 Q 在向量 CB 的右侧。

  循环一周会发现点 P 永远在左侧,而 Q 则有时候在左侧有时候在右侧。至于左右可以通过向量几何的叉积的正负来判断。通过这个特性可以判断像素是不是处于多边形的包围中。

  算法中可以通过这个原理构建 SDF 的核函数。理论上只能对凸多边形有效果,其他多边形需要转化成多个凸多边形后依次加以判断(实际上还存在其他问题)。在判断像素的时候同时需要找到像素距离每个边的最短距离,通过这个距离可以控制边缘的一些策略用来抗锯齿。

  Skia 中并未暴露 SDF 相关的策略接口,但是在内核代码中存在类似的实现。

  万字长文:我是如何把 Skia 的体积缩小到 1/8 的?

  上文描述了几个典型的裁剪方式。理论上需要瘦身上层业务进行合理的选择,用以达到最佳的性能,而不是无脑的 ClipPath。

  和 Skia 的差异

  SaveLayers

  暂时不觉得需要提供 SaveLayers 这类接口。

  CPU Backend 渲染器

  只支持硬件加速渲染,尽可能的多支持不同的硬件。不考虑 CPU Backend,对普众的消费电子设备显卡应该和 CPU 一样是标配。不存在 GPU 的设备不在考虑范围内。

  为什么体积会小这么多

  主要有以下几个因素

  我们实现的渲染器不支持 CPU 软渲染策略,一些都是为了硬件加速设计的,更加简洁。作为 Shader 处理的逻辑,核心的编译器相关模块都是离线实现。同样的 Skia 的 SKSL 编译器需要内置到 Skia 的核心逻辑中难以剥离。由于 Skia 的历史非常悠久,存在相当多的 legacy 性质的代码和模块。

  总结

  至此整个矢量渲染器的核心技术就已经描述完毕。每个部分单独实现难度并不大,但是集合起来构建一个完备的项目还是太“顶”了。如果你也想实现一个类似的渲染器,那么祝你好运。

  关于渲染器的未来使用场景

  跨平台

  这个方向毋庸置疑,未来类似 Flutter 这样的跨多端的会慢慢变成主流(多年前笔者从事 Windows 开发,就是先用系统的渲染器绘制一套 UI 体系,然后在上面做各种业务。Flutter 在移动端算一个新东西,但是业界早有类似的解决方案),那么构建适合自己业务的渲染引擎非常有必要,也是技术实力的体现。

  Mini 浏览器

  随着前端的敏捷的开发方式慢慢在整个行业得到接受,国内有众多尝试在 系统原生组件或者 Flutter 上构建类似浏览器的逻辑(远不如浏览器那么复杂)。我把这类项目称为 Mini 浏览器项目,那么渲染器可以最大化减少包体积,提高渲染性能。

  天下没有免费的午餐,没有哪一个硬件渲染器能够保证,随意使用其 API 就能得到好的性能。Flutter 本身也因为过多使用 Skia 的 ClipPath 和 SaveLayers 导致性能低下。对渲染本身足够理解、对硬件的足够理解,知己知彼才能做到最好。

  混合渲染

  纵观全文,我都致力于把二维渲染实时转化成由三角形构成的 Mesh。那么 3D 游戏为什么可以在渲染复杂的场景下提供好的性能?原因在于 3D 游戏中使用的 3D 模型大多都是通过 “3DMax” “Maya” “Blender”这里建模工具离线构建的。

  从三角形的 Mesh 角度来说,2D 和 3D 没有本质区别,所以可以混合到一起渲染。这会带来一些新的原来不具备的特性。移动设备时至今日运算能力已经很强了,但是交互方式却没有大的变化,随着混合模式下的渲染会带来更加新颖的体验的交互模式。

  作者介绍

  陈国栋,主要从事多端跨平台、计算机图形学、编译器、高并发、分布式共识和一致性的研究和实践。曾在腾讯、百度、蘑菇街、爱奇艺等公司任职。目前就职于字节跳动 Client Infra 团队。我所在的团队对外负责的技术产品有 Lynx(移动端动态化跨平台引擎)、JavaScript 虚拟机、浏览器内核、自渲染框架内核等。欢迎有意向在以上几个方向参与研究和研发的同学加入我们。

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多