首页
/ Remotion 效果系统深度解析:基于官方 add-effect 技能文档,完整掌握为 @remotion/effects 新增特效的全流程

Remotion 效果系统深度解析:基于官方 add-effect 技能文档,完整掌握为 @remotion/effects 新增特效的全流程

2026-09-05 14:32:35作者:明树来

本篇基于 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>

当前 packages/effects/src/ 目录同时存在两类形态,可直接对照参考:brightness.tshalftone.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() 辅助函数把可选参数解析为完整参数;
  • 参数校验复用两个共享模块:
  • 获取 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 拼入 shapedotSizerotation 等全部 10 个解析参数
setup(target) 一次性初始化:获取 WebGL2 上下文、编译着色器、链接 program、搭建全屏四边形 VAO/VBO、创建纹理、记录 uniform 位置 compileShaderlinkProgram → 创建 vao/vbo/texturegetUniformLocation
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(含 numberenumcolor、嵌套 variants 等类型)
validateParams 运行前参数校验,抛出带明确子串的 TypeError validateHalftoneParams(含改名检测、跨字段约束)

setup 阶段获取上下文的三个关键选项(premultipliedAlpha: truealpha: truepreserveDrawingBuffer: true)与随后的 gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true),是保证透明区域正确合成的基础;apply 中的 flipSourceY 由渲染管线传入,控制源纹理上传时是否垂直翻转,两个位置都要正确处理,否则会出现画面上下颠倒。

2.2 校验辅助函数与错误信息

validate-effect-param.ts 提供 assertEffectParamsObject(非对象抛 TypeError 并附上实际值的 JSON 表示)、assertRequiredFiniteNumberassertOptionalColorassertOptionalBoolean 等断言;color-utils.ts 则提供 assertOptionalFiniteNumbervalidateUnitInterval(0–1 区间)、validateNonNegativevalidateSignedUnitInterval(-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 的 build API 将每个入口打包为 dist/esm/<name>.mjsremotionreact 等被声明为 external;漏加这一步,ESM 子路径产物根本不会被构建
  • packages/effects/package.json
    • 添加 exports["./<effect-name>"],同时给出 typesmoduleimport 三个条件;
    • 添加对应的 typesVersions 条目(供 TypeScript 解析子路径类型)。

子路径导入 @remotion/effects/my-effect 完全依赖这两个声明,缺一个都会导致运行时或类型解析失败。

若采用目录型实现,需在顶层文件 re-export,例如:

export {myEffect, type MyEffectParams} from './my-effect/index.js';

仓库中 chromatic-aberration.tschromatic-aberration/ 目录、wave.tswave/ 目录的 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 numbermust 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 必须包含 slugtitlesidebar_labelcrumb: '@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 小节。

同步更新三个索引位置:

文档措辞细节可配合仓库内 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 中某必填字段的 defaultundefined 时,才提供 initialValues

Demo 细节可参考 docs-demo 技能(.agents/skills/docs-demo/SKILL.md)。

7. 渲染目录(TOC)预览合成

目录卡片必须来自 packages/docs 中真实存在的 Remotion 合成,不允许手写图片资源;预览图一律渲染为 PNG。

第一步,在 packages/docs/src/remotion/Root.tsxeffect-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

常见陷阱清单

技能文档末尾的陷阱列表是整篇流程的风险摘要,逐条对照仓库事实:

  1. 不要遗漏 package.jsonexportstypesVersions@remotion/effects/my-effect 这类子路径导入完全依赖它们;
  2. 不要遗漏 bundle.ts:漏掉后 ESM 子路径(dist/esm/*.mjs)不会被构建;
  3. 不要在 packages/docs/src/remotion 留下临时渲染入口Root.tsx 只保留正式合成;
  4. 不要用手写 SVG 充当 TOC 预览图:卡片必须来自真实 Still 渲染的 PNG;
  5. 除非特效有意改变透明通道,否则保持 alpha:canvas 存储的是预乘 alpha(premultiplied alpha),像素计算时需知晓这一点;
  6. 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.tspackages/effects/package.json
参数测试 packages/effects/src/test/effect-params.test.ts
文档 packages/docs/docs/effects/<effect-name>.mdxpackages/docs/sidebars.tspackages/docs/docs/effects/table-of-contents.tsxpackages/docs/src/data/articles.ts(生成器产出)
交互 Demo packages/docs/components/effects/effects-<effect-name>-preview.tsxpackages/docs/components/effects-demos/registry.ts
预览合成与图片 packages/docs/src/remotion/Root.tsxpackages/docs/static/img/packages/docs/static/generated/
Agent 技能 packages/skills/skills/remotion-markup/effects.md(仅在机制性变化时)

这套流程的精髓在于「单一事实来源」:schema 同时驱动 Studio 可视化编辑、文档与 Demo 的注册,calculateKey 同时驱动渲染缓存,documentationLink 同时驱动 Studio 跳转与文档链接测试——只要每一步都严格对齐既有实现,新特效就能以最小成本无缝融入整个 Remotion 生态。

登录后查看全文
热门项目推荐
相关项目推荐