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

一开始,没有人觉得不对,直到产品多次提到,为什么我们的转场效果这么差呢?还有伴随而来的问题:增删改渲染资源,全链路都要发布一遍(前端、后端、单元测试等)。发布嘛,倒是其次,但是渲染效果差的问题,在单 pass 的渲染支持下是难以优化的。比如高斯模糊:通常需要 ping-pong 的方式,像乒乓球一样,得需要至少两只球拍(FBO),多次动作(draw call)才能完成。对此,在保留历史渲染资源的情况下,我开始了扩展渲染资源的探索。
渲染资源外包
正常来讲,对于效果类的渲染资源,大点的公司应该专门有一批 TA(技术美术)岗位,在已有框架下实现产品提的效果。SDK 的开发人员需要给 TA 提供配套的使用工具,包括但不限于:
- 提供效果编辑器(如动画编辑器,可考虑使用 AE 设计 + 导出实现,参考 PAG);
- 提供可用的参数(对于转场的 shader:texture0、texture1、progress 等);
- 提供 debug 环境;
- 插拔式的线上资源管理;
只有这样,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"
}
这里面包含了:
- 基本信息(名称、描述、类型);
- 预览图、预览视频;
- 建议时长;
- 对外参数;
- 核心渲染资源。
前端拿到这个资源,可以自行构建编辑界面(基本信息+参数+预览信息),如剪映放大镜效果的编辑界面:

资源定义
渲染资源主要包含以下几部分:
- 渲染通道定义(renderPass[]);
- 外部纹理输入(texture[]);
- 动画(animation[]);
- 外部参数(uniform[]);
- 基本信息(name、desc、format 等,方便编辑器展示);
渲染通道(renderPass)
renderPass 作为一个数组,每一个对象对应创建一次 FBO 和一次渲染调用,结果会存在 FBO 中。由于这里涉及到 FBO 的频繁申请与释放,所以内部要实现一个 FBO 缓存池,从而避免 FBO 频繁创建与销毁带来的性能开销。其中,核心的参数是:
- fboSize(创建 FBO 的比例,如高斯模糊等,可创建低分辨率 FBO 减少性能消耗。1.0 表示原分辨率);
- shader(核心渲染文件,含 vert 和 frag);
- asInputTexIndex(作为其它 pass 的输入,系统依赖这个字段进行 FBO 资源回收,当不作为后续任何通道输入时则可以回收);
- name(uniform 的变量名);
- pipe(纹理通道,注意默认占用的通道,比如转场的 texture0 和 texture1,占用 0、1 通道作为原始输入);
- renderPassIndex(渲染 pass 索引);
有了以上通道定义,即可实现多 pass 的渲染效果。典型案例是高斯模糊:第一个 pass 水平方向模糊,输出到 FBO1;第二个 pass 读取 FBO1 作为输入(通过 asInputTexIndex 指定),进行垂直方向模糊,最终输出。
这种 ping-pong 渲染方式,将二维模糊拆分为两次一维模糊,既保证了效果质量,又大幅降低了计算量(O(n²) 降到 O(2n))。类似的还有 Bloom 效果:降采样 → 模糊 → 上采样 → 混合,需要 4-5 个 pass 配合。
通过 asInputTexIndex 串联各个 pass 的输入输出,系统可以自动管理 FBO 的生命周期,在不再需要时及时回收,避免资源浪费。
外部纹理输入(texture)
复杂的渲染效果,离不开外部的纹理支持。如 LUT、冲刺、星光闪烁等效果。texture 定义如下:
- pipe(通道数组,和 asInputTexIndex 一样);
- name(uniform 变量名);
- repeatMode(动画播放模式,如带透明度通道的视频,作为 Mask 使用时。0、1、2:停在最后一帧、循环播放、倒放等);
- renderPassIndex(作为哪些渲染通道的输入?与 pipe 数组长度需一致);
texture 参数允许在渲染资源中使用外部纹理,极大地扩展了效果的表现力。
最典型的应用是 mask 纹理:通过一张黑白图或带 alpha 通道的视频,控制效果的显示区域和强度。比如星光闪烁效果,可以用一张星星形状的纹理作为 mask;心形转场,可以用心形 mask 控制过渡区域。
除此之外,LUT 纹理用于调色滤镜,noise 纹理用于添加噪点质感,gradient 纹理用于渐变映射。相比硬编码在 shader 中的数据,外部纹理可以随时替换,一套 shader 代码配合不同的纹理,就能实现数十种视觉效果的变体。
动画(animation)
动画数据用于场景中需要动画的部分,无论是顶点动画或是纹理动画,对于渲染 SDK 来说,都是着色器的 uniform 输入。
- name(uniform 名,顶点和片段都能拿到);
- strength(动画强度,可与 uniform 结构联动,供前端调整。输入到 uniform 的是 strength * value);
- speed(动画速度,可与 uniform 结构联动,供前端调整。影响取样的 timeline);
- repeatMode(同 texture);
- interpolationType(插值方式,linear、cubic 等);
- channelNum(通道数,1、2、3、4 分别对应 float、vec2、vec3、vec4,简单设计可只有这几个);
- renderPassIndex(同 texture);
- animationInfo(动画数据数组,使用时,结合 speed + 时间进行采样);
- data(核心数据,包含 channelNum 个维度数据);
- time(对应采样点时间);
这里的动画数据,通常由设计同学在配套的设计软件中制作完成。
理想方案是使用 AE 这类专业软件,开发对应的导出插件(如 PAG 的 AE 插件),将关键帧数据导出为 JSON 格式。这样设计师无需了解底层实现,专注于动画效果本身。
另一个方案是自己开发动画编辑器,直接生成符合格式的动画数据,但开发成本较高,适合有大量动画资源需求的团队。对于小规模项目,手写 JSON 配置也是可行的,毕竟 animationInfo 的数据结构足够清晰:每个关键帧包含 time 和 data,线性插值即可完成大部分需求。
外部参数(uniform)
有些渲染资源,需要对外暴露渲染参数如:动画强度、速度以及高斯模糊的“模糊度”等。
- name(外部名称,因为前端通常需要一个参数名称,而内部的变量通常是英文,所以这里 name 给外部使用);
- description(参数描述,前端可选展示);
- type(animation 给动画使用,支持 speed 和 strength,仅支持 "float" 和 "animation");
- defaultValue(参数默认值);
- uniformTarget(uniform 变量名称,区别 name,内部使用);
- range(参数范围,前端编辑使用);
- renderPassIndex(同 texture);
外部参数专门设计给编辑器使用,这是前端需要重点关注的部分。
通过这些参数定义,前端可以自动构建编辑界面:将 name 作为标签显示,根据 range 生成滑块控件,defaultValue 作为初始值。这样做的好处是,每当新增一个渲染资源,前端无需修改任何代码,只要解析 uniform 数组,就能自动生成对应的参数面板。
剪映渲染资源分析
剪映作为国内视频剪辑软件的标杆,其渲染资源设计经过了大量实战验证。通过逆向分析剪映的资源包,可以发现其渲染资源分为两类:静态资源和动态资源。
静态资源
静态资源指的是可以纯粹通过 shader + 动画数据完成的效果,不需要复杂的逻辑运算。以"向左"转场为例:

对应剪映资源结构:

核心设计对比:
剪映使用了类似的 renderPass 设计,但有一些不同之处:
- 依赖关系表达:剪映用
inputEffect字段表示 pass 之间的依赖关系,我们设计成了asInputTexIndex+renderPassIndex的组合。我们的设计更明确,系统可以根据这个信息自动判断 FBO 何时可以回收。 -
渲染流程:这个"向左"转场包含 3 个 pass:
-
Pass 1:对输入 A(前一帧)做高斯模糊,使用
easeInOutQuint缓动函数控制位移纹理采样 - Pass 2:对输入 B(后一帧)做高斯模糊,同样使用缓动函数
- Pass 3:根据进度混合输出(
gl_FragColor = iTime <= 0.50 ? from : to)
这 3 个 pass 其实可以合并为 1 个 pass 完成。在 fragment shader 中直接采样两张纹理,分别做模糊计算后混合。
动态资源
动态资源是静态资源无法覆盖的场景:需要在渲染过程中执行复杂的逻辑运算、条件判断、或者需要 CPU 参与计算的效果。以剪映的"推远2"为例:

动态资源的复杂性:
这类资源本质上是 AE 项目导出的,包含:
- Lua 脚本:描述动画逻辑、图层关系、表达式计算;
- 资源文件:预合成、图片序列、音频等;
- 渲染指令:需要一个专门的渲染引擎解析 Lua,生成渲染指令树;
为什么需要动态资源?
某些效果无法用静态的 shader + 动画描述,典型案例:
- 文字动画(需要解析文本,按字符或单词拆分,分别应用动画)
- 粒子系统(粒子生命周期、物理模拟需要逻辑计算)
- 复杂合成(多图层、蒙版、轨道遮罩等 AE 特性)
- 表达式驱动(AE 的表达式可以是任意 JavaScript 代码)
目前设计的静态资源格式,无法支持动态资源。要支持需要:
- 集成 Lua 虚拟机或 JavaScript 引擎
- 实现 AE 渲染引擎的子集(如 PAG 做的)
- 开发对应的资源导出工具
这个工程量是巨大的,PAG 团队花了数年时间才做到对 AE 特性的较好支持。对于小团队来说,静态资源是很不错的选择。