Header menu logo Mibo

3D Layout Engine

The Layout3D engine provides a voxel-based level design system for 3D games. It lives in Mibo.Layout3D.

Core Concepts

The system is built on three primitives:

CellGrid3D - The Storage

A dense 3D array that stores your cell content:

open Mibo.Layout3D
open Microsoft.Xna.Framework

// Create a 100x50x50 grid with 2x2x2 unit cells
let grid = CellGrid3D.create 100 50 50 (Vector3(2f, 2f, 2f)) Vector3.Zero

Each cell holds 'T voption - either ValueSome content or ValueNone (empty). This struct-based option type has zero heap allocation per cell.

Axis Convention: - X = width (left/right) - Y = height (up/down) - Z = depth (forward/back)

Basic Operations

// Set a cell
CellGrid3D.set 5 3 10 myCell grid

// Get a cell (returns voption)
match CellGrid3D.get 5 3 10 grid with
| ValueSome cell -> // use cell
| ValueNone -> // empty

// Get world position for a cell
let worldPos = CellGrid3D.getWorldPos 5 3 10 grid  // Vector3(10f, 6f, 20f)

Iteration

// Iterate all populated cells
grid
|> CellGrid3D.iter (fun x y z cell ->
    printfn "Cell at (%d, %d, %d)" x y z
)

// Iterate only visible cells (culled to frustum)
// This is the foundation of efficient rendering in 3D, ensuring
// you only spawn/render models within the camera's view.
let viewBounds = BoundingBox(min, max)
grid
|> CellGrid3D.iterVolume viewBounds (fun x y z cell ->
    // render cell at (x, y, z)
)

GridSection3D - The Cursor

A section is a lightweight view into a grid. It provides:

You rarely create sections directly - the Layout3D.run function creates the root section for you.

Layout3D DSL - Composing Content

The Layout3D module provides a fluent DSL for placing content. All functions return the section, enabling pipeline composition.

Basic Usage

open Mibo.Layout3D

let myGrid =
    CellGrid3D.create 20 15 20 (Vector3(2f, 2f, 2f)) Vector3.Zero
    |> Layout3D.run (fun section ->
        section
        |> Layout3D.fill 0 0 0 20 15 20 FloorCell      // Fill entire volume
        |> Layout3D.shell 0 0 0 20 15 20 WallCell     // Add shell
        |> Layout3D.set 10 7 10 ChestCell             // Place item
    )

Scoping with Sections

Create sub-sections for relative positioning:

section
|> Layout3D.section 5 3 0 (fun inner ->
    // (0, 0, 0) here maps to (5, 3, 0) in the parent
    inner
    |> Layout3D.fill 0 0 0 4 4 4 FloorCell
)
// Returns to parent section, can continue chaining
|> Layout3D.section 12 3 0 (fun inner ->
    inner |> Layout3D.fill 0 0 0 4 4 4 FloorCell
)

Structural Helpers

// Padding - shrink section by N cells on all sides
section |> Layout3D.padding 2 (fun inner -> ...)

// PaddingEx - explicit padding for each side: left, bottom, back, right, top, front
section |> Layout3D.paddingEx 1 1 1 1 1 1 (fun inner -> ...)

// Center - position a fixed-size block in the center
section |> Layout3D.center 4 4 4 (fun inner -> ...)

// Flow - place stamps along axis with spacing
section |> Layout3D.flowX 5 stamps
section |> Layout3D.flowY 5 stamps
section |> Layout3D.flowZ 5 stamps

Primitives

Layout3D.set x y z content section                    // Single cell
Layout3D.fill x y z w h d content section             // Box volume
Layout3D.clear x y z w h d section                     // Clear volume

Planes (Single-Cell Thickness)

Layout3D.floorXZ x y z w d content section    // Horizontal floor
Layout3D.wallXY x y z w h content section     // Vertical wall (XY plane)
Layout3D.wallYZ x y z h d content section     // Vertical wall (YZ plane)

3D Shapes

Layout3D.shell x y z w h d content section    // Hollow box (6 faces)
Layout3D.edges x y z w h d content section    // 12 edges only

Repetition

Layout3D.repeatX x y z count content section  // Line along X
Layout3D.repeatY x y z count content section  // Line along Y (column)
Layout3D.repeatZ x y z count content section  // Line along Z
Layout3D.column x y z height content section   // Alias for repeatY

3D Geometry

Layout3D.line x1 y1 z1 x2 y2 z2 content section               // 3D Bresenham line
Layout3D.sphere cx cy cz radius filled content section        // Sphere
Layout3D.cylinder cx cz y radius height filled content section // Cylinder (Y-aligned)

Patterns

Layout3D.checker3D odd even section                // 3D checkerboard
Layout3D.checkerXZ y odd even section              // Planar checker (Floor)
Layout3D.checkerXY z odd even section              // Planar checker (Wall)
Layout3D.checkerYZ x odd even section              // Planar checker (Wall)
Layout3D.checkerShell x y z w h d odd even section // Box skin checker

Layout3D.scatter3D count seed content section      // Volumetric scatter
Layout3D.scatterXZ y count seed content section    // Planar scatter
Layout3D.scatterShell x y z w h d count seed content section

Layout3D.generate x y z w h d generator section    // Volumetric generate
Layout3D.generateXZ y generator section            // Planar generate
Layout3D.generateShell x y z w h d generator section

Iteration / Transformation

Layout3D.iter x y z w h d action section    // Read access to volume
Layout3D.map x y z w h d mapping section    // Transform existing content
Layout3D.replace oldContent newContent section  // Find and replace
Layout3D.setIfEmpty x y z content section  // Conditional set

Creating Your Own Stamps

A stamp is simply a function GridSection3D<'T> -> GridSection3D<'T>. You can create reusable stamps just like HTML custom elements:

Simple Stamp

/// A treasure chest on a pedestal
let treasureChest (section: GridSection3D<Cell>) =
    section
    |> Layout3D.fill 0 0 1 3 1 3 PedestalCell   // Base
    |> Layout3D.set 1 1 1 ChestCell              // Chest on top

Parameterized Stamp

/// A configurable room with floor, walls, and ceiling
let room width height depth floor wall ceiling (section: GridSection3D<Cell>) =
    section
    |> Layout3D.fill 0 0 0 width depth 1 floor          // Floor
    |> Layout3D.fill 0 height 0 width depth 1 ceiling   // Ceiling
    |> Layout3D.shell 0 0 0 width height depth wall    // Walls

Composing Stamps

Stamps compose with >> (function composition):

let guardPost =
    room 8 6 6 FloorCell WallCell CeilingCell
    >> Layout3D.center 2 1 2 (treasureChest)
    >> Layout3D.section 6 2 2 (torchStand)

Building a Component Library

Organize stamps into domain modules:

module Dungeon =
    let cell = room 5 5 5 FloorCell WallCell CeilingCell

    let corridor length =
        Layout3D.fill 0 0 0 length 3 3 FloorCell
        >> Layout3D.shell 0 0 0 length 3 3 WallCell

    let intersection =
        cell
        >> Layout3D.clear 2 0 2 1 5 1  // North door
        >> Layout3D.clear 2 0 0 1 5 1  // South door
        >> Layout3D.clear 0 2 2 5 1 1  // West door
        >> Layout3D.clear 0 2 0 5 1 1  // East door

Use them:

level
|> Layout3D.run (fun section ->
    section
    |> Layout3D.section 0 0 0 Dungeon.cell
    |> Layout3D.section 5 1 0 (Dungeon.corridor 10)
    |> Layout3D.section 15 0 0 Dungeon.intersection
)

The Stamp Pattern

Think of stamps Lego blocks, using a few blocks on top of each other you can build a bigger thing.

The key insight: stamps are just functions. You can store them, pass them around, compose them, and build complex structures from simple pieces.

Domain Modules

Mibo includes pre-built stamps for common 3D game types:

These serve as examples and starting points. Copy and modify them for your game's needs.

Rendering Integration

Basic Rendering

grid
|> CellGrid3D.iter (fun x y z content ->
    let worldPos = CellGrid3D.getWorldPos x y z grid
    spawnModel worldPos content
)

Volume-Culled Rendering

let frustumBounds = getCameraFrustumBounds()
grid
|> CellGrid3D.iterVolume frustumBounds (fun x y z content ->
    let worldPos = CellGrid3D.getWorldPos x y z grid
    spawnModel worldPos content
)

Using the Renderer Helper

open Mibo.Layout3D

// Basic render
CellGridRenderer3D.render grid (fun worldPos content ->
    spawnModel worldPos content
)

// Volume-culled render
CellGridRenderer3D.renderVolume frustumBounds grid (fun worldPos content ->
    spawnModel worldPos content
)

For large grids (1000+ cells), prefer Layout3D.generate over setting cells individually - it's a single pass with no intermediate allocations.

3D-Specific Considerations

Multi-Cell Models

  1. Use anchor-cell only and handle size at render time
  2. Create stamps that fill all occupied cells for blocking/collision

Rotation and Orientation

The grid stores position only. Rotation is: - Determined at render time by the user - Derived from context (auto-tiling, neighbor checks) - Stored in user's cell content type if needed: type Cell = { Kind: EntityType; Facing: Facing }

namespace Microsoft
val grid: obj
union case ValueOption.ValueSome: 'T -> ValueOption<'T>
val cell: obj
union case ValueOption.ValueNone: ValueOption<'T>
val printfn: format: Printf.TextWriterFormat<'T> -> 'T
val min: e1: 'T -> e2: 'T -> 'T (requires comparison)
val max: e1: 'T -> e2: 'T -> 'T (requires comparison)
val floor: value: 'T -> 'T (requires member Floor)

Type something to start searching.