Status: FINAL (2026-07-26), amended 2026-09-01 after the first style review of the ported corpus. §5, §7 and §8 changed — see §15. The amendments reconcile this spec with AGENTS.md v1.0, which is the operational contract agents read; where the two ever disagree, AGENTS.md is what actually steers the work and this file should be corrected to match. Executing per docs/ROADMAP.md. Name note: "r3f-examples" chosen as pragmatic working name; may fold into pmndrs/examples someday — not the current intent. Context: promoting the react-three-fiber v10 release. Research reports live in
research/.
The official three.js examples rebuilt in clean, idiomatic React Three Fiber v10 — WebGPU-first — designed to be read equally well by humans and coding agents, showcasing how dramatically R3F simplifies genuinely annoying three.js tasks, and demonstrating that R3F is an AND with vanilla three.js, not an OR.
- Prove and promote R3F v10 + drei v11 on the WebGPU renderer.
- The "wow" factor is a primary goal: where three.js needs 200 lines of loader/setup
code, show
<Canvas><CameraControls /><Gltf src="/model.glb" /></Canvas>. Minimal code on each example's index file is a design objective, not a side effect. - Actively showcase v10's DX surface: the phase-based scheduler/useFrame,
useNodes/TSL hooks,fromRef(),background/environmentas Canvas options, declarative postprocessing — consult the v10 branch docs (.mdx) as we build; they are the primary source (the docs site is not yet updated). - Establish and document the conventions of modern R3F — general "how R3F works," not "how to build one-page demos." The doc co-evolves with the examples (§7) and is exportable as docs + lint rules.
- Serve as v10/drei-v11's de facto integration test suite; surface drei WebGPU gaps by building our own components where drei doesn't cover something yet (§5 utils).
- Better discovery than threejs.org/examples: searchable/filterable sidebar, tags, per-example links (source, original example, open-in-agent).
- Not a re-teaching of three.js fundamentals.
- No ports of API/stress/capability tests, renderer internals, or the TSL editor / transpiler / graph pages — those are tooling UIs (Monaco, the Inspector), not scenes. Marked internal, not porting (Dennis, 2026-09-03; v1.1 had them as "later-phase").
- WebXR: explicitly out of scope for now — a final-phase item if things go fast. (webaudio was
in this clause until 2026-09-03; it is ported —
src/examples/audio/.) - Not pixel-parity with upstream. Idiomatic-primary; divergence expected (§8).
- Not a fork of the three.js examples — originals are linked, not vendored.
Inventory (dev branch, 2026-07-26): 595 total examples; 221 webgpu; 296 webgl (219 with no
name-matched webgpu counterpart — semantic dedup in progress, expected to shrink sharply);
77 exist on both renderers. WebGPU set grows ~5/month; re-diff files.json monthly.
-
Phase 1: 100% of the
webgpu_*set minus stress/internal tests (~190–200 ports). Start with the 77 dual-renderer examples (best-understood ground truth), then the webgpu-only set (compute, TSL, MRT — the high-value thin-training-data territory).(Amended 2026-09-02 — "stress/internal tests" enumerated.) These 10 r185
webgpu_*pages are excluded from Phase 1: they exercise renderer internals or measure performance rather than teaching a visual technique, so a React port of them would demonstrate nothing about R3F.excluded why sandboxscratch page, no stable subject test_memoryallocates until it breaks; a leak harness performance,performance_renderbundlethroughput benchmarks pmrem_test,furnace_testrenderer-correctness checks (white-furnace, PMREM) compile_asyncshader-compilation timing, not a scene multisampled_renderbuffers,reversed_depth_bufferrenderer configuration probes centroid_samplingMSAA sampling-mode probe Two are genuinely arguable —
furnace_testis a real PBR-correctness technique andcentroid_samplingteaches an MSAA subtlety a reader could hit. Revisit if the Patterns track wants a "renderer correctness" group. Phase 1 target is therefore 75 remaining ports, not 85. -
Patterns track (not ports): ~12–20 app-scale examples — decided 2026-09-03: out of 1.0 and likely a separate repo; this project duplicates the existing three.js examples. Kept here as the pointer. Teaching what single-canvas demos can't: canvas in a real layout, persistent canvas across routes, shared DOM↔scene state, suspense/loading orchestration, multiple views, DOM↔scene events, testing R3F components. The gallery site itself is built with these patterns (dogfooding).
-
Phase 2: webgl-only examples — validated list only (research/webgl-unique-list.md). Semantic dedup confirmed the 219 "gap" was mostly illusory: 83 are technique-covered by differently-named webgpu examples, 55 are low-value (deprecated/stress/thin toggles). Genuinely unique: 34 non-loader examples (decals, CSG, marchingcubes, text geometry, multiple-views, BVH raycasting, path tracer, offscreencanvas worker, modifiers, etc.) + 47 loader-format gallery examples (only glTF/KTX2/MaterialX have webgpu ports). Phase 2 = curate from the 34; the loader gallery is its own decision (❓ port all formats, or a representative set?). Ported to WebGPURenderer where feasible.
-
Final phase (aspirational): WebXR (@react-three/xr). webaudio shipped 2026-09-03 (5 examples, new
audiocategory,startClickmanifest field). The TSL editor/ transpiler/graph pages are excluded outright (§3), not deferred.
- Language: TypeScript. No JS examples.
- Examples live in category folders:
src/examples/<category>/<slug>.tsx, or<category>/<slug>/<slug>.tsxwhen subcomponents are needed (entry filename matches the folder). The category never appears in the URL — the route is always/examples/<slug>. ~200 lines on the index file triggers the folder pattern, and that split is itself a taught, standardized pattern: split by scene role, not by arbitrary size. - The example owns its
<Canvas>. The scene lives self-contained inside<Canvas>— no forced<Scene>extraction (real-world r3f almost never does that). The file is the unit of reuse; extraction into a user's project is handled by tooling (§6), not by file structure contortions. - Header comment block at the top of every entry file, written for an intermediate R3F reader first: one or two plain sentences on what you are looking at, the original link, then DEMONSTRATES. DIVERGENCE is optional — a faithful port says nothing, and requiring the section only manufactures boilerplate. Schema in AGENTS.md § House style.
- No module-scope mutable state. (One-time idempotent registration at module scope is
fine —
extend(),RectAreaLightNode.setLTC.) - Controls: leva
useControls, placed NEXT TO what they control — never at the page root and drilled down as props. leva merges multipleuseControlscalls into one panel, and the v10 TSL hooks are designed to consume its output directly (useControls->useUniforms->useNodes). The only hard constraint is that fiber hooks must be inside<Canvas>, so the consuming component is a Canvas child. (Amended: the original "controls at the edge" wording is what produced corpus-wide prop drilling.) - Inspector: deferred. v10 has a root-state slot but this repo has never wired it;
every port drops
renderer.inspectorand leva covers the control surface. Revisit when the drei wrapper lands — until then do not write it into examples. - Shared
utils/folder: our own reusable components built for the demos — both drei-gap fillers (each one a documented candidate/brief for a future drei component) and demo furniture (stages, grids, loaders' UX). - Drei imports follow v11 renderer-split subpaths (
/webgpu,/core); never/legacyin webgpu examples. Known v11 gaps (MeshReflectorMaterial, SpotlightMaterial, AccumulativeShadows — drei #2533/#2658): build our own in utils/ and flag upstream. - Post-processing: R3F v10's native TSL pipeline (
useRenderPipelinewrapping THREE.PostProcessing) — NOT @react-three/postprocessing (stalled, WebGL-only). - Per-example metadata (sibling meta or frontmatter): title, slug, original URL, tags, APIs used, difficulty, divergence/enhancement notes. Feeds the site index and agents.
- Consistent, predictable structure so agents pattern-match across the corpus; the corpus itself must model good habits (agents learn from example gravity more than prose).
- Mechanism strategy (validated — see research/agent-interface-options.md):
- Backbone:
AGENTS.mdat repo root (the proven standard: 60k+ repos, Linux Foundation-stewarded, native in Codex/Cursor/Copilot/20+ tools) + thinCLAUDE.mdwhose first line imports it (@AGENTS.md— Claude Code doesn't read AGENTS.md natively). - Machine-readable
examples.jsonmanifest (slug, tags, APIs, paths) — cheapest highest-leverage move; feeds the site, agents, and the extraction tool from one file. - llms.txt: generate it (cheap) but treat as decorative, not discovery — evidence is damning: ~97% of llms.txt files get zero bot requests; Google explicitly compares it to the dead keywords meta tag; AI crawlers fetch HTML directly.
- In-repo Claude Code Skill for "lift an example into your project" (Claude-only but real; complements the cross-tool extraction CLI).
- MCP server: deferred. Real precedents are shared hubs (docs.pmnd.rs, Context7), not per-repo servers; users don't install long-tail project servers, and GitHub Pages cannot host one (static-only). Revisit on Cloudflare Workers if the corpus outgrows the static manifest; meanwhile pursue inclusion in existing hubs (docs.pmnd.rs MCP, Context7 indexing) rather than running our own.
- Consumers served: (a) site-browsing agents → HTML + manifest; (b) cloned-repo agents → AGENTS.md + structure; (c) user's-own-project agents → extraction CLI + skill.
- Backbone:
- Extraction story: not necessarily a CLI — a launch-task page. Every served example page carries a top-level agent-readable pointer in its HTML (first-line comment/meta + visible link) to a "for agents" page with extraction instructions: where the file lives, the starter-shell template, how to wire deps. Agents reading the page HTML get routed there; humans get the same page linked in the UI. Starter shell template lives in-repo.
- Open-in-agent buttons (researched — see research/agent-open-buttons.md): v1 row =
GitHub source link + Claude Code (
claude.ai/code?prompt=...&repositories=owner/repo— officially documented; only mechanism carrying both prompt AND repo; subfolder goes in the prompt text) + StackBlitz (/github/owner/repo/tree/branch/<subfolder>— best subfolder support, no auth for public repos) + Codespaces badge + Cursor best-effort (cursor://deeplink is prompt-text-only, and Cursor now interposes a confirmation dialog after 2026 deeplink-abuse disclosures). Skip: Codex (no URL-launch scheme exists; tasks bind to pre-provisioned environments) and CodeSandbox (repo imports shut down July 2026). All links are client-side URL templates from {owner, repo, branch, examplePath} — no library exists, hand-assemble the row.
- Three sections, explicitly labeled so agents know which transfers:
- House style — what makes a port good, and the FIRST thing an agent reads. The mandate: a port that is longer, more indirect, or more imperative than the vanilla original has failed even if it renders perfectly.
- R3F v10 idioms — general rules valid in any app (the "how R3F works" layer).
- Repo format — this repo's shape (categories, header schema, thresholds, manifest). The doc states that examples are micro-scoped by design and points to the patterns track.
- Prune on every dependency bump. The doc went stale against fiber alpha.4 and kept
mandating workarounds for four bugs that had been fixed (B9/B12/B16/B17), which is how
agents ended up writing
useMemowhere v10 hooks belong and prop-drilling nodes that a scoped store could have carried. A rule that outlives its bug is worse than no rule. - Co-evolution loop: seed from known v10/v11 idioms → port a batch → every review divergence becomes an example fix OR a doc amendment, never silent → periodically re-conform older examples. Doc carries a changelog for later agent batches.
- Mechanize everything mechanizable: checkable conventions become eslint rules (custom plugin); lint feedback beats prose for steering agents. Prose reserved for judgment calls.
- Micro-examples cross-link to patterns rather than absorbing app architecture.
- Idiomatic-primary. Divergence from the originals is expected and fine.
- "Poimandres baseline" = a visible, generic
<DemoHelpers>component (working name): infinite grid, CameraControls, readiness signal — a real toggleable component users see and can turn off, not hidden furniture. The baseline was SET BY BUILDING THE FIRST EXAMPLE TOGETHER (Dennis + Fable) — that example is the golden path everything else conforms to. - Tone mapping is a per-port decision, not a default. fiber's Canvas defaults to
ACESFilmic while the three.js originals render with the WebGPURenderer default
(NoToneMapping) unless they set one; taking the default visibly mutes emissive and unlit
palettes. Decide
renderer={{ toneMapping }}deliberately on every port and compare against the LIVE original, not the (stale) gallery thumbnail. (Amended: "not picky" was wrong — it silently changed the look of emissive examples.) - Enhancements: restrained, and NOT extra controls. A richer drei option or a better GLB is welcome. Adding UI the original never had is not: we are comparing this demo to that demo, so a control that doubles or triples the code is a net loss even when it is fun. If a control forces state lifting, registries or instance plumbing, drop it. (Amended: "added controls" as a blanket encouragement is a direct cause of the bloat found in the first style review.)
- Quality/perf drift vs vanilla would be surprising (thin wrapper over core) — not a primary test axis.
- Stack: Vite + react-router SPA. Tailwind + shadcn-style UI. No Next.js.
- Single app; examples are glob-routed files within it — NOT per-demo standalone packages.
- Gallery + sidebar with search and tag filters; thumbnail per example. Per-example page: live demo, code view, links (GitHub, original three.js example, open-in-agent buttons).
- Hosting: GitHub Pages (static export), custom domain later. Cloudflare only if a real need emerges (e.g. MCP endpoint compute, advanced CI).
- Definition of done per example: typechecks, passes conventions lint, builds, initializes and renders on WebGPU, metadata complete, header block present.
- Test harness verifies "it works," not upstream parity. No pixel-diff against three.js.
- First-port review: screenshot contact sheets batched for human/Fable visual review (the oracle at creation time is a person, not upstream pixels).
- Regression thereafter: golden screenshots of OUR OWN output via Playwright.
- CI design (researched — research/webgpu-ci-github.md): free
ubuntu-latestrunners ARE viable. Key fact: WebGPU initializes fine on SwiftShader (software Vulkan), but headless Chrome on Linux never presents the WebGPU canvas (confirmed Chromium bug → black screenshots — the likely cause of past bad headless experiences). Fix: headed Chromium under Xvfb. Three.js's own screenshot CI proves the pattern at 150+ examples: puppeteer + mesa/xvfb, 5-way sharding, pixelmatch with loose thresholds, a readiness signal (window._renderFinished-style) instead of sleeps, exception list for non-deterministic demos. We adopt a readiness signal in the shell from day one.- Tier 1 (every PR): full smoke suite — readiness signal fires, canvas context is
really
webgpu, canvas non-black, console clean. - Tier 1.5 — "animates" (SHIPPED,
tests/animates.spec.ts): two-frame pixel diff plus dual-root-warning capture. Added because smoke's non-black check cannot see a FROZEN scene — a corpus sweep found 17 examples rendering a static first frame with a clean console. Examples that are static by design declare"static": truein the manifest (the test then asserts a live loop instead); long stop-go easings declare"animationWindowMs". Local-only for now: SwiftShader's frame rate would need the window retuned before this can gate CI. - Tier 2 (every PR): screenshot regression on changed examples only; goldens generated on the same SwiftShader path (never mix GPU/software goldens).
- Tier 3 (nightly): full-corpus screenshot run (catches shared-utils regressions).
- Tier 4 (manual dispatch): real-GPU runner (GitHub gpu-t4 or cheaper third-party) to disambiguate SwiftShader flakiness when nightly goes red.
- Cadence (amended 2026-07-28, porting phase): smoke does NOT run on every push —
at corpus scale that is ~30 min of software raster. It runs on PRs, nightly, and on
demand (
gh workflow run ci.yml); the fast lint/build job still gates every push. Local Metal is the oracle — land nothing that is not green there first. Revisit when the corpus is complete and pushes drop to a few a month. - Cloudflare Browser Rendering investigated and rejected: headless Chrome underneath, same black-canvas limitation.
- Follow-up: clone pmndrs/examples' packages/e2e and read its Canvas-monkeypatch Vite plugin directly (same-stack precedent).
- Tier 1 (every PR): full smoke suite — readiness signal fires, canvas context is
really
- Pin a three.js version per site release; monthly
files.jsonre-diff + agent-driven upgrade-and-port-new-examples pass. - Track drei v11 gap-closures: when drei ships a component we stubbed in utils/, migrate the affected examples (agent task) and retire the util.
- Fable: spec, conventions doc, orchestration, batch review, contact-sheet review.
- Opus 5 / Sonnet: porting, research, verification passes.
- Pipeline per example: port → self-verify (typecheck/lint/build/render) → screenshot → batch review → merge. Blocked/divergent is a first-class pipeline state (drei gaps etc.).
- three.js: shallow/sparse checkout —
examples/+src/at the pinned version. No history. - react-three-fiber
v10branch — code AND the .mdx docs (primary doc source until the docs site updates). Pin tocanarynpm tag or build from branch. - drei
v11branch/alpha; leva; the drei Inspector-hooks PR and three.js Inspector source. - Local agent reference corpus assembled from the above + our conventions doc.
- Loader gallery scope (all 47 formats vs representative subset) — decide at Phase 2.
- Asset hosting: hotlink threejs.org for now; must self-host when examples swap in alternate/better models.
<DemoHelpers>exact API — SETTLED in M1 (grid, CameraControls with dolly/polar/zoom clamps,controlsRefescape hatch, readiness signal).- Restyle scale: the first style review (2026-09-01) left 115 of 131 examples needing a pass. Whether to restyle all of them or a flagship subset is decided at the pilot gate.
- v1.1 (2026-09-01) — first style review of the ported corpus (131 examples, 16
hand-tuned by Dennis). Three spec clauses turned out to be causes of the drift, not
just silent about it:
- §5 "controls at the edge" -> controls next to what they control. The old wording
produced corpus-wide prop drilling of leva values into
useUniforms. - §8 "Enhancements encouraged … added controls" -> restrained, and not extra controls. Invented UI is the main reason several ports are 2-3x the size they need.
- §8 "Tonemapping: R3F's ACES default, not picky" -> a deliberate per-port decision. The default silently mutes emissive palettes against the originals. Also: §5 folder categories + ~200-line threshold + DIVERGENCE made optional; §5 Inspector marked deferred (never wired, every port drops it); §7 restructured to three labelled sections with a prune-on-bump rule; §10 records the shipped animates tier and the real CI cadence. Operational detail lives in AGENTS.md v1.0.
- §5 "controls at the edge" -> controls next to what they control. The old wording
produced corpus-wide prop drilling of leva values into
- v1.2 (2026-09-03) — §3/§4: the three TSL tooling pages (
webgpu_tsl_editor,webgpu_tsl_transpiler,webgpu_tsl_graph) moved from "later-phase project" to excluded as internal tooling — their UI (a Monaco editor, the Inspector's node graph) is the demo, so a port would be an editor product, not an example. The transpiler runtime is already exercised byshadertoy. Same day, webaudio was greenlit and ported (§3/§4): final phase is now WebXR only.