Debug Controls
@three-flatland/tweakpane wraps Tweakpane v4 with a Flatland-themed pane, a compact renderer stats row, a cycling FPS/MS/GPU/MEM graph, and React hooks that survive Strict Mode. It’s the UI layer every three-flatland example uses — available as a standalone package so you can drop the same controls into your own project.
Installation
npm install @three-flatland/tweakpane tweakpane @tweakpane/plugin-essentialspnpm add @three-flatland/tweakpane tweakpane @tweakpane/plugin-essentialsyarn add @three-flatland/tweakpane tweakpane @tweakpane/plugin-essentialsbun add @three-flatland/tweakpane tweakpane @tweakpane/plugin-essentialsPeer dependencies: three >= 0.183, tweakpane ^4.0.5, @tweakpane/plugin-essentials ^0.2.1. For the React entry point: @react-three/fiber >= 10.0.0-alpha.2 and react >= 19.
Import from two entry points:
@three-flatland/tweakpane— vanilla Three.js:createPane,wireSceneStats,addStatsGraph,applyTheme,FLATLAND_THEME@three-flatland/tweakpane/react— React hooks:usePane,usePaneFolder,usePaneInput,usePaneButton,useStatsMonitor,useFpsGraph
What You Get
Calling createPane() (or usePane()) gives you a pane with:
- Collapsible header with a pin toggle. The pane dims to translucent when the mouse leaves; pin it to keep it opaque.
- Cycling stats graph at the top. Click to cycle through
fps → ms → gpu → mem(GPU mode appears only when the renderer supports timestamp queries). - Compact stats row — five-column readout for
draws · triangles · primitives · geometries · textures. Auto-formats large numbers (1.2K,10M,999K). - Flatland theme matching the docs aesthetic.
All four are driven by a single StatsHandle that you can wire into your render loop in two lines.
Three.js Usage
For plain Three.js, pass your Scene to createPane — it hooks scene.onAfterRender so draws, triangles, and memory readouts update automatically from renderer.info:
import { WebGPURenderer } from 'three/webgpu'import { Scene, OrthographicCamera } from 'three'import { createPane } from '@three-flatland/tweakpane'
const renderer = new WebGPURenderer({ trackTimestamp: true })await renderer.init()
const scene = new Scene()const camera = new OrthographicCamera(/* ... */)
// Pass `scene` to auto-wire draw/triangle/memory statsconst { pane, stats } = createPane({ scene })
const params = { speed: 1.0, color: '#99d9ef' }pane.addBinding(params, 'speed', { min: 0, max: 5 })pane.addBinding(params, 'color')
function animate() { requestAnimationFrame(animate) stats.begin() // starts FPS/MS measurement renderer.render(scene, camera) stats.end() // ends FPS/MS measurement}animate()stats.begin() / stats.end() measure CPU frame time for the FPS/MS graph. Everything else — draw calls, triangles, GPU time, geometry/texture counts — updates from renderer.info via the scene.onAfterRender hook.
GPU timing
The graph’s gpu mode reads Three.js’s GPU timestamp queries and shows real GPU frame time. It’s silently skipped unless you opt in:
const renderer = new WebGPURenderer({ trackTimestamp: true })On WebGPU, the adapter must support GPUFeatureName.TimestampQuery (most modern desktop browsers do). On WebGL2, the EXT_disjoint_timer_query_webgl2 extension must be available. Values trail by 1–2 frames because the readback is async.
Manual stats (no scene)
If you can’t pass scene — e.g. you’re rendering multiple scenes or using a custom pipeline — call stats.update() yourself each frame with values from renderer.info.render:
const { pane, stats } = createPane()
function animate() { stats.begin() renderer.render(scene, camera) stats.update({ drawCalls: renderer.info.render.calls, triangles: renderer.info.render.triangles, geometries: renderer.info.memory.geometries, textures: renderer.info.memory.textures, }) stats.end()}wireSceneStats
wireSceneStats(scene, stats) is the low-level helper createPane({ scene }) uses internally. Call it directly when you want to wire stats into a scene separately from pane creation:
import { createPane, wireSceneStats } from '@three-flatland/tweakpane'
const { pane, stats } = createPane() // pane without auto-wiringconst unwire = wireSceneStats(scene, stats)
// later:unwire() // restores the previous onAfterRender hookpane.dispose()React Usage
The React entry point (@three-flatland/tweakpane/react) mirrors the vanilla API as hooks that work inside an R3F <Canvas>. Create the pane with usePane(), then wire stats to the R3F frame loop with useStatsMonitor(stats):
import { Canvas, useFrame } from '@react-three/fiber/webgpu'import { usePane, usePaneFolder, usePaneInput, usePaneButton, useStatsMonitor,} from '@three-flatland/tweakpane/react'
function Scene() { const { pane, stats } = usePane()
// Controls — each hook returns [value, setValue] const folder = usePaneFolder(pane, 'Sprite') const [speed] = usePaneInput(folder, 'speed', 1.0, { min: 0, max: 5 }) const [color] = usePaneInput(folder, 'color', '#99d9ef')
usePaneButton(folder, 'Reset', () => console.log('reset'))
// Wire draws/tris/GPU time + FPS graph into the R3F frame loop useStatsMonitor(stats)
useFrame((_, delta) => { // ... use `speed` and `color` — each value triggers a React re-render })
return <>{/* sprites, meshes, etc. */}</>}
export default function App() { return ( <Canvas renderer={{ trackTimestamp: true }}> <Scene /> </Canvas> )}React hook reference
| Hook | Purpose |
|---|---|
usePane(options?) | Create the pane. Returns { pane, stats }. |
usePaneFolder(parent, title, options?) | Create a collapsible folder. Returns FolderApi. |
usePaneInput(parent, key, initial, options?) | Bind an input. Returns [value, setValue] — each change triggers a React re-render. |
usePaneButton(parent, title, onClick) | Add a button. Uses a callback ref so onClick is never stale. |
useFpsGraph(parent) | Standalone cycling stats graph (if you aren’t using usePane). |
useStatsMonitor(stats) | Wire a StatsHandle into the R3F frame loop. Required in every example. |
All hooks create bindings synchronously during render (so controls appear without pop-in) and defer disposal across React Strict Mode’s cleanup/re-mount cycle.
useStatsMonitor under the hood
useStatsMonitor(stats) does two things:
- Calls
wireSceneStats(scene, stats)inside auseEffect— usinguseThree((s) => s.scene)to grab the active scene. This capturesrenderer.info.renderandrenderer.info.memoryviascene.onAfterRender, and queues GPU timestamp resolution as a microtask each frame. - Registers two
useFramecallbacks withpriority: Infinityandpriority: -Infinitysostats.begin()runs first andstats.end()runs last in each frame — measuring the full update phase for the FPS/MS graph.
Examples that take over rendering
Some examples run useFrame(..., { phase: 'render' }) so they own the render call (e.g. post-processing pipelines driving their own RenderPipeline). When a user job occupies the render phase, R3F skips its auto-render — which means scene.onAfterRender never fires and draws/triangles aren’t captured.
The workaround: push the values into stats.update() yourself immediately after your render call, while renderer.info.render is still valid:
useFrame(({ gl }) => { pipeline.current.render() stats.update({ drawCalls: gl.info.render.calls, triangles: gl.info.render.triangles, })}, { phase: 'render' })See examples/react/pass-effects/App.tsx for a complete version.
Theme
The pane uses FLATLAND_THEME — monospace font, muted palette, and CSS variables that match the docs site. To apply the theme to an existing Tweakpane instance:
import { Pane } from 'tweakpane'import { applyTheme, FLATLAND_THEME } from '@three-flatland/tweakpane'
const pane = new Pane({ title: 'Controls' })applyTheme(pane.element, FLATLAND_THEME)applyTheme sets CSS custom properties on the pane element — you can fork FLATLAND_THEME (it’s a plain object) to tweak colors without touching the package.
Options
createPane({ title: 'Controls', // Pane header title (default: 'Controls') expanded: true, // Initial expansion state (default: true) stats: true, // Show stats graph + row (default: true) scene, // Three.js Scene for auto-wiring (optional) debug: true, // Log one-time backend diagnostics on first frame container, // Custom container element (optional)})debug defaults to true because three-flatland is dev-focused. It logs two console.info calls total per pane (backend name, trackTimestamp state, first resolved GPU time) — pass debug: false if you’re embedding a pane in a public-facing app.
Next Steps
- Flatland guide — The main Flatland pipeline the examples render with
- Basic Sprite example — See
createPane/usePanein context - Pass Effects example —
stats.update()inside a custom render phase