Claude Code and Three.js: build a 3D browser game

By GamesByAI · Updated · 37 sources

Three.js runs 298 of the 492 games in our catalog, and Claude Code is the tool their creators credit most often. The library is plain JavaScript with no editor, so a coding agent can write, run and check the whole game itself. The catch is the release. Three.js changes its API several times a year, and models still write code for releases that are years old.

This guide sets up a project that keeps the agent on the current API, builds a small game in slices and makes the agent prove each change in a real browser. Every tool and API fact was checked on October 1, 2026. To install the agent, use the Claude Code guide or the Codex guide. For engine choice and the general build order, see how to make a 3D game with AI.

1. Pin a Three.js release before the first prompt

Use Three.js r186, published on September 24, 2026 (release notes). On npm it’s three@0.186.1 (npm). Four releases have shipped in 2026, r183 to r186, and each can rename or remove APIs. The Migration Guide lists every change and says deprecation warnings last for 10 releases, so code from an old tutorial can fail outright.

The installation manual calls npm with a build tool “the recommended approach for most users” and offers a CDN import map for projects without one. A game that will grow models, tests and a release build wants npm and Vite:

npm create vite@latest my-game -- --template vanilla-ts
cd my-game
npm install --save-exact three@0.186.1
npm install --save-dev @types/three@0.186.0

--save-exact writes the version without a caret, so a fresh install can’t move the project to a newer release behind the agent’s back. The type definitions follow the same release numbers. In r186’s types, renderer.outputEncoding and THREE.sRGBEncoding no longer exist, so the type check rejects them; deprecated APIs such as THREE.Clock still compile, with a deprecation mark in the editor.

2. Give the agent the current Three.js docs

Three.js publishes docs written for language models. llms.txt tells a model to use ES modules and an import map instead of old three.min.js script tags. It makes WebGLRenderer the default and keeps WebGPURenderer with TSL for custom node materials. llms-full.txt adds code examples and the full TSL reference, then links every class to its own Markdown page, such as https://threejs.org/docs/pages/WebGLRenderer.html.md (generator script).

Save a copy of the guidance in the repo on the day you pin. The per-class pages online always describe the newest release, so have the agent fetch one only for the class it’s working on:

mkdir -p docs
curl -o docs/three-r186-llms-full.txt https://threejs.org/docs/llms-full.txt

The installed package has the final word. The npm package ships the library’s source and its addons (package.json), so node_modules/three/src/ and node_modules/three/examples/jsm/ show exactly what r186 contains. Tell the agent to look there before it uses an API it isn’t sure of.

3. Old Three.js code that models still write

These patterns fill years of tutorials, so they turn up in agent output. Each one changed in the release listed, according to the Migration Guide and the API docs.

Old code Current code in r186 Changed in
a three.min.js script tag and a global THREE ES module imports from three, through npm or an import map build files removed in r161
addons from examples/js/ imports from three/addons/ r148
Geometry, Face3 BufferGeometry Geometry r125, Face3 r126
outputEncoding, sRGBEncoding outputColorSpace, sRGB by default r152
texture.encoding texture.colorSpace, by role: SRGBColorSpace for color maps, NoColorSpace for data maps, LinearSRGBColorSpace for linear HDR r152
physicallyCorrectLights nothing: physically based lighting is the default r150, r155
THREE.Clock THREE.Timer, with update() once per frame Timer in core r179, Clock deprecated r183
a requestAnimationFrame loop renderer.setAnimationLoop() the renderer docs advise it (docs)
ShaderMaterial, EffectComposer under three/webgpu TSL node materials and the WebGPU post-processing stack unsupported by WebGPURenderer (manual)
RGBELoader HDRLoader r180

The texture row fails without an error. In r186 THREE.sRGBEncoding is undefined, so texture.encoding = THREE.sRGBEncoding only adds an unused encoding property, and the texture keeps its default color space. The color management manual says color textures such as .map “must be annotated” with SRGBColorSpace, so that texture renders with wrong colors and no console message.

Don’t swap the old line for SRGBColorSpace everywhere, though: the same manual leaves normal and roughness maps at NoColorSpace and marks linear formats such as OpenEXR as LinearSRGBColorSpace.

The rules file below turns the left column into one “never use” line. It costs a few lines of context in every session and stops the agent from reaching for a pattern it saw in older code.

4. Check the release a Three.js skill was written for

A skill is a folder with a SKILL.md that Claude Code loads when a task matches its description (skills docs). Our skills guide covers the format and the folders Codex and Cursor read. Community Three.js packs exist, and they age like any tutorial:

  • threejs-game-skills (MIT) covers whole games: a director skill, gameplay systems with a Vite and TypeScript scaffold, QA and release checks, and generators for models, textures and audio. The generators call paid services (Tripo, ElevenLabs) with your own API keys, and the director includes a script that sources your shell profiles to check whether those keys are set (README).
  • threejs-skills covers the API area by area. Its README says it was audited against “r160+”, and its fundamentals skill still uses THREE.Clock and requestAnimationFrame, the two patterns the r186 docs replace.

Read every skill, and every script it runs, before you install it. Then tell the agent which source wins when they disagree: your rules file first, then the pinned docs, then any skill.

5. Copy the project rules for Three.js

Save these rules as AGENTS.md in the project root. Codex reads it as project guidance (Codex docs), and Claude Code reads it when the project has no CLAUDE.md. With a CLAUDE.md, put @AGENTS.md at its top (Claude Code docs). They are conventions for this project, not engine requirements:

# Game rules: Three.js r186

- Three.js 0.186.1 from npm, pinned exactly. Vite, TypeScript, @types/three.
  Import from 'three' and 'three/addons/...'. No CDN script tags.
- Renderer: WebGLRenderer. Do not import 'three/webgpu' unless I ask.
- Unsure of an API? Check node_modules/three/src and node_modules/three/examples/jsm
  before writing it. docs/three-r186-llms-full.txt has the guidance and TSL.
- Never use: Geometry, outputEncoding, sRGBEncoding, texture.encoding,
  physicallyCorrectLights, Clock, RGBELoader, examples/js.
- Loop: renderer.setAnimationLoop. Time: one THREE.Timer with
  timer.connect(document) and timer.update() once per frame.
  Simulate in fixed 1/60 s steps; clamp the frame delta to 0.1 s.
- Scale: 1 unit = 1 meter, +Y up, the same as glTF.
- Size the canvas with CSS. Drawing buffer = CSS size x min(devicePixelRatio, 2).
- Removing an object does not free GPU memory. Dispose geometries, materials
  and textures you drop, unless another object still uses them.
- One shadow-casting light. Repeated meshes use InstancedMesh.
- No new Vector3, Quaternion or Matrix4 inside the frame loop: reuse scratch objects.
- HUD, menus and text are HTML over the canvas.
- Models: GLB in public/models/, loaded through src/loaders.ts.
- With ?test=1, expose window.gameState(), window.advance(ms), window.restart()
  and window.renderInfo(). Keep their output stable.
- After every change run npm run typecheck, npm test and npm run e2e.
  Report renderer.info draw calls and triangles after visual changes.

Several rules rest on documented facts. Timer.connect(document) uses the Page Visibility API to avoid a huge time step after the player switches tabs (Timer docs). glTF defines +Y as up and meters as its unit, so models land at the right size (glTF spec). Removing a mesh from the scene doesn’t dispose its geometry or material (disposal manual).

The pixel-ratio cap is our choice, and the reason comes from the responsive design manual: a phone with a device pixel ratio of 3 has to render 9 times the pixels. Each shadow-casting light draws every shadow-casting object again, and a point light six times (shadows manual), so one shadow light is the cheap default.

6. Build the game one slice per prompt

The example game: drive a toy car over a hill road and collect five rings before a 60-second timer runs out. Each prompt ends with a check the agent can run.

First prompt: the scene and the controls.

Read AGENTS.md. Create the scene: a ground plane, a box for the car, a third-person camera that follows it and one directional light that casts shadows. Arrow keys and WASD drive; on a phone, dragging on the left half steers and holding the right half accelerates. Add the ?test=1 hooks from the rules. Done when I can drive in a circle on desktop and in phone emulation, and npm run typecheck passes.

Second prompt: one complete run.

Add five glowing rings as one InstancedMesh, a 60-second timer, win and lose screens in HTML and a Restart button. Restart disposes everything the run created and builds it again. Done when restarting ten times leaves renderer.info.memory geometries and textures at the numbers they had after the first start.

Third prompt: the first real model.

Replace the car box with public/models/car.glb, loaded through src/loaders.ts. Keep the box as a fallback if loading fails, and log the error. Report draw calls and triangles before and after.

When a check fails, paste the actual error and the line it points to, and ask for a fix that keeps the current behavior. A screenshot helps with what the agent can’t read from a number, such as a camera clipping into a hill.

7. Test WebGL in a headless browser

Three facts decide whether a browser test of a Three.js game means anything.

  1. The test browser may have no WebGL. On machines without a GPU, Chromium can fall back to SwiftShader, a software renderer. Its docs say that automatic fallback is deprecated and that WebGL context creation will soon fail instead. To test on headless or GPU-less systems, opt in with --enable-unsafe-swiftshader, which is meant for content you trust, such as your own game (Chromium docs).
  2. Reading the canvas can return a black image. By default the browser clears a WebGL drawing buffer after drawing it, as the manual’s screenshot tip shows. Take screenshots with Playwright, or render right before you read the canvas.
  3. Frame rate on a test machine says little about phones. Test counts instead. renderer.info reports draw calls, triangles, geometries and textures (renderer docs). They stay stable when the scene, camera, viewport, renderer settings and browser stay fixed. Hardware support can still change them: without the WEBGL_multi_draw extension, a BatchedMesh is drawn one object per call instead of in one call (r186 source). Run budget tests on one pinned browser.

Install Playwright with npm install --save-dev @playwright/test and npx playwright install chromium. This config starts the Vite dev server for the test run (web server docs) and passes the SwiftShader flag through the browser’s launch arguments (launch options):

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: 'e2e',
  webServer: {
    command: 'npx vite --port 5173 --strictPort',
    url: 'http://localhost:5173',
    reuseExistingServer: true,
  },
  use: {
    baseURL: 'http://localhost:5173',
    viewport: { width: 960, height: 540 },
    launchOptions: { args: ['--enable-unsafe-swiftshader'] },
  },
});

The game’s test hook reports the renderer’s numbers:

// src/test-hooks.ts: installed only when the URL has ?test=1
import type { WebGLRenderer } from 'three';

export function installRenderInfo(renderer: WebGLRenderer) {
  Object.assign(window, {
    renderInfo: () => ({
      calls: renderer.info.render.calls,
      triangles: renderer.info.render.triangles,
      geometries: renderer.info.memory.geometries,
      textures: renderer.info.memory.textures,
    }),
  });
}

And one test checks that WebGL exists, that the scene stays inside its budget and that restarts free their memory:

// e2e/smoke.spec.js
import { test, expect } from '@playwright/test';

// Placeholders: replace with numbers measured on your slowest phone.
const MAX_CALLS = 120;
const MAX_TRIANGLES = 250_000;

test('renders, stays in budget and frees memory on restart', async ({ page }) => {
  const errors = [];
  page.on('pageerror', (e) => errors.push(e.message));
  page.on('console', (m) => m.type() === 'error' && errors.push(m.text()));

  await page.goto('/?test=1&seed=1');
  const webgl2 = await page.evaluate(() =>
    Boolean(document.createElement('canvas').getContext('webgl2')));
  expect(webgl2).toBe(true);
  await page.waitForFunction(() => window.gameState?.().scene === 'playing');
  await page.evaluate(() => window.advance(1000));

  const first = await page.evaluate(() => window.renderInfo());
  expect(first.calls).toBeLessThanOrEqual(MAX_CALLS);
  expect(first.triangles).toBeLessThanOrEqual(MAX_TRIANGLES);

  for (let i = 0; i < 10; i++) {
    await page.evaluate(() => { window.restart(); window.advance(500); });
  }
  const after = await page.evaluate(() => window.renderInfo());
  expect(after.geometries).toBe(first.geometries);
  expect(after.textures).toBe(first.textures);

  await expect(page).toHaveScreenshot('driving.png', { maxDiffPixelRatio: 0.01 });
  expect(errors).toEqual([]);
});

The two ceilings are placeholders. Play a build on the slowest phone you target, and once it holds a steady frame rate, read renderInfo() there and use those numbers. The restart loop catches the classic leak: a run that rebuilds its meshes without disposing the old ones adds geometries every time. Playtesting games with AI explains gameState(), advance() and screenshot baselines in detail.

8. Keep a Three.js game fast on phones

Try these four levers, and measure with renderInfo() before and after each one.

  • Draw calls. InstancedMesh renders many objects with the same geometry and material in fewer draw calls (docs). Trees, rings, coins and bullets are the usual candidates.
  • Shadows. Keep one shadow-casting light, and turn castShadow off for small objects nobody would miss (shadows manual).
  • Shader compile stalls. Call await renderer.compileAsync(scene, camera) behind the start screen. It uses the KHR_parallel_shader_compile extension, and the docs recommend it over compile() (renderer docs).
  • Models. Compress them with glTF Transform, then give the loader every decoder it might need.

That last step trips agents most. In glTF Transform 4.5.1, optimize compresses with Meshopt and caps textures at 2048 pixels by default (CLI source). A model compressed that way loads only if GLTFLoader has a Meshopt decoder, Draco files need a DRACOLoader, and KTX2 textures need a KTX2Loader (GLTFLoader docs):

npx @gltf-transform/cli optimize car-raw.glb public/models/car.glb \
  --texture-compress webp --texture-size 1024
mkdir -p public/draco public/basis
cp node_modules/three/examples/jsm/libs/draco/gltf/* public/draco/
cp node_modules/three/examples/jsm/libs/basis/* public/basis/
// src/loaders.ts: one loader for the whole game, so decoders load once
import type { WebGLRenderer } from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';

export function createGltfLoader(renderer: WebGLRenderer) {
  return new GLTFLoader()
    .setDRACOLoader(new DRACOLoader().setDecoderPath('./draco/'))
    .setKTX2Loader(new KTX2Loader().setTranscoderPath('./basis/').detectSupport(renderer))
    .setMeshoptDecoder(MeshoptDecoder);
}

The DRACOLoader docs recommend one instance reused across the game. KTX2Loader must run detectSupport() before it loads a texture (KTX2Loader docs). Image to 3D for games covers making the models in the first place.

For a person debugging by hand, the Three.js repository includes a Chrome DevTools extension that shows the scene tree, each object’s material and every renderer’s stats and memory.

9. Ship the Three.js build

Set base: './' in vite.config.ts. Vite’s docs list it for embedded deployment (Vite docs), and itch.io pages and iframes serve your game from a folder, not the site root. Then npm run build writes the game to dist/.

Start play from a button. The manual notes that a canvas only receives keyboard events with a tabindex (tips), so listen for keys on window, and let the first click give the page focus. Play the release build on a real phone before you publish. How to make a mobile game with AI covers touch and screen fit, and where to publish a browser game covers hosts.

Three.js games built with Claude Code and other agents

Claude Code is credited on 119 of the Three.js games we list, Cursor on 72 and Codex on 59, as of October 1, 2026. The entries record the engine and the tools, not which skills, rules or tests a creator used.

AI tools credited on the Three.js games in our catalog
  1. Claude Code11940%
  2. Cursor7224%
  3. Codex5920%
  4. Google Antigravity165%
  5. Claude app124%
  6. Gemini app62%

Out of 298 games; a game can count in several bars.

  • A Game About Capybaras Delivering Food won Vibe Jam 2026, first of 945 entries. Its creator credits Claude Code, and the entry says music, sound effects and 3D rigging came from separate AI tools.
  • Eyrie placed sixth in the same jam and won its Most Atmospheric award: drop-in online co-op, on desktop and phones, made with Claude Code.
  • Sunset City - The Dark Tower placed eighth. Its creator describes it as Three.js concepts gathered over time and finally combined into one game, built with Claude Code.
  • Floppy Brawler placed 13th: an online ragdoll brawler whose creator says Claude’s first output on day one started the project.
  • Out Of The Box credits Claude Code and Claude Opus 5.5 in its README. The lab is built from shader geometry, and its music and effects are synthesized in Web Audio rather than loaded from files.
  • Three.js Quake is a port of Quake’s shareware episode by Mr.doob, who created Three.js. Its README credits the port to him “with @claude”.

Browse the Three.js hub, Claude Code games, Cursor games, Codex games and the Vibe Jam 2026 archive.

Finish the Three.js build

Call the build done when the release version starts, plays a full run, restarts ten times without growing renderer.info.memory, and works with touch on a real phone, with no console errors. Record the Three.js release, the renderer and the budgets with the build, so the next session starts from the same facts.

Then submit the game with its playable URL and an honest account of what the AI made. Every game is reviewed before it’s listed, under our editorial policy.

Games to look at

Questions

Can Claude Code make Three.js games?

Yes. Three.js is plain JavaScript with no editor, so Claude Code can write the game, run it and check it in a browser. Our catalog lists 119 Three.js games whose creators credit Claude Code, including A Game About Capybaras Delivering Food, the Vibe Jam 2026 winner.

Which Three.js version should I tell the agent to use?

Pin one release and name it in your rules file. On October 1, 2026 the latest is r186, npm package three@0.186.1. Older tutorials and training data use APIs that have since been removed, such as outputEncoding and Geometry, or deprecated, such as Clock, which r183 replaced with Timer.

Should a game made with AI use WebGPURenderer?

Start with WebGLRenderer. The Three.js manual calls it the recommended choice for pure WebGL 2 apps and still describes WebGPURenderer as experimental. Switch when you need TSL or compute shaders, and know that ShaderMaterial, onBeforeCompile and EffectComposer don't work with it.

Why is my Three.js game black in a headless browser test?

Usually WebGL is missing or the canvas was read after the browser cleared it. Chromium has deprecated its automatic fallback to software WebGL, so on a machine without a GPU start the test browser with --enable-unsafe-swiftshader, and take screenshots with Playwright instead of canvas.toDataURL().

Are there Three.js skills for Claude Code?

Yes. Three.js itself publishes docs for language models at threejs.org/docs/llms.txt, and community skill packs such as threejs-game-skills and threejs-skills exist. Check which release a skill was written for: one popular pack still teaches THREE.Clock, which r183 deprecated.

Does this setup work with Codex or Cursor?

Yes. Codex reads AGENTS.md, Claude Code reads it when a project has no CLAUDE.md, and Cursor reads it too. The checks are npm scripts that any agent can run.