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); + } + } } }