A high-performance Swift/Metal library for loading, rendering, editing, and exporting 3D Gaussian Splats on Apple platforms.
MetalSplatter implements GPU-accelerated rendering of scenes captured via 3D Gaussian Splatting for Real-Time Radiance Field Rendering. It supports PLY, SPLAT, SPZ, SPX, glTF/GLB, and SOGS I/O, plus an editable splat workflow built around SplatEditor for selection, transforms, cutting, alignment, visibility changes, undo/redo, and export.
- Multi-Platform Rendering: iOS/iPadOS, macOS, and visionOS with platform-optimized rendering paths
- Editable Splat Workflows:
SplatEditorwith GPU-backed selection, preview transforms, committed transforms, half-space plane cuts, hide/show, lock/unlock, delete, duplicate, separate, undo/redo, and export - Multiple File Formats: PLY (ASCII/binary), SPLAT, SPZ/SPX, glTF/GLB, and SOGS v1/v2
- Advanced Rendering Pipeline: Single-stage and multi-stage pipelines with tile memory for high-quality depth blending
- GPU-Accelerated Sorting: O(n) counting sort with camera-relative binning for optimal visual quality
- Spherical Harmonics: Full SH support (degrees 0-3) for view-dependent lighting effects
- Level of Detail: Distance-based LOD with configurable thresholds and skip factors
- Metal 4 Support: Bindless rendering, tensor operations, and SIMD-group optimizations on supported hardware
- Selection Feedback: Selected splats can be tinted and outlined; locked splats use a separate tint path
- AR Integration: ARKit support on iOS for augmented reality experiences
- Vision Pro Stereo: Vertex amplification for efficient stereo rendering via CompositorServices
| Module | Description |
|---|---|
| MetalSplatter | Core Metal rendering engine for gaussian splats |
| PLYIO | Standalone PLY file reader/writer (ASCII and binary) |
| SplatIO | Reads and writes gaussian splat scene formats |
| SplatConverter | Command-line tool for format conversion and inspection |
| SampleApp | Demo application with iOS/iPadOS editing tools |
| SampleBoxRenderer | Debug renderer for integration testing |
Add MetalSplatter to your Package.swift:
dependencies: [
.package(url: "https://github.com/lanxinger/MetalSplatter.git", from: "2.0.0")
]Then add the modules you need to your target:
.target(
name: "YourApp",
dependencies: [
.product(name: "MetalSplatter", package: "MetalSplatter"),
.product(name: "SplatIO", package: "MetalSplatter"),
]
)- File → Add Package Dependencies
- Enter the repository URL:
https://github.com/lanxinger/MetalSplatter.git - Select the modules you need
import MetalSplatter
import SplatIO
// Initialize the renderer
let renderer = try SplatRenderer(
device: device,
colorFormat: .bgra8Unorm,
depthFormat: .depth32Float,
sampleCount: 1,
maxViewCount: 2, // 2 for stereo, 1 for mono
maxSimultaneousRenders: 3
)
// Load splats from file (auto-detects format)
let reader = try AutodetectSceneReader(url)
let points = try reader.readScene()
try renderer.add(points)
// In your render loop
try renderer.render(
viewports: [viewportDescriptor],
colorTexture: drawable.texture,
colorStoreAction: .store,
depthTexture: depthTexture,
depthStoreAction: .dontCare,
rasterizationRateMap: nil,
renderTargetArrayLength: 0,
to: commandBuffer
)// PLY files
let plyReader = try SplatPLYSceneReader(url)
// Binary .splat files
let splatReader = try DotSplatSceneReader(url)
// Compressed SPZ files
let spzReader = try SPZSceneReader(url)
// SOGS format (WebP-based)
let sogsReader = try SplatSOGSSceneReaderV2(url)// Write to binary PLY
let plyWriter = try SplatPLYSceneWriter(toFileAtPath: outputURL.path, append: false)
try plyWriter.start(binary: true, pointCount: points.count)
try plyWriter.write(points)
try plyWriter.close()
// Write to SPZ format
let spzWriter = SPZSceneWriter()
try spzWriter.writeScene(points, to: outputURL)
// Write to glTF
let gltfWriter = GltfGaussianSplatSceneWriter(container: .gltf)
try gltfWriter.writeScene(points, to: outputURL)
// Write to SOGS v2 (.sog)
let sogWriter = SOGSV2SceneWriter()
try sogWriter.writeScene(points, to: outputURL)import MetalSplatter
import SplatIO
let renderer = try SplatRenderer(
device: device,
colorFormat: .bgra8Unorm,
depthFormat: .depth32Float,
sampleCount: 1,
maxViewCount: 1,
maxSimultaneousRenders: 3
)
let points = try AutodetectSceneReader(url).readScene()
let editor = try await SplatEditor(points: points, renderer: renderer)
try await editor.select(
.rect(normalizedMin: SIMD2<Float>(0.4, 0.4), normalizedMax: SIMD2<Float>(0.6, 0.6)),
mode: .replace,
viewport: viewportDescriptor
)
await editor.beginPreviewTransform(pivot: SIMD3<Float>(0, 0, 0))
try await editor.updatePreviewTransform(
SplatEditTransform(translation: SIMD3<Float>(0.1, 0.0, 0.0))
)
try await editor.commitPreviewTransform()
let editedPoints = try await editor.exportVisiblePoints()SplatEditor supports:
- Point, rect, mask, sphere, and box selection queries
- Density-based outlier selection via
selectOutliers(config:mode:) - Move, rotate, and scale preview transforms with commit/cancel
- Direct committed transforms for alignment or scripted edits
- Half-space plane selection and cuts via
SplatCutPlane/SplatCutPlaneSide - Hide, unhide, lock, unlock, delete, duplicate, and separate operations
- Undo/redo and snapshot inspection via
SplatEditorSnapshot - Export of the current visible edited scene back through
SplatIO
Common editing patterns include:
- Selection-first edits with point, volume, mask, flood-fill, and color-match tools
- Dedicated cut workflows that delete one side of an axis-aligned plane
- Alignment workflows that center or floor a selected region, or rotate it by quarter turns around X/Y/Z
- Non-destructive preview transforms for gesture-driven move/rotate/scale before commit
Try the included sample application to see MetalSplatter in action. The current editing UI is focused on iOS/iPadOS.
- Clone the repository
- Open
SampleApp/MetalSplatter_SampleApp.xcodeproj - Select an iPhone or iPad target and set your development team if needed
- Important: Use Release configuration for best performance (Debug is >10x slower)
- Build and run
- Load a supported splat file to visualize and edit
The iOS/iPadOS demo includes:
- Selection tools: point, rect, brush, lasso, flood, eyedropper/color-match, sphere, box, polygon, and measure
- Cut tools: axis-aligned plane cuts with selectable side and bounds-based plane positioning
- Alignment tools: center, center+floor, floor, and
-90° / +90° / 180°rotations around X/Y/Z - Edit tools: move, rotate, scale, hide/show, lock/unlock, delete, duplicate, separate, undo/redo, and export
- Selection utilities: replace/add/subtract combine modes plus all/none/invert helpers
- Renderer feedback: selection tint plus an outline pass for selected splats
The alignment tool is intended to make common import-fixup tasks easy:
- Recenter an off-origin model without manually dragging it into place
- Drop a model onto the ground plane by moving its minimum Y to
0 - Correct upside-down or sideways imports with single-tap quarter turns
- Apply the operation to the current selection, or to all visible editable splats when nothing is selected
Tip: For best framerate, run without the debugger attached (stop in Xcode, then launch from Home screen).
Convert between splat file formats and inspect splat data:
# Build the converter
swift build -c release
# Convert PLY to binary SPLAT format
swift run SplatConverter input.ply -o output.splat
# Convert to ASCII PLY
swift run SplatConverter input.ply -f ply-ascii -o output.ply
# Convert to glTF
swift run SplatConverter input.ply -f gltf -o output.gltf
# Convert to GLB
swift run SplatConverter input.ply -f glb -o output.glb
# Convert to SOGS v2
swift run SplatConverter input.ply -f sog -o output.sog
# Reorder by Morton code for better GPU cache coherency
swift run SplatConverter input.ply -o output.splat --morton-order
# Inspect splat data
swift run SplatConverter input.ply --describe --start 0 --count 10 -vOptions:
-o, --output-file: Output file path-f, --output-format: Format (dotSplat,ply,ply-binary,ply-ascii,gltf,glb,sog)-m, --morton-order: Reorder splats by Morton code for spatial locality--describe: Print splat details--start: First splat index (default: 0)--count: Maximum splats to process-v, --verbose: Verbose output with timing
| Format | Extensions | Read | Write | Notes |
|---|---|---|---|---|
| PLY | .ply |
✓ | ✓ | ASCII and binary, full SH support |
| SPLAT | .splat |
✓ | ✓ | Compact binary format |
| SPZ | .spz, .spz.gz |
✓ | ✓ | Gzip-compressed format |
| SPX | .spx |
✓ | ✓ | Alternative binary format |
| glTF | .gltf |
✓ | ✓ | KHR_gaussian_splatting JSON + BIN |
| GLB | .glb |
✓ | ✓ | Binary KHR_gaussian_splatting container |
| SOGS v1 | .sogs, meta.json |
✓ | - | WebP-based folder layout |
| SOGS v2 | .sog |
✓ | ✓ | Bundled archive format |
| SOGS ZIP | .zip |
✓ | - | Legacy ZIP archive |
Use AutodetectSceneReader for automatic format detection based on file extension and content.
// Multi-stage pipeline for high-quality depth blending
renderer.useMultiStagePipeline = true
// High-quality depth for Vision Pro frame reprojection
renderer.highQualityDepth = true
// Order-independent transparency (no sorting required)
renderer.useDitheredTransparency = true
// Metal 3+ mesh shaders
renderer.meshShaderEnabled = true
// Metal 4 bindless rendering
renderer.useMetal4Bindless = true// O(n) counting sort (recommended for large scenes)
renderer.useCountingSort = true
// Camera-relative bin weighting for better near-field precision
renderer.useCameraRelativeBinning = true
// Morton code reordering for GPU cache optimization
renderer.mortonOrderingEnabled = true
// Sorting thresholds (camera movement before re-sorting)
renderer.sortPositionEpsilon = 0.01 // meters
renderer.sortDirectionEpsilon = 0.0001 // ~0.5-1 degreeReduce sorting frequency during user interaction for smoother response:
// Begin interaction (e.g., on gesture start)
renderer.beginInteraction()
// During interaction, sorting thresholds are relaxed:
// - sortPositionEpsilon: 0.01 → 0.05
// - sortDirectionEpsilon: 0.0001 → 0.003
// - minimumSortInterval: 0 → 0.033 (~30 sorts/sec max)
// End interaction (high-quality sort triggered after delay)
renderer.endInteraction()// Distance thresholds for LOD levels
renderer.lodThresholds = SIMD3<Float>(10, 25, 50)
// Skip factors per LOD level (1 = all, 2 = half, etc.)
renderer.lodSkipFactors = [1, 2, 4, 8]
// Maximum render distance
renderer.maxRenderDistance = 100.0// Update threshold for view-dependent lighting
renderer.shDirectionEpsilon = 0.001 // ~2.5 degree rotation
// Minimum time between SH updates
renderer.minimumSHUpdateInterval = 0.016 // ~60 updates/sec max// Visualize overdraw (coverage issues)
renderer.debugOptions.insert(.overdraw)
// Visualize LOD bands
renderer.debugOptions.insert(.lodTint)
// Show axis-aligned bounding box
renderer.debugOptions.insert(.showAABB)renderer.onFrameReady = { stats in
print("Ready: \(stats.ready)")
print("Splat count: \(stats.splatCount)")
print("Sort duration: \(stats.sortDuration ?? 0)ms")
print("Frame time: \(stats.frameTime)ms")
print("Buffer uploads: \(stats.bufferUploadCount)")
}
renderer.onSortComplete = { duration in
print("Sort completed in \(duration)ms")
}
renderer.onRenderStart = { }
renderer.onRenderComplete = { }- Uses
MTKViewwithMTKViewDelegatepattern - Full gesture support (pinch zoom, rotation, panning)
- AR support via ARKit on iOS
- Uses CompositorServices with spatial rendering
- Automatic stereo via vertex amplification
- World tracking integration
- Optimized for Vision Pro display characteristics
The iOS Simulator on Intel Macs (x86_64) is not supported due to Metal limitations.
# Clone the repository
git clone https://github.com/lanxinger/MetalSplatter.git
cd MetalSplatter
# Build all targets (release mode recommended)
swift build -c release
# Run tests
swift test
# Build specific target
swift build --target MetalSplatter -c releaseApps and projects using MetalSplatter:
- MetalSplatter Viewer - Official Vision Pro app with camera controls and splat gallery
- OverSoul - Spatial photos, 3D models, and immersive spaces for Vision Pro
Using MetalSplatter in your project? Let us know!
- Capture your own: Use a camera or drone, then train with Nerfstudio
- Luma AI: Capture with the iPhone app, export in "splat" format
- Original paper data: Scene data from the original paper
- RadianceFields.com - News and articles about 3DGS and NeRFs
- MrNeRF's Awesome 3D Gaussian Splatting - Comprehensive research list
- Kevin Kwok's WebGL implementation (demo)
- Mark Kellogg's three.js implementation (demo)
- Aras Pranckevičius's Unity implementation and blog posts: 1, 2, 3
- Original reference implementation
MIT License - Copyright 2023 Sean Cier
See LICENSE for details.
