chore: sync workspace state
This commit is contained in:
35
MVS/3DGS-Unity/Editor/GaussianMoveTool.cs
Normal file
35
MVS/3DGS-Unity/Editor/GaussianMoveTool.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
MVS/3DGS-Unity/Editor/GaussianMoveTool.cs.meta
Normal file
11
MVS/3DGS-Unity/Editor/GaussianMoveTool.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c9f40b54eb504648b2a0beadabbcc8d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
70
MVS/3DGS-Unity/Editor/GaussianRotateTool.cs
Normal file
70
MVS/3DGS-Unity/Editor/GaussianRotateTool.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
3
MVS/3DGS-Unity/Editor/GaussianRotateTool.cs.meta
Normal file
3
MVS/3DGS-Unity/Editor/GaussianRotateTool.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5128238188a44c86914a22a862195242
|
||||
timeCreated: 1697805149
|
||||
68
MVS/3DGS-Unity/Editor/GaussianScaleTool.cs
Normal file
68
MVS/3DGS-Unity/Editor/GaussianScaleTool.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
3
MVS/3DGS-Unity/Editor/GaussianScaleTool.cs.meta
Normal file
3
MVS/3DGS-Unity/Editor/GaussianScaleTool.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dbf3d17a31b942b28f5d8c187adb8fdf
|
||||
timeCreated: 1697732813
|
||||
1208
MVS/3DGS-Unity/Editor/GaussianSplatAssetCreator.cs
Normal file
1208
MVS/3DGS-Unity/Editor/GaussianSplatAssetCreator.cs
Normal file
File diff suppressed because it is too large
Load Diff
11
MVS/3DGS-Unity/Editor/GaussianSplatAssetCreator.cs.meta
Normal file
11
MVS/3DGS-Unity/Editor/GaussianSplatAssetCreator.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 635bd950b8a74c84f870d5c8f02c3974
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
71
MVS/3DGS-Unity/Editor/GaussianSplatAssetEditor.cs
Normal file
71
MVS/3DGS-Unity/Editor/GaussianSplatAssetEditor.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
MVS/3DGS-Unity/Editor/GaussianSplatAssetEditor.cs.meta
Normal file
11
MVS/3DGS-Unity/Editor/GaussianSplatAssetEditor.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 75971a29a6deda14c9b1ff5f4ab2f2a0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
444
MVS/3DGS-Unity/Editor/GaussianSplatRendererEditor.cs
Normal file
444
MVS/3DGS-Unity/Editor/GaussianSplatRendererEditor.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
11
MVS/3DGS-Unity/Editor/GaussianSplatRendererEditor.cs.meta
Normal file
11
MVS/3DGS-Unity/Editor/GaussianSplatRendererEditor.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b0ce434aee9ae4ee6b1f5cd10ae7c8cb
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
210
MVS/3DGS-Unity/Editor/GaussianSplatValidator.cs
Normal file
210
MVS/3DGS-Unity/Editor/GaussianSplatValidator.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
MVS/3DGS-Unity/Editor/GaussianSplatValidator.cs.meta
Normal file
11
MVS/3DGS-Unity/Editor/GaussianSplatValidator.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2f8e75b80eb181a4698f733ba59b694b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
21
MVS/3DGS-Unity/Editor/GaussianSplattingEditor.asmdef
Normal file
21
MVS/3DGS-Unity/Editor/GaussianSplattingEditor.asmdef
Normal 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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14414175af4b366469db63f2efee475f
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
43
MVS/3DGS-Unity/Editor/GaussianTool.cs
Normal file
43
MVS/3DGS-Unity/Editor/GaussianTool.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
MVS/3DGS-Unity/Editor/GaussianTool.cs.meta
Normal file
11
MVS/3DGS-Unity/Editor/GaussianTool.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6203c808ab9e64a4a8ff0277c5aa7669
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
192
MVS/3DGS-Unity/Editor/GaussianToolContext.cs
Normal file
192
MVS/3DGS-Unity/Editor/GaussianToolContext.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
3
MVS/3DGS-Unity/Editor/GaussianToolContext.cs.meta
Normal file
3
MVS/3DGS-Unity/Editor/GaussianToolContext.cs.meta
Normal file
@@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 80d7ecbaa1b24e6399ee95f6fc0b9c90
|
||||
timeCreated: 1697718362
|
||||
8
MVS/3DGS-Unity/Editor/Icons.meta
Normal file
8
MVS/3DGS-Unity/Editor/Icons.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 770e497b696b99641aa1bf295d0b3552
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
MVS/3DGS-Unity/Editor/Icons/GaussianContext.png
Normal file
BIN
MVS/3DGS-Unity/Editor/Icons/GaussianContext.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
153
MVS/3DGS-Unity/Editor/Icons/GaussianContext.png.meta
Normal file
153
MVS/3DGS-Unity/Editor/Icons/GaussianContext.png.meta
Normal 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:
|
||||
BIN
MVS/3DGS-Unity/Editor/Icons/GaussianContext@2x.png
Normal file
BIN
MVS/3DGS-Unity/Editor/Icons/GaussianContext@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
153
MVS/3DGS-Unity/Editor/Icons/GaussianContext@2x.png.meta
Normal file
153
MVS/3DGS-Unity/Editor/Icons/GaussianContext@2x.png.meta
Normal 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:
|
||||
BIN
MVS/3DGS-Unity/Editor/Icons/d_GaussianContext.png
Normal file
BIN
MVS/3DGS-Unity/Editor/Icons/d_GaussianContext.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
231
MVS/3DGS-Unity/Editor/Icons/d_GaussianContext.png.meta
Normal file
231
MVS/3DGS-Unity/Editor/Icons/d_GaussianContext.png.meta
Normal 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:
|
||||
BIN
MVS/3DGS-Unity/Editor/Icons/d_GaussianContext@2x.png
Normal file
BIN
MVS/3DGS-Unity/Editor/Icons/d_GaussianContext@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
153
MVS/3DGS-Unity/Editor/Icons/d_GaussianContext@2x.png.meta
Normal file
153
MVS/3DGS-Unity/Editor/Icons/d_GaussianContext@2x.png.meta
Normal 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:
|
||||
8
MVS/3DGS-Unity/Editor/Utils.meta
Normal file
8
MVS/3DGS-Unity/Editor/Utils.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f812890ad0ea4c747bdc67b6d2c1c627
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
26
MVS/3DGS-Unity/Editor/Utils/CaptureScreenshot.cs
Normal file
26
MVS/3DGS-Unity/Editor/Utils/CaptureScreenshot.cs
Normal 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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
11
MVS/3DGS-Unity/Editor/Utils/CaptureScreenshot.cs.meta
Normal file
11
MVS/3DGS-Unity/Editor/Utils/CaptureScreenshot.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c80a2b8daebbc1449b79e5ec436f39d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
274
MVS/3DGS-Unity/Editor/Utils/FilePickerControl.cs
Normal file
274
MVS/3DGS-Unity/Editor/Utils/FilePickerControl.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
MVS/3DGS-Unity/Editor/Utils/FilePickerControl.cs.meta
Normal file
11
MVS/3DGS-Unity/Editor/Utils/FilePickerControl.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 69e6c946494a9b2479ce96542339029c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
595
MVS/3DGS-Unity/Editor/Utils/KMeansClustering.cs
Normal file
595
MVS/3DGS-Unity/Editor/Utils/KMeansClustering.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
MVS/3DGS-Unity/Editor/Utils/KMeansClustering.cs.meta
Normal file
11
MVS/3DGS-Unity/Editor/Utils/KMeansClustering.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9cecadf9c980a4ad9a30d0e1ae09d16a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
107
MVS/3DGS-Unity/Editor/Utils/PLYFileReader.cs
Normal file
107
MVS/3DGS-Unity/Editor/Utils/PLYFileReader.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
11
MVS/3DGS-Unity/Editor/Utils/PLYFileReader.cs.meta
Normal file
11
MVS/3DGS-Unity/Editor/Utils/PLYFileReader.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 27964c85711004ddca73909489af2e2e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
403
MVS/3DGS-Unity/Editor/Utils/TinyJsonParser.cs
Normal file
403
MVS/3DGS-Unity/Editor/Utils/TinyJsonParser.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
11
MVS/3DGS-Unity/Editor/Utils/TinyJsonParser.cs.meta
Normal file
11
MVS/3DGS-Unity/Editor/Utils/TinyJsonParser.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e9ea5041388393f459c378c31e4d7b1f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user