Metal-based 3D volume renderer for macOS and iOS — ray marching through MTLTexture3D with gradient lighting and a configurable transfer function pipeline.
Indigo is a volume rendering engine written in Swift and Metal integrated into applications for macOS and iOS. The core rendering engine performs GPU-resident ray marching through a MTLTexture3D populated from either image slices or synthetic floating-point phantoms. Gradient-based lighting and a configurable Transfer Function (TF) pipeline map volumetric density to colour and opacity.
macOS Application
iOS Application
Application System [User Input]
│
▼
┌──────────────────────────────────────────┐
│ VolumeRenderView ──► DialToolbar │
│ MetalVolumeView ──► SelectorPanel │
│ VolumeRenderViewModel │
└─────────────┬───────────────────────┬────┘
│ render() │ load()
▼ ▼
╔══════════════════════════════════════════════╗ IndigoVolumeEngine
║ VolumeRenderer ──► VolumeShaders.metal ║
║ │ ║
║ ├──────────────► TransferFunction ║
║ └──────────────► VolumeData ║
║ ▲ ║
║ VolumeSliceLoader ║
╚══════════════════════════════════════════════╝
│ rgba32Float MTLTexture2D
▼
┌──────────────────────────┐
│ TextureBlitRenderer │
└─────────────┬────────────┘
│ bgra8Unorm → CAMetalDrawable
▼
Display
| Component | Role |
|---|---|
VolumeRenderView | Root SwiftUI scene; hosts gesture recognizers (drag · pinch · rotate), three collapsible trays, and the MetalVolumeView canvas |
VolumeRenderViewModel | Main-actor ObservableObject; owns the renderer and device; mediates load requests, render triggers, and all published UI state |
DialToolbar / SelectorPanel | Pure-SwiftUI instrument panels — AnalogKnob, ManettinoSelector, TFSwatchGrid — with no Metal coupling |
MetalVolumeView + TextureBlitRenderer | Platform-bridged MTKView wrapper; blits the compute-output rgba32Float texture to the drawable each frame |
VolumeRenderer | Manages three MTLComputePipelineStates (DVR · MIP · Debug); assembles VolumeUniforms (208 bytes, 16-byte aligned) and dispatches compute threads; exposes clip-plane axis and distance |
VolumeShaders.metal | volumeRayMarch: front-to-back ray accumulation with gradient lighting, window/level transform, TF texture sample, and axis-aligned clip-plane discard. volumeMIP: max-intensity projection. volumeDebug: hit/density/ray-length visualisation |
TransferFunction | Interpolates RGBA control points into a 256-sample rgba32Float MTLTexture1D; bound at texture(2); nine named presets (CT Bone, CT Angio, MRI Brain, …) |
VolumeData | Owns the r32Float MTLTexture3D; exposes volumeScale = dims × voxelSpacing normalised to unit cube; provides synthetic generators (sphere, Shepp-Logan phantom, noise) |
VolumeSliceLoader | Ingests PNG/JPEG/TIFF slices (8-bit and 16-bit), raw binary volumes, and NIfTI-1 files (.nii / .nii.gz); applies slope/intercept and percentile window/level; emits per-slice progress callbacks |
| Connection | Carries | Transport |
|---|---|---|
| User → ViewModel | Gesture deltas, knob values, preset picks, file URLs | SwiftUI bindings / @ObservedObject didSet |
| ViewModel → VolumeSliceLoader | Directory / file URL, extension filter | Task.detached → throws VolumeData; progress (Float) → Void callback dispatched to main thread |
| VolumeSliceLoader → VolumeData | Normalised [Float], width × height × depth, voxel spacing | init(device:data:…) — MTLTexture.replace(region:…) CPU upload |
| ViewModel → VolumeRenderer | Render parameters, TransferFunction, VolumeData, clip-plane axis + distance | Property writes + render(width:height:transferFunction:) call |
| VolumeRenderer → GPU kernel | VolumeUniforms (208 B), MTLTexture3D at binding 1, MTLTexture1D TF at binding 2, output MTLTexture2D at binding 0 | MTLComputeCommandEncoder.setBytes / setTexture → dispatchThreadgroups |
| GPU kernel → TextureBlitRenderer | rgba32Float MTLTexture2D (compute output) | MTLTexture reference passed through VolumeRenderViewModel.currentTexture |
| TextureBlitRenderer → Display | bgra8Unorm pixel data | Fragment-shader blit → CAMetalDrawable → MTLCommandBuffer.present |