APIMODELSAPIMODELS

Claude Code + Three.js: 3D scenes, small games and video export

TL;DR Claude Code does its best three.js work when you ask for one self-contained HTML page with three.js pinned through an import map, make everything that moves a pure function of time behind a window.renderAt(t) function, and have it capture and look at its own frames before it calls the work done. The same function lets Playwright capture every frame and ffmpeg turn them into an MP4: on our machine a 120-frame, 1280×720 test scene took 6.5 seconds to capture without a GPU. Save the rules as a Claude Code skill so every session follows them. Use Claude Fable 5.2 for the most demanding scenes and Opus 5.5 or Sonnet 5.5 for quick iterations.

Three.js is where Claude’s code-only creative work is easiest to share: one HTML file opens in any browser. After Claude Fable 5.2 launched, creators posted scenes and games built entirely in code, such as a single three.js page of a voxel island of Paris in 1888 that exposed a renderAt(t) function so every frame could be captured, a tilt maze where the model profiled its own code and cut frame time from 55 ms to 9 ms, and a small first-person shooter, playable in the browser, whose models, textures and sounds were all generated in code.

This page turns those habits into a workflow you can reuse. The scene file, the capture script and the ffmpeg command below were run on our own machine before we published them; they test the toolchain, not any model. We run apimodels.app, so the setup step points Claude Code at our endpoint, but every step works the same with any Anthropic-compatible endpoint.

Pick the model and point Claude Code at it

Set three environment variables: ANTHROPIC_BASE_URL=https://api.apimodels.app, your key in ANTHROPIC_AUTH_TOKEN, and ANTHROPIC_MODEL set to the model you want. You can switch inside a session with /model. Our suggestion is to start a scene on the strongest model and do the small tweaks on a less expensive one, since most of the work in a three.js session is re-reading the same file.

Model idPrice per 1M tokens (input / output)Use it for
claude-fable-5-2$4.80 / $24Large scenes, games, anything that needs many rounds of self-review
claude-opus-5-5$2.40 / $12Most scenes and most fix rounds
claude-sonnet-5-5$1.20 / $6Small fixes, parameter tweaks, quick prototypes

Ask for one self-contained page

Ask for a single HTML file that loads three.js through an import map with a pinned version. There is no build step, nothing to install, and the file opens the same way next month. Then describe the scene the way a director would: the subject, the camera move, the light and time of day, the mood, the length and the resolution. Say that every model, texture and sound must be generated in code unless you supply the assets, so nothing depends on files you cannot redistribute.

Two lines change the result more than any adjective. One requires a window.renderAt(t) function, explained in the next section. The other says the work is not done until Claude has captured a few frames, looked at them and fixed what looks wrong.

Make every frame a function of time

A normal three.js page animates with requestAnimationFrame and a clock, so what you see depends on how fast the machine is. Screen-recording such a page drops frames whenever rendering is slower than real time, and no two recordings match. A renderAt(t) function fixes that: it sets every moving thing from the time t alone and then renders, so frame 45 is the same picture whether you render it first, last or twice.

Three rules keep it honest: no Math.random in the scene unless it comes from a seeded generator, no reading the clock inside scene logic, and physics stepped with a fixed time step from zero up to t. Keep a live preview loop for when you open the page normally, and switch it off when the page is loaded with ?capture. In our test, rendering frame 45 straight after jumping to 3.9 seconds gave a PNG byte-for-byte identical to the one captured in order.

Let Claude look at its own frames

Claude Code can run the capture script itself and then open the PNG files, so it can see its own scene. Ask it to capture four to six key moments at a small size first, describe what is wrong with each (clipping, flat lighting, a camera that drifts out of frame, a material that reads as plastic), fix those things, and only then capture the full sequence. This loop is what separated the strongest public Fable 5.2 work from first drafts.

Export to MP4

The capture script in Quick start opens the page in headless Chromium with ?capture, calls renderAt for each frame and saves a PNG of the canvas; ffmpeg then encodes the frames. It needs no GPU: the SwiftShader flags make WebGL run on the CPU. On an Apple Silicon Mac our simple test scene, 120 frames at 1280×720, captured in 6.5 seconds and encoded to a 4-second H.264 file. Heavy scenes with shadows, post-processing or millions of particles capture much more slowly on the CPU; run headed Chrome on a machine with a GPU if that matters.

Small games

Keep the scope tiny and say so: one level or arena, the controls listed, the win and lose conditions. Ask for code-generated models and Web Audio sound effects, a visible frame-time readout while you develop, and a small test hook such as window.game.state, so a Playwright script can press keys and check that the score, health or position actually change. Then ask Claude to play-test with that script and to profile the page when the frame time climbs; profiling is how the tilt maze above went from 55 ms to 9 ms per frame.

Save the rules as a Claude Code skill

A skill is a folder with a SKILL.md file that Claude Code loads when a task matches its description. Put the file below in ~/.claude/skills/threejs-scene/ for all your projects, or in .claude/skills/threejs-scene/ inside one repository, and copy capture.mjs next to it. After that, a request such as “make a three.js scene of a lighthouse at dusk” picks up the single-file, renderAt and self-review rules without repeating them in every prompt.

What a scene costs

The code itself is the small part. As a rough estimate at 10 to 15 tokens per line, a 1,500-line page is about 20,000 output tokens, roughly $0.48 on Claude Fable 5.2 or $0.24 on Opus 5.5 at our prices. A real session costs several times that, because the model thinks before it writes, re-reads the file on every turn and goes through several fix rounds; long single-scene sessions from creators ran from tens of minutes to hours. Cache reads keep the re-reading affordable at $0.12 per million tokens on both models, and your console’s call records show the exact charge for every request.

When three.js is the wrong tool

Skip this route for photoreal people and complex character animation: code-built characters still look like rigs, and a video model or a Blender pipeline gets there sooner. Skip it too when the asset has to come from a specific licensed model or texture you cannot ship in a public page. And if the deliverable is a long, heavy film rather than an interactive page, budget for slow CPU capture or render on a GPU machine.

Início rápido

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."}]
  }'

Perguntas frequentes

Can Claude build three.js games?

Claude can build small three.js games when the scope is tight: one level, listed controls, and models, textures and sounds generated in code. After Claude Fable 5.2 launched, one creator posted a small first-person shooter, playable in the browser, with every model, texture and sound generated in code. Ask for a test hook such as window.game.state so Claude can play-test with Playwright, and expect large games to need many sessions.

How do I turn a three.js animation into a video?

Render it frame by frame instead of screen-recording it. Expose a window.renderAt(t) function that draws the scene for time t, capture each frame with a Playwright script, and encode the PNGs with ffmpeg. Our 120-frame, 1280×720 test captured in 6.5 seconds without a GPU, and a frame rendered out of order came out byte-for-byte identical to the one captured in sequence.

Is there a Claude Code skill for three.js?

Claude Code has no built-in three.js skill, but writing one takes a single SKILL.md file. Put it in ~/.claude/skills/threejs-scene/ with a description that names three.js scenes and games, and list the rules you want every session to follow: one self-contained page, renderAt(t), code-generated assets and a capture-and-fix loop. The one on this page is free to copy.

Which Claude model is best for three.js?

Claude Fable 5.2 is the strongest choice for large scenes and games that need many rounds of self-review; Anthropic’s launch table puts it ahead of Opus 5.5 on agentic coding. For most scenes and fix rounds, Opus 5.5 at $2.40 / $12 per million tokens on apimodels.app is half the price of Fable 5.2, and Sonnet 5.5 at $1.20 / $6 covers small tweaks.

Why does my screen-recorded three.js video stutter?

A screen recording drops frames whenever the scene renders slower than real time, so heavy moments stutter and two recordings never match. Drive the animation from time with renderAt(t) and capture frame by frame; each frame then takes as long as it needs and the video plays back smoothly at the frame rate you choose.

Do I need a GPU to capture three.js frames?

No GPU is needed for simple scenes: headless Chromium with the SwiftShader flags renders WebGL on the CPU, and our 120-frame test scene captured in 6.5 seconds on an Apple Silicon Mac. Shadows, post-processing and large particle counts slow CPU rendering down a lot, so use a GPU machine with headed Chrome for heavy scenes.

Relacionado

Presente em
APIMODELS on Product HuntAPIMODELS featured on ToolifyAPIMODELS featured on There's An AI For That