Logo Mibo

3D from 2D

A 3D map is a 2D footprint plus a height per cell. The footprint is a CellGrid2D. The vertical axis stays out of the authoring model. This page covers the cell type, the surface, the build, the occupancy, the stack, and the draw.

An instance span stretches one model over several cells. The rules are in Instances and Occupancy.

Cell type

The framework reads the cell through projections. A 3D cell carries these fields:

Field

Purpose

Read by

A model identity, or a ModelInfo with the authored size

Keys the draw and gives the transform its divisor

getKey, getMeshesAndMaterial, getTransform

Height: float32

Vertical stretch, in cells

Stack.feet, the transform

Span: InstanceSpan

Grid-plane occupancy

Occupancy.scan

Lift: float32

Where the column stands

The transform

Solid: bool

Gameplay meaning

Collision, hover

type ModelInfo = {
  Name: string
  SizeX: float32
  SizeY: float32
  SizeZ: float32
}

type BlockCell = {
  Model: ModelInfo
  Height: float32
  Span: InstanceSpan
  Lift: float32
  Solid: bool
}
let spanOf (cell: BlockCell) = cell.Span
let heightOf (cell: BlockCell) = cell.Height
let withSpan (cell: BlockCell) (span: InstanceSpan) = { cell with Span = span }

A 2D flat map needs no Span and no Lift.

Surface

A document resolves words, kernels, and elements through a Doc.Surface<'T>. Build the surface once. Span and WithSpan are required fields; ValueNone means "every cell covers one cell" and "a statement cannot size one". The complete declaration is in Authored Maps.

let surface: Doc.Surface<BlockCell> = {
  Words = frozen [ "grass", grass; "slab", slab; "pillar", pillar ]
  Kernels = frozen [ "field", Gen2 field; "wood", wordKernel 6 6 grass wall ]
  Elements = frozen [ "hut", hut; "rampart", rampart ]
  Span = ValueSome spanOf        // required; ValueNone: every cell covers one cell
  WithSpan = ValueSome withSpan  // required; ValueNone: a statement cannot size one
}

Build

Code-first: Flow.run builds the grid. Scan it into an Occupancy.

let struct (grid, marks) = Flow.run map myGrid

let occupancy =
  Occupancy.scan spanOf grid
  |> Result.defaultWith failwith

Authored: build every layer in one call. Each BuiltLayer carries its grid, landmarks, and occupancy.

match DocFlow.buildLayersXml(surface, source) with
| Ok layers -> layers
| Error reason -> failwith reason

DocFlow.build and buildXml return one grid and no occupancy. They refuse a document that places a spanning word. A document without layers builds as one layer named main.

Stack

A layer above another needs the height the layers below reach at each cell. Stack.feet derives that lift.

let feet = Stack.feet occupancies grids heightOf

for i in 0 .. grids.Length - 1 do
  let drawn = CellGrid2D.create width height cellSize Vector2.Zero

  CellGrid2D.iter
    (fun x y cell ->
      CellGrid2D.set x y { cell with Lift = feet[i][x + y * width] } drawn)
    grids[i]

feet[i] is the height under layer i, at x + y * Width. Layer 0 stands on the plane. Heights add. A spanning anchor lifts its whole rectangle, so a decoration over a plate lands on the plate.

Query

One query serves a hover, a collision test, and a spawn: which instance owns a cell.

let ownerAt (layer: BuiltLayer<BlockCell>) (x: int) (y: int) =
  Occupancy.owner x y layer.Occupancy
  |> ValueOption.bind(fun at ->
    CellGrid2D.get at.X at.Y layer.Grid
    |> ValueOption.map(fun cell -> struct (at, cell)))

A covered cell answers with the instance that covers it. Occupancy.rectOf gives the instance rectangle. Landmarks answers which document element painted the cell.

Draw

Create one context per map with a rectangle transform. The transform receives the rectangle and the anchor position. It scales the model over the cells it covers.

let context =
  InstancedRenderContext<BlockCell, string>.Rect(
    getKey = (fun cell -> cell.Model.Name),
    getMeshesAndMaterial = meshesOf,
    getTransform =
      fun (rect: CellRect) (basePos: Vector3) (cell: BlockCell) ->
        let boxW = float32 rect.W * cellSize
        let boxD = float32 rect.H * cellSize   // rect.H is depth
        let boxH = cell.Height * cellSize

        Matrix4x4.CreateScale(
          boxW / cell.Model.SizeX,
          boxH / cell.Model.SizeY,
          boxD / cell.Model.SizeZ)
        * Matrix4x4.CreateTranslation(
          basePos.X + boxW * 0.5f,
          basePos.Y + cell.Lift,
          basePos.Z + boxD * 0.5f)
  )

Height scales the model on Y. rect.W and rect.H scale it on X and Z. Lift moves it up. On MonoGame, the types are Microsoft.Xna.Framework.Vector3 and Matrix. The plain constructor takes Vector3 -> 'T -> Matrix4x4; its rectangle is the cell.

Draw each layer through its occupancy:

context.ResetFrameBuffers()

for layer in layers do
  context.RenderInstanced(buffer, layer.Grid, layer.Occupancy)

For a large world, window the draw. The window is a world-space box in int coordinates. The member converts it with CellGrid2D.visibleRange and visits the anchors whose rectangle it meets.

context.RenderWindowInstanced(
  buffer,
  int camera.Left,
  int camera.Top,
  int camera.Right,
  int camera.Bottom,
  layer.Grid,
  layer.Occupancy
)

The Draw DSL routes to the same members: buffer.renderFootprintInstanced(ctx, grid, occupancy) and buffer.renderFootprintWindowInstanced(ctx, left, top, right, bottom, grid, occupancy). Each has a shaderForKey overload. Per-key grouping, effect scopes, and pooled buffers are in GPU Instancing.

Order

  1. Read the source, or build the stamp in code.
  2. Parse, resolve, emit, and paint: one grid per layer.
  3. Scan each layer into an Occupancy.
  4. Derive Stack.feet when more than one layer draws.
  5. Cache the map. Steps 1 to 5 run once.
  6. Per frame: reset the context buffers, compute the world window, draw each layer.
  7. Query on demand against the cached occupancy and grid.

Next

type ModelInfo = { Name: string SizeX: float32 SizeY: float32 SizeZ: float32 }
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
Multiple items
val float32: value: 'T -> float32 (requires member op_Explicit)

--------------------
type float32 = System.Single

--------------------
type float32<'Measure> = float32
type BlockCell = { Model: ModelInfo Height: float32 Span: obj Lift: float32 Solid: bool }
type bool = System.Boolean
val spanOf: cell: BlockCell -> obj
val cell: BlockCell
BlockCell.Span: obj
val heightOf: cell: BlockCell -> float32
BlockCell.Height: float32
val withSpan: cell: BlockCell -> span: 'a -> BlockCell
val span: 'a
val surface: BlockCell
union case ValueOption.ValueSome: 'T -> ValueOption<'T>
val grid: obj
val marks: obj
val occupancy: obj
Multiple items
module Result from Microsoft.FSharp.Core

--------------------
type Result<'T,'TError> = | Ok of ResultValue: 'T | Error of ErrorValue: 'TError
val defaultWith: defThunk: ('Error -> 'T) -> result: Result<'T,'Error> -> 'T
val failwith: message: string -> 'T
union case Result.Ok: ResultValue: 'T -> Result<'T,'TError>
val layers: obj
union case Result.Error: ErrorValue: 'TError -> Result<'T,'TError>
val reason: string
val feet: obj
val i: int32
val drawn: obj
val ownerAt: layer: 'a -> x: int -> y: int -> struct ('b * 'c) voption
val layer: 'a
val x: int
Multiple items
val int: value: 'T -> int (requires member op_Explicit)

--------------------
type int = int32

--------------------
type int<'Measure> = int
val y: int
Multiple items
module ValueOption from Microsoft.FSharp.Core

--------------------
type ValueOption<'T> = | ValueNone | ValueSome of 'T static member Some: value: 'T -> 'T voption static member op_Implicit: value: 'T -> 'T voption member IsNone: bool member IsSome: bool member Value: 'T static member None: 'T voption
val bind: binder: ('T -> 'U voption) -> voption: 'T voption -> 'U voption
val at: 'b
val map: mapping: ('T -> 'U) -> voption: 'T voption -> 'U voption
val cell: 'c
val context: obj
val layer: obj

Type something to start searching.