|

用 Claude Code 一次会话搭出 2.5D 视差动画工作室:3 段核心代码实现逐帧确定性导出 MP4

用 Claude Code 一次会话搭出 2.5D 视差动画工作室:3 段核心代码实现逐帧确定性导出 MP4

作者注:从提示词、景深补偿公式到 Playwright+ffmpeg 逐帧导出,完整拆解如何用 Claude Code 搭建一个“导出不掉帧”的 2.5D 视差动画工作室。

很多人第一次用 AI 编程助手做动画,浏览器里预览得很顺,一导出视频就出现卡顿、跳帧和时长对不上。问题往往出在架构上:一开始没有把“导出”当成设计的一部分。本文围绕 纯函数 renderAt(t) + 逐帧 seek 截图 这条主线,讲清楚如何用 Claude Code 在一次会话里搭出 2.5D 视差动画工作室,并稳定导出 MP4。

这不是又一篇“让 AI 帮你写个动画”的演示。本文会拆开景深补偿公式的推导、一个很典型的偏移 bug,以及实时播放和逐帧导出的本质区别。

核心价值:读完本文,你会拿到一套可复用的提示词结构、一条可复现的导出管线,以及一份能迁移到其他程序化视频项目的验证清单。

claude-code-2-5d-parallax-motion-studio-deterministic-mp4-export-image-0


Claude Code 2.5D 视差动画工作室核心要点

参考资料中的实践来自一篇 2026-11-09 发布的文章。作者用 Claude Code 搭配 Opus 5.5,只用一次会话就做出一个单文件 HTML 工作室,外加一个 25 行左右的 Playwright 导出脚本。在作者的笔记本上,6 秒 720p 的视频(180 帧)大约 20 秒导出完成。作者没有给出硬件配置,所以这个耗时只能作为量级参考。

要点 说明 价值
场景图(Scene Graph) 每个图层有 z 深度、尺寸、位置和 HTML 内容 不用 3D 模型也能做出纵深感
时间轴(Timeline) 相机与图层的关键帧,每段配一条缓动曲线 镜头运动可编辑、可复用
确定性渲染器 renderAt(t) 对任意时刻 t 输出唯一画面 导出结果与机器快慢无关
景深补偿缩放 尺寸和偏移同时乘以 1 - z/perspective 静止时所见即所写,运动时自带视差
逐帧导出管线 Playwright 按 i/fps seek 截图,交给 ffmpeg 编码 每台机器输出一致,不掉帧

2.5D 视差动画的原理

2.5D 的做法是把扁平的 2D 素材放到 3D 空间的不同深度上,再让一台虚拟相机穿过它们。离相机近的图层移动快,远的移动慢,人眼会把这种速度差读成纵深,这就是视差(parallax)。整个过程没有建模、灯光,也没有 3D 引擎,浏览器的 CSS perspective 和 preserve-3d 就够用了。

“工作室”和“一次性动画”的区别在于三个部件是否齐全:场景图、时间轴和确定性渲染器。很多教程会跳过第三个,可它决定了导出的 MP4 会不会卡顿。所以在写第一行提示词时,就要把它定为硬性要求。

claude-code-2-5d-parallax-motion-studio-deterministic-mp4-export-image-1


Claude Code 2.5D 工作室的环境与模型配置

参考资料推荐用 Opus 5.5 完成首次构建。Anthropic 官方的模型概览把它定位为“长时间运行的智能体编程与知识工作”模型,1M token 上下文,最大输出 128K。官方定价为每百万输入 / 输出 token 4 / 20 美元,实际价格请以官方页面为准。

建议在启动时锁定具体模型 ID,避免项目中途别名切换导致行为变化:

claude --model claude-opus-5-5 --effort high

Opus 5.5 默认使用 medium effort。首次构建时,agent 要一次写对投影数学、关键帧引擎和导出管线,所以建议调到 high。之后只是改关键帧数值或颜色这类小修改,可以回到 medium,节省 token。

阶段 推荐 effort 典型任务 理由
首次构建 high 投影数学、关键帧引擎、导出脚本 多个子系统要一次对齐
迭代微调 medium 调关键帧、改配色、加图层 改动范围小,逻辑已稳定
“描述镜头”接口 low(起步) 返回固定 schema 的 JSON 关键帧 任务短、输出结构固定

用其他模型做同等构建效果如何、成本差多少,参考资料没有给出对比数据。如果要换模型,建议用同一份提示词各跑一遍,再用后文的静帧验证法逐项对比。


用 Claude Code 构建 2.5D 工作室的提示词结构

一个写得具体的提示词,效果好过五个含糊的提示词。参考资料里的构建规格是一整段提示词,下面是保留原意的版本:

Build a 2.5D motion studio as one file, studio.html, with no dependencies.
- Scene: 6 layers (sky, moon, hills, city skyline, a title card "Ship it.",
  foreground foliage), each at its own z depth, using CSS perspective and preserve-3d.
- Camera: keyframes for x, y, z (dolly) and yaw (orbit around the card).
- Per-layer tracks: opacity, scale, y. Easings: linear, easeInOutCubic,
  easeInOutSine, easeOutBack.
- A timeline bar with keyframe markers, a playhead, click to scrub, space to play or pause.
- renderAt(t) must be pure in t. Expose window.__seek(t) that resolves after
  the frame paints, and a ?capture mode that hides the UI.
Then write export.mjs: Playwright opens ?capture, seeks every frame at 30 fps,
screenshots it and pipes PNGs to ffmpeg as H.264.

这段提示词可以拆成四层:场景、相机、轨道与缓动、交互 UI,最后是导出契约。真正起决定作用的是最后两行。它们要求 renderAt(t) 必须是关于 t 的纯函数,要求暴露一个“画完才返回”的 __seek,还要求提供隐藏 UI 的 ?capture 模式。这样导出能力就成了架构约束,agent 写代码时会照着它来设计。

如果你要迁移到自己的项目,可以把这份结构当模板:前面写清“画什么”,最后用两三行写死“怎么被确定性地取帧”。


Claude Code 生成的 2.5D 核心代码解析

整个工作室大约 150 行,关键是下面三段。

关键帧采样与缓动

每条轨道都是一个按时间排序的关键帧数组。sample() 先找到 t 前后的两个关键帧,用前一个关键帧的缓动函数算出进度,再对所有数值字段做插值:

function sample(keys, t) {
  if (t <= keys[0].t) return keys[0];
  for (let i = 0; i < keys.length - 1; i++) {
    const a = keys[i], b = keys[i + 1];
    if (t <= b.t) {
      const p = (easings[a.ease] || easings.linear)((t - a.t) / (b.t - a.t));
      const out = {};
      for (const k in b)
        if (typeof b[k] === 'number' && k !== 't') out[k] = a[k] + (b[k] - a[k]) * p;
      return out;
    }
  }
  return keys[keys.length - 1];
}

这个函数不读当前时间、不依赖上一帧状态,也不用随机数。同一个 t 永远得到同一组数值,renderAt(t) 的确定性就是从这里来的。

景深补偿:2.5D 视差的核心公式

这里的思路是相机不动,让整个世界反向运动。每个图层除了在 z 轴上平移,还要乘一个补偿缩放:

const PERSPECTIVE = 1200; // 必须与 #viewport 的 perspective 一致
world.style.transform =
  `translate3d(${-cam.x}px, ${-cam.y}px, ${cam.z}px) rotateY(${-cam.yaw}deg)`;
const comp = 1 - L.z / PERSPECTIVE;
el.style.transform = `translate(-50%,-50%) ` +
  `translate3d(${x * comp}px, ${y * comp}px, ${L.z}px) scale(${comp * s})`;

推导过程如下。CSS 透视距离为 p 时,深度为 z 的点(z 为负表示远离观察者)投影到屏幕上,会被缩放 p / (p - z) 倍。取 p = 1200、z = -900,得到 1200 / 2100 ≈ 0.571,也就是图层只剩原始尺寸的约 57%。

要抵消这个缩小,就乘以它的倒数 (p - z) / p = 1 - z/p = 1 + 900/1200 = 1.75。代码里的 comp 就是这个值。由于 0.571 × 1.75 = 1,相机静止时,每个图层都会以你设定的尺寸和位置出现。

视差是怎么出现的?相机移动时,整个 world 平移了同样的 dx,但每个图层在屏幕上的位移仍然要乘以各自的 p/(p - z)。远处图层的系数小,所以移得少;近处图层系数大,所以移得多。补偿只修正了静止状态的构图,不影响运动时的速度差,2.5D 的效果靠的就是这一点。

图层 z 投影缩放 p/(p−z) 补偿系数 comp 相机平移时相对速度
0(标题卡平面) 1.000 1.000 基准
-300 0.800 1.250 较慢
-900 0.571 1.750 明显更慢
-1800 0.400 2.500 接近静止背景

(表中数值按 p = 1200 计算,-300 与 -1800 两行为示例推导。)

一个值得记住的 bug:只补偿尺寸,没补偿偏移

第一版代码只给图层乘了 scale(comp),x/y 偏移没有乘 comp。结果远处的山丘偏移量被透视压缩了,构图整体错位,山丘一路升高,盖住了大约三分之二的天空。修复方法很简单:x 和 y 也乘以 comp,因为透视对位置和尺寸的缩放是同一个系数。

更值得注意的是这个 bug 是怎么发现的。作者看了一帧渲染结果就发现了问题,读代码时并没有看出来。所以在信任任何镜头运动之前,先让 agent 渲染起点、中点、终点三张静帧。

claude-code-2-5d-parallax-motion-studio-deterministic-mp4-export-image-2


2.5D 视差动画逐帧确定性导出 MP4

实时播放与逐帧导出的本质差异

实时预览用 requestAnimationFrame,机器一忙就会丢帧,这在预览阶段可以接受。导出不能丢帧。如果用录屏,录到的是“真实时间里屏幕刚好显示的东西”,慢一帧就会变成重复帧或缺帧。

维度 实时播放(rAF / 录屏) 逐帧 seek 导出
时间来源 系统时钟 帧序号 i / fps
机器繁忙时 掉帧、重复帧 只是变慢,画面不变
跨机器一致性 不一致 每台机器输出相同
帧数 不可控 严格等于 duration × fps
适用场景 编辑预览、拖动时间轴 最终成片、CI 自动出片

极简导出脚本

导出器的时间由帧序号驱动。每帧先 seek,等画面绘制完成再截图,然后把 PNG 写进 ffmpeg 的标准输入:

import { chromium } from 'playwright';
import { spawn } from 'node:child_process';
import { pathToFileURL } from 'node:url';

const out = process.argv[2] || 'out.mp4';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
await page.goto(pathToFileURL('studio.html').href + '?capture');

const { duration, fps } = await page.evaluate(() => window.__scene);
const ffmpeg = spawn('ffmpeg', ['-y', '-f', 'image2pipe', '-framerate', String(fps),
  '-i', '-', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-crf', '18', out],
  { stdio: ['pipe', 'ignore', 'inherit'] });

const frames = Math.round(duration * fps);
for (let i = 0; i < frames; i++) {
  await page.evaluate(t => window.__seek(t), i / fps);
  ffmpeg.stdin.write(await page.screenshot({ type: 'png' }));
}
ffmpeg.stdin.end();
await new Promise(r => ffmpeg.on('close', r));
await browser.close();

有几个细节需要注意。window.__seek(t) 要等两个动画帧之后才 resolve,这样截图拿到的一定是新的 transform。image2pipe 让 ffmpeg 从 stdin 读取 PNG 序列。yuv420p 保证生成的文件能在浏览器中正常播放。如果 Playwright 还没装浏览器,先运行一次 npx playwright install chromium。

查看 __seek 与静帧验证的参考实现(示意代码)
// studio.html 内:确定性 seek,画完两帧后再返回
window.__seek = (t) => new Promise(resolve => {
  renderAt(t);                       // 纯函数:只依赖 t
  requestAnimationFrame(() =>
    requestAnimationFrame(resolve)); // 确保样式已提交并绘制
});
// stills.mjs:渲染起点 / 中点 / 终点三张静帧,先看图再信任镜头
import { chromium } from 'playwright';
import { pathToFileURL } from 'node:url';

const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } });
await page.goto(pathToFileURL('studio.html').href + '?capture');
const { duration } = await page.evaluate(() => window.__scene);

for (const [name, t] of [['start', 0], ['mid', duration / 2], ['end', duration - 1e-3]]) {
  await page.evaluate(t => window.__seek(t), t);
  await page.screenshot({ path: `still-${name}.png` });
}
await browser.close();

以上两段是按参考资料思路写的示意实现,具体以 agent 在你项目里生成的代码为准。

作者的实测结果:180 帧、1280×720、30 fps、时长 6.0 秒,输出 MP4 约 420 KB,总耗时约 20 秒。画面中城市塔楼的位移小于深色前景山丘,前景山丘最终完全移出画面,这正是视差效果。

claude-code-2-5d-parallax-motion-studio-deterministic-mp4-export-image-3


为 2.5D 工作室加一个“描述镜头”功能

接下来可以让编剧输入一句镜头描述,由模型返回关键帧 JSON。参考资料中的做法是直接调用 Anthropic Messages API:

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2026-10-21" \
  -H "content-type: application/json" \
  -d '{
    "model": "claude-opus-5-5",
    "max_tokens": 4096,
    "output_config": { "effort": "low" },
    "system": "Return only JSON: {\"camera\": [{\"t\",\"x\",\"y\",\"z\",\"yaw\",\"ease\"}], \"tracks\": {}}. Times in seconds, 0 to 6.",
    "messages": [{ "role": "user", "content": "Slow push-in, slight orbit right, title fades up in the first second." }]
  }'

这里有两个容易踩的坑。第一,effort 要写在 output_config.effort 里。第二,Opus 5.5 始终使用自适应思考,传 thinking: {"type": "disabled"} 会返回 400,而且思考 token 也计入 max_tokens。max_tokens 设得太低,JSON 会在数组中途被截断,stop_reason 为 "max_tokens"。

截断的或夹杂多余文字的回复,会在渲染时让 sample() 出错,所以这个调用最好配一个契约测试。参考资料用 Apidog CLI 让同一个 Claude Code 会话创建并运行测试,断言状态码 200、stop_reason 不是 "max_tokens"、返回文本能解析为 JSON,且 camera 数组的 t 从 0 递增到 6。用其他 API 测试工具或自己写的断言脚本也能达到同样目的。修改 system prompt 或 effort 后要重跑一遍,因为两者都会改变返回内容。


常见问题 FAQ

Q1:2.5D 视差动画和真正的 3D 动画有什么区别?

2.5D 把扁平的 2D 素材放在不同深度上,再用移动的相机拍摄,靠速度差产生纵深感。它不需要 3D 模型、灯光和 3D 引擎,用浏览器 CSS 就能实现,代价是相机不能大角度绕到图层背后。

Q2:为什么不直接录屏,而要逐帧 seek 截图?

录屏跟随真实时间,任何一帧慢了都会变成丢帧或重复帧。按 `i / fps` seek 后截图,每一帧的内容只由帧序号决定,所以在任何机器上的输出都一样。

Q3:补偿系数 1.75 是怎么算出来的?

透视距离 1200、深度 -900 时,投影缩放为 1200/2100 ≈ 0.571。补偿取倒数 `1 – z/p = 1 + 900/1200 = 1.75`,两者相乘等于 1,静止时图层就回到设计尺寸和位置。

Q4:首次构建为什么建议用 Opus 5.5 和 high effort?

首次构建要在一次会话里同时写好投影数学、关键帧引擎和导出管线。参考资料中,一次提示词加一轮修复就得到了可用的工作室。后续的小修改用 medium 或更低就够了。换成其他模型的效果目前没有公开对比数据,需要自己测试。

Q5:没有 API Key 能用这个工作室吗?

可以。工作室本身和 MP4 导出都在本地运行,不需要 Key。只有“描述镜头”功能要调用 Messages API,需要你自己的 Anthropic API Key。用 Claude Code 构建时消耗的 token,则按你所用账号的计费方式结算。

Q6:这套方法有哪些已知限制?

逐帧截图 PNG 的速度受分辨率和页面复杂度影响,参考资料里 20 秒的耗时只是单台笔记本的单次结果。CSS 3D 对大角度 yaw 和半透明叠层的渲染可能因浏览器版本而有差异。模型 ID、API 版本头和 effort 参数都有时效性,使用前请核对官方文档。

Q7:如何把“确定性渲染 + 静帧验证”迁移到其他程序化视频项目?

可以按下面的清单逐项检查:

– 渲染函数只接收 t,不读系统时钟、不依赖上一帧状态、不用未固定种子的随机数
– 提供一个“绘制完成才返回”的 seek 接口
– 提供隐藏 UI 的 capture 模式和固定视口尺寸
– 导出帧数严格等于 `round(duration × fps)`
– 改动镜头后先渲染起点、中点、终点三张静帧,确认无误再导出全片
– 对模型生成的 JSON 关键帧加契约测试:可解析、时间单调递增、未截断


Claude Code 2.5D 视差动画核心要点 Key Takeaways

  • 导出是设计的一部分:在提示词最后几行写死纯函数 renderAt(t) 和 seek 契约
  • 景深补偿公式:comp = 1 − z/perspective,尺寸和 x/y 偏移都要乘
  • 视差从哪里来:补偿只修正静止构图,运动时各层仍按深度呈现不同速度
  • 先看静帧再信镜头:起点、中点、终点三张图能发现读代码看不出的 bug
  • 按帧序号驱动时间:Playwright seek 加截图,ffmpeg image2pipe 编码成 H.264
  • effort 分阶段调整:首建用 high,微调用 medium,结构化 JSON 接口从 low 起步
  • 模型输出要加契约测试:防止 max_tokens 截断导致关键帧解析失败

总结

用 Claude Code 搭建 2.5D 视差动画工作室,核心有三点:

  1. 架构:场景图、时间轴、确定性渲染器三件套,第三件决定成片质量
  2. 数学:透视补偿系数 1 - z/p 同时作用于尺寸和偏移,静止时构图准确,运动时自带视差
  3. 管线:按 i/fps 逐帧 seek、截图后交给 ffmpeg 编码,输出不受机器性能影响

建议先照着本文的提示词结构跑通一次 6 秒样片,养成“改镜头先看三张静帧”的习惯。之后再加图层、接入“描述镜头”接口,或者把同一套确定性思路迁移到 Remotion 等其他程序化视频方案。


📚 参考资料

  1. How to Build a 2.5D Motion Studio With Opus 5.5:本文实践来源
  2. 链接: apidog.com/blog/build-2-5d-motion-studio-opus-5-5
  3. 说明: 包含完整提示词、工作室代码、导出脚本与“描述镜头”接口示例

  4. Anthropic 模型与 effort 文档:模型定位、上下文、定价与 effort 参数说明

  5. 链接: docs.anthropic.com
  6. 说明: 核对 claude-opus-5-5 的模型 ID、API 版本头和 output_config 用法

  7. ffmpeg 格式文档:image2pipe 等输入格式说明

  8. 链接: ffmpeg.org/ffmpeg-formats.html
  9. 说明: 理解 stdin 读取 PNG 序列与 H.264 编码参数

  10. Playwright 文档:浏览器自动化与截图 API

  11. 链接: playwright.dev
  12. 说明: page.evaluate、page.screenshot 与浏览器安装命令

延伸阅读

类似文章