Skip to content

Self-contained 2D rendering pipeline with camera, post-processing, and global uniforms.

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.

FlatlandSpriteGroup
Use caseSelf-contained 2D pipelineEmbed sprites into your own scene
CameraInternal pixel-perfect orthographic cameraYou provide the camera
Post-processingBuilt-in pass effect pipelineNot included
Global uniformsAutomatic time, viewport, tintNot included
Render callflatland.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.

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()
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.

flatland.add() routes objects automatically:

  • Sprite2D instances go to the internal SpriteGroup for batched rendering
  • Light2D instances are tracked for the lighting system
  • Other Object3D instances are added directly to the internal scene
// Sprite2D → batched via SpriteGroup
const sprite = new Sprite2D({ texture, anchor: [0.5, 0.5] })
flatland.add(sprite)
// Light2D → tracked by lighting system
const light = new Light2D({ type: 'point', position: [50, 50], color: 0xff6600 })
flatland.add(light)
// Other Three.js objects → added to internal scene
const mesh = new Mesh(geometry, material)
flatland.add(mesh)

Global uniforms are automatically wired to each sprite’s material on add().

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 resources
flatland.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 / PropertyDescription
setLighting(effect)Set the active LightEffect (or null to disable)
lightingCurrent LightEffect (read-only)
lightsAll tracked Light2D instances (read-only)
effect.resolutionScaleProcessing 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.

The Flatland Pipeline — one frameleft → right: each stage hands the frame to the nextECS systemstransformSyncSystemtransforms → batchanimationSystemframe cursorsbatchSystemgroup by materiallightSystemLight2D → LightStorePre-passesLightEffect onlyOcclusionPasscaster silhouettesSDFGeneratorJFA → SDFForwardPlusLightingtile-bin lightsMain sceneSprite2DMaterialdraw call / batchMaterialEffect chainper-instance:tint · outline · dissolveNormalMapProviderLightEffectDefault · SimpleDirect · Radiancereads tiles + SDFPost-processPassEffect chain — insertion orderposterizelcdGridvignette… more effects
Per-frame render pipeline

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.

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.

UniformDescription
timeElapsed seconds (undefined = auto)
globalTintColor tint for all sprites
viewportSizeLogical viewport size in pixels
pixelRatioDevice pixel ratio
windWind direction and strength
fogColorFog color
fogRangeFog near/far range

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 frame
flatland.globals.time = undefined
// Manual mode — set exact value
flatland.globals.time = performance.now() / 1000

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.

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.

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 updates
post.bands = 10

The render pipeline auto-initializes on the first addPass() call. Passes chain in insertion order — each receives the previous pass’s output.

MethodDescription
addPass(pass, order?)Add a pass effect
removePass(pass)Remove a specific pass
clearPasses()Remove all passes
passesRead-only array of current passes

See the Pass Effects guide for creating custom effects and the full node library.

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 mesh
mesh.material.map = flatland.texture
// Each frame: render 2D first, then 3D
flatland.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 })

Clean up all resources when done:

flatland.dispose()

This destroys the ECS world, sprite batches, render pipeline, and all pass effect entities.