在 FFmpeg 8.1 版本中,一个名为 drawvg 的实验性 filter 正式加入,它为视频处理管线引入了自定义矢量图形渲染能力。与传统的 drawtext 或 drawbox 等简单图形滤镜不同,drawvg 提供了一套完整的脚本语言 VGS(Vector Graphics Script),允许开发者以声明式方式描述复杂的二维图形,并将其合成到视频帧之上。

drawvg 在 Filter 管线中的定位

FFmpeg 的 filter 机制是其多媒体处理架构的核心组件。每个 filter 接收一个或多个输入帧流,经过处理后输出到下游。在典型的 filtergraph 中,数据沿着输入节点流向输出节点,中间可以分叉、合并、或者循环。drawvg filter 在这个架构中扮演的是「图形叠加层」的角色:它读取上游的视频帧作为背景,然后在其上渲染 VGS 脚本描述的矢量图形,最后将合成后的帧传递给下游的下一个 filter。

从技术实现角度看,drawvg 的工作流程涉及三个关键阶段。首先是脚本解析阶段:VGS 脚本在 filter 初始化时被解析并编译为内部的指令序列,这个解析过程利用了 FFmpeg 自身的表达式求值机制,因此脚本中可以直接嵌入 FFmpeg 表达式。其次是帧级渲染阶段:对于每一帧输入,drawvg 创建一个 Cairo 绘图上下文(cairo_t),将视频帧作为目标表面,然后逐条执行编译后的 VGS 指令流。第三是输出阶段:渲染完成后的帧被传递到 filterchain 的下一环。

这种设计使得 drawvg 能够与 FFmpeg 生态中的其他 filter 无缝协作。一个典型的用例是将 cropdetect 的计算结果通过元数据传递给 drawvg,从而在视频上可视化地标注自动检测到的裁剪区域。cropdetect filter 会将计算出的裁剪参数(x、y、width、height)写入每帧的元数据,而 drawvg 可以通过 getmetadata 命令读取这些值并绘制对应的矩形边框。

VGS 语言的设计哲学与核心语法

VGS 语言的语法设计大量借鉴了 SVG 的路径语法和 PostScript 的绘图命令,但做了大量简化以适应视频渲染这个特定场景。语言的核心概念是「路径」(path):一系列由线段和曲线组成的几何形状,这些形状可以用于填充(fill)或描边(stroke)。

一个最简单的 VGS 脚本只需要几行代码即可绘制一个圆形:

circle (w / 2) (h / 2) (w / 3)
setcolor blue
fill

这段脚本在帧中心绘制一个蓝色的填充圆。值得注意的是,表达式 wh 是 drawvg 提供的内置变量,分别代表输入帧的宽度和高度。这种设计使得同一个脚本可以自适应不同分辨率的视频,无需手动指定坐标。

在命令解析层面,VGS 采用了空格分隔的简洁语法,与传统编程语言的大括号和分号风格形成鲜明对比。命令名称还支持单字母缩写形式:moveto 可以写作 Mlineto 写作 Lclosepath 写作 Z。此外,连续使用相同命令时可以省略名称,例如 l 10 10 20 20 30 30 等价于 l 10 10 l 20 20 l 30 30

路径绘制只是 VGS 能力的冰山一角。该语言还支持完整的变换矩阵操作(translate、rotate、scale、scalexy),用户可以通过这些命令对后续的所有绘图操作应用平移、旋转和缩放。状态栈机制(save 和 restore)允许保存和恢复当前的绘图状态,包括当前颜色、变换矩阵、线宽、虚线模式等,这对于构建复杂的分层图形十分有用。

动态渲染与帧级动画

drawvg 最强大的特性之一是支持基于时间的动态渲染。VGS 脚本中可以引用内置变量 t(时间戳,单位为秒)和 n(帧编号),这使得图形能够随视频播放而自动演变。结合 FFmpeg 表达式丰富的数学函数库,开发者可以创造出平滑的动画效果。

例如,下面的脚本绘制了一个从左到右摆动的圆形:

circle (w / 2 + sin(2 * t) * w / 4) (h / 2) (w / 5)
setcolor teal
fill

sin(2 * t) 产生一个周期为 π 秒的正弦波形,圆的中心点随之在帧中心两侧往复运动。这种动画完全在 GPU 或 CPU 上实时计算,无需预渲染。

对于更复杂的动画需求,VGS 提供了 repeat 循环和 if 条件判断。循环变量 i 在每次迭代时自动更新,配合 randomg 函数(drawvg 特有的全局随机数生成器)可以创建随机分布的粒子效果。下面的脚本在每帧生成 50 条下落的雨丝:

rect 0 0 w h
setcolor midnightblue
fill

setcolor white

repeat 50 {
    setvar offset (t * (randomg(0) + 1))

    moveto
        (mod(randomg(0) + offset / 6, 1) * w)
        (mod(randomg(0) + offset, 1) * h)

    rlineto 6 36
    setlinewidth (randomg(1) / 2 + 0.2)
    stroke
}

这段代码巧妙地利用了取模运算和随机数,使雨滴从顶部不断落下并循环。

与其他 Filter 的深度集成

drawvg 之所以在视频处理管线中具有独特价值,关键在于它能够作为 filtergraph 中的中间节点,与其他 filter 形成复杂的协作关系。一个典型的集成模式是使用 alphamerge 和 overlay 创建自定义转场效果。

在这种模式中,drawvg 负责渲染一个灰度遮罩(白色区域表示可见,黑色表示透明),这个遮罩通过 alphamerge filter 被应用到目标视频的 alpha 通道上,然后 overlay filter 将处理后的视频叠加到另一个视频之上。这种技术可以实现任意形状的转场,而不受 xfade filter 内置转场效果的限制。

另一个有价值的应用场景是与 displace filter 配合创建波形扭曲效果。drawvg 渲染出不同灰度的矩形图案,经过 boxblur 平滑处理后,作为 displace filter 的 xmap 或 ymap 输入,引导像素的位移方向和幅度。这种组合可以在不修改原始视频编码的情况下实现丰富的视觉特效。

性能考量与工程实践

在实际部署中,drawvg 的性能表现取决于几个关键因素。首先是 VGS 脚本的复杂度:包含大量路径、循环和复杂表达式的脚本会显著增加每帧的处理时间。其次是输出分辨率:高分辨率视频(如 4K 或更高)意味着更大的绘图画布和更多的像素处理。第三是 Cairo 渲染后端的選擇:默认情况下 drawvg 使用软件渲染,但对于支持硬件加速的场景,可以通过配置 Cairo 的 OpenGL 或 Vulkan 后端来提升性能。

调试 VGS 脚本时,print 命令非常有用。它可以输出任意表达式的计算值到 FFmpeg 的日志系统,帮助开发者理解脚本在每帧的实际行为。然而需要注意,过度使用 print 会产生大量日志输出,影响整体处理效率,建议在生产环境中移除或禁用调试输出。

小结

drawvg filter 为 FFmpeg 引入了一套灵活且强大的矢量图形渲染方案。通过 VGS 语言,开发者可以在视频处理管线中动态绘制自定义图形,创建动画效果,并与已有的大量 filter 形成协作。从技术架构来看,它将 Cairo 的高质量 2D 渲染能力与 FFmpeg 的 filter 图灵完备性相结合,为视频后期特效制作提供了一个新的工具选择。

资料来源:drawvg 官方文档(https://ayosec.github.io/ffmpeg-drawvg/)和 FFmpeg 官方语言参考(https://ffmpeg.org/drawvg-reference.html)。