From a54299a560a204b7374ed0512a28611b29266f63 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 11 Mar 2025 13:05:15 +0100 Subject: [PATCH] Optimize ViewportRubberBandSelector #3151 --- .../Gizmo/ViewportRubberBandSelector.cs | 217 +++++++++++------- Source/Editor/SceneGraph/ActorNode.cs | 29 +-- .../SceneGraph/Actors/StaticModelNode.cs | 58 +++-- .../Viewport/MainEditorGizmoViewport.cs | 7 +- Source/Engine/Graphics/Mesh.cs | 4 +- Source/Engine/Graphics/Models/Mesh.cpp | 11 +- Source/Engine/Graphics/Models/Mesh.h | 4 +- 7 files changed, 208 insertions(+), 122 deletions(-) diff --git a/Source/Editor/Gizmo/ViewportRubberBandSelector.cs b/Source/Editor/Gizmo/ViewportRubberBandSelector.cs index ddb889dd8..5ad861820 100644 --- a/Source/Editor/Gizmo/ViewportRubberBandSelector.cs +++ b/Source/Editor/Gizmo/ViewportRubberBandSelector.cs @@ -1,8 +1,6 @@ -// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. +// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. -using System; using System.Collections.Generic; -using FlaxEditor; using FlaxEditor.Gizmo; using FlaxEditor.SceneGraph; using FlaxEditor.Viewport; @@ -20,7 +18,8 @@ public class ViewportRubberBandSelector private Float2 _cachedStartingMousePosition; private Rectangle _rubberBandRect; private Rectangle _lastRubberBandRect; - + private List _nodesCache; + private List _hitsCache; private IGizmoOwner _owner; /// @@ -53,7 +52,7 @@ public class ViewportRubberBandSelector { _tryStartRubberBand = false; } - + if (_isRubberBandSpanning) { _isRubberBandSpanning = false; @@ -61,7 +60,7 @@ public class ViewportRubberBandSelector } return false; } - + /// /// Tries to create a rubber band selection. /// @@ -75,7 +74,7 @@ public class ViewportRubberBandSelector _isRubberBandSpanning = false; return; } - + if (_tryStartRubberBand && (Mathf.Abs(_owner.MouseDelta.X) > 0.1f || Mathf.Abs(_owner.MouseDelta.Y) > 0.1f) && canStart) { _isRubberBandSpanning = true; @@ -87,90 +86,138 @@ public class ViewportRubberBandSelector { _rubberBandRect.Width = mousePosition.X - _cachedStartingMousePosition.X; _rubberBandRect.Height = mousePosition.Y - _cachedStartingMousePosition.Y; - if (_lastRubberBandRect != _rubberBandRect) { - // Select rubberbanded rect actor nodes - var adjustedRect = _rubberBandRect; - _lastRubberBandRect = _rubberBandRect; - if (adjustedRect.Width < 0 || adjustedRect.Height < 0) - { - // make sure we have a well-formed rectangle i.e. size is positive and X/Y is upper left corner - var size = adjustedRect.Size; - adjustedRect.X = Mathf.Min(adjustedRect.X, adjustedRect.X + adjustedRect.Width); - adjustedRect.Y = Mathf.Min(adjustedRect.Y, adjustedRect.Y + adjustedRect.Height); - size.X = Mathf.Abs(size.X); - size.Y = Mathf.Abs(size.Y); - adjustedRect.Size = size; - } - - // Get hits from graph nodes. - List hits = new List(); - var nodes = _owner.SceneGraphRoot.GetAllChildActorNodes(); - foreach (var node in nodes) - { - // Check for custom can select code - if (!node.CanSelectActorNodeWithSelector()) - continue; - - var a = node.Actor; - // Skip actor if outside of view frustum - var actorBox = a.EditorBox; - if (viewFrustum.Contains(actorBox) == ContainmentType.Disjoint) - continue; - - // Get valid selection points - var points = node.GetActorSelectionPoints(); - bool containsAllPoints = points.Length != 0; - foreach (var point in points) - { - _owner.Viewport.ProjectPoint(point, out var loc); - if (!adjustedRect.Contains(loc)) - { - containsAllPoints = false; - break; - } - } - if (containsAllPoints) - { - if (a.HasPrefabLink && _owner is not PrefabWindowViewport) - hits.Add(_owner.SceneGraphRoot.Find(a.GetPrefabRoot())); - else - hits.Add(node); - } - } - - var editor = Editor.Instance; - if (_owner.IsControlDown) - { - var newSelection = new List(); - var currentSelection = _owner.SceneGraphRoot.Selection; - newSelection.AddRange(currentSelection); - foreach (var hit in hits) - { - if (currentSelection.Contains(hit)) - newSelection.Remove(hit); - else - newSelection.Add(hit); - } - _owner.Select(newSelection); - } - else if (Input.GetKey(KeyboardKeys.Shift)) - { - var newSelection = new List(); - var currentSelection = _owner.SceneGraphRoot.Selection; - newSelection.AddRange(hits); - newSelection.AddRange(currentSelection); - _owner.Select(newSelection); - } - else - { - _owner.Select(hits); - } + UpdateRubberBand(ref viewFrustum); } } } + private struct ViewportProjection + { + private Viewport _viewport; + private Matrix _viewProjection; + + public void Init(EditorViewport editorViewport) + { + // Inline EditorViewport.ProjectPoint to save on calculation for large set of points + _viewport = new Viewport(0, 0, editorViewport.Width, editorViewport.Height); + var frustum = editorViewport.ViewFrustum; + _viewProjection = frustum.Matrix; + } + + public void ProjectPoint(ref Vector3 worldSpaceLocation, out Float2 viewportSpaceLocation) + { + _viewport.Project(ref worldSpaceLocation, ref _viewProjection, out var projected); + viewportSpaceLocation = new Float2((float)projected.X, (float)projected.Y); + } + } + + private void UpdateRubberBand(ref BoundingFrustum viewFrustum) + { + Profiler.BeginEvent("UpdateRubberBand"); + + // Select rubberbanded rect actor nodes + var adjustedRect = _rubberBandRect; + _lastRubberBandRect = _rubberBandRect; + if (adjustedRect.Width < 0 || adjustedRect.Height < 0) + { + // Make sure we have a well-formed rectangle i.e. size is positive and X/Y is upper left corner + var size = adjustedRect.Size; + adjustedRect.X = Mathf.Min(adjustedRect.X, adjustedRect.X + adjustedRect.Width); + adjustedRect.Y = Mathf.Min(adjustedRect.Y, adjustedRect.Y + adjustedRect.Height); + size.X = Mathf.Abs(size.X); + size.Y = Mathf.Abs(size.Y); + adjustedRect.Size = size; + } + + // Get hits from graph nodes + if (_nodesCache == null) + _nodesCache = new List(); + else + _nodesCache.Clear(); + var nodes = _nodesCache; + _owner.SceneGraphRoot.GetAllChildActorNodes(nodes); + if (_hitsCache == null) + _hitsCache = new List(); + else + _hitsCache.Clear(); + var hits = _hitsCache; + + // Process all nodes + var projection = new ViewportProjection(); + projection.Init(_owner.Viewport); + foreach (var node in nodes) + { + // Check for custom can select code + if (!node.CanSelectActorNodeWithSelector()) + continue; + var a = node.Actor; + + // Skip actor if outside of view frustum + var actorBox = a.EditorBox; + if (viewFrustum.Contains(actorBox) == ContainmentType.Disjoint) + continue; + + // Get valid selection points + var points = node.GetActorSelectionPoints(); + if (LoopOverPoints(points, ref adjustedRect, ref projection)) + { + if (a.HasPrefabLink && _owner is not PrefabWindowViewport) + hits.Add(_owner.SceneGraphRoot.Find(a.GetPrefabRoot())); + else + hits.Add(node); + } + } + + // Process selection + if (_owner.IsControlDown) + { + var newSelection = new List(); + var currentSelection = new List(_owner.SceneGraphRoot.SceneContext.Selection); + newSelection.AddRange(currentSelection); + foreach (var hit in hits) + { + if (currentSelection.Contains(hit)) + newSelection.Remove(hit); + else + newSelection.Add(hit); + } + _owner.Select(newSelection); + } + else if (Input.GetKey(KeyboardKeys.Shift)) + { + var newSelection = new List(); + var currentSelection = new List(_owner.SceneGraphRoot.SceneContext.Selection); + newSelection.AddRange(hits); + newSelection.AddRange(currentSelection); + _owner.Select(newSelection); + } + else + { + _owner.Select(hits); + } + + Profiler.EndEvent(); + } + + private bool LoopOverPoints(Vector3[] points, ref Rectangle adjustedRect, ref ViewportProjection projection) + { + Profiler.BeginEvent("LoopOverPoints"); + bool containsAllPoints = points.Length != 0; + for (int i = 0; i < points.Length; i++) + { + projection.ProjectPoint(ref points[i], out var loc); + if (!adjustedRect.Contains(loc)) + { + containsAllPoints = false; + break; + } + } + Profiler.EndEvent(); + return containsAllPoints; + } + /// /// Used to draw the rubber band. Begins render 2D. /// diff --git a/Source/Editor/SceneGraph/ActorNode.cs b/Source/Editor/SceneGraph/ActorNode.cs index 5191f83c3..0740fb84f 100644 --- a/Source/Editor/SceneGraph/ActorNode.cs +++ b/Source/Editor/SceneGraph/ActorNode.cs @@ -188,25 +188,28 @@ namespace FlaxEditor.SceneGraph /// An array of ActorNodes public ActorNode[] GetAllChildActorNodes() { - // Check itself - if (ChildNodes == null || ChildNodes.Count == 0) - return []; - - // Check deeper var nodes = new List(); - for (int i = 0; i < ChildNodes.Count; i++) + GetAllChildActorNodes(nodes); + return nodes.ToArray(); + } + + /// + /// Get all nested actor nodes under this actor node. + /// + /// The output list to fill with results. + public void GetAllChildActorNodes(List nodes) + { + var children = ChildNodes; + if (children == null) + return; + for (int i = 0; i < children.Count; i++) { - if (ChildNodes[i] is ActorNode node) + if (children[i] is ActorNode node) { nodes.Add(node); - var childNodes = node.GetAllChildActorNodes(); - if (childNodes.Length > 0) - { - nodes.AddRange(childNodes); - } + node.GetAllChildActorNodes(nodes); } } - return nodes.ToArray(); } /// diff --git a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs index 02161d277..b024c0287 100644 --- a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs +++ b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs @@ -24,6 +24,9 @@ namespace FlaxEditor.SceneGraph.Actors public sealed class StaticModelNode : ActorNode { private Dictionary _vertices; + private Vector3[] _selectionPoints; + private Transform _selectionPointsTransform; + private Model _selectionPointsModel; /// public StaticModelNode(Actor actor) @@ -31,6 +34,16 @@ namespace FlaxEditor.SceneGraph.Actors { } + /// + public override void OnDispose() + { + _vertices = null; + _selectionPoints = null; + _selectionPointsModel = null; + + base.OnDispose(); + } + /// public override bool OnVertexSnap(ref Ray ray, Real hitDistance, out Vector3 result) { @@ -91,25 +104,40 @@ namespace FlaxEditor.SceneGraph.Actors /// public override Vector3[] GetActorSelectionPoints() { - if (Actor is not StaticModel sm || !sm.Model) - return base.GetActorSelectionPoints(); - - // Check collision proxy points for more accurate selection. - var vecPoints = new List(); - var m = sm.Model.LODs[0]; - foreach (var mesh in m.Meshes) + if (Actor is StaticModel sm && sm.Model) { - var points = mesh.GetCollisionProxyPoints(); - foreach (var point in points) + // Try to use cache + var model = sm.Model; + var transform = Actor.Transform; + if (_selectionPoints != null && + _selectionPointsTransform == transform && + _selectionPointsModel == model) + return _selectionPoints; + Profiler.BeginEvent("GetActorSelectionPoints"); + + // Check collision proxy points for more accurate selection + var vecPoints = new List(); + var m = model.LODs[0]; + foreach (var mesh in m.Meshes) { - vecPoints.Add(Actor.Transform.LocalToWorld(point)); + var points = mesh.GetCollisionProxyPoints(); + vecPoints.EnsureCapacity(vecPoints.Count + points.Length); + for (int i = 0; i < points.Length; i++) + { + vecPoints.Add(transform.LocalToWorld(points[i])); + } + } + + Profiler.EndEvent(); + if (vecPoints.Count != 0) + { + _selectionPoints = vecPoints.ToArray(); + _selectionPointsTransform = transform; + _selectionPointsModel = model; + return _selectionPoints; } } - - // Fall back to base actor editor box if no points from collision proxy. - if (vecPoints.Count == 0) - return base.GetActorSelectionPoints(); - return vecPoints.ToArray(); + return base.GetActorSelectionPoints(); } private void OnAddMeshCollider(EditorWindow window) diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index b8b7271c2..da3cf5bf6 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -111,7 +111,6 @@ namespace FlaxEditor.Viewport private double _lockedFocusOffset; private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32); private EditorSpritesRenderer _editorSpritesRenderer; - private ViewportRubberBandSelector _rubberBandSelector; /// @@ -607,9 +606,9 @@ namespace FlaxEditor.Viewport { base.OnMouseMove(location); - // Dont allow rubber band selection when gizmo is controlling mouse, vertex painting mode, or cloth painting is enabled - _rubberBandSelector.TryCreateRubberBand(!((Gizmos.Active.IsControllingMouse || Gizmos.Active is VertexPaintingGizmo || Gizmos.Active is ClothPaintingGizmo) || IsControllingMouse || IsRightMouseButtonDown), - _viewMousePos, ViewFrustum); + // Don't allow rubber band selection when gizmo is controlling mouse, vertex painting mode, or cloth painting is enabled + bool canStart = !((Gizmos.Active.IsControllingMouse || Gizmos.Active is VertexPaintingGizmo || Gizmos.Active is ClothPaintingGizmo) || IsControllingMouse || IsRightMouseButtonDown); + _rubberBandSelector.TryCreateRubberBand(canStart, _viewMousePos, ViewFrustum); } /// diff --git a/Source/Engine/Graphics/Mesh.cs b/Source/Engine/Graphics/Mesh.cs index e87a38ae7..1b8c16dbe 100644 --- a/Source/Engine/Graphics/Mesh.cs +++ b/Source/Engine/Graphics/Mesh.cs @@ -634,13 +634,15 @@ namespace FlaxEngine return result; } +#if FLAX_EDITOR /// /// Gets the collision proxy points for the mesh. /// /// The triangle points in the collision proxy. - internal Float3[] GetCollisionProxyPoints() + internal Vector3[] GetCollisionProxyPoints() { return Internal_GetCollisionProxyPoints(__unmanagedPtr, out _); } +#endif } } diff --git a/Source/Engine/Graphics/Models/Mesh.cpp b/Source/Engine/Graphics/Models/Mesh.cpp index ac194d905..721aaffd8 100644 --- a/Source/Engine/Graphics/Models/Mesh.cpp +++ b/Source/Engine/Graphics/Models/Mesh.cpp @@ -847,13 +847,16 @@ MArray* Mesh::DownloadBuffer(bool forceGpu, MTypeObject* resultType, int32 typeI return result; } -Array Mesh::GetCollisionProxyPoints() const +#if USE_EDITOR + +Array Mesh::GetCollisionProxyPoints() const { + PROFILE_CPU(); Array result; #if USE_PRECISE_MESH_INTERSECTS - for (int i = 0; i < _collisionProxy.Triangles.Count(); ++i) + for (int32 i = 0; i < _collisionProxy.Triangles.Count(); i++) { - auto triangle = _collisionProxy.Triangles[i]; + auto triangle = _collisionProxy.Triangles.Get()[i]; result.Add(triangle.V0); result.Add(triangle.V1); result.Add(triangle.V2); @@ -863,3 +866,5 @@ Array Mesh::GetCollisionProxyPoints() const } #endif + +#endif diff --git a/Source/Engine/Graphics/Models/Mesh.h b/Source/Engine/Graphics/Models/Mesh.h index 2c8f8beda..78c60ec50 100644 --- a/Source/Engine/Graphics/Models/Mesh.h +++ b/Source/Engine/Graphics/Models/Mesh.h @@ -320,6 +320,8 @@ private: API_FUNCTION(NoProxy) bool UpdateTrianglesUInt(int32 triangleCount, const MArray* trianglesObj); API_FUNCTION(NoProxy) bool UpdateTrianglesUShort(int32 triangleCount, const MArray* trianglesObj); API_FUNCTION(NoProxy) MArray* DownloadBuffer(bool forceGpu, MTypeObject* resultType, int32 typeI); - API_FUNCTION(NoProxy) Array GetCollisionProxyPoints() const; +#if USE_EDITOR + API_FUNCTION(NoProxy) Array GetCollisionProxyPoints() const; +#endif #endif };