Skip to content

Painter mode (oit:false) for games and sims

insomni defaults to order-independent transparency (OIT): a per-pixel A-buffer that blends overlapping translucent shapes correctly regardless of submission order. Setting config.oit: false turns that off and falls back to the sorted alpha-blend path — plain depth-stamped painter order, the same model most 2D game engines and canvas renderers use by default.

const renderer = createRenderer(gpu, canvas, {
config: { oit: false },
});

This is not a degraded mode you fall into because OIT wasn’t available — it is a deliberate, fully supported profile. Choose it when your scene’s correctness model is “submission order is the depth story,” not “translucent layers commute.”

The oit-toggle example shows exactly what this page is about: the same three overlapping translucent shapes, reversing their submission order every frame. With OIT on, the composite stays stable — the per-pixel resolve doesn’t care what order they were pushed in. With OIT off, reversing the order visibly shifts the blend, because painter mode’s contract really is “submission order is the depth story.”

OIT on vs off Toggling config.oit every few seconds: order-independent vs order-dependent blending of the same overlapping shapes.

With oit: false:

  • No A-buffer allocation — no storage buffer sized to the canvas, no per-pixel fragment budget (K) to size or overflow. renderer.oitInfo is null.
  • Transparent draws route through the plain sorted alpha-blend pass instead of the OIT build/resolve sweeps.
  • Glyphs and sprites fall back to a depth-stamped painter order: they still carry the order they were submitted with, so opaque geometry occludes them correctly, but translucent-over-translucent blending now depends on submission order (see Transparency (OIT)).
  • Layer-pushed CustomDrawables (Layer.pushCustom) get real inline depth interleave with the rest of the layer’s shapes, because the whole frame is one painter sweep — no separate OIT emit/resolve pipeline to route them through.
  • You already sort by design. Tile-based games, sprite stacks with a fixed Y-depth convention, or any scene where “draw order == depth order” is the actual authoring model. OIT’s whole value proposition — don’t sort, it’s still correct — is wasted effort if the scene is sorted anyway.
  • You want deterministic layering over commutative blending. OIT guarantees overlapping translucent shapes look the same no matter which order you pushed them in. A sim that wants “this glow is always drawn under that panel, full stop” doesn’t need that guarantee — it needs the simpler, cheaper contract that painter order already provides.
  • You’re memory- or fill-rate-constrained. No A-buffer means no storage-buffer budget to size against maxStorageBufferBindingSize, and no risk of per-pixel fragment overflow (K exceeded → dropped fragments) on dense translucent stacks. See the resolve cap for what that failure mode looks like when OIT is on.
  • You interleave custom drawables with shapes. Inline pushCustom depth ordering is a painter-mode-native feature; under OIT, layer-pushed customs don’t get the same per-shape interleave guarantees.

If your scene has arbitrary, dynamically-ordered translucent content — particles, overlapping UI panels, charts with layered fills where you can’t guarantee push order — OIT’s per-pixel correctness is worth the A-buffer cost. Painter mode trades that guarantee away in exchange for simplicity and lower memory/fill-rate overhead; it’s a trade, not a strict downgrade.

Damage-tracked partial redraw and OIT compose cleanly: the OIT resolve pass (like the build/emit sweep before it) is scissored to each damaged region and issues one draw per disjoint rect, not a full-canvas draw — see Damage-tracked rendering. So a scene with translucent content doesn’t lose its partial-redraw savings just because OIT is on; painter mode’s advantages are the ones listed above (no A-buffer, deterministic layering, inline custom-drawable interleave), not a forced full-canvas OIT resolve.