Create a Flatland, add objects to it, render each frame, and dispose it.
The setup assumes an installed three-flatland project and a texture available at
public/sprites/hero.png.
Flatland explains what the class owns and why. The
Flatland API reference lists every
constructor option, method, and uniform.
Create the instance
Section titled “Create the instance”import { WebGPURenderer } from 'three/webgpu'import { Flatland, Sprite2D, TextureLoader } from 'three-flatland'
const renderer = new WebGPURenderer()await renderer.init()document.body.appendChild(renderer.domElement)
function resizeRenderer() { renderer.setSize(window.innerWidth, window.innerHeight)}resizeRenderer()window.addEventListener('resize', resizeRenderer)
const flatland = new Flatland({ viewSize: 400, clearColor: 0x1a1a2e })const texture = await new TextureLoader().loadAsync('/sprites/hero.png')
flatland.add(new Sprite2D({ texture }))
let animationFrame = 0function animate() { flatland.render(renderer) animationFrame = requestAnimationFrame(animate)}animate()import { Suspense, useRef } from 'react'import { Canvas, extend, useFrame, useLoader, useThree } from '@react-three/fiber/webgpu'import { Flatland, Sprite2D, TextureLoader } from 'three-flatland/react'import type { Flatland as FlatlandType } from 'three-flatland/react'import type { WebGPURenderer } from 'three/webgpu'
extend({ Flatland, Sprite2D, TextureLoader })
function Scene() { const flatlandRef = useRef<FlatlandType>(null) const texture = useLoader(TextureLoader, '/sprites/hero.png') const renderer = useThree((state) => state.renderer)
// Render in the 'render' phase so R3F skips its own render useFrame(() => { flatlandRef.current?.render(renderer as unknown as WebGPURenderer) }, { phase: 'render' })
return ( <flatland ref={flatlandRef} viewSize={400} clearColor={0x1a1a2e}> <sprite2D texture={texture} /> </flatland> )}
export default function App() { return ( <Canvas renderer={{ antialias: false }}> <Suspense fallback={null}> <Scene /> </Suspense> </Canvas> )}Add objects
Section titled “Add objects”flatland.add() dispatches on type, described in object routing.
import { Light2D } from 'three-flatland'import { Mesh, PlaneGeometry } from 'three'import { MeshBasicNodeMaterial } from 'three/webgpu'
// Sprite2D → batched via SpriteGroupconst sprite = new Sprite2D({ texture, anchor: [0.5, 0.5] })flatland.add(sprite)
// Light2D → tracked by lighting systemconst light = new Light2D({ type: 'point', position: [50, 50], color: 0xff6600 })flatland.add(light)
// Other Three.js objects → added to internal sceneconst geometry = new PlaneGeometry(10, 10)const material = new MeshBasicNodeMaterial({ color: 0x6699ff })const mesh = new Mesh(geometry, material)flatland.add(mesh)Turn on lighting
Section titled “Turn on lighting”Call setLighting() with a preset. All sprites are lit by default; set lit: false on a sprite to opt out.
import { DefaultLightEffect } from '@three-flatland/presets'
const lighting = new DefaultLightEffect()lighting.resolutionScale = 0.5 // Optional: half-resolution effect resourcesflatland.setLighting(lighting)const indicator = new Sprite2D({ texture, lit: false }) // always full brightnessFlatland disposes an attached effect’s GPU resources when you replace or clear it. See effect ownership for the full contract, and the lighting setup guide for presets, Light2D types, and custom effects.
Set the render size
Section titled “Set the render size”For a canvas, resize the renderer when the canvas dimensions change, as in the Three.js setup.
React Three Fiber resizes its Canvas renderer. Flatland synchronizes its managed camera and
lighting resources during the next flatland.render(renderer) call.
To set a manual surface size, call flatland.resize(width, height). With canvas output, pass
logical CSS pixels. With a render target attached, pass physical texture pixels. The destination
attached when you call resize() determines the units for that manual sizing session, even if
you change the destination later.
To resume automatic surface sizing and camera framing, set flatland.aspect = 'auto'. For a
regular managed camera with a fixed ratio, assign a numeric aspect instead; this resumes automatic
effect sizing while retaining that camera ratio. See the
Flatland API reference
for the complete sizing contract.
Read global uniforms in a shader
Section titled “Read global uniforms in a shader”Every global has a matching TSL node: timeNode, globalTintNode, viewportSizeNode,
pixelRatioNode, windNode, fogColorNode, and fogRangeNode.
import { Sprite2DMaterial } from 'three-flatland'
const material = new Sprite2DMaterial({ colorTransform: (ctx) => { // Use the global tint node in a custom effect const tinted = ctx.color.rgb.mul(flatland.globals.globalTintNode) return tinted.toVec4(ctx.color.a) },})Check batching
Section titled “Check batching”const stats = flatland.spriteGroup.stats
console.log(`Sprites: ${stats.spriteCount}`)console.log(`Batches: ${stats.batchCount}`)console.log(`Visible: ${stats.visibleSprites}`)Draw calls are not part of stats. Read renderer.info.render.calls after a frame instead.
Add a post-processing pass
Section titled “Add a post-processing pass”Install @three-flatland/nodes to use posterize in this example.
import { Flatland, createPassEffect } from 'three-flatland'import { posterize } from '@three-flatland/nodes'
const PosterizePass = createPassEffect({ name: 'posterize', schema: { bands: 6 }, pass: ({ uniforms }) => (input, uv) => posterize(input, uniforms.bands),})
const flatland = new Flatland({ viewSize: 400 })const post = new PosterizePass()flatland.addPass(post)
// Update a pass parameterpost.bands = 10By default, passes run in insertion order, and each receives the previous pass’s output. An explicit
order passed to addPass() changes that sequence. The pipeline initializes on the next render
after a pass is added. See
post-processing for the pipeline behavior, and the
pass effects guide for creating custom effects.
Render to a texture
Section titled “Render to a texture”import { RenderTarget } from 'three'
const target = new RenderTarget(512, 512)const flatland = new Flatland({ renderTarget: target })
// Use the texture on a 3D meshmesh.material.map = flatland.texture
// Each frame: render 2D first, then 3Dflatland.render(renderer)renderer.render(scene3D, camera3D)Flatland defaults a render target with no color-space metadata to sRGB, which suits 2D color output. Set the target’s color space before its first GPU use, or pass a fresh target: changing that metadata after Three has allocated the target does not recreate its attachment format.
For linear or HDR output, declare the intent on the target. Flatland preserves explicit color-space and texture-type settings.
import { HalfFloatType, LinearSRGBColorSpace, RenderTarget } from 'three'
const hdrTarget = new RenderTarget(512, 512, { type: HalfFloatType, colorSpace: LinearSRGBColorSpace,})const hdrFlatland = new Flatland({ renderTarget: hdrTarget })Dispose
Section titled “Dispose”cancelAnimationFrame(animationFrame)window.removeEventListener('resize', resizeRenderer)flatland.dispose()renderer.dispose()renderer.domElement.remove()This releases Flatland’s renderer state, sprite batches, render pipeline and pass effects. The cleanup above applies to the Three.js setup. Dispose application-owned textures and other assets when their owners no longer need them; Flatland does not replace their lifecycle management.
Next steps
Section titled “Next steps”- Flatland — what the pipeline owns and when to use it
- 2D lighting — Light2D sources and LightEffect presets
- Pass effects — creating custom effects and the node library
- Pixel-perfect rendering — camera scaling, snapping, and presets
- TSL nodes — per-sprite material effects