2D Rendering
The 2D rendering pipeline is a deferred command system: each frame, your view function populates a RenderBuffer2D with Command2D values, and the Renderer2D<'Model> sorts them by layer and executes them in order.
What and Why
A deferred renderer means you describe what to draw without worrying about when to draw it. The renderer handles:
- Layer ordering — Commands are sorted by
int<RenderLayer>so backgrounds draw before foregrounds. - Camera transforms —
.beginCamera()/.endCamera()bracket world-space content. - Shader modes —
.beginShader()/.endShader()enable per-section effects. - Post-processing — Screen-space shader passes applied after the scene renders.
- GPU batching — the backend auto-batches standard draw calls; the renderer never interferes.
This is especially useful for:
- 2D lighting — Light commands and sprites are interleaved in the same buffer and processed together.
- UI overlays — Draw UI on a higher layer (and optionally a separate camera) above your game world.
- Debug visualization — Toggle debug shapes on/off by adding or removing commands.
- Portability — The same view function works with any renderer configuration.
When to use deferred vs immediate
Situation |
Approach |
|---|---|
Sprites, text, shapes, tiles |
Use the fluent Draw members (deferred) |
Custom GPU work (e.g. raw rlgl meshes, instancing) |
Use |
One-off GPU operations |
Prefer deferred; use immediate only when the backend lacks the API |
How it works
Program.mkProgram init update
|> Program.withRenderer (fun () -> Renderer2D.create myView)
Each frame, the runtime calls myView ctx model buffer. Your view adds commands, the renderer sorts and executes:
buffer.Clear()— wipe previous frame's commandsmyView ctx model buffer— populate with this frame's draw commandsbuffer.Sort()— sort by layer (ascending)- Execute in order, managing camera/shader state transitions
Adding commands
Everyday view code chains members of the fluent Draw DSL on the buffer — see Draw DSL for the full surface.
Lighting
The 2D lighting system (Mibo.Elmish.Graphics2D.Lighting) provides point lights, directional lights, ambient light, and SDF soft shadows — all GPU-driven with no extra render passes.
buffer
.setAmbient(lightingCtx, gray, layer = 5<RenderLayer>)
.addDirectionalLight(lightingCtx, sunDir, sunColor, intensity = 1.5f, layer = 6<RenderLayer>)
.addPointLight(lightingCtx, torchLight, layer = 7<RenderLayer>)
.litSprite(lightingCtx, playerSprite)
.endLighting(lightingCtx, layer = 999<RenderLayer>)
.drop()
See Lighting & Shadows for details.
Multi-camera rendering
Use Camera2DConfig for viewport-based rendering, split-screen, or picture-in-picture cameras:
// Split-screen left/right
let left = Camera2D.splitScreenLeft cam1 Color.CornflowerBlue
let right = Camera2D.splitScreenRight cam2 Color.DarkGreen
buffer
.beginCameraWith(left)
// ... left viewport ...
.endCamera(layer = 100<RenderLayer>)
.beginCameraWith(right, layer = 200<RenderLayer>)
// ... right viewport ...
.endCamera(layer = 300<RenderLayer>)
.drop()
Camera2DConfig controls viewport (normalized 0–1 coordinates) and clear color. See Camera for the full API.
Next steps
- Draw DSL — The fluent, backend-neutral draw surface (2D and 3D)
- Buffer & Commands — How to build every type of draw command
- Lighting & Shadows — Point, directional, ambient lights + SDF shadows
- Particles — Batched textured quads
- Custom Commands — Custom rendering with DrawImmediate
- Performance — Writing efficient rendering code
- Camera — Cameras and coordinate transforms
Mibo