Rendering 2D
2D rendering in Mibo lives in Mibo.Elmish.Graphics2D.
The core building blocks are:
RenderBuffer<int<RenderLayer>, RenderCmd2D>(submission + sorting)Batch2DRenderer(executes commands withSpriteBatch)
Mibo provides multiple API styles for 2D rendering, so you can choose the one that fits your coding style:
- DSL API: Computation expressions for declarative sprite and text configuration
- Draw2D API: Fluent builder for common operations
- Buffer2D API: Functional style with pipelined operations
- Extension methods: .NET-style methods on
RenderBuffer
Minimal 2D renderer
open Mibo.Elmish.Graphics2D
let view (ctx: GameContext) (model: Model) (buffer: RenderBuffer<RenderCmd2D>) =
Draw2D.sprite model.PlayerTex model.PlayerRect
|> Draw2D.atLayer 10<RenderLayer>
|> Draw2D.submit buffer
let program =
Program.mkProgram init update
|> Program.withRenderer (Batch2DRenderer.create view)
Render layers
RenderLayer is an int unit-of-measure.
- lower layers draw first
- higher layers draw last
If you enable sorting (default), the renderer sorts the buffer by layer each frame.
Cameras
The 2D renderer responds to SetCamera commands.
let cam = Camera2D.create model.CamPos model.Zoom viewportSize
Draw2D.camera cam 0<RenderLayer> buffer
Multi-camera in 2D
You can switch cameras multiple times in a single frame by emitting multiple camera/view commands and grouping your draws under each camera.
The built-in 2D renderer supports the same multi-camera "amenities" as 3D:
SetViewport(split-screen / minimaps)ClearTargetbetween cameras
Example (two viewports, two cameras):
let leftVp = Viewport(0, 0, viewportSize.X / 2, viewportSize.Y)
let rightVp = Viewport(viewportSize.X / 2, 0, viewportSize.X / 2, viewportSize.Y)
Draw2D.viewport leftVp 0<RenderLayer> buffer
Draw2D.clear (ValueSome Color.CornflowerBlue) false 1<RenderLayer> buffer
Draw2D.camera leftCam 2<RenderLayer> buffer
// left-side sprites...
Draw2D.viewport rightVp 10<RenderLayer> buffer
Draw2D.clear (ValueSome Color.DarkSlateGray) false 11<RenderLayer> buffer
Draw2D.camera rightCam 12<RenderLayer> buffer
// right-side sprites...
Note on ordering: if Batch2DConfig.SortCommands = true (the default), only layer ordering is guaranteed (commands within the same layer may reorder). For stateful sequences (viewport/clear/camera/effect), either:
- put each state change on its own layer (as above), or
- set
SortCommands = falseand rely on submission order.
API styles
DSL API (Computation Expressions)
The DSL API uses F# computation expressions for declarative sprite and text configuration:
open Mibo.Elmish.Graphics2D.DSL
let sprite =
sprite {
texture playerTexture
at 100 100
size 64 64
color Color.White
sourceRect (Rectangle(0, 0, 32, 32))
rotatedBy System.Math.PI / 4.0f
centered
flippedH
depth 0.5f
layer 10<RenderLayer>
}
sprite |> buffer.Sprite
let text =
text {
font myFont
content "Hello, World!"
at 50 50
color Color.Yellow
scale 1.5f
rotatedBy 0.1f
origin Vector2.Zero
layer 5<RenderLayer>
}
text |> buffer.Text
DSL Sprite Options
texture tex: Set the texture (automatically sets size to texture dimensions)normalMap tex: Set the normal map for lightingat x y: Position (supportsint*int,float32*float32, orVector2)size w h: Set width and heightsourceRect rect: Use a sub-rectangle of the texturecolor c: Tint colorrotatedBy radians: Rotation in radiansorigin o: Origin point for rotation/scalingcentered: Set origin to center of spriteflippedH: Flip horizontallyflippedV: Flip verticallydepth d: Depth value for sorting within layerlayer l: Render layer
DSL Text Options
font f: Sprite font to usecontent s: Text string to renderat x y: Position (supportsint*int,float32*float32, orVector2)color c: Text colorscale s: Scale multiplierrotatedBy radians: Rotation in radiansorigin o: Origin pointlayer l: Render layer
Draw2D API (Fluent Builder)
The Draw2D API uses pipelined functions for common operations:
Draw2D.sprite model.PlayerTex model.PlayerRect
|> Draw2D.withSource (Rectangle(0, 0, 32, 32))
|> Draw2D.withColor Color.White
|> Draw2D.atLayer 10<RenderLayer>
|> Draw2D.submit buffer
Draw2D.camera cam 0<RenderLayer> buffer
Draw2D.viewport vp 0<RenderLayer> buffer
Draw2D.clear (ValueSome Color.Black) false 0<RenderLayer> buffer
Draw2D.effect (ValueSome myEffect) 0<RenderLayer> buffer
Draw2D.blendState BlendState.AlphaBlend 0<RenderLayer> buffer
Draw2D.samplerState SamplerState.PointClamp 0<RenderLayer> buffer
Draw2D.depthStencilState DepthStencilState.Default 0<RenderLayer> buffer
Draw2D.rasterizerState RasterizerState.CullNone 0<RenderLayer> buffer
Buffer2D API (Functional Style)
The Buffer2D module provides functional-style operations:
open Mibo.Elmish.Graphics2D.DSL.Buffer2D
buffer
|> camera cam
|> clear Color.Black
|> sprite mySprite
|> text myText
|> blendState BlendState.Additive
|> effect myShader
|> lighting myLightingConfig
|> pointLight myPointLight
|> directionalLight myDirLight
|> occluder myOccluder
|> particles tex fx particleArray count
|> line p1 p2 Color.Red
|> rect myRect Color.Blue
|> circle center radius Color.Green
|> submit
Extension Methods API
.NET-style extension methods on RenderBuffer:
buffer.Sprite(playerTexture, 100, 100, layer = 10<RenderLayer>)
buffer.Text(myFont, "Hello", 50, 50, layer = 5<RenderLayer>)
buffer.Camera(cam, 0<RenderLayer>)
buffer.Clear(Color.Black, 0<RenderLayer>)
buffer.BlendState(BlendState.AlphaBlend, 0<RenderLayer>)
buffer.Effect(myEffect, 0<RenderLayer>)
buffer.Lighting(lightingState, 0<RenderLayer>)
buffer.AddLighting(ambientLight, 0<RenderLayer>)
buffer.PointLight(pointLight, 0<RenderLayer>)
buffer.DirectionalLight(dirLight, 0<RenderLayer>)
buffer.Occluder(occluder, 0<RenderLayer>)
buffer.Particles(texture, effect, particles, count, 10<RenderLayer>)
buffer.Line(p1, p2, Color.Red, 10<RenderLayer>)
buffer.Rect(rect, Color.Blue, 10<RenderLayer>)
buffer.Circle(center, radius, Color.Green, segments = 32, layer = 10<RenderLayer>)
Text Rendering
Text can be rendered using any of the API styles:
// Using DSL
let text =
text {
font myFont
content "Score: 100"
at 10 10
color Color.White
layer 100<RenderLayer>
} |> buffer.Text
// Using Draw2D
Draw2D.text myFont "Score: 100" position
|> Draw2D.atLayer 100<RenderLayer>
|> Draw2D.submit buffer
// Using extensions
buffer.Text(myFont, "Score: 100", 10, 10, layer = 100<RenderLayer>)
Shape Drawing
Mibo provides built-in support for drawing basic shapes:
Lines
// Buffer2D
buffer |> line p1 p2 Color.Red
// Extensions
buffer.Line(Vector2(0f, 0f), Vector2(100f, 100f), Color.Red, 5<RenderLayer>)
Rectangles
// Buffer2D
buffer |> rect (Rectangle(10, 10, 100, 50)) Color.Blue
// Extensions
buffer.Rect(myRect, Color.Blue, 10<RenderLayer>)
Circles
// Buffer2D
buffer |> circle (Vector2(50f, 50f)) 25f Color.Green
// Extensions (with custom segment count)
buffer.Circle(center, radius, Color.Green, segments = 32, layer = 10<RenderLayer>)
Particle System
For particle rendering, use the particle drawing command:
let particles = [| createParticle(); createParticle(); ... |]
// Buffer2D
buffer
|> particles texture effect particles particles.Length
// Extensions
buffer.Particles(texture, effect, particles, particles.Length, 15<RenderLayer>)
Post-Processing
Mibo provides a flexible post-processing system with built-in effects:
Configuration
open Mibo.Elmish.Graphics2D
let postProcessConfig =
PostProcess2DConfig.none
|> PostProcess2DConfig.withVignette (VignetteConfig.defaults vignetteEffect)
|> PostProcess2DConfig.withBloom (BloomConfig2D.defaults extractFx blurFx compositeFx)
|> PostProcess2DConfig.withColorGrade (ColorGradeConfig.defaults effect lutTexture)
|> PostProcess2DConfig.withCustomPasses [| { Effect = customEffect; SetupEffect = ValueNone } |]
let renderer =
Batch2DRenderer.createWithConfig
{ Batch2DConfig.defaults with
PostProcess = ValueSome postProcessConfig }
view
Vignette
Adds a darkening effect around the edges:
let vignette = VignetteConfig.defaults vignetteEffect
let config = PostProcess2DConfig.none |> PostProcess2DConfig.withVignette vignette
Adjust Radius and Softness to control the effect.
Bloom
Creates a glowing effect from bright areas:
let bloom = BloomConfig2D.defaults extractFx blurFx compositeFx
let config = PostProcess2DConfig.none |> PostProcess2DConfig.withBloom bloom
Adjust Threshold, Intensity, and Scatter to control the effect.
Color Grading
Apply color grading using a lookup table (LUT):
let colorGrade = ColorGradeConfig.defaults effect lutTexture
let config = PostProcess2DConfig.none |> PostProcess2DConfig.withColorGrade colorGrade
Custom Passes
Add your own post-processing effects:
let customPass = {
Effect = myEffect
SetupEffect = ValueSome (fun effect gameTime target ->
effect.Parameters["Time"].SetValue(single gameTime.TotalGameTime.TotalSeconds)
effect.Parameters["Resolution"].SetValue(Vector2(single target.Width, single target.Height))
)
}
let config = PostProcess2DConfig.none |> PostProcess2DConfig.withCustomPasses [| customPass |]
2D Lighting
Mibo includes a comprehensive 2D lighting system with support for point lights, directional lights, ambient lighting, and dynamic shadows.
Basic Lighting Setup
open Mibo.Elmish.Graphics2D
let lightingConfig =
{ Lighting2DConfig.defaults with
Enabled = true
Shadows = ValueSome { Shadows2DConfig.defaults with Quality = SoftShadowQuality2D.High } }
let renderer =
Batch2DRenderer.createWithConfig
{ Batch2DConfig.defaults with Lighting = ValueSome lightingConfig }
view
Lighting Commands
Set Lighting State
// Buffer2D
buffer |> lighting lightingState
// Extensions
buffer.Lighting(lightingState, 0<RenderLayer>)
Add Ambient Light
let ambient = { AmbientLight2D.defaults with Color = Color(0.1f, 0.1f, 0.15f, 1.0f) }
// Buffer2D
buffer |> addLighting ambient
// Extensions
buffer.AddLighting(ambient, 0<RenderLayer>)
Add Point Light
let pointLight = {
Position = Vector2(100f, 100f)
Color = Color.Orange
Radius = 200f
Intensity = 1.5f
}
// Buffer2D
buffer |> pointLight pointLight
// Extensions
buffer.PointLight(pointLight, 0<RenderLayer>)
Add Directional Light
let dirLight = {
Direction = Vector2(1f, -1f) |> Vector2.Normalize
Color = Color.White
Intensity = 0.8f
}
// Buffer2D
buffer |> directionalLight dirLight
// Extensions
buffer.DirectionalLight(dirLight, 0<RenderLayer>)
Add Occluder (for Shadows)
let occluder = {
Vertices = [| Vector2(50f, 50f); Vector2(150f, 50f); Vector2(100f, 150f) |]
Color = Color.Gray
}
// Buffer2D
buffer |> occluder occluder
// Extensions
buffer.Occluder(occluder, 0<RenderLayer>)
Shadow Quality
Control shadow quality with the SoftShadowQuality2D enum:
NoValue: No shadowsLow: Low quality, better performanceMedium: Balanced quality and performanceHigh: High quality shadows
let shadowConfig = {
Shadows2DConfig.defaults with
Quality = SoftShadowQuality2D.High
Bias = 0.001f // Adjust to prevent shadow acne
}
Configuration
Batch2DConfig
The Batch2DConfig record controls the renderer's behavior:
type Batch2DConfig = {
ClearColor: Color voption
SortCommands: bool
SortMode: SortMode
BlendState: BlendState voption
SamplerState: SamplerState voption
DepthStencilState: DepthStencilState voption
RasterizerState: RasterizerState voption
Effect: Effect voption
Transform: Matrix voption
PostProcess: PostProcess2DConfig voption
Lighting: Lighting2DConfig voption
Shader: ShaderBase2D voption
LitSprite: ShaderBase2D voption
ShadowCaster: ShaderBase2D voption
FinalBlendState: BlendState voption
}
Create a custom configuration:
Batch2DRenderer.createWithConfig
{ Batch2DConfig.defaults with
ClearColor = ValueSome Color.Black
SamplerState = ValueSome SamplerState.PointClamp
Lighting = ValueSome lightingConfig
PostProcess = ValueSome postProcessConfig }
view
You can also change key SpriteBatch state within a frame via commands:
Draw2D.effect(per-segment effect;ValueNonerestores default)Draw2D.blendStateDraw2D.samplerStateDraw2D.depthStencilStateDraw2D.rasterizerState
Shaders
Mibo provides shader base types for 2D rendering:
LitSprite: For sprites with normal maps and lightingShadowCaster: For rendering shadow castersPostProcess: For post-processing effects
Configure shaders via Batch2DConfig:
{ Batch2DConfig.defaults with
Shader = ValueSome ShaderBase2D.LitSprite // Indicates lighting should be used
LitSprite = ValueSome myLitSpriteShader // YOUR shader effect
ShadowCaster = ValueSome myShadowShader } // YOUR shader effect
Note: All shader effects are user-provided. Mibo only defines the contracts that your shaders must implement.
Shader Contracts
Mibo does not provide shaders - you are responsible for authoring your own HLSL shaders that conform to the parameter contracts described below. The 2D sample shader files are merely examples demonstrating one possible implementation of these contracts.
When implementing custom shaders for 2D rendering, your shaders must conform to specific parameter contracts. Mibo automatically sets these parameters before drawing.
Common Parameters
These parameters are set for all sprite-based rendering (sprites, text, particles):
Matrix Parameters
World(Matrix): Identity matrix for spritesView(Matrix): Camera view matrixViewMatrix(Matrix): Same as View (alias)Projection(Matrix): Camera projection matrixProjectionMatrix(Matrix): Same as Projection (alias)
Texture Parameters
NormalMap(Texture2D): Normal map texture (if available), otherwise a default flat normal map is providedTexture(Texture2D): Main texture alias (for particles)DiffuseTexture(Texture2D): Diffuse texture alias (for particles)
Vertex Input Structure
Vertex shaders should expect the following input structure:
|
Vertex Output Structure
For custom vertex shaders, pass world position to pixel shader:
|
LitSprite Shader Contract
When using ShaderBase2D.LitSprite (a user-provided effect set via Batch2DConfig.LitSprite), the renderer sets comprehensive lighting parameters.
Lighting Parameters
Ambient Lighting:
AmbientColor(Vector4): Ambient light color and intensity
Point Lights (dynamic count):
PointLightPositions[](Vector2 array): World space positionsPointLightColors[](Vector4 array): RGBA colorsPointLightRadii[](float array): Maximum influence radiusPointFalloffs[](float array): Falloff exponent valuesPointLightCount(int): Number of active point lights
Note: The F# side has no constraints on the number of lights. Arrays are dynamically sized to accommodate however many lights you add. Your shader should declare array sizes based on the maximum number of lights you want to support (considering performance).
Directional Lights (dynamic count):
DirectionalLightDirections[](Vector2 array): Normalized direction vectorsDirectionalLightColors[](Vector4 array): RGBA colorsDirectionalLightCount(int): Number of active directional lights
Note: Similar to point lights, directional lights have no inherent limit in the F# system. Your shader determines the maximum supported count.
Light Index Buffer (Tiled Lighting):
LightIndexBuffer(Texture2D): Texture containing light indices per tileTileSize(float): World space size of each tileTilesX(float): Number of tiles horizontallyMaxLightsPerTile(int): Maximum lights per tileLightIndexBufferWidth(float): Width of index buffer textureLightIndexBufferHeight(float): Height of index buffer texture
Camera and Viewport Parameters
ViewportSize(Vector2): Current viewport dimensionsViewportSizeInv(Vector2): 1.0 / ViewportSizeViewProjectionMatrix(Matrix): Combined view and projectionInverseViewMatrix(Matrix): Inverse of view matrixInverseProjectionMatrix(Matrix): Inverse of projection matrix
Shadow Parameters (Optional, when shadows enabled)
ShadowAtlas(Texture2D): Shadow map textureShadowAtlasSize(Vector2): Atlas dimensionsShadowBias(float): Depth bias to prevent shadow acneProjectionSize(float): World space size for shadow projectionPointLightShadowIndices[](float array): Atlas row indices for point lightsDirectionalLightShadowIndices[](float array): Atlas row indices for directional lights
Note: Shadow index arrays are dynamically sized. Your shader should declare these based on your maximum light counts.
Note: Shadow parameters are only set when Shadows2DConfig is enabled.
Example LitSprite Implementation
The following is an example implementation showing one way to implement the LitSprite contract. You may implement it differently as long as you respect the parameter contract.
|
ShadowCaster Shader Contract
Shadow casters render occluder geometry to a shadow atlas. When you enable shadows via Lighting2DConfig.Shadows, you must provide a shadow caster shader via Batch2DConfig.ShadowCaster that conforms to this contract.
Vertex Input Structure
|
Light Parameters
Point Light (Polar Projection):
LightPosition(Vector2): World space positionLightRadius(float): Maximum range for depth calculation
Directional Light (Orthographic Projection):
LightDirection(Vector2): Normalized direction vectorShadowOrigin(Vector2): World space origin for shadow map (snapped)ProjectionSize(float): World space half-extent of projection volume
Detection:
LightRadius < 0.0indicates directional lightLightRadius >= 0.0indicates point light
Atlas Parameters
AtlasWidth(float): Width of shadow atlas (angular resolution for point lights)AtlasHeight(float): Height (number of light rows)
Vertex Output Structure
|
Output Requirements:
Position.x: Projected to [-1, 1] based on light type (angle for point, perpendicular projection for directional)Position.y: Always 0.0 (each light gets its own row via viewport scissor)Position.z: Normalized depth [0, 1]Depth: Same as Position.z (passed through to pixel shader)
Example ShadowCaster Implementation
The following is an example implementation showing one way to implement the ShadowCaster contract. You may implement it differently as long as you respect the parameter contract.
|
Post-Process Shader Contract
Post-processing effects operate on full-screen quads. When you configure post-processing via Batch2DConfig.PostProcess, you provide your own effect instances (e.g., vignette, bloom, color grading, or custom passes). These effects must conform to the parameter contracts described below.
Important: Mibo does not provide post-process shaders - you author them yourself. The 2D sample includes example implementations for reference.
Common Parameters
Post-process shaders may use custom parameters, but common ones include:
Time(float): Current game time (set by custom pass SetupEffect)Resolution(Vector2): Render target dimensions (set by custom pass SetupEffect)SceneTexture(Texture2D): Input scene texture (name may vary)
Vertex Input/Output
|
Transform: Typically Position is already in clip space (-1 to 1), or apply mul(Position, mul(View, Projection)).
Built-in Post-Process Parameters
Vignette:
Radius(float): Effect radius (default 0.7)Softness(float): Edge softness (default 0.3)
Bloom:
Threshold(float): Brightness threshold for bloom extraction (default 0.8)Intensity(float): Bloom intensity (default 1.0)Scatter(float): Bloom scatter/spread (default 0.7)BloomTexture(Texture2D): Bloom texture input (for composite pass)
Color Grading:
LutTexture(Texture3D): Lookup table textureLutSize(float): LUT dimension (default 32)Blend(float): Blend factor (0.0 = no grading, 1.0 = full grading, default 1.0)
Example Post-Process Implementation
The following is an example implementation showing one way to implement a post-process shader. You may implement it differently as long as you respect the parameter contract for your specific effect type.
|
Technique Naming
Mibo uses SpriteBatch.Begin() with your effect, which will apply the first pass of the first technique. Your shaders should define a technique named appropriately:
- For sprites/text:
technique SpriteBatch { pass P0 { ... } } - For shadow casters:
technique ShadowCaster { pass P0 { ... } } - For post-processing: Name appropriately (e.g.,
technique BloomExtract)
Note: Technique naming is a convention to make your shader code readable. Mibo simply applies the first technique's first pass.
Safe Parameter Access
Mibo uses SafeSetParam which silently ignores missing parameters. This allows your shader to be compatible even if it doesn't use all parameters. Only declare the parameters you need.
Choosing Light Limits
The F# lighting system has no inherent limits on the number of lights. However, your shader must declare maximum array sizes. Consider these factors when choosing limits:
- GPU registers: Each array element consumes registers
- Performance: More lights = more calculations per pixel
- Use case: UI overlays may need fewer lights than complex scenes
Example shader configuration:
|
The system will only send data for the actual number of active lights, but your arrays should be large enough to handle your worst-case scenario.
Shader Model Requirements
- OpenGL:
vs_3_0,ps_3_0 - DirectX:
vs_5_0,ps_5_0(or lower for compatibility)
Custom GPU work (escape hatch)
The built-in 2D SpriteBatch renderer exposes DrawCustom for "do whatever you want" rendering.
Draw2D.custom
(fun ctx ->
// Raw GPU work (primitives, render targets, post-effects, etc.)
// Note: SpriteBatch is ended before this runs, and restarted after.
())
50<RenderLayer>
buffer
If you need a fully bespoke pipeline, you can still implement your own IRenderer<'Model> and add it with Program.withRenderer.
See also: Rendering overview (overall renderer composition) and Camera.
<summary>Provides constants and static methods for trigonometric, logarithmic, and other common mathematical functions.</summary>
val single: value: 'T -> single (requires member op_Explicit)
--------------------
type single = System.Single
--------------------
type single<'Measure> = float32<'Measure>
Mibo