SpriteGroup automatically batches sprites that share the same material into single draw calls. You don’t need to do anything special—just add sprites and the batching happens behind the scenes.
The defaults are tuned for the common case (thousands of sprites, one or two materials). You almost never need to think about batching — but on the day you do, this page is the map.
How It Works
Section titled “How It Works”When you add sprites to a SpriteGroup, they’re grouped by material and layer into efficient GPU batches. Systems run automatically during Three.js rendering — no manual update() call needed.
import { Sprite2D, SpriteGroup, Sprite2DMaterial } from 'three-flatland';
const spriteGroup = new SpriteGroup();scene.add(spriteGroup);
// These sprites share a material, so they batch togetherconst material = new Sprite2DMaterial({ map: spriteSheet });
for (let i = 0; i < 1000; i++) { const sprite = new Sprite2D({ material }); sprite.position.set(Math.random() * 100, Math.random() * 100, 0); spriteGroup.add(sprite);}
// In your render loop — no update() call neededfunction animate() { renderer.render(scene, camera); requestAnimationFrame(animate);}import { useMemo } from 'react';import { extend } from '@react-three/fiber/webgpu';import { Sprite2D, SpriteGroup, Sprite2DMaterial } from 'three-flatland/react';
extend({ Sprite2D, SpriteGroup, Sprite2DMaterial });
function Particles({ count = 1000, spriteSheet }) { // Create shared material const material = useMemo(() => new Sprite2DMaterial({ map: spriteSheet }), [spriteSheet]);
// Generate random positions once const positions = useMemo( () => Array.from({ length: count }, () => [Math.random() * 100, Math.random() * 100, 0]), [count] );
return ( <spriteGroup> {positions.map((pos, i) => ( <sprite2D key={i} material={material} position={pos} /> ))} </spriteGroup> );}Renderer Options
Section titled “Renderer Options”The SpriteGroup constructor accepts optional batch configuration:
const spriteGroup = new SpriteGroup({ maxBatchSize: 8192, // Max sprites per batch (default: 8192) autoSort: true, // Enable automatic sorting (default: true) frustumCulling: true, // Enable frustum culling (default: true) autoInvalidateTransforms: true, // Auto-sync transforms every frame (default: true)});scene.add(spriteGroup);Automatic Change Detection
Section titled “Automatic Change Detection”Property changes on sprites are detected automatically through the ECS. When you update a sprite’s tint, alpha, flip, frame, layer, or position, the renderer syncs only the changed data to the GPU on the next frame. No manual invalidation needed.
// Just change properties — the renderer handles the restsprite.tint = [1, 0, 0];sprite.alpha = 0.5;sprite.layer = Layers.UI;sprite.position.x += 10;Performance Monitoring
Section titled “Performance Monitoring”Check batch efficiency with the stats property:
const stats = 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:console.log(`Draw calls: ${renderer.info.render.calls}`);Goal: Keep batchCount low relative to spriteCount. If you have 1000 sprites but 100 batches, you’re not batching efficiently.
Optimization Tips
Section titled “Optimization Tips”Share Materials
Section titled “Share Materials”// Good: One material, one batchconst material = new Sprite2DMaterial({ map: atlas });sprites.forEach(s => s.material = material);
// Bad: Many materials, many batchessprites.forEach(s => { s.material = new Sprite2DMaterial({ map: atlas }); // Creates new batch each time!});Use Texture Atlases
Section titled “Use Texture Atlases”Combine textures into a single atlas. All sprites using the atlas can share one material:
// Load a sprite sheet with frame dataconst { texture, frames } = await SpriteSheetLoader.load('/sprites/atlas.json');
const material = new Sprite2DMaterial({ map: texture });
// Different frames, same material = same batchconst player = new Sprite2D({ material, frame: frames.get('player') });const enemy = new Sprite2D({ material, frame: frames.get('enemy') });Organize by Layer
Section titled “Organize by Layer”Sprites are sorted by layer before batching. Keep related sprites on the same layer:
import { Layers } from 'three-flatland';
// Background sprites batch togetherbackgrounds.forEach(s => s.layer = Layers.BACKGROUND);
// Entity sprites batch togetherentities.forEach(s => s.layer = Layers.ENTITIES);Custom Materials
Section titled “Custom Materials”If you’re authoring a custom Sprite2DMaterial subclass — or a stand-alone TSL shader that consumes the same per-instance data — you read from the interleaved instance buffer that every batch shares.
Interleaved instance buffer layout
Section titled “Interleaved instance buffer layout”Each SpriteBatch allocates a single InstancedInterleavedBuffer with stride = 16 floats per instance. Four named attribute views look into that buffer:
| Attribute | Type | Floats | Contents |
|---|---|---|---|
instanceUV | vec4 | 0..3 | Atlas UV rect — x, y, w, h for the sprite’s frame in the atlas. |
instanceColor | vec4 | 4..7 | Per-instance tint and alpha — r, g, b, a. |
instanceSystem | vec4 | 8..11 | [flipX, flipY, systemFlags, enableBits]. |
instanceExtras | vec4 | 12..15 | [shadowRadius, reserved, reserved, reserved]. |
flipX and flipY are +1 (unflipped) or -1 (flipped). systemFlags is an integer bitfield:
| Bit | Mask | Meaning |
|---|---|---|
| 0 | LIT_FLAG_MASK | Sprite receives lighting. |
| 1 | RECEIVE_SHADOWS_MASK | Sprite’s lit fragments may be darkened by the shadow trace. |
| 2 | CAST_SHADOW_MASK | Sprite contributes to the occlusion pre-pass. |
| 3..23 | reserved | — |
enableBits is the active-MaterialEffect bitmask: bit N is set while the Nth registered effect is active on the instance (24 slots, see EFFECT_BIT_OFFSET). System bits and effect-enable bits live in different vec4 components on purpose so adding system flags never costs an effect slot.
Effect-specific data — the per-instance uniform fields declared in a MaterialEffect schema — lives on separate instanced attributes named effectBuf0, effectBuf1, … allocated only when at least one registered effect declares per-instance fields. This is a change from the previous layout where system data and effect data shared effectBuf0.
TSL helpers
Section titled “TSL helpers”packages/three-flatland/src/materials/instanceAttributes.ts exports the recommended accessors for use inside TSL nodes. They hide the packed layout and stay forward-compatible if the buffer is rearranged:
| Helper | Returns | Reads |
|---|---|---|
readFlip() | vec2 | instanceSystem.xy (±1 per axis) |
readSystemFlags() | int | instanceSystem.z (raw bitfield) |
readEnableBits() | int | instanceSystem.w (effect-active mask) |
readShadowRadius() | float | instanceExtras.x |
readLitFlag() | bool | bit 0 of system flags |
readReceiveShadowsFlag() | bool | bit 1 of system flags |
readCastShadowFlag() | bool | bit 2 of system flags |
Don’t call attribute('instanceSystem', 'vec4') directly unless you specifically need the raw vec4 — the helpers keep the call site readable and survive layout changes.
Example — fade unlit sprites with colorTransform
Section titled “Example — fade unlit sprites with colorTransform”A custom colorTransform that crossfades between two tints based on each sprite’s lit flag, applied per-instance from the same batch:
import { vec4 } from 'three/tsl'import { Sprite2DMaterial, readLitFlag } from 'three-flatland'
const material = new Sprite2DMaterial({ map: atlas, colorTransform: (ctx) => { const litColor = ctx.color.rgb const dimColor = ctx.color.rgb.mul(0.4) const isLit = readLitFlag() return vec4(isLit.select(litColor, dimColor), ctx.color.a) },})Lit sprites in this batch render at full color; sprites with lit = false render dimmed — all from the same draw call.
For the broader colorTransform / MaterialEffect API and where it sits in the pipeline, see the Pass Effects guide. For the Sprite2D-side accessors that drive these flags (lit, receiveShadows, castsShadow, shadowRadius), see the Sprites guide.
Next Steps
Section titled “Next Steps”- Flatland Guide — Wrap SpriteGroup with camera, post-processing, and global uniforms
- Sprites Guide — Sprite2D properties and configuration
- Pass Effects Guide — MaterialEffect / colorTransform reference