首页
/ OpenMontage Remotion 3D 实战指南:用 Three.js 与 React Three Fiber 编写确定性逐帧渲染

OpenMontage Remotion 3D 实战指南:用 Three.js 与 React Three Fiber 编写确定性逐帧渲染

2026-09-08 17:59:59作者:谭伦延

在 OpenMontage 的 remotion-best-practices 技能包 中,3d.md 规则文件 明确了在 Remotion 合成里使用 Three.js 与 React Three Fiber(R3F)的唯一正确姿势。本指南将该规则文件展开为可落地的最佳实践:从安装 @remotion/three 起,逐步讲解 <ThreeCanvas> 的包裹与光照配置、由 useCurrentFrame() 驱动的确定性动画约束,以及 <Sequence> 与 3D 画布协作时的 layout 陷阱。读完你就能写出在渲染与预览中表现一致、绝不闪帧的 3D 镜头,直接服务于 OpenMontage 以 Remotion 为核心的视频合成管线。

为什么 3D 内容是 Remotion 的特殊公民

OpenMontage 在 skills/core/remotion.md 中确立了 Remotion 是一切最终渲染的默认合成引擎:视频片段、静帧、动画场景、图表与文字卡片统一由一次 React 渲染管线处理。这套管线的基础是「确定性」——渲染器把时间切分为离散帧(frame),每一帧的像素都由该帧的帧号唯一决定。

Remotion 渲染的底层原理是逐帧驱动 Chromium 截图:对每个 frame 重新渲染一次 React 树,再收集为视频。这就带来一条硬约束:画面里不允许存在任何不受帧号控制的「自发」运动。Three.js 生态天然习惯用自身的 rAF 循环(requestAnimationFrame)或 R3F 的 useFrame() 来驱动动画——这套机制在普通网页里没问题,但在 Remotion 里会脱离帧同步,导致不同帧之间状态错乱,渲染时出现肉眼可见的闪烁(flickering)与帧间不一致。

因此 3D 场景在 Remotion 中必须遵守与常规 DOM 动画一致的哲学,但实现上有三处专属于 3D 的差异:

  1. 渲染容器必须换成 @remotion/three 提供的 <ThreeCanvas>,而不是裸用 R3F 的 <Canvas>
  2. 禁用 @react-three/fiberuseFrame(),动画只能由 Remotion 的 useCurrentFrame() 驱动;
  3. 画布内的 <Sequence> 必须显式声明 layout="none"

下文逐条展开。

前置依赖:安装 @remotion/three

规则文档明确:使用 3D 内容前必须先安装 @remotion/three 包。这一专有包把 R3F 的 <Canvas> 桥接到 Remotion 的渲染循环,使其能按帧截图。安装命令按项目包管理器四选一:

npx remotion add @remotion/three # 若项目使用 npm
bunx remotion add @remotion/three # 若项目使用 bun
yarn remotion add @remotion/three # 若项目使用 yarn
pnpm exec remotion add @remotion/three # 若项目使用 pnpm

以 OpenMontage 的 remotion-composer 为例,其当前显式依赖列表包含 remotion@^4.0.484@remotion/cli@remotion/captions@remotion/transitions 等(React 18.2),而 3D 所需的 @remotion/three 尚未进入该清单——说明 3D 镜头属于「按需引入」能力,任何 Agent 在编写带 3D 的合成时都应通过上述 remotion add 命令补装,而不是直接手动改动依赖清单。

<ThreeCanvas> 包裹一切 3D 内容

规则第 27 行的措辞是 MUST:所有 3D 内容必须包在 <ThreeCanvas> 中,且必须带上 widthheight 两个 prop,并补充恰当的光照(Three.js 的标准材质在无光环境下会渲染成全黑,缺少光照是新手最常见的「模型看不见」的原因)。

import { ThreeCanvas } from "@remotion/three";
import { useVideoConfig } from "remotion";

const { width, height } = useVideoConfig();

<ThreeCanvas width={width} height={height}>
  <ambientLight intensity={0.4} />
  <directionalLight position={[5, 5, 5]} intensity={0.8} />
  <mesh>
    <sphereGeometry args={[1, 32, 32]} />
    <meshStandardMaterial color="red" />
  </mesh>
</ThreeCanvas>;

这里 width / height 来自 Remotion 的 useVideoConfig(),它返回当前合成在注册时定义的 widthheightfpsdurationInFrames。把画布尺寸与合成尺寸严格对齐,能保证 3D 画面以精确到像素的分辨率参与帧渲染。

在实际项目中,更常见的做法是把合成信息通过 props 下沉:OpenMontage 的媒体配置表(见 skills/core/remotion.md)会把 youtube_landscape(1920×1080@30)、tiktok_vertical(1080×1920@30)、cinematic_wide(2560×1080@24)等档位映射为合成的宽高与帧率。3D 场景画布直接对齐这些数值即可无缝嵌入不同画幅。

铁律:禁止任何不受 useCurrentFrame() 驱动的动画

规则文件给出两条硬性约束:

  1. Shaders、模型等一律不得自行动画——除非由 useCurrentFrame() 驱动,否则渲染时必然闪帧;
  2. 禁止使用 @react-three/fiberuseFrame()

这条铁律在技能包中并非孤例:同一目录下的 animations.md 对普通 DOM 动画也要求「一切动画必须由 useCurrentFrame() 驱动」,并明令禁止 CSS transition/animation 与 Tailwind 动画类。3D 规则把同一哲学推广到 GPU 侧的材质、着色器与模型变换上。原因在于:Remotion 渲染每帧都启动一次新的渲染循环,若 3D 对象携带自身时间轴,第 N 帧截图时对象可能恰好处于上一次循环的任意中间态——帧间状态不可复现,画面就会抖动或闪烁。

正确写法是把帧号换算成动画参数后,以声明式 props 下发给 Three.js 对象:

const frame = useCurrentFrame();
const rotationY = frame * 0.02;

<mesh rotation={[0, rotationY, 0]}>
  <boxGeometry args={[2, 2, 2]} />
  <meshStandardMaterial color="#4a9eff" />
</mesh>;

frame * 0.02 表示每帧旋转 0.02 弧度(≈每秒 0.6 弧度,30fps 下约 34.4°/s),配合 OpenMontage 对插值的约束(interpolate 必须 clamp)可以做出更可控的运动曲线。由于所有运动只依赖帧号这一个变量,同一合成在 Studio 预览、本地 remotion render 与 CI 渲染中产出完全一致的画面,这恰恰是后期校验(如场景中点抽帧审查)的前提。

<ThreeCanvas> 内使用 <Sequence> 必须设置 layout="none"

当你想在同一个 3D 场景里编排多段内容(例如前 2 秒展示球体、后 2 秒切换为立方体)时,可以像普通合成一样使用 Remotion 的 <Sequence>。但规则强调:<ThreeCanvas> 内任何 <Sequence>layout prop 必须设为 none

import { Sequence } from "remotion";
import { ThreeCanvas } from "@remotion/three";

const { width, height } = useVideoConfig();

<ThreeCanvas width={width} height={height}>
  <Sequence layout="none">
    <mesh>
      <boxGeometry args={[2, 2, 2]} />
      <meshStandardMaterial color="#4a9eff" />
    </mesh>
  </Sequence>
</ThreeCanvas>;

原因与 R3F 的渲染模型有关:<ThreeCanvas> 内部是 WebGL 场景图,而非普通 DOM 的文档流。<Sequence> 默认的 layout 行为会以绝对定位方式在 DOM 层创建占位容器,这在普通 React 子树里用来精确控制画面区域,但在 Three 场景图中会产生不期望的布局副作用,导致子内容定位错乱。显式声明 layout="none" 后,<Sequence> 退化为纯时间轴容器——只负责把内部节点按时间切分(每个子序列内部继承 Remotion 的时间偏移),不触碰任何布局。这与 remotion/reference.md<Sequence layout> 参数("none" | "absolute-fill")的说明一致:3D 场景只取 none 分支。

可组合的进阶形态:3D + 逐帧动画 + 时间编排

将上面的规则组合起来,可以得到一个在 OpenMontage 合成中可直接复用的 3D 场景模板——同时示范「确定性动画」「时间分片」「合成级引用」三个要点:

import { ThreeCanvas } from "@remotion/three";
import { Sequence, useCurrentFrame, useVideoConfig } from "remotion";

const SpinCube = () => {
  const frame = useCurrentFrame();
  // 每帧自转 0.03 弧度;注意这里拿到的是 Sequence 内相对帧
  return (
    <mesh rotation={[0, frame * 0.03, 0]}>
      <boxGeometry args={[2, 2, 2]} />
      <meshStandardMaterial color="#4a9eff" />
    </mesh>
  );
};

export const My3DScene = () => {
  const { width, height } = useVideoConfig();
  return (
    <ThreeCanvas width={width} height={height}>
      <ambientLight intensity={0.4} />
      <directionalLight position={[5, 5, 5]} intensity={0.8} />
      <Sequence layout="none" durationInFrames={90}>
        <SpinCube />
      </Sequence>
    </ThreeCanvas>
  );
};

值得强调的细节:useCurrentFrame()<Sequence> 内部返回的是相对该 Sequence 起点的帧号(见 remotion/reference.md),因此上例中子组件的动画会自动跟随 Sequence 的时间窗归零重算,无需手工减去 from。这一行为保证了 3D 动画片段可以像普通素材一样被拖进任何时间轴位置而不破坏运动曲线。

规则之外:何时、在哪一层使用 3D

需要明确的是,本规则解决的是「Remotion 内部怎么写 3D」,而非「3D 资产怎么做」。OpenMontage 仓库同时沉淀了大量 Three.js 领域技能(如 threejs-world-generationthreejs-animationthreejs-materialsthreejs-shaders 等),它们负责在项目侧生产 .gltf/.glb 模型、材质与着色器方案;而 3d.md 是这些资产进入视频帧渲染的最后一公里——任何携带模型、着色器或动画回调的代码一旦进入 Remotion 合成,就必须满足上述三条约束。

从仓库现状推断:remotion-composer 的主力场景类型(文本卡、图表、动漫风格场景、影片场景,见 skills/core/remotion.md)当前均不依赖 GPU 场景图,因此 3D 属于按合成需求选用的增强能力。当 Agent 或开发者决定引入 3D 镜头时,本规则是判断代码是否「可渲染」的验收清单:

  • [ ] @remotion/three 已通过 remotion add 安装;
  • [ ] 3D 内容外包于 <ThreeCanvas> 且提供了与合成一致的 width / height
  • [ ] 场景包含至少一组有效光照(环境光 + 方向光);
  • [ ] 未出现 useFrame()requestAnimationFrame 或任何自驱动动画循环;
  • [ ] 全部运动由 useCurrentFrame()(必要时结合 interpolate 与 clamp)换算而来;
  • [ ] 画布内 <Sequence> 均设置了 layout="none"

小结

Three.js 的声明式场景与 Remotion 的逐帧确定性在哲学上并不冲突,差异只在于「谁拥有时钟」。遵循 OpenMontage 技能包在 3d.md 中沉淀的规则:用 <ThreeCanvas> 承载场景、让每一处运动都归帧号所有、禁止 useFrame()、给画布内的 <Sequence> 打上 layout="none",即可让 GPU 侧的高质量 3D 画面与图表、字幕、转场在同一渲染管线内无缝共存,产出每一帧都稳定可复现的成品视频。

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

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
857
1.35 K
docsdocs
暂无描述
Markdown
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391