Header menu logo Mibo

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:

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.