Remotion 视觉效果实战指南:掌握effects数组与createEffect()自定义特效开发
【免费下载链接】remotion🎥 Make videos programmatically with React项目地址: https://gitcode.com/GitHub_Trending/re/remotion
Canvas/WebGL2 视觉特效是 Remotion 程序化视频体系中的重要一环:把brightness()、blur()、rgbShift()这类「效果函数」塞进effects数组,即可对<Video>、<CanvasImage>、<Solid>、<HtmlInCanvas>等基于 canvas 的组件逐帧施加像素级变换。本指南以仓库内 Agent 技能文档 packages/skills/skills/remotion-markup/effects.md 为核心脉络,讲解如何安装并使用内置效果、如何在渲染时开启 WebGL2、如何通过createEffect()编写可复用、可参数化、可在 Studio 中编辑并可与官方效果自由叠加的自定义特效,最终给出可复制、可运行的完整代码。
effects 系统速览与适用范围
安装效果包
内置效果统一由@remotion/effects包提供,安装方式与 Remotion 其他生态包一致:
npx remotion add @remotion/effects安装完成后,效果函数就可以作为参数传给基于 canvas 的组件上的effectsprop。注意适用面:effects不是给普通 DOM 层级的</p>布局用的,它面向渲染到 canvas 的源组件,文档明确列出的接受者为:
<Video>(来自@remotion/media)<Solid><CanvasImage><HtmlInCanvas>
一个最直观的用法示例:
import {Video} from '@remotion/media'; import {blur} from '@remotion/effects/blur'; <Video src="https://remotion.media/video.mp4" effects={[blur({radius: 8})]} />;渲染时开启 WebGL2
这些效果依赖 WebGL2。在 Studio 交互预览之外,正式渲染(render)时需要显式开启 WebGL,否则效果可能无法工作:
import {Config} from '@remotion/cli/config'; Config.setChromiumOpenGlRenderer('angle');从 packages/effects/package.json 的exports字段可以看到,@remotion/effects把每个效果暴露成了独立的子路径(如@remotion/effects/blur、@remotion/effects/starburst),同时根入口@remotion/effects汇总导出。效果参数精确写法请查阅对应效果的文档(或仓库中 packages/effects/src 内同名源码)。
内置效果全览
完整效果清单
原文档共列出 50 余个开箱即用的效果函数,按其名称与用途可粗略归为如下几类:
- 颜色校正类:
brightness()、contrast()、colorKey()、duotone()、grayscale()、hue()、invert()、saturation()、tint()、linearGradient()、linearGradientTint()、thermalVision(); - 模糊类:
blur()、linearProgressiveBlur()、radialProgressiveBlur()、zoomBlur(); - 光效与辉光类:
dropShadow()、glow()、lightTrail()、evolve()、venetianBlinds()、shine()、lightLeak()、starburst(); - 几何/位移/畸变类:
mirror()、scale()、uvTranslate()、xyTranslate()、barrelDistortion()、chromaticAberration()、fisheye()、cornerPin()、wave()、vignette(); - 材质与纹理类:
burlap()、emboss()、dotGrid()、halftone()、noise()、noiseDisplacement()、paper()、roughenEdges()、pattern()、pixelate()、pixelDissolve()、scanlines()、speckle()、shrinkwrap()、contourLines()、checkerboard()、halftoneLinearGradient()、gridlines()、whiteNoise()、tvSignalOff()、lines()、rings()、waves()、zigzag()。
效果可以堆叠使用:把它们全部放进同一个effects数组,渲染管线会按数组顺序逐层处理(这也是后续要讲的效果链的核心行为)。
引入路径规则
大多数@remotion/effects的效果走@remotion/effects/<效果slug>子路径导入;其中两个平移效果是特例:uvTranslate()与xyTranslate()都从@remotion/effects/translate导入(见 packages/effects/package.json 中./translate子路径导出)。直接使用示例:
import {brightness} from "@remotion/effects"; <Video src="https://remotion.media/video.mp4" effects={[brightness({})]} />;效果链的底层工作方式(源码级)
在深入自定义效果之前,先理解框架如何执行effects数组,能帮你写出更符合运行模型的效果实现。执行逻辑集中在 packages/core/src/effects/run-effect-chain.ts:
- 过滤 disabled 效果:
runEffectChain首先剔除params.disabled === true的效果,再按 backend 分组,避免空跑或不必要的后端切换。 - Canvas 池 + ping-pong:每个效果链状态(
EffectChainState)持有一个与输出同尺寸的CanvasPool(见同目录canvas-pool.ts),同一后端的效果在两张 scratch canvas 之间来回 ping-pong 绘制,因此效果自身不需要每帧分配 canvas——这正是 packages/core/src/effects/effect-types.ts 注释中强调的契约。 - setup 缓存与回收:
setup()的结果按「效果定义 × target canvas」缓存在WeakMap中,并通过cleanupRegistry在链结束时统一回调cleanup()释放资源。 - 跨后端桥接:效果按
backend('2d' | 'webgl2' | 'webgpu')分组为若干 run,依次执行。2D → WebGL2 直接传递 canvas;其他跨后端桥接使用createImageBitmap避免隐式 GPU readback 阻塞渲染帧率。 - Y 轴翻转契约:
apply收到flipSourceY标志——DOM 朝向的 canvas 源上传 WebGL 纹理时需要UNPACK_FLIP_Y_WEBGL,而从 WebGL 桥接来的ImageBitmap已按上传朝向就绪,不需要再翻转。
类型契约定义在 packages/core/src/effects/effect-types.ts:所有 canvas 存储premultiplied alpha且按sRGB 编码;若效果在线性空间做色彩数学,需自行完成 sRGB 往返转换。
自定义效果:何时用与怎么用
选用原则
原文档给出明确的决策边界:
- 当用户需要一个可复用、参数化、可在 Studio 中编辑、可与其他效果叠加的效果工厂时,用
createEffect(); - 优先于
<HtmlInCanvas onPaint>——onPaint适合一次性内联绘制,而createEffect让变换具备「效果对象」的一切能力; - 文件组织:项目内临时效果放在组合旁,例如
src/effects/palette-map.ts;打算进入@remotion/effects仓库的效果,则遵循仓库的add-effect技能(agent 工作流约定)而不是本文的快速写法。
createEffect()的参数契约
createEffect()接受一个EffectDefinition,其配置项与原文档一致,含义如下:
| 配置项 | 类型/取值 | 作用 |
|---|---|---|
type | 字符串 | 稳定的reverse-DNS标识符,如com.example.paletteMap;用于效果身份区分 |
label | 字符串 | Studio 中显示的标签,惯例写成调用形式,如paletteMap() |
documentationLink | URL 或null | 指向效果文档;没有则传null |
backend | "2d"/"webgl2"/"webgpu" | 声明效果运行后端 |
calculateKey(params) | (params) => string | 返回包含所有影响输出参数的稳定字符串,用于效果实例的 memoization 比较 |
setup(target) | (canvas) => S | 创建可复用的后端状态;无状态则返回null |
apply({source, target, width, height, params, state, flipSourceY}) | 函数 | 把变换后的结果绘制到target上,每帧调用 |
cleanup(state) | (state) => void | 释放setup()创建的 GPU/CPU 资源 |
schema | InteractivitySchema | 定义 Studio 控件;disabled字段由框架自动追加 |
validateParams(params) | 函数 | 参数缺失或非法时抛错(在工厂调用时立即执行) |
2D 自定义效果:完整最小实现
原文档给出了一个可直接运行的「半透明合成」效果示例(将不透明度参数映射为ctx.filter输出),完整继承如下:
import {createEffect, type InteractivitySchema} from 'remotion'; type MyEffectParams = { readonly amount?: number; }; const myEffectSchema = { amount: { type: 'number', min: 0, max: 1, step: 0.01, default: 1, description: 'Amount', }, } as const satisfies InteractivitySchema; const resolve = (params: MyEffectParams) => ({ amount: params.amount ?? 1, }); export const myEffect = createEffect<MyEffectParams, null>({ type: 'com.example.myEffect', label: 'myEffect()', documentationLink: null, backend: '2d', calculateKey: (params) => { const {amount} = resolve(params); return `my-effect-${amount}`; }, setup: () => null, apply: ({source, target, width, height, params}) => { const ctx = target.getContext('2d'); if (!ctx) { throw new Error('Could not get a 2D context for myEffect().'); } const {amount} = resolve(params); ctx.clearRect(0, 0, width, height); ctx.filter = `opacity(${amount * 100}%)`; ctx.drawImage(source, 0, 0, width, height); ctx.filter = 'none'; }, cleanup: () => undefined, schema: myEffectSchema, validateParams: ({amount = 1}) => { if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0 || amount > 1) { throw new TypeError('amount must be a number between 0 and 1'); } }, });要点拆解:
backend: '2d'的适用场景:简单的像素遍历、filter、drawImage或imageData类处理。当需要 shader 数学或 GPU 性能时才切换到 WebGL2;resolve()帮助函数:统一收敛可选参数与默认值,同时被calculateKey、apply、validateParams复用,避免默认值散落多处;- 重置 2D 上下文可变状态:本例在绘制后把
filter复位为'none',这是必须养成的习惯(globalAlpha、变换矩阵、合成模式等同理),否则状态会泄漏到下一帧或后续效果。
仓库中同风格的完整 2D 实例可对照 packages/example/src/EffectsTestbed/sample-posterize-2d.ts(一个带levels/amount两个参数的 posterize 色调分离效果,通过getImageData/putImageData逐像素量化)。
WebGL2 自定义效果:RGB 通道分离
当效果需要逐像素 shader 计算时,应选择backend: 'webgl2'。生命周期分工与原文档一致:
setup():获取 WebGL2 上下文,编译/链接 shader,创建全屏 quad 的 VAO/VBO 与纹理,读取 uniform location,全部存入 state;apply():上传source纹理,设置 viewport 与 uniform,绘制全屏三角形带;cleanup():删除纹理、缓冲、program、VAO,释放 GPU 资源。
原文档的最小骨架示例(RGB 通道偏移,红色与蓝色通道沿水平方向错位):
import {createEffect, type InteractivitySchema} from 'remotion'; type RgbShiftParams = { readonly amount?: number; }; type RgbShiftState = { readonly gl: WebGL2RenderingContext; readonly program: WebGLProgram; readonly vao: WebGLVertexArrayObject; readonly vbo: WebGLBuffer; readonly texture: WebGLTexture; readonly uSource: WebGLUniformLocation | null; readonly uOffset: WebGLUniformLocation | null; }; const rgbShiftSchema = { amount: { type: 'number', min: 0, max: 80, step: 1, default: 12, description: 'Amount', }, } as const satisfies InteractivitySchema; export const rgbShift = createEffect<RgbShiftParams, RgbShiftState>({ type: 'com.example.rgbShift', label: 'rgbShift()', documentationLink: null, backend: 'webgl2', calculateKey: ({amount = 12}) => `rgb-shift-${amount}`, setup: (target) => { const gl = target.getContext('webgl2', { premultipliedAlpha: true, alpha: true, preserveDrawingBuffer: true, }); if (!gl) { throw new Error('Could not get a WebGL2 context for rgbShift().'); } gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true); // Compile/link shaders, create a fullscreen quad VAO/VBO, create a // CLAMP_TO_EDGE RGBA texture, and get uSource/uOffset uniform locations. return createRgbShiftState(gl); }, apply: ({source, width, height, params, state, flipSourceY}) => { const amount = params.amount ?? 12; const {gl} = state; gl.viewport(0, 0, width, height); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flipSourceY); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, state.texture); gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, source as TexImageSource, ); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.useProgram(state.program); if (state.uSource) gl.uniform1i(state.uSource, 0); if (state.uOffset) gl.uniform2f(state.uOffset, amount / width, 0); gl.bindVertexArray(state.vao); gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4); }, cleanup: ({gl, program, vao, vbo, texture}) => { gl.deleteTexture(texture); gl.deleteBuffer(vbo); gl.deleteProgram(program); gl.deleteVertexArray(vao); }, schema: rgbShiftSchema, validateParams: ({amount = 12}) => { if (typeof amount !== 'number' || !Number.isFinite(amount) || amount < 0 || amount > 80) { throw new TypeError('amount must be a number between 0 and 80'); } }, });上面的createRgbShiftState是骨架占位。仓库提供了完整的、可直接运行的对应实现:packages/example/src/EffectsTestbed/sample-rgb-shift-webgl.ts,包含:
- 完整 GLSL
#version 300 es顶点/片元着色器(片元着色器分别以clamp(vUv ± uOffset)采样红色与蓝色通道,再与绿色通道重组为vec4(red, base.g, blue, base.a)); - 带错误日志的
compileShader/createProgram辅助函数; - 全屏三角形带(两个 vec2 属性交错为 16 字节 stride)的 VAO/VBO 初始化;
apply中显式clearColor(0,0,0,0)与clear(COLOR_BUFFER_BIT),以及绘制后的状态复位(解绑 VAO/纹理、useProgram(null));cleanup按序释放 texture、buffer、program、vertex array。
原文档同时建议,需要2D 与 WebGL2 成对参照时阅读packages/example/src/EffectsTestbed/sample-posterize-2d.ts与packages/example/src/EffectsTestbed/sample-rgb-shift-webgl.ts。若要在 Studio 中体验全部效果,可查看效果测试台 packages/example/src/EffectsTestbed/EffectsTestbed.tsx,另有 packages/example/src/EffectsTestbed/palette-map.ts 与 packages/example/src/EffectsTestbed/PaletteMapEffect.tsx 这类更贴近真实调色盘映射的实现。
把自定义效果放进 composition
createEffect()返回的工厂函数可以直接放进任何接受effects的组件。以下来自原文档的组合示例将自研效果作用于<CanvasImage>:
import {CanvasImage, staticFile} from 'remotion'; import {myEffect} from './effects/my-effect'; export const MyComp: React.FC = () => { return ( <CanvasImage src={staticFile('image.png')} effects={[myEffect({amount: 0.8})]} /> ); };框架如何包装自定义效果(源码解读)
createEffect的实现在 packages/core/src/effects/create-effect.ts,理解它能解释原文档中多条「使用规范」的由来:
disabled由框架注入:框架级字段disabledEffectField会被自动并入每个效果的 schema(Studio 中呈现为时间线效果行的「眼睛」开关,对应/api/save-effect-props持久化),也会并入工厂的入参类型。因此不要在自定义 params 类型或 schema 里重复声明disabled——通过返回的工厂传入disabled?: boolean即可。calculateKey被包装:源码用-disabled-${disabled}后缀包裹用户的calculateKey。这样在 Studio/代码中切换disabled也会使缓存 key 失效,效果链能及时重算(原文档也提到getEffectFieldsToShow会过滤该字段,让开关成为唯一控件)。- 工厂调用时立即校验:返回的工厂在构造 descriptor 前先调用
validateParams抛错。测试 packages/core/src/test/create-effect-validate-params.test.ts 验证了「必需参数缺失时调用工厂即抛TypeError,传入合法值则不抛」;packages/core/src/test/create-effect-disabled.test.ts 则验证disabled的注入行为。 - 类型擦除以支持自由组合:descriptor 把
P/S擦除为unknown,使不同效果的 descriptor 可以在同一个EffectsProp数组中自由编排。工厂的类型签名还是条件类型(effect-types.ts 的EffectFactory):当你的P含必填字段(如TintParams.color)时,工厂强制要求传参;全部可选时参数可省略。
编写自定义效果的最终检查清单
原文档在收尾处列出的一组硬性规范,是让效果进入 Studio、时间线与渲染管线的关键,逐条摘录并补充原因:
disabled只通过工厂注入:不要把它写进自定义参数类型或 schema;- 必填参数在工厂调用时用
validateParams校验:createEffect包装层保证它在返回 descriptor 之前执行,缺失参数应当立即抛错而不是在渲染帧中静默失败; - 默认值双写:
schema与resolve()帮助函数中都要包含默认值——schema 的默认值用于 Studio 控件初始态,resolve()的默认值用于渲染时的参数归一; - 复位 2D 上下文可变状态:
filter、globalAlpha、变换矩阵、合成(compositing)等绘制后必须复位,否则会串染到下一帧或链上的后续效果; - 除非效果刻意改变透明度,否则保留 alpha:所有链内 canvas 均以 premultiplied alpha 存储,透明通道的破坏会直接影响与其他效果的合成结果。
视觉验证与测试资源
想让效果在浏览器中通过截图像素级比对验证,仓库在 packages/effects/src/visual-test/effects-visual.test.ts 提供了基于浏览器(Playwright + Vitest)的视觉回归用例,截图输出于同目录__screenshots__;单元层面对效果参数边界与工厂行为的校验可参考 packages/effects/src/test/effect-params.test.ts、scale.test.ts、translate.test.ts等。整体脉络是:先用本文方法把效果以createEffect封装为独立模块,在 packages/example/src/EffectsTestbed 这类测试台中挂到真实 composition 上目测与堆叠验证,最后以视觉测试固化输出,从而保证效果在 Studio 预览与正式渲染两种路径下表现一致。
【免费下载链接】remotion🎥 Make videos programmatically with React项目地址: https://gitcode.com/GitHub_Trending/re/remotion
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考