diff --git a/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs b/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs
index d53844499..7d84d8933 100644
--- a/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs
+++ b/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs
@@ -204,7 +204,7 @@ namespace FlaxEditor.CustomEditors.Editors
var frameRect = new Rectangle(0, 0, Width, 16);
if (isSelected)
frameRect.Width -= 16;
- if (_supportsPickDropDown)
+ if (_supportsPickDropDown && isEnabled)
frameRect.Width -= 16;
var nameRect = new Rectangle(2, 1, frameRect.Width - 4, 14);
var button1Rect = new Rectangle(nameRect.Right + 2, 1, 14, 14);
@@ -240,7 +240,7 @@ namespace FlaxEditor.CustomEditors.Editors
}
// Draw picker button
- if (_supportsPickDropDown)
+ if (_supportsPickDropDown && isEnabled)
{
var pickerRect = isSelected ? button2Rect : button1Rect;
Render2D.DrawSprite(style.ArrowDown, pickerRect, isEnabled && pickerRect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey);
diff --git a/Source/Editor/Gizmo/TransformGizmoBase.Settings.cs b/Source/Editor/Gizmo/TransformGizmoBase.Settings.cs
index 5b54a7c79..4d9326bb5 100644
--- a/Source/Editor/Gizmo/TransformGizmoBase.Settings.cs
+++ b/Source/Editor/Gizmo/TransformGizmoBase.Settings.cs
@@ -35,12 +35,12 @@ namespace FlaxEditor.Gizmo
///
/// The inner minimum of the multiscale
///
- private const float InnerExtend = AxisOffset + 0.5f;
+ private const float InnerExtend = AxisOffset;
///
/// The outer maximum of the multiscale
///
- private const float OuterExtend = AxisOffset * 3.5f;
+ private const float OuterExtend = AxisOffset + 1.25f;
// Cube with the size AxisThickness, then moves it along the axis (AxisThickness) and finally makes it really long (AxisLength)
private BoundingBox XAxisBox = new BoundingBox(new Vector3(-AxisThickness), new Vector3(AxisThickness)).MakeOffsetted(AxisOffset * Vector3.UnitX).Merge(AxisLength * Vector3.UnitX);
@@ -75,6 +75,11 @@ namespace FlaxEditor.Gizmo
///
public bool ScaleSnapEnabled = false;
+ ///
+ /// True if enable absolute grid snapping (snaps objects to world-space grid, not the one relative to gizmo location)
+ ///
+ public bool AbsoluteSnapEnabled = false;
+
///
/// Translation snap value
///
diff --git a/Source/Editor/Gizmo/TransformGizmoBase.cs b/Source/Editor/Gizmo/TransformGizmoBase.cs
index cab0ab462..da1ed575e 100644
--- a/Source/Editor/Gizmo/TransformGizmoBase.cs
+++ b/Source/Editor/Gizmo/TransformGizmoBase.cs
@@ -2,8 +2,10 @@
#if USE_LARGE_WORLDS
using Real = System.Double;
+using Mathr = FlaxEngine.Mathd;
#else
using Real = System.Single;
+using Mathr = FlaxEngine.Mathf;
#endif
using System;
@@ -40,8 +42,10 @@ namespace FlaxEditor.Gizmo
private Vector3 _intersectPosition;
private bool _isActive;
private bool _isDuplicating;
+ private bool _hasAbsoluteSnapped;
private bool _isTransforming;
+ private bool _isSelected;
private Vector3 _lastIntersectionPosition;
private Quaternion _rotationDelta = Quaternion.Identity;
@@ -269,7 +273,17 @@ namespace FlaxEditor.Gizmo
_intersectPosition = ray.GetPoint(intersection);
if (!_lastIntersectionPosition.IsZero)
_tDelta = _intersectPosition - _lastIntersectionPosition;
- delta = new Vector3(0, _tDelta.Y, _tDelta.Z);
+ if (isScaling)
+ {
+ var tDeltaAbs = Vector3.Abs(_tDelta);
+ var maxDelta = Math.Max(tDeltaAbs.Y, tDeltaAbs.Z);
+ var sign = Math.Sign(tDeltaAbs.Y > tDeltaAbs.Z ? _tDelta.Y : _tDelta.Z);
+ delta = new Vector3(0, maxDelta * sign, maxDelta * sign);
+ }
+ else
+ {
+ delta = new Vector3(0, _tDelta.Y, _tDelta.Z);
+ }
}
break;
}
@@ -280,7 +294,17 @@ namespace FlaxEditor.Gizmo
_intersectPosition = ray.GetPoint(intersection);
if (!_lastIntersectionPosition.IsZero)
_tDelta = _intersectPosition - _lastIntersectionPosition;
- delta = new Vector3(_tDelta.X, _tDelta.Y, 0);
+ if (isScaling)
+ {
+ var tDeltaAbs = Vector3.Abs(_tDelta);
+ var maxDelta = Math.Max(tDeltaAbs.X, tDeltaAbs.Y);
+ var sign = Math.Sign(tDeltaAbs.X > tDeltaAbs.Y ? _tDelta.X : _tDelta.Y);
+ delta = new Vector3(maxDelta * sign, maxDelta * sign, 0);
+ }
+ else
+ {
+ delta = new Vector3(_tDelta.X, _tDelta.Y, 0);
+ }
}
break;
}
@@ -291,7 +315,17 @@ namespace FlaxEditor.Gizmo
_intersectPosition = ray.GetPoint(intersection);
if (!_lastIntersectionPosition.IsZero)
_tDelta = _intersectPosition - _lastIntersectionPosition;
- delta = new Vector3(_tDelta.X, 0, _tDelta.Z);
+ if (isScaling)
+ {
+ var tDeltaAbs = Vector3.Abs(_tDelta);
+ var maxDelta = Math.Max(tDeltaAbs.X, tDeltaAbs.Z);
+ var sign = Math.Sign(tDeltaAbs.X > tDeltaAbs.Z ? _tDelta.X : _tDelta.Z);
+ delta = new Vector3(maxDelta * sign, 0, maxDelta * sign);
+ }
+ else
+ {
+ delta = new Vector3(_tDelta.X, 0, _tDelta.Z);
+ }
}
break;
}
@@ -305,7 +339,24 @@ namespace FlaxEditor.Gizmo
if (!_lastIntersectionPosition.IsZero)
_tDelta = _intersectPosition - _lastIntersectionPosition;
}
- delta = _tDelta;
+ if (isScaling)
+ {
+ var tDeltaAbs = Vector3.Abs(_tDelta);
+ var maxDelta = Math.Max(tDeltaAbs.X, tDeltaAbs.Y);
+ maxDelta = Math.Max(maxDelta, tDeltaAbs.Z);
+ Real sign = 0;
+ if (Mathf.NearEqual(maxDelta, tDeltaAbs.X))
+ sign = Math.Sign(_tDelta.X);
+ else if (Mathf.NearEqual(maxDelta, tDeltaAbs.Y))
+ sign = Math.Sign(_tDelta.Y);
+ else if (Mathf.NearEqual(maxDelta, tDeltaAbs.Z))
+ sign = Math.Sign(_tDelta.Z);
+ delta = new Vector3(maxDelta * sign);
+ }
+ else
+ {
+ delta = _tDelta;
+ }
break;
}
}
@@ -318,6 +369,7 @@ namespace FlaxEditor.Gizmo
if ((isScaling ? ScaleSnapEnabled : TranslationSnapEnable) || Owner.UseSnapping)
{
var snapValue = new Vector3(isScaling ? ScaleSnapValue : TranslationSnapValue);
+
_translationScaleSnapDelta += delta;
if (!isScaling && snapValue.X < 0.0f)
{
@@ -336,11 +388,29 @@ namespace FlaxEditor.Gizmo
else
snapValue.Z = (Real)b.Minimum.Z - b.Maximum.Z;
}
+
+ Vector3 absoluteDelta = Vector3.Zero;
+ if (!_hasAbsoluteSnapped && AbsoluteSnapEnabled && ActiveTransformSpace == TransformSpace.World)
+ {
+ // Remove delta to offset local-space grid into the world-space grid
+ _hasAbsoluteSnapped = true;
+ Vector3 currentTranslationScale = isScaling ? GetSelectedTransform(0).Scale : GetSelectedTransform(0).Translation;
+ absoluteDelta = currentTranslationScale - new Vector3(
+ Mathr.Round(currentTranslationScale.X / snapValue.X) * snapValue.X,
+ Mathr.Round(currentTranslationScale.Y / snapValue.Y) * snapValue.Y,
+ Mathr.Round(currentTranslationScale.Z / snapValue.Z) * snapValue.Z);
+ }
+
delta = new Vector3(
(int)(_translationScaleSnapDelta.X / snapValue.X) * snapValue.X,
(int)(_translationScaleSnapDelta.Y / snapValue.Y) * snapValue.Y,
(int)(_translationScaleSnapDelta.Z / snapValue.Z) * snapValue.Z);
_translationScaleSnapDelta -= delta;
+ delta -= absoluteDelta;
+ }
+ else
+ {
+ _hasAbsoluteSnapped = false;
}
if (_activeMode == Mode.Translate)
@@ -370,12 +440,33 @@ namespace FlaxEditor.Gizmo
if (RotationSnapEnabled || Owner.UseSnapping)
{
float snapValue = RotationSnapValue * Mathf.DegreesToRadians;
+
+ float absoluteDelta = 0.0f;
+ if (!_hasAbsoluteSnapped && AbsoluteSnapEnabled && ActiveTransformSpace == TransformSpace.World)
+ {
+ // Remove delta to offset world-space grid into the local-space grid
+ _hasAbsoluteSnapped = true;
+ float currentAngle = 0.0f;
+ switch (_activeAxis)
+ {
+ case Axis.X: currentAngle = GetSelectedTransform(0).Orientation.EulerAngles.X; break;
+ case Axis.Y: currentAngle = GetSelectedTransform(0).Orientation.EulerAngles.Y; break;
+ case Axis.Z: currentAngle = GetSelectedTransform(0).Orientation.EulerAngles.Z; break;
+ }
+ absoluteDelta = currentAngle - (Mathf.Round(currentAngle / RotationSnapValue) * RotationSnapValue);
+ }
+
_rotationSnapDelta += delta;
float snapped = Mathf.Round(_rotationSnapDelta / snapValue) * snapValue;
_rotationSnapDelta -= snapped;
delta = snapped;
+ delta -= absoluteDelta * Mathf.DegreesToRadians;
+ }
+ else
+ {
+ _hasAbsoluteSnapped = false;
}
switch (_activeAxis)
@@ -408,7 +499,7 @@ namespace FlaxEditor.Gizmo
}
///
- public override bool IsControllingMouse => _isTransforming;
+ public override bool IsControllingMouse => _isTransforming || _isSelected;
///
public override void Update(float dt)
@@ -433,6 +524,7 @@ namespace FlaxEditor.Gizmo
// Check if user is holding left mouse button and any axis is selected
if (isLeftBtnDown && _activeAxis != Axis.None)
{
+ _isSelected = true; // setting later is too late, need to set here for rubber band selection in GizmoViewport
switch (_activeMode)
{
case Mode.Translate:
@@ -450,6 +542,7 @@ namespace FlaxEditor.Gizmo
}
else
{
+ _isSelected = false;
// If nothing selected, try to select any axis
if (!isLeftBtnDown && !Owner.IsRightMouseButtonDown)
{
@@ -517,6 +610,7 @@ namespace FlaxEditor.Gizmo
// Clear cache
_accMoveDelta = Vector3.Zero;
_lastIntersectionPosition = _intersectPosition = Vector3.Zero;
+ _isSelected = false;
EndTransforming();
}
}
diff --git a/Source/Editor/Gizmo/ViewportRubberBandSelector.cs b/Source/Editor/Gizmo/ViewportRubberBandSelector.cs
new file mode 100644
index 000000000..0381c6535
--- /dev/null
+++ b/Source/Editor/Gizmo/ViewportRubberBandSelector.cs
@@ -0,0 +1,277 @@
+// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
+
+using System.Collections.Generic;
+using FlaxEditor.Gizmo;
+using FlaxEditor.SceneGraph;
+using FlaxEditor.Viewport;
+using FlaxEngine.GUI;
+
+namespace FlaxEngine.Gizmo;
+
+///
+/// Class for adding viewport rubber band selection.
+///
+public class ViewportRubberBandSelector
+{
+ private bool _isMosueCaptured;
+ private bool _isRubberBandSpanning;
+ private bool _tryStartRubberBand;
+ private Float2 _cachedStartingMousePosition;
+ private Rectangle _rubberBandRect;
+ private Rectangle _lastRubberBandRect;
+ private List _nodesCache;
+ private List _hitsCache;
+ private IGizmoOwner _owner;
+
+ ///
+ /// Constructs a rubber band selector with a designated gizmo owner.
+ ///
+ /// The gizmo owner.
+ public ViewportRubberBandSelector(IGizmoOwner owner)
+ {
+ _owner = owner;
+ }
+
+ ///
+ /// Triggers the start of a rubber band selection.
+ ///
+ /// True if selection started, otherwise false.
+ public bool TryStartingRubberBandSelection()
+ {
+ if (!_isRubberBandSpanning && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown)
+ {
+ _tryStartRubberBand = true;
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Release the rubber band selection.
+ ///
+ /// Returns true if rubber band is currently spanning
+ public bool ReleaseRubberBandSelection()
+ {
+ if (_isMosueCaptured)
+ {
+ _isMosueCaptured = false;
+ _owner.Viewport.EndMouseCapture();
+ }
+ if (_tryStartRubberBand)
+ {
+ _tryStartRubberBand = false;
+ }
+ if (_isRubberBandSpanning)
+ {
+ _isRubberBandSpanning = false;
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// Tries to create a rubber band selection.
+ ///
+ /// Whether the creation can start.
+ /// The current mouse position.
+ /// The view frustum.
+ public void TryCreateRubberBand(bool canStart, Float2 mousePosition, BoundingFrustum viewFrustum)
+ {
+ if (_isRubberBandSpanning && !canStart)
+ {
+ _isRubberBandSpanning = false;
+ return;
+ }
+
+ if (_tryStartRubberBand && (Mathf.Abs(_owner.MouseDelta.X) > 0.1f || Mathf.Abs(_owner.MouseDelta.Y) > 0.1f) && canStart)
+ {
+ _isRubberBandSpanning = true;
+ _cachedStartingMousePosition = mousePosition;
+ _rubberBandRect = new Rectangle(_cachedStartingMousePosition, Float2.Zero);
+ _tryStartRubberBand = false;
+ }
+ else if (_isRubberBandSpanning && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown)
+ {
+ _rubberBandRect.Width = mousePosition.X - _cachedStartingMousePosition.X;
+ _rubberBandRect.Height = mousePosition.Y - _cachedStartingMousePosition.Y;
+ if (_lastRubberBandRect != _rubberBandRect)
+ {
+ if (!_isMosueCaptured)
+ {
+ _isMosueCaptured = true;
+ _owner.Viewport.StartMouseCapture();
+ }
+ 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.
+ ///
+ /// The GPU Context.
+ /// The GPU texture target.
+ /// The GPU texture target depth.
+ public void Draw(GPUContext context, GPUTexture target, GPUTexture targetDepth)
+ {
+ // Draw RubberBand for rect selection
+ if (!_isRubberBandSpanning)
+ return;
+ Render2D.Begin(context, target, targetDepth);
+ Draw2D();
+ Render2D.End();
+ }
+
+ ///
+ /// Used to draw the rubber band. Use if already rendering 2D context.
+ ///
+ public void Draw2D()
+ {
+ if (!_isRubberBandSpanning)
+ return;
+ Render2D.FillRectangle(_rubberBandRect, Style.Current.Selection);
+ Render2D.DrawRectangle(_rubberBandRect, Style.Current.SelectionBorder);
+ }
+
+ ///
+ /// Immediately stops the rubber band.
+ ///
+ /// True if rubber band was active before stopping.
+ public bool StopRubberBand()
+ {
+ if (_isMosueCaptured)
+ {
+ _isMosueCaptured = false;
+ _owner.Viewport.EndMouseCapture();
+ }
+ var result = _tryStartRubberBand;
+ _isRubberBandSpanning = false;
+ _tryStartRubberBand = false;
+ return result;
+ }
+}
diff --git a/Source/Editor/Managed/ManagedEditor.cpp b/Source/Editor/Managed/ManagedEditor.cpp
index 652878d4d..afa47a159 100644
--- a/Source/Editor/Managed/ManagedEditor.cpp
+++ b/Source/Editor/Managed/ManagedEditor.cpp
@@ -606,6 +606,11 @@ void ManagedEditor::WipeOutLeftoverSceneObjects()
{
if (sceneObject->HasParent())
continue; // Skip sub-objects
+ auto* actor = Cast(sceneObject);
+ if (!actor)
+ actor = sceneObject->GetParent();
+ if (actor && actor->HasTag(TEXT("__EditorInternal")))
+ continue; // Skip internal objects used by Editor (eg. EditorScene)
LOG(Error, "Object '{}' (ID={}, Type={}) is still in memory after play end but should be destroyed (memory leak).", sceneObject->GetNamePath(), sceneObject->GetID(), sceneObject->GetType().ToString());
sceneObject->DeleteObject();
diff --git a/Source/Editor/SceneGraph/ActorNode.cs b/Source/Editor/SceneGraph/ActorNode.cs
index a1e1643a8..0740fb84f 100644
--- a/Source/Editor/SceneGraph/ActorNode.cs
+++ b/Source/Editor/SceneGraph/ActorNode.cs
@@ -182,6 +182,54 @@ namespace FlaxEditor.SceneGraph
return null;
}
+ ///
+ /// Get all nested actor nodes under this actor node.
+ ///
+ /// An array of ActorNodes
+ public ActorNode[] GetAllChildActorNodes()
+ {
+ var nodes = new List();
+ 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 (children[i] is ActorNode node)
+ {
+ nodes.Add(node);
+ node.GetAllChildActorNodes(nodes);
+ }
+ }
+ }
+
+ ///
+ /// Whether an actor node can be selected with a selector.
+ ///
+ /// True if the actor node can be selected
+ public virtual bool CanSelectActorNodeWithSelector()
+ {
+ return Actor && Actor.HideFlags is not (HideFlags.DontSelect or HideFlags.FullyHidden) && Actor is not EmptyActor && IsActive;
+ }
+
+ ///
+ /// The selection points used to check if an actor node can be selected.
+ ///
+ /// The points to use if the actor can be selected.
+ public virtual Vector3[] GetActorSelectionPoints()
+ {
+ return Actor.EditorBox.GetCorners();
+ }
+
///
/// Gets a value indicating whether this actor can be used to create prefab from it (as a root).
///
diff --git a/Source/Editor/SceneGraph/Actors/CameraNode.cs b/Source/Editor/SceneGraph/Actors/CameraNode.cs
index 6c485314d..cada3364c 100644
--- a/Source/Editor/SceneGraph/Actors/CameraNode.cs
+++ b/Source/Editor/SceneGraph/Actors/CameraNode.cs
@@ -58,5 +58,11 @@ namespace FlaxEditor.SceneGraph.Actors
return Camera.Internal_IntersectsItselfEditor(FlaxEngine.Object.GetUnmanagedPtr(_actor), ref ray.Ray, out distance);
}
+
+ ///
+ public override Vector3[] GetActorSelectionPoints()
+ {
+ return [Actor.Position];
+ }
}
}
diff --git a/Source/Editor/SceneGraph/Actors/SceneNode.cs b/Source/Editor/SceneGraph/Actors/SceneNode.cs
index bb5998ef4..9e380ff91 100644
--- a/Source/Editor/SceneGraph/Actors/SceneNode.cs
+++ b/Source/Editor/SceneGraph/Actors/SceneNode.cs
@@ -33,6 +33,12 @@ namespace FlaxEditor.SceneGraph.Actors
}
}
+ ///
+ public override bool CanSelectActorNodeWithSelector()
+ {
+ return false;
+ }
+
///
/// Gets the scene.
///
diff --git a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs
index ee208f448..428341477 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,6 +104,45 @@ namespace FlaxEditor.SceneGraph.Actors
contextMenu.AddButton("Add collider", () => OnAddMeshCollider(window)).Enabled = ((StaticModel)Actor).Model != null;
}
+ ///
+ public override Vector3[] GetActorSelectionPoints()
+ {
+ if (Actor is StaticModel sm && sm.Model)
+ {
+ // 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)
+ {
+ 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;
+ }
+ }
+ return base.GetActorSelectionPoints();
+ }
+
private void OnAddMeshCollider(EditorWindow window)
{
// Allow collider to be added to evey static model selection
diff --git a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs
index 91bd6c530..ef7663e5d 100644
--- a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs
+++ b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs
@@ -78,5 +78,11 @@ namespace FlaxEditor.SceneGraph.Actors
if (Actor is UICanvas uiCanvas && uiCanvas.Is3D)
DebugDraw.DrawWireBox(uiCanvas.Bounds, Color.BlueViolet);
}
+
+ ///
+ public override bool CanSelectActorNodeWithSelector()
+ {
+ return Actor is UICanvas uiCanvas && uiCanvas.Is3D && base.CanSelectActorNodeWithSelector();
+ }
}
}
diff --git a/Source/Editor/SceneGraph/Actors/UIControlNode.cs b/Source/Editor/SceneGraph/Actors/UIControlNode.cs
index 1d912db48..57a956665 100644
--- a/Source/Editor/SceneGraph/Actors/UIControlNode.cs
+++ b/Source/Editor/SceneGraph/Actors/UIControlNode.cs
@@ -40,5 +40,31 @@ namespace FlaxEditor.SceneGraph.Actors
control.PerformLayout();
}
}
+
+ ///
+ public override bool CanSelectActorNodeWithSelector()
+ {
+ // Check if control and skip if canvas is 2D
+ if (Actor is not UIControl uiControl)
+ return false;
+ UICanvas canvas = null;
+ var controlParent = uiControl.Parent;
+ while (controlParent != null && controlParent is not Scene)
+ {
+ if (controlParent is UICanvas uiCanvas)
+ {
+ canvas = uiCanvas;
+ break;
+ }
+ controlParent = controlParent.Parent;
+ }
+
+ if (canvas != null)
+ {
+ if (canvas.Is2D)
+ return false;
+ }
+ return base.CanSelectActorNodeWithSelector();
+ }
}
}
diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs
index a1b709efd..d165e9f2e 100644
--- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs
+++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs
@@ -166,7 +166,7 @@ namespace FlaxEditor.SceneGraph.GUI
/// The filter text.
public void UpdateFilter(string filterText)
{
- // SKip hidden actors
+ // Skip hidden actors
var actor = Actor;
if (actor != null && (actor.HideFlags & HideFlags.HideInHierarchy) != 0)
return;
@@ -238,7 +238,7 @@ namespace FlaxEditor.SceneGraph.GUI
}
else
{
- if (Actor !=null)
+ if (Actor != null)
{
var actorTypeText = trimmedFilter.Replace("a:", "", StringComparison.OrdinalIgnoreCase).Trim();
var name = TypeUtils.GetTypeDisplayName(Actor.GetType());
@@ -248,6 +248,26 @@ namespace FlaxEditor.SceneGraph.GUI
}
}
}
+ // Check for control type
+ else if (trimmedFilter.Contains("c:", StringComparison.OrdinalIgnoreCase))
+ {
+ if (trimmedFilter.Equals("c:", StringComparison.OrdinalIgnoreCase))
+ {
+ if (Actor != null)
+ hasFilter = true;
+ }
+ else
+ {
+ if (Actor != null && Actor is UIControl uic && uic.Control != null)
+ {
+ var controlTypeText = trimmedFilter.Replace("c:", "", StringComparison.OrdinalIgnoreCase).Trim();
+ var name = TypeUtils.GetTypeDisplayName(uic.Control.GetType());
+ var nameNoSpaces = name.Replace(" ", "");
+ if (name.Contains(controlTypeText, StringComparison.OrdinalIgnoreCase) || nameNoSpaces.Contains(controlTypeText, StringComparison.OrdinalIgnoreCase))
+ hasFilter = true;
+ }
+ }
+ }
// Match text
else
{
diff --git a/Source/Editor/Utilities/EditorScene.cpp b/Source/Editor/Utilities/EditorScene.cpp
index e42580111..88d0cd5e3 100644
--- a/Source/Editor/Utilities/EditorScene.cpp
+++ b/Source/Editor/Utilities/EditorScene.cpp
@@ -10,6 +10,9 @@ EditorScene::EditorScene(const SpawnParams& params)
SceneBeginData beginData;
EditorScene::BeginPlay(&beginData);
beginData.OnDone();
+
+ // Mark as internal to prevent collection in ManagedEditor::WipeOutLeftoverSceneObjects
+ Tags.Add(Tags::Get(TEXT("__EditorInternal")));
}
void EditorScene::Update()
diff --git a/Source/Editor/Viewport/EditorGizmoViewport.cs b/Source/Editor/Viewport/EditorGizmoViewport.cs
index d230f53fb..39c3d4bf3 100644
--- a/Source/Editor/Viewport/EditorGizmoViewport.cs
+++ b/Source/Editor/Viewport/EditorGizmoViewport.cs
@@ -138,7 +138,9 @@ namespace FlaxEditor.Viewport
if (useProjectCache)
{
// Initialize snapping enabled from cached values
- if (editor.ProjectCache.TryGetCustomData("TranslateSnapState", out bool cachedBool))
+ if (editor.ProjectCache.TryGetCustomData("AbsoluteSnapState", out bool cachedBool))
+ transformGizmo.AbsoluteSnapEnabled = cachedBool;
+ if (editor.ProjectCache.TryGetCustomData("TranslateSnapState", out cachedBool))
transformGizmo.TranslationSnapEnable = cachedBool;
if (editor.ProjectCache.TryGetCustomData("RotationSnapState", out cachedBool))
transformGizmo.RotationSnapEnabled = cachedBool;
@@ -162,13 +164,31 @@ namespace FlaxEditor.Viewport
TooltipText = $"Gizmo transform space (world or local) ({inputOptions.ToggleTransformSpace})",
Parent = transformSpaceWidget
};
+ transformSpaceWidget.Parent = viewport;
+
+ // Absolute snapping widget
+ var absoluteSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
+ var enableAbsoluteSnapping = new ViewportWidgetButton("A", SpriteHandle.Invalid, null, true)
+ {
+ Checked = transformGizmo.AbsoluteSnapEnabled,
+ TooltipText = "Enable absolute grid snapping (world-space absolute grid, rather than object-relative grid)",
+ Parent = absoluteSnappingWidget
+ };
+ enableAbsoluteSnapping.Toggled += _ =>
+ {
+ transformGizmo.AbsoluteSnapEnabled = !transformGizmo.AbsoluteSnapEnabled;
+ if (useProjectCache)
+ editor.ProjectCache.SetCustomData("AbsoluteSnapState", transformGizmo.AbsoluteSnapEnabled);
+ };
+ absoluteSnappingWidget.Parent = viewport;
+
transformSpaceToggle.Toggled += _ =>
{
transformGizmo.ToggleTransformSpace();
if (useProjectCache)
editor.ProjectCache.SetCustomData("TransformSpaceState", transformGizmo.ActiveTransformSpace.ToString());
+ absoluteSnappingWidget.Visible = transformGizmo.ActiveTransformSpace == TransformGizmoBase.TransformSpace.World;
};
- transformSpaceWidget.Parent = viewport;
// Scale snapping widget
var scaleSnappingWidget = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight);
@@ -383,17 +403,17 @@ namespace FlaxEditor.Viewport
gizmoModeRotate.Checked = mode == TransformGizmoBase.Mode.Rotate;
gizmoModeScale.Checked = mode == TransformGizmoBase.Mode.Scale;
};
-
+
// Setup input actions
- viewport.InputActions.Add(options => options.TranslateMode, () =>
+ viewport.InputActions.Add(options => options.TranslateMode, () =>
{
viewport.GetInput(out var input);
if (input.IsMouseRightDown)
return;
-
+
transformGizmo.ActiveMode = TransformGizmoBase.Mode.Translate;
});
- viewport.InputActions.Add(options => options.RotateMode, () =>
+ viewport.InputActions.Add(options => options.RotateMode, () =>
{
viewport.GetInput(out var input);
if (input.IsMouseRightDown)
diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
index cbb35e155..943bb6e3c 100644
--- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs
+++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs
@@ -7,10 +7,14 @@ using FlaxEditor.Gizmo;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.SceneGraph;
using FlaxEditor.Scripting;
+using FlaxEditor.Tools;
using FlaxEditor.Viewport.Modes;
using FlaxEditor.Windows;
using FlaxEngine;
+using FlaxEngine.Gizmo;
using FlaxEngine.GUI;
+using FlaxEngine.Tools;
+
using Object = FlaxEngine.Object;
namespace FlaxEditor.Viewport
@@ -107,6 +111,7 @@ namespace FlaxEditor.Viewport
private double _lockedFocusOffset;
private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32);
private EditorSpritesRenderer _editorSpritesRenderer;
+ private ViewportRubberBandSelector _rubberBandSelector;
///
/// Drag and drop handlers
@@ -213,6 +218,9 @@ namespace FlaxEditor.Viewport
TransformGizmo.ApplyTransformation += ApplyTransform;
TransformGizmo.Duplicate += _editor.SceneEditing.Duplicate;
Gizmos.Active = TransformGizmo;
+
+ // Add rubber band selector
+ _rubberBandSelector = new ViewportRubberBandSelector(this);
// Add grid
Grid = new GridGizmo(this);
@@ -367,7 +375,10 @@ namespace FlaxEditor.Viewport
{
Gizmos[i].Draw(ref renderContext);
}
-
+
+ // Draw RubberBand for rect selection
+ _rubberBandSelector.Draw(context, target, targetDepth);
+
// Draw selected objects debug shapes and visuals
if (DrawDebugDraw && (renderContext.View.Flags & ViewFlags.DebugDraw) == ViewFlags.DebugDraw)
{
@@ -478,6 +489,22 @@ namespace FlaxEditor.Viewport
TransformGizmo.EndTransforming();
}
+ ///
+ public override void OnLostFocus()
+ {
+ base.OnLostFocus();
+
+ _rubberBandSelector.StopRubberBand();
+ }
+
+ ///
+ public override void OnMouseLeave()
+ {
+ base.OnMouseLeave();
+
+ _rubberBandSelector.StopRubberBand();
+ }
+
///
/// Focuses the viewport on the current selection of the gizmo.
///
@@ -576,6 +603,24 @@ namespace FlaxEditor.Viewport
base.OrientViewport(ref orientation);
}
+ ///
+ public override void OnMouseMove(Float2 location)
+ {
+ base.OnMouseMove(location);
+
+ // 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 || IsAltKeyDown);
+ _rubberBandSelector.TryCreateRubberBand(canStart, _viewMousePos, ViewFrustum);
+ }
+
+ ///
+ protected override void OnLeftMouseButtonDown()
+ {
+ base.OnLeftMouseButtonDown();
+
+ _rubberBandSelector.TryStartingRubberBandSelection();
+ }
+
///
protected override void OnLeftMouseButtonUp()
{
@@ -583,8 +628,12 @@ namespace FlaxEditor.Viewport
if (_prevInput.IsControllingMouse || !Bounds.Contains(ref _viewMousePos))
return;
- // Try to pick something with the current gizmo
- Gizmos.Active?.Pick();
+ // Select rubberbanded rect actor nodes or pick with gizmo
+ if (!_rubberBandSelector.ReleaseRubberBandSelection())
+ {
+ // Try to pick something with the current gizmo
+ Gizmos.Active?.Pick();
+ }
// Keep focus
Focus();
@@ -592,6 +641,22 @@ namespace FlaxEditor.Viewport
base.OnLeftMouseButtonUp();
}
+ ///
+ public override bool OnMouseUp(Float2 location, MouseButton button)
+ {
+ if (base.OnMouseUp(location, button))
+ return true;
+
+ // Handle mouse going up when using rubber band with mouse capture that click up outside the view
+ if (button == MouseButton.Left && !new Rectangle(Float2.Zero, Size).Contains(ref location))
+ {
+ _rubberBandSelector.ReleaseRubberBandSelection();
+ return true;
+ }
+
+ return false;
+ }
+
///
public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
{
diff --git a/Source/Editor/Windows/Assets/AudioClipWindow.cs b/Source/Editor/Windows/Assets/AudioClipWindow.cs
index 5336b2e3c..17e816e57 100644
--- a/Source/Editor/Windows/Assets/AudioClipWindow.cs
+++ b/Source/Editor/Windows/Assets/AudioClipWindow.cs
@@ -1,6 +1,5 @@
// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
-using System.IO;
using System.Xml;
using FlaxEditor.Content;
using FlaxEditor.Content.Import;
diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs
index e8f5626ca..60c832ee0 100644
--- a/Source/Editor/Windows/GameWindow.cs
+++ b/Source/Editor/Windows/GameWindow.cs
@@ -21,7 +21,7 @@ namespace FlaxEditor.Windows
{
private readonly RenderOutputControl _viewport;
private readonly GameRoot _guiRoot;
- private bool _showGUI = true;
+ private bool _showGUI = true, _editGUI = true;
private bool _showDebugDraw = false;
private bool _audioMuted = false;
private float _audioVolume = 1;
@@ -84,6 +84,22 @@ namespace FlaxEditor.Windows
}
}
+ ///
+ /// Gets or sets a value indicating whether allow editing game GUI in the view or keep it visible-only.
+ ///
+ public bool EditGUI
+ {
+ get => _editGUI;
+ set
+ {
+ if (value != _editGUI)
+ {
+ _editGUI = value;
+ _guiRoot.Editable = value;
+ }
+ }
+ }
+
///
/// Gets or sets a value indicating whether show Debug Draw shapes in the view or keep it hidden.
///
@@ -275,8 +291,9 @@ namespace FlaxEditor.Windows
///
private class GameRoot : UIEditorRoot
{
- public override bool EnableInputs => !Time.GamePaused && Editor.IsPlayMode;
- public override bool EnableSelecting => !Editor.IsPlayMode || Time.GamePaused;
+ internal bool Editable = true;
+ public override bool EnableInputs => !Time.GamePaused && Editor.IsPlayMode && Editable;
+ public override bool EnableSelecting => (!Editor.IsPlayMode || Time.GamePaused) && Editable;
public override TransformGizmo TransformGizmo => Editor.Instance.MainTransformGizmo;
}
@@ -670,6 +687,13 @@ namespace FlaxEditor.Windows
checkbox.StateChanged += x => ShowGUI = x.Checked;
}
+ // Edit GUI
+ {
+ var button = menu.AddButton("Edit GUI");
+ var checkbox = new CheckBox(140, 2, EditGUI) { Parent = button };
+ checkbox.StateChanged += x => EditGUI = x.Checked;
+ }
+
// Show Debug Draw
{
var button = menu.AddButton("Show Debug Draw");
@@ -1193,6 +1217,7 @@ namespace FlaxEditor.Windows
public override void OnLayoutSerialize(XmlWriter writer)
{
writer.WriteAttributeString("ShowGUI", ShowGUI.ToString());
+ writer.WriteAttributeString("EditGUI", EditGUI.ToString());
writer.WriteAttributeString("ShowDebugDraw", ShowDebugDraw.ToString());
writer.WriteAttributeString("DefaultViewportScaling", JsonSerializer.Serialize(_defaultViewportScaling));
writer.WriteAttributeString("CustomViewportScaling", JsonSerializer.Serialize(_customViewportScaling));
@@ -1203,6 +1228,8 @@ namespace FlaxEditor.Windows
{
if (bool.TryParse(node.GetAttribute("ShowGUI"), out bool value1))
ShowGUI = value1;
+ if (bool.TryParse(node.GetAttribute("EditGUI"), out value1))
+ EditGUI = value1;
if (bool.TryParse(node.GetAttribute("ShowDebugDraw"), out value1))
ShowDebugDraw = value1;
if (node.HasAttribute("CustomViewportScaling"))
@@ -1228,6 +1255,7 @@ namespace FlaxEditor.Windows
public override void OnLayoutDeserialize()
{
ShowGUI = true;
+ EditGUI = true;
ShowDebugDraw = false;
}
}
diff --git a/Source/Editor/Windows/PropertiesWindow.cs b/Source/Editor/Windows/PropertiesWindow.cs
index 423d3af18..d10b1916f 100644
--- a/Source/Editor/Windows/PropertiesWindow.cs
+++ b/Source/Editor/Windows/PropertiesWindow.cs
@@ -74,6 +74,11 @@ namespace FlaxEditor.Windows
if (Level.ScenesCount > 1)
return;
_actorScrollValues.Clear();
+ if (LockObjects)
+ {
+ LockObjects = false;
+ Presenter.Deselect();
+ }
}
private void OnScrollValueChanged()
diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
index c7e9dc41b..93c244555 100644
--- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
+++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp
@@ -65,6 +65,7 @@ namespace ALC
struct SourceData
{
AudioDataInfo Format;
+ float Pan;
bool Spatial;
};
@@ -106,6 +107,26 @@ namespace ALC
namespace Source
{
+ void SetupSpatial(uint32 sourceID, float pan, bool spatial)
+ {
+ alSourcei(sourceID, AL_SOURCE_RELATIVE, !spatial); // Non-spatial sounds use AL_POSITION for panning
+#ifdef AL_SOFT_source_spatialize
+ alSourcei(sourceID, AL_SOURCE_SPATIALIZE_SOFT, spatial || Math::Abs(pan) > ZeroTolerance ? AL_TRUE : AL_FALSE); // Fix multi-channel sources played as spatial or non-spatial sources played with panning
+#endif
+ if (spatial)
+ {
+#ifdef AL_EXT_STEREO_ANGLES
+ const float panAngle = pan * PI_HALF;
+ const ALfloat panAngles[2] = { (ALfloat)(PI / 6.0 - panAngle), (ALfloat)(-PI / 6.0 - panAngle) }; // Angles are specified counter-clockwise in radians
+ alSourcefv(sourceID, AL_STEREO_ANGLES, panAngles);
+#endif
+ }
+ else
+ {
+ alSource3f(sourceID, AL_POSITION, pan, 0, -sqrtf(1.0f - pan * pan));
+ }
+ }
+
void Rebuild(uint32& sourceID, const Vector3& position, const Quaternion& orientation, float volume, float pitch, float pan, bool loop, bool spatial, float attenuation, float minDistance, float doppler)
{
ASSERT_LOW_LAYER(sourceID == 0);
@@ -116,11 +137,8 @@ namespace ALC
alSourcef(sourceID, AL_PITCH, pitch);
alSourcef(sourceID, AL_SEC_OFFSET, 0.0f);
alSourcei(sourceID, AL_LOOPING, loop);
- alSourcei(sourceID, AL_SOURCE_RELATIVE, !spatial); // Non-spatial sounds use AL_POSITION for panning
alSourcei(sourceID, AL_BUFFER, 0);
-#ifdef AL_SOFT_source_spatialize
- alSourcei(sourceID, AL_SOURCE_SPATIALIZE_SOFT, AL_TRUE); // Always spatialize, fixes multi-channel played as spatial
-#endif
+ SetupSpatial(sourceID, pan, spatial);
if (spatial)
{
alSourcef(sourceID, AL_ROLLOFF_FACTOR, attenuation);
@@ -128,18 +146,12 @@ namespace ALC
alSourcef(sourceID, AL_REFERENCE_DISTANCE, FLAX_DST_TO_OAL(minDistance));
alSource3f(sourceID, AL_POSITION, FLAX_POS_TO_OAL(position));
alSource3f(sourceID, AL_VELOCITY, FLAX_VEL_TO_OAL(Vector3::Zero));
-#ifdef AL_EXT_STEREO_ANGLES
- const float panAngle = pan * PI_HALF;
- const ALfloat panAngles[2] = { (ALfloat)(PI / 6.0 - panAngle), (ALfloat)(-PI / 6.0 - panAngle) }; // Angles are specified counter-clockwise in radians
- alSourcefv(sourceID, AL_STEREO_ANGLES, panAngles);
-#endif
}
else
{
alSourcef(sourceID, AL_ROLLOFF_FACTOR, 0.0f);
alSourcef(sourceID, AL_DOPPLER_FACTOR, 1.0f);
alSourcef(sourceID, AL_REFERENCE_DISTANCE, 0.0f);
- alSource3f(sourceID, AL_POSITION, pan, 0, -sqrtf(1.0f - pan * pan));
alSource3f(sourceID, AL_VELOCITY, 0.0f, 0.0f, 0.0f);
}
}
@@ -160,12 +172,11 @@ namespace ALC
if (Device == nullptr)
return;
- ALCint attrsHrtf[] = { ALC_HRTF_SOFT, ALC_TRUE };
- const ALCint* attrList = nullptr;
+ ALCint attrList[] = { ALC_HRTF_SOFT, ALC_FALSE };
if (Audio::GetEnableHRTF())
{
LOG(Info, "Enabling OpenAL HRTF");
- attrList = attrsHrtf;
+ attrList[1] = ALC_TRUE;
}
Context = alcCreateContext(Device, attrList);
@@ -318,6 +329,7 @@ uint32 AudioBackendOAL::Source_Add(const AudioDataInfo& format, const Vector3& p
auto& data = ALC::SourcesData[sourceID];
data.Format = format;
data.Spatial = spatial;
+ data.Pan = pan;
ALC::Locker.Unlock();
return sourceID;
@@ -370,20 +382,11 @@ void AudioBackendOAL::Source_PitchChanged(uint32 sourceID, float pitch)
void AudioBackendOAL::Source_PanChanged(uint32 sourceID, float pan)
{
ALC::Locker.Lock();
- const bool spatial = ALC::SourcesData[sourceID].Spatial;
+ auto& e = ALC::SourcesData[sourceID];
+ e.Pan = pan;
+ const bool spatial = e.Spatial;
ALC::Locker.Unlock();
- if (spatial)
- {
-#ifdef AL_EXT_STEREO_ANGLES
- const float panAngle = pan * PI_HALF;
- const ALfloat panAngles[2] = { (ALfloat)(PI / 6.0 - panAngle), (ALfloat)(-PI / 6.0 - panAngle) }; // Angles are specified counter-clockwise in radians
- alSourcefv(sourceID, AL_STEREO_ANGLES, panAngles);
-#endif
- }
- else
- {
- alSource3f(sourceID, AL_POSITION, pan, 0, -sqrtf(1.0f - pan * pan));
- }
+ ALC::Source::SetupSpatial(sourceID, pan, spatial);
}
void AudioBackendOAL::Source_IsLoopingChanged(uint32 sourceID, bool loop)
@@ -393,6 +396,9 @@ void AudioBackendOAL::Source_IsLoopingChanged(uint32 sourceID, bool loop)
void AudioBackendOAL::Source_SpatialSetupChanged(uint32 sourceID, bool spatial, float attenuation, float minDistance, float doppler)
{
+ ALC::Locker.Lock();
+ const bool pan = ALC::SourcesData[sourceID].Spatial;
+ ALC::Locker.Unlock();
if (spatial)
{
alSourcef(sourceID, AL_ROLLOFF_FACTOR, attenuation);
@@ -405,6 +411,7 @@ void AudioBackendOAL::Source_SpatialSetupChanged(uint32 sourceID, bool spatial,
alSourcef(sourceID, AL_DOPPLER_FACTOR, 1.0f);
alSourcef(sourceID, AL_REFERENCE_DISTANCE, 0.0f);
}
+ ALC::Source::SetupSpatial(sourceID, pan, spatial);
}
void AudioBackendOAL::Source_Play(uint32 sourceID)
@@ -602,7 +609,7 @@ void AudioBackendOAL::Buffer_Write(uint32 bufferID, byte* samples, const AudioDa
if (!format)
{
- LOG(Error, "Not suppported audio data format for OpenAL device: BitDepth={}, NumChannels={}", info.BitDepth, info.NumChannels);
+ LOG(Error, "Not supported audio data format for OpenAL device: BitDepth={}, NumChannels={}", info.BitDepth, info.NumChannels);
}
}
diff --git a/Source/Engine/ContentImporters/ImportAudio.cpp b/Source/Engine/ContentImporters/ImportAudio.cpp
index 00b914259..1cb6143be 100644
--- a/Source/Engine/ContentImporters/ImportAudio.cpp
+++ b/Source/Engine/ContentImporters/ImportAudio.cpp
@@ -159,19 +159,19 @@ CreateAssetResult ImportAudio::Import(CreateAssetContext& context, AudioDecoder&
else
{
// Split audio data into a several chunks (uniform data spread)
- const int32 minChunkSize = 1 * 1024 * 1024; // 1 MB
- const int32 dataAlignment = info.NumChannels * bytesPerSample; // Ensure to never split samples in-between (eg. 24-bit that uses 3 bytes)
- const int32 chunkSize = Math::Max(minChunkSize, (int32)Math::AlignUp(bufferSize / ASSET_FILE_DATA_CHUNKS, dataAlignment));
- const int32 chunksCount = Math::CeilToInt((float)bufferSize / chunkSize);
+ const uint32 minChunkSize = 1 * 1024 * 1024; // 1 MB
+ const uint32 dataAlignment = info.NumChannels * bytesPerSample * ASSET_FILE_DATA_CHUNKS; // Ensure to never split samples in-between (eg. 24-bit that uses 3 bytes)
+ const uint32 chunkSize = Math::AlignUp(Math::Max(minChunkSize, bufferSize / ASSET_FILE_DATA_CHUNKS), dataAlignment);
+ const int32 chunksCount = Math::CeilToInt((float)bufferSize / (float)chunkSize);
ASSERT(chunksCount > 0 && chunksCount <= ASSET_FILE_DATA_CHUNKS);
byte* ptr = sampleBuffer.Get();
- int32 size = bufferSize;
+ uint32 size = bufferSize;
for (int32 chunkIndex = 0; chunkIndex < chunksCount; chunkIndex++)
{
if (context.AllocateChunk(chunkIndex))
return CreateAssetResult::CannotAllocateChunk;
- const int32 t = Math::Min(size, chunkSize);
+ const uint32 t = Math::Min(size, chunkSize);
WRITE_DATA(chunkIndex, ptr, t);
diff --git a/Source/Engine/Graphics/Models/Mesh.cpp b/Source/Engine/Graphics/Models/Mesh.cpp
index 803321344..c7035c3ef 100644
--- a/Source/Engine/Graphics/Models/Mesh.cpp
+++ b/Source/Engine/Graphics/Models/Mesh.cpp
@@ -520,4 +520,24 @@ PRAGMA_ENABLE_DEPRECATION_WARNINGS
return result;
}
+#if USE_EDITOR
+
+Array Mesh::GetCollisionProxyPoints() const
+{
+ PROFILE_CPU();
+ Array result;
+#if USE_PRECISE_MESH_INTERSECTS
+ for (int32 i = 0; i < _collisionProxy.Triangles.Count(); i++)
+ {
+ auto triangle = _collisionProxy.Triangles.Get()[i];
+ result.Add(triangle.V0);
+ result.Add(triangle.V1);
+ result.Add(triangle.V2);
+ }
+#endif
+ return result;
+}
+
+#endif
+
#endif
diff --git a/Source/Engine/Graphics/Models/Mesh.cs b/Source/Engine/Graphics/Models/Mesh.cs
index e04348e54..ea4097d95 100644
--- a/Source/Engine/Graphics/Models/Mesh.cs
+++ b/Source/Engine/Graphics/Models/Mesh.cs
@@ -536,5 +536,16 @@ namespace FlaxEngine
return result;
}
+
+#if FLAX_EDITOR
+ ///
+ /// Gets the collision proxy points for the mesh.
+ ///
+ /// The triangle points in the collision proxy.
+ internal Vector3[] GetCollisionProxyPoints()
+ {
+ return Internal_GetCollisionProxyPoints(__unmanagedPtr, out _);
+ }
+#endif
}
}
diff --git a/Source/Engine/Graphics/Models/Mesh.h b/Source/Engine/Graphics/Models/Mesh.h
index a10e36797..6119bb203 100644
--- a/Source/Engine/Graphics/Models/Mesh.h
+++ b/Source/Engine/Graphics/Models/Mesh.h
@@ -171,5 +171,8 @@ private:
API_FUNCTION(NoProxy) bool UpdateMeshUInt(int32 vertexCount, int32 triangleCount, const MArray* verticesObj, const MArray* trianglesObj, const MArray* normalsObj, const MArray* tangentsObj, const MArray* uvObj, const MArray* colorsObj);
API_FUNCTION(NoProxy) bool UpdateMeshUShort(int32 vertexCount, int32 triangleCount, const MArray* verticesObj, const MArray* trianglesObj, const MArray* normalsObj, const MArray* tangentsObj, const MArray* uvObj, const MArray* colorsObj);
API_FUNCTION(NoProxy) MArray* DownloadBuffer(bool forceGpu, MTypeObject* resultType, int32 typeI);
+#if USE_EDITOR
+ API_FUNCTION(NoProxy) Array GetCollisionProxyPoints() const;
+#endif
#endif
};
diff --git a/Source/Engine/Graphics/Models/ModelData.h b/Source/Engine/Graphics/Models/ModelData.h
index fba857ff3..4f63bcbd0 100644
--- a/Source/Engine/Graphics/Models/ModelData.h
+++ b/Source/Engine/Graphics/Models/ModelData.h
@@ -91,7 +91,7 @@ public:
int32 LightmapUVsIndex = -1;
///
- /// Global translation for this mesh to be at its local origin.
+ /// Local translation for this mesh to be at it's local origin.
///
Vector3 OriginTranslation = Vector3::Zero;
diff --git a/Source/Engine/Level/Actors/ModelInstanceActor.cpp b/Source/Engine/Level/Actors/ModelInstanceActor.cpp
index 6021daadc..6c770ad64 100644
--- a/Source/Engine/Level/Actors/ModelInstanceActor.cpp
+++ b/Source/Engine/Level/Actors/ModelInstanceActor.cpp
@@ -9,6 +9,11 @@ ModelInstanceActor::ModelInstanceActor(const SpawnParams& params)
{
}
+String ModelInstanceActor::MeshReference::ToString() const
+{
+ return String::Format(TEXT("Actor={},LOD={},Mesh={}"), Actor ? Actor->GetNamePath() : String::Empty, LODIndex, MeshIndex);
+}
+
void ModelInstanceActor::SetEntries(const Array& value)
{
WaitForModelLoad();
diff --git a/Source/Engine/Level/Actors/ModelInstanceActor.h b/Source/Engine/Level/Actors/ModelInstanceActor.h
index c3c118f7c..631df063e 100644
--- a/Source/Engine/Level/Actors/ModelInstanceActor.h
+++ b/Source/Engine/Level/Actors/ModelInstanceActor.h
@@ -27,6 +27,8 @@ API_CLASS(Abstract) class FLAXENGINE_API ModelInstanceActor : public Actor
API_FIELD() int32 LODIndex = 0;
// Index of the mesh (within the LOD).
API_FIELD() int32 MeshIndex = 0;
+
+ String ToString() const;
};
protected:
diff --git a/Source/Engine/Physics/Actors/Cloth.cpp b/Source/Engine/Physics/Actors/Cloth.cpp
index 8dce977db..f81dccd9b 100644
--- a/Source/Engine/Physics/Actors/Cloth.cpp
+++ b/Source/Engine/Physics/Actors/Cloth.cpp
@@ -647,6 +647,7 @@ void Cloth::CalculateInvMasses(Array& invMasses)
if (_paint.Count() != verticesCount)
{
// Fix incorrect paint data
+ LOG(Warning, "Incorrect cloth '{}' paint size {} for mesh '{}' that has {} vertices", GetNamePath(), _paint.Count(), mesh.ToString(), verticesCount);
int32 countBefore = _paint.Count();
_paint.Resize(verticesCount);
for (int32 i = countBefore; i < verticesCount; i++)
@@ -795,7 +796,10 @@ bool Cloth::OnPreUpdate()
if (!positionStream.IsValid() || !blendIndicesStream.IsValid() || !blendWeightsStream.IsValid())
return false;
if (verticesCount != _paint.Count())
+ {
+ LOG(Warning, "Incorrect cloth '{}' paint size {} for mesh '{}' that has {} vertices", GetNamePath(), _paint.Count(), mesh.ToString(), verticesCount);
return false;
+ }
PROFILE_CPU_NAMED("Skinned Pose");
PhysicsBackend::LockClothParticles(_cloth);
const Span particles = PhysicsBackend::GetClothParticles(_cloth);
diff --git a/Source/Engine/Render2D/Font.h b/Source/Engine/Render2D/Font.h
index 187d5b24a..d642e4116 100644
--- a/Source/Engine/Render2D/Font.h
+++ b/Source/Engine/Render2D/Font.h
@@ -399,7 +399,7 @@ public:
///
/// The input text to test.
/// The layout properties.
- /// The minimum size for that text and fot to render properly.
+ /// The minimum size for that text and font to render properly.
API_FUNCTION() Float2 MeasureText(const StringView& text, API_PARAM(Ref) const TextLayoutOptions& layout);
///
@@ -408,7 +408,7 @@ public:
/// The input text to test.
/// The input text range (substring range of the input text parameter).
/// The layout properties.
- /// The minimum size for that text and fot to render properly.
+ /// The minimum size for that text and font to render properly.
API_FUNCTION() Float2 MeasureText(const StringView& text, API_PARAM(Ref) const TextRange& textRange, API_PARAM(Ref) const TextLayoutOptions& layout)
{
return MeasureText(textRange.Substring(text), layout);
@@ -418,7 +418,7 @@ public:
/// Measures minimum size of the rectangle that will be needed to draw given text
/// .
/// The input text to test.
- /// The minimum size for that text and fot to render properly.
+ /// The minimum size for that text and font to render properly.
API_FUNCTION() FORCE_INLINE Float2 MeasureText(const StringView& text)
{
return MeasureText(text, TextLayoutOptions());
@@ -429,7 +429,7 @@ public:
/// .
/// The input text to test.
/// The input text range (substring range of the input text parameter).
- /// The minimum size for that text and fot to render properly.
+ /// The minimum size for that text and font to render properly.
API_FUNCTION() FORCE_INLINE Float2 MeasureText(const StringView& text, API_PARAM(Ref) const TextRange& textRange)
{
return MeasureText(textRange.Substring(text), TextLayoutOptions());
diff --git a/Source/Engine/Scripting/Scripting.cpp b/Source/Engine/Scripting/Scripting.cpp
index 4731a088d..a0b55376f 100644
--- a/Source/Engine/Scripting/Scripting.cpp
+++ b/Source/Engine/Scripting/Scripting.cpp
@@ -732,7 +732,12 @@ Array Scripting::GetObjects()
{
Array objects;
_objectsLocker.Lock();
+#if USE_OBJECTS_DISPOSE_CRASHES_DEBUGGING
+ for (const auto& e : _objectsDictionary)
+ objects.Add(e.Value.Ptr);
+#else
_objectsDictionary.GetValues(objects);
+#endif
_objectsLocker.Unlock();
return objects;
}
diff --git a/Source/Engine/Serialization/JsonTools.cpp b/Source/Engine/Serialization/JsonTools.cpp
index 07fe027ad..95ba1e162 100644
--- a/Source/Engine/Serialization/JsonTools.cpp
+++ b/Source/Engine/Serialization/JsonTools.cpp
@@ -185,7 +185,7 @@ Ray JsonTools::GetRay(const Value& value)
{
return Ray(
GetVector3(value, "Position", Vector3::Zero),
- GetVector3(value, "Direction", Vector3::One)
+ GetVector3(value, "Direction", Vector3::Forward)
);
}
diff --git a/Source/Engine/Tools/AudioTool/AudioTool.cpp b/Source/Engine/Tools/AudioTool/AudioTool.cpp
index f1709af7e..aa9570989 100644
--- a/Source/Engine/Tools/AudioTool/AudioTool.cpp
+++ b/Source/Engine/Tools/AudioTool/AudioTool.cpp
@@ -4,6 +4,7 @@
#include "AudioTool.h"
#include "Engine/Core/Core.h"
+#include "Engine/Core/Math/Math.h"
#include "Engine/Core/Memory/Allocation.h"
#if USE_EDITOR
#include "Engine/Serialization/Serialization.h"
@@ -307,8 +308,9 @@ void AudioTool::ConvertFromFloat(const float* input, int32* output, uint32 numSa
{
for (uint32 i = 0; i < numSamples; i++)
{
- const float sample = *(float*)input;
- output[i] = static_cast(sample * 2147483647.0f);
+ float sample = *(float*)input;
+ sample = Math::Clamp(sample, -1.0f + ZeroTolerance, +1.0f - ZeroTolerance);
+ output[i] = static_cast(sample * 2147483648.0f);
input++;
}
}
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp
index 0ffdc5ce1..1b3d1231e 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp
+++ b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp
@@ -597,24 +597,10 @@ bool ImportMesh(int32 index, ModelData& result, AssimpImporterData& data, String
// Link mesh
meshData->NodeIndex = nodeIndex;
AssimpNode* curNode = &data.Nodes[meshData->NodeIndex];
- Vector3 translation = Vector3::Zero;
- Vector3 scale = Vector3::One;
- Quaternion rotation = Quaternion::Identity;
-
- while (true)
- {
- translation += curNode->LocalTransform.Translation;
- scale *= curNode->LocalTransform.Scale;
- rotation *= curNode->LocalTransform.Orientation;
-
- if (curNode->ParentIndex == -1)
- break;
- curNode = &data.Nodes[curNode->ParentIndex];
- }
-
- meshData->OriginTranslation = translation;
- meshData->OriginOrientation = rotation;
- meshData->Scaling = scale;
+
+ meshData->OriginTranslation = curNode->LocalTransform.Translation;
+ meshData->OriginOrientation = curNode->LocalTransform.Orientation;
+ meshData->Scaling = curNode->LocalTransform.Scale;
if (result.LODs.Count() <= lodIndex)
result.LODs.Resize(lodIndex + 1);
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp
index d4ea9d0aa..c36e9f50a 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp
+++ b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp
@@ -992,7 +992,8 @@ bool ProcessMesh(ModelData& result, OpenFbxImporterData& data, const ofbx::Mesh*
}*/
// Get local transform for origin shifting translation
- auto translation = ToMatrix(aMesh->getGlobalTransform()).GetTranslation();
+
+ auto translation = Float3((float)aMesh->getLocalTranslation().x, (float)aMesh->getLocalTranslation().y, (float)aMesh->getLocalTranslation().z);
auto scale = data.GlobalSettings.UnitScaleFactor;
if (data.GlobalSettings.CoordAxis == ofbx::CoordSystem_RightHanded)
mesh.OriginTranslation = scale * Vector3(translation.X, translation.Y, -translation.Z);
diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp
index 3bb098a60..71f49e0a2 100644
--- a/Source/Engine/Tools/ModelTool/ModelTool.cpp
+++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp
@@ -1549,23 +1549,25 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
// Prepare import transformation
Transform importTransform(options.Translation, options.Rotation, Float3(options.Scale));
- if (options.UseLocalOrigin && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems())
- {
- importTransform.Translation -= importTransform.Orientation * data.LODs[0].Meshes[0]->OriginTranslation * importTransform.Scale;
- }
- if (options.CenterGeometry && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems())
- {
- // Calculate the bounding box (use LOD0 as a reference)
- BoundingBox box = data.LODs[0].GetBox();
- auto center = data.LODs[0].Meshes[0]->OriginOrientation * importTransform.Orientation * box.GetCenter() * importTransform.Scale * data.LODs[0].Meshes[0]->Scaling;
- importTransform.Translation -= center;
- }
// Apply the import transformation
- if (!importTransform.IsIdentity() && data.Nodes.HasItems())
+ if ((!importTransform.IsIdentity() || options.UseLocalOrigin || options.CenterGeometry) && data.Nodes.HasItems())
{
if (options.Type == ModelType::SkinnedModel)
{
+ // Setup other transform options
+ if (options.UseLocalOrigin && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems())
+ {
+ importTransform.Translation -= importTransform.Orientation * data.LODs[0].Meshes[0]->OriginTranslation * importTransform.Scale;
+ }
+ if (options.CenterGeometry && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems())
+ {
+ // Calculate the bounding box (use LOD0 as a reference)
+ BoundingBox box = data.LODs[0].GetBox();
+ auto center = data.LODs[0].Meshes[0]->OriginOrientation * importTransform.Orientation * box.GetCenter() * importTransform.Scale * data.LODs[0].Meshes[0]->Scaling;
+ importTransform.Translation -= center;
+ }
+
// Transform the root node using the import transformation
auto& root = data.Skeleton.RootNode();
Transform meshTransform = root.LocalTransform.WorldToLocal(importTransform).LocalToWorld(root.LocalTransform);
@@ -1596,9 +1598,31 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
}
else
{
- // Transform the root node using the import transformation
- auto& root = data.Nodes[0];
- root.LocalTransform = importTransform.LocalToWorld(root.LocalTransform);
+ // Transform the nodes using the import transformation
+ if (data.LODs.HasItems() && data.LODs[0].Meshes.HasItems())
+ {
+ for (int i = 0; i < data.LODs[0].Meshes.Count(); ++i)
+ {
+ auto* meshData = data.LODs[0].Meshes[i];
+ Transform transform = importTransform;
+ if (options.UseLocalOrigin)
+ {
+ transform.Translation -= transform.Orientation * meshData->OriginTranslation * transform.Scale;
+ }
+ if (options.CenterGeometry)
+ {
+ // Calculate the bounding box (use LOD0 as a reference)
+ BoundingBox box = data.LODs[0].GetBox();
+ auto center = meshData->OriginOrientation * transform.Orientation * box.GetCenter() * transform.Scale * meshData->Scaling;
+ transform.Translation -= center;
+ }
+
+ int32 nodeIndex = meshData->NodeIndex;
+
+ auto& node = data.Nodes[nodeIndex];
+ node.LocalTransform = transform.LocalToWorld(node.LocalTransform);
+ }
+ }
}
}