APIMODELSAPIMODELS

Claude Code 写 Three.js:3D 场景、小游戏和导出视频

TL;DR 让 Claude Code 写好 Three.js,关键是四件事:要一个单文件 HTML 页面,three.js 用 import map 固定版本;所有会动的东西都只由时间决定,统一放进 window.renderAt(t);交付前让它自己截几帧看过、改过;再把这些规则存成 Claude Code 技能,每次会话都照做。同一个 renderAt(t) 还能让 Playwright 逐帧截图、ffmpeg 合成 MP4:在我们的电脑上,一个 120 帧、1280×720 的测试场景不用显卡,6.5 秒就截完了。最难的场景交给 Claude Fable 5.2,日常改动用 Opus 5.5 或 Sonnet 5.5。

Three.js 是 Claude「只靠写代码做创作」最容易分享的形式:一个 HTML 文件,任何浏览器都能打开。Claude Fable 5.2 发布后,不少创作者发了全靠代码做出来的场景和游戏:一个单文件 three.js 页面做的 1888 年巴黎体素小岛,页面暴露 renderAt(t) 函数,每一帧都能截下来;一个倾斜木球迷宫,模型自己做了性能分析,把每帧耗时从 55 毫秒降到 9 毫秒;还有一个能在浏览器里玩的小型第一人称射击游戏,模型、贴图和音效全部用代码生成。

这一页把这些做法整理成一套能复用的流程。下面的场景文件、截帧脚本和 ffmpeg 命令,发布前都在我们自己的电脑上原样跑过;验证的是工具链,不是哪个模型。我们运营 apimodels.app,所以配置那一步写的是我们的地址,但每一步换成任何兼容 Anthropic 的地址都一样能用。

选模型,配好 Claude Code

设三个环境变量:ANTHROPIC_BASE_URL=https://api.apimodels.app,ANTHROPIC_AUTH_TOKEN 填你的 key,ANTHROPIC_MODEL 填想用的模型;会话里也可以用 /model 随时切换。我们的建议是:场景的第一版用最强的模型写,后面的小改动换成更省钱的模型,因为 Three.js 会话里大部分开销都花在反复读同一个文件上。

模型名每百万 token 价格(输入 / 输出)适合
claude-fable-5-2$4.80 / $24大场景、游戏,需要很多轮自查的活
claude-opus-5-5$2.40 / $12大多数场景和修改
claude-sonnet-5-5$1.20 / $6小修小改、调参数、快速出原型

要一个单文件页面

要求做成一个 HTML 文件,three.js 用 import map 引入并固定版本号:不用构建、不用安装,下个月打开也一样。然后像导演一样描述场景:主体、镜头怎么动、光线和时间、氛围、时长和分辨率。再写明所有模型、贴图和声音都用代码生成,除非你自己提供素材,这样成品不依赖任何你不能再分发的文件。

有两句话比任何形容词都管用。一句是要求页面提供 window.renderAt(t) 函数,下一节细讲;另一句是:在它自己截几帧看过、把看着不对的地方改掉之前,不算完成。

让每一帧只由时间决定

普通的 Three.js 页面用 requestAnimationFrame 加时钟来动,画面取决于机器有多快。录屏的话,只要渲染慢于实时就会丢帧,而且每次录出来都不一样。renderAt(t) 解决的就是这个:它只根据时间 t 设置所有会动的东西,然后渲染一帧。不管先渲染、后渲染还是渲染两次,第 45 帧都是同一张画面。

要守住三条:场景里不用 Math.random,要用就用带种子的随机数生成器;场景逻辑里不读时钟;物理模拟用固定步长从 0 一路算到 t。正常打开页面时保留实时预览循环,带 ?capture 参数打开时关掉它。我们测试时,先跳到 3.9 秒再渲染第 45 帧,截出来的 PNG 和按顺序截的那张逐字节相同。

让 Claude 看自己的截图

Claude Code 可以自己跑截帧脚本,再打开 PNG 文件,所以它能看到自己做的场景。让它先用小尺寸截四到六个关键时刻,逐张说出哪里不对(穿模、光线发平、镜头跑出画面、材质像塑料),改完再截完整序列。公开的 Fable 5.2 作品里,好作品和初稿的区别主要就在这一圈自查。

导出成 MP4

「调用示例」里的截帧脚本会用无头 Chromium 带 ?capture 打开页面,逐帧调用 renderAt,把画布存成 PNG;然后用 ffmpeg 合成视频。整个过程不需要显卡:两个 SwiftShader 参数让 WebGL 在 CPU 上跑。在一台 Apple 芯片的 Mac 上,我们这个简单的测试场景(120 帧、1280×720)6.5 秒截完,合成了 4 秒的 H.264 视频。带阴影、后期处理或上百万粒子的重场景,在 CPU 上截会慢得多;在意速度的话,换一台有显卡的机器,用有界面的 Chrome 跑。

做小游戏

范围一定要小,而且要写明:一个关卡或一块场地、列出操作方式、写清输赢条件。要求模型和 Web Audio 音效都用代码生成,开发时在画面上显示每帧耗时,再留一个小的测试入口,比如 window.game.state,让 Playwright 脚本能按键,并检查分数、血量或位置是不是真的变了。然后让 Claude 用这个脚本自己试玩,帧耗时上去了就做性能分析;上面那个木球迷宫,就是靠性能分析从每帧 55 毫秒降到 9 毫秒的。

把规则存成 Claude Code 技能

技能就是一个带 SKILL.md 的文件夹,任务和它的描述对得上时,Claude Code 会自动加载。把下面这个文件放进 ~/.claude/skills/threejs-scene/(所有项目都生效),或者某个仓库里的 .claude/skills/threejs-scene/(只对这个仓库生效),再把 capture.mjs 放在旁边。之后只要说一句「用 three.js 做一个黄昏灯塔的场景」,单文件、renderAt、自查这些规则就都带上了,不用每次都写进提示词。

做一个场景花多少钱

代码本身只占小头。按每行 10 到 15 个 token 粗估,1,500 行的页面大约 2 万个输出 token,按我们的价格,用 Claude Fable 5.2 约 $0.48,用 Opus 5.5 约 $0.24。真实会话会是这个数的好几倍:模型写之前要思考,每一轮都要重读文件,还要改好几轮;创作者公开的单个场景,会话时长从几十分钟到几个小时不等。重读的部分走缓存,两个模型的缓存读都是每百万 token $0.12;每次请求的准确扣费都能在控制台的调用记录里看到。

什么时候别用 Three.js

要逼真的人物、复杂的角色动画,别走这条路:代码搭出来的角色还是一眼就看得出骨架,用视频模型或者 Blender 流程更快。素材必须用某个有版权的特定模型或贴图、又不能放进公开网页时,也别用。如果要交付的是又长又重的影片而不是交互网页,要么接受 CPU 截帧很慢,要么放到有显卡的机器上渲染。

调用示例

scene.html · minimal page with renderAt(t) (tested)

<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>renderAt demo</title>
<style>html,body{margin:0;background:#0b0d12;overflow:hidden}canvas{display:block}</style>
<script type="importmap">
{ "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } }
</script>
</head>
<body>
<script type="module">
import * as THREE from 'three';

const W = 1280, H = 720, DURATION = 4; // seconds
const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setPixelRatio(1);
renderer.setSize(W, H);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x0b0d12);
const camera = new THREE.PerspectiveCamera(40, W / H, 0.1, 100);

scene.add(new THREE.HemisphereLight(0xbfd4ff, 0x20140a, 0.6));
const sun = new THREE.DirectionalLight(0xffe2b8, 2.2);
sun.position.set(4, 6, 3);
scene.add(sun);

const ground = new THREE.Mesh(new THREE.CircleGeometry(6, 64), new THREE.MeshStandardMaterial({ color: 0x1c2230, roughness: 0.9 }));
ground.rotation.x = -Math.PI / 2;
scene.add(ground);

const gem = new THREE.Mesh(new THREE.IcosahedronGeometry(1, 0), new THREE.MeshStandardMaterial({ color: 0x6fd3ff, metalness: 0.3, roughness: 0.25, flatShading: true }));
gem.position.y = 1.4;
scene.add(gem);

// Everything that moves is a pure function of t: no clock, no Math.random, no accumulated state.
function renderAt(t) {
  const a = (t / DURATION) * Math.PI * 2;
  gem.rotation.set(a * 0.5, a, 0);
  gem.position.y = 1.4 + Math.sin(a * 2) * 0.2;
  camera.position.set(Math.cos(a * 0.25) * 6, 2.5, Math.sin(a * 0.25) * 6);
  camera.lookAt(0, 1.2, 0);
  renderer.render(scene, camera);
}
window.renderAt = renderAt;
window.DURATION = DURATION;

// Live preview only when not being captured.
if (!new URLSearchParams(location.search).has('capture')) {
  const start = performance.now();
  renderer.setAnimationLoop(() => renderAt(((performance.now() - start) / 1000) % DURATION));
} else {
  renderAt(0);
}
</script>
</body>
</html>

capture.mjs · capture every frame with Playwright (tested)

// capture.mjs: render every frame of a page that exposes window.renderAt(t)
// usage: node capture.mjs scene.html frames 30
import { chromium } from 'playwright'
import { mkdirSync } from 'node:fs'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'

const [file, outDir = 'frames', fps = 30] = process.argv.slice(2)
mkdirSync(outDir, { recursive: true })

const browser = await chromium.launch({
  executablePath: process.env.CHROME_BIN || undefined, // optional: an existing Chrome
  args: ['--use-angle=swiftshader', '--enable-unsafe-swiftshader'], // WebGL without a GPU
})
const page = await browser.newPage({ viewport: { width: 1280, height: 720 } })
page.on('pageerror', (e) => console.error('page error:', e.message))
await page.goto(pathToFileURL(resolve(file)).href + '?capture=1')
await page.waitForFunction(() => typeof window.renderAt === 'function')

const frames = Math.round((await page.evaluate(() => window.DURATION)) * fps)
for (let i = 0; i < frames; i++) {
  await page.evaluate((t) => window.renderAt(t), i / fps)
  await page.locator('canvas').screenshot({ path: `${outDir}/${String(i).padStart(4, '0')}.png` })
}
console.log(`${frames} frames written to ${outDir}/`)
await browser.close()

Terminal · install, capture, encode (capture and encode tested)

pnpm add -D playwright          # or: npm i -D playwright
npx playwright install chromium
node capture.mjs scene.html frames 30
ffmpeg -framerate 30 -i frames/%04d.png -c:v libx264 -pix_fmt yuv420p -crf 18 scene.mp4

~/.claude/skills/threejs-scene/SKILL.md (ours)

---
name: threejs-scene
description: Build, check and export three.js scenes and small browser games. Use when the user asks for a 3D web scene, a three.js animation or game, or a video made from one.
---

# three.js scenes

1. Deliver one self-contained HTML file. Load three.js through an import map
   with a pinned version. No build step.
2. Generate every model, texture and sound in code unless the user supplies
   assets.
3. Expose window.renderAt(t) and window.DURATION. Everything that moves is a
   pure function of t: no Math.random without a seeded generator, no clock
   reads in scene logic, physics stepped with a fixed time step from 0 to t.
   Run a live preview loop only when the URL has no ?capture.
4. Before calling the work done, run capture.mjs (next to this file) for four
   to six key moments at a small size, open the PNGs, list what looks wrong,
   fix it, and repeat. Then capture the full sequence.
5. For games, keep the scope to one level, show frame time on screen while
   developing, expose window.game.state, and play-test with a Playwright
   script that presses keys and checks that the state changes.
6. To make a video: node capture.mjs scene.html frames 30, then
   ffmpeg -framerate 30 -i frames/%04d.png -c:v libx264 -pix_fmt yuv420p scene.mp4

Prompt template · scene (ours)

Make a three.js scene: [subject], [camera move], [light and time of day],
[mood]. [length] seconds at [1280x720, 30fps].
Follow the threejs-scene skill: one self-contained page, renderAt(t),
everything generated in code. Capture key frames, look at them and fix what
looks wrong before you tell me it is done. Then export scene.mp4.

Prompt template · small game (ours)

Build a small [genre] game in three.js: one [level / arena], controls
[keys], you win when [condition], you lose when [condition].
All models, textures and Web Audio sounds generated in code.
Show frame time on screen and expose window.game.state.
Play-test it with a Playwright script that presses the keys and checks the
state changes; profile and fix anything above 16 ms per frame.

cURL

curl https://api.apimodels.app/v1/messages \
  -H "x-api-key: $APIMODELS_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-fable-5-2",
    "max_tokens": 64000,
    "output_config": { "effort": "high" },
    "messages": [{"role": "user", "content": "Write one self-contained three.js HTML page of a lighthouse at dusk. Expose window.renderAt(t) and window.DURATION = 8; everything that moves must be a pure function of t."}]
  }'

常见问题

Claude 能用 three.js 写游戏吗?

能,前提是范围够小:一个关卡、写明操作方式,模型、贴图和音效都用代码生成。Claude Fable 5.2 发布后,就有创作者发了一个能在浏览器里玩的小型第一人称射击游戏,模型、贴图和音效全是代码生成的。记得要求一个测试入口(比如 window.game.state),让 Claude 能用 Playwright 自己试玩;大型游戏要做好分很多次会话的准备。

three.js 动画怎么导出成视频?

逐帧渲染,不要录屏。页面提供一个 window.renderAt(t),按时间 t 画出当前画面,用 Playwright 脚本逐帧截图,再用 ffmpeg 把 PNG 合成视频。我们 120 帧、1280×720 的测试场景不用显卡 6.5 秒截完,打乱顺序单独渲染的一帧和按顺序截的那帧逐字节相同。

有没有 three.js 的 Claude Code 技能(skill)?

Claude Code 没有自带 three.js 技能,但自己写一个只要一个 SKILL.md。放进 ~/.claude/skills/threejs-scene/,描述里写明用于 three.js 场景和游戏,再列出希望每次会话都遵守的规则:单文件页面、renderAt(t)、素材用代码生成、截图自查再改。本页给的那份可以直接复制。

写 three.js 用哪个 Claude 模型最好?

大场景、需要很多轮自查的游戏,用 Claude Fable 5.2 最稳,Anthropic 的发布表上它的 agent 编程成绩高于 Opus 5.5。大多数场景和修改用 Opus 5.5 就够,apimodels.app 上每百万 token $2.40 / $12,是 Fable 5.2 的一半;小修小改用 Sonnet 5.5,$1.20 / $6。

录屏录下来的 three.js 视频为什么一卡一卡的?

只要场景渲染慢于实时,录屏就会丢帧,所以画面一重就卡,而且两次录的都不一样。用 renderAt(t) 让动画只由时间驱动,再逐帧截图;每一帧想渲染多久都行,合成出来的视频按你设定的帧率顺滑播放。

截 three.js 的帧需要显卡吗?

简单场景不需要:无头 Chromium 加上 SwiftShader 参数,WebGL 在 CPU 上就能跑,我们 120 帧的测试场景在 Apple 芯片的 Mac 上 6.5 秒截完。阴影、后期处理、大量粒子会让 CPU 渲染慢很多,重场景建议换有显卡的机器、用有界面的 Chrome 跑。

相关阅读

收录于
APIMODELS on Product HuntAPIMODELS featured on ToolifyAPIMODELS featured on There's An AI For That