diff --git a/Assets/GSTestScene.unity b/Assets/GSTestScene.unity index f50c1049..365301f6 100644 --- a/Assets/GSTestScene.unity +++ b/Assets/GSTestScene.unity @@ -345,7 +345,6 @@ MonoBehaviour: m_SortNthFrame: 1 m_RenderMode: 0 m_PointDisplaySize: 3 - m_RenderInSceneView: 1 m_ShaderSplats: {fileID: 4800000, guid: ed800126ae8844a67aad1974ddddd59c, type: 3} m_ShaderComposite: {fileID: 4800000, guid: 7e184af7d01193a408eb916d8acafff9, type: 3} m_ShaderDebugPoints: {fileID: 4800000, guid: b44409fc67214394f8f47e4e2648425e, type: 3} diff --git a/Assets/GaussianSplatting/Scripts/Editor/FilePickerPropertyDrawer.cs b/Assets/GaussianSplatting/Scripts/Editor/FilePickerPropertyDrawer.cs index 5b8030ed..3e83398c 100644 --- a/Assets/GaussianSplatting/Scripts/Editor/FilePickerPropertyDrawer.cs +++ b/Assets/GaussianSplatting/Scripts/Editor/FilePickerPropertyDrawer.cs @@ -23,8 +23,16 @@ public static string PathToDisplayString(string path) return ""; 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; } diff --git a/Assets/GaussianSplatting/Scripts/Editor/GaussianSplatAssetCreator.cs b/Assets/GaussianSplatting/Scripts/Editor/GaussianSplatAssetCreator.cs index 421ccd2b..17d6b818 100644 --- a/Assets/GaussianSplatting/Scripts/Editor/GaussianSplatAssetCreator.cs +++ b/Assets/GaussianSplatting/Scripts/Editor/GaussianSplatAssetCreator.cs @@ -16,6 +16,8 @@ public class GaussianSplatAssetCreator : EditorWindow { const string kProgressTitle = "Creating Gaussian Splat Asset"; const string kCamerasJson = "cameras.json"; + const string kPrefQuality = "nesnausk.GaussianSplatting.CreatorQuality"; + const string kPrefOutputFolder = "nesnausk.GaussianSplatting.CreatorOutputFolder"; enum DataQuality { @@ -61,6 +63,12 @@ public static void Init() window.Show(); } + void Awake() + { + m_Quality = (DataQuality)EditorPrefs.GetInt(kPrefQuality, (int)DataQuality.Medium); + m_OutputFolder = EditorPrefs.GetString(kPrefOutputFolder, "Assets/GaussianAssets"); + } + void OnEnable() { ApplyQualityLevel(); @@ -89,11 +97,18 @@ void OnGUI() EditorGUILayout.Space(); GUILayout.Label("Output", EditorStyles.boldLabel); rect = EditorGUILayout.GetControlRect(true); - m_OutputFolder = m_FilePicker.PathFieldGUI(rect, new GUIContent("Output Folder"), m_OutputFolder, null, "GaussianAssetOutputFolder"); + string newOutputFolder = m_FilePicker.PathFieldGUI(rect, new GUIContent("Output Folder"), m_OutputFolder, null, "GaussianAssetOutputFolder"); + if (newOutputFolder != m_OutputFolder) + { + m_OutputFolder = newOutputFolder; + EditorPrefs.SetString(kPrefOutputFolder, m_OutputFolder); + } + var newQuality = (DataQuality) EditorGUILayout.EnumPopup("Quality", m_Quality); if (newQuality != m_Quality) { m_Quality = newQuality; + EditorPrefs.SetInt(kPrefQuality, (int)m_Quality); ApplyQualityLevel(); } diff --git a/Assets/GaussianSplatting/Scripts/Editor/GaussianSplatRendererEditor.cs b/Assets/GaussianSplatting/Scripts/Editor/GaussianSplatRendererEditor.cs index 7394ee14..24188d39 100644 --- a/Assets/GaussianSplatting/Scripts/Editor/GaussianSplatRendererEditor.cs +++ b/Assets/GaussianSplatting/Scripts/Editor/GaussianSplatRendererEditor.cs @@ -1,12 +1,37 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Unity.Collections.LowLevel.Unsafe; using Unity.Mathematics; using UnityEditor; +using UnityEditor.EditorTools; using UnityEngine; +using UnityEngine.Experimental.Rendering; [CustomEditor(typeof(GaussianSplatRenderer))] public class GaussianSplatRendererEditor : Editor { int m_CameraIndex = 0; + static HashSet s_AllEditors = new(); + + public static void RepaintAll() + { + foreach (var e in s_AllEditors) + e.Repaint(); + } + + public void OnEnable() + { + s_AllEditors.Add(this); + } + + public void OnDisable() + { + s_AllEditors.Remove(this); + } + public override void OnInspectorGUI() { DrawDefaultInspector(); @@ -31,15 +56,11 @@ public override void OnInspectorGUI() } var asset = gs.asset; - EditorGUILayout.Space(); - GUILayout.Label("Stats / Controls", EditorStyles.boldLabel); - { - using var _ = new EditorGUI.DisabledScope(true); - EditorGUILayout.IntField("Splat Count", asset.m_SplatCount); - } var cameras = asset.m_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) @@ -48,5 +69,315 @@ public override void OnInspectorGUI() gs.ActivateCamera(camIndex); } } + + if (gs.editModified || gs.editSelectedSplats != 0 || gs.editDeletedSplats != 0) + { + EditorGUILayout.Space(); + GUILayout.Label("Editing", EditorStyles.boldLabel); + EditorGUILayout.LabelField("Splats", $"{asset.m_SplatCount:N0}"); + EditorGUILayout.LabelField("Deleted", $"{gs.editDeletedSplats:N0}"); + EditorGUILayout.LabelField("Selected", $"{gs.editSelectedSplats:N0}"); + if (gs.editModified) + { + if (GUILayout.Button("Export modified PLY")) + ExportPlyFile(gs); + if (asset.m_PosFormat > GaussianSplatAsset.VectorFormat.Norm16 || + asset.m_ScaleFormat > GaussianSplatAsset.VectorFormat.Norm16 || + !GraphicsFormatUtility.IsFloatFormat(asset.m_ColorFormat) || + asset.m_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 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.m_BoundsMin, gs.asset.m_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) + { + var path = EditorUtility.SaveFilePanel( + "Export Gaussian Splat PLY file", "", $"{gs.asset.name}-edit.ply", "ply"); + if (string.IsNullOrWhiteSpace(path)) + return; + int kSplatSize = UnsafeUtility.SizeOf(); + using var gpuData = new GraphicsBuffer(GraphicsBuffer.Target.Structured, gs.asset.m_SplatCount, kSplatSize); + if (!gs.EditExportData(gpuData)) + return; + + GaussianSplatAssetCreator.InputSplatData[] data = new GaussianSplatAssetCreator.InputSplatData[gpuData.count]; + gpuData.GetData(data); + + var gpuDeleted = gs.gpuSplatDeletedBuffer; + 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; + if ((deleted[wordIdx] & (1u << bitIdx)) == 0) + ++aliveCount; + } + + using FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write); + var header = $@"ply +format binary_little_endian 1.0 +element vertex {aliveCount} +property float x +property float y +property float z +property float nx +property float ny +property float nz +property float f_dc_0 +property float f_dc_1 +property float f_dc_2 +property float f_rest_0 +property float f_rest_1 +property float f_rest_2 +property float f_rest_3 +property float f_rest_4 +property float f_rest_5 +property float f_rest_6 +property float f_rest_7 +property float f_rest_8 +property float f_rest_9 +property float f_rest_10 +property float f_rest_11 +property float f_rest_12 +property float f_rest_13 +property float f_rest_14 +property float f_rest_15 +property float f_rest_16 +property float f_rest_17 +property float f_rest_18 +property float f_rest_19 +property float f_rest_20 +property float f_rest_21 +property float f_rest_22 +property float f_rest_23 +property float f_rest_24 +property float f_rest_25 +property float f_rest_26 +property float f_rest_27 +property float f_rest_28 +property float f_rest_29 +property float f_rest_30 +property float f_rest_31 +property float f_rest_32 +property float f_rest_33 +property float f_rest_34 +property float f_rest_35 +property float f_rest_36 +property float f_rest_37 +property float f_rest_38 +property float f_rest_39 +property float f_rest_40 +property float f_rest_41 +property float f_rest_42 +property float f_rest_43 +property float f_rest_44 +property float opacity +property float scale_0 +property float scale_1 +property float scale_2 +property float rot_0 +property float rot_1 +property float rot_2 +property float rot_3 +end_header +"; + fs.Write(Encoding.UTF8.GetBytes(header)); + for (int i = 0; i < data.Length; ++i) + { + int wordIdx = i >> 5; + int bitIdx = i & 31; + if ((deleted[wordIdx] & (1u << bitIdx)) == 0) + { + var splat = data[i]; + byte* ptr = (byte*)&splat; + fs.Write(new ReadOnlySpan(ptr, kSplatSize)); + } + } } } + +[EditorTool("GaussianSplats Tool", typeof(GaussianSplatRenderer))] +class GaussianSplatsTool : EditorTool +{ + Vector2 m_MouseStartDragPos; + + public override GUIContent toolbarIcon => EditorGUIUtility.TrIconContent("EditCollider", "Edit Gaussian Splats"); + + 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; + } + } + + public override void OnToolGUI(EditorWindow window) + { + if (!(window is SceneView sceneView)) + return; + var gs = target as GaussianSplatRenderer; + if (!gs) + return; + + 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 (HandleUtility.nearestControl == id && evt.button == 0 && !evt.alt) // ignore Alt to allow orbiting scene view + { + // shift/command adds to selection + if (!evt.shift && !EditorGUI.actionKey) + gs.EditDeselectAll(); + + // record selection state at start + gs.EditStoreInitialSelection(); + 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); + 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: + if (gs.editSelectedSplats > 0) + { + var selBounds = GaussianSplatRendererEditor.TransformBounds(gs.transform, gs.editSelectedBounds); + Handles.color = new Color(1,0,1,0.7f); + Handles.DrawWireCube(selBounds.center, selBounds.size); + } + 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); + } +} + diff --git a/Assets/GaussianSplatting/Scripts/GaussianSplatRenderer.cs b/Assets/GaussianSplatting/Scripts/GaussianSplatRenderer.cs index 4c8e4596..325f049a 100644 --- a/Assets/GaussianSplatting/Scripts/GaussianSplatRenderer.cs +++ b/Assets/GaussianSplatting/Scripts/GaussianSplatRenderer.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using Unity.Collections.LowLevel.Unsafe; +using Unity.Mathematics; using Unity.Profiling; using Unity.Profiling.LowLevel; using UnityEngine; @@ -69,8 +71,6 @@ public bool GatherSplatsForCamera(Camera cam) var gs = kvp.Key; if (gs == null || !gs.isActiveAndEnabled || !gs.HasValidAsset || !gs.HasValidRenderSetup) continue; - if (!gs.m_RenderInSceneView && cam.cameraType == CameraType.SceneView) - continue; m_ActiveSplats.Add((kvp.Key, kvp.Value)); } if (m_ActiveSplats.Count == 0) @@ -221,7 +221,6 @@ public enum RenderMode public RenderMode m_RenderMode = RenderMode.Splats; [Range(1.0f,15.0f)] public float m_PointDisplaySize = 3.0f; - public bool m_RenderInSceneView = true; [Header("Resources")] @@ -243,6 +242,10 @@ public enum RenderMode internal GraphicsBuffer m_GpuChunks; internal GraphicsBuffer m_GpuView; internal GraphicsBuffer m_GpuIndexBuffer; + GraphicsBuffer m_GpuSplatSelectedInitBuffer; + GraphicsBuffer m_GpuSplatSelectedBuffer; + GraphicsBuffer m_GpuSplatDeletedBuffer; + GraphicsBuffer m_GpuSplatEditDataBuffer; FfxParallelSort m_SorterFfx; FfxParallelSort.Args m_SorterFfxArgs; @@ -256,11 +259,30 @@ public enum RenderMode GaussianSplatAsset m_PrevAsset; Hash128 m_PrevHash; - static ProfilerMarker s_ProfSort = new ProfilerMarker(ProfilerCategory.Render, "GaussianSplat.Sort", MarkerFlags.SampleGPU); - static ProfilerMarker s_ProfView = new ProfilerMarker(ProfilerCategory.Render, "GaussianSplat.View", MarkerFlags.SampleGPU); + static readonly ProfilerMarker s_ProfSort = new(ProfilerCategory.Render, "GaussianSplat.Sort", MarkerFlags.SampleGPU); + static readonly ProfilerMarker s_ProfView = new(ProfilerCategory.Render, "GaussianSplat.View", MarkerFlags.SampleGPU); + + [field: NonSerialized] public bool editModified { get; private set; } + [field: NonSerialized] public uint editSelectedSplats { get; private set; } + [field: NonSerialized] public uint editDeletedSplats { get; private set; } + [field: NonSerialized] public Bounds editSelectedBounds { get; private set; } public GaussianSplatAsset asset => m_Asset; + enum KernelIndices : int + { + SetIndices, + CalcDistances, + CalcViewData, + UpdateEditData, + InitEditData, + ClearBuffer, + InvertBuffer, + OrBuffers, + SelectionUpdate, + ExportData, + } + public bool HasValidAsset => m_Asset != null && m_Asset.m_SplatCount > 0 && @@ -306,10 +328,10 @@ void CreateResourcesForAsset() m_GpuSortKeys = new GraphicsBuffer(GraphicsBuffer.Target.Structured, m_Asset.m_SplatCount, 4) { name = "GaussianSplatSortIndices" }; // init keys buffer to splat indices - m_CSSplatUtilities.SetBuffer(0, "_SplatSortKeys", m_GpuSortKeys); + m_CSSplatUtilities.SetBuffer((int)KernelIndices.SetIndices, "_SplatSortKeys", m_GpuSortKeys); m_CSSplatUtilities.SetInt("_SplatCount", m_GpuSortDistances.count); - m_CSSplatUtilities.GetKernelThreadGroupSizes(0, out uint gsX, out uint gsY, out uint gsZ); - m_CSSplatUtilities.Dispatch(0, (m_GpuSortDistances.count + (int)gsX - 1)/(int)gsX, 1, 1); + m_CSSplatUtilities.GetKernelThreadGroupSizes((int)KernelIndices.SetIndices, out uint gsX, out _, out _); + m_CSSplatUtilities.Dispatch((int)KernelIndices.SetIndices, (m_GpuSortDistances.count + (int)gsX - 1)/(int)gsX, 1, 1); m_SorterFfxArgs.inputKeys = m_GpuSortDistances; m_SorterFfxArgs.inputValues = m_GpuSortKeys; @@ -343,6 +365,9 @@ void SetAssetDataOnCS(CommandBuffer cmb, ComputeShader cs, int kernelIndex) cmb.SetComputeBufferParam(cs, kernelIndex, "_SplatOther", m_GpuOtherData); cmb.SetComputeBufferParam(cs, kernelIndex, "_SplatSH", m_GpuSHData); cmb.SetComputeTextureParam(cs, kernelIndex, "_SplatColor", m_GpuColorData); + cmb.SetComputeBufferParam(cs, kernelIndex, "_SplatSelectedBits", m_GpuSplatSelectedBuffer ?? m_GpuPosData); + cmb.SetComputeBufferParam(cs, kernelIndex, "_SplatDeletedBits", m_GpuSplatDeletedBuffer ?? m_GpuPosData); + cmb.SetComputeIntParam(cs, "_SplatBitsValid", m_GpuSplatSelectedBuffer != null && m_GpuSplatDeletedBuffer != null ? 1 : 0); uint format = (uint)m_Asset.m_PosFormat | ((uint)m_Asset.m_ScaleFormat << 8) | ((uint)m_Asset.m_SHFormat << 16); cmb.SetComputeIntParam(cs, "_SplatFormat", (int)format); } @@ -353,6 +378,9 @@ internal void SetAssetDataOnMaterial(MaterialPropertyBlock mat) mat.SetBuffer("_SplatOther", m_GpuOtherData); mat.SetBuffer("_SplatSH", m_GpuSHData); mat.SetTexture("_SplatColor", m_GpuColorData); + mat.SetBuffer("_SplatSelectedBits", m_GpuSplatSelectedBuffer ?? m_GpuPosData); + mat.SetBuffer("_SplatDeletedBits", m_GpuSplatDeletedBuffer ?? m_GpuPosData); + mat.SetInt("_SplatBitsValid", m_GpuSplatSelectedBuffer != null && m_GpuSplatDeletedBuffer != null ? 1 : 0); uint format = (uint)m_Asset.m_PosFormat | ((uint)m_Asset.m_ScaleFormat << 8) | ((uint)m_Asset.m_SHFormat << 16); mat.SetInteger("_SplatFormat", (int)format); } @@ -368,6 +396,10 @@ void DisposeResourcesForAsset() m_GpuIndexBuffer?.Dispose(); m_GpuSortDistances?.Dispose(); m_GpuSortKeys?.Dispose(); + m_GpuSplatSelectedInitBuffer?.Dispose(); + m_GpuSplatSelectedBuffer?.Dispose(); + m_GpuSplatDeletedBuffer?.Dispose(); + m_GpuSplatEditDataBuffer?.Dispose(); m_SorterFfxArgs.resources.Dispose(); m_GpuPosData = null; @@ -379,6 +411,15 @@ void DisposeResourcesForAsset() m_GpuIndexBuffer = null; m_GpuSortDistances = null; m_GpuSortKeys = null; + m_GpuSplatSelectedInitBuffer = null; + m_GpuSplatSelectedBuffer = null; + m_GpuSplatDeletedBuffer = null; + m_GpuSplatEditDataBuffer = null; + + editSelectedSplats = 0; + editDeletedSplats = 0; + editModified = false; + editSelectedBounds = default; } public void OnDisable() @@ -394,7 +435,7 @@ public void OnDisable() internal void CalcViewData(CommandBuffer cmb, Camera cam, Matrix4x4 matrix) { - if (cam.cameraType == CameraType.Preview || !m_RenderInSceneView && cam.cameraType == CameraType.SceneView) + if (cam.cameraType == CameraType.Preview) return; using var prof = s_ProfView.Auto(); @@ -410,13 +451,12 @@ internal void CalcViewData(CommandBuffer cmb, Camera cam, Matrix4x4 matrix) Vector4 camPos = cam.transform.position; // calculate view dependent data for each splat - const int kernelIdx = 2; - SetAssetDataOnCS(cmb, m_CSSplatUtilities, kernelIdx); + SetAssetDataOnCS(cmb, m_CSSplatUtilities, (int)KernelIndices.CalcViewData); cmb.SetComputeIntParam(m_CSSplatUtilities, "_SplatCount", m_GpuView.count); - cmb.SetComputeBufferParam(m_CSSplatUtilities, kernelIdx, "_SplatViewData", m_GpuView); - cmb.SetComputeBufferParam(m_CSSplatUtilities, kernelIdx, "_OrderBuffer", m_GpuSortKeys); - cmb.SetComputeBufferParam(m_CSSplatUtilities, kernelIdx, "_SplatChunks", m_GpuChunks); + cmb.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.CalcViewData, "_SplatViewData", m_GpuView); + cmb.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.CalcViewData, "_OrderBuffer", m_GpuSortKeys); + cmb.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.CalcViewData, "_SplatChunks", m_GpuChunks); cmb.SetComputeMatrixParam(m_CSSplatUtilities, "_MatrixVP", matProj * matView); cmb.SetComputeMatrixParam(m_CSSplatUtilities, "_MatrixMV", matView * matO2W); @@ -430,13 +470,13 @@ internal void CalcViewData(CommandBuffer cmb, Camera cam, Matrix4x4 matrix) cmb.SetComputeFloatParam(m_CSSplatUtilities, "_SplatOpacityScale", m_OpacityScale); cmb.SetComputeIntParam(m_CSSplatUtilities, "_SHOrder", m_SHOrder); - m_CSSplatUtilities.GetKernelThreadGroupSizes(kernelIdx, out uint gsX, out uint gsY, out uint gsZ); - cmb.DispatchCompute(m_CSSplatUtilities, kernelIdx, (m_GpuView.count + (int)gsX - 1)/(int)gsX, 1, 1); + m_CSSplatUtilities.GetKernelThreadGroupSizes((int)KernelIndices.CalcViewData, out uint gsX, out uint gsY, out uint gsZ); + cmb.DispatchCompute(m_CSSplatUtilities, (int)KernelIndices.CalcViewData, (m_GpuView.count + (int)gsX - 1)/(int)gsX, 1, 1); } internal void SortPoints(CommandBuffer cmd, Camera cam, Matrix4x4 matrix) { - if (cam.cameraType == CameraType.Preview || !m_RenderInSceneView && cam.cameraType == CameraType.SceneView) + if (cam.cameraType == CameraType.Preview) return; Matrix4x4 worldToCamMatrix = cam.worldToCameraMatrix; @@ -445,18 +485,17 @@ internal void SortPoints(CommandBuffer cmd, Camera cam, Matrix4x4 matrix) worldToCamMatrix.m22 *= -1; // calculate distance to the camera for each splat - int kernelIdx = 1; cmd.BeginSample(s_ProfSort); - cmd.SetComputeBufferParam(m_CSSplatUtilities, kernelIdx, "_SplatSortDistances", m_GpuSortDistances); - cmd.SetComputeBufferParam(m_CSSplatUtilities, kernelIdx, "_SplatSortKeys", m_GpuSortKeys); - cmd.SetComputeBufferParam(m_CSSplatUtilities, kernelIdx, "_SplatChunks", m_GpuChunks); - cmd.SetComputeBufferParam(m_CSSplatUtilities, kernelIdx, "_SplatPos", m_GpuPosData); + cmd.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.CalcDistances, "_SplatSortDistances", m_GpuSortDistances); + cmd.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.CalcDistances, "_SplatSortKeys", m_GpuSortKeys); + cmd.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.CalcDistances, "_SplatChunks", m_GpuChunks); + cmd.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.CalcDistances, "_SplatPos", m_GpuPosData); cmd.SetComputeIntParam(m_CSSplatUtilities, "_SplatFormat", (int)m_Asset.m_PosFormat); cmd.SetComputeMatrixParam(m_CSSplatUtilities, "_LocalToWorldMatrix", matrix); cmd.SetComputeMatrixParam(m_CSSplatUtilities, "_WorldToCameraMatrix", worldToCamMatrix); cmd.SetComputeIntParam(m_CSSplatUtilities, "_SplatCount", m_Asset.m_SplatCount); - m_CSSplatUtilities.GetKernelThreadGroupSizes(kernelIdx, out uint gsX, out _, out _); - cmd.DispatchCompute(m_CSSplatUtilities, kernelIdx, (m_GpuSortDistances.count + (int)gsX - 1)/(int)gsX, 1, 1); + m_CSSplatUtilities.GetKernelThreadGroupSizes((int)KernelIndices.CalcDistances, out uint gsX, out _, out _); + cmd.DispatchCompute(m_CSSplatUtilities, (int)KernelIndices.CalcDistances, (m_GpuSortDistances.count + (int)gsX - 1)/(int)gsX, 1, 1); // sort the splats m_SorterFfx.Dispatch(cmd, m_SorterFfxArgs); @@ -474,7 +513,7 @@ public void Update() CreateResourcesForAsset(); } } - + public void ActivateCamera(int index) { Camera mainCam = Camera.main; @@ -496,4 +535,190 @@ public void ActivateCamera(int index) UnityEditor.EditorUtility.SetDirty(camTr); #endif } + + void ClearGraphicsBuffer(GraphicsBuffer buf, uint value = 0) + { + m_CSSplatUtilities.SetBuffer((int)KernelIndices.ClearBuffer, "_DstBuffer", buf); + m_CSSplatUtilities.SetInt("_BufferSize", buf.count); + m_CSSplatUtilities.SetInt("_DstBufferValue", (int)value); + m_CSSplatUtilities.GetKernelThreadGroupSizes((int)KernelIndices.ClearBuffer, out uint gsX, out _, out _); + m_CSSplatUtilities.Dispatch((int)KernelIndices.ClearBuffer, (int)((buf.count+gsX-1)/gsX), 1, 1); + } + + void InvertGraphicsBuffer(GraphicsBuffer buf) + { + m_CSSplatUtilities.SetBuffer((int)KernelIndices.InvertBuffer, "_DstBuffer", buf); + m_CSSplatUtilities.SetInt("_BufferSize", buf.count); + m_CSSplatUtilities.GetKernelThreadGroupSizes((int)KernelIndices.InvertBuffer, out uint gsX, out _, out _); + m_CSSplatUtilities.Dispatch((int)KernelIndices.InvertBuffer, (int)((buf.count+gsX-1)/gsX), 1, 1); + } + + void UnionGraphicsBuffers(GraphicsBuffer dst, GraphicsBuffer src) + { + m_CSSplatUtilities.SetBuffer((int)KernelIndices.OrBuffers, "_SrcBuffer", src); + m_CSSplatUtilities.SetBuffer((int)KernelIndices.OrBuffers, "_DstBuffer", dst); + m_CSSplatUtilities.SetInt("_BufferSize", dst.count); + m_CSSplatUtilities.GetKernelThreadGroupSizes((int)KernelIndices.OrBuffers, out uint gsX, out _, out _); + m_CSSplatUtilities.Dispatch((int)KernelIndices.OrBuffers, (int)((dst.count+gsX-1)/gsX), 1, 1); + } + + static float SortableUintToFloat(uint v) + { + uint mask = ((v >> 31) - 1) | 0x80000000u; + return math.asfloat(v ^ mask); + } + + void UpdateEditCountsAndBounds() + { + if (m_GpuSplatSelectedBuffer == null) + { + editSelectedSplats = 0; + editDeletedSplats = 0; + editModified = false; + editSelectedBounds = default; + return; + } + + m_CSSplatUtilities.SetBuffer((int)KernelIndices.InitEditData, "_DstBuffer", m_GpuSplatEditDataBuffer); + m_CSSplatUtilities.Dispatch((int)KernelIndices.InitEditData, 1, 1, 1); + + using CommandBuffer cmb = new CommandBuffer(); + SetAssetDataOnCS(cmb, m_CSSplatUtilities, (int)KernelIndices.UpdateEditData); + cmb.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.UpdateEditData, "_SplatChunks", m_GpuChunks); + cmb.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.UpdateEditData, "_SplatSelectedBits", m_GpuSplatSelectedBuffer); + cmb.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.UpdateEditData, "_SplatDeletedBits", m_GpuSplatDeletedBuffer); + cmb.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.UpdateEditData, "_DstBuffer", m_GpuSplatEditDataBuffer); + cmb.SetComputeIntParam(m_CSSplatUtilities, "_BufferSize", m_GpuSplatSelectedBuffer.count); + cmb.SetComputeIntParam(m_CSSplatUtilities, "_SplatCount", m_Asset.m_SplatCount); + m_CSSplatUtilities.GetKernelThreadGroupSizes((int)KernelIndices.UpdateEditData, out uint gsX, out _, out _); + cmb.DispatchCompute(m_CSSplatUtilities, (int)KernelIndices.UpdateEditData, (int)((m_GpuSplatSelectedBuffer.count+gsX-1)/gsX), 1, 1); + Graphics.ExecuteCommandBuffer(cmb); + + uint[] res = new uint[m_GpuSplatEditDataBuffer.count]; + m_GpuSplatEditDataBuffer.GetData(res); + editSelectedSplats = res[0]; + editDeletedSplats = res[1]; + Vector3 bmin = new Vector3(SortableUintToFloat(res[2]), SortableUintToFloat(res[3]), SortableUintToFloat(res[4])); + Vector3 bmax = new Vector3(SortableUintToFloat(res[5]), SortableUintToFloat(res[6]), SortableUintToFloat(res[7])); + Bounds bounds = default; + bounds.SetMinMax(bmin, bmax); + if (bounds.extents.sqrMagnitude < 0.01) + bounds.extents = new Vector3(0.1f,0.1f,0.1f); + editSelectedBounds = bounds; + } + + bool EnsureSelectionBuffers() + { + if (!HasValidAsset || !HasValidRenderSetup) + return false; + + if (m_GpuSplatSelectedBuffer == null) + { + var target = GraphicsBuffer.Target.Raw | GraphicsBuffer.Target.CopySource | + GraphicsBuffer.Target.CopyDestination; + var size = (m_Asset.m_SplatCount + 31) / 32; + m_GpuSplatSelectedBuffer = new GraphicsBuffer(target, size, 4) {name = "GaussianSplatSelected"}; + m_GpuSplatSelectedInitBuffer = new GraphicsBuffer(target, size, 4) {name = "GaussianSplatSelectedInit"}; + m_GpuSplatDeletedBuffer = new GraphicsBuffer(target, size, 4) {name = "GaussianSplatDeleted"}; + m_GpuSplatEditDataBuffer = new GraphicsBuffer(target, 1 + 1 + 6, 4) {name = "GaussianSplatEditData"}; // selected count, deleted bound, float3 min, float3 max + ClearGraphicsBuffer(m_GpuSplatSelectedBuffer); + ClearGraphicsBuffer(m_GpuSplatSelectedInitBuffer); + ClearGraphicsBuffer(m_GpuSplatDeletedBuffer); + } + return m_GpuSplatSelectedBuffer != null; + } + + public void EditStoreInitialSelection() + { + if (!EnsureSelectionBuffers()) return; + Graphics.CopyBuffer(m_GpuSplatSelectedBuffer, m_GpuSplatSelectedInitBuffer); + } + + public void EditUpdateSelection(Vector2 rectMin, Vector2 rectMax, Camera cam) + { + if (!EnsureSelectionBuffers()) return; + + Graphics.CopyBuffer(m_GpuSplatSelectedInitBuffer, m_GpuSplatSelectedBuffer); + + var tr = transform; + Matrix4x4 matView = cam.worldToCameraMatrix; + Matrix4x4 matProj = GL.GetGPUProjectionMatrix(cam.projectionMatrix, true); + Matrix4x4 matO2W = tr.localToWorldMatrix; + Matrix4x4 matW2O = tr.worldToLocalMatrix; + int screenW = cam.pixelWidth, screenH = cam.pixelHeight; + Vector4 screenPar = new Vector4(screenW, screenH, 0, 0); + Vector4 camPos = cam.transform.position; + + var cmb = new CommandBuffer { name = "SplatSelectionUpdate" }; + SetAssetDataOnCS(cmb, m_CSSplatUtilities, (int)KernelIndices.SelectionUpdate); + cmb.SetComputeIntParam(m_CSSplatUtilities, "_SplatCount", m_Asset.m_SplatCount); + cmb.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.SelectionUpdate, "_SplatChunks", m_GpuChunks); + + cmb.SetComputeMatrixParam(m_CSSplatUtilities, "_MatrixVP", matProj * matView); + cmb.SetComputeMatrixParam(m_CSSplatUtilities, "_MatrixMV", matView * matO2W); + cmb.SetComputeMatrixParam(m_CSSplatUtilities, "_MatrixP", matProj); + cmb.SetComputeMatrixParam(m_CSSplatUtilities, "_MatrixObjectToWorld", matO2W); + cmb.SetComputeMatrixParam(m_CSSplatUtilities, "_MatrixWorldToObject", matW2O); + + cmb.SetComputeVectorParam(m_CSSplatUtilities, "_VecScreenParams", screenPar); + cmb.SetComputeVectorParam(m_CSSplatUtilities, "_VecWorldSpaceCameraPos", camPos); + + cmb.SetComputeVectorParam(m_CSSplatUtilities, "_SelectionRect", new Vector4(rectMin.x, rectMax.y, rectMax.x, rectMin.y)); + + m_CSSplatUtilities.GetKernelThreadGroupSizes((int)KernelIndices.SelectionUpdate, out uint gsX, out _, out _); + cmb.DispatchCompute(m_CSSplatUtilities, (int)KernelIndices.SelectionUpdate, (m_Asset.m_SplatCount + (int)gsX - 1)/(int)gsX, 1, 1); + Graphics.ExecuteCommandBuffer(cmb); + cmb.Dispose(); + UpdateEditCountsAndBounds(); + } + + public void EditDeleteSelected() + { + if (!EnsureSelectionBuffers()) return; + UnionGraphicsBuffers(m_GpuSplatDeletedBuffer, m_GpuSplatSelectedBuffer); + EditDeselectAll(); + UpdateEditCountsAndBounds(); + if (editDeletedSplats != 0) + editModified = true; + } + + public void EditSelectAll() + { + if (!EnsureSelectionBuffers()) return; + ClearGraphicsBuffer(m_GpuSplatSelectedBuffer, ~0u); + UpdateEditCountsAndBounds(); + } + + public void EditDeselectAll() + { + if (!EnsureSelectionBuffers()) return; + ClearGraphicsBuffer(m_GpuSplatSelectedBuffer); + UpdateEditCountsAndBounds(); + } + + public void EditInvertSelection() + { + if (!EnsureSelectionBuffers()) return; + InvertGraphicsBuffer(m_GpuSplatSelectedBuffer); + UpdateEditCountsAndBounds(); + } + + public bool EditExportData(GraphicsBuffer dstData) + { + if (!EnsureSelectionBuffers()) return false; + + var cmb = new CommandBuffer { name = "SplatExportData" }; + SetAssetDataOnCS(cmb, m_CSSplatUtilities, (int)KernelIndices.ExportData); + cmb.SetComputeIntParam(m_CSSplatUtilities, "_SplatCount", m_Asset.m_SplatCount); + cmb.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.ExportData, "_SplatChunks", m_GpuChunks); + cmb.SetComputeBufferParam(m_CSSplatUtilities, (int)KernelIndices.ExportData, "_ExportBuffer", dstData); + + m_CSSplatUtilities.GetKernelThreadGroupSizes((int)KernelIndices.ExportData, out uint gsX, out _, out _); + cmb.DispatchCompute(m_CSSplatUtilities, (int)KernelIndices.ExportData, (m_Asset.m_SplatCount + (int)gsX - 1)/(int)gsX, 1, 1); + Graphics.ExecuteCommandBuffer(cmb); + cmb.Dispose(); + return true; + } + + public GraphicsBuffer gpuSplatDeletedBuffer => m_GpuSplatDeletedBuffer; } diff --git a/Assets/GaussianSplatting/Shaders/GaussianSplatting.hlsl b/Assets/GaussianSplatting/Shaders/GaussianSplatting.hlsl index 4b9c6514..6b4ee62d 100644 --- a/Assets/GaussianSplatting/Shaders/GaussianSplatting.hlsl +++ b/Assets/GaussianSplatting/Shaders/GaussianSplatting.hlsl @@ -344,7 +344,7 @@ float3 LoadAndDecodeVector(ByteAddressBuffer dataBuffer, uint addrU, uint fmt) return res; } -float3 LoadSplatPos(uint index) +float3 LoadSplatPosValue(uint index) { uint fmt = _SplatFormat & 0xFF; uint stride = 0; @@ -359,6 +359,17 @@ float3 LoadSplatPos(uint index) return LoadAndDecodeVector(_SplatPos, index * stride, fmt); } +float3 LoadSplatPos(uint idx) +{ + uint chunkIdx = idx / kChunkSize; + SplatChunkInfo chunk = _SplatChunks[chunkIdx]; + float3 posMin = float3(chunk.posX.x, chunk.posY.x, chunk.posZ.x); + float3 posMax = float3(chunk.posX.y, chunk.posY.y, chunk.posZ.y); + float3 pos = LoadSplatPosValue(idx); + pos = lerp(posMin, posMax, pos); + return pos; +} + half4 LoadSplatColTex(uint3 coord) { return _SplatColor.Load(coord); @@ -380,7 +391,7 @@ SplatData LoadSplatData(uint idx) half3 shMin = half3(f16tof32(chunk.shR ), f16tof32(chunk.shG ), f16tof32(chunk.shB )); half3 shMax = half3(f16tof32(chunk.shR>>16), f16tof32(chunk.shG>>16), f16tof32(chunk.shB>>16)); - s.pos = lerp(posMin, posMax, LoadSplatPos(idx)); + s.pos = lerp(posMin, posMax, LoadSplatPosValue(idx)); uint scaleFmt = (_SplatFormat >> 8) & 0xFF; uint shFormat = (_SplatFormat >> 16) & 0xFF; diff --git a/Assets/GaussianSplatting/Shaders/RenderGaussianSplats.shader b/Assets/GaussianSplatting/Shaders/RenderGaussianSplats.shader index 6a7954dd..42db7abd 100644 --- a/Assets/GaussianSplatting/Shaders/RenderGaussianSplats.shader +++ b/Assets/GaussianSplatting/Shaders/RenderGaussianSplats.shader @@ -23,12 +23,14 @@ StructuredBuffer _OrderBuffer; struct v2f { half4 col : COLOR0; - float2 centerScreenPos : TEXCOORD3; - float3 conic : TEXCOORD4; + float2 centerScreenPos : TEXCOORD0; + float3 conic : TEXCOORD1; float4 vertex : SV_POSITION; }; StructuredBuffer _SplatViewData; +ByteAddressBuffer _SplatSelectedBits; +uint _SplatBitsValid; v2f vert (uint vtxID : SV_VertexID, uint instID : SV_InstanceID) { @@ -59,6 +61,18 @@ v2f vert (uint vtxID : SV_VertexID, uint instID : SV_InstanceID) float2 deltaScreenPos = quadPos * radius * 2 / _ScreenParams.xy; o.vertex = centerClipPos; o.vertex.xy += deltaScreenPos * centerClipPos.w; + + // is this splat selected? + if (_SplatBitsValid) + { + uint wordIdx = instID / 32; + uint bitIdx = instID & 31; + uint selVal = _SplatSelectedBits.Load(wordIdx * 4); + if (selVal & (1 << bitIdx)) + { + o.col.a = -1; + } + } } return o; } @@ -68,7 +82,26 @@ half4 frag (v2f i) : SV_Target float2 d = CalcScreenSpaceDelta(i.vertex.xy, i.centerScreenPos, _ProjectionParams); float power = CalcPowerFromConic(i.conic, d); half alpha = exp(power); - alpha = saturate(alpha * i.col.a); + if (i.col.a >= 0) + { + alpha = saturate(alpha * i.col.a); + } + else + { + // "selected" splat: magenta outline, increase opacity, magenta tint + half3 selectedColor = half3(1,0,1); + if (alpha > 7.0/255.0) + { + if (alpha < 10.0/255.0) + { + alpha = 1; + i.col.rgb = selectedColor; + } + alpha = saturate(alpha + 0.3); + } + i.col.rgb = lerp(i.col.rgb, selectedColor, 0.5); + } + if (alpha < 1.0/255.0) discard; diff --git a/Assets/GaussianSplatting/Shaders/SplatUtilities.compute b/Assets/GaussianSplatting/Shaders/SplatUtilities.compute index b9d7a201..167b9d38 100644 --- a/Assets/GaussianSplatting/Shaders/SplatUtilities.compute +++ b/Assets/GaussianSplatting/Shaders/SplatUtilities.compute @@ -3,6 +3,13 @@ #pragma kernel CSSetIndices #pragma kernel CSCalcDistances #pragma kernel CSCalcViewData +#pragma kernel CSUpdateEditData +#pragma kernel CSInitEditData +#pragma kernel CSClearBuffer +#pragma kernel CSInvertBuffer +#pragma kernel CSOrBuffers +#pragma kernel CSSelectionUpdate +#pragma kernel CSExportData #pragma use_dxc @@ -14,6 +21,14 @@ RWStructuredBuffer _SplatSortDistances; RWStructuredBuffer _SplatSortKeys; uint _SplatCount; +// radix sort etc. friendly, see http://stereopsis.com/radix.html +uint FloatToSortableUint(float f) +{ + uint fu = asuint(f); + uint mask = -((int)(fu >> 31)) | 0x80000000; + return fu ^ mask; +} + [numthreads(GROUP_SIZE,1,1)] void CSSetIndices (uint3 id : SV_DispatchThreadID) { @@ -33,20 +48,11 @@ void CSCalcDistances (uint3 id : SV_DispatchThreadID) uint origIdx = _SplatSortKeys[idx]; - uint chunkIdx = origIdx / kChunkSize; - SplatChunkInfo chunk = _SplatChunks[chunkIdx]; - float3 posMin = float3(chunk.posX.x, chunk.posY.x, chunk.posZ.x); - float3 posMax = float3(chunk.posX.y, chunk.posY.y, chunk.posZ.y); - float3 pos = LoadSplatPos(origIdx); - pos = lerp(posMin, posMax, pos); pos = mul(_LocalToWorldMatrix, float4(pos.xyz, 1)).xyz; pos = mul(_WorldToCameraMatrix, float4(pos.xyz, 1)).xyz; - // make distance radix sort friendly from http://stereopsis.com/radix.html - uint fu = asuint(pos.z); - uint mask = -((int)(fu >> 31)) | 0x80000000; - _SplatSortDistances[idx] = fu ^ mask; + _SplatSortDistances[idx] = FloatToSortableUint(pos.z); } RWStructuredBuffer _SplatViewData; @@ -63,6 +69,11 @@ float _SplatScale; float _SplatOpacityScale; uint _SHOrder; +RWByteAddressBuffer _SplatSelectedBits; +ByteAddressBuffer _SplatDeletedBits; +uint _SplatBitsValid; + + [numthreads(GROUP_SIZE,1,1)] void CSCalcViewData (uint3 id : SV_DispatchThreadID) { @@ -75,6 +86,19 @@ void CSCalcViewData (uint3 id : SV_DispatchThreadID) float3 centerWorldPos = mul(_MatrixObjectToWorld, float4(splat.pos,1)).xyz; float4 centerClipPos = mul(_MatrixVP, float4(centerWorldPos, 1)); + + // deleted? + if (_SplatBitsValid) + { + uint wordIdx = idx / 32; + uint bitIdx = idx & 31; + uint wordVal = _SplatDeletedBits.Load(wordIdx * 4); + if (wordVal & (1 << bitIdx)) + { + centerClipPos.w = 0; + } + } + view.pos = centerClipPos; bool behindCam = centerClipPos.w <= 0; if (!behindCam) @@ -117,3 +141,174 @@ void CSCalcViewData (uint3 id : SV_DispatchThreadID) _SplatViewData[idx] = view; } + +RWByteAddressBuffer _DstBuffer; +ByteAddressBuffer _SrcBuffer; +uint _BufferSize; +uint _DstBufferValue; + +[numthreads(GROUP_SIZE,1,1)] +void CSUpdateEditData (uint3 id : SV_DispatchThreadID) +{ + uint idx = id.x; + if (idx >= _BufferSize) + return; + + uint valSel = _SplatSelectedBits.Load(idx * 4); + uint valDel = _SplatDeletedBits.Load(idx * 4); + valSel &= ~valDel; // don't count deleted splats as selected + if (valSel != 0) + { + // update selection bounds + uint splatIdxStart = idx * 32; + uint splatIdxEnd = min(splatIdxStart + 32, _SplatCount); + float3 bmin = 1.0e38; + float3 bmax = -1.0e38; + uint mask = 1; + for (uint sidx = splatIdxStart; sidx < splatIdxEnd; ++sidx, mask <<= 1) + { + if (valSel & mask) + { + float3 spos = LoadSplatPos(sidx); + bmin = min(bmin, spos); + bmax = max(bmax, spos); + } + } + _DstBuffer.InterlockedMin(8, FloatToSortableUint(bmin.x)); + _DstBuffer.InterlockedMin(12, FloatToSortableUint(bmin.y)); + _DstBuffer.InterlockedMin(16, FloatToSortableUint(bmin.z)); + _DstBuffer.InterlockedMax(20, FloatToSortableUint(bmax.x)); + _DstBuffer.InterlockedMax(24, FloatToSortableUint(bmax.y)); + _DstBuffer.InterlockedMax(28, FloatToSortableUint(bmax.z)); + } + uint sumSel = countbits(valSel); + uint sumDel = countbits(valDel); + _DstBuffer.InterlockedAdd(0, sumSel); + _DstBuffer.InterlockedAdd(4, sumDel); +} + +[numthreads(1,1,1)] +void CSInitEditData (uint3 id : SV_DispatchThreadID) +{ + _DstBuffer.Store2(0, uint2(0,0)); // selected + deleted counts + uint initMin = FloatToSortableUint(1.0e38); + uint initMax = FloatToSortableUint(-1.0e38); + _DstBuffer.Store3(8, uint3(initMin, initMin, initMin)); + _DstBuffer.Store3(20, uint3(initMax, initMax, initMax)); +} + +[numthreads(GROUP_SIZE,1,1)] +void CSClearBuffer (uint3 id : SV_DispatchThreadID) +{ + uint idx = id.x; + if (idx >= _BufferSize) + return; + _DstBuffer.Store(idx * 4, _DstBufferValue); +} + +[numthreads(GROUP_SIZE,1,1)] +void CSInvertBuffer (uint3 id : SV_DispatchThreadID) +{ + uint idx = id.x; + if (idx >= _BufferSize) + return; + uint v = _DstBuffer.Load(idx * 4); + _DstBuffer.Store(idx * 4, ~v); +} + +[numthreads(GROUP_SIZE,1,1)] +void CSOrBuffers (uint3 id : SV_DispatchThreadID) +{ + uint idx = id.x; + if (idx >= _BufferSize) + return; + uint a = _SrcBuffer.Load(idx * 4); + uint b = _DstBuffer.Load(idx * 4); + _DstBuffer.Store(idx * 4, a | b); +} + +float4 _SelectionRect; + +[numthreads(GROUP_SIZE,1,1)] +void CSSelectionUpdate (uint3 id : SV_DispatchThreadID) +{ + uint idx = id.x; + if (idx >= _SplatCount) + return; + + float3 pos = LoadSplatPos(idx); + + float3 centerWorldPos = mul(_MatrixObjectToWorld, float4(pos,1)).xyz; + float4 centerClipPos = mul(_MatrixVP, float4(centerWorldPos, 1)); + bool behindCam = centerClipPos.w <= 0; + if (behindCam) + return; + + float2 pixelPos = (centerClipPos.xy / centerClipPos.w * float2(0.5, -0.5) + 0.5) * _VecScreenParams.xy; + if (pixelPos.x < _SelectionRect.x || pixelPos.x > _SelectionRect.z || + pixelPos.y < _SelectionRect.y || pixelPos.y > _SelectionRect.w) + { + return; + } + uint wordIdx = idx / 32; + uint bitIdx = idx & 31; + _SplatSelectedBits.InterlockedOr(wordIdx * 4, 1 << bitIdx); +} + + +struct ExportSplatData +{ + float3 pos; + float3 nor; + float3 dc0; + float4 shR14; float4 shR58; float4 shR9C; float3 shRDF; + float4 shG14; float4 shG58; float4 shG9C; float3 shGDF; + float4 shB14; float4 shB58; float4 shB9C; float3 shBDF; + float opacity; + float3 scale; + float4 rot; +}; +RWStructuredBuffer _ExportBuffer; + +float3 ColorToSH0(float3 col) +{ + return (col - 0.5) / 0.2820948; +} +float InvSigmoid(float v) +{ + return log(v / max(1 - v, 1.0e-6)); +} + +[numthreads(GROUP_SIZE,1,1)] +void CSExportData (uint3 id : SV_DispatchThreadID) +{ + uint idx = id.x; + if (idx >= _SplatCount) + return; + SplatData src = LoadSplatData(idx); + ExportSplatData dst; + dst.pos = src.pos; + dst.nor = 0; + dst.dc0 = ColorToSH0(src.sh.col); + + dst.shR14 = float4(src.sh.sh1.r, src.sh.sh2.r, src.sh.sh3.r, src.sh.sh4.r); + dst.shR58 = float4(src.sh.sh5.r, src.sh.sh6.r, src.sh.sh7.r, src.sh.sh8.r); + dst.shR9C = float4(src.sh.sh9.r, src.sh.sh10.r, src.sh.sh11.r, src.sh.sh12.r); + dst.shRDF = float3(src.sh.sh13.r, src.sh.sh14.r, src.sh.sh15.r); + + dst.shG14 = float4(src.sh.sh1.g, src.sh.sh2.g, src.sh.sh3.g, src.sh.sh4.g); + dst.shG58 = float4(src.sh.sh5.g, src.sh.sh6.g, src.sh.sh7.g, src.sh.sh8.g); + dst.shG9C = float4(src.sh.sh9.g, src.sh.sh10.g, src.sh.sh11.g, src.sh.sh12.g); + dst.shGDF = float3(src.sh.sh13.g, src.sh.sh14.g, src.sh.sh15.g); + + dst.shB14 = float4(src.sh.sh1.b, src.sh.sh2.b, src.sh.sh3.b, src.sh.sh4.b); + dst.shB58 = float4(src.sh.sh5.b, src.sh.sh6.b, src.sh.sh7.b, src.sh.sh8.b); + dst.shB9C = float4(src.sh.sh9.b, src.sh.sh10.b, src.sh.sh11.b, src.sh.sh12.b); + dst.shBDF = float3(src.sh.sh13.b, src.sh.sh14.b, src.sh.sh15.b); + + dst.opacity = InvSigmoid(src.opacity); + dst.scale = log(src.scale); + dst.rot = src.rot.wxyz; + + _ExportBuffer[idx] = dst; +} diff --git a/Doc/shotEdit.jpg b/Doc/shotEdit.jpg new file mode 100644 index 00000000..b1efd88c Binary files /dev/null and b/Doc/shotEdit.jpg differ diff --git a/ProjectSettings/ProjectSettings.asset b/ProjectSettings/ProjectSettings.asset index 67ed3908..538aeb82 100644 --- a/ProjectSettings/ProjectSettings.asset +++ b/ProjectSettings/ProjectSettings.asset @@ -341,7 +341,7 @@ PlayerSettings: m_APIs: 0b000000 m_Automatic: 1 - m_BuildTarget: WindowsStandaloneSupport - m_APIs: 120000000200000015000000 + m_APIs: 1200000015000000 m_Automatic: 0 m_BuildTargetVRSettings: - m_BuildTarget: Standalone diff --git a/readme.md b/readme.md index c47932e6..a34b1777 100644 --- a/readme.md +++ b/readme.md @@ -17,7 +17,7 @@ Code in here so far is randomly cribbled together from reading the paper (as wel ## Usage -:warning: Note: this is all _**a toy**_, it is not robust, it does not handle errors gracefully, it does not interact or composite well with the "rest of rendering", it is not fast, etc. etc. Also, do not file bugs or issues just yet; I will most likely just ignore them and do whatever I please. I told you so! :warning: +:warning: Note: this is all _**a toy**_, it can be not robust, not handle errors, not composite well with the rest of rendering, not be fast, etc. Also, do not file bugs or issues just yet; I will most likely just ignore them and do whatever I please. I told you so! :warning: First download or clone this repository and open as a Unity (2022.3, other versions might also work) project. Note that the project requires DX12 or Vulkan on Windows, i.e. DX11 will not work. @@ -52,6 +52,13 @@ If you are using **URP**, add GaussianSplatURPFeature to the URP renderer settin CustomPass volume object and a GaussianSplatHDRPPass entry to it. Maybe also set injection point to "after postprocess" to stop auto-exposure from going wild. + + +When a GaussianSplatRenderer object is selected, there's an additional tool that shows up in the scene view to edit the splats. +You can rectangle-drag to select them (shift+drag adds to selection). Usual Select All, Invert Selection etc. shortcuts work too. +Delete/Backspace deletes the selected splats. In the inspector there's a button then to export the "edited" object back into +a Gaussian Splat PLY file. This is best done using Very High import option for the original splat PLY file. + _That's it!_ Wishlist that I may or might not do at some point: