2D矢量动画在B站的探索与实践-矢量图形的描述
刘孙瑞
移动技术部高级开发工程师
钱爱娟
移动技术部开发工程师
随着互联网视频行业的发展,用户对App交互过程中的互动动画和视频中的特效动画,有着越来越高的要求。传统App中大量存在的交互动画,一般采用帧动画的实现形式(每一帧都是一张独立的图片)。帧动画在客户端有着实现简单的优点,但随着应用功能的不断增多,帧动画的缺陷也越来越明显,帧动画由一张张图片组成,资源体积上占用十分大。同时客户端的平台不断增多,设备的屏幕大小也各不相同,要想不同大小屏幕上实现同样效果的动画,需要设计提供不同分辨率大小的资源,这在工程实践上十分不便。与帧动画不同的矢量动画,则解决了这些痛点。矢量动画通过描述图形的绘制行为,来存储图像。这样的存储方式,在资源大小上有着天然的优势,同时矢量动画的绘制可以根据场景的分辨率进行实时的缩放与抗锯齿,较好的解决帧动画在缩放大小时的走样问题。
市面上已有lottie,svga,pag等优秀的矢量动画库,但是在B站业务场景下,并不能完美的解决自身业务痛点。B站拥有着一套自研的渲染引擎 Chronos,目前已支撑了移动端视频播放界面弹幕等视频特效业务,部分视频中的弹幕特效,以及大部分视频中的三连组件。Chronos 拥有对GPU接口的抽象和渲染管线,我们可以在Chronos基础上,完成一个矢量动画绘制模块,解决B站部分场景下特效动画的痛点。
想象我们回到幼儿园美术课,在美术课上要画一颗树我们要怎么做。
我们先要将画笔挪到画纸上一个特定位置,先画一个圆,表示树冠部分,再将画笔挪到树冠的下半部分,画一个矩形表示树干。然后再分别对树冠和树干进行上色。
以上讨论的是如何绘制一张静态的矢量图,如何让这张图动起来,也是个不小的问题。最简单的方案是将描述静态矢量图中的一部分参数,使用关键帧描述,然后对其进行插值,包括对颜色进行插值,对曲线进行插值等等。这样可以完成,粉色正方形变为蓝紫色的圆再变绿色的四叶草的动画。
矢量动画大致流程如下图,本系列文章将从最基础的几何知识开始讲起,
先从静态矢量图形的描述,然后通过三角剖分,再到绘制一个静态矢量图形。
而后讨论如何实现矢量图形的描边与抗锯齿等进阶功能,支持画出更炫酷的矢量图。
最后讨论如何实现一个动画系统,让静态的图形动起来。
本文中,我们主要讨论两件事,一是静态矢量图形如何描述,二是讲解一些必要的曲线相关的几何基础,为后续文章提前做好数学储备。
矢量动画要解决的第一个问题是,如何描述绘制行为。画画的时候,我们需要挪动画笔,因此我们需要MoveTo的绘制动作。我们同样需要画直线,需要LineTo的绘制动作。那么曲线如何描述呢?常用的曲线描述一般采用贝塞尔曲线的方式。贝塞尔曲线的阶数越高,我们可以拥有更多的控制点来描述曲线的变化。常用的二、三阶贝塞尔曲线即可满足大部分业务需求,通过QuadTo和CubicTo来表示这两种曲线的绘制。但是贝塞尔曲线无法描述椭圆、双曲线等圆锥曲线,这需要引入一种新的曲线——有理贝塞尔曲线,有理贝塞尔曲线通过ConicTo来表示。
通过以下五种最基本的 绘制动作 就可以描述大多数图形:- MoveTo (p) :把当前顶点移动到新的位置 p
- LineTo (p) :把当前顶点移动到新的位置 p,并连接一条线
- QuadTo (p1, p2):画一条以当前顶点,p1,p2为控制点的二次贝塞尔曲线
二次贝塞尔曲线 :3个控制点 ,曲线方程为:
![]()
- CubicTo (p1, p2, p3):画一条以当前顶点,p1,p2,p3为控制点的三次贝塞尔曲线
三次贝塞尔曲线 :4个控制点 ,曲线方程为:
![]()
- ConicTo (p1, p2, w):画一条以当前顶点,p1(w为其权重),p2为控制点的二次有理贝塞尔曲线
二次有理贝塞尔曲线 (标准形式):3个控制点 ,曲线方程为: ,正实数 是与控制点 相关的权重, 的大小决定了 所代表的圆锥曲线的类型:
当 时, 是椭圆 当 时, 是抛物线 当 时, 是双曲线 ![]()
拥有以上对图形轮廓的描述,加上填充规则,可以描述大多数业务场景下的绘图需求。
描述图形的 Path 类包括以下 主要数据成员 :- 动作数组 vector<DrawVerb> draw_verbs_
- 顶点数组 vector<glm::vec2> draw_points_
- 权重数组 vector<float> conic_weights_
上述三类数据的对应关系如下:
![]()
- 填充规则 ,包括:
-
奇-偶规则 :从点 往任意方向作一条射线,若与该射线相交的边的数目为奇数,则 是图形内部点,否则是外部点;
-
非零缠绕规则 :首先使图形的边变为矢量。将环绕数初始化为零。再从点 作任意方向的一条射线。当从 点沿射线方向移动时,对在每个方向上穿过射线的边计数,每当图形的边从右到左穿过射线时,环绕数加1,从左到右时,环绕数减1。处理完图形的所有相关边之后,若环绕数为非零,则 为内部点,否则, 是外部点。
Path 类中的绘制动作中包含三种曲线,即二次/三次/二次有理贝塞尔曲线。我们之后的一系列算法需要大量相关知识的支撑。因此,我们在接下来的部分将详细介绍涉及的两大类曲线,即贝塞尔曲线以及有理贝塞尔曲线,包括曲线的定义、重要性质和相关重要算法。当然,我们的介绍重点还是落在涉及到的三种特定曲线上。
次贝塞尔曲线 : 个控制点 ,曲线方程为:
基函数
一阶导数为:
可以发现, 的一阶导数是由 个控制点 定义的 次贝塞尔曲线
-
次曲线 经过 第一个控制点 和最后一个控制点
- 由于 ,非负性显然
-
基函数 是 的二项式展开中的系数,即 - 由非负性以及和为1,可以得到任何基函数的值都属于
-
令 ,则 -
令 ,则
我们经常需要访问曲线上任意一点的坐标,而 De Casteljau 算法是计算贝塞尔曲线上任意参数 对应坐标的一般方法,因此接下来我们将简单介绍该方法!
如何在曲线上找到给定参数 对应的点 ?
-
一个简单的方法是将 代入每个基函数,计算每个基函数与其对应的控制点的乘积,最后将它们相加。虽然这很直接,但它在 数值上不稳定 (即,在评估伯恩斯坦多项式的过程中可能会引入数值误差) -
一种 数值稳定 的递归算法 De Casteljau 算法——绘制曲线的 几何方法 ,且非常容易实现
基本概念 : 在线段 中选择一个点 ,使得 以 的比率划分线段 ,则
几何解释 : 为了计算 次贝塞尔曲线上的点
为了方便描述算法,我们将原始控制点表示为 ,其中下标第一位的0表示第0次迭代,第2次迭代则为 ...
-
首先将 个控制点连接形成一条折线 -
利用上述公式,计算出折线中每条线段 上的点 ,使得点 分该线段的比率为 。这样,我们将得到 个点 ,定义了一条由 条线段组成的新折线 -
将该过程应用于这条新的折线,得到第二条由 个点 和 条线段组成的折线 -
重复这个过程 次产生单个点
-
首先,将所有给定的控制点排列成一列,即图中最左边的一列 -
对于每对相邻的控制点,画一个东南向箭头和一个东北向箭头,并在两个相邻箭头的交点处写下一个新点
例如,如果两个相邻点是 和 ,则新点是 。东南/东北箭头表示将 / 乘以它的尾部点 / ,新点即为求和:
- 从初始列第 0 列,我们计算第 1 列;从第 1 列我们得到第 2 列,依此类推。最终,在 n 次应用之后,我们将到达一个点 ,这就是曲线上的点!
De Casteljau 算法的 正确性证明 [1]
在之后的算法中(例如折线化、描边等),我们经常需要将原始贝塞尔曲线拆分成多个同阶的贝塞尔曲线以进行下一步操作。因此,介绍曲线的细分算法是十分必要的!
细分 (Subdividing) :将给定的贝塞尔曲线在某个 对应的 处分裂成两条曲线,每条曲线仍然是一条贝塞尔曲线
- 因为生成的贝塞尔曲线必须有自己的新控制点,所以 原始控制点集被丢弃
-
分裂后的每段都是原始 次贝塞尔曲线的子集,因此 新生成的贝塞尔曲线必须是 次
明确目标 : 给定一组 个控制点 和一个在 范围内的参数 ,要找到两组 个控制点 和 ,使得:
-
由 定义的贝塞尔曲线是原始贝塞尔曲线在 的部分 -
由 定义的贝塞尔曲线是原始贝塞尔曲线在 的部分
下图说明了这些点的选择:
-
左边的折线由点 组成(蓝色折线) -
右边的折线由点 组成(粉色折线)
回想一下 De Casteljau 算法的三角计算方案:
-
对于给定的 ,计算 需要 次迭代 -
在计算过程中,可以收集每列上的第一个和最后一个点 - 第一个点的集合给出了与定义在 上的原始曲线片段相对应的细分
- 最后一个点的集合给出了与定义在 上的原始曲线片段相对应的细分
因此,在下图的三角形方案中,箭头方向的 上边缘 和箭头相反方向的 下边缘 分别提供了第一段和第二段曲线的控制点
细分算法的 正确性证明 [2]
使用多项式的参数表示不够强大,这是因为 很多曲线 (例如,椭圆和双曲线) 无法通过这种方式表示 。克服这个缺点的一种方法是使用 齐次坐标 。例如:
-
空间曲线 用四个函数而不是三个表示: -
平面曲线 用三个函数而不是两个表示:
其中 是某个闭区间 中的参数,将上述曲线转换为它们的常规形式:
-
空间曲线 : -
平面曲线 :
显然,如果 ,即为常函数,齐次形式将简化为多项式形式。
为了区分,我们将:
- 齐次形式的参数曲线称为 有理曲线 (rational curve)
- 多项式形式的曲线称为 多项式曲线 (polynomial curve)
次有理贝塞尔曲线 : 个控制点 ,每个控制点 都与一个非负的权重 相关联 ,曲线方程为:
基函数
贝塞尔曲线到有理贝塞尔曲线的 推导过程 [3]
-
次曲线 经过 第一个控制点 和最后一个控制点 -
凸包性 :有理贝塞尔曲线完全位于给定 个控制点的凸包中 -
波折递减性 :如果曲线在平面中,则意味着直线与有理贝塞尔曲线相交的次数 不超过 与曲线的控制折线(相邻控制点连接的折线)相交的次数 - 射影不变性 :将射影变换应用于有理贝塞尔曲线,等价于将射影变换应用于其控制点而得到的有理贝塞尔曲线
-
修改控制点 的权重 会将曲线 推离或拉向 控制点 ( 具体证明 [4] )
Path 类中引入二次有理贝塞尔曲线的目的是为了描述圆锥曲线,因此,接下来我们将使用部分篇幅来介绍二次有理贝塞尔曲线如何表示圆锥曲线!
所有的圆锥曲线都可以表示为以下形式,其中 、 、 不同时为 0:
由三个 非共线 控制点 、 和 定义的2次贝塞尔曲线是抛物线的一段,我们希望 扩展这个概念 来定义椭圆和双曲线段!
假设 经过 、 并分别 在 、 处与 、 相切 的二次曲线由上述的2次隐式方程 表示,其中六个系数是未知数。如果 不为 0,我们可以将整个等式除以 ,得到如下方程:
那么,我们实际上只有 五个未知数 !(如果 为零,我们还是有五个未知数)
方程 的梯度为:点 处的切线与梯度相互垂直,又梯度的斜率为 ,所以 处的切线斜率为
因此我们可以得到以下四个方程:
-
曲线通过 : -
与 处的圆锥曲线相切: -
曲线通过 : -
与 处的圆锥曲线相切:
现在,我们得到了四个方程,每个方程关于未知数 、 、 、 和 都是线性的。如果我们能 找到一个更多的条件 来产生一个线性方程,我们将有5个具有5个未知数的线性方程。求解这个线性方程组会产生所有5个系数,且二次曲线是唯一确定的。
非常自然的想法是添加一个点。将该点的坐标代入方程将得到一个类似于控制点 和 的方程。但是, 这个点在哪里?
- 这个点应该在三个控制点的三角形内,这样才能 保持凸包性质
- 这个点的位置也应该是 “ 规则的 ” ,这样我们就可以很容易地改变这个点来产生另一个圆锥曲线
- 一种方法是让该点位于 连接 和 中点的线段上 。这样,在这条线段上移动该点会产生不同的圆锥曲线
注意到:
-
如果所选点从 的中点向 移动,则它帮助定义的圆锥曲线向控制点 移动 -
如果该点远离 ,它帮助定义的曲线也会远离 - 因此,这个观察使我们推测这个移动点的位置可以 通过分配给控制点 的权重来控制
由于我们只需要一个点,它受控制点 的权重影响,因此我们可以使用具有三个控制点 、 、 的 二次有理贝塞尔曲线 ,权重分别为 1、 和 1,分配给控制点 的权重将控制额外点的位置。二次有理贝塞尔曲线的方程为:
下面,为方便讨论,我们将 和 放 轴的相对两侧,以 的中点为坐标原点。
使用此设置,我们有 。设 的中点为 ,有理贝塞尔曲线在 处与线段 相交,如下图所示:
计算 时的 有: ,表示 和 是同一个点,则有:
若 ,则二次有理贝塞尔曲线变为二次贝塞尔曲线,即抛物线。在这种情况下,点 是线段 的中点。那么。这条有理贝塞尔曲线 何时是椭圆或双曲线?
这就要用到 射影几何中的一个定理 。如下图,从给定圆锥曲线外部的点 绘制两条在 和 处与曲线相交的切线,以及满足在 处与弦 相交且交圆锥曲线于 和 的任意一条割线,其中 在三角形 内。若:
-
曲线是椭圆,这些点的顺序是 、 、 、 - 曲线是双曲线,则 点位于曲线的另一个分支上!
定理表明以下关系成立:
又 ,则有:-
若曲线是 椭圆 ,则有 ,即 -
若曲线是 双曲线 ,则有 ,即
回到二次有理贝塞尔曲线,我们有 , , , , ,则有:
-
若曲线是 椭圆 ,则有 ,即 -
若曲线是 双曲线 ,则有 ,即
至此,我们得到了如何用二次有理贝塞尔曲线来表示不同类型的圆锥曲线!
我们可能会疑惑二次有理贝塞尔曲线 和上述 的形式有没有什么区别,或者说是否是等价的?答案是肯定的,我们只需要重新参数化操作就可以得到二次有理贝塞尔曲线的 标准形式 (后一种 表示方式)!
有理重新参数化的一般形式为:-
时 ,则有 -
时 ,则有
则有: ,
使用 替换 , 替换 ,代入一般形式 ,解出
即二次有理贝塞尔曲线的一般形式可以转化为等价的标准形式:
次有理贝塞尔曲线的有理 De Casteljau 算法 具体可以参考 论文 [5] ,其 递归表示 为:
我们主要关注二次有理贝塞尔曲线(权重为 1、 、1)的有理 De Casteljau 算法:- 权重 的更新方式为:
-
控制点的更新方式为:
- 计算权重 :
-
计算控制点:
-
由控制点 、 、 确定的曲线 ,是原曲线的 部分
到此为止,我们主要介绍了静态矢量图形的描述,以及贝塞尔曲线相关的几何基础知识。下一章中,我们将讨论如何将Path折线化,然后进行三角剖分,敬请期待!
[2] https://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/b-sub-correct.html
[3] https://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/NURBS/NURBS-def.html
[4] https://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/NURBS/NURBS-mod-weight.html#In-Depth-Discussion
[5] Šír, Zbyněk, and Bert Jüttler. "On de Casteljau-type algorithms for rational Bézier curves." Journal of Computational and Applied Mathematics 288 (2015): 244-250