Flatland is the high-level entry point for three-flatland. It wraps a SpriteGroup with an orthographic camera, global uniforms, post-processing pipeline, and render target support into a single object.
If you’re new and not sure whether you need Flatland or SpriteGroup — start here. The table below tells you when to graduate.
When to Use Flatland vs SpriteGroup
Section titled “When to Use Flatland vs SpriteGroup”| Flatland | SpriteGroup | |
|---|---|---|
| Use case | Self-contained 2D pipeline | Embed sprites into your own scene |
| Camera | Internal pixel-perfect orthographic camera | You provide the camera |
| Post-processing | Built-in pass effect pipeline | Not included |
| Global uniforms | Automatic time, viewport, tint | Not included |
| Render call | flatland.render(...) | renderer.render(...) |
Use Flatland when you want a complete 2D rendering setup. Use SpriteGroup when you need to mix sprites into an existing 3D scene with your own camera and render loop.
Basic Setup
Section titled “Basic Setup”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 }))
function animate() { flatland.render(renderer) requestAnimationFrame(animate)}animate()import { useRef } from 'react'import { Canvas, extend, useFrame, useThree } from '@react-three/fiber/webgpu'import { Flatland, Sprite2D, Sprite2DMaterial } from 'three-flatland/react'import type { Flatland as FlatlandType } from 'three-flatland/react'import type { WebGPURenderer } from 'three/webgpu'
extend({ Flatland, Sprite2D, Sprite2DMaterial })
function Scene() { const flatlandRef = useRef<FlatlandType>(null) 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 }}> <Scene /> </Canvas> )}Constructor Options
Section titled “Constructor Options”const flatland = new Flatland({ viewSize: 400, // Orthographic view height in world units pixelPerfect: true, // Managed PixelPerfectCamera (system default) clearColor: 0x1a1a2e, // Background color clearAlpha: 1, // Background alpha (< 1 for transparent) autoClear: true, // Clear before each render postProcessing: false, // Enable post-processing pipeline aspect: undefined, // Regular camera only: auto-sync, or set a number to pin it camera: null, // Custom OrthographicCamera (null = internal) renderTarget: null, // RenderTarget (null = render to viewport)})See the FlatlandOptions API reference for full type details.
Flatland’s managed camera follows the library-wide pixel-art preset by default. See Pixel-Perfect Rendering for the camera, object snapping, HiDPI behavior, and the FlatlandConfig hierarchy. Pass pixelPerfect: false for a regular orthographic camera.
Adding Objects
Section titled “Adding Objects”flatland.add() routes objects automatically:
- Sprite2D instances go to the internal
SpriteGroupfor batched rendering - Light2D instances are tracked for the lighting system
- Other Object3D instances are added directly to the internal scene
// 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 mesh = new Mesh(geometry, material)flatland.add(mesh)Global uniforms are automatically wired to each sprite’s material on add().
Lighting
Section titled “Lighting”Flatland manages 2D lighting with Light2D sources and a LightEffect pipeline. Activate lighting by calling setLighting() with a preset:
import { DefaultLightEffect } from '@three-flatland/presets'
const lighting = new DefaultLightEffect()lighting.resolutionScale = 0.5 // Optional: half-resolution effect resourcesflatland.setLighting(lighting)All sprites receive lighting by default. Set lit: false to opt out:
const indicator = new Sprite2D({ texture, lit: false }) // always full brightness| Method / Property | Description |
|---|---|
setLighting(effect) | Set the active LightEffect (or null to disable) |
lighting | Current LightEffect (read-only) |
lights | All tracked Light2D instances (read-only) |
effect.resolutionScale | Processing resolution relative to the physical surface (default: 1) |
resolutionScale belongs to the effect that owns the surface-dependent resources. A value of 0.5 receives half-width and half-height dimensions in LightEffect.resize(), reducing area to one quarter while leaving camera framing, canvas resolution, renderer DPR, and logical viewport uniforms unchanged. Change Three.js renderer.setPixelRatio() or R3F <Canvas dpr={...}> instead when the entire frame should render at a lower resolution.
Flatland owns an attached effect’s lifecycle. Replacing the active effect or calling setLighting(null) disposes its GPU resources before detaching it. The effect instance can be attached again and will receive a fresh init → resize → update sequence, but effect-owned GPU resource handles must not be used while it is detached.
See the 2D Lighting guide for preset comparisons, Light2D types, and custom effects.
Render Loop
Section titled “Render Loop”Each frame, call render(). Flatland reads the renderer drawing buffer or active render target size and updates its camera when the surface dimensions change:
function animate() { flatland.render(renderer) requestAnimationFrame(animate)}render() syncs the camera, global uniforms, lighting buffers, and sprite batches before drawing. Surface-dependent GPU resources use physical drawing-buffer pixels so canvas DPR and render-target texels stay in the same coordinate space; globals.viewportSize remains the renderer’s logical size and is paired with globals.pixelRatio. The default PixelPerfectCamera selects and centers an integer-sized viewport inside the physical surface, so resolvedAspect can differ from the full surface when letterboxing is active. With pixelPerfect: false, pass aspect or assign flatland.aspect to pin the regular internal camera.
Calling resize(width, height) starts a manual sizing session, and the active destination at that moment fixes the unit for the session. With no render target attached, pass logical CSS pixels; the renderer’s current DPR is applied on every render. A target attached later is therefore sized to width × DPR by height × DPR. With a render target attached, pass physical texels, matching RenderTarget.setSize(); those dimensions remain physical across later target swaps or a return to the canvas. Automatic sizing only observes user-owned render targets and never resizes them. Assigning either a numeric aspect or 'auto' after resize() ends the manual sizing session and resumes automatic effect sizing; 'auto' also resumes automatic camera framing. Read flatland.resolvedAspect when you need the current numeric ratio.
When you supply a camera through camera, Flatland preserves its authored frustum. The aspect controls above apply to the internal camera; resolvedAspect reports the supplied camera’s actual frustum ratio.
Global Uniforms
Section titled “Global Uniforms”Every Flatland instance exposes a globals object with shared uniforms. These update once per frame and are available to all sprite materials via TSL nodes.
| Uniform | Description |
|---|---|
time | Elapsed seconds (undefined = auto) |
globalTint | Color tint for all sprites |
viewportSize | Logical viewport size in pixels |
pixelRatio | Device pixel ratio |
wind | Wind direction and strength |
fogColor | Fog color |
fogRange | Fog near/far range |
Auto vs Manual Time
Section titled “Auto vs Manual Time”By default, time is undefined and Flatland accumulates elapsed time automatically. Set it to a number for manual control:
// Auto mode (default) — time accumulates each frameflatland.globals.time = undefined
// Manual mode — set exact valueflatland.globals.time = performance.now() / 1000Using Uniforms in TSL
Section titled “Using Uniforms in TSL”Access the TSL node directly for custom material effects:
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) },})Each global has a corresponding TSL node: timeNode, globalTintNode, viewportSizeNode, pixelRatioNode, windNode, fogColorNode, fogRangeNode.
Statistics
Section titled “Statistics”Monitor batching with flatland.spriteGroup.stats:
const stats = flatland.spriteGroup.stats
console.log(`Sprites: ${stats.spriteCount}`)console.log(`Batches: ${stats.batchCount}`)console.log(`Visible: ${stats.visibleSprites}`)Draw calls aren’t part of stats — read them from the renderer after a frame: renderer.info.render.calls reflects actual GPU work.
Post-Processing
Section titled “Post-Processing”Add full-screen pass effects with addPass():
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)
// Zero-cost parameter updatespost.bands = 10The render pipeline auto-initializes on the first addPass() call. Passes chain in insertion order — each receives the previous pass’s output.
| Method | Description |
|---|---|
addPass(pass, order?) | Add a pass effect |
removePass(pass) | Remove a specific pass |
clearPasses() | Remove all passes |
passes | Read-only array of current passes |
See the Pass Effects guide for creating custom effects and the full node library.
Render to Texture
Section titled “Render to Texture”Render Flatland output to a texture for use in 3D scenes:
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 is
the usual choice for 2D color output. It also keeps the pipeline write and the
target metadata aligned, so sampling flatland.texture in another Flatland or
Three.js scene preserves the source colors without a manual transfer function.
Set the target’s color space before its first GPU use (or pass a fresh target to Flatland). Changing color-space metadata after Three has allocated the target does not recreate its GPU attachment format.
For linear or HDR output, declare that 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 })Disposal
Section titled “Disposal”Clean up all resources when done:
flatland.dispose()This destroys the ECS world, sprite batches, render pipeline, and all pass effect entities.
Next Steps
Section titled “Next Steps”- 2D Lighting — Light2D sources and LightEffect presets
- Pass Effects — Full-screen post-processing effects
- Batch Rendering — How sprite batching works under the hood
- Pixel-Perfect Rendering — Camera scaling, projected-pivot snapping, and presets
- TSL Nodes — Per-sprite material effects and low-level TSL usage