MeshSplatBench (also presented as MeshSuite) is a unified, Python-first benchmarking toolkit and evaluation framework for comparing triangle-splatting-style radiance field reconstruction methods, bridging academic novel view synthesis research and real-time industrial game engine deployment (Unity). Visit the Project Page for interactive comparisons and full leaderboards.
MeshSplatBench provides a single, consistent evaluation framework for comparing different triangle-based radiance field reconstruction methods. Instead of wrangling per-repo scripts and ad-hoc metrics, you get:
- Standardized metrics -- PSNR, SSIM, LPIPS, and geometry quality, computed the same way for every method.
- Adapter pattern -- plug any triangle-splatting method into the same API.
- Reproducible experiments -- YAML configs lock down every hyperparameter.
- CLI-first workflow -- inspect, profile, evaluate, and compare from the terminal.
- Industrial Engine Parity -- export method-preserving
.triassetpackages and evaluate real-time framerates and rendering fidelity natively in Unity.
| Method | Adapter | Status |
|---|---|---|
| 2D Triangle Splatting (2DTS) | D2TSAdapter |
Config/train/render/eval/export support |
| Triangle Splatting | TriangleSplattingAdapter |
Config/train/render/eval/profile/export support |
| MeshSplatting | MeshSplattingAdapter |
Config/train/render/eval/profile/export support |
| DiffSoup | DiffSoupAdapter |
Config/train/render/eval/export support; In-shader Micro-MLP support |
git clone https://github.com/prinasi/MeshSplatBench.git
cd MeshSplatBench
conda env create -f environment.yml
conda activate msbenchIf you already have a Conda or Python environment with PyTorch (>= 2.0 with CUDA support), install MeshSplatBench in editable mode:
git clone https://github.com/prinasi/MeshSplatBench.git
cd MeshSplatBenchTo build all bundled CUDA rendering backends (Triangle Splatting, MeshSplatting, 2DTS, Simple-KNN, and DiffSoup):
# Ensure CUDA_HOME points to CUDA 11.8+ (required for Ada Lovelace / sm_89 / RTX 40-series cards)
export CUDA_HOME=/usr/local/cuda-11.8
export PATH=$CUDA_HOME/bin:$PATH
pip install -e ".[dev,cuda]" --no-build-isolationTip for Multi-CUDA Systems: If your system default
/usr/bin/nvccis an older version (e.g., CUDA 11.5), settingCUDA_HOME(orexport CUDACXX=$CUDA_HOME/bin/nvcc) ensures both PyTorch and CMake (used by DiffSoup) use the matching CUDA 11.8+ compiler.
If you only need to work on Python code, CLI tools, evaluation scripts, or tests without compiling C++/CUDA extensions:
MSBENCH_SKIP_CUDA=1 pip install -e ".[dev]" --no-build-isolationIf you only require specific CUDA backends (e.g., skipping DiffSoup CMake compilation or only building Triangle Splatting):
# Compile only triangle-splatting, mesh-splatting, 2dts, and simple-knn (skip DiffSoup)
MSBENCH_CUDA_BACKENDS=triangle-splatting,mesh-splatting,2dts,simple-knn pip install -e ".[dev]" --no-build-isolation
# Or compile only Triangle Splatting and its KNN dependency
MSBENCH_CUDA_BACKENDS=triangle-splatting,simple-knn pip install -e ".[dev]" --no-build-isolationSupported backend identifiers: triangle-splatting, mesh-splatting, 2dts, simple-knn, diffsoup.
| Environment Variable | Description | Default |
|---|---|---|
MSBENCH_SKIP_CUDA |
Skip compiling all CUDA extensions (1, true, yes) |
Unset |
MSBENCH_CUDA_BACKENDS |
Comma-separated list of backends to build | All |
MSBENCH_CUDA_ARCHS |
Semicolon-separated CUDA architectures for CMake (DiffSoup) | 89 |
MSBENCH_CCACHE |
Enable ccache compilation caching for NVCC (1, true, yes) |
Unset |
MAX_BUILD_JOBS |
Maximum parallel compilation jobs | Auto |
MeshSplatBench evaluates methods on standard novel view synthesis benchmarks: Mip-NeRF 360, NeRF-Synthetic (Blender), Tanks & Temples, and DTU.
Readers should download the datasets from their official sources and place or symlink them into the data/ directory according to the default format:
mkdir -p data
ln -s /path/to/MipNeRF360 data/mipnerf360
ln -s /path/to/nerf_synthetic data/nerf_synthetic
ln -s /path/to/tandt data/tandt
ln -s /path/to/DTU data/dtuDetailed directory trees, download links, and COLMAP structures are documented in data/README.md.
# Train, render, evaluate, inspect, and profile from one experiment config
msbench train --config configs/triangle-splatting/mipnerf360/bicycle.yaml
msbench render images --config configs/triangle-splatting/mipnerf360/bicycle.yaml
msbench eval images --config configs/triangle-splatting/mipnerf360/bicycle.yaml
msbench inspect --config configs/triangle-splatting/mipnerf360/bicycle.yaml
msbench profile --config configs/triangle-splatting/mipnerf360/bicycle.yaml
# Config values can still be overridden from the CLI
msbench render video --config configs/triangle-splatting/mipnerf360/bicycle.yaml --output-dir outputs/demo-video
# Train complete datasets with the scene pipeline helper
bash single_train.sh triangle-splatting mipnerf360/all 0
bash single_train.sh triangle-splatting tandt/all 0
bash single_train.sh triangle-splatting dtu/all 0
bash single_train.sh mesh-splatting mipnerf360/all 0
bash single_train.sh mesh-splatting tandt/all 0
bash single_train.sh mesh-splatting dtu/all 0
bash single_train.sh 2dts mipnerf360/all 0
bash single_train.sh 2dts tandt/all 0
bash single_train.sh 2dts dtu/all 0
# Export feature-preserving Unity-native assets from completed runs
bash single_export_unity.sh triangle-splatting mipnerf360/all 0
bash single_export_unity.sh mesh-splatting mipnerf360/garden 0
# Export missing Unity assets, capture Unity frames, profile FPS/memory, and format report tables
bash single_unity_eval.sh 2dts mipnerf360/bicycle 0 --unity "$UNITY" --unity-project unitymsbench profile measures the rendering performance of a trained checkpoint
and writes a versioned JSON report. It reuses the same config resolution as
train/render/eval, so a completed run can be profiled with the same config:
msbench profile --config configs/triangle-splatting/mipnerf360/bicycle.yamlThe defaults in configs/base/_base_.yaml write the report to
outputs/{method}/{dataset}/{scene}/profile.json and measure the test split
with 100 timed repeats after 5 untimed warmup renders.
| Metric | Top-level key | Notes |
|---|---|---|
| Forward latency | forward_latency_ms |
Mean of the timed eval renders |
| FPS | fps |
1000 / forward_latency_ms |
| Peak CUDA memory | peak_cuda_memory_gb |
Peak device allocation during one eval render |
| Primitive count | primitive_count |
Triangles/faces/primitives in the checkpoint |
| Checkpoint size | checkpoint_size_mb |
On-disk state-dict size |
| Backward latency | backward_latency_ms |
Mean train-mode backward time; opt-in |
The forward object (and backward, when enabled) carries the full timing
summary (mean_ms, std_ms, p50_ms, p95_ms, min_ms, max_ms, fps),
and memory reports both allocated and reserved peaks. Timings use CUDA events
when the adapter runs on a CUDA device and wall-clock time otherwise. The
report shape is versioned under schema_version.
Explicit command-line flags override the config; otherwise the profile:
block from --config is used:
profile:
split: test
repeats: 100
warmup: 5
backward: false
output: outputs/{method}/{dataset}/{scene}/profile.json| Flag | Description |
|---|---|
--method/-m, --checkpoint/-c, --dataset/-d |
Override adapter method, checkpoint path, or dataset root |
--scene |
Scene name; resolves {scene} path templates |
--split/-s |
Dataset split (train/test/all) |
--repeats/-r |
Number of timed repetitions |
--warmup/-w |
Number of untimed warmup renders |
--backward/--no-backward |
Also measure backward-pass latency |
--output/-o |
JSON report path |
Fast smoke run on the same config:
msbench profile \
--config configs/triangle-splatting/mipnerf360/bicycle.yaml \
--repeats 3 \
--warmup 1 \
--backward \
--output /tmp/bicycle-profile.jsonTriangle Splatting bicycle (4.89 M triangles, RTX 4090):
{
"schema_version": 1,
"method": "triangle-splatting",
"dataset": "data/mipnerf360/bicycle",
"split": "test",
"device": "cuda:0",
"camera_name": "_DSC8679",
"repeats": 100,
"warmup": 5,
"status": "complete",
"forward_latency_ms": 14.847,
"fps": 67.35,
"peak_cuda_memory_gb": 3.399,
"primitive_count": 4888414,
"checkpoint_size_mb": 1100.22,
"forward": {
"mean_ms": 14.847,
"std_ms": 0.151,
"p50_ms": 14.850,
"p95_ms": 15.037,
"min_ms": 14.557,
"max_ms": 15.632,
"fps": 67.35
},
"memory": {
"peak_memory_allocated_gb": 3.399,
"peak_memory_reserved_gb": 3.697
}
}Running with --backward adds a backward summary with the same shape plus
backward_latency_ms (about 27.7 ms for this scene).
Native rasterization requires the bundled CUDA extensions. Rebuild them whenever the active PyTorch/CUDA environment changes; an ABI mismatch surfaces as an extension import error when the adapter loads:
conda activate msbench
MSBENCH_BUILD_CUDA=1 \
MSBENCH_CUDA_BACKENDS=triangle-splatting,simple-knn \
python setup.py build_ext --inplace --forceSet MSBENCH_CUDA_BACKENDS to the backends you need and MSBENCH_CUDA_ARCHS
to your GPU architecture (default 89); set MSBENCH_SKIP_CUDA=1 to skip
extension builds entirely.
MeshSplatBench can export method-preserving Unity .triasset packages, render Unity
test-view images for PSNR/SSIM/LPIPS, and profile GPU FPS/memory through a
Linux standalone Development Player. The workflow is designed for completed
config-driven runs under outputs/{method}/{dataset}/{scene}.
Install Unity with Linux standalone build support. Set the usual path:
export UNITY=/path/to/Unity/Editor/Unitysingle_unity_eval.sh applies the Vulkan compatibility patch automatically on
Linux. It also builds or reuses a Linux standalone Development Player at:
outputs/unity_player/linux/MeshSplatBenchProfilePlayer.x86_64
./single_export_unity.sh 2dts mipnerf360/all cpu
./single_export_unity.sh triangle-splatting mipnerf360/garden cpuEach scene writes:
outputs/{method}/{dataset}/{scene}/unity_native/{method}.triasset/
Existing valid packages are skipped; use --force to re-export.
./single_unity_eval.sh 2dts mipnerf360/all 0 \
--unity "$UNITY" \
--unity-project unityFor all methods:
for method in 2dts triangle-splatting mesh-splatting diffsoup; do
./single_unity_eval.sh "$method" mipnerf360/all 0 \
--unity "$UNITY" \
--unity-project unity
doneThe default condition is method-aware; outputs are written under each scene's
unity_method_aware/ directory and summarized in:
outputs/unity_reports/{method}/unity_method_aware/unity_metrics_report.csv
outputs/unity_reports/{method}/unity_method_aware/unity_metrics_report.json
outputs/unity_reports/{method}/unity_method_aware/unity_metrics_report.md
The terminal summary reports:
PSNR | SSIM | LPIPS | GPU FPS | GPU P50 ms | GPU P95 ms | Render MiB | Asset MiB
GPU FPS is computed as 1000 / GPU P50 ms. CPU/engine FPS is deliberately
not used for paper tables.
Use --general-purpose and a separate output name:
for method in 2dts triangle-splatting mesh-splatting diffsoup; do
./single_unity_eval.sh "$method" mipnerf360/all 0 \
--unity "$UNITY" \
--unity-project unity \
--general-purpose \
--output-name unity_general_purpose
doneMethods whose exported manifest does not declare a comparable general-purpose appearance are rejected instead of producing misleading numbers.
To isolate the value of MeshSplatting's shared-vertex topology, use:
./single_mesh_topology_unity_eval.sh mipnerf360/all 0 \
--unity "$UNITY" \
--unity-project unityThis runs mesh-splatting with three deployment layouts:
mesh: preserves the exported indexed mesh and renders it through a real UnityMeshFilter/MeshRendererwithMesh.SetIndices. For MeshSplatting,single_mesh_topology_unity_eval.shenables--indexed-mesh-method-aware, so the Unity indexed mesh path still evaluates the learned SH appearance instead of falling back to the older DC-only vertex-color baseline.shader-soup: loads the same indexed.triasset, but uses the procedural shader path as a corner-level triangle-soup intervention without duplicating buffers.materialized-soup: exports a separate.triassetwith per-triangle duplicated positions/SH/opacity attributes and sequential soup indices. This is intended for asset-memory, load-feasibility, and CG compatibility evidence.
Reports are written under:
outputs/unity_reports/mesh-splatting/unity_mesh_topology/{indexed_mesh,shader_soup,materialized_soup}/
The lower-level wrapper also accepts --topology indexed|soup directly when a
single shader-level condition is needed:
./single_unity_eval.sh mesh-splatting mipnerf360/bicycle 0 \
--unity "$UNITY" \
--unity-project unity \
--topology soup \
--output-name unity_method_aware_soupFor a single true Unity indexed MeshRenderer run, use:
./single_unity_eval.sh mesh-splatting mipnerf360/bicycle 0 \
--unity "$UNITY" \
--unity-project unity \
--general-purpose \
--topology indexed \
--indexed-mesh-method-aware \
--output-name unity_indexed_mesh_method_awareFor true exported soup assets, use a separate asset subdirectory:
./single_unity_eval.sh mesh-splatting mipnerf360/bicycle 0 \
--unity "$UNITY" \
--unity-project unity \
--asset-subdir unity_native_materialized_soup \
--export-topology soup \
--output-name unity_method_aware_materialized_soupBy default, GPU speed is measured with the standalone Player:
--profile-runtime player
--profile-runs 3
--profile-views 3
--profile-warmup 60
--profile-frames 180
--gpu-timing-min-fraction 0.5
This means each scene profiles three independent Player runs, three held-out
views per run, and 180 timed frames per view after warmup. Unity's
FrameTimingManager may not return GPU timing on every frame, so
--gpu-timing-min-fraction controls the minimum valid GPU sample coverage. A
profile with no valid GPU timing fails rather than falling back to CPU time.
For fast smoke tests:
./single_unity_eval.sh diffsoup mipnerf360/bicycle 0 \
--unity "$UNITY" \
--unity-project unity \
--profile-runs 1 \
--profile-views 1 \
--profile-warmup 20 \
--profile-frames 60 \
--fps-warmup 3 \
--fps-frames 12If quality images and metrics already exist and only GPU profiles need to be rebuilt:
./single_unity_eval.sh diffsoup mipnerf360/all 0 \
--unity "$UNITY" \
--unity-project unity \
--skip-capture \
--skip-metrics \
--force-profileThe wrapper reads each scene config. For Mip-NeRF 360, image_dir: images plus
resolution: 4 resolves to images_4; resolution: 2 resolves to images_2.
This keeps Unity quality/profile resolution aligned with native evaluation.
The Unity camera replay currently requires COLMAP sparse/0/cameras.bin and
sparse/0/images.bin.
Experiment configs use _base_ inheritance. Common defaults live in
configs/base/_base_.yaml, dataset-level defaults live in files such as
configs/base/mipnerf360.yaml, method-level shared settings live in files such
as configs/base/triangle-splatting.yaml, and method/dataset/scene configs
only keep the fields that are unique to that run.
Scene configs are grouped by method and dataset:
configs/triangle-splatting/
├── dtu/scan24.yaml
├── mipnerf360/bicycle.yaml
└── tandt/truck.yaml
configs/mesh-splatting/
├── dtu/scan24.yaml
├── mipnerf360/bicycle.yaml
└── tandt/truck.yaml
configs/2dts/
├── dtu/scan24.yaml
├── mipnerf360/bicycle.yaml
├── native/dtu.yaml
└── tandt/truck.yaml
For example, configs/triangle-splatting/mipnerf360/bicycle.yaml inherits the
MipNeRF360 dataset defaults plus Triangle Splatting defaults, then only sets the
scene and scene-specific training options:
_base_:
- ../../base/mipnerf360.yaml
- ../../base/triangle-splatting.yaml
dataset:
scene: bicycle
trainer:
outdoor: truedataset.root and dataset.scene are resolved together, so a dataset base can
declare root: data/mipnerf360 while a scene config declares scene: bicycle;
the effective dataset path becomes data/mipnerf360/bicycle. String templates
such as outputs/{method}/{dataset}/{scene} are resolved after all inherited
configs are merged.
To run a whole dataset, use single_train.sh with a dataset target ending in
/all. It expands the configured scene list, trains each scene, renders/evals,
exports videos, and prints a summary table:
bash single_train.sh triangle-splatting mipnerf360/all 0
bash single_train.sh triangle-splatting tandt/all 0
bash single_train.sh triangle-splatting dtu/all 0
bash single_train.sh mesh-splatting mipnerf360/all 0
bash single_train.sh mesh-splatting tandt/all 0
bash single_train.sh mesh-splatting dtu/all 0
bash single_train.sh 2dts mipnerf360/all 0
bash single_train.sh 2dts tandt/all 0
bash single_train.sh 2dts dtu/all 0Use all as the target to run all built-in datasets in one pass.
DTU Evaluation Modes: DTU training and evaluation defaults to masked foreground evaluation composited on a white background (
--dtu-foregroundor alias--dtu-masked). To train and evaluate on unmasked full images with natural backgrounds (Mode A), pass--dtu-fullor alias--dtu-unmasked:# Unmasked full-image evaluation (Mode A) bash single_train.sh triangle-splatting dtu/all 0 --dtu-unmasked # Masked white-background evaluation (Mode B, default) bash single_train.sh triangle-splatting dtu/all 0 --dtu-masked
Triangle Splatting configs inherit scene-specific triangle count caps from
configs/base/triangle-splatting-caps.yaml through
configs/base/triangle-splatting.yaml. During config finalization, the cap for
dataset.scene is written to trainer.max_shapes unless the scene config
explicitly overrides it.
MeshSplatting configs live under configs/mesh-splatting/ for MipNeRF360,
Tanks&Temples, and DTU. They use the bundled MeshSplatting renderer and a
method-specific trainer that preserves shared-vertex topology updates. DTU
configs follow the upstream DTU settings, including restricted Delaunay at
iteration 11000; that stage requires the MeshSplatting effrdel dependency to
be installed in the active environment.
Every method now writes to one consistent per-scene directory so training checkpoints, rendered images, exported geometry, videos, and logs always live in the same place regardless of backend.
outputs/{method}/{dataset}/{scene}/
├── ckpt/ # all training checkpoints (one folder, every method)
│ ├── 30000.ckpt # 2DTS
│ ├── point_cloud/{N}.ply # 2DTS exported point clouds
│ ├── point_cloud/iteration_{N}/*.pt # triangle-/mesh-splatting
│ └── final_params.pt # DiffSoup
├── renders/ # rendered images + matching ground truth, by split
│ ├── train/
│ │ ├── renders/ # predicted RGB
│ │ ├── gt/ # dataset ground truth
│ │ ├── manifest.json
│ │ └── metrics.json
│ └── test/
│ ├── renders/
│ ├── gt/
│ ├── manifest.json
│ └── metrics.json
├── mesh/ # exported geometry used for mesh/DTU metrics
│ ├── fuse_post.ply # TSDF mesh (mip360/tandt/dtu)
│ └── {N}_pcd.ply # 2DTS DTU point-cloud export
├── video/ # trajectory video
│ ├── render_traj.mp4
│ └── frames/
├── logs/ # every log: pipeline *.log, training stdout,
│ # tensorboard events, cfg dumps, loss curves
├── config.yaml # resolved config snapshot
├── metrics.json # aggregated image metrics (test split)
├── mesh_metrics.json # DTU/mesh geometry metrics
├── profile.json # renderer profiling report (msbench profile)
├── stats.json # model statistics (msbench inspect)
└── train_stats.json # training time / peak GPU memory
The adapter.checkpoint template in each method base config points at ckpt/:
| Method | adapter.checkpoint |
|---|---|
| 2DTS | outputs/{method}/{dataset}/{scene}/ckpt |
| Triangle Splatting | outputs/{method}/{dataset}/{scene}/ckpt/point_cloud/iteration_{max_steps} |
| MeshSplatting | outputs/{method}/{dataset}/{scene}/ckpt/point_cloud/iteration_{max_steps} |
| DiffSoup | outputs/{method}/{dataset}/{scene}/ckpt/final_params.pt |
MeshSplatBench/
├── msbench/
│ ├── core/ # Cameras, stats, profiling, registry, config
│ ├── primitives/ # Triangle primitive types (independent, mesh, convex)
│ ├── renderers/ # Adapter wrappers for each method
│ ├── trainers/ # Training loop, losses, hooks
│ ├── metrics/ # PSNR, SSIM, LPIPS, geometry metrics
│ ├── datasets/ # Scene dataset loaders
│ ├── cli/ # Typer-based CLI commands
│ └── utils/ # Logging, timing, visualization helpers
├── configs/ # YAML experiment configs
├── tests/ # pytest test suite
└── pyproject.toml
If you find this benchmark, codebase, or results helpful in your research, please cite our paper:
@misc{zhang2026meshsplatbenchunifiedbenchmarktrianglebased,
title={MeshSplatBench: A Unified Benchmark for Triangle-Based Neural Rendering},
author={Kaixuan Zhang and Minxian Li and Mingwu Ren and Xiatian Zhu},
year={2026},
eprint={2609.01306},
archivePrefix={arXiv},
primaryClass={cs.GR},
url={https://arxiv.org/abs/2609.01306},
}This project is licensed under the MIT License. Third-party submodules and vendor components under msbench/vendor/ and submodules/ are subject to their respective original licenses.