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 groups draw calls that share settings into one GPU operation; 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
let createRenderer () = Renderer2D.create myView
Program.mkProgram init update
|> Program.withRenderer createRenderer
Each frame, the runtime calls myView ctx model buffer. Your view adds commands, the renderer sorts and executes:
buffer.Clear(): wipe previous frame's commands (the renderer clears the buffer for you at the start of each frame; do not callClearyourself from your view)myView 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.
// lightingCtx comes from init; sunDir, sunColor, torchLight and
// playerSprite are your game's values
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