自定义特效、动画、转场资源设计(支持剪映资源)

2025年又快结束了,在年末,将自己之前适配剪映特效、转场以及动画的技术方案拿出来分享一下,期望能对看到的同学提供一点帮助。 特效、转场、动画资源,在视频剪辑领域的重要性不言而喻。开发人员如何让视频编辑 SDK 支持丰富的渲染效果呢?这一直是一个很麻烦的点。一些通用的转场库,如用 GLSL 写的:gl-transitions,可以直接使用单 pass 渲染完成,但是诸如:背景高斯模糊、弹跳等效果,就得专门定制渲染代码去完成了。对此,我设计了一种渲染资源格式,可以支持多 pass、多纹理、动画的渲染资源格式,可以对剪映的静态渲染资源很好的支持(后续会介绍剪映的两种渲染资源:静态/动态)。以下都将特效、转场、动画叫做渲染资源吧。

历史方案的弊端

一开始,我们的渲染资源使用的是 PAG + OpenGL 自己实现。通过将渲染资源内置到 SDK 内部,通过 name 进行索引。这样做的好处很明显:外部不用关注特效实现,产品提渲染需求,技术实现具体的渲染资源。代码仓库就像下面这样:

1766904606621

一开始,没有人觉得不对,直到产品多次提到,为什么我们的转场效果这么差呢?还有伴随而来的问题:增删改渲染资源,全链路都要发布一遍(前端、后端、单元测试等)。发布嘛,倒是其次,但是渲染效果差的问题,在单 pass 的渲染支持下是难以优化的。比如高斯模糊:通常需要 ping-pong 的方式,像乒乓球一样,得需要至少两只球拍(FBO),多次动作(draw call)才能完成。对此,在保留历史渲染资源的情况下,我开始了扩展渲染资源的探索。

渲染资源外包

正常来讲,对于效果类的渲染资源,大点的公司应该专门有一批 TA(技术美术)岗位,在已有框架下实现产品提的效果。SDK 的开发人员需要给 TA 提供配套的使用工具,包括但不限于:

  1. 提供效果编辑器(如动画编辑器,可考虑使用 AE 设计 + 导出实现,参考 PAG);
  2. 提供可用的参数(对于转场的 shader:texture0、texture1、progress 等);
  3. 提供 debug 环境;
  4. 插拔式的线上资源管理;

只有这样,SDK 开发与特效才能解耦。程序员擅长代码架构,TA 擅长视觉效果,专业的事交给专业的人做。就像游戏开发一样,技术可能只占成功因素的 30%,更多还是要靠美术和 TA 对游戏、画面表现效果的理解。 为此,我计划将渲染资源独立 SDK 开发,通过 OSS 管理,效果编辑器的事情,得等有专业的 TA、公司成规模了再开发吧😊(可惜没等到,TA 还是我,给自己写效果编辑器?那得是跟自己多大仇啊)。

资源设计

渲染资源要支持:

这几类效果,同时需要支持多 pass 渲染,最终设计如下:

{
    "renderPass": [
        {
            "fboSize": 1.0,
            "shader": {
                "vert": "xxx",
                "frag": "xxx"
            },
            "asInputTexIndex": [
                {
                    "name": "textureInput",
                    "pipe": 2,
                    "renderPassIndex": 1
                }
            ]
        },
        {
            "fboSize": 1.0,
            "shader": {
                "vert": "xxx",
                "frag": "xxx"
            }
        }
    ],
    "texture": [
        {
            "pipe": [
                2
            ],
            "name": "mask",
            "url": "xxx",
            "repeatMode": 2,
            "renderPassIndex": [
                0
            ]
        }
    ],
    "animation": [
        {
            "name": "animation1",
            "strength": 1.0,
            "speed": 1.0,
            "repeatMode": 2,
            "interpolationType": "linear",
            "channelNum": 3,
            "animationInfo": [
                {
                    "data": [
                        1.0,
                        1.0,
                        1.0
                    ],
                    "time": 0
                },
                {
                    "data": [
                        2.0,
                        2.0,
                        2.0
                    ],
                    "time": 3000
                }
            ],
            "renderPassIndex": [
                0
            ]
        }
    ],
    "uniform": [
        {
            "name": "参数1",
            "description": "参数1描述",
            "type": "float",
            "uniformTarget": "uniform1",
            "defaultValue": 1.0,
            "range": [
                0.0,
                1.0
            ],
            "value": 1.0,
            "renderPassIndex": [
                0
            ]
        },
        {
            "name": "参数2",
            "description": "参数2描述",
            "type": "animation",
            "defaultValue": 1.0,
            "uniformTarget": "animation1",
            "range": [
                0.0,
                1.0
            ],
            "value": 1.0,
            "renderPassIndex": [
                0
            ]
        }
    ],
    "name": "冲刺",
    "desc": "冲刺效果",
    "format": "transition",
    "suggestionDuration": 3000,
    "previewUrl": "xxx",
    "coverimage": "xxx"
}

这里面包含了:

前端拿到这个资源,可以自行构建编辑界面(基本信息+参数+预览信息),如剪映放大镜效果的编辑界面:

1766907482140

资源定义

渲染资源主要包含以下几部分:

  1. 渲染通道定义(renderPass[]);
  2. 外部纹理输入(texture[]);
  3. 动画(animation[]);
  4. 外部参数(uniform[]);
  5. 基本信息(name、desc、format 等,方便编辑器展示);

渲染通道(renderPass)

renderPass 作为一个数组,每一个对象对应创建一次 FBO 和一次渲染调用,结果会存在 FBO 中。由于这里涉及到 FBO 的频繁申请与释放,所以内部要实现一个 FBO 缓存池,从而避免 FBO 频繁创建与销毁带来的性能开销。其中,核心的参数是:

有了以上通道定义,即可实现多 pass 的渲染效果。典型案例是高斯模糊:第一个 pass 水平方向模糊,输出到 FBO1;第二个 pass 读取 FBO1 作为输入(通过 asInputTexIndex 指定),进行垂直方向模糊,最终输出。

这种 ping-pong 渲染方式,将二维模糊拆分为两次一维模糊,既保证了效果质量,又大幅降低了计算量(O(n²) 降到 O(2n))。类似的还有 Bloom 效果:降采样 → 模糊 → 上采样 → 混合,需要 4-5 个 pass 配合。

通过 asInputTexIndex 串联各个 pass 的输入输出,系统可以自动管理 FBO 的生命周期,在不再需要时及时回收,避免资源浪费。

外部纹理输入(texture)

复杂的渲染效果,离不开外部的纹理支持。如 LUT、冲刺、星光闪烁等效果。texture 定义如下:

texture 参数允许在渲染资源中使用外部纹理,极大地扩展了效果的表现力。

最典型的应用是 mask 纹理:通过一张黑白图或带 alpha 通道的视频,控制效果的显示区域和强度。比如星光闪烁效果,可以用一张星星形状的纹理作为 mask;心形转场,可以用心形 mask 控制过渡区域。

除此之外,LUT 纹理用于调色滤镜,noise 纹理用于添加噪点质感,gradient 纹理用于渐变映射。相比硬编码在 shader 中的数据,外部纹理可以随时替换,一套 shader 代码配合不同的纹理,就能实现数十种视觉效果的变体。

动画(animation)

动画数据用于场景中需要动画的部分,无论是顶点动画或是纹理动画,对于渲染 SDK 来说,都是着色器的 uniform 输入。

这里的动画数据,通常由设计同学在配套的设计软件中制作完成。

理想方案是使用 AE 这类专业软件,开发对应的导出插件(如 PAG 的 AE 插件),将关键帧数据导出为 JSON 格式。这样设计师无需了解底层实现,专注于动画效果本身。

另一个方案是自己开发动画编辑器,直接生成符合格式的动画数据,但开发成本较高,适合有大量动画资源需求的团队。对于小规模项目,手写 JSON 配置也是可行的,毕竟 animationInfo 的数据结构足够清晰:每个关键帧包含 time 和 data,线性插值即可完成大部分需求。

外部参数(uniform)

有些渲染资源,需要对外暴露渲染参数如:动画强度、速度以及高斯模糊的“模糊度”等。

外部参数专门设计给编辑器使用,这是前端需要重点关注的部分。

通过这些参数定义,前端可以自动构建编辑界面:将 name 作为标签显示,根据 range 生成滑块控件,defaultValue 作为初始值。这样做的好处是,每当新增一个渲染资源,前端无需修改任何代码,只要解析 uniform 数组,就能自动生成对应的参数面板。

剪映渲染资源分析

剪映作为国内视频剪辑软件的标杆,其渲染资源设计经过了大量实战验证。通过逆向分析剪映的资源包,可以发现其渲染资源分为两类:静态资源和动态资源。

静态资源

静态资源指的是可以纯粹通过 shader + 动画数据完成的效果,不需要复杂的逻辑运算。以"向左"转场为例:

1766975532513

对应剪映资源结构:

1766976377106

核心设计对比

剪映使用了类似的 renderPass 设计,但有一些不同之处:

  1. 依赖关系表达:剪映用 inputEffect 字段表示 pass 之间的依赖关系,我们设计成了 asInputTexIndex + renderPassIndex 的组合。我们的设计更明确,系统可以根据这个信息自动判断 FBO 何时可以回收。
  2. 渲染流程:这个"向左"转场包含 3 个 pass:

  3. Pass 1:对输入 A(前一帧)做高斯模糊,使用 easeInOutQuint 缓动函数控制位移纹理采样

  4. Pass 2:对输入 B(后一帧)做高斯模糊,同样使用缓动函数
  5. Pass 3:根据进度混合输出(gl_FragColor = iTime <= 0.50 ? from : to

这 3 个 pass 其实可以合并为 1 个 pass 完成。在 fragment shader 中直接采样两张纹理,分别做模糊计算后混合。

动态资源

动态资源是静态资源无法覆盖的场景:需要在渲染过程中执行复杂的逻辑运算、条件判断、或者需要 CPU 参与计算的效果。以剪映的"推远2"为例:

1766977773246

动态资源的复杂性

这类资源本质上是 AE 项目导出的,包含:

  1. Lua 脚本:描述动画逻辑、图层关系、表达式计算;
  2. 资源文件:预合成、图片序列、音频等;
  3. 渲染指令:需要一个专门的渲染引擎解析 Lua,生成渲染指令树;

为什么需要动态资源?

某些效果无法用静态的 shader + 动画描述,典型案例:

目前设计的静态资源格式,无法支持动态资源。要支持需要:

  1. 集成 Lua 虚拟机或 JavaScript 引擎
  2. 实现 AE 渲染引擎的子集(如 PAG 做的)
  3. 开发对应的资源导出工具

这个工程量是巨大的,PAG 团队花了数年时间才做到对 AE 特性的较好支持。对于小团队来说,静态资源是很不错的选择。