Header menu logo Mibo

Rendering 3D

3D rendering in Mibo lives in Mibo.Elmish.Graphics3D.

The core building blocks are:

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:

The renderer partitions the command stream into opaque/transparent lists and sorts the transparent list correctly.

Camera and multi-camera rendering

RenderCmd3D includes:

Because submission order is preserved, you can do multi-camera rendering by emitting:

  1. SetViewport
  2. ClearTarget
  3. SetCamera
  4. 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:

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:

Quads (Sprite3D)

DrawQuad is a fast way to draw lots of simple, textured rectangles in 3D without building a full Model.

Typical uses:

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:

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:

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.

See also: Camera and Culling.

val view: ctx: 'a -> model: 'b -> buffer: 'c -> 'd
val ctx: 'a
val model: 'b
val buffer: 'c
union case ValueOption.ValueNone: ValueOption<'T>
val program: obj
namespace Microsoft
union case ValueOption.ValueSome: 'T -> ValueOption<'T>
val vp: obj
val mini: obj
val q: obj
val b: obj
val tree: obj
val vertices: obj array
property System.Array.Length: int with get
<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>
val gridEffect: obj
val setup: e: 'a -> ec: 'b -> 'c
val e: 'a
val ec: 'b
val be: obj
val setup: effect: 'a -> ec: 'b -> 'c
val effect: 'a

Type something to start searching.