Rendering 3D (Legacy)
⚠️ DEPRECATION NOTICE
This document describes the Legacy 3D System (
Mibo.Elmish.Graphics3D).For all new projects, please use the modern Render Pipeline (
Mibo.Rendering.Graphics3D), which supports Shadows, PBR, Bloom, and Tiled Forward Lighting.The legacy system is retained only for backward compatibility and extremely simple prototypes.
3D rendering in Mibo lives in Mibo.Elmish.Graphics3D.
The core building blocks are:
RenderBuffer<unit, RenderCmd3D>(submission order is preserved)Batch3DRenderer(executes commands and manages opaque/transparent passes)LineBatch(efficiently manages and batches 3D line primitives)
Minimal 3D renderer
open Mibo.Elmish.Graphics3D
let view (ctx: GameContext) (model: Model) (buffer: RenderBuffer<RenderCmd3D>) =
buffer.Add((), SetCamera model.Camera)
buffer.Add((), DrawMesh(Opaque, model.Level, Matrix.Identity, ValueNone, ValueNone, ValueNone))
let program =
Program.mkProgram init update
|> Program.withRenderer (Batch3DRenderer.create view)
Passes: opaque vs transparent
Most commands specify a RenderPass:
Opaque: depth write on, opaque blendingTransparent: depth read + alpha blending (sorted back-to-front)
The renderer partitions the command stream into opaque/transparent lists and sorts the transparent list correctly.
Camera and multi-camera rendering
RenderCmd3D includes:
SetViewportfor split screen / minimapsClearTargetto clear between camerasSetCamerafor subsequent draws
Because submission order is preserved, you can do multi-camera rendering by emitting:
SetViewportClearTargetSetCamera- draw commands
…and repeating.
Example (world + minimap):
open Microsoft.Xna.Framework
open Microsoft.Xna.Framework.Graphics
open Mibo.Elmish.Graphics3D
let view (ctx: GameContext) (model: Model) (buffer: RenderBuffer<RenderCmd3D>) =
// Main camera (full screen)
Draw3D.viewport ctx.GraphicsDevice.Viewport buffer
Draw3D.clear (ValueSome Color.CornflowerBlue) true buffer
Draw3D.camera model.MainCamera buffer
Draw3D.mesh model.Level Matrix.Identity
|> Draw3D.withBasicEffect
|> Draw3D.submit buffer
// Minimap camera (top-right)
let vp = ctx.GraphicsDevice.Viewport
let mini = Viewport(vp.Width - 256, 0, 256, 256)
Draw3D.viewport mini buffer
Draw3D.clear (ValueSome Color.Black) true buffer
Draw3D.camera model.MiniMapCamera buffer
Draw3D.mesh model.Level Matrix.Identity
|> Draw3D.withBasicEffect
|> Draw3D.submit buffer
Custom draws (escape hatch)
If you need custom GPU work without forking the renderer:
DrawCustom (GameContext * View * Projection -> unit)
This is handy for special effects, custom vertex buffers, debug gizmos, etc.
You can also use the helper Draw3D.custom:
Draw3D.custom
(fun (ctx, view, proj) ->
// example: set device state, draw debug primitives, etc.
// ctx.GraphicsDevice.DrawUserPrimitives(...)
())
buffer
Quads and billboards
The 3D renderer includes a built-in Sprite3D path for the “90% case”: unlit textured quads and billboards.
This path:
- does not require user-managed effects
- supports texture atlases via
UvRect - participates in the renderer’s opaque/transparent sorting (transparent is back-to-front)
- works naturally with multi-camera / multi-viewport rendering
Quads (Sprite3D)
DrawQuad is a fast way to draw lots of simple, textured rectangles in 3D without building a full Model.
Typical uses:
- ground decals / markers ("target here")
- simple planes (floors/walls) for prototypes
- tile-like world geometry
Quads are represented as center + basis half-extents (center, right, up), which is both flexible and fast.
There are helpers for common planes like XZ (ground decals) and XY (in-world UI).
open Microsoft.Xna.Framework
open Mibo.Elmish.Graphics3D
// Draw a 2x2 ground decal centered at (10,0,5)
let q =
Draw3D.quadOnXZ (Vector3(10f, 0f, 5f)) (Vector2(2f, 2f))
|> Draw3D.withQuadColor (Color.White)
|> Draw3D.withQuadUv UvRect.full
Draw3D.quad model.DecalTex q buffer
Billboards (Sprite3D)
DrawBillboard draws a quad that always faces the camera. This is ideal for “2D sprites in 3D space”.
Typical uses:
- particles (smoke, fire, sparks)
- floating UI / markers (quest icons)
- impostors / far-distance foliage
open Microsoft.Xna.Framework
open Mibo.Elmish.Graphics3D
let b =
Draw3D.billboard3D (Vector3(0f, 1.5f, 0f)) (Vector2(0.5f, 0.5f))
|> Draw3D.withBillboardRotation 0.0f
|> Draw3D.withBillboardColor Color.White
|> Draw3D.withBillboardUv UvRect.full
Draw3D.billboard model.ParticleTex b buffer
If you want “tree style” billboards that rotate only around an up axis:
let tree =
Draw3D.billboard3D pos (Vector2(2f, 4f))
|> Draw3D.cylindrical Vector3.Up
Draw3D.billboard model.TreeTex tree buffer
Lines and Grids
Mibo provides three ways to render 3D lines, ranging from simple debugging to high-performance effects.
Simple segments
For drawing a single line segment (e.g., debug rays or simple outlines):
open Mibo.Elmish.Graphics3D
Draw3D.line
(Vector3(0f, 0f, 0f))
(Vector3(10f, 0f, 0f))
Color.Red
buffer
Multiple segments
For drawing many lines efficiently (e.g., grids, wireframes):
let vertices = [|
VertexPositionColor(Vector3(0f, 0f, 0f), Color.White)
VertexPositionColor(Vector3(10f, 0f, 0f), Color.White)
// ...
|]
// lineCount is the number of segments (2 vertices per segment)
Draw3D.lines vertices (vertices.Length / 2) buffer
Custom effects
For advanced effects like glowing lines, dashed lines, or distance-faded grids (shader-based):
let gridEffect = Assets.effect "Effects/Grid" ctx
let setup (e: Effect) (ec: EffectContext) =
e.Parameters.["World"].SetValue(ec.World)
e.Parameters.["View"].SetValue(ec.View)
e.Parameters.["Projection"].SetValue(ec.Projection)
e.Parameters.["PlayerPosition"].SetValue(playerPos)
e.Parameters.["MaxDistance"].SetValue(7.0f)
Draw3D.linesEffect
Transparent
gridEffect
(ValueSome setup)
vertices
(vertices.Length / 2)
buffer
Using your own effect (advanced)
If you want full control over shader semantics, use the effect-driven commands:
Draw3D.quadEffectDraw3D.billboardEffect
These take an Effect and an optional setup callback (EffectSetup) which receives an EffectContext containing View and Projection.
Example: quadEffect (custom shader / parameters)
open Microsoft.Xna.Framework
open Microsoft.Xna.Framework.Graphics
open Mibo.Elmish.Graphics3D
// Cache effects in your model/renderer state; do not reallocate per draw.
let be = new BasicEffect(ctx.GraphicsDevice)
be.TextureEnabled <- true
be.VertexColorEnabled <- true
let setup : EffectSetup =
fun effect ec ->
match effect with
| :? BasicEffect as be ->
be.View <- ec.View
be.Projection <- ec.Projection
// World is baked into the quad's vertices (center/right/up).
// Any other parameters can be set here.
| _ -> ()
let q =
Draw3D.quadOnXZ (Vector3(10f, 0f, 5f)) (Vector2(2f, 2f))
|> Draw3D.withQuadColor Color.White
|> Draw3D.withQuadUv UvRect.full
be.Texture <- model.DecalTex
Draw3D.quadEffect Transparent (be :> Effect) (ValueSome setup) q buffer
Example: billboardEffect (custom shader / parameters)
open Microsoft.Xna.Framework
open Microsoft.Xna.Framework.Graphics
open Mibo.Elmish.Graphics3D
let be = new BasicEffect(ctx.GraphicsDevice)
be.TextureEnabled <- true
be.VertexColorEnabled <- true
let setup : EffectSetup =
fun effect ec ->
match effect with
| :? BasicEffect as be ->
be.View <- ec.View
be.Projection <- ec.Projection
| _ -> ()
let b =
Draw3D.billboard3D (Vector3(0f, 1.5f, 0f)) (Vector2(0.5f, 0.5f))
|> Draw3D.withBillboardColor Color.White
|> Draw3D.withBillboardRotation 0.25f
|> Draw3D.withBillboardUv UvRect.full
be.Texture <- model.ParticleTex
Draw3D.billboardEffect Transparent (be :> Effect) (ValueSome setup) b buffer
If you need arbitrary GPU work (render targets, post-processing, unusual state), use DrawCustom / Draw3D.custom.
<summary>Gets the total number of elements in all the dimensions of the <see cref="T:System.Array" />.</summary>
<exception cref="T:System.OverflowException">The array is multidimensional and contains more than <see cref="F:System.Int32.MaxValue">Int32.MaxValue</see> elements.</exception>
<returns>The total number of elements in all the dimensions of the <see cref="T:System.Array" />; zero if there are no elements in the array.</returns>
Mibo