Skip to content

WilliamLiu-1997/3DGS-PLY-3DTiles-Converter

Repository files navigation

3DGS-PLY-3DTiles-Converter

Convert Gaussian Splatting PLY files into 3D Tiles with SPZ-compressed GLB content.

npm version CI License

3DGS-PLY-3DTiles-Converter

Node.js CLI and library for converting GraphDECO or KHR-native Gaussian Splatting PLY files into explicit 3D Tiles. The converter builds a visual-cost-aware adaptive k-d tree, writes SPZ-compressed GLB tile content, uses a temp-file-backed pipeline for large inputs, and supports optional geospatial placement through a root transform or WGS84 coordinate.

Install

npm install 3dgs-ply-3dtiles-converter

Requires Node.js 18 or newer.

CLI

3dgs-ply-3dtiles-converter [options] <input.ply> <output_dir>

Example:

3dgs-ply-3dtiles-converter scene.ply out_tiles

You can also run it without installing globally:

npx 3dgs-ply-3dtiles-converter scene.ply out_tiles

From a cloned repository:

node ./bin/3dgs-ply-3dtiles-converter.js scene.ply out_tiles

Self-test:

3dgs-ply-3dtiles-converter --self-test out_self_test --no-open-inspector

By default, conversion removes the existing output_dir before rebuilding and opens the generated tileset in 3dtiles-inspector after success. Temporary workspace files are created under output_dir unless --tmp-dir is provided. Use --continue to resume from a preserved failed workspace, and use --no-open-inspector for batch or CI runs.

Output

Generated output includes:

  • tileset.json - compact explicit 3D Tiles tileset.
  • build_summary.json - compact conversion metadata, per-stage timings, memory plan, checkpoint state, tiling and geometric-error metadata, and placement fields.
  • tiles/{level}/{x}.glb - SPZ-compressed tile content.

Generated tileset.json files declare top-level 3DTILES_content_gltf extension metadata so CesiumJS can detect KHR_gaussian_splatting and KHR_gaussian_splatting_compression_spz_2 content.

Source Coordinates

The converter inspects PLY header comments for explicit coordinate metadata. PLYs with projected/geospatial comments such as epsg or offsetx/offsety/offsetz are treated as source Z-up data. PLYs without usable coordinate-system metadata keep the default GraphDECO/COLMAP camera-style basis: +Y down and +Z forward.

The resolved source coordinate system is written to build_summary.json as source_coordinate_system, with source and reason fields for auditability.

API

const { convert } = require('3dgs-ply-3dtiles-converter');

(async () => {
  const result = await convert('data/scene.ply', './out/tileset', {
    memoryBudget: 4,
    openInspector: false,
  });

  console.log(result.outputDir, result.splatCount);
})();

convert(inputPath, outputDir, options) returns:

Field Description
inputPath Absolute input PLY path.
outputDir Absolute output directory.
splatCount Parsed splat count.
shDegree Inferred spherical-harmonics degree.
args Normalized conversion arguments. If coordinate is provided, args.transform contains the generated ENU root transform.

The library API accepts the same option names as the CLI, using camelCase fields. For example, --memory-budget 4 maps to { memoryBudget: 4 }. Pass silent: true to suppress converter logs, or pass a custom logger with line, info, ok, warn, and error methods.

Options

Area CLI API Default Notes
Input convention --input-convention <value> inputConvention graphdeco Use graphdeco or khr_native; controls quaternion interpretation and opacity mapping.
Linear scale input --linear-scale-input linearScaleInput false Converts linear scale values to log scale.
Color space --color-space <value> colorSpace srgb_rec709_display Use lin_rec709_display or srgb_rec709_display; written to tileset extension metadata.
Tiling depth --max-depth <int> maxDepth auto Maximum tree LOD depth. If omitted, computed from splat count and samplingRatePerLevel.
Max leaf size --max-leaf-limit <int> maxLeafLimit 50000 Maximum deepest-level emitted splat budget; coarser same-depth virtual split limits derive from this and samplingRatePerLevel.
Min leaf size --min-leaf-limit <int> minLeafLimit maxLeafLimit / 10 Target splat-count limit for normal logical leaf partitioning.
Coverage boost --coverage-boost-scale <number> coverageBoostScale 1 Scales the display-only adaptive parent LOD coverage boost; 0 disables the boost.
Geometric error floor --min-geometric-error <number> minGeometricError null Minimum geometric error for the deepest emitted level.
Placement matrix --transform <json_matrix4> transform null Writes tileset.root.transform directly.
Placement coordinate --coordinate <json_[lat,long,height]> coordinate null Generates an ENU root transform from WGS84 degrees/meters.
LOD sampling --sampling-rate-per-level <0..1] samplingRatePerLevel 0.5 Sampling ratio between LOD levels.
Bounds mode --obb or --aabb orientedBoundingBoxes false --obb uses one root PCA basis for tile boxes, k-d splits, and simplify voxel grids; projected positions are not cached.
Memory budget --memory-budget <gb> memoryBudget 3 Sizes scan buffers, bucket buffers, simplify scratch space, write concurrency, and workers.
Temp directory --tmp-dir <dir> tmpDir output_dir Root directory for the temporary workspace.
Inspector --open-inspector or --no-open-inspector openInspector true Opens the generated tileset in 3dtiles-inspector after success.
Self-test count --self-test-count <int> selfTestCount 1000000 Number of synthetic splats when using --self-test.
Output cleanup --clean or --continue clean true --continue preserves the output directory and resumes a failed checkpoint.

Use --help to print the CLI usage text.

SPZ SH coefficient quantization is fixed at 8 bits for SH1, SH2, and SH3.

Gaussian Splats Simplify

The converter builds simplified Gaussian splat content for parent LOD tiles instead of only dropping points. It groups nearby splats, compares covariance, color, opacity, and visual footprint, then selects or merges representatives so lower-detail tiles keep stable coverage and recognizable structure at distance.

Use --sampling-rate-per-level to control how aggressively each parent LOD is reduced. Use --coverage-boost-scale to adjust the display-only coverage boost for parent LOD content, or set it to 0 to disable the boost.

Gaussian splats simplification overall comparison
Overall comparison after Gaussian splat simplification.

Gaussian splats simplification detail comparison
Detail comparison showing local structure preservation.

Tiling and Performance

The converter builds a visual-cost-aware adaptive k-d tree with AABB bounds and long-axis-biased, compact-volume-biased weighted SAH split planes. With --obb, the converter computes one root PCA basis during the scan pass and uses that same basis for emitted tile boxes, adaptive split scoring/routing, and exact-simplify voxel grids; child tiles do not compute independent PCA boxes. Logical k-d splits continue while depth < maxDepth and a node has more than minLeafLimit splats. Same-depth virtual splits do not add LOD depth; they subdivide oversized non-root nodes for output shape and content budget control. Their per-depth trigger cap is maxLeafLimit at maxDepth and scales upward for coarser depths as ceil(maxLeafLimit / (samplingRatePerLevel ** (maxDepth - depth))).

Large PLY files are processed through a staged temp-file-backed pipeline: scan global bounds and positions, build the adaptive tiling tree, partition leaf buckets, build parent LODs bottom-up from handoff buckets, then write tileset metadata. The pipeline stages position data in memory when it fits the configured memoryBudget; otherwise it streams positions through the temp workspace. The temp workspace defaults to output_dir/.tmp-ply-partitions; with --tmp-dir <dir>, it is created as <dir>/.tmp-ply-partitions. Successful conversions remove the temp workspace. Failed conversions preserve it so a later run with --continue and the same temp directory can reuse checkpoints.

Internal scan, partition, build, content, and worker concurrency are derived from memoryBudget and available CPU parallelism. The bottom-up build scheduler starts ready parent nodes as soon as child handoffs are complete, keeps runtime RSS reserve headroom, and grants spare memory to simplify/cache-heavy tasks when available. Larger budgets allow larger scan and bucket chunks, more simplify scratch space, more bucket entry caching, and more build/content workers, up to internal safety limits.

Geometric Error

The converter resolves a single root geometricError and scales child tile errors by logical LOD depth:

  • If --min-geometric-error / minGeometricError is set, the root value is back-computed so the deepest emitted logical level receives that configured value.
  • Otherwise, the converter chooses a reference logical level and back-computes the root value from that average. It first averages deepest emitted leaf tiles with more than 3 splats.
  • If every deepest leaf tile has 3 or fewer splats, the reference switches to internal tiles at the previous logical depth.
  • Reference estimates require each reference tile's accumulated visual weight and use max((tileDiagonal * 0.1) / sqrt(tileVisualWeight), tileDiagonal * 1e-6, 1e-6) for multi-splat tiles.

For each emitted tile:

tile.geometricError = rootGeometricError * geometricErrorScaleForDepth(tileLogicalDepth)

With the default samplingRatePerLevel of 0.5, that means depth 0 uses the root error, depth 1 uses half, depth 2 uses one quarter, and so on. Virtual splits can create deeper physical tile paths, but their emitted errors still follow the logical depth.

Global Placement

When the tileset needs geospatial placement, use one placement option:

  • --coordinate "[lat,long,height]" or { coordinate: [lat, long, height] } anchors the tileset origin at a WGS84 coordinate and generates a standard ENU transform.
  • --transform "[...16 numbers...]" or { transform: [...] } writes tileset.root.transform directly.

transform is interpreted in final 3D Tiles tile coordinates, not raw glTF Y-up node space.

Examples:

3dgs-ply-3dtiles-converter scene.ply out_tiles --coordinate "[31.2304,121.4737,30]" --no-open-inspector
await convert('scene.ply', './out_tiles', {
  coordinate: [31.2304, 121.4737, 30],
  openInspector: false,
});

Inspector

By default, a successful conversion opens the generated tileset in 3dtiles-inspector. Use it to inspect loading behavior, adjust placement and geometric-error settings, crop Gaussian splats, and save supported edits back to tileset.json and build_summary.json. Disable the inspector with --no-open-inspector or openInspector: false for batch runs.

3DTiles-Inspector

To crop Gaussian splats, click Draw Region in the inspector, then drag around the splats you want to remove. Until you move the camera, the pending crop stays editable: drag any corner or edge handle to refine the convex quadrilateral. Use the far-plane handle to set the crop depth, then click Confirm and Save. Saving rewrites the affected local SPZ-compressed .glb tile content, prunes empty tiles where possible, and refreshes the affected bounding volumes.

3DTiles-Inspector Crop Regions

Errors

Conversion failures throw ConversionError. Common causes include invalid option values, missing input/output paths, unsupported PLY fields, or missing required Gaussian PLY properties.

About

Convert Gaussian Splatting PLY files into 3D Tiles with adaptive LOD simplification and SPZ-compressed GLB output.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors