Remotion 视觉效果实战指南:掌握 `effects` 数组与 `createEffect()` 自定义特效开发
2026/9/8 22:57:43 网站建设 项目流程

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:

  1. 过滤 disabled 效果runEffectChain首先剔除params.disabled === true的效果,再按 backend 分组,避免空跑或不必要的后端切换。
  2. Canvas 池 + ping-pong:每个效果链状态(EffectChainState)持有一个与输出同尺寸的CanvasPool(见同目录canvas-pool.ts),同一后端的效果在两张 scratch canvas 之间来回 ping-pong 绘制,因此效果自身不需要每帧分配 canvas——这正是 packages/core/src/effects/effect-types.ts 注释中强调的契约。
  3. setup 缓存与回收setup()的结果按「效果定义 × target canvas」缓存在WeakMap中,并通过cleanupRegistry在链结束时统一回调cleanup()释放资源。
  4. 跨后端桥接:效果按backend'2d' | 'webgl2' | 'webgpu')分组为若干 run,依次执行。2D → WebGL2 直接传递 canvas;其他跨后端桥接使用createImageBitmap避免隐式 GPU readback 阻塞渲染帧率。
  5. 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()
documentationLinkURL 或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 资源
schemaInteractivitySchema定义 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'的适用场景:简单的像素遍历、filterdrawImageimageData类处理。当需要 shader 数学或 GPU 性能时才切换到 WebGL2;
  • resolve()帮助函数:统一收敛可选参数与默认值,同时被calculateKeyapplyvalidateParams复用,避免默认值散落多处;
  • 重置 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.tspackages/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、时间线与渲染管线的关键,逐条摘录并补充原因:

  1. disabled只通过工厂注入:不要把它写进自定义参数类型或 schema;
  2. 必填参数在工厂调用时用validateParams校验createEffect包装层保证它在返回 descriptor 之前执行,缺失参数应当立即抛错而不是在渲染帧中静默失败;
  3. 默认值双写schemaresolve()帮助函数中都要包含默认值——schema 的默认值用于 Studio 控件初始态,resolve()的默认值用于渲染时的参数归一;
  4. 复位 2D 上下文可变状态filterglobalAlpha、变换矩阵、合成(compositing)等绘制后必须复位,否则会串染到下一帧或链上的后续效果;
  5. 除非效果刻意改变透明度,否则保留 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.tstranslate.test.ts等。整体脉络是:先用本文方法把效果以createEffect封装为独立模块,在 packages/example/src/EffectsTestbed 这类测试台中挂到真实 composition 上目测与堆叠验证,最后以视觉测试固化输出,从而保证效果在 Studio 预览与正式渲染两种路径下表现一致。

【免费下载链接】remotion🎥 Make videos programmatically with React项目地址: https://gitcode.com/GitHub_Trending/re/remotion

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询