diff --git a/CHANGELOG.md b/CHANGELOG.md index 20d4700..bef5cc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added runtime PLY loading from byte arrays. A new method `LoadFromPlyBytes(byte[])` is added to `GsplatAsset` and its derived classes. ([#34](https://github.com/wuyize25/gsplat-unity/pull/34) by [@TakashiYoshinaga](https://github.com/TakashiYoshinaga)) +- Added an `OpacityPruneThreshold` option to `GsplatImporter`. Splats with opacity below this threshold are culled at import time. Currently only supported for `.ply` import. `LoadFromPly` / `LoadFromPlyBytes` take an optional `opacityPruneThreshold` parameter, so runtime byte-array loading can prune as well. ([#36](https://github.com/wuyize25/gsplat-unity/pull/36) by [@TakashiYoshinaga](https://github.com/TakashiYoshinaga)) + ### Fixed - Fixed PLY header parsing to count only vertex element properties. The package now supports PLY files exported by Apple's [ml-sharp](https://github.com/apple/ml-sharp). ([#33](https://github.com/wuyize25/gsplat-unity/pull/33) by [@TakashiYoshinaga](https://github.com/TakashiYoshinaga)) diff --git a/Editor/GsplatImporter.cs b/Editor/GsplatImporter.cs index 212fc67..5defa8e 100644 --- a/Editor/GsplatImporter.cs +++ b/Editor/GsplatImporter.cs @@ -15,6 +15,12 @@ public class GsplatImporter : ScriptedImporter { public CompressionMode Compression = CompressionMode.Spark; + [Tooltip("Removes splats whose opacity (after sigmoid) is below this value at import time. 0 disables pruning.\n" + + "Start around 0.01–0.05 and increase while checking the visual result. Reduces overdraw, memory usage, and load time.\n" + + "Currently only supported for .ply import.")] + [Range(0f, 0.99f)] + public float OpacityPruneThreshold = 0f; + [Tooltip("The coordinate frame the source asset was authored in.\n\n" + "Positions, rotations, and SH coefficients are converted to Unity (RUF) at import time.\n\n" + "RUB — standard output of 3DGS training tools, gsplat, nerfstudio, and Niantic SPZ.\n" + @@ -48,13 +54,17 @@ public override void OnImportAsset(AssetImportContext ctx) ProgressCallback progress = (info, p) => EditorUtility.DisplayProgressBar( "Importing Gsplat Asset", info, p); + if (isSpz && OpacityPruneThreshold > 0f) + UnityEngine.Debug.LogWarning( + $"[Gsplat Import] {ctx.assetPath}: OpacityPruneThreshold is currently only supported for .ply import (ignored for .spz)"); + if (gsplatAsset is GsplatAssetSpzUncompressed spzUncompressedAsset) { spzTimings = spzUncompressedAsset.LoadFromSpz(ctx.assetPath, SourceCoordinates, progress); } else if (gsplatAsset is GsplatAssetSpz spzAsset) { - string cachePath = GetCachePath(ctx.assetPath, Compression, SourceCoordinates); + string cachePath = GetCachePath(ctx.assetPath, Compression, SourceCoordinates, OpacityPruneThreshold); if (!spzAsset.TryLoadFromCache(cachePath)) { spzTimings = spzAsset.LoadFromSpz(ctx.assetPath, SourceCoordinates, progress); @@ -70,7 +80,7 @@ public override void OnImportAsset(AssetImportContext ctx) } else { - gsplatAsset.LoadFromPly(ctx.assetPath, progress, SourceCoordinates); + gsplatAsset.LoadFromPly(ctx.assetPath, progress, SourceCoordinates, OpacityPruneThreshold); } } catch (Exception e) @@ -89,6 +99,15 @@ public override void OnImportAsset(AssetImportContext ctx) swTotal?.Stop(); } + if (gsplatAsset.PrunedSplatCount > 0) + { + var removed = gsplatAsset.PrunedSplatCount; + var sourceSplatCount = gsplatAsset.SplatCount + gsplatAsset.PrunedSplatCount; + UnityEngine.Debug.Log( + $"[Gsplat Import] {Path.GetFileName(ctx.assetPath)}: pruned {removed:N0} / {sourceSplatCount:N0} splats " + + $"({removed * 100.0 / sourceSplatCount:F1}%) below opacity {OpacityPruneThreshold}"); + } + ctx.AddObjectToAsset("gsplatAsset", gsplatAsset); ctx.SetMainObject(gsplatAsset); @@ -97,13 +116,16 @@ public override void OnImportAsset(AssetImportContext ctx) #endif } - // Cache key combines filename, file size, last-write time, compression mode, and - // source coordinate frame so any change in source file or import settings busts the cache. - static string GetCachePath(string assetPath, CompressionMode compression, SourceCoordinates sourceCoordinates) + // Cache key combines filename, file size, last-write time, compression mode, source + // coordinate frame, and prune threshold so any change in source file or import settings busts the cache. + static string GetCachePath(string assetPath, CompressionMode compression, SourceCoordinates sourceCoordinates, + float opacityPruneThreshold) { var fi = new FileInfo(assetPath); string stem = Path.GetFileNameWithoutExtension(assetPath); - string key = $"{stem}_{fi.Length}_{fi.LastWriteTimeUtc.Ticks}_{compression}_{sourceCoordinates}"; + string key = + $"{stem}_{fi.Length}_{fi.LastWriteTimeUtc.Ticks}_{compression}_{sourceCoordinates}_" + + opacityPruneThreshold.ToString("R", System.Globalization.CultureInfo.InvariantCulture); key = string.Join("_", key.Split(Path.GetInvalidFileNameChars())); return Path.Combine("Library", "GsplatCache", key + ".bin"); } diff --git a/Editor/GsplatImporterEditor.cs b/Editor/GsplatImporterEditor.cs index b6b970f..b90ce2d 100644 --- a/Editor/GsplatImporterEditor.cs +++ b/Editor/GsplatImporterEditor.cs @@ -1,6 +1,7 @@ // Copyright (c) 2025 Niantic Spatial // SPDX-License-Identifier: MIT +using System.IO; using UnityEditor; using UnityEditor.AssetImporters; @@ -16,8 +17,14 @@ public override void OnInspectorGUI() EditorGUILayout.PropertyField(serializedObject.FindProperty("Compression")); EditorGUILayout.PropertyField(serializedObject.FindProperty("SourceCoordinates")); + var importer = target as GsplatImporter; + var assetPath = importer ? importer.assetPath : string.Empty; + var ext = Path.GetExtension(assetPath).ToLowerInvariant(); + if (ext == ".ply") + EditorGUILayout.PropertyField(serializedObject.FindProperty("OpacityPruneThreshold")); + serializedObject.ApplyModifiedProperties(); ApplyRevertGUI(); } } -} +} \ No newline at end of file diff --git a/README.md b/README.md index 69e4ccb..4970ec6 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ The next steps depend on the Render Pipeline you are using: Copy or drag & drop the PLY file anywhere into your project's `Assets` folder. The package will then automatically read the file and import it as a derived class of `Gsplat Asset`. The package supports two compression modes for the asset: `Uncompressed` and `Spark` (packed). The default mode is `Spark`, which is inspired by [spark.js](https://github.com/sparkjsdev/spark). You can change the compression mode in the inspector of the imported `Gsplat Asset`. +The `Opacity Prune Threshold` import option removes splats whose opacity (after sigmoid) is below the threshold at import time. Since overdraw of many overlapping translucent splats is the main rendering cost of Gaussian Splatting, pruning nearly transparent splats can significantly improve frame rate, and also reduces memory usage and load time. Set the slider in the inspector of the imported asset and press `Apply` to reimport; the Console then logs how many splats were pruned (e.g. `[Gsplat Import] scene.ply: pruned N / M splats (x.x%)`). Start around `0.01`–`0.05` and increase while checking the visual result (thin fog-like details disappear first — as a reference, a threshold of `0.05` removed about 40% of the splats on one of our scans with little visible change). Setting it back to `0` and reimporting fully restores the original data. Currently only supported for `.ply` import. + ### Add Gsplat Renderer Create or choose a game object in your scene, and add the `Gsplat Renderer` component on it. Point the `Gsplat Asset` field to one of your imported Gsplat Assets. Then it should appear in the viewport. diff --git a/Runtime/GsplatAsset.cs b/Runtime/GsplatAsset.cs index fdd445e..357d60e 100644 --- a/Runtime/GsplatAsset.cs +++ b/Runtime/GsplatAsset.cs @@ -140,6 +140,8 @@ public PlyHeaderInfo(Stream fs) public abstract class GsplatAsset : ScriptableObject { public uint SplatCount; + // Pruned splat count during import time; SplatCount + PrunedSplatCount = splat count of the source file. + public uint PrunedSplatCount; public byte SHBands; // 0, 1, 2, 3, or 4 public Bounds Bounds; public abstract CompressionMode Compression { get; } @@ -154,10 +156,10 @@ public abstract class GsplatAsset : ScriptableObject public abstract void Allocate(); public abstract void LoadFromPly(string plyPath, ProgressCallback progressCallback = null, - SourceCoordinates sourceCoordinates = SourceCoordinates.RUF); + SourceCoordinates sourceCoordinates = SourceCoordinates.RUF, float opacityPruneThreshold = 0f); public virtual void LoadFromPlyBytes(byte[] plyBytes, ProgressCallback progressCallback = null, - SourceCoordinates sourceCoordinates = SourceCoordinates.RUF) + SourceCoordinates sourceCoordinates = SourceCoordinates.RUF, float opacityPruneThreshold = 0f) => throw new NotSupportedException($"{GetType().Name} does not support loading PLY from bytes."); public abstract GsplatResource CreateResource(); diff --git a/Runtime/GsplatAssetSpark.cs b/Runtime/GsplatAssetSpark.cs index 303da06..0797c94 100644 --- a/Runtime/GsplatAssetSpark.cs +++ b/Runtime/GsplatAssetSpark.cs @@ -163,30 +163,32 @@ public override void InitOrder(ISorterResource sorterResource, GsplatResource re } public override void LoadFromPly(string plyPath, ProgressCallback progressCallback = null, - SourceCoordinates sourceCoordinates = SourceCoordinates.RUF) + SourceCoordinates sourceCoordinates = SourceCoordinates.RUF, float opacityPruneThreshold = 0f) { using var fs = new FileStream(plyPath, FileMode.Open, FileAccess.Read); // C# arrays and NativeArrays make it hard to have a "byte" array larger than 2GB :/ if (fs.Length >= 2 * 1024 * 1024 * 1024L) throw new NotSupportedException("currently files larger than 2GB are not supported"); - LoadFromPlyStream(fs, progressCallback, sourceCoordinates); + LoadFromPlyStream(fs, progressCallback, sourceCoordinates, opacityPruneThreshold); } public override void LoadFromPlyBytes(byte[] plyBytes, ProgressCallback progressCallback = null, - SourceCoordinates sourceCoordinates = SourceCoordinates.RUF) + SourceCoordinates sourceCoordinates = SourceCoordinates.RUF, float opacityPruneThreshold = 0f) { if (plyBytes == null || plyBytes.Length == 0) throw new ArgumentException("PLY byte array is null or empty.", nameof(plyBytes)); using var ms = new MemoryStream(plyBytes, writable: false); - LoadFromPlyStream(ms, progressCallback, sourceCoordinates); + LoadFromPlyStream(ms, progressCallback, sourceCoordinates, opacityPruneThreshold); } - void LoadFromPlyStream(Stream fs, ProgressCallback progressCallback, SourceCoordinates sourceCoordinates) + void LoadFromPlyStream(Stream fs, ProgressCallback progressCallback, SourceCoordinates sourceCoordinates, + float opacityPruneThreshold) { var plyInfo = new PlyHeaderInfo(fs); var shCoeffs = plyInfo.SHPropertyCount / 3; SplatCount = plyInfo.VertexCount; + var sourceSplatCount = plyInfo.VertexCount; SHBands = GsplatUtils.CalcSHBandsFromSHPropertyCount(plyInfo.SHPropertyCount); if (SHBands > 4 || GsplatUtils.SHBandsToCoefficientCount(SHBands) * 3 != plyInfo.SHPropertyCount) @@ -206,6 +208,7 @@ void LoadFromPlyStream(Stream fs, ProgressCallback progressCallback, SourceCoord Allocate(); var buffer = new byte[plyInfo.PropertyCount * sizeof(float)]; var shBandData = new float[9 * 3]; // max band 4: 9 coeffs × 3 channels; reused each splat + uint w = 0; // write index; splats below the prune threshold are skipped for (uint i = 0; i < plyInfo.VertexCount; i++) { var readBytes = fs.Read(buffer); @@ -213,6 +216,10 @@ void LoadFromPlyStream(Stream fs, ProgressCallback progressCallback, SourceCoord throw new EndOfStreamException($"unexpected end of file, got {readBytes} bytes at vertex {i}"); var properties = MemoryMarshal.Cast(buffer); + progressCallback?.Invoke("Reading vertices", i / (float)plyInfo.VertexCount); + + if (GsplatUtils.Sigmoid(properties[plyInfo.OpacityOffset]) < opacityPruneThreshold) + continue; for (int j = 1, shReadOffset = 0; j <= SHBands; j++) { @@ -225,10 +232,10 @@ void LoadFromPlyStream(Stream fs, ProgressCallback progressCallback, SourceCoord shBandData[k * 3 + 2] = sign * properties[shReadOffset + k + plyInfo.SHOffset + shCoeffs * 2]; } - if (j == 1) PackSH1(shBandData, PackedSH1.AsSpan((int)i * 2, 2)); - if (j == 2) PackSH2(shBandData, PackedSH2.AsSpan((int)i * 4, 4)); - if (j == 3) PackSH3(shBandData, PackedSH3.AsSpan((int)i * 4, 4)); - if (j == 4) PackSH4(shBandData, PackedSH4.AsSpan((int)i * 4, 4)); + if (j == 1) PackSH1(shBandData, PackedSH1.AsSpan((int)w * 2, 2)); + if (j == 2) PackSH2(shBandData, PackedSH2.AsSpan((int)w * 4, 4)); + if (j == 3) PackSH3(shBandData, PackedSH3.AsSpan((int)w * 4, 4)); + if (j == 4) PackSH4(shBandData, PackedSH4.AsSpan((int)w * 4, 4)); shReadOffset += bandSize; } @@ -244,7 +251,7 @@ void LoadFromPlyStream(Stream fs, ProgressCallback progressCallback, SourceCoord posYSign * properties[plyInfo.PositionOffset + 1], posZSign * properties[plyInfo.PositionOffset + 2]); - if (i == 0) Bounds = new Bounds(position, Vector3.zero); + if (w == 0) Bounds = new Bounds(position, Vector3.zero); else Bounds.Encapsulate(position); var scale = new Vector3( @@ -258,8 +265,27 @@ void LoadFromPlyStream(Stream fs, ProgressCallback progressCallback, SourceCoord rotYSign * properties[plyInfo.RotationOffset + 2], rotZSign * properties[plyInfo.RotationOffset + 3]); - PackedSplats[i] = PackSplat(color, position, scale, rotation); - progressCallback?.Invoke("Reading vertices", i / (float)plyInfo.VertexCount); + PackedSplats[w] = PackSplat(color, position, scale, rotation); + w++; + } + + if (w == 0 && plyInfo.VertexCount > 0) + throw new NotSupportedException( + $"opacity prune threshold {opacityPruneThreshold} removed all {plyInfo.VertexCount} splats"); + + if (w != SplatCount) + { + SplatCount = w; + PrunedSplatCount = sourceSplatCount - SplatCount; + Array.Resize(ref PackedSplats, (int)w); + if (SHBands >= 1) + Array.Resize(ref PackedSH1, (int)w * 2); + if (SHBands >= 2) + Array.Resize(ref PackedSH2, (int)w * 4); + if (SHBands >= 3) + Array.Resize(ref PackedSH3, (int)w * 4); + if (SHBands >= 4) + Array.Resize(ref PackedSH4, (int)w * 4); } } diff --git a/Runtime/GsplatAssetSpz.cs b/Runtime/GsplatAssetSpz.cs index 729e9f3..8353f0c 100644 --- a/Runtime/GsplatAssetSpz.cs +++ b/Runtime/GsplatAssetSpz.cs @@ -52,11 +52,11 @@ public DecodeContext(SpzData data, SourceCoordinates srcCoords, int shBands) } public override void LoadFromPly(string plyPath, ProgressCallback progressCallback = null, - SourceCoordinates sourceCoordinates = SourceCoordinates.RUF) + SourceCoordinates sourceCoordinates = SourceCoordinates.RUF, float opacityPruneThreshold = 0f) => throw new NotSupportedException("GsplatAssetSpz loads SPZ files, not PLY."); public override void LoadFromPlyBytes(byte[] plyBytes, ProgressCallback progressCallback = null, - SourceCoordinates sourceCoordinates = SourceCoordinates.RUF) + SourceCoordinates sourceCoordinates = SourceCoordinates.RUF, float opacityPruneThreshold = 0f) => throw new NotSupportedException("GsplatAssetSpz loads SPZ files, not PLY."); public SpzPhaseTimings LoadFromSpz(string spzPath, diff --git a/Runtime/GsplatAssetSpzUncompressed.cs b/Runtime/GsplatAssetSpzUncompressed.cs index 6c4303d..df34143 100644 --- a/Runtime/GsplatAssetSpzUncompressed.cs +++ b/Runtime/GsplatAssetSpzUncompressed.cs @@ -10,11 +10,11 @@ namespace Gsplat public class GsplatAssetSpzUncompressed : GsplatAssetUncompressed { public override void LoadFromPly(string plyPath, ProgressCallback progressCallback = null, - SourceCoordinates sourceCoordinates = SourceCoordinates.RUF) + SourceCoordinates sourceCoordinates = SourceCoordinates.RUF, float opacityPruneThreshold = 0f) => throw new System.NotSupportedException("GsplatAssetSpzUncompressed loads SPZ files, not PLY."); public override void LoadFromPlyBytes(byte[] plyBytes, ProgressCallback progressCallback = null, - SourceCoordinates sourceCoordinates = SourceCoordinates.RUF) + SourceCoordinates sourceCoordinates = SourceCoordinates.RUF, float opacityPruneThreshold = 0f) => throw new System.NotSupportedException("GsplatAssetSpzUncompressed loads SPZ files, not PLY."); public SpzPhaseTimings LoadFromSpz(string spzPath, diff --git a/Runtime/GsplatAssetUncompressed.cs b/Runtime/GsplatAssetUncompressed.cs index 0a6ae74..0e59659 100644 --- a/Runtime/GsplatAssetUncompressed.cs +++ b/Runtime/GsplatAssetUncompressed.cs @@ -124,30 +124,32 @@ public override void InitOrder(ISorterResource sorterResource, GsplatResource re } public override void LoadFromPly(string plyPath, ProgressCallback progressCallback = null, - SourceCoordinates sourceCoordinates = SourceCoordinates.RUF) + SourceCoordinates sourceCoordinates = SourceCoordinates.RUF, float opacityPruneThreshold = 0f) { using var fs = new FileStream(plyPath, FileMode.Open, FileAccess.Read); // C# arrays and NativeArrays make it hard to have a "byte" array larger than 2GB :/ if (fs.Length >= 2 * 1024 * 1024 * 1024L) throw new NotSupportedException("currently files larger than 2GB are not supported"); - LoadFromPlyStream(fs, progressCallback, sourceCoordinates); + LoadFromPlyStream(fs, progressCallback, sourceCoordinates, opacityPruneThreshold); } public override void LoadFromPlyBytes(byte[] plyBytes, ProgressCallback progressCallback = null, - SourceCoordinates sourceCoordinates = SourceCoordinates.RUF) + SourceCoordinates sourceCoordinates = SourceCoordinates.RUF, float opacityPruneThreshold = 0f) { if (plyBytes == null || plyBytes.Length == 0) throw new ArgumentException("PLY byte array is null or empty.", nameof(plyBytes)); using var ms = new MemoryStream(plyBytes, writable: false); - LoadFromPlyStream(ms, progressCallback, sourceCoordinates); + LoadFromPlyStream(ms, progressCallback, sourceCoordinates, opacityPruneThreshold); } - void LoadFromPlyStream(Stream fs, ProgressCallback progressCallback, SourceCoordinates sourceCoordinates) + void LoadFromPlyStream(Stream fs, ProgressCallback progressCallback, SourceCoordinates sourceCoordinates, + float opacityPruneThreshold) { var plyInfo = new PlyHeaderInfo(fs); var shCoeffs = plyInfo.SHPropertyCount / 3; SplatCount = plyInfo.VertexCount; + var sourceSplatCount = plyInfo.VertexCount; SHBands = GsplatUtils.CalcSHBandsFromSHPropertyCount(plyInfo.SHPropertyCount); if (SHBands > 4 || GsplatUtils.SHBandsToCoefficientCount(SHBands) * 3 != plyInfo.SHPropertyCount) @@ -164,6 +166,7 @@ void LoadFromPlyStream(Stream fs, ProgressCallback progressCallback, SourceCoord Allocate(); var buffer = new byte[plyInfo.PropertyCount * sizeof(float)]; + uint w = 0; // write index; splats below the prune threshold are skipped for (uint i = 0; i < plyInfo.VertexCount; i++) { var readBytes = fs.Read(buffer); @@ -171,15 +174,21 @@ void LoadFromPlyStream(Stream fs, ProgressCallback progressCallback, SourceCoord throw new EndOfStreamException($"unexpected end of file, got {readBytes} bytes at vertex {i}"); var properties = MemoryMarshal.Cast(buffer); - Positions[i] = new Vector3( + progressCallback?.Invoke("Reading vertices", i / (float)plyInfo.VertexCount); + + var alpha = GsplatUtils.Sigmoid(properties[plyInfo.OpacityOffset]); + if (alpha < opacityPruneThreshold) + continue; + + Positions[w] = new Vector3( posXSign * properties[plyInfo.PositionOffset], posYSign * properties[plyInfo.PositionOffset + 1], posZSign * properties[plyInfo.PositionOffset + 2]); - Colors[i] = new Vector4( + Colors[w] = new Vector4( properties[plyInfo.ColorOffset], properties[plyInfo.ColorOffset + 1], properties[plyInfo.ColorOffset + 2], - GsplatUtils.Sigmoid(properties[plyInfo.OpacityOffset])); + alpha); for (int j = 0, bandOffset = 0; j < SHBands; j++) { @@ -187,7 +196,7 @@ void LoadFromPlyStream(Stream fs, ProgressCallback progressCallback, SourceCoord for (int k = 0; k < bandSize; k++) { float sign = GsplatUtils.ShSign(sourceCoordinates, j + 1, k); - int idx = (int)i * shCoeffs + bandOffset + k; + int idx = (int)w * shCoeffs + bandOffset + k; SHs[idx] = sign * new Vector3( properties[bandOffset + k + plyInfo.SHOffset], properties[bandOffset + k + plyInfo.SHOffset + shCoeffs], @@ -196,20 +205,35 @@ void LoadFromPlyStream(Stream fs, ProgressCallback progressCallback, SourceCoord bandOffset += bandSize; } - Scales[i] = new Vector3( + Scales[w] = new Vector3( Mathf.Exp(properties[plyInfo.ScaleOffset]), Mathf.Exp(properties[plyInfo.ScaleOffset + 1]), Mathf.Exp(properties[plyInfo.ScaleOffset + 2])); - Rotations[i] = new Vector4( + Rotations[w] = new Vector4( properties[plyInfo.RotationOffset], rotXSign * properties[plyInfo.RotationOffset + 1], rotYSign * properties[plyInfo.RotationOffset + 2], rotZSign * properties[plyInfo.RotationOffset + 3]).normalized; - if (i == 0) Bounds = new Bounds(Positions[i], Vector3.zero); - else Bounds.Encapsulate(Positions[i]); + if (w == 0) Bounds = new Bounds(Positions[w], Vector3.zero); + else Bounds.Encapsulate(Positions[w]); + w++; + } + + if (w == 0 && plyInfo.VertexCount > 0) + throw new NotSupportedException( + $"opacity prune threshold {opacityPruneThreshold} removed all {plyInfo.VertexCount} splats"); - progressCallback?.Invoke("Reading vertices", i / (float)plyInfo.VertexCount); + if (w != SplatCount) + { + SplatCount = w; + PrunedSplatCount = sourceSplatCount - SplatCount; + Array.Resize(ref Positions, (int)w); + Array.Resize(ref Colors, (int)w); + Array.Resize(ref Scales, (int)w); + Array.Resize(ref Rotations, (int)w); + if (SHBands > 0) + Array.Resize(ref SHs, (int)w * shCoeffs); } } } diff --git a/Runtime/RuntimePlyBytesLoaderTest.cs b/Runtime/RuntimePlyBytesLoaderTest.cs index 835e774..7cb48ce 100644 --- a/Runtime/RuntimePlyBytesLoaderTest.cs +++ b/Runtime/RuntimePlyBytesLoaderTest.cs @@ -17,6 +17,10 @@ public class RuntimePlyBytesLoaderTest : MonoBehaviour public CompressionMode Compression = CompressionMode.Spark; public SourceCoordinates SourceCoordinates = SourceCoordinates.RUB; + [Tooltip("Removes splats whose opacity (after sigmoid) is below this value while loading. 0 disables pruning.")] + [Range(0f, 0.99f)] + public float OpacityPruneThreshold = 0f; + void Start() { var path = Path.IsPathRooted(PlyPath) @@ -34,11 +38,17 @@ void Start() GsplatAsset asset = Compression == CompressionMode.Spark ? ScriptableObject.CreateInstance() : ScriptableObject.CreateInstance(); - asset.LoadFromPlyBytes(bytes, null, SourceCoordinates); + asset.LoadFromPlyBytes(bytes, null, SourceCoordinates, OpacityPruneThreshold); GetComponent().GsplatAsset = asset; - Debug.Log($"Loaded {asset.SplatCount} splats (SH bands {asset.SHBands}) " + - $"from {bytes.Length} bytes via LoadFromPlyBytes"); + var pruned = asset.PrunedSplatCount; + var sourceSplatCount = asset.SplatCount + asset.PrunedSplatCount; + Debug.Log($"Loaded {asset.SplatCount:N0} splats (SH bands {asset.SHBands}) " + + $"from {bytes.Length:N0} bytes via LoadFromPlyBytes" + + (pruned > 0 + ? $", pruned {pruned:N0} / {sourceSplatCount:N0} splats " + + $"({pruned * 100.0 / sourceSplatCount:F1}%) below opacity {OpacityPruneThreshold}" + : "")); } } -} +} \ No newline at end of file