Web-based viewer for Triangle Splatting meshes with progressive LOD (Level of Detail) using Potree's octree streaming. Streams millions of triangles progressively in the browser — only the triangles visible at the current LOD are loaded and rendered.
▶ Watch demo video — Progressive LOD streaming of a ~1M triangle garden scene
# 1. Download the garden demo mesh (~344MB)
./scripts/download_demo.sh
# 2. Convert and build octree (requires PotreeConverter)
./scripts/run_demo.sh
# 3. Launch viewer
cd viewer && bun install && bun dev
# Open: http://localhost:5173/?data=output_garden_demo- Python 3.10+ with uv
- Bun (for the viewer dev server)
- PotreeConverter — download the latest release for your platform, extract it, and either:
- Add the extracted directory to your
PATH, or - Set
export POTREE_CONVERTER=/path/to/PotreeConverter
- Add the extracted directory to your
Convert a Triangle Splatting mesh to the Potree viewer format:
uv run python convert.py input.off output_dir/
cd viewer && bun dev
# Open: http://localhost:5173/?data=../output_dirThe .off files are generated by the Triangle Splatting project using create_off.py, which exports a trained model checkpoint as a colored OFF mesh. The project provides two training modes:
train_game_engine.py— optimized for real-time rendering. Aggressively prunes semi-transparent triangles, producing ~1M opaque triangles. Best for this viewer.train.py— standard training for maximum quality. Produces ~3.9M triangles, many semi-transparent, requiring alpha blending for best results.
Pre-trained .off meshes (garden and room scenes) are available on the
Triangle Splatting Google Drive.
For full SH (spherical harmonics) support from a training checkpoint:
# Step 1: Extract triangles, centroids, and SH coefficients
uv run python convert_triangle_splatting.py model.pt output_dir/
# Step 2: Convert centroids to LAS format
uv run python ply2las.py output_dir/centroids.ply output_dir/centroids.las
# Step 3: Build Potree octree
PotreeConverter output_dir/centroids.las -o output_dir/potree
# Step 4: Merge SH metadata into Potree metadata
uv run python scripts/merge_sh_metadata.py output_dir/This pipeline preserves view-dependent spherical harmonics coefficients (up to degree 3) for realistic lighting. Requires PyTorch.
| Key | Action |
|---|---|
| W/S/A/D | Move forward / backward / left / right |
| Q/E | Move up / down |
| Shift | Move 3x faster |
| L | Cycle material mode (shaded / flat / SH shader) |
| 0-3 | Set spherical harmonics degree |
| T | Toggle trackball helper |
| C | Copy camera URL to clipboard |
| Mouse drag | Orbit |
| Scroll | Zoom |
| Right-click drag | Pan |
Triangle Splatting produces meshes with millions of triangles. Loading all of them at once in a browser is impractical. We need progressive LOD — load coarse geometry first, refine as the user zooms in.
Potree already solves progressive LOD for point clouds — it organizes points into an octree and streams only the visible nodes. We piggyback on this infrastructure for triangles:
-
At conversion time, each triangle is reduced to its centroid (a single point). These centroids are fed into PotreeConverter, which builds the octree. Each centroid carries an
original_indexattribute linking it back to the full triangle intriangles.bin. -
At render time, potree-core handles the LOD: it loads octree nodes based on camera distance and screen-space budget, deciding which centroids (and thus which triangles) are visible.
-
TriangleLODManagerwatches potree-core's visibility decisions. When a node becomes visible, the manager reads theoriginal_indexvalues from that node's points and fetches the corresponding triangle geometry fromtriangles.binusing HTTP Range requests — loading only the bytes needed, not the whole file.
Potree octree (centroids) Triangle data (separate file)
┌─────────────────────┐ ┌──────────────────────┐
│ Node visible? │ │ triangles.bin │
│ centroid_0 [idx=5] │──Range──> │ [5]: v0,v1,v2 │
│ centroid_1 [idx=8] │──Range──> │ [8]: v0,v1,v2 │
│ centroid_2 [idx=2] │──Range──> │ [2]: v0,v1,v2 │
└─────────────────────┘ └──────────────────────┘
│ │
LOD managed by Three.js mesh created
potree-core per visible node
graph TD
A[".off mesh"] --> B["off2centroids.py"]
B --> C["centroids.ply<br/><i>one centroid point per triangle</i>"]
B --> D["triangles.bin<br/><i>vertex offsets (9 floats × 36 bytes each)</i>"]
B --> E["sh.bin<br/><i>spherical harmonics coefficients</i>"]
C --> F["ply2las.py"]
F --> G["centroids.las<br/><i>LAS 1.4 with original_index attribute</i>"]
G --> H["PotreeConverter"]
H --> I["potree/metadata.json<br/>potree/hierarchy.bin<br/>potree/octree.bin"]
style A fill:#4a9,stroke:#333,color:#fff
style I fill:#49a,stroke:#333,color:#fff
style D fill:#a94,stroke:#333,color:#fff
style E fill:#a94,stroke:#333,color:#fff
The key insight: centroids are indexed by Potree's octree for LOD decisions, while triangles.bin and sh.bin hold the actual rendering data, fetched on demand via HTTP Range requests using the original_index attribute as a lookup key.
Each frame: potree.updatePointClouds() → lodManager.sync() → create/destroy Three.js meshes for newly visible/hidden nodes.
- Triangle Splatting — Triangle Splatting for Real-Time Radiance Field Rendering (3DV 2026)
- Potree / potree-core — Point cloud octree streaming
- Three.js — WebGL rendering