Remotion 效果系统深度解析:基于官方 add-effect 技能文档,完整掌握为 @remotion/effects 新增特效的全流程
本篇基于 Remotion 官方仓库内的 Agent 技能文档 .agents/skills/add-effect/SKILL.md,系统讲解向 @remotion/effects 包新增一个 WebGL2 特效的完整工程流程:从命名规范、createEffect() 实现模板、package.json 子路径导出注册,到测试、文档、交互式 Demo 与目录卡片渲染。读完本文,你可以独立产出一个可被 Studio 可视化编辑、可通过 @remotion/effects/<effect-name> 子路径导入、并带有官方文档页面与预览图的标准特效。
1. 选择合适的效果形态与命名规范
新增特效的第一步是确定实现形态。技能文档给出以下决策准则:
- 优先选择 WebGL2 后端。只有当 WebGL 无法表达该效果时,才考虑 2D Canvas 后端;
- 简单特效放在单文件
packages/effects/src/<effect-name>.ts; - 需要多份着色器、运行时辅助模块或多个文件时,使用目录
packages/effects/src/<effect-name>/,并在顶层放一个 re-export 文件; - 严格遵循包内既有的命名约定:
- 文件名 / 子路径:kebab-case(如
chromatic-aberration); - 函数名:camelCase(如
chromaticAberration); - 参数类型:PascalCase(如
ChromaticAberrationParams); - 效果类型字符串:
remotion/<kebab-case-name>。
- 文件名 / 子路径:kebab-case(如
当前 packages/effects/src/ 目录同时存在两类形态,可直接对照参考:brightness.ts、halftone.ts 等单文件特效,以及 blur/、chromatic-aberration/、wave/ 等目录型特效(目录内含 -runtime.ts 等运行时辅助文件)。
2. 实现效果:createEffect 完整模板
技能文档给出的实现要点如下(每一项在仓库源码中均有对应实例):
- 从
remotion导入SequenceSchema类型与Internals命名空间; - 通过
const {createEffect, createWebGL2ContextError} = Internals;解构出核心 API; - 默认值以
const常量定义; - 用
satisfies SequenceSchema定义 schema——schema 中声明的字段会直接出现在 Studio 的可视化编辑面板中; - 导出参数类型(
export type XxxParams); - 用
resolve()辅助函数把可选参数解析为完整参数; - 参数校验复用两个共享模块:
- validate-effect-param.ts(对象/数字/颜色/布尔断言);
- color-utils.ts(区间校验、颜色解析等);
- 获取 WebGL2 上下文失败时,抛出
createWebGL2ContextError('<effect name> effect'); - 将
documentationLink设为https://www.remotion.dev/docs/effects/<slug>; - 所有解析后的参数都必须包含进
calculateKey(),保证缓存 key 随参数变化。
以下是技能文档提供的标准模板(createMyEffectState() 代表着色器编译、程序链接、全屏四边形与纹理搭建等一次性 setup 逻辑,可参考 halftone.ts 等既有实现):
import type {SequenceSchema} from 'remotion';
import {Internals} from 'remotion';
import {assertOptionalFiniteNumber, validateUnitInterval} from './color-utils.js';
import {assertEffectParamsObject} from './validate-effect-param.js';
const {createEffect, createWebGL2ContextError} = Internals;
const DEFAULT_AMOUNT = 1 as const;
const myEffectSchema = {
amount: {
type: 'number',
min: 0,
max: 1,
step: 0.01,
default: DEFAULT_AMOUNT,
description: 'Amount',
},
} as const satisfies SequenceSchema;
export type MyEffectParams = {
readonly amount?: number;
};
type MyEffectResolved = {
amount: number;
};
const resolve = (p: MyEffectParams): MyEffectResolved => ({
amount: p.amount ?? DEFAULT_AMOUNT,
});
const validateMyEffectParams = (params: MyEffectParams): void => {
assertEffectParamsObject(params, 'My effect');
assertOptionalFiniteNumber(params.amount, 'amount');
validateUnitInterval(params.amount ?? DEFAULT_AMOUNT, 'amount');
};
type MyEffectState = {
readonly gl: WebGL2RenderingContext;
readonly program: WebGLProgram;
readonly vao: WebGLVertexArrayObject;
readonly vbo: WebGLBuffer;
readonly texture: WebGLTexture;
readonly uSource: WebGLUniformLocation | null;
readonly uAmount: WebGLUniformLocation | null;
};
const VERTEX_SHADER = /* glsl */ `#version 300 es
in vec2 aPos;
in vec2 aUv;
out vec2 vUv;
void main() {
vUv = aUv;
gl_Position = vec4(aPos, 0.0, 1.0);
}
`;
const FRAGMENT_SHADER = /* glsl */ `#version 300 es
precision highp float;
in vec2 vUv;
out vec4 fragColor;
uniform sampler2D uSource;
uniform float uAmount;
void main() {
vec4 color = texture(uSource, vUv);
fragColor = vec4(color.rgb * uAmount, color.a);
}
`;
// Follow existing helpers in halftone.ts or a runtime file for shader
// compilation, program linking, fullscreen-quad setup, and texture setup.
export const myEffect = createEffect<MyEffectParams, MyEffectState>({
type: 'remotion/my-effect',
label: 'My Effect',
documentationLink: 'https://www.remotion.dev/docs/effects/my-effect',
backend: 'webgl2',
calculateKey: (params) => {
const r = resolve(params);
return `my-effect-${r.amount}`;
},
setup: (target) => {
const gl = target.getContext('webgl2', {
premultipliedAlpha: true,
alpha: true,
preserveDrawingBuffer: true,
});
if (!gl) {
throw createWebGL2ContextError('my effect effect');
}
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);
return createMyEffectState(gl, VERTEX_SHADER, FRAGMENT_SHADER);
},
apply: ({source, width, height, params, state, flipSourceY}) => {
const r = resolve(params);
state.gl.viewport(0, 0, width, height);
state.gl.bindFramebuffer(state.gl.FRAMEBUFFER, null);
state.gl.activeTexture(state.gl.TEXTURE0);
state.gl.bindTexture(state.gl.TEXTURE_2D, state.texture);
state.gl.pixelStorei(state.gl.UNPACK_FLIP_Y_WEBGL, flipSourceY);
state.gl.texImage2D(
state.gl.TEXTURE_2D,
0,
state.gl.RGBA,
state.gl.RGBA,
state.gl.UNSIGNED_BYTE,
source as TexImageSource,
);
state.gl.useProgram(state.program);
if (state.uSource) state.gl.uniform1i(state.uSource, 0);
if (state.uAmount) state.gl.uniform1f(state.uAmount, r.amount);
state.gl.bindVertexArray(state.vao);
state.gl.drawArrays(state.gl.TRIANGLE_STRIP, 0, 4);
},
cleanup: ({gl, program, vao, vbo, texture}) => {
gl.deleteTexture(texture);
gl.deleteBuffer(vbo);
gl.deleteProgram(program);
gl.deleteVertexArray(vao);
},
schema: myEffectSchema,
validateParams: validateMyEffectParams,
});
2.1 createEffect 各字段的职责拆解
结合 halftone.ts 这个真实的 WebGL2 特效实现,可以逐一印证模板中各字段的作用:
| 字段 | 职责 | 在 halftone.ts 中的体现 |
|---|---|---|
type |
特效的唯一标识字符串 | dev.remotion.effects.halftone(注意:该特效早于 remotion/ 前缀约定,新特效应按技能文档使用 remotion/<kebab-case-name>) |
label |
Studio 中展示的名称 | halftone() |
documentationLink |
文档页链接,Studio 中可点击跳转 | https://www.remotion.dev/docs/effects/halftone |
backend |
声明渲染后端 | 'webgl2' |
calculateKey |
依据解析后参数生成缓存 key,任一参数变化都会产生新 key | 拼入 shape、dotSize、rotation 等全部 10 个解析参数 |
setup(target) |
一次性初始化:获取 WebGL2 上下文、编译着色器、链接 program、搭建全屏四边形 VAO/VBO、创建纹理、记录 uniform 位置 | compileShader → linkProgram → 创建 vao/vbo/texture → getUniformLocation |
apply({source, width, height, params, state, flipSourceY}) |
每帧绘制:绑定纹理并上传 source、设置 uniform、drawArrays 绘制全屏四边形 |
见 halftone.ts |
cleanup |
释放 GPU 资源(texture、vbo、program、vao) | gl.deleteBuffer/deleteProgram/deleteVertexArray/deleteTexture |
schema |
声明 Studio 可视化编辑字段 | halftoneSchema(含 number、enum、color、嵌套 variants 等类型) |
validateParams |
运行前参数校验,抛出带明确子串的 TypeError |
validateHalftoneParams(含改名检测、跨字段约束) |
setup 阶段获取上下文的三个关键选项(premultipliedAlpha: true、alpha: true、preserveDrawingBuffer: true)与随后的 gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true),是保证透明区域正确合成的基础;apply 中的 flipSourceY 由渲染管线传入,控制源纹理上传时是否垂直翻转,两个位置都要正确处理,否则会出现画面上下颠倒。
2.2 校验辅助函数与错误信息
validate-effect-param.ts 提供 assertEffectParamsObject(非对象抛 TypeError 并附上实际值的 JSON 表示)、assertRequiredFiniteNumber、assertOptionalColor、assertOptionalBoolean 等断言;color-utils.ts 则提供 assertOptionalFiniteNumber、validateUnitInterval(0–1 区间)、validateNonNegative、validateSignedUnitInterval(-1 到 1)以及基于 1×1 Canvas 的 parseColorRgba 颜色解析。值得注意的是 halftone.ts 中的校验还包含跨字段约束(colorMode: 'source' 时不允许设置 dotColor)与参数改名提示("color" has been renamed to "dotColor"),新特效在参数演进时应保持同样的错误信息风格——这直接服务于第 4 步测试中对错误子串的断言。
3. 注册包入口点
实现文件写好后,必须更新两处构建配置:
- packages/effects/bundle.ts:把新的
src/<effect-name>.ts加入effectEntrypoints数组。该脚本用 Bun 的buildAPI 将每个入口打包为dist/esm/<name>.mjs,remotion、react等被声明为 external;漏加这一步,ESM 子路径产物根本不会被构建; - packages/effects/package.json:
- 添加
exports["./<effect-name>"],同时给出types、module、import三个条件; - 添加对应的
typesVersions条目(供 TypeScript 解析子路径类型)。
- 添加
子路径导入 @remotion/effects/my-effect 完全依赖这两个声明,缺一个都会导致运行时或类型解析失败。
若采用目录型实现,需在顶层文件 re-export,例如:
export {myEffect, type MyEffectParams} from './my-effect/index.js';
仓库中 chromatic-aberration.ts 对 chromatic-aberration/ 目录、wave.ts 对 wave/ 目录的 re-export 即为现成范例。
4. 添加测试
测试统一维护在 packages/effects/src/test/effect-params.test.ts。为每个新特效补充以下内容:
- 导入新特效函数;
- 加入「documentation link」统一测试(断言
effect().definition.documentationLink精确等于https://www.remotion.dev/docs/effects/<slug>); - 当所有字段都是可选时,测试零参数调用(默认值路径);
- 存在必填参数时,测试缺省必填参数会报错;
- 测试非法取值,并断言错误信息的精确子串(如
must be a finite number、must be <= 1); - 测试有意义的参数会产生不同的
effectKey——这正是第 2 步calculateKey()必须纳入全部解析参数的原因。
运行方式:
cd packages/effects
bun test src/test
bunx turbo make --filter="@remotion/effects"
其中 bun test src/test 对应 package.json 中的 "test": "bun test src/test" 脚本;bunx turbo make 触发 tsgo 类型检查 + bundle.ts ESM 构建("make": "tsgo && bun --env-file=../.env.bundle bundle.ts")。
5. 编写文档页
在 packages/docs/docs/effects/<effect-name>.mdx 新建文档页,参照既有特效页结构:
- Frontmatter 必须包含
slug、title、sidebar_label、crumb: '@remotion/effects'; image:字段只允许在运行bun render-cards.ts生成卡片后再添加;- H1 格式为
# effectName()<AvailableFrom v="..." />; - 紧跟一句
_Part of the @remotion/effects package._; - 简短描述 +
<EffectsDemo type="effects-<effect-name>" />交互 Demo; - 提供带
title="MyComp.tsx"的 twoslash 示例; - 每个选项单独用
###小标题说明,可选参数名带?后缀; - 增加
disabled?小节与 See also 小节。
同步更新三个索引位置:
- packages/docs/sidebars.ts——按字母序插入
'effects/<effect-name>'; - packages/docs/docs/effects/table-of-contents.tsx——在正确分类下添加卡片;
- packages/docs/src/data/articles.ts——必须通过运行卡片生成器更新,禁止手工编辑。
文档措辞细节可配合仓库内 writing-docs 技能(位于 .agents/skills/writing-docs/SKILL.md)。
6. 添加交互式文档 Demo
创建 packages/docs/components/effects/effects-<effect-name>-preview.tsx,复用其他特效相同的预览源(EFFECTS_PREVIEW_IMAGE_SRC):
import {myEffect} from '@remotion/effects/my-effect';
import React from 'react';
import {CanvasImage} from 'remotion';
import {EFFECTS_PREVIEW_IMAGE_SRC} from './effects-preview-image';
export const EffectsMyEffectPreview: React.FC<{
readonly amount: number;
}> = ({amount}) => {
return (
<CanvasImage
src={EFFECTS_PREVIEW_IMAGE_SRC}
width={1280}
height={720}
fit="cover"
effects={[myEffect({amount})]}
/>
);
};
要点:文档特效预览必须使用 fit="cover",让共享预览图填满 16:9 画布,避免出现透明边条。
随后在 packages/docs/components/effects-demos/registry.ts 中注册:
- 导入预览组件,并导入真实特效 schema(或从
effect().definition.schema读取); - 添加
id: 'effects-<effect-name>'的注册条目; - 只有当 schema 中某必填字段的
default为undefined时,才提供initialValues。
Demo 细节可参考 docs-demo 技能(.agents/skills/docs-demo/SKILL.md)。
7. 渲染目录(TOC)预览合成
目录卡片必须来自 packages/docs 中真实存在的 Remotion 合成,不允许手写图片资源;预览图一律渲染为 PNG。
第一步,在 packages/docs/src/remotion/Root.tsx 的 effect-previews 目录下追加一个 Still:
<Still
id="effects-my-effect-preview"
component={EffectsMyEffectPreview}
width={1280}
height={720}
defaultProps={{
amount: 1,
}}
/>
width/height 必须与预览组件的 CanvasImage 保持一致;预览组件若使用共享文档预览图,CanvasImage 保持 fit="cover"——把 16:9 的预览渲染进不同宽高比的合成,会在生成的 TOC 图中留下黑边。
第二步,在 packages/docs 下执行:
bunx remotion still src/remotion/entry.ts effects-my-effect-preview static/img/effects-my-effect-preview.png --overwrite --image-format=png
第三步,同时提交两样东西:Root.tsx 中的合成条目,以及渲染出的 packages/docs/static/img/effects-my-effect-preview.png。
8. 生成文档卡片
cd packages/docs
bun render-cards.ts
提交生成的 packages/docs/static/generated/articles-docs-effects-<effect-name>.png,并给文档页补上新产生的 image: frontmatter 行。注意:render-cards.ts 是「机会式」生成器,可能顺带产出本次改动之外的缺失卡片——与本变更无关的图片必须删除,保持提交聚焦。
9. 同步 Remotion Agent 技能
保持面向 Agent 的 Remotion 技能与新特效同步:仅在新特效改变了通用使用机制、导入约定、安装指引或自定义特效建议时,才更新 packages/skills/skills/remotion-markup/effects.md。该文件不应重复完整特效清单——官方文档的目录(table of contents)才是规范列表,避免两处清单漂移。
10. 格式化、构建与提交前检查
cd packages/effects
bunx oxfmt src --write
cd ../..
bun run build
bun run formatting
说明:若改动触及 docs 源码,bun run formatting 会覆盖 packages/docs/src;纯 MDX 文档页的修改不要跑格式化器,避免污染文档。提交前最后执行:
git diff --check
git status --short
常见陷阱清单
技能文档末尾的陷阱列表是整篇流程的风险摘要,逐条对照仓库事实:
- 不要遗漏
package.json的exports与typesVersions:@remotion/effects/my-effect这类子路径导入完全依赖它们; - 不要遗漏
bundle.ts:漏掉后 ESM 子路径(dist/esm/*.mjs)不会被构建; - 不要在
packages/docs/src/remotion留下临时渲染入口:Root.tsx只保留正式合成; - 不要用手写 SVG 充当 TOC 预览图:卡片必须来自真实
Still渲染的 PNG; - 除非特效有意改变透明通道,否则保持 alpha:canvas 存储的是预乘 alpha(premultiplied alpha),像素计算时需知晓这一点;
- WebGL 颜色计算的预乘问题:在做亮度或阈值计算前,通常要先对采样到的 RGB 做反预乘(unpremultiply,如
halftone.ts中的vec3 rgb = alpha > 0.001 ? texColor.rgb / alpha : vec3(0.0);),输出时再重新预乘。
结语:一次变更涉及的文件全景
汇总上述步骤,一次「新增特效」变更至少触碰以下仓库位置,可作为自查清单:
| 类别 | 路径 |
|---|---|
| 特效实现 | packages/effects/src/<effect-name>.ts(或目录 + 顶层 re-export) |
| 构建注册 | packages/effects/bundle.ts、packages/effects/package.json |
| 参数测试 | packages/effects/src/test/effect-params.test.ts |
| 文档 | packages/docs/docs/effects/<effect-name>.mdx、packages/docs/sidebars.ts、packages/docs/docs/effects/table-of-contents.tsx、packages/docs/src/data/articles.ts(生成器产出) |
| 交互 Demo | packages/docs/components/effects/effects-<effect-name>-preview.tsx、packages/docs/components/effects-demos/registry.ts |
| 预览合成与图片 | packages/docs/src/remotion/Root.tsx、packages/docs/static/img/、packages/docs/static/generated/ |
| Agent 技能 | packages/skills/skills/remotion-markup/effects.md(仅在机制性变化时) |
这套流程的精髓在于「单一事实来源」:schema 同时驱动 Studio 可视化编辑、文档与 Demo 的注册,calculateKey 同时驱动渲染缓存,documentationLink 同时驱动 Studio 跳转与文档链接测试——只要每一步都严格对齐既有实现,新特效就能以最小成本无缝融入整个 Remotion 生态。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00