Optimize ViewportRubberBandSelector

#3151
This commit is contained in:
Wojtek Figat
2025-03-11 13:05:15 +01:00
parent 273b366f44
commit a54299a560
7 changed files with 208 additions and 122 deletions

View File

@@ -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 System.Collections.Generic;
using FlaxEditor;
using FlaxEditor.Gizmo; using FlaxEditor.Gizmo;
using FlaxEditor.SceneGraph; using FlaxEditor.SceneGraph;
using FlaxEditor.Viewport; using FlaxEditor.Viewport;
@@ -20,7 +18,8 @@ public class ViewportRubberBandSelector
private Float2 _cachedStartingMousePosition; private Float2 _cachedStartingMousePosition;
private Rectangle _rubberBandRect; private Rectangle _rubberBandRect;
private Rectangle _lastRubberBandRect; private Rectangle _lastRubberBandRect;
private List<ActorNode> _nodesCache;
private List<SceneGraphNode> _hitsCache;
private IGizmoOwner _owner; private IGizmoOwner _owner;
/// <summary> /// <summary>
@@ -87,15 +86,43 @@ public class ViewportRubberBandSelector
{ {
_rubberBandRect.Width = mousePosition.X - _cachedStartingMousePosition.X; _rubberBandRect.Width = mousePosition.X - _cachedStartingMousePosition.X;
_rubberBandRect.Height = mousePosition.Y - _cachedStartingMousePosition.Y; _rubberBandRect.Height = mousePosition.Y - _cachedStartingMousePosition.Y;
if (_lastRubberBandRect != _rubberBandRect) if (_lastRubberBandRect != _rubberBandRect)
{ {
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 // Select rubberbanded rect actor nodes
var adjustedRect = _rubberBandRect; var adjustedRect = _rubberBandRect;
_lastRubberBandRect = _rubberBandRect; _lastRubberBandRect = _rubberBandRect;
if (adjustedRect.Width < 0 || adjustedRect.Height < 0) 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 // Make sure we have a well-formed rectangle i.e. size is positive and X/Y is upper left corner
var size = adjustedRect.Size; var size = adjustedRect.Size;
adjustedRect.X = Mathf.Min(adjustedRect.X, adjustedRect.X + adjustedRect.Width); adjustedRect.X = Mathf.Min(adjustedRect.X, adjustedRect.X + adjustedRect.Width);
adjustedRect.Y = Mathf.Min(adjustedRect.Y, adjustedRect.Y + adjustedRect.Height); adjustedRect.Y = Mathf.Min(adjustedRect.Y, adjustedRect.Y + adjustedRect.Height);
@@ -104,16 +131,29 @@ public class ViewportRubberBandSelector
adjustedRect.Size = size; adjustedRect.Size = size;
} }
// Get hits from graph nodes. // Get hits from graph nodes
List<SceneGraphNode> hits = new List<SceneGraphNode>(); if (_nodesCache == null)
var nodes = _owner.SceneGraphRoot.GetAllChildActorNodes(); _nodesCache = new List<ActorNode>();
else
_nodesCache.Clear();
var nodes = _nodesCache;
_owner.SceneGraphRoot.GetAllChildActorNodes(nodes);
if (_hitsCache == null)
_hitsCache = new List<SceneGraphNode>();
else
_hitsCache.Clear();
var hits = _hitsCache;
// Process all nodes
var projection = new ViewportProjection();
projection.Init(_owner.Viewport);
foreach (var node in nodes) foreach (var node in nodes)
{ {
// Check for custom can select code // Check for custom can select code
if (!node.CanSelectActorNodeWithSelector()) if (!node.CanSelectActorNodeWithSelector())
continue; continue;
var a = node.Actor; var a = node.Actor;
// Skip actor if outside of view frustum // Skip actor if outside of view frustum
var actorBox = a.EditorBox; var actorBox = a.EditorBox;
if (viewFrustum.Contains(actorBox) == ContainmentType.Disjoint) if (viewFrustum.Contains(actorBox) == ContainmentType.Disjoint)
@@ -121,17 +161,7 @@ public class ViewportRubberBandSelector
// Get valid selection points // Get valid selection points
var points = node.GetActorSelectionPoints(); var points = node.GetActorSelectionPoints();
bool containsAllPoints = points.Length != 0; if (LoopOverPoints(points, ref adjustedRect, ref projection))
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) if (a.HasPrefabLink && _owner is not PrefabWindowViewport)
hits.Add(_owner.SceneGraphRoot.Find(a.GetPrefabRoot())); hits.Add(_owner.SceneGraphRoot.Find(a.GetPrefabRoot()));
@@ -140,11 +170,11 @@ public class ViewportRubberBandSelector
} }
} }
var editor = Editor.Instance; // Process selection
if (_owner.IsControlDown) if (_owner.IsControlDown)
{ {
var newSelection = new List<SceneGraphNode>(); var newSelection = new List<SceneGraphNode>();
var currentSelection = _owner.SceneGraphRoot.Selection; var currentSelection = new List<SceneGraphNode>(_owner.SceneGraphRoot.SceneContext.Selection);
newSelection.AddRange(currentSelection); newSelection.AddRange(currentSelection);
foreach (var hit in hits) foreach (var hit in hits)
{ {
@@ -158,7 +188,7 @@ public class ViewportRubberBandSelector
else if (Input.GetKey(KeyboardKeys.Shift)) else if (Input.GetKey(KeyboardKeys.Shift))
{ {
var newSelection = new List<SceneGraphNode>(); var newSelection = new List<SceneGraphNode>();
var currentSelection = _owner.SceneGraphRoot.Selection; var currentSelection = new List<SceneGraphNode>(_owner.SceneGraphRoot.SceneContext.Selection);
newSelection.AddRange(hits); newSelection.AddRange(hits);
newSelection.AddRange(currentSelection); newSelection.AddRange(currentSelection);
_owner.Select(newSelection); _owner.Select(newSelection);
@@ -167,8 +197,25 @@ public class ViewportRubberBandSelector
{ {
_owner.Select(hits); _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;
} }
/// <summary> /// <summary>

View File

@@ -188,26 +188,29 @@ namespace FlaxEditor.SceneGraph
/// <returns>An array of ActorNodes</returns> /// <returns>An array of ActorNodes</returns>
public ActorNode[] GetAllChildActorNodes() public ActorNode[] GetAllChildActorNodes()
{ {
// Check itself
if (ChildNodes == null || ChildNodes.Count == 0)
return [];
// Check deeper
var nodes = new List<ActorNode>(); var nodes = new List<ActorNode>();
for (int i = 0; i < ChildNodes.Count; i++) GetAllChildActorNodes(nodes);
return nodes.ToArray();
}
/// <summary>
/// Get all nested actor nodes under this actor node.
/// </summary>
/// <param name="nodes">The output list to fill with results.</param>
public void GetAllChildActorNodes(List<ActorNode> nodes)
{ {
if (ChildNodes[i] is ActorNode node) var children = ChildNodes;
if (children == null)
return;
for (int i = 0; i < children.Count; i++)
{
if (children[i] is ActorNode node)
{ {
nodes.Add(node); nodes.Add(node);
var childNodes = node.GetAllChildActorNodes(); node.GetAllChildActorNodes(nodes);
if (childNodes.Length > 0)
{
nodes.AddRange(childNodes);
} }
} }
} }
return nodes.ToArray();
}
/// <summary> /// <summary>
/// Whether an actor node can be selected with a selector. /// Whether an actor node can be selected with a selector.

View File

@@ -24,6 +24,9 @@ namespace FlaxEditor.SceneGraph.Actors
public sealed class StaticModelNode : ActorNode public sealed class StaticModelNode : ActorNode
{ {
private Dictionary<IntPtr, Mesh.Vertex[]> _vertices; private Dictionary<IntPtr, Mesh.Vertex[]> _vertices;
private Vector3[] _selectionPoints;
private Transform _selectionPointsTransform;
private Model _selectionPointsModel;
/// <inheritdoc /> /// <inheritdoc />
public StaticModelNode(Actor actor) public StaticModelNode(Actor actor)
@@ -31,6 +34,16 @@ namespace FlaxEditor.SceneGraph.Actors
{ {
} }
/// <inheritdoc />
public override void OnDispose()
{
_vertices = null;
_selectionPoints = null;
_selectionPointsModel = null;
base.OnDispose();
}
/// <inheritdoc /> /// <inheritdoc />
public override bool OnVertexSnap(ref Ray ray, Real hitDistance, out Vector3 result) public override bool OnVertexSnap(ref Ray ray, Real hitDistance, out Vector3 result)
{ {
@@ -91,25 +104,40 @@ namespace FlaxEditor.SceneGraph.Actors
/// <inheritdoc /> /// <inheritdoc />
public override Vector3[] GetActorSelectionPoints() public override Vector3[] GetActorSelectionPoints()
{ {
if (Actor is not StaticModel sm || !sm.Model) if (Actor is StaticModel sm && sm.Model)
return base.GetActorSelectionPoints(); {
// 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. // Check collision proxy points for more accurate selection
var vecPoints = new List<Vector3>(); var vecPoints = new List<Vector3>();
var m = sm.Model.LODs[0]; var m = model.LODs[0];
foreach (var mesh in m.Meshes) foreach (var mesh in m.Meshes)
{ {
var points = mesh.GetCollisionProxyPoints(); var points = mesh.GetCollisionProxyPoints();
foreach (var point in points) vecPoints.EnsureCapacity(vecPoints.Count + points.Length);
for (int i = 0; i < points.Length; i++)
{ {
vecPoints.Add(Actor.Transform.LocalToWorld(point)); vecPoints.Add(transform.LocalToWorld(points[i]));
} }
} }
// Fall back to base actor editor box if no points from collision proxy. Profiler.EndEvent();
if (vecPoints.Count == 0) if (vecPoints.Count != 0)
{
_selectionPoints = vecPoints.ToArray();
_selectionPointsTransform = transform;
_selectionPointsModel = model;
return _selectionPoints;
}
}
return base.GetActorSelectionPoints(); return base.GetActorSelectionPoints();
return vecPoints.ToArray();
} }
private void OnAddMeshCollider(EditorWindow window) private void OnAddMeshCollider(EditorWindow window)

View File

@@ -111,7 +111,6 @@ namespace FlaxEditor.Viewport
private double _lockedFocusOffset; private double _lockedFocusOffset;
private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32); private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32);
private EditorSpritesRenderer _editorSpritesRenderer; private EditorSpritesRenderer _editorSpritesRenderer;
private ViewportRubberBandSelector _rubberBandSelector; private ViewportRubberBandSelector _rubberBandSelector;
/// <summary> /// <summary>
@@ -607,9 +606,9 @@ namespace FlaxEditor.Viewport
{ {
base.OnMouseMove(location); base.OnMouseMove(location);
// Dont allow rubber band selection when gizmo is controlling mouse, vertex painting mode, or cloth painting is enabled // Don't 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), bool canStart = !((Gizmos.Active.IsControllingMouse || Gizmos.Active is VertexPaintingGizmo || Gizmos.Active is ClothPaintingGizmo) || IsControllingMouse || IsRightMouseButtonDown);
_viewMousePos, ViewFrustum); _rubberBandSelector.TryCreateRubberBand(canStart, _viewMousePos, ViewFrustum);
} }
/// <inheritdoc /> /// <inheritdoc />

View File

@@ -634,13 +634,15 @@ namespace FlaxEngine
return result; return result;
} }
#if FLAX_EDITOR
/// <summary> /// <summary>
/// Gets the collision proxy points for the mesh. /// Gets the collision proxy points for the mesh.
/// </summary> /// </summary>
/// <returns>The triangle points in the collision proxy.</returns> /// <returns>The triangle points in the collision proxy.</returns>
internal Float3[] GetCollisionProxyPoints() internal Vector3[] GetCollisionProxyPoints()
{ {
return Internal_GetCollisionProxyPoints(__unmanagedPtr, out _); return Internal_GetCollisionProxyPoints(__unmanagedPtr, out _);
} }
#endif
} }
} }

View File

@@ -847,13 +847,16 @@ MArray* Mesh::DownloadBuffer(bool forceGpu, MTypeObject* resultType, int32 typeI
return result; return result;
} }
Array<Float3> Mesh::GetCollisionProxyPoints() const #if USE_EDITOR
Array<Vector3> Mesh::GetCollisionProxyPoints() const
{ {
PROFILE_CPU();
Array<Vector3> result; Array<Vector3> result;
#if USE_PRECISE_MESH_INTERSECTS #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.V0);
result.Add(triangle.V1); result.Add(triangle.V1);
result.Add(triangle.V2); result.Add(triangle.V2);
@@ -863,3 +866,5 @@ Array<Float3> Mesh::GetCollisionProxyPoints() const
} }
#endif #endif
#endif

View File

@@ -320,6 +320,8 @@ private:
API_FUNCTION(NoProxy) bool UpdateTrianglesUInt(int32 triangleCount, const MArray* trianglesObj); API_FUNCTION(NoProxy) bool UpdateTrianglesUInt(int32 triangleCount, const MArray* trianglesObj);
API_FUNCTION(NoProxy) bool UpdateTrianglesUShort(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) MArray* DownloadBuffer(bool forceGpu, MTypeObject* resultType, int32 typeI);
API_FUNCTION(NoProxy) Array<Float3> GetCollisionProxyPoints() const; #if USE_EDITOR
API_FUNCTION(NoProxy) Array<Vector3> GetCollisionProxyPoints() const;
#endif
#endif #endif
}; };