Skip to content

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

Terminal window
npm install @three-flatland/tweakpane tweakpane @tweakpane/plugin-essentials

Peer 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:

  1. Collapsible header with a pin toggle. The pane dims to translucent when the mouse leaves; pin it to keep it opaque.
  2. Cycling stats graph at the top. Click to cycle through fps → ms → gpu → mem (GPU mode appears only when the renderer supports timestamp queries).
  3. Compact stats row — five-column readout for draws · triangles · primitives · geometries · textures. Auto-formats large numbers (1.2K, 10M, 999K).
  4. 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 stats
const { 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-wiring
const unwire = wireSceneStats(scene, stats)
// later:
unwire() // restores the previous onAfterRender hook
pane.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

HookPurpose
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:

  1. Calls wireSceneStats(scene, stats) inside a useEffect — using useThree((s) => s.scene) to grab the active scene. This captures renderer.info.render and renderer.info.memory via scene.onAfterRender, and queues GPU timestamp resolution as a microtask each frame.
  2. Registers two useFrame callbacks with priority: Infinity and priority: -Infinity so stats.begin() runs first and stats.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