chore: sync workspace state

This commit is contained in:
2026-03-29 01:36:53 +08:00
parent eb5de3e3d4
commit e5cb79f3ce
4935 changed files with 35593 additions and 360696 deletions

View File

@@ -0,0 +1,35 @@
// SPDX-License-Identifier: MIT
using GaussianSplatting.Runtime;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityEngine;
namespace GaussianSplatting.Editor
{
[EditorTool("Gaussian Move Tool", typeof(GaussianSplatRenderer), typeof(GaussianToolContext))]
class GaussianMoveTool : GaussianTool
{
public override void OnToolGUI(EditorWindow window)
{
var gs = GetRenderer();
if (!gs || !CanBeEdited() || !HasSelection())
return;
var tr = gs.transform;
EditorGUI.BeginChangeCheck();
var selCenterLocal = GetSelectionCenterLocal();
var selCenterWorld = tr.TransformPoint(selCenterLocal);
var newPosWorld = Handles.DoPositionHandle(selCenterWorld, Tools.handleRotation);
if (EditorGUI.EndChangeCheck())
{
var newPosLocal = tr.InverseTransformPoint(newPosWorld);
var wasModified = gs.editModified;
gs.EditTranslateSelection(newPosLocal - selCenterLocal);
if (!wasModified)
GaussianSplatRendererEditor.RepaintAll();
Event.current.Use();
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9c9f40b54eb504648b2a0beadabbcc8d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,70 @@
// SPDX-License-Identifier: MIT
using GaussianSplatting.Runtime;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityEngine;
namespace GaussianSplatting.Editor
{
/* not working correctly yet
[EditorTool("Gaussian Rotate Tool", typeof(GaussianSplatRenderer), typeof(GaussianToolContext))]
class GaussianRotateTool : GaussianTool
{
Quaternion m_CurrentRotation = Quaternion.identity;
Vector3 m_FrozenSelCenterLocal = Vector3.zero;
bool m_FreezePivot = false;
public override void OnActivated()
{
m_FreezePivot = false;
}
public override void OnToolGUI(EditorWindow window)
{
var gs = GetRenderer();
if (!gs || !CanBeEdited() || !HasSelection())
return;
var tr = gs.transform;
var evt = Event.current;
var selCenterLocal = GetSelectionCenterLocal();
if (evt.type == EventType.MouseDown)
{
gs.EditStorePosMouseDown();
gs.EditStoreOtherMouseDown();
m_FrozenSelCenterLocal = selCenterLocal;
m_FreezePivot = true;
}
if (evt.type == EventType.MouseUp)
{
m_CurrentRotation = Quaternion.identity;
m_FreezePivot = false;
}
if (m_FreezePivot)
selCenterLocal = m_FrozenSelCenterLocal;
EditorGUI.BeginChangeCheck();
var selCenterWorld = tr.TransformPoint(selCenterLocal);
var newRotation = Handles.DoRotationHandle(m_CurrentRotation, selCenterWorld);
if (EditorGUI.EndChangeCheck())
{
Matrix4x4 localToWorld = gs.transform.localToWorldMatrix;
Matrix4x4 worldToLocal = gs.transform.worldToLocalMatrix;
var wasModified = gs.editModified;
var rotToApply = newRotation;
gs.EditRotateSelection(selCenterLocal, localToWorld, worldToLocal, rotToApply);
m_CurrentRotation = newRotation;
if (!wasModified)
GaussianSplatRendererEditor.RepaintAll();
if(GUIUtility.hotControl == 0)
{
m_CurrentRotation = Tools.handleRotation;
}
}
}
}
*/
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5128238188a44c86914a22a862195242
timeCreated: 1697805149

View File

@@ -0,0 +1,68 @@
// SPDX-License-Identifier: MIT
using GaussianSplatting.Runtime;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityEngine;
namespace GaussianSplatting.Editor
{
/* // not working correctly yet when the GS itself has scale
[EditorTool("Gaussian Scale Tool", typeof(GaussianSplatRenderer), typeof(GaussianToolContext))]
class GaussianScaleTool : GaussianTool
{
Vector3 m_CurrentScale = Vector3.one;
Vector3 m_FrozenSelCenterLocal = Vector3.zero;
bool m_FreezePivot = false;
public override void OnActivated()
{
m_FreezePivot = false;
}
public override void OnToolGUI(EditorWindow window)
{
var gs = GetRenderer();
if (!gs || !CanBeEdited() || !HasSelection())
return;
var tr = gs.transform;
var evt = Event.current;
var selCenterLocal = GetSelectionCenterLocal();
if (evt.type == EventType.MouseDown)
{
gs.EditStorePosMouseDown();
m_FrozenSelCenterLocal = selCenterLocal;
m_FreezePivot = true;
}
if (evt.type == EventType.MouseUp)
{
m_CurrentScale = Vector3.one;
m_FreezePivot = false;
}
if (m_FreezePivot)
selCenterLocal = m_FrozenSelCenterLocal;
EditorGUI.BeginChangeCheck();
var selCenterWorld = tr.TransformPoint(selCenterLocal);
m_CurrentScale = Handles.DoScaleHandle(m_CurrentScale, selCenterWorld, Tools.handleRotation, HandleUtility.GetHandleSize(selCenterWorld));
if (EditorGUI.EndChangeCheck())
{
Matrix4x4 localToWorld = Matrix4x4.identity;
Matrix4x4 worldToLocal = Matrix4x4.identity;
if (Tools.pivotRotation == PivotRotation.Global)
{
localToWorld = gs.transform.localToWorldMatrix;
worldToLocal = gs.transform.worldToLocalMatrix;
}
var wasModified = gs.editModified;
gs.EditScaleSelection(selCenterLocal, localToWorld, worldToLocal, m_CurrentScale);
if (!wasModified)
GaussianSplatRendererEditor.RepaintAll();
evt.Use();
}
}
}
*/
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: dbf3d17a31b942b28f5d8c187adb8fdf
timeCreated: 1697732813

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 635bd950b8a74c84f870d5c8f02c3974
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,71 @@
// SPDX-License-Identifier: MIT
using GaussianSplatting.Runtime;
using Unity.Collections.LowLevel.Unsafe;
using UnityEditor;
using UnityEngine;
namespace GaussianSplatting.Editor
{
[CustomEditor(typeof(GaussianSplatAsset))]
[CanEditMultipleObjects]
public class GaussianSplatAssetEditor : UnityEditor.Editor
{
public override void OnInspectorGUI()
{
var gs = target as GaussianSplatAsset;
if (!gs)
return;
using var _ = new EditorGUI.DisabledScope(true);
if (targets.Length == 1)
SingleAssetGUI(gs);
else
{
int totalCount = 0;
foreach (var tgt in targets)
{
var gss = tgt as GaussianSplatAsset;
if (gss)
{
totalCount += gss.splatCount;
}
}
EditorGUILayout.TextField("Total Splats", $"{totalCount:N0}");
}
}
static void SingleAssetGUI(GaussianSplatAsset gs)
{
var splatCount = gs.splatCount;
EditorGUILayout.TextField("Splats", $"{splatCount:N0}");
var prevBackColor = GUI.backgroundColor;
if (gs.formatVersion != GaussianSplatAsset.kCurrentVersion)
GUI.backgroundColor *= Color.red;
EditorGUILayout.IntField("Version", gs.formatVersion);
GUI.backgroundColor = prevBackColor;
long sizePos = gs.posData != null ? gs.posData.dataSize : 0;
long sizeOther = gs.otherData != null ? gs.otherData.dataSize : 0;
long sizeCol = gs.colorData != null ? gs.colorData.dataSize : 0;
long sizeSH = GaussianSplatAsset.CalcSHDataSize(gs.splatCount, gs.shFormat);
long sizeChunk = gs.chunkData != null ? gs.chunkData.dataSize : 0;
EditorGUILayout.TextField("Memory", EditorUtility.FormatBytes(sizePos + sizeOther + sizeSH + sizeCol + sizeChunk));
EditorGUI.indentLevel++;
EditorGUILayout.TextField("Positions", $"{EditorUtility.FormatBytes(sizePos)} ({gs.posFormat})");
EditorGUILayout.TextField("Other", $"{EditorUtility.FormatBytes(sizeOther)} ({gs.scaleFormat})");
EditorGUILayout.TextField("Base color", $"{EditorUtility.FormatBytes(sizeCol)} ({gs.colorFormat})");
EditorGUILayout.TextField("SHs", $"{EditorUtility.FormatBytes(sizeSH)} ({gs.shFormat})");
EditorGUILayout.TextField("Chunks",
$"{EditorUtility.FormatBytes(sizeChunk)} ({UnsafeUtility.SizeOf<GaussianSplatAsset.ChunkInfo>()} B/chunk)");
EditorGUI.indentLevel--;
EditorGUILayout.Vector3Field("Bounds Min", gs.boundsMin);
EditorGUILayout.Vector3Field("Bounds Max", gs.boundsMax);
EditorGUILayout.TextField("Data Hash", gs.dataHash.ToString());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 75971a29a6deda14c9b1ff5f4ab2f2a0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,444 @@
// SPDX-License-Identifier: MIT
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using GaussianSplatting.Runtime;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Mathematics;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityEngine;
using GaussianSplatRenderer = GaussianSplatting.Runtime.GaussianSplatRenderer;
namespace GaussianSplatting.Editor
{
[CustomEditor(typeof(GaussianSplatRenderer))]
[CanEditMultipleObjects]
public class GaussianSplatRendererEditor : UnityEditor.Editor
{
const string kPrefExportBake = "nesnausk.GaussianSplatting.ExportBakeTransform";
SerializedProperty m_PropAsset;
SerializedProperty m_PropSplatScale;
SerializedProperty m_PropOpacityScale;
SerializedProperty m_PropSHOrder;
SerializedProperty m_PropSHOnly;
SerializedProperty m_PropSortNthFrame;
SerializedProperty m_PropRenderMode;
SerializedProperty m_PropPointDisplaySize;
SerializedProperty m_PropCutouts;
SerializedProperty m_PropShaderSplats;
SerializedProperty m_PropShaderComposite;
SerializedProperty m_PropShaderDebugPoints;
SerializedProperty m_PropShaderDebugBoxes;
SerializedProperty m_PropCSSplatUtilities;
bool m_ResourcesExpanded = false;
int m_CameraIndex = 0;
bool m_ExportBakeTransform;
static int s_EditStatsUpdateCounter = 0;
static HashSet<GaussianSplatRendererEditor> s_AllEditors = new();
public static void BumpGUICounter()
{
++s_EditStatsUpdateCounter;
}
public static void RepaintAll()
{
foreach (var e in s_AllEditors)
e.Repaint();
}
public void OnEnable()
{
m_ExportBakeTransform = EditorPrefs.GetBool(kPrefExportBake, false);
m_PropAsset = serializedObject.FindProperty("m_Asset");
m_PropSplatScale = serializedObject.FindProperty("m_SplatScale");
m_PropOpacityScale = serializedObject.FindProperty("m_OpacityScale");
m_PropSHOrder = serializedObject.FindProperty("m_SHOrder");
m_PropSHOnly = serializedObject.FindProperty("m_SHOnly");
m_PropSortNthFrame = serializedObject.FindProperty("m_SortNthFrame");
m_PropRenderMode = serializedObject.FindProperty("m_RenderMode");
m_PropPointDisplaySize = serializedObject.FindProperty("m_PointDisplaySize");
m_PropCutouts = serializedObject.FindProperty("m_Cutouts");
m_PropShaderSplats = serializedObject.FindProperty("m_ShaderSplats");
m_PropShaderComposite = serializedObject.FindProperty("m_ShaderComposite");
m_PropShaderDebugPoints = serializedObject.FindProperty("m_ShaderDebugPoints");
m_PropShaderDebugBoxes = serializedObject.FindProperty("m_ShaderDebugBoxes");
m_PropCSSplatUtilities = serializedObject.FindProperty("m_CSSplatUtilities");
s_AllEditors.Add(this);
}
public void OnDisable()
{
s_AllEditors.Remove(this);
}
public override void OnInspectorGUI()
{
var gs = target as GaussianSplatRenderer;
if (!gs)
return;
serializedObject.Update();
GUILayout.Label("Data Asset", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(m_PropAsset);
if (!gs.HasValidAsset)
{
var msg = gs.asset != null && gs.asset.formatVersion != GaussianSplatAsset.kCurrentVersion
? "Gaussian Splat asset version is not compatible, please recreate the asset"
: "Gaussian Splat asset is not assigned or is empty";
EditorGUILayout.HelpBox(msg, MessageType.Error);
}
EditorGUILayout.Space();
GUILayout.Label("Render Options", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(m_PropSplatScale);
EditorGUILayout.PropertyField(m_PropOpacityScale);
EditorGUILayout.PropertyField(m_PropSHOrder);
EditorGUILayout.PropertyField(m_PropSHOnly);
EditorGUILayout.PropertyField(m_PropSortNthFrame);
EditorGUILayout.Space();
GUILayout.Label("Debugging Tweaks", EditorStyles.boldLabel);
EditorGUILayout.PropertyField(m_PropRenderMode);
if (m_PropRenderMode.intValue is (int)GaussianSplatRenderer.RenderMode.DebugPoints or (int)GaussianSplatRenderer.RenderMode.DebugPointIndices)
EditorGUILayout.PropertyField(m_PropPointDisplaySize);
EditorGUILayout.Space();
m_ResourcesExpanded = EditorGUILayout.Foldout(m_ResourcesExpanded, "Resources", true, EditorStyles.foldoutHeader);
if (m_ResourcesExpanded)
{
EditorGUILayout.PropertyField(m_PropShaderSplats);
EditorGUILayout.PropertyField(m_PropShaderComposite);
EditorGUILayout.PropertyField(m_PropShaderDebugPoints);
EditorGUILayout.PropertyField(m_PropShaderDebugBoxes);
EditorGUILayout.PropertyField(m_PropCSSplatUtilities);
}
bool validAndEnabled = gs && gs.enabled && gs.gameObject.activeInHierarchy && gs.HasValidAsset;
if (validAndEnabled && !gs.HasValidRenderSetup)
{
EditorGUILayout.HelpBox("Shader resources are not set up", MessageType.Error);
validAndEnabled = false;
}
if (validAndEnabled && targets.Length == 1)
{
EditCameras(gs);
EditGUI(gs);
}
if (validAndEnabled && targets.Length > 1)
{
MultiEditGUI();
}
serializedObject.ApplyModifiedProperties();
}
void EditCameras(GaussianSplatRenderer gs)
{
var asset = gs.asset;
var cameras = asset.cameras;
if (cameras != null && cameras.Length != 0)
{
EditorGUILayout.Space();
GUILayout.Label("Cameras", EditorStyles.boldLabel);
var camIndex = EditorGUILayout.IntSlider("Camera", m_CameraIndex, 0, cameras.Length - 1);
camIndex = math.clamp(camIndex, 0, cameras.Length - 1);
if (camIndex != m_CameraIndex)
{
m_CameraIndex = camIndex;
gs.ActivateCamera(camIndex);
}
}
}
void MultiEditGUI()
{
DrawSeparator();
CountTargetSplats(out var totalSplats, out var totalObjects);
EditorGUILayout.LabelField("Total Objects", $"{totalObjects}");
EditorGUILayout.LabelField("Total Splats", $"{totalSplats:N0}");
if (totalSplats > GaussianSplatAsset.kMaxSplats)
{
EditorGUILayout.HelpBox($"Can't merge, too many splats (max. supported {GaussianSplatAsset.kMaxSplats:N0})", MessageType.Warning);
return;
}
var targetGs = (GaussianSplatRenderer) target;
if (!targetGs || !targetGs.HasValidAsset || !targetGs.isActiveAndEnabled)
{
EditorGUILayout.HelpBox($"Can't merge into {target.name} (no asset or disable)", MessageType.Warning);
return;
}
if (targetGs.asset.chunkData != null)
{
EditorGUILayout.HelpBox($"Can't merge into {target.name} (needs to use Very High quality preset)", MessageType.Warning);
return;
}
if (GUILayout.Button($"Merge into {target.name}"))
{
MergeSplatObjects();
}
}
void CountTargetSplats(out int totalSplats, out int totalObjects)
{
totalObjects = 0;
totalSplats = 0;
foreach (var obj in targets)
{
var gs = obj as GaussianSplatRenderer;
if (!gs || !gs.HasValidAsset || !gs.isActiveAndEnabled)
continue;
++totalObjects;
totalSplats += gs.splatCount;
}
}
void MergeSplatObjects()
{
CountTargetSplats(out var totalSplats, out _);
if (totalSplats > GaussianSplatAsset.kMaxSplats)
return;
var targetGs = (GaussianSplatRenderer) target;
int copyDstOffset = targetGs.splatCount;
targetGs.EditSetSplatCount(totalSplats);
foreach (var obj in targets)
{
var gs = obj as GaussianSplatRenderer;
if (!gs || !gs.HasValidAsset || !gs.isActiveAndEnabled)
continue;
if (gs == targetGs)
continue;
gs.EditCopySplatsInto(targetGs, 0, copyDstOffset, gs.splatCount);
copyDstOffset += gs.splatCount;
gs.gameObject.SetActive(false);
}
Debug.Assert(copyDstOffset == totalSplats, $"Merge count mismatch, {copyDstOffset} vs {totalSplats}");
Selection.activeObject = targetGs;
}
void EditGUI(GaussianSplatRenderer gs)
{
++s_EditStatsUpdateCounter;
DrawSeparator();
bool wasToolActive = ToolManager.activeContextType == typeof(GaussianToolContext);
GUILayout.BeginHorizontal();
bool isToolActive = GUILayout.Toggle(wasToolActive, "Edit", EditorStyles.miniButton);
using (new EditorGUI.DisabledScope(!gs.editModified))
{
if (GUILayout.Button("Reset", GUILayout.ExpandWidth(false)))
{
if (EditorUtility.DisplayDialog("Reset Splat Modifications?",
$"This will reset edits of {gs.name} to match the {gs.asset.name} asset. Continue?",
"Yes, reset", "Cancel"))
{
gs.enabled = false;
gs.enabled = true;
}
}
}
GUILayout.EndHorizontal();
if (!wasToolActive && isToolActive)
{
ToolManager.SetActiveContext<GaussianToolContext>();
if (Tools.current == Tool.View)
Tools.current = Tool.Move;
}
if (wasToolActive && !isToolActive)
{
ToolManager.SetActiveContext<GameObjectToolContext>();
}
if (isToolActive && gs.asset.chunkData != null)
{
EditorGUILayout.HelpBox("Splat move/rotate/scale tools need Very High splat quality preset", MessageType.Warning);
}
EditorGUILayout.Space();
GUILayout.BeginHorizontal();
if (GUILayout.Button("Add Cutout"))
{
GaussianCutout cutout = ObjectFactory.CreateGameObject("GSCutout", typeof(GaussianCutout)).GetComponent<GaussianCutout>();
Transform cutoutTr = cutout.transform;
cutoutTr.SetParent(gs.transform, false);
cutoutTr.localScale = (gs.asset.boundsMax - gs.asset.boundsMin) * 0.25f;
gs.m_Cutouts ??= Array.Empty<GaussianCutout>();
ArrayUtility.Add(ref gs.m_Cutouts, cutout);
gs.UpdateEditCountsAndBounds();
EditorUtility.SetDirty(gs);
Selection.activeGameObject = cutout.gameObject;
}
if (GUILayout.Button("Use All Cutouts"))
{
gs.m_Cutouts = FindObjectsByType<GaussianCutout>(FindObjectsSortMode.InstanceID);
gs.UpdateEditCountsAndBounds();
EditorUtility.SetDirty(gs);
}
if (GUILayout.Button("No Cutouts"))
{
gs.m_Cutouts = Array.Empty<GaussianCutout>();
gs.UpdateEditCountsAndBounds();
EditorUtility.SetDirty(gs);
}
GUILayout.EndHorizontal();
EditorGUILayout.PropertyField(m_PropCutouts);
bool hasCutouts = gs.m_Cutouts != null && gs.m_Cutouts.Length != 0;
bool modifiedOrHasCutouts = gs.editModified || hasCutouts;
var asset = gs.asset;
EditorGUILayout.Space();
EditorGUI.BeginChangeCheck();
m_ExportBakeTransform = EditorGUILayout.Toggle("Export in world space", m_ExportBakeTransform);
if (EditorGUI.EndChangeCheck())
{
EditorPrefs.SetBool(kPrefExportBake, m_ExportBakeTransform);
}
if (GUILayout.Button("Export PLY"))
ExportPlyFile(gs, m_ExportBakeTransform);
if (asset.posFormat > GaussianSplatAsset.VectorFormat.Norm16 ||
asset.scaleFormat > GaussianSplatAsset.VectorFormat.Norm16 ||
asset.colorFormat > GaussianSplatAsset.ColorFormat.Float16x4 ||
asset.shFormat > GaussianSplatAsset.SHFormat.Float16)
{
EditorGUILayout.HelpBox(
"It is recommended to use High or VeryHigh quality preset for editing splats, lower levels are lossy",
MessageType.Warning);
}
bool displayEditStats = isToolActive || modifiedOrHasCutouts;
EditorGUILayout.Space();
EditorGUILayout.LabelField("Splats", $"{gs.splatCount:N0}");
if (displayEditStats)
{
EditorGUILayout.LabelField("Cut", $"{gs.editCutSplats:N0}");
EditorGUILayout.LabelField("Deleted", $"{gs.editDeletedSplats:N0}");
EditorGUILayout.LabelField("Selected", $"{gs.editSelectedSplats:N0}");
if (hasCutouts)
{
if (s_EditStatsUpdateCounter > 10)
{
gs.UpdateEditCountsAndBounds();
s_EditStatsUpdateCounter = 0;
}
}
}
}
static void DrawSeparator()
{
EditorGUILayout.Space(12f, true);
GUILayout.Box(GUIContent.none, "sv_iconselector_sep", GUILayout.Height(2), GUILayout.ExpandWidth(true));
EditorGUILayout.Space();
}
bool HasFrameBounds()
{
return true;
}
Bounds OnGetFrameBounds()
{
var gs = target as GaussianSplatRenderer;
if (!gs || !gs.HasValidRenderSetup)
return new Bounds(Vector3.zero, Vector3.one);
Bounds bounds = default;
bounds.SetMinMax(gs.asset.boundsMin, gs.asset.boundsMax);
if (gs.editSelectedSplats > 0)
{
bounds = gs.editSelectedBounds;
}
bounds.extents *= 0.7f;
return TransformBounds(gs.transform, bounds);
}
public static Bounds TransformBounds(Transform tr, Bounds bounds )
{
var center = tr.TransformPoint(bounds.center);
var ext = bounds.extents;
var axisX = tr.TransformVector(ext.x, 0, 0);
var axisY = tr.TransformVector(0, ext.y, 0);
var axisZ = tr.TransformVector(0, 0, ext.z);
// sum their absolute value to get the world extents
ext.x = Mathf.Abs(axisX.x) + Mathf.Abs(axisY.x) + Mathf.Abs(axisZ.x);
ext.y = Mathf.Abs(axisX.y) + Mathf.Abs(axisY.y) + Mathf.Abs(axisZ.y);
ext.z = Mathf.Abs(axisX.z) + Mathf.Abs(axisY.z) + Mathf.Abs(axisZ.z);
return new Bounds { center = center, extents = ext };
}
static unsafe void ExportPlyFile(GaussianSplatRenderer gs, bool bakeTransform)
{
var path = EditorUtility.SaveFilePanel(
"Export Gaussian Splat PLY file", "", $"{gs.asset.name}-edit.ply", "ply");
if (string.IsNullOrWhiteSpace(path))
return;
int kSplatSize = UnsafeUtility.SizeOf<GaussianSplatAssetCreator.InputSplatData>();
using var gpuData = new GraphicsBuffer(GraphicsBuffer.Target.Structured, gs.splatCount, kSplatSize);
if (!gs.EditExportData(gpuData, bakeTransform))
return;
GaussianSplatAssetCreator.InputSplatData[] data = new GaussianSplatAssetCreator.InputSplatData[gpuData.count];
gpuData.GetData(data);
var gpuDeleted = gs.GpuEditDeleted;
uint[] deleted = new uint[gpuDeleted.count];
gpuDeleted.GetData(deleted);
// count non-deleted splats
int aliveCount = 0;
for (int i = 0; i < data.Length; ++i)
{
int wordIdx = i >> 5;
int bitIdx = i & 31;
bool isDeleted = (deleted[wordIdx] & (1u << bitIdx)) != 0;
bool isCutout = data[i].nor.sqrMagnitude > 0;
if (!isDeleted && !isCutout)
++aliveCount;
}
using FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write);
// note: this is a long string! but we don't use multiline literal because we want guaranteed LF line ending
var header = $"ply\nformat binary_little_endian 1.0\nelement vertex {aliveCount}\nproperty float x\nproperty float y\nproperty float z\nproperty float nx\nproperty float ny\nproperty float nz\nproperty float f_dc_0\nproperty float f_dc_1\nproperty float f_dc_2\nproperty float f_rest_0\nproperty float f_rest_1\nproperty float f_rest_2\nproperty float f_rest_3\nproperty float f_rest_4\nproperty float f_rest_5\nproperty float f_rest_6\nproperty float f_rest_7\nproperty float f_rest_8\nproperty float f_rest_9\nproperty float f_rest_10\nproperty float f_rest_11\nproperty float f_rest_12\nproperty float f_rest_13\nproperty float f_rest_14\nproperty float f_rest_15\nproperty float f_rest_16\nproperty float f_rest_17\nproperty float f_rest_18\nproperty float f_rest_19\nproperty float f_rest_20\nproperty float f_rest_21\nproperty float f_rest_22\nproperty float f_rest_23\nproperty float f_rest_24\nproperty float f_rest_25\nproperty float f_rest_26\nproperty float f_rest_27\nproperty float f_rest_28\nproperty float f_rest_29\nproperty float f_rest_30\nproperty float f_rest_31\nproperty float f_rest_32\nproperty float f_rest_33\nproperty float f_rest_34\nproperty float f_rest_35\nproperty float f_rest_36\nproperty float f_rest_37\nproperty float f_rest_38\nproperty float f_rest_39\nproperty float f_rest_40\nproperty float f_rest_41\nproperty float f_rest_42\nproperty float f_rest_43\nproperty float f_rest_44\nproperty float opacity\nproperty float scale_0\nproperty float scale_1\nproperty float scale_2\nproperty float rot_0\nproperty float rot_1\nproperty float rot_2\nproperty float rot_3\nend_header\n";
fs.Write(Encoding.UTF8.GetBytes(header));
for (int i = 0; i < data.Length; ++i)
{
int wordIdx = i >> 5;
int bitIdx = i & 31;
bool isDeleted = (deleted[wordIdx] & (1u << bitIdx)) != 0;
bool isCutout = data[i].nor.sqrMagnitude > 0;
if (!isDeleted && !isCutout)
{
var splat = data[i];
byte* ptr = (byte*)&splat;
fs.Write(new ReadOnlySpan<byte>(ptr, kSplatSize));
}
}
Debug.Log($"Exported PLY {path} with {aliveCount:N0} splats");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b0ce434aee9ae4ee6b1f5cd10ae7c8cb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,210 @@
// SPDX-License-Identifier: MIT
using System.IO;
using GaussianSplatting.Runtime;
using Unity.Burst;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEditor;
using UnityEngine;
using UnityEngine.Experimental.Rendering;
namespace GaussianSplatting.Editor
{
[BurstCompile]
public static class GaussianSplatValidator
{
struct RefItem
{
public string assetPath;
public int cameraIndex;
public float fov;
}
// currently on RTX 3080Ti: 43.76, 39.36, 43.50 PSNR
[MenuItem("Tools/Gaussian Splats/Debug/Validate Render against SBIR")]
public static void ValidateSBIR()
{
ValidateImpl("SBIR");
}
// currently on RTX 3080Ti: matches
[MenuItem("Tools/Gaussian Splats/Debug/Validate Render against D3D12")]
public static void ValidateD3D12()
{
ValidateImpl("D3D12");
}
static unsafe void ValidateImpl(string refPrefix)
{
var gaussians = Object.FindObjectOfType(typeof(GaussianSplatRenderer)) as GaussianSplatRenderer;
{
if (gaussians == null)
{
Debug.LogError("No GaussianSplatRenderer object found");
return;
}
}
var items = new RefItem[]
{
new() {assetPath = "bicycle", cameraIndex = 0, fov = 39.09651f},
new() {assetPath = "truck", cameraIndex = 30, fov = 50},
new() {assetPath = "garden", cameraIndex = 30, fov = 47},
};
var cam = Camera.main;
var oldAsset = gaussians.asset;
var oldCamPos = cam.transform.localPosition;
var oldCamRot = cam.transform.localRotation;
var oldCamFov = cam.fieldOfView;
for (var index = 0; index < items.Length; index++)
{
var item = items[index];
EditorUtility.DisplayProgressBar("Validating Gaussian splat rendering", item.assetPath, (float)index / items.Length);
var path = $"Assets/GaussianAssets/{item.assetPath}-point_cloud-iteration_30000-point_cloud.asset";
var gs = AssetDatabase.LoadAssetAtPath<GaussianSplatAsset>(path);
if (gs == null)
{
Debug.LogError($"Did not find asset for validation item {item.assetPath} at {path}");
continue;
}
var refImageFile = $"../../docs/RefImages/{refPrefix}_{item.assetPath}{item.cameraIndex}.png"; // use our snapshot by default
if (!File.Exists(refImageFile))
{
Debug.LogError($"Did not find reference image for validation item {item.assetPath} at {refImageFile}");
continue;
}
var compareTexture = new Texture2D(4, 4, GraphicsFormat.R8G8B8A8_SRGB, TextureCreationFlags.None);
byte[] refImageBytes = File.ReadAllBytes(refImageFile);
ImageConversion.LoadImage(compareTexture, refImageBytes, false);
int width = compareTexture.width;
int height = compareTexture.height;
var renderTarget = RenderTexture.GetTemporary(width, height, 24, GraphicsFormat.R8G8B8A8_SRGB);
cam.targetTexture = renderTarget;
cam.fieldOfView = item.fov;
var captureTexture = new Texture2D(width, height, GraphicsFormat.R8G8B8A8_SRGB, TextureCreationFlags.None);
NativeArray<Color32> diffPixels = new(width * height, Allocator.Persistent);
gaussians.m_Asset = gs;
gaussians.Update();
gaussians.ActivateCamera(item.cameraIndex);
cam.Render();
Graphics.SetRenderTarget(renderTarget);
captureTexture.ReadPixels(new Rect(0, 0, width, height), 0, 0);
NativeArray<Color32> refPixels = compareTexture.GetPixelData<Color32>(0);
NativeArray<Color32> gotPixels = captureTexture.GetPixelData<Color32>(0);
float psnr = 0, rmse = 0;
int errorsCount = 0;
DiffImagesJob difJob = new DiffImagesJob();
difJob.difPixels = diffPixels;
difJob.refPixels = refPixels;
difJob.gotPixels = gotPixels;
difJob.psnrPtr = &psnr;
difJob.rmsePtr = &rmse;
difJob.difPixCount = &errorsCount;
difJob.Schedule().Complete();
string pathDif = $"../../Shot-{refPrefix}-{item.assetPath}{item.cameraIndex}-diff.png";
string pathRef = $"../../Shot-{refPrefix}-{item.assetPath}{item.cameraIndex}-ref.png";
string pathGot = $"../../Shot-{refPrefix}-{item.assetPath}{item.cameraIndex}-got.png";
if (errorsCount > 50 || psnr < 90.0f)
{
Debug.LogWarning(
$"{refPrefix} {item.assetPath} cam {item.cameraIndex}: RMSE {rmse:F2} PSNR {psnr:F2} diff pixels {errorsCount:N0}");
NativeArray<byte> pngBytes = ImageConversion.EncodeNativeArrayToPNG(diffPixels,
GraphicsFormat.R8G8B8A8_SRGB, (uint) width, (uint) height);
File.WriteAllBytes(pathDif, pngBytes.ToArray());
pngBytes.Dispose();
pngBytes = ImageConversion.EncodeNativeArrayToPNG(refPixels, GraphicsFormat.R8G8B8A8_SRGB,
(uint) width, (uint) height);
File.WriteAllBytes(pathRef, pngBytes.ToArray());
pngBytes.Dispose();
pngBytes = ImageConversion.EncodeNativeArrayToPNG(gotPixels, GraphicsFormat.R8G8B8A8_SRGB,
(uint) width, (uint) height);
File.WriteAllBytes(pathGot, pngBytes.ToArray());
pngBytes.Dispose();
}
else
{
File.Delete(pathDif);
File.Delete(pathRef);
File.Delete(pathGot);
}
diffPixels.Dispose();
RenderTexture.ReleaseTemporary(renderTarget);
Object.DestroyImmediate(captureTexture);
Object.DestroyImmediate(compareTexture);
}
cam.targetTexture = null;
gaussians.m_Asset = oldAsset;
gaussians.Update();
cam.transform.localPosition = oldCamPos;
cam.transform.localRotation = oldCamRot;
cam.fieldOfView = oldCamFov;
EditorUtility.ClearProgressBar();
}
[BurstCompile]
struct DiffImagesJob : IJob
{
public NativeArray<Color32> refPixels;
public NativeArray<Color32> gotPixels;
public NativeArray<Color32> difPixels;
[NativeDisableUnsafePtrRestriction] public unsafe float* rmsePtr;
[NativeDisableUnsafePtrRestriction] public unsafe float* psnrPtr;
[NativeDisableUnsafePtrRestriction] public unsafe int* difPixCount;
public unsafe void Execute()
{
const int kDiffScale = 5;
const int kDiffThreshold = 3 * kDiffScale;
*difPixCount = 0;
double sumSqDif = 0;
for (int i = 0; i < refPixels.Length; ++i)
{
Color32 cref = refPixels[i];
// note: LoadImage always loads PNGs into ARGB order, so swizzle to normal RGBA
cref = new Color32(cref.g, cref.b, cref.a, 255);
refPixels[i] = cref;
Color32 cgot = gotPixels[i];
cgot.a = 255;
gotPixels[i] = cgot;
Color32 cdif = new Color32(0, 0, 0, 255);
cdif.r = (byte)math.abs(cref.r - cgot.r);
cdif.g = (byte)math.abs(cref.g - cgot.g);
cdif.b = (byte)math.abs(cref.b - cgot.b);
sumSqDif += cdif.r * cdif.r + cdif.g * cdif.g + cdif.b * cdif.b;
cdif.r = (byte)math.min(255, cdif.r * kDiffScale);
cdif.g = (byte)math.min(255, cdif.g * kDiffScale);
cdif.b = (byte)math.min(255, cdif.b * kDiffScale);
difPixels[i] = cdif;
if (cdif.r >= kDiffThreshold || cdif.g >= kDiffThreshold || cdif.b >= kDiffThreshold)
{
(*difPixCount)++;
}
}
double meanSqDif = sumSqDif / (refPixels.Length * 3);
double rmse = math.sqrt(meanSqDif);
double psnr = 20.0 * math.log10(255.0) - 10.0 * math.log10(rmse * rmse);
*rmsePtr = (float) rmse;
*psnrPtr = (float) psnr;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2f8e75b80eb181a4698f733ba59b694b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,21 @@
{
"name": "GaussianSplattingEditor",
"rootNamespace": "GaussianSplatting.Editor",
"references": [
"GUID:4b653174f8fcdcd49b4c9a6f1ca8c7c3",
"GUID:2665a8d13d1b3f18800f46e256720795",
"GUID:d8b63aba1907145bea998dd612889d6b",
"GUID:e0cd26848372d4e5c891c569017e11f1"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 14414175af4b366469db63f2efee475f
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,43 @@
// SPDX-License-Identifier: MIT
using GaussianSplatting.Runtime;
using UnityEditor.EditorTools;
using UnityEngine;
namespace GaussianSplatting.Editor
{
abstract class GaussianTool : EditorTool
{
protected GaussianSplatRenderer GetRenderer()
{
var gs = target as GaussianSplatRenderer;
if (!gs || !gs.HasValidAsset || !gs.HasValidRenderSetup)
return null;
return gs;
}
protected bool CanBeEdited()
{
var gs = GetRenderer();
if (!gs)
return false;
return gs.asset.chunkData == null; // need to be lossless / non-chunked for editing
}
protected bool HasSelection()
{
var gs = GetRenderer();
if (!gs)
return false;
return gs.editSelectedSplats > 0;
}
protected Vector3 GetSelectionCenterLocal()
{
var gs = GetRenderer();
if (!gs || gs.editSelectedSplats == 0)
return Vector3.zero;
return gs.editSelectedBounds.center;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6203c808ab9e64a4a8ff0277c5aa7669
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,192 @@
// SPDX-License-Identifier: MIT
using System;
using GaussianSplatting.Runtime;
using UnityEditor;
using UnityEditor.EditorTools;
using UnityEngine;
namespace GaussianSplatting.Editor
{
[EditorToolContext("GaussianSplats", typeof(GaussianSplatRenderer)), Icon(k_IconPath)]
class GaussianToolContext : EditorToolContext
{
const string k_IconPath = "Packages/org.nesnausk.gaussian-splatting/Editor/Icons/GaussianContext.png";
Vector2 m_MouseStartDragPos;
protected override Type GetEditorToolType(Tool tool)
{
if (tool == Tool.Move)
return typeof(GaussianMoveTool);
//if (tool == Tool.Rotate)
// return typeof(GaussianRotateTool); // not correctly working yet
//if (tool == Tool.Scale)
// return typeof(GaussianScaleTool); // not working correctly yet when the GS itself has scale
return null;
}
public override void OnWillBeDeactivated()
{
var gs = target as GaussianSplatRenderer;
if (!gs)
return;
gs.EditDeselectAll();
}
static void HandleKeyboardCommands(Event evt, GaussianSplatRenderer gs)
{
if (evt.type != EventType.ValidateCommand && evt.type != EventType.ExecuteCommand)
return;
bool execute = evt.type == EventType.ExecuteCommand;
switch (evt.commandName)
{
// ugh, EventCommandNames string constants is internal :(
case "SoftDelete":
case "Delete":
if (execute)
{
gs.EditDeleteSelected();
GaussianSplatRendererEditor.RepaintAll();
}
evt.Use();
break;
case "SelectAll":
if (execute)
{
gs.EditSelectAll();
GaussianSplatRendererEditor.RepaintAll();
}
evt.Use();
break;
case "DeselectAll":
if (execute)
{
gs.EditDeselectAll();
GaussianSplatRendererEditor.RepaintAll();
}
evt.Use();
break;
case "InvertSelection":
if (execute)
{
gs.EditInvertSelection();
GaussianSplatRendererEditor.RepaintAll();
}
evt.Use();
break;
}
}
static bool IsViewToolActive()
{
return Tools.viewToolActive || Tools.current == Tool.View || (Event.current != null && Event.current.alt);
}
public override void OnToolGUI(EditorWindow window)
{
if (!(window is SceneView sceneView))
return;
var gs = target as GaussianSplatRenderer;
if (!gs)
return;
GaussianSplatRendererEditor.BumpGUICounter();
int id = GUIUtility.GetControlID(FocusType.Passive);
Event evt = Event.current;
HandleKeyboardCommands(evt, gs);
var evtType = evt.GetTypeForControl(id);
switch (evtType)
{
case EventType.Layout:
// make this be the default tool, so that we get focus when user clicks on nothing else
HandleUtility.AddDefaultControl(id);
break;
case EventType.MouseDown:
if (IsViewToolActive())
break;
if (HandleUtility.nearestControl == id && evt.button == 0)
{
// shift/command adds to selection, ctrl removes from selection: if none of these
// are present, start a new selection
if (!evt.shift && !EditorGUI.actionKey && !evt.control)
gs.EditDeselectAll();
// record selection state at start
gs.EditStoreSelectionMouseDown();
GaussianSplatRendererEditor.RepaintAll();
GUIUtility.hotControl = id;
m_MouseStartDragPos = evt.mousePosition;
evt.Use();
}
break;
case EventType.MouseDrag:
if (GUIUtility.hotControl == id && evt.button == 0)
{
Rect rect = FromToRect(m_MouseStartDragPos, evt.mousePosition);
Vector2 rectMin = HandleUtility.GUIPointToScreenPixelCoordinate(rect.min);
Vector2 rectMax = HandleUtility.GUIPointToScreenPixelCoordinate(rect.max);
gs.EditUpdateSelection(rectMin, rectMax, sceneView.camera, evt.control);
GaussianSplatRendererEditor.RepaintAll();
evt.Use();
}
break;
case EventType.MouseUp:
if (GUIUtility.hotControl == id && evt.button == 0)
{
m_MouseStartDragPos = Vector2.zero;
GUIUtility.hotControl = 0;
evt.Use();
}
break;
case EventType.Repaint:
// draw cutout gizmos
Handles.color = new Color(1,0,1,0.7f);
var prevMatrix = Handles.matrix;
foreach (var cutout in gs.m_Cutouts)
{
if (!cutout)
continue;
Handles.matrix = cutout.transform.localToWorldMatrix;
if (cutout.m_Type == GaussianCutout.Type.Ellipsoid)
{
Handles.DrawWireDisc(Vector3.zero, Vector3.up, 1.0f);
Handles.DrawWireDisc(Vector3.zero, Vector3.right, 1.0f);
Handles.DrawWireDisc(Vector3.zero, Vector3.forward, 1.0f);
}
if (cutout.m_Type == GaussianCutout.Type.Box)
Handles.DrawWireCube(Vector3.zero, Vector3.one * 2);
}
Handles.matrix = prevMatrix;
// draw selection bounding box
if (gs.editSelectedSplats > 0)
{
var selBounds = GaussianSplatRendererEditor.TransformBounds(gs.transform, gs.editSelectedBounds);
Handles.DrawWireCube(selBounds.center, selBounds.size);
}
// draw drag rectangle
if (GUIUtility.hotControl == id && evt.mousePosition != m_MouseStartDragPos)
{
GUIStyle style = "SelectionRect";
Handles.BeginGUI();
style.Draw(FromToRect(m_MouseStartDragPos, evt.mousePosition), false, false, false, false);
Handles.EndGUI();
}
break;
}
}
// build a rect that always has a positive size
static Rect FromToRect(Vector2 from, Vector2 to)
{
if (from.x > to.x)
(from.x, to.x) = (to.x, from.x);
if (from.y > to.y)
(from.y, to.y) = (to.y, from.y);
return new Rect(from.x, from.y, to.x - from.x, to.y - from.y);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 80d7ecbaa1b24e6399ee95f6fc0b9c90
timeCreated: 1697718362

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 770e497b696b99641aa1bf295d0b3552
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,153 @@
fileFormatVersion: 2
guid: c61202bc8cc557546afa505174da220e
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 1
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 128
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Nintendo Switch
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: LinuxHeadlessSimulation
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 64
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,153 @@
fileFormatVersion: 2
guid: 15d0de03329e14440b034e884fe10379
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 1
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 128
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Nintendo Switch
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: LinuxHeadlessSimulation
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 64
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,231 @@
fileFormatVersion: 2
guid: 81df9c0903abfa345a9022d090982f5d
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 1
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 128
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: iPhone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Windows Store Apps
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: tvOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Lumin
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: CloudRendering
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Nintendo Switch
maxTextureSize: 64
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: LinuxHeadlessSimulation
maxTextureSize: 64
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 64
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,153 @@
fileFormatVersion: 2
guid: e37880566baf3964e9b75e45adb36f3f
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 12
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 2
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 1
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 128
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Nintendo Switch
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: LinuxHeadlessSimulation
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Server
maxTextureSize: 64
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f812890ad0ea4c747bdc67b6d2c1c627
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,26 @@
// SPDX-License-Identifier: MIT
using UnityEditor;
using UnityEngine;
namespace GaussianSplatting.Editor.Utils
{
public class CaptureScreenshot : MonoBehaviour
{
[MenuItem("Tools/Gaussian Splats/Debug/Capture Screenshot %g")]
public static void CaptureShot()
{
int counter = 0;
string path;
while(true)
{
path = $"Shot-{counter:0000}.png";
if (!System.IO.File.Exists(path))
break;
++counter;
}
ScreenCapture.CaptureScreenshot(path);
Debug.Log($"Captured {path}");
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6c80a2b8daebbc1449b79e5ec436f39d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,274 @@
// SPDX-License-Identifier: MIT
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Experimental;
using UnityEngine;
namespace GaussianSplatting.Editor.Utils
{
public class FilePickerControl
{
const string kLastPathPref = "nesnausk.utils.FilePickerLastPath";
static Texture2D s_FolderIcon => EditorGUIUtility.FindTexture(EditorResources.emptyFolderIconName);
static Texture2D s_FileIcon => EditorGUIUtility.FindTexture(EditorResources.folderIconName);
static GUIStyle s_StyleTextFieldText;
static GUIStyle s_StyleTextFieldDropdown;
static readonly int kPathFieldControlID = "FilePickerPathField".GetHashCode();
const int kIconSize = 15;
const int kRecentPathsCount = 20;
public static string PathToDisplayString(string path)
{
if (string.IsNullOrWhiteSpace(path))
return "<none>";
path = path.Replace('\\', '/');
string[] parts = path.Split('/');
// check if filename is not some super generic one
var baseName = Path.GetFileNameWithoutExtension(parts[^1]).ToLowerInvariant();
if (baseName != "point_cloud" && baseName != "splat" && baseName != "input")
return parts[^1];
// otherwise if filename is just some generic "point cloud" type, then take some folder names above it into account
if (parts.Length >= 4)
path = string.Join('/', parts.TakeLast(4));
path = path.Replace('/', '-');
return path;
}
class PreviousPaths
{
public PreviousPaths(List<string> paths)
{
this.paths = paths;
UpdateContent();
}
public void UpdateContent()
{
this.content = paths.Select(p => new GUIContent(PathToDisplayString(p))).ToArray();
}
public List<string> paths;
public GUIContent[] content;
}
Dictionary<string, PreviousPaths> m_PreviousPaths = new();
void PopulatePreviousPaths(string nameKey)
{
if (m_PreviousPaths.ContainsKey(nameKey))
return;
List<string> prevPaths = new();
for (int i = 0; i < kRecentPathsCount; ++i)
{
string path = EditorPrefs.GetString($"{kLastPathPref}-{nameKey}-{i}");
if (!string.IsNullOrWhiteSpace(path))
prevPaths.Add(path);
}
m_PreviousPaths.Add(nameKey, new PreviousPaths(prevPaths));
}
void UpdatePreviousPaths(string nameKey, string path)
{
if (!m_PreviousPaths.ContainsKey(nameKey))
{
m_PreviousPaths.Add(nameKey, new PreviousPaths(new List<string>()));
}
var prevPaths = m_PreviousPaths[nameKey];
prevPaths.paths.Remove(path);
prevPaths.paths.Insert(0, path);
while (prevPaths.paths.Count > kRecentPathsCount)
prevPaths.paths.RemoveAt(prevPaths.paths.Count - 1);
prevPaths.UpdateContent();
for (int i = 0; i < prevPaths.paths.Count; ++i)
{
EditorPrefs.SetString($"{kLastPathPref}-{nameKey}-{i}", prevPaths.paths[i]);
}
}
static bool CheckPath(string path, bool isFolder)
{
if (string.IsNullOrWhiteSpace(path))
return false;
if (isFolder)
{
if (!Directory.Exists(path))
return false;
}
else
{
if (!File.Exists(path))
return false;
}
return true;
}
static string PathAbsToStorage(string path)
{
path = path.Replace('\\', '/');
var dataPath = Application.dataPath;
if (path.StartsWith(dataPath, StringComparison.Ordinal))
{
path = Path.GetRelativePath($"{dataPath}/..", path);
path = path.Replace('\\', '/');
}
return path;
}
bool CheckAndSetNewPath(ref string path, string nameKey, bool isFolder)
{
path = PathAbsToStorage(path);
if (CheckPath(path, isFolder))
{
EditorPrefs.SetString($"{kLastPathPref}-{nameKey}", path);
UpdatePreviousPaths(nameKey, path);
GUI.changed = true;
Event.current.Use();
return true;
}
return false;
}
string PreviousPathsDropdown(Rect position, string value, string nameKey, bool isFolder)
{
PopulatePreviousPaths(nameKey);
if (string.IsNullOrWhiteSpace(value))
value = EditorPrefs.GetString($"{kLastPathPref}-{nameKey}");
m_PreviousPaths.TryGetValue(nameKey, out var prevPaths);
EditorGUI.BeginDisabledGroup(prevPaths == null || prevPaths.paths.Count == 0);
EditorGUI.BeginChangeCheck();
int oldIndent = EditorGUI.indentLevel;
EditorGUI.indentLevel = 0;
int parameterIndex = EditorGUI.Popup(position, GUIContent.none, -1, prevPaths.content, s_StyleTextFieldDropdown);
if (EditorGUI.EndChangeCheck() && parameterIndex < prevPaths.paths.Count)
{
string newValue = prevPaths.paths[parameterIndex];
if (CheckAndSetNewPath(ref newValue, nameKey, isFolder))
value = newValue;
}
EditorGUI.indentLevel = oldIndent;
EditorGUI.EndDisabledGroup();
return value;
}
// null extension picks folders
public string PathFieldGUI(Rect position, GUIContent label, string value, string extension, string nameKey)
{
s_StyleTextFieldText ??= new GUIStyle("TextFieldDropDownText");
s_StyleTextFieldDropdown ??= new GUIStyle("TextFieldDropdown");
bool isFolder = extension == null;
int controlId = GUIUtility.GetControlID(kPathFieldControlID, FocusType.Keyboard, position);
Rect fullRect = EditorGUI.PrefixLabel(position, controlId, label);
Rect textRect = new Rect(fullRect.x, fullRect.y, fullRect.width - s_StyleTextFieldDropdown.fixedWidth, fullRect.height);
Rect dropdownRect = new Rect(textRect.xMax, fullRect.y, s_StyleTextFieldDropdown.fixedWidth, fullRect.height);
Rect iconRect = new Rect(textRect.xMax - kIconSize, textRect.y, kIconSize, textRect.height);
value = PreviousPathsDropdown(dropdownRect, value, nameKey, isFolder);
string displayText = PathToDisplayString(value);
Event evt = Event.current;
switch (evt.type)
{
case EventType.KeyDown:
if (GUIUtility.keyboardControl == controlId)
{
if (evt.keyCode is KeyCode.Backspace or KeyCode.Delete)
{
value = null;
EditorPrefs.SetString($"{kLastPathPref}-{nameKey}", "");
GUI.changed = true;
evt.Use();
}
}
break;
case EventType.Repaint:
s_StyleTextFieldText.Draw(textRect, new GUIContent(displayText), controlId, DragAndDrop.activeControlID == controlId);
GUI.DrawTexture(iconRect, isFolder ? s_FolderIcon : s_FileIcon, ScaleMode.ScaleToFit);
break;
case EventType.MouseDown:
if (evt.button != 0 || !GUI.enabled)
break;
if (textRect.Contains(evt.mousePosition))
{
if (iconRect.Contains(evt.mousePosition))
{
if (string.IsNullOrWhiteSpace(value))
value = EditorPrefs.GetString($"{kLastPathPref}-{nameKey}");
string newPath;
string openToPath = string.Empty;
if (isFolder)
{
if (Directory.Exists(value))
openToPath = value;
newPath = EditorUtility.OpenFolderPanel("Select folder", openToPath, "");
}
else
{
if (File.Exists(value))
openToPath = Path.GetDirectoryName(value);
newPath = EditorUtility.OpenFilePanel("Select file", openToPath, extension);
}
if (CheckAndSetNewPath(ref newPath, nameKey, isFolder))
{
value = newPath;
GUI.changed = true;
evt.Use();
}
}
else if (File.Exists(value) || Directory.Exists(value))
{
EditorUtility.RevealInFinder(value);
}
GUIUtility.keyboardControl = controlId;
}
break;
case EventType.DragUpdated:
case EventType.DragPerform:
if (textRect.Contains(evt.mousePosition) && GUI.enabled)
{
if (DragAndDrop.paths.Length > 0)
{
DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
string path = DragAndDrop.paths[0];
path = PathAbsToStorage(path);
if (CheckPath(path, isFolder))
{
if (evt.type == EventType.DragPerform)
{
UpdatePreviousPaths(nameKey, path);
value = path;
GUI.changed = true;
DragAndDrop.AcceptDrag();
DragAndDrop.activeControlID = 0;
}
else
DragAndDrop.activeControlID = controlId;
}
else
DragAndDrop.visualMode = DragAndDropVisualMode.Rejected;
evt.Use();
}
}
break;
case EventType.DragExited:
if (GUI.enabled)
{
HandleUtility.Repaint();
}
break;
}
return value;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 69e6c946494a9b2479ce96542339029c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,595 @@
// SPDX-License-Identifier: MIT
using System;
using Unity.Burst;
using Unity.Burst.Intrinsics;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Profiling;
using Unity.Profiling.LowLevel;
namespace GaussianSplatting.Editor.Utils
{
// Implementation of "Mini Batch" k-means clustering ("Web-Scale K-Means Clustering", Sculley 2010)
// using k-means++ for cluster initialization.
[BurstCompile]
public struct KMeansClustering
{
static ProfilerMarker s_ProfCalculate = new(ProfilerCategory.Render, "KMeans.Calculate", MarkerFlags.SampleGPU);
static ProfilerMarker s_ProfPlusPlus = new(ProfilerCategory.Render, "KMeans.InitialPlusPlus", MarkerFlags.SampleGPU);
static ProfilerMarker s_ProfInitialDistanceSum = new(ProfilerCategory.Render, "KMeans.Initialize.DistanceSum", MarkerFlags.SampleGPU);
static ProfilerMarker s_ProfInitialPickPoint = new(ProfilerCategory.Render, "KMeans.Initialize.PickPoint", MarkerFlags.SampleGPU);
static ProfilerMarker s_ProfInitialDistanceUpdate = new(ProfilerCategory.Render, "KMeans.Initialize.DistanceUpdate", MarkerFlags.SampleGPU);
static ProfilerMarker s_ProfAssignClusters = new(ProfilerCategory.Render, "KMeans.AssignClusters", MarkerFlags.SampleGPU);
static ProfilerMarker s_ProfUpdateMeans = new(ProfilerCategory.Render, "KMeans.UpdateMeans", MarkerFlags.SampleGPU);
public static bool Calculate(int dim, NativeArray<float> inputData, int batchSize, float passesOverData, Func<float,bool> progress, NativeArray<float> outClusterMeans, NativeArray<int> outDataLabels)
{
// Parameter checks
if (dim < 1)
throw new InvalidOperationException($"KMeans: dimensionality has to be >= 1, was {dim}");
if (batchSize < 1)
throw new InvalidOperationException($"KMeans: batch size has to be >= 1, was {batchSize}");
if (passesOverData < 0.0001f)
throw new InvalidOperationException($"KMeans: passes over data must be positive, was {passesOverData}");
if (inputData.Length % dim != 0)
throw new InvalidOperationException($"KMeans: input length must be multiple of dim={dim}, was {inputData.Length}");
if (outClusterMeans.Length % dim != 0)
throw new InvalidOperationException($"KMeans: output means length must be multiple of dim={dim}, was {outClusterMeans.Length}");
int dataSize = inputData.Length / dim;
int k = outClusterMeans.Length / dim;
if (k < 1)
throw new InvalidOperationException($"KMeans: cluster count length must be at least 1, was {k}");
if (dataSize < k)
throw new InvalidOperationException($"KMeans: input length ({inputData.Length}) must at least as long as clusters ({outClusterMeans.Length})");
if (dataSize != outDataLabels.Length)
throw new InvalidOperationException($"KMeans: output labels length must be {dataSize}, was {outDataLabels.Length}");
using var prof = s_ProfCalculate.Auto();
batchSize = math.min(dataSize, batchSize);
uint rngState = 1;
// Do initial cluster placement
int initBatchSize = 10 * k;
const int kInitAttempts = 3;
if (!InitializeCentroids(dim, inputData, initBatchSize, ref rngState, kInitAttempts, outClusterMeans, progress))
return false;
NativeArray<float> counts = new(k, Allocator.TempJob);
NativeArray<float> batchPoints = new(batchSize * dim, Allocator.TempJob);
NativeArray<int> batchClusters = new(batchSize, Allocator.TempJob);
bool cancelled = false;
for (float calcDone = 0.0f, calcLimit = dataSize * passesOverData; calcDone < calcLimit; calcDone += batchSize)
{
if (progress != null && !progress(0.3f + calcDone / calcLimit * 0.4f))
{
cancelled = true;
break;
}
// generate a batch of random input points
MakeRandomBatch(dim, inputData, ref rngState, batchPoints);
// find which of the current centroids each batch point is closest to
{
using var profPart = s_ProfAssignClusters.Auto();
AssignClustersJob job = new AssignClustersJob
{
dim = dim,
data = batchPoints,
means = outClusterMeans,
indexOffset = 0,
clusters = batchClusters,
};
job.Schedule(batchSize, 1).Complete();
}
// update the centroids
{
using var profPart = s_ProfUpdateMeans.Auto();
UpdateCentroidsJob job = new UpdateCentroidsJob
{
m_Clusters = outClusterMeans,
m_Dim = dim,
m_Counts = counts,
m_BatchSize = batchSize,
m_BatchClusters = batchClusters,
m_BatchPoints = batchPoints
};
job.Schedule().Complete();
}
}
// finally find out closest clusters for all input points
{
using var profPart = s_ProfAssignClusters.Auto();
const int kAssignBatchCount = 256 * 1024;
AssignClustersJob job = new AssignClustersJob
{
dim = dim,
data = inputData,
means = outClusterMeans,
indexOffset = 0,
clusters = outDataLabels,
};
for (int i = 0; i < dataSize; i += kAssignBatchCount)
{
if (progress != null && !progress(0.7f + (float) i / dataSize * 0.3f))
{
cancelled = true;
break;
}
job.indexOffset = i;
job.Schedule(math.min(kAssignBatchCount, dataSize - i), 512).Complete();
}
}
counts.Dispose();
batchPoints.Dispose();
batchClusters.Dispose();
return !cancelled;
}
static unsafe float DistanceSquared(int dim, NativeArray<float> a, int aIndex, NativeArray<float> b, int bIndex)
{
aIndex *= dim;
bIndex *= dim;
float d = 0;
if (X86.Avx.IsAvxSupported)
{
// 8x wide with AVX
int i = 0;
float* aptr = (float*) a.GetUnsafeReadOnlyPtr() + aIndex;
float* bptr = (float*) b.GetUnsafeReadOnlyPtr() + bIndex;
for (; i + 7 < dim; i += 8)
{
v256 va = X86.Avx.mm256_loadu_ps(aptr);
v256 vb = X86.Avx.mm256_loadu_ps(bptr);
v256 vd = X86.Avx.mm256_sub_ps(va, vb);
vd = X86.Avx.mm256_mul_ps(vd, vd);
vd = X86.Avx.mm256_hadd_ps(vd, vd);
d += vd.Float0 + vd.Float1 + vd.Float4 + vd.Float5;
aptr += 8;
bptr += 8;
}
// remainder
for (; i < dim; ++i)
{
float delta = *aptr - *bptr;
d += delta * delta;
aptr++;
bptr++;
}
}
else if (Arm.Neon.IsNeonSupported)
{
// 4x wide with NEON
int i = 0;
float* aptr = (float*) a.GetUnsafeReadOnlyPtr() + aIndex;
float* bptr = (float*) b.GetUnsafeReadOnlyPtr() + bIndex;
for (; i + 3 < dim; i += 4)
{
v128 va = Arm.Neon.vld1q_f32(aptr);
v128 vb = Arm.Neon.vld1q_f32(bptr);
v128 vd = Arm.Neon.vsubq_f32(va, vb);
vd = Arm.Neon.vmulq_f32(vd, vd);
d += Arm.Neon.vaddvq_f32(vd);
aptr += 4;
bptr += 4;
}
// remainder
for (; i < dim; ++i)
{
float delta = *aptr - *bptr;
d += delta * delta;
aptr++;
bptr++;
}
}
else
{
for (var i = 0; i < dim; ++i)
{
float delta = a[aIndex + i] - b[bIndex + i];
d += delta * delta;
}
}
return d;
}
static unsafe void CopyElem(int dim, NativeArray<float> src, int srcIndex, NativeArray<float> dst, int dstIndex)
{
UnsafeUtility.MemCpy((float*) dst.GetUnsafePtr() + dstIndex * dim,
(float*) src.GetUnsafeReadOnlyPtr() + srcIndex * dim, dim * 4);
}
[BurstCompile]
struct ClosestDistanceInitialJob : IJobParallelFor
{
public int dim;
[ReadOnly] public NativeArray<float> data;
[ReadOnly] public NativeArray<float> means;
public NativeArray<float> minDistSq;
public int pointIndex;
public void Execute(int index)
{
if (index == pointIndex)
return;
minDistSq[index] = DistanceSquared(dim, data, index, means, 0);
}
}
[BurstCompile]
struct ClosestDistanceUpdateJob : IJobParallelFor
{
public int dim;
[ReadOnly] public NativeArray<float> data;
[ReadOnly] public NativeArray<float> means;
[ReadOnly] public NativeBitArray taken;
public NativeArray<float> minDistSq;
public int meanIndex;
public void Execute(int index)
{
if (taken.IsSet(index))
return;
float distSq = DistanceSquared(dim, data, index, means, meanIndex);
minDistSq[index] = math.min(minDistSq[index], distSq);
}
}
[BurstCompile]
struct CalcDistSqJob : IJobParallelFor
{
public const int kBatchSize = 1024;
public int dataSize;
[ReadOnly] public NativeBitArray taken;
[ReadOnly] public NativeArray<float> minDistSq;
public NativeArray<float> partialSums;
public void Execute(int batchIndex)
{
int iStart = math.min(batchIndex * kBatchSize, dataSize);
int iEnd = math.min((batchIndex + 1) * kBatchSize, dataSize);
float sum = 0;
for (int i = iStart; i < iEnd; ++i)
{
if (taken.IsSet(i))
continue;
sum += minDistSq[i];
}
partialSums[batchIndex] = sum;
}
}
[BurstCompile]
static int PickPointIndex(int dataSize, ref NativeArray<float> partialSums, ref NativeBitArray taken, ref NativeArray<float> minDistSq, float rval)
{
// Skip batches until we hit the ones that might have value to pick from: binary search for the batch
int indexL = 0;
int indexR = partialSums.Length;
while (indexL < indexR)
{
int indexM = (indexL + indexR) / 2;
if (partialSums[indexM] < rval)
indexL = indexM + 1;
else
indexR = indexM;
}
float acc = 0.0f;
if (indexL > 0)
{
acc = partialSums[indexL-1];
}
// Now search for the needed point
int pointIndex = -1;
for (int i = indexL * CalcDistSqJob.kBatchSize; i < dataSize; ++i)
{
if (taken.IsSet(i))
continue;
acc += minDistSq[i];
if (acc >= rval)
{
pointIndex = i;
break;
}
}
// If we have not found a point, pick the last available one
if (pointIndex < 0)
{
for (int i = dataSize - 1; i >= 0; --i)
{
if (taken.IsSet(i))
continue;
pointIndex = i;
break;
}
}
if (pointIndex < 0)
pointIndex = 0;
return pointIndex;
}
static void KMeansPlusPlus(int dim, int k, NativeArray<float> data, NativeArray<float> means, NativeArray<float> minDistSq, ref uint rngState)
{
using var prof = s_ProfPlusPlus.Auto();
int dataSize = data.Length / dim;
NativeBitArray taken = new NativeBitArray(dataSize, Allocator.TempJob);
// Select first mean randomly
int pointIndex = (int)(pcg_random(ref rngState) % dataSize);
taken.Set(pointIndex, true);
CopyElem(dim, data, pointIndex, means, 0);
// For each point: closest squared distance to the picked point
{
ClosestDistanceInitialJob job = new ClosestDistanceInitialJob
{
dim = dim,
data = data,
means = means,
minDistSq = minDistSq,
pointIndex = pointIndex
};
job.Schedule(dataSize, 1024).Complete();
}
int sumBatches = (dataSize + CalcDistSqJob.kBatchSize - 1) / CalcDistSqJob.kBatchSize;
NativeArray<float> partialSums = new(sumBatches, Allocator.TempJob);
int resultCount = 1;
while (resultCount < k)
{
// Find total sum of distances of not yet taken points
float distSqTotal = 0;
{
using var profPart = s_ProfInitialDistanceSum.Auto();
CalcDistSqJob job = new CalcDistSqJob
{
dataSize = dataSize,
taken = taken,
minDistSq = minDistSq,
partialSums = partialSums
};
job.Schedule(sumBatches, 1).Complete();
for (int i = 0; i < sumBatches; ++i)
{
distSqTotal += partialSums[i];
partialSums[i] = distSqTotal;
}
}
// Pick a non-taken point, with a probability proportional
// to distance: points furthest from any cluster are picked more.
{
using var profPart = s_ProfInitialPickPoint.Auto();
float rval = pcg_hash_float(rngState + (uint)resultCount, distSqTotal);
pointIndex = PickPointIndex(dataSize, ref partialSums, ref taken, ref minDistSq, rval);
}
// Take this point as a new cluster mean
taken.Set(pointIndex, true);
CopyElem(dim, data, pointIndex, means, resultCount);
++resultCount;
if (resultCount < k)
{
// Update distances of the points: since it tracks closest one,
// calculate distance to the new cluster and update if smaller.
using var profPart = s_ProfInitialDistanceUpdate.Auto();
ClosestDistanceUpdateJob job = new ClosestDistanceUpdateJob
{
dim = dim,
data = data,
means = means,
minDistSq = minDistSq,
taken = taken,
meanIndex = resultCount - 1
};
job.Schedule(dataSize, 256).Complete();
}
}
taken.Dispose();
partialSums.Dispose();
}
// For each data point, find cluster index that is closest to it
[BurstCompile]
struct AssignClustersJob : IJobParallelFor
{
public int indexOffset;
public int dim;
[ReadOnly] public NativeArray<float> data;
[ReadOnly] public NativeArray<float> means;
[NativeDisableParallelForRestriction] public NativeArray<int> clusters;
[NativeDisableContainerSafetyRestriction] [NativeDisableParallelForRestriction] public NativeArray<float> distances;
public void Execute(int index)
{
index += indexOffset;
int meansCount = means.Length / dim;
float minDist = float.MaxValue;
int minIndex = 0;
for (int i = 0; i < meansCount; ++i)
{
float dist = DistanceSquared(dim, data, index, means, i);
if (dist < minDist)
{
minIndex = i;
minDist = dist;
}
}
clusters[index] = minIndex;
if (distances.IsCreated)
distances[index] = minDist;
}
}
static void MakeRandomBatch(int dim, NativeArray<float> inputData, ref uint rngState, NativeArray<float> outBatch)
{
var job = new MakeBatchJob
{
m_Dim = dim,
m_InputData = inputData,
m_Seed = pcg_random(ref rngState),
m_OutBatch = outBatch
};
job.Schedule().Complete();
}
[BurstCompile]
struct MakeBatchJob : IJob
{
public int m_Dim;
public NativeArray<float> m_InputData;
public NativeArray<float> m_OutBatch;
public uint m_Seed;
public void Execute()
{
uint dataSize = (uint)(m_InputData.Length / m_Dim);
int batchSize = m_OutBatch.Length / m_Dim;
NativeHashSet<int> picked = new(batchSize, Allocator.Temp);
while (picked.Count < batchSize)
{
int index = (int)(pcg_hash(m_Seed++) % dataSize);
if (!picked.Contains(index))
{
CopyElem(m_Dim, m_InputData, index, m_OutBatch, picked.Count);
picked.Add(index);
}
}
picked.Dispose();
}
}
[BurstCompile]
struct UpdateCentroidsJob : IJob
{
public int m_Dim;
public int m_BatchSize;
[ReadOnly] public NativeArray<int> m_BatchClusters;
public NativeArray<float> m_Counts;
[ReadOnly] public NativeArray<float> m_BatchPoints;
public NativeArray<float> m_Clusters;
public void Execute()
{
for (int i = 0; i < m_BatchSize; ++i)
{
int clusterIndex = m_BatchClusters[i];
m_Counts[clusterIndex]++;
float alpha = 1.0f / m_Counts[clusterIndex];
for (int j = 0; j < m_Dim; ++j)
{
m_Clusters[clusterIndex * m_Dim + j] = math.lerp(m_Clusters[clusterIndex * m_Dim + j],
m_BatchPoints[i * m_Dim + j], alpha);
}
}
}
}
static bool InitializeCentroids(int dim, NativeArray<float> inputData, int initBatchSize, ref uint rngState, int initAttempts, NativeArray<float> outClusters, Func<float,bool> progress)
{
using var prof = s_ProfPlusPlus.Auto();
int k = outClusters.Length / dim;
int dataSize = inputData.Length / dim;
initBatchSize = math.min(initBatchSize, dataSize);
NativeArray<float> centroidBatch = new(initBatchSize * dim, Allocator.TempJob);
NativeArray<float> validationBatch = new(initBatchSize * dim, Allocator.TempJob);
MakeRandomBatch(dim, inputData, ref rngState, centroidBatch);
MakeRandomBatch(dim, inputData, ref rngState, validationBatch);
NativeArray<int> tmpIndices = new(initBatchSize, Allocator.TempJob);
NativeArray<float> tmpDistances = new(initBatchSize, Allocator.TempJob);
NativeArray<float> curCentroids = new(k * dim, Allocator.TempJob);
float minDistSum = float.MaxValue;
bool cancelled = false;
for (int ia = 0; ia < initAttempts; ++ia)
{
if (progress != null && !progress((float) ia / initAttempts * 0.3f))
{
cancelled = true;
break;
}
KMeansPlusPlus(dim, k, centroidBatch, curCentroids, tmpDistances, ref rngState);
{
using var profPart = s_ProfAssignClusters.Auto();
AssignClustersJob job = new AssignClustersJob
{
dim = dim,
data = validationBatch,
means = curCentroids,
indexOffset = 0,
clusters = tmpIndices,
distances = tmpDistances
};
job.Schedule(initBatchSize, 1).Complete();
}
float distSum = 0;
foreach (var d in tmpDistances)
distSum += d;
// is this centroid better?
if (distSum < minDistSum)
{
minDistSum = distSum;
outClusters.CopyFrom(curCentroids);
}
}
centroidBatch.Dispose();
validationBatch.Dispose();
tmpDistances.Dispose();
tmpIndices.Dispose();
curCentroids.Dispose();
return !cancelled;
}
// https://www.reedbeta.com/blog/hash-functions-for-gpu-rendering/
static uint pcg_hash(uint input)
{
uint state = input * 747796405u + 2891336453u;
uint word = ((state >> (int)((state >> 28) + 4u)) ^ state) * 277803737u;
return (word >> 22) ^ word;
}
static float pcg_hash_float(uint input, float upTo)
{
uint val = pcg_hash(input);
float f = math.asfloat(0x3f800000 | (val >> 9)) - 1.0f;
return f * upTo;
}
static uint pcg_random(ref uint rng_state)
{
uint state = rng_state;
rng_state = rng_state * 747796405u + 2891336453u;
uint word = ((state >> (int)((state >> 28) + 4u)) ^ state) * 277803737u;
return (word >> 22) ^ word;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9cecadf9c980a4ad9a30d0e1ae09d16a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,107 @@
// SPDX-License-Identifier: MIT
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Unity.Collections;
namespace GaussianSplatting.Editor.Utils
{
public static class PLYFileReader
{
public static void ReadFileHeader(string filePath, out int vertexCount, out int vertexStride, out List<string> attrNames)
{
vertexCount = 0;
vertexStride = 0;
attrNames = new List<string>();
if (!File.Exists(filePath))
return;
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
ReadHeaderImpl(filePath, out vertexCount, out vertexStride, out attrNames, fs);
}
static void ReadHeaderImpl(string filePath, out int vertexCount, out int vertexStride, out List<string> attrNames, FileStream fs)
{
// C# arrays and NativeArrays make it hard to have a "byte" array larger than 2GB :/
if (fs.Length >= 2 * 1024 * 1024 * 1024L)
throw new IOException($"PLY {filePath} read error: currently files larger than 2GB are not supported");
// read header
vertexCount = 0;
vertexStride = 0;
attrNames = new List<string>();
const int kMaxHeaderLines = 9000;
for (int lineIdx = 0; lineIdx < kMaxHeaderLines; ++lineIdx)
{
var line = ReadLine(fs);
if (line == "end_header" || line.Length == 0)
break;
var tokens = line.Split(' ');
if (tokens.Length == 3 && tokens[0] == "element" && tokens[1] == "vertex")
vertexCount = int.Parse(tokens[2]);
if (tokens.Length == 3 && tokens[0] == "property")
{
ElementType type = tokens[1] switch
{
"float" => ElementType.Float,
"double" => ElementType.Double,
"uchar" => ElementType.UChar,
_ => ElementType.None
};
vertexStride += TypeToSize(type);
attrNames.Add(tokens[2]);
}
}
//Debug.Log($"PLY {filePath} vtx {vertexCount} stride {vertexStride} attrs #{attrNames.Count} {string.Join(',', attrNames)}");
}
public static void ReadFile(string filePath, out int vertexCount, out int vertexStride, out List<string> attrNames, out NativeArray<byte> vertices)
{
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read);
ReadHeaderImpl(filePath, out vertexCount, out vertexStride, out attrNames, fs);
vertices = new NativeArray<byte>(vertexCount * vertexStride, Allocator.Persistent);
var readBytes = fs.Read(vertices);
if (readBytes != vertices.Length)
throw new IOException($"PLY {filePath} read error, expected {vertices.Length} data bytes got {readBytes}");
}
enum ElementType
{
None,
Float,
Double,
UChar
}
static int TypeToSize(ElementType t)
{
return t switch
{
ElementType.None => 0,
ElementType.Float => 4,
ElementType.Double => 8,
ElementType.UChar => 1,
_ => throw new ArgumentOutOfRangeException(nameof(t), t, null)
};
}
static string ReadLine(FileStream fs)
{
var byteBuffer = new List<byte>();
while (true)
{
int b = fs.ReadByte();
if (b == -1 || b == '\n')
break;
byteBuffer.Add((byte)b);
}
// if line had CRLF line endings, remove the CR part
if (byteBuffer.Count > 0 && byteBuffer.Last() == '\r')
byteBuffer.RemoveAt(byteBuffer.Count-1);
return Encoding.UTF8.GetString(byteBuffer.ToArray());
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 27964c85711004ddca73909489af2e2e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,403 @@
/*
Embedded TinyJSON from https://github.com/pbhogan/TinyJSON (version 71fed96, 2019 May 10) directly here, with
custom namespace wrapped around it so it does not clash with any other embedded TinyJSON. Original license:
The MIT License (MIT)
Copyright (c) 2018 Alex Parker
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
namespace GaussianSplatting.Editor.Utils
{
// Really simple JSON parser in ~300 lines
// - Attempts to parse JSON files with minimal GC allocation
// - Nice and simple "[1,2,3]".FromJson<List<int>>() API
// - Classes and structs can be parsed too!
// class Foo { public int Value; }
// "{\"Value\":10}".FromJson<Foo>()
// - Can parse JSON without type information into Dictionary<string,object> and List<object> e.g.
// "[1,2,3]".FromJson<object>().GetType() == typeof(List<object>)
// "{\"Value\":10}".FromJson<object>().GetType() == typeof(Dictionary<string,object>)
// - No JIT Emit support to support AOT compilation on iOS
// - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead.
// - Only public fields and property setters on classes/structs will be written to
//
// Limitations:
// - No JIT Emit support to parse structures quickly
// - Limited to parsing <2GB JSON files (due to int.MaxValue)
// - Parsing of abstract classes or interfaces is NOT supported and will throw an exception.
public static class JSONParser
{
[ThreadStatic] static Stack<List<string>> splitArrayPool;
[ThreadStatic] static StringBuilder stringBuilder;
[ThreadStatic] static Dictionary<Type, Dictionary<string, FieldInfo>> fieldInfoCache;
[ThreadStatic] static Dictionary<Type, Dictionary<string, PropertyInfo>> propertyInfoCache;
public static T FromJson<T>(this string json)
{
// Initialize, if needed, the ThreadStatic variables
if (propertyInfoCache == null) propertyInfoCache = new Dictionary<Type, Dictionary<string, PropertyInfo>>();
if (fieldInfoCache == null) fieldInfoCache = new Dictionary<Type, Dictionary<string, FieldInfo>>();
if (stringBuilder == null) stringBuilder = new StringBuilder();
if (splitArrayPool == null) splitArrayPool = new Stack<List<string>>();
//Remove all whitespace not within strings to make parsing simpler
stringBuilder.Length = 0;
for (int i = 0; i < json.Length; i++)
{
char c = json[i];
if (c == '"')
{
i = AppendUntilStringEnd(true, i, json);
continue;
}
if (char.IsWhiteSpace(c))
continue;
stringBuilder.Append(c);
}
//Parse the thing!
return (T)ParseValue(typeof(T), stringBuilder.ToString());
}
static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json)
{
stringBuilder.Append(json[startIdx]);
for (int i = startIdx + 1; i < json.Length; i++)
{
if (json[i] == '\\')
{
if (appendEscapeCharacter)
stringBuilder.Append(json[i]);
stringBuilder.Append(json[i + 1]);
i++;//Skip next character as it is escaped
}
else if (json[i] == '"')
{
stringBuilder.Append(json[i]);
return i;
}
else
stringBuilder.Append(json[i]);
}
return json.Length - 1;
}
//Splits { <value>:<value>, <value>:<value> } and [ <value>, <value> ] into a list of <value> strings
static List<string> Split(string json)
{
List<string> splitArray = splitArrayPool.Count > 0 ? splitArrayPool.Pop() : new List<string>();
splitArray.Clear();
if (json.Length == 2)
return splitArray;
int parseDepth = 0;
stringBuilder.Length = 0;
for (int i = 1; i < json.Length - 1; i++)
{
switch (json[i])
{
case '[':
case '{':
parseDepth++;
break;
case ']':
case '}':
parseDepth--;
break;
case '"':
i = AppendUntilStringEnd(true, i, json);
continue;
case ',':
case ':':
if (parseDepth == 0)
{
splitArray.Add(stringBuilder.ToString());
stringBuilder.Length = 0;
continue;
}
break;
}
stringBuilder.Append(json[i]);
}
splitArray.Add(stringBuilder.ToString());
return splitArray;
}
internal static object ParseValue(Type type, string json)
{
if (type == typeof(string))
{
if (json.Length <= 2)
return string.Empty;
StringBuilder parseStringBuilder = new StringBuilder(json.Length);
for (int i = 1; i < json.Length - 1; ++i)
{
if (json[i] == '\\' && i + 1 < json.Length - 1)
{
int j = "\"\\nrtbf/".IndexOf(json[i + 1]);
if (j >= 0)
{
parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]);
++i;
continue;
}
if (json[i + 1] == 'u' && i + 5 < json.Length - 1)
{
UInt32 c = 0;
if (UInt32.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out c))
{
parseStringBuilder.Append((char)c);
i += 5;
continue;
}
}
}
parseStringBuilder.Append(json[i]);
}
return parseStringBuilder.ToString();
}
if (type.IsPrimitive)
{
var result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture);
return result;
}
if (type == typeof(decimal))
{
decimal result;
decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result);
return result;
}
if (type == typeof(DateTime))
{
DateTime result;
DateTime.TryParse(json.Replace("\"",""), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out result);
return result;
}
if (json == "null")
{
return null;
}
if (type.IsEnum)
{
if (json[0] == '"')
json = json.Substring(1, json.Length - 2);
try
{
return Enum.Parse(type, json, false);
}
catch
{
return 0;
}
}
if (type.IsArray)
{
Type arrayType = type.GetElementType();
if (json[0] != '[' || json[json.Length - 1] != ']')
return null;
List<string> elems = Split(json);
Array newArray = Array.CreateInstance(arrayType, elems.Count);
for (int i = 0; i < elems.Count; i++)
newArray.SetValue(ParseValue(arrayType, elems[i]), i);
splitArrayPool.Push(elems);
return newArray;
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
{
Type listType = type.GetGenericArguments()[0];
if (json[0] != '[' || json[json.Length - 1] != ']')
return null;
List<string> elems = Split(json);
var list = (IList)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count });
for (int i = 0; i < elems.Count; i++)
list.Add(ParseValue(listType, elems[i]));
splitArrayPool.Push(elems);
return list;
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
Type keyType, valueType;
{
Type[] args = type.GetGenericArguments();
keyType = args[0];
valueType = args[1];
}
//Refuse to parse dictionary keys that aren't of type string
if (keyType != typeof(string))
return null;
//Must be a valid dictionary element
if (json[0] != '{' || json[json.Length - 1] != '}')
return null;
//The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
List<string> elems = Split(json);
if (elems.Count % 2 != 0)
return null;
var dictionary = (IDictionary)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count / 2 });
for (int i = 0; i < elems.Count; i += 2)
{
if (elems[i].Length <= 2)
continue;
string keyValue = elems[i].Substring(1, elems[i].Length - 2);
object val = ParseValue(valueType, elems[i + 1]);
dictionary[keyValue] = val;
}
return dictionary;
}
if (type == typeof(object))
{
return ParseAnonymousValue(json);
}
if (json[0] == '{' && json[json.Length - 1] == '}')
{
return ParseObject(type, json);
}
return null;
}
static object ParseAnonymousValue(string json)
{
if (json.Length == 0)
return null;
if (json[0] == '{' && json[json.Length - 1] == '}')
{
List<string> elems = Split(json);
if (elems.Count % 2 != 0)
return null;
var dict = new Dictionary<string, object>(elems.Count / 2);
for (int i = 0; i < elems.Count; i += 2)
dict[elems[i].Substring(1, elems[i].Length - 2)] = ParseAnonymousValue(elems[i + 1]);
return dict;
}
if (json[0] == '[' && json[json.Length - 1] == ']')
{
List<string> items = Split(json);
var finalList = new List<object>(items.Count);
for (int i = 0; i < items.Count; i++)
finalList.Add(ParseAnonymousValue(items[i]));
return finalList;
}
if (json[0] == '"' && json[json.Length - 1] == '"')
{
string str = json.Substring(1, json.Length - 2);
return str.Replace("\\", string.Empty);
}
if (char.IsDigit(json[0]) || json[0] == '-')
{
if (json.Contains("."))
{
double result;
double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result);
return result;
}
else
{
int result;
int.TryParse(json, out result);
return result;
}
}
if (json == "true")
return true;
if (json == "false")
return false;
// handles json == "null" as well as invalid JSON
return null;
}
static Dictionary<string, T> CreateMemberNameDictionary<T>(T[] members) where T : MemberInfo
{
Dictionary<string, T> nameToMember = new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < members.Length; i++)
{
T member = members[i];
if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true))
continue;
string name = member.Name;
if (member.IsDefined(typeof(DataMemberAttribute), true))
{
DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true);
if (!string.IsNullOrEmpty(dataMemberAttribute.Name))
name = dataMemberAttribute.Name;
}
nameToMember.Add(name, member);
}
return nameToMember;
}
static object ParseObject(Type type, string json)
{
object instance = FormatterServices.GetUninitializedObject(type);
//The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
List<string> elems = Split(json);
if (elems.Count % 2 != 0)
return instance;
Dictionary<string, FieldInfo> nameToField;
Dictionary<string, PropertyInfo> nameToProperty;
if (!fieldInfoCache.TryGetValue(type, out nameToField))
{
nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
fieldInfoCache.Add(type, nameToField);
}
if (!propertyInfoCache.TryGetValue(type, out nameToProperty))
{
nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
propertyInfoCache.Add(type, nameToProperty);
}
for (int i = 0; i < elems.Count; i += 2)
{
if (elems[i].Length <= 2)
continue;
string key = elems[i].Substring(1, elems[i].Length - 2);
string value = elems[i + 1];
FieldInfo fieldInfo;
PropertyInfo propertyInfo;
if (nameToField.TryGetValue(key, out fieldInfo))
fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value));
else if (nameToProperty.TryGetValue(key, out propertyInfo))
propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null);
}
return instance;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e9ea5041388393f459c378c31e4d7b1f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: