diff --git a/Source/Editor/CustomEditors/Dedicated/NavMeshBoundsVolumeEditor.cs b/Source/Editor/CustomEditors/Dedicated/NavMeshBoundsVolumeEditor.cs new file mode 100644 index 000000000..2cbf01e41 --- /dev/null +++ b/Source/Editor/CustomEditors/Dedicated/NavMeshBoundsVolumeEditor.cs @@ -0,0 +1,38 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using FlaxEngine; + +namespace FlaxEditor.CustomEditors.Dedicated +{ + /// + /// Custom editor for . + /// + /// + [CustomEditor(typeof(NavMeshBoundsVolume)), DefaultEditor] + internal class NavMeshBoundsVolumeEditor : ActorEditor + { + /// + public override void Initialize(LayoutElementsContainer layout) + { + base.Initialize(layout); + + if (Values.HasDifferentTypes == false) + { + var button = layout.Button("Build"); + button.Button.Clicked += OnBuildClicked; + } + } + + private void OnBuildClicked() + { + foreach (var value in Values) + { + if (value is NavMeshBoundsVolume volume) + { + Navigation.BuildNavMesh(volume.Box, volume.Scene); + Editor.Instance.Scene.MarkSceneEdited(volume.Scene); + } + } + } + } +} diff --git a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs index 28593a7f5..02742b512 100644 --- a/Source/Editor/CustomEditors/Editors/CollectionEditor.cs +++ b/Source/Editor/CustomEditors/Editors/CollectionEditor.cs @@ -593,11 +593,12 @@ namespace FlaxEditor.CustomEditors.Editors panel.Panel.Offsets = new Margin(7, 7, 0, 0); panel.Panel.BackgroundColor = _background; var elementType = ElementType; + var bindingAttr = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public; bool single = elementType.IsPrimitive || elementType.Equals(new ScriptType(typeof(string))) || elementType.IsEnum || - (elementType.GetFields().Length == 1 && elementType.GetProperties().Length == 0) || - (elementType.GetProperties().Length == 1 && elementType.GetFields().Length == 0) || + (elementType.GetFields(bindingAttr).Length == 1 && elementType.GetProperties(bindingAttr).Length == 0) || + (elementType.GetProperties(bindingAttr).Length == 1 && elementType.GetFields(bindingAttr).Length == 0) || elementType.Equals(new ScriptType(typeof(JsonAsset))) || elementType.Equals(new ScriptType(typeof(SettingsBase))); if (_cachedDropPanels == null) diff --git a/Source/Editor/Editor.cs b/Source/Editor/Editor.cs index 58466e35d..c881d9dcd 100644 --- a/Source/Editor/Editor.cs +++ b/Source/Editor/Editor.cs @@ -23,6 +23,7 @@ using FlaxEngine.Assertions; using FlaxEngine.GUI; using FlaxEngine.Interop; using FlaxEngine.Json; +using FlaxEngine.Utilities; #pragma warning disable CS1591 @@ -1370,7 +1371,7 @@ namespace FlaxEditor public void BuildCSG() { var scenes = Level.Scenes; - scenes.ToList().ForEach(x => x.BuildCSG(0)); + scenes.ForEach(x => x.BuildCSG(0)); Scene.MarkSceneEdited(scenes); } @@ -1380,7 +1381,7 @@ namespace FlaxEditor public void BuildNavMesh() { var scenes = Level.Scenes; - scenes.ToList().ForEach(x => Navigation.BuildNavMesh(x, 0)); + Navigation.BuildNavMesh(); Scene.MarkSceneEdited(scenes); } diff --git a/Source/Editor/GUI/ContextMenu/ContextMenu.cs b/Source/Editor/GUI/ContextMenu/ContextMenu.cs index 896bd5bc2..f5705c6f5 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenu.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenu.cs @@ -502,6 +502,7 @@ namespace FlaxEditor.GUI.ContextMenu if (base.OnKeyDown(key)) return true; + // Keyboard navigation around the menu switch (key) { case KeyboardKeys.ArrowDown: @@ -526,6 +527,20 @@ namespace FlaxEditor.GUI.ContextMenu } } break; + case KeyboardKeys.ArrowRight: + for (int i = 0; i < _panel.Children.Count; i++) + { + if (_panel.Children[i] is ContextMenuChildMenu item && item.Visible && item.IsFocused && !item.ContextMenu.IsOpened) + { + item.ShowChild(this); + item.ContextMenu._panel.Children.FirstOrDefault(x => x is ContextMenuButton && x.Visible)?.Focus(); + break; + } + } + break; + case KeyboardKeys.ArrowLeft: + ParentCM?.RootWindow.Focus(); + break; } return false; diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs index 0261792e7..2f517d903 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuBase.cs @@ -72,6 +72,11 @@ namespace FlaxEditor.GUI.ContextMenu /// public bool HasChildCMOpened => _childCM != null; + /// + /// Gets the parent context menu (if exists). + /// + public ContextMenuBase ParentCM => _parentCM; + /// /// Gets the topmost context menu. /// @@ -81,9 +86,7 @@ namespace FlaxEditor.GUI.ContextMenu { var cm = this; while (cm._parentCM != null && cm._isSubMenu) - { cm = cm._parentCM; - } return cm; } } @@ -108,6 +111,11 @@ namespace FlaxEditor.GUI.ContextMenu /// public bool UseInput = true; + /// + /// Optional flag that can disable UI navigation (tab/enter). + /// + public bool UseNavigation = true; + /// /// Initializes a new instance of the class. /// @@ -594,6 +602,21 @@ namespace FlaxEditor.GUI.ContextMenu case KeyboardKeys.Escape: Hide(); return true; + case KeyboardKeys.Return: + if (UseNavigation && Root?.FocusedControl != null) + { + Root.SubmitFocused(); + return true; + } + break; + case KeyboardKeys.Tab: + if (UseNavigation && Root != null) + { + bool shiftDown = Root.GetKey(KeyboardKeys.Shift); + Root.Navigate(shiftDown ? NavDirection.Previous : NavDirection.Next); + return true; + } + break; } return false; } diff --git a/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs b/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs index 74ab560fb..78337d011 100644 --- a/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs +++ b/Source/Editor/GUI/ContextMenu/ContextMenuChildMenu.cs @@ -29,7 +29,7 @@ namespace FlaxEditor.GUI.ContextMenu CloseMenuOnClick = false; } - private void ShowChild(ContextMenu parentContextMenu) + internal void ShowChild(ContextMenu parentContextMenu) { // Hide parent CM popups and set itself as child var vAlign = parentContextMenu.ItemsAreaMargin.Top; diff --git a/Source/Editor/GUI/CurveEditor.Contents.cs b/Source/Editor/GUI/CurveEditor.Contents.cs index c2831046b..75f37d457 100644 --- a/Source/Editor/GUI/CurveEditor.Contents.cs +++ b/Source/Editor/GUI/CurveEditor.Contents.cs @@ -522,6 +522,16 @@ namespace FlaxEditor.GUI cm.AddButton("Show whole curve", _editor.ShowWholeCurve); cm.AddButton("Reset view", _editor.ResetView); } + cm.AddSeparator(); + var presetCm = cm.AddChildMenu("Apply preset"); + foreach (var value in Enum.GetValues(typeof(CurvePreset))) + { + CurvePreset preset = (CurvePreset)value; + string name = Utilities.Utils.GetPropertyNameUI(preset.ToString()); + var b = presetCm.ContextMenu.AddButton(name, () => _editor.ApplyPreset(preset)); + b.Enabled = !(_editor is LinearCurveEditor && (preset != CurvePreset.Constant && preset != CurvePreset.Linear)); + } + _editor.OnShowContextMenu(cm, selectionCount); cm.Show(this, location); } @@ -619,6 +629,33 @@ namespace FlaxEditor.GUI } } + /// + /// A list of avaliable curve presets for the . + /// + public enum CurvePreset + { + /// + /// A curve where every point has the same value. + /// + Constant, + /// + /// A curve linear curve. + /// + Linear, + /// + /// A curve that starts a slowly and then accelerates until the end. + /// + EaseIn, + /// + /// A curve that starts a steep and then flattens until the end. + /// + EaseOut, + /// + /// A combination of the and preset. + /// + Smoothstep + } + /// public override void OnKeyframesDeselect(IKeyframesEditor editor) { diff --git a/Source/Editor/GUI/CurveEditor.cs b/Source/Editor/GUI/CurveEditor.cs index 706d07b32..4fb727ea1 100644 --- a/Source/Editor/GUI/CurveEditor.cs +++ b/Source/Editor/GUI/CurveEditor.cs @@ -19,6 +19,48 @@ namespace FlaxEditor.GUI /// public abstract partial class CurveEditor : CurveEditorBase where T : new() { + /// + /// Represents a single point in a . + /// + protected struct CurvePresetPoint + { + /// + /// The time. + /// + public float Time; + + /// + /// The value. + /// + public float Value; + + /// + /// The in tangent. Will be ignored in + /// + public float TangentIn; + + /// + /// The out tangent. Will be ignored in + /// + public float TangentOut; + } + + /// + /// A curve preset. + /// + protected struct CurveEditorPreset() + { + /// + /// If the tangents will be linear or smooth. + /// + public bool LinearTangents; + + /// + /// The points of the preset. + /// + public List Points; + } + private class Popup : ContextMenuBase { private CustomEditorPresenter _presenter; @@ -26,11 +68,12 @@ namespace FlaxEditor.GUI private List _keyframeIndices; private bool _isDirty; - public Popup(CurveEditor editor, object[] selection, List keyframeIndices = null, float height = 140.0f) - : this(editor, height) + public Popup(CurveEditor editor, object[] selection, List keyframeIndices = null, float maxHeight = 140.0f) + : this(editor, maxHeight) { _presenter.Select(selection); _presenter.OpenAllGroups(); + Size = new Float2(Size.X, Mathf.Min(_presenter.ContainerControl.Size.Y, maxHeight)); _keyframeIndices = keyframeIndices; if (keyframeIndices != null && selection.Length != keyframeIndices.Count) throw new Exception(); @@ -169,7 +212,7 @@ namespace FlaxEditor.GUI if (IsSelected) color = Editor.ContainsFocus ? style.SelectionBorder : Color.Lerp(style.ForegroundDisabled, style.SelectionBorder, 0.4f); if (IsMouseOver) - color *= 1.1f; + color *= 1.5f; Render2D.FillRectangle(rect, color); } @@ -285,7 +328,7 @@ namespace FlaxEditor.GUI /// /// The keyframes size. /// - protected static readonly Float2 KeyframesSize = new Float2(7.0f); + protected static readonly Float2 KeyframesSize = new Float2(8.0f); /// /// The colors for the keyframe points. @@ -326,6 +369,63 @@ namespace FlaxEditor.GUI private Color _labelsColor; private Font _labelsFont; + /// + /// Preset values for to be applied to a . + /// + protected Dictionary Presets = new Dictionary + { + { CurvePreset.Constant, new CurveEditorPreset + { + LinearTangents = true, + Points = new List + { + new CurvePresetPoint { Time = 0f, Value = 0.5f, TangentIn = 0f, TangentOut = 0f }, + new CurvePresetPoint { Time = 1f, Value = 0.5f, TangentIn = 0f, TangentOut = 0f }, + } + } + }, + { CurvePreset.EaseIn, new CurveEditorPreset + { + LinearTangents = false, + Points = new List + { + new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f }, + new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = -1.4f, TangentOut = 0f }, + } + } + }, + { CurvePreset.EaseOut, new CurveEditorPreset + { + LinearTangents = false, + Points = new List + { + new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f }, + new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 1.4f }, + } + } + }, + { CurvePreset.Linear, new CurveEditorPreset + { + LinearTangents = true, + Points = new List + { + new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f }, + new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f }, + } + } + }, + { CurvePreset.Smoothstep, new CurveEditorPreset + { + LinearTangents = false, + Points = new List + { + new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f }, + new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f }, + } + } + }, + }; + /// /// The keyframe UI points. /// @@ -568,6 +668,28 @@ namespace FlaxEditor.GUI /// The list of indices of the keyframes to remove. protected abstract void RemoveKeyframesInternal(HashSet indicesToRemove); + /// + /// Tries to convert a float to the type of the type wildcard of the curve editor. + /// + /// The float. + /// The converted value. + public static object ConvertCurvePresetValueToCurveEditorType(float value) + { + if (typeof(T) == typeof(Float2)) + return new Float2(value); + if (typeof(T) == typeof(Float3)) + return new Float3(value); + if (typeof(T) == typeof(Float4)) + return new Float4(value); + if (typeof(T) == typeof(Vector2)) + return new Vector2(value); + if (typeof(T) == typeof(Vector3)) + return new Vector3(value); + if (typeof(T) == typeof(Vector4)) + return new Vector4(value); + return value; + } + /// /// Called when showing a context menu. Can be used to add custom buttons with actions. /// @@ -752,6 +874,17 @@ namespace FlaxEditor.GUI ShowCurve(false); } + /// + /// Applies a to the curve editor. + /// + /// The preset. + public virtual void ApplyPreset(CurvePreset preset) + { + // Remove existing keyframes + SelectAll(); + RemoveKeyframes(); + } + /// public override void Evaluate(out object result, float time, bool loop = false) { @@ -1028,6 +1161,31 @@ namespace FlaxEditor.GUI return true; } + bool left = key == KeyboardKeys.ArrowLeft; + bool right = key == KeyboardKeys.ArrowRight; + bool up = key == KeyboardKeys.ArrowUp; + bool down = key == KeyboardKeys.ArrowDown; + + if (left || right || up || down) + { + bool shift = Root.GetKey(KeyboardKeys.Shift); + bool alt = Root.GetKey(KeyboardKeys.Alt); + float deltaValue = 10f; + if (shift || alt) + deltaValue = shift ? 2.5f : 5f; + + Float2 moveDelta = Float2.Zero; + if (left || right) + moveDelta.X = left ? -deltaValue : deltaValue; + if (up || down) + moveDelta.Y = up ? -deltaValue : deltaValue; + + _contents.OnMoveStart(Float2.Zero); + _contents.OnMove(moveDelta); + _contents.OnMoveEnd(Float2.Zero); + return true; + } + return false; } @@ -1526,6 +1684,22 @@ namespace FlaxEditor.GUI _tangents[i].Visible = false; } + /// + public override void ApplyPreset(CurvePreset preset) + { + base.ApplyPreset(preset); + + CurveEditorPreset data = Presets[preset]; + foreach (var point in data.Points) + { + float time = point.Time; + object value = ConvertCurvePresetValueToCurveEditorType((float)point.Value); + AddKeyframe(time, value); + } + + ShowWholeCurve(); + } + /// protected override void DrawCurve(ref Rectangle viewRect) { @@ -2312,6 +2486,30 @@ namespace FlaxEditor.GUI } } + /// + public override void ApplyPreset(CurvePreset preset) + { + base.ApplyPreset(preset); + + CurveEditorPreset data = Presets[preset]; + + foreach (var point in data.Points) + { + float time = point.Time; + object value = ConvertCurvePresetValueToCurveEditorType((float)point.Value); + object tangentIn = ConvertCurvePresetValueToCurveEditorType((float)point.TangentIn); + object tangentOut = ConvertCurvePresetValueToCurveEditorType((float)point.TangentOut); + + AddKeyframe(time, value, tangentIn, tangentOut); + } + + SelectAll(); + if (data.LinearTangents) + SetTangentsLinear(); + + ShowWholeCurve(); + } + /// protected override void SetScaleInternal(ref Float2 scale) { diff --git a/Source/Editor/GUI/Docking/DockPanel.cs b/Source/Editor/GUI/Docking/DockPanel.cs index bfac161f0..c8900dcba 100644 --- a/Source/Editor/GUI/Docking/DockPanel.cs +++ b/Source/Editor/GUI/Docking/DockPanel.cs @@ -469,7 +469,7 @@ namespace FlaxEditor.GUI.Docking var childPanels = _childPanels.ToArray(); if (childPanels.Length != 0) { - // Move tabs from child panels into this one + // Fallback: move tabs from child panels into this one. DockWindow selectedTab = null; foreach (var childPanel in childPanels) { @@ -490,7 +490,8 @@ namespace FlaxEditor.GUI.Docking { // Unlink splitter var splitterParent = splitter.Parent; - Assert.IsNotNull(splitterParent); + if (splitterParent == null) + return; splitter.Parent = null; // Move controls from second split panel to the split panel parent @@ -507,17 +508,63 @@ namespace FlaxEditor.GUI.Docking splitter.Dispose(); } } + else if (IsMaster && _childPanels.Count != 0) + { + if (TryCollapseSplitter(_tabsProxy?.Parent as Panel)) + return; + } else if (!IsMaster) { throw new InvalidOperationException(); } } + else if (_childPanels.Count != 0) + { + if (TryCollapseSplitter(_tabsProxy?.Parent as Panel)) + return; + } else if (!IsMaster) { throw new InvalidOperationException(); } } + internal bool CollapseEmptyTabsProxy() + { + if (TabsCount == 0 && ChildPanelsCount > 0) + { + return TryCollapseSplitter(_tabsProxy?.Parent as Panel); + } + return false; + } + + private bool TryCollapseSplitter(Panel removedPanelParent) + { + if (removedPanelParent == null) + return false; + if (!(removedPanelParent.Parent is SplitPanel tabsSplitter)) + return false; + + var splitterParent = tabsSplitter.Parent; + if (splitterParent == null) + return false; + tabsSplitter.Parent = null; + + var scrPanel = removedPanelParent == tabsSplitter.Panel2 ? tabsSplitter.Panel1 : tabsSplitter.Panel2; + var srcPanelChildrenCount = scrPanel.ChildrenCount; + for (int i = srcPanelChildrenCount - 1; i >= 0 && scrPanel.ChildrenCount > 0; i--) + { + scrPanel.GetChild(i).Parent = splitterParent; + } + Assert.IsTrue(scrPanel.ChildrenCount == 0); + Assert.IsTrue(splitterParent.ChildrenCount == srcPanelChildrenCount); + + tabsSplitter.Dispose(); + if (_tabsProxy != null && _tabsProxy.Parent == removedPanelParent) + _tabsProxy = null; + return true; + } + internal virtual void DockWindowInternal(DockState state, DockWindow window, bool autoSelect = true, float? splitterValue = null) { DockWindow(state, window, autoSelect, splitterValue); diff --git a/Source/Editor/GUI/Tree/TreeNode.cs b/Source/Editor/GUI/Tree/TreeNode.cs index ed1257819..b0ce8c251 100644 --- a/Source/Editor/GUI/Tree/TreeNode.cs +++ b/Source/Editor/GUI/Tree/TreeNode.cs @@ -1140,8 +1140,11 @@ namespace FlaxEditor.GUI.Tree ParentTree.DraggedOverNode = this; // Expand node if mouse goes over arrow - if (ArrowRect.Contains(location) && HasAnyVisibleChild) + if (ArrowRect.Contains(location) && HasAnyVisibleChild && IsCollapsed) + { Expand(true); + ParentTree?.FlushPendingPerformLayout(); + } result = OnDragEnterHeader(data); } @@ -1172,8 +1175,11 @@ namespace FlaxEditor.GUI.Tree ParentTree.DraggedOverNode = this; // Expand node if mouse goes over arrow - if (ArrowRect.Contains(location) && HasAnyVisibleChild) + if (ArrowRect.Contains(location) && HasAnyVisibleChild && IsCollapsed) + { Expand(true); + ParentTree?.FlushPendingPerformLayout(); + } if (!_isDragOverHeader) result = OnDragEnterHeader(data); diff --git a/Source/Editor/Gizmo/GizmoBase.cs b/Source/Editor/Gizmo/GizmoBase.cs index 57cd62d79..80c3e5012 100644 --- a/Source/Editor/Gizmo/GizmoBase.cs +++ b/Source/Editor/Gizmo/GizmoBase.cs @@ -13,6 +13,7 @@ namespace FlaxEditor.Gizmo public abstract class GizmoBase { private IGizmoOwner _owner; + private bool _visible = true; /// /// Gets the gizmo owner. @@ -34,6 +35,11 @@ namespace FlaxEditor.Gizmo /// public virtual BoundingSphere FocusBounds => BoundingSphere.Empty; + /// + /// Gets or sets a value indicating whether this gizmo is visible. + /// + public bool Visible { get { return _visible; } set { _visible = value; } } + /// /// Initializes a new instance of the class. /// diff --git a/Source/Editor/Gizmo/ViewportRubberBandSelector.cs b/Source/Editor/Gizmo/ViewportRubberBandSelector.cs index 542e8b388..66e835fac 100644 --- a/Source/Editor/Gizmo/ViewportRubberBandSelector.cs +++ b/Source/Editor/Gizmo/ViewportRubberBandSelector.cs @@ -36,11 +36,12 @@ public sealed class ViewportRubberBandSelector /// Triggers the start of a rubber band selection. /// /// True if selection started, otherwise false. - public bool TryStartingRubberBandSelection() + public bool TryStartingRubberBandSelection(Float2 mousePosition) { if (!_isRubberBandSpanning && _owner.Gizmos.Active != null && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown) { _tryStartRubberBand = true; + _cachedStartingMousePosition = mousePosition; return true; } return false; @@ -82,12 +83,15 @@ public sealed class ViewportRubberBandSelector return; } - if (_tryStartRubberBand && (Mathf.Abs(_owner.MouseDelta.X) > 0.1f || Mathf.Abs(_owner.MouseDelta.Y) > 0.1f) && canStart) + if (_tryStartRubberBand && canStart) { - _isRubberBandSpanning = true; - _cachedStartingMousePosition = mousePosition; - _rubberBandRect = new Rectangle(_cachedStartingMousePosition, Float2.Zero); - _tryStartRubberBand = false; + var delta = mousePosition - _cachedStartingMousePosition; + if (Mathf.Abs(delta.X) > 0.1f || Mathf.Abs(delta.Y) > 0.1f) + { + _isRubberBandSpanning = true; + _rubberBandRect = new Rectangle(_cachedStartingMousePosition, Float2.Zero); + _tryStartRubberBand = false; + } } else if (_isRubberBandSpanning && _owner.Gizmos.Active != null && !_owner.Gizmos.Active.IsControllingMouse && !_owner.IsRightMouseButtonDown) { diff --git a/Source/Editor/Modules/SceneEditingModule.cs b/Source/Editor/Modules/SceneEditingModule.cs index c36866bc3..b1a5be6f1 100644 --- a/Source/Editor/Modules/SceneEditingModule.cs +++ b/Source/Editor/Modules/SceneEditingModule.cs @@ -229,7 +229,7 @@ namespace FlaxEditor.Modules if (!isPlayMode && options.General.AutoRebuildNavMesh && actor.Scene && node.AffectsNavigationWithChildren) { var bounds = actor.BoxWithChildren; - Navigation.BuildNavMesh(actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs); } } diff --git a/Source/Editor/Modules/WindowsModule.cs b/Source/Editor/Modules/WindowsModule.cs index 218394a3b..162fa3a40 100644 --- a/Source/Editor/Modules/WindowsModule.cs +++ b/Source/Editor/Modules/WindowsModule.cs @@ -491,10 +491,15 @@ namespace FlaxEditor.Modules Editor.LogWarning("Empty panel inside layout."); p.RemoveIt(); } + else + { + p.CollapseEmptyTabsProxy(); + } } } panel.SelectTab(selectedTab); + panel.CollapseEmptyTabsProxy(); } private static void SaveBounds(XmlWriter writer, Window win) diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs index a759b7247..9b15c0c09 100644 --- a/Source/Editor/Options/InputOptions.cs +++ b/Source/Editor/Options/InputOptions.cs @@ -387,6 +387,14 @@ namespace FlaxEditor.Options [EditorDisplay("Viewport"), EditorOrder(1760)] public InputBinding ToggleOrthographic = new InputBinding(KeyboardKeys.NumpadDecimal); + [DefaultValue(typeof(InputBinding), "G")] + [EditorDisplay("Viewport"), EditorOrder(1770)] + public InputBinding ToggleGameView = new InputBinding(KeyboardKeys.G); + + [DefaultValue(typeof(InputBinding), "P")] + [EditorDisplay("Viewport"), EditorOrder(1770)] + public InputBinding ToggleNavMeshVisibility = new InputBinding(KeyboardKeys.P); + #endregion #region Debug Views diff --git a/Source/Editor/SceneGraph/Actors/SplineNode.cs b/Source/Editor/SceneGraph/Actors/SplineNode.cs index de319ca1d..515939526 100644 --- a/Source/Editor/SceneGraph/Actors/SplineNode.cs +++ b/Source/Editor/SceneGraph/Actors/SplineNode.cs @@ -555,7 +555,7 @@ namespace FlaxEditor.SceneGraph.Actors var options = Editor.Instance.Options.Options.General; if (options.AutoRebuildNavMesh) { - Navigation.BuildNavMesh(collider.Scene, collider.Box, options.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(collider.Box, options.AutoRebuildNavMeshTimeoutMs); } } } diff --git a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs index e95364c2d..4cd63f05d 100644 --- a/Source/Editor/SceneGraph/Actors/StaticModelNode.cs +++ b/Source/Editor/SceneGraph/Actors/StaticModelNode.cs @@ -47,6 +47,11 @@ namespace FlaxEditor.SceneGraph.Actors } } + /// + /// Gets the model used by this actor. + /// + public Model Model => ((StaticModel)Actor).Model; + /// public StaticModelNode(Actor actor) : base(actor) @@ -120,12 +125,12 @@ namespace FlaxEditor.SceneGraph.Actors { base.OnContextMenu(contextMenu, window); - // Check if every selected node is a primitive + // Check if every selected node is a primitive or has collision asset var selection = GetSelection(window); bool autoOptionEnabled = true; foreach (var node in selection) { - if (node is StaticModelNode staticModelNode && !staticModelNode.IsPrimitive) + if (node is StaticModelNode staticModelNode && (!staticModelNode.IsPrimitive && GetCollisionData(staticModelNode.Model) == null)) { autoOptionEnabled = false; break; @@ -201,6 +206,54 @@ namespace FlaxEditor.SceneGraph.Actors return Array.Empty(); } + private static bool TryCollisionData(Model model, BinaryAssetItem assetItem, out CollisionData collisionData) + { + collisionData = FlaxEngine.Content.Load(assetItem.ID); + if (collisionData) + { + var options = collisionData.Options; + if (options.Model == model.ID || options.Model == Guid.Empty) + return true; + } + return false; + } + + private CollisionData GetCollisionData(Model model) + { + if (model == null) + return null; + + // Check if there already is collision data for that model to reuse + var modelItem = (AssetItem)Editor.Instance.ContentDatabase.Find(model.ID); + if (modelItem?.ParentFolder != null) + { + foreach (var child in modelItem.ParentFolder.Children) + { + // Check if there is collision that was made with this model + if (child is BinaryAssetItem b && b.IsOfType()) + { + if (TryCollisionData(model, b, out var collisionData)) + return collisionData; + } + + // Check if there is an auto-imported collision + if (child is ContentFolder childFolder && childFolder.ShortName == modelItem.ShortName) + { + foreach (var childFolderChild in childFolder.Children) + { + if (childFolderChild is BinaryAssetItem c && c.IsOfType()) + { + if (TryCollisionData(model, c, out var collisionData)) + return collisionData; + } + } + } + } + } + + return null; + } + private void CreateAuto(StaticModel actor, Spawner spawner, bool singleNode) { // Special case for in-built Editor models that can use analytical collision @@ -243,6 +296,15 @@ namespace FlaxEditor.SceneGraph.Actors collider.LocalPosition = new Vector3(0, 50.0f, 0); collider.LocalOrientation = Quaternion.Euler(0, 0, 90.0f); } + else + { + var collider = new MeshCollider + { + Transform = actor.Transform, + CollisionData = GetCollisionData(model), + }; + spawner(collider); + } } private void CreateBox(StaticModel actor, Spawner spawner, bool singleNode) diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index e46038639..9ac43082e 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -304,25 +304,14 @@ namespace FlaxEditor.Surface.Archetypes } } - internal sealed class CustomCodeNode : SurfaceNode + internal sealed class CustomCodeNode : ResizableSurfaceNode { - private Rectangle _resizeButtonRect; - private Float2 _startResizingSize; - private Float2 _startResizingCornerOffset; - private bool _isResizing; private CustomCodeTextBox _textBox; - private int SizeValueIndex => Archetype.TypeID == 8 ? 1 : 3; // Index of the Size stored in Values array - - private Float2 SizeValue - { - get => (Float2)Values[SizeValueIndex]; - set => SetValue(SizeValueIndex, value, false); - } - public CustomCodeNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { + _sizeValueIndex = Archetype.TypeID == 8 ? 1 : 3; // Index of the Size stored in Values array Float2 pos = new Float2(FlaxEditor.Surface.Constants.NodeMarginX, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize), size; if (nodeArch.TypeID == 8) { @@ -345,126 +334,19 @@ namespace FlaxEditor.Surface.Archetypes _textBox.EditEnd += () => SetValue(0, _textBox.Text); } - public override bool CanSelect(ref Float2 location) - { - return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location); - } - public override void OnSurfaceLoaded(SurfaceNodeActions action) { base.OnSurfaceLoaded(action); _textBox.Text = (string)Values[0]; - - var size = SizeValue; - if (Surface != null && Surface.GridSnappingEnabled) - size = Surface.SnapToGrid(size, true); - Resize(size.X, size.Y); } public override void OnValuesChanged() { base.OnValuesChanged(); - var size = SizeValue; - Resize(size.X, size.Y); _textBox.Text = (string)Values[0]; } - - protected override void UpdateRectangles() - { - base.UpdateRectangles(); - - const float buttonMargin = FlaxEditor.Surface.Constants.NodeCloseButtonMargin; - const float buttonSize = FlaxEditor.Surface.Constants.NodeCloseButtonSize; - _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin - 4, buttonSize, buttonSize); - } - - public override void Draw() - { - base.Draw(); - - var style = Style.Current; - if (_isResizing) - { - Render2D.FillRectangle(_resizeButtonRect, style.Selection); - Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); - } - Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); - } - - public override void OnLostFocus() - { - if (_isResizing) - EndResizing(); - - base.OnLostFocus(); - } - - public override void OnEndMouseCapture() - { - if (_isResizing) - EndResizing(); - - base.OnEndMouseCapture(); - } - - public override bool OnMouseDown(Float2 location, MouseButton button) - { - if (base.OnMouseDown(location, button)) - return true; - - if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit) - { - // Start sliding - _isResizing = true; - _startResizingSize = Size; - _startResizingCornerOffset = Size - location; - StartMouseCapture(); - Cursor = CursorType.SizeNWSE; - return true; - } - - return false; - } - - public override void OnMouseMove(Float2 location) - { - if (_isResizing) - { - var emptySize = CalculateNodeSize(0, 0); - var size = Float2.Max(location - emptySize + _startResizingCornerOffset, new Float2(240, 160)); - Resize(size.X, size.Y); - } - else - { - base.OnMouseMove(location); - } - } - - public override bool OnMouseUp(Float2 location, MouseButton button) - { - if (button == MouseButton.Left && _isResizing) - { - EndResizing(); - return true; - } - - return base.OnMouseUp(location, button); - } - - private void EndResizing() - { - Cursor = CursorType.Default; - EndMouseCapture(); - _isResizing = false; - if (_startResizingSize != Size) - { - var emptySize = CalculateNodeSize(0, 0); - SizeValue = Size - emptySize; - Surface.MarkAsEdited(false); - } - } } internal enum MaterialTemplateInputsMapping diff --git a/Source/Editor/Surface/Archetypes/Parameters.cs b/Source/Editor/Surface/Archetypes/Parameters.cs index 3d74d2e10..3383e7662 100644 --- a/Source/Editor/Surface/Archetypes/Parameters.cs +++ b/Source/Editor/Surface/Archetypes/Parameters.cs @@ -1100,6 +1100,27 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.ComboBox(2 + 20, 0, 116) } }, + new NodeArchetype + { + TypeID = 5, + Create = (id, context, arch, groupArch) => new SurfaceNodeParamsSet(id, context, arch, groupArch), + Title = "Set Parameter", + Description = "Parameter value setter invoked when the animation pose is evaluated (output pose comes from input)", + Flags = NodeFlags.AnimGraph, + Size = new Float2(140, 40), + DefaultValues = new object[] + { + Guid.Empty, + null + }, + Elements = new[] + { + NodeElementArchetype.Factory.Output(0, string.Empty, typeof(void), 0), + NodeElementArchetype.Factory.Input(0, string.Empty, true, typeof(void), 2), + NodeElementArchetype.Factory.Input(1, string.Empty, true, ScriptType.Null, 1, 1), + NodeElementArchetype.Factory.ComboBox(2 + 20, 0, 116) + } + }, }; } } diff --git a/Source/Editor/Surface/Archetypes/Textures.cs b/Source/Editor/Surface/Archetypes/Textures.cs index 56f8154d7..f09ee5015 100644 --- a/Source/Editor/Surface/Archetypes/Textures.cs +++ b/Source/Editor/Surface/Archetypes/Textures.cs @@ -23,11 +23,14 @@ namespace FlaxEditor.Surface.Archetypes TextureGroup = 4, } - internal class SampleTextureNode : SurfaceNode + internal class TextureSamplerNode : SurfaceNode { private ComboBox _textureGroupPicker; + protected int _samplerTypeValueIndex = -1; + protected int _textureGroupValueIndex = -1; + protected int _level = 5; - public SampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + protected TextureSamplerNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { } @@ -48,13 +51,13 @@ namespace FlaxEditor.Surface.Archetypes private void UpdateUI() { - if ((int)Values[0] == (int)CommonSamplerType.TextureGroup) + if ((int)Values[_samplerTypeValueIndex] == (int)CommonSamplerType.TextureGroup) { if (_textureGroupPicker == null) { _textureGroupPicker = new ComboBox { - Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.LayoutOffsetY * 5), + Location = new Float2(FlaxEditor.Surface.Constants.NodeMarginX + 50, FlaxEditor.Surface.Constants.NodeMarginY + FlaxEditor.Surface.Constants.NodeHeaderSize + FlaxEditor.Surface.Constants.LayoutOffsetY * _level), Width = 100, Parent = this, }; @@ -71,7 +74,7 @@ namespace FlaxEditor.Surface.Archetypes _textureGroupPicker.Visible = true; } _textureGroupPicker.SelectedIndexChanged -= OnSelectedTextureGroupChanged; - _textureGroupPicker.SelectedIndex = (int)Values[2]; + _textureGroupPicker.SelectedIndex = (int)Values[_textureGroupValueIndex]; _textureGroupPicker.SelectedIndexChanged += OnSelectedTextureGroupChanged; } else if (_textureGroupPicker != null) @@ -83,7 +86,39 @@ namespace FlaxEditor.Surface.Archetypes private void OnSelectedTextureGroupChanged(ComboBox comboBox) { - SetValue(2, _textureGroupPicker.SelectedIndex); + SetValue(_textureGroupValueIndex, _textureGroupPicker.SelectedIndex); + } + } + + internal class SampleTextureNode : TextureSamplerNode + { + public SampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + : base(id, context, nodeArch, groupArch) + { + _samplerTypeValueIndex = 0; + _textureGroupValueIndex = 2; + } + } + + internal class TriplanarSampleTextureNode : TextureSamplerNode + { + public TriplanarSampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + : base(id, context, nodeArch, groupArch) + { + _samplerTypeValueIndex = 3; + _textureGroupValueIndex = 5; + _level = 5; + } + } + + internal class ProceduralSampleTextureNode : TextureSamplerNode + { + public ProceduralSampleTextureNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + : base(id, context, nodeArch, groupArch) + { + _samplerTypeValueIndex = 0; + _textureGroupValueIndex = 2; + _level = 4; } } @@ -280,9 +315,9 @@ namespace FlaxEditor.Surface.Archetypes ConnectionsHints = ConnectionsHint.Vector, DefaultValues = new object[] { - 0, - -1.0f, - 0, + (int)CommonSamplerType.LinearClamp, // Sampler + -1.0f, // Level + 0, // Texture Group }, Elements = new[] { @@ -402,6 +437,7 @@ namespace FlaxEditor.Surface.Archetypes new NodeArchetype { TypeID = 16, + Create = (id, context, arch, groupArch) => new TriplanarSampleTextureNode(id, context, arch, groupArch), Title = "Triplanar Texture", Description = "Projects a texture using world-space coordinates with triplanar mapping.", Flags = NodeFlags.MaterialGraph, @@ -411,8 +447,9 @@ namespace FlaxEditor.Surface.Archetypes Float3.One, // Scale 1.0f, // Blend Float2.Zero, // Offset - 2, // Sampler + (int)CommonSamplerType.LinearWrap, // Sampler false, // Local + 0, // Texture Group }, Elements = new[] { @@ -430,17 +467,17 @@ namespace FlaxEditor.Surface.Archetypes new NodeArchetype { TypeID = 17, - Create = (id, context, arch, groupArch) => new SampleTextureNode(id, context, arch, groupArch), + Create = (id, context, arch, groupArch) => new ProceduralSampleTextureNode(id, context, arch, groupArch), Title = "Procedural Sample Texture", Description = "Samples a texture to create a more natural look with less obvious tiling.", Flags = NodeFlags.MaterialGraph, - Size = new Float2(240, 110), + Size = new Float2(240, 130), ConnectionsHints = ConnectionsHint.Vector, DefaultValues = new object[] { - 2, - -1.0f, - 0, + (int)CommonSamplerType.LinearWrap, // Sampler + -1.0f, // Level + 0, // Texture Group }, Elements = new[] { @@ -448,8 +485,8 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Input(1, "UVs", true, null, 1), NodeElementArchetype.Factory.Input(2, "Offset", true, typeof(Float2), 3), NodeElementArchetype.Factory.Output(0, "Color", typeof(Float4), 4), - NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 4, "Sampler"), - NodeElementArchetype.Factory.ComboBox(50, Surface.Constants.LayoutOffsetY * 4, 100, 0, typeof(CommonSamplerType)) + NodeElementArchetype.Factory.Text(0, Surface.Constants.LayoutOffsetY * 3, "Sampler"), + NodeElementArchetype.Factory.ComboBox(50, Surface.Constants.LayoutOffsetY * 3, 100, 0, typeof(CommonSamplerType)) } }, new NodeArchetype @@ -469,6 +506,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 23, Title = "Triplanar Normal Map", + Create = (id, context, arch, groupArch) => new TriplanarSampleTextureNode(id, context, arch, groupArch), Description = "Projects a normal map texture using world-space coordinates with triplanar mapping.", Flags = NodeFlags.MaterialGraph, Size = new Float2(280, 100), @@ -477,8 +515,9 @@ namespace FlaxEditor.Surface.Archetypes Float3.One, // Scale 1.0f, // Blend Float2.Zero, // Offset - 2, // Sampler + (int)CommonSamplerType.LinearWrap, // Sampler false, // Local + 0, // Texture Group }, Elements = new[] { diff --git a/Source/Editor/Surface/Archetypes/Tools.cs b/Source/Editor/Surface/Archetypes/Tools.cs index aacebd189..68a733197 100644 --- a/Source/Editor/Surface/Archetypes/Tools.cs +++ b/Source/Editor/Surface/Archetypes/Tools.cs @@ -453,7 +453,7 @@ namespace FlaxEditor.Surface.Archetypes } } - private class CurveNode : SurfaceNode where T : struct + private class CurveNode : ResizableSurfaceNode where T : struct { private BezierCurveEditor _curve; private bool _isSavingCurve; @@ -467,7 +467,7 @@ namespace FlaxEditor.Surface.Archetypes Create = (id, context, arch, groupArch) => new CurveNode(id, context, arch, groupArch), Description = "An animation spline represented by a set of keyframes, each representing an endpoint of a Bezier curve.", Flags = NodeFlags.AllGraphs, - Size = new Float2(400, 180.0f), + Size = new Float2(400, 180), DefaultValues = new object[] { // Keyframes count @@ -491,6 +491,8 @@ namespace FlaxEditor.Surface.Archetypes 0.0f, zero, zero, zero, 0.0f, zero, zero, zero, 0.0f, zero, zero, zero, + + new Float2(400, 180), }, Elements = new[] { @@ -504,30 +506,52 @@ namespace FlaxEditor.Surface.Archetypes public CurveNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { + _sizeValueIndex = 29; // Index of the Size stored in Values array } - + /// public override void OnLoaded(SurfaceNodeActions action) { base.OnLoaded(action); + // Create curve editor var upperLeft = GetBox(0).BottomLeft; var upperRight = GetBox(1).BottomRight; float curveMargin = 20.0f; - _curve = new BezierCurveEditor { MaxKeyframes = 7, Bounds = new Rectangle(upperLeft + new Float2(curveMargin, 10.0f), upperRight.X - upperLeft.X - curveMargin * 2.0f, 140.0f), - Parent = this + Parent = this, + AnchorMax = Float2.One, }; _curve.Edited += OnCurveEdited; _curve.UnlockChildrenRecursive(); _curve.PerformLayout(); + // Sync keyframes UpdateCurveKeyframes(); } + /// + public override void OnSurfaceLoaded(SurfaceNodeActions action) + { + base.OnSurfaceLoaded(action); + + // Ensure the whole curve is shown + _curve.ShowWholeCurve(); + } + + public override void OnValuesChanged() + { + base.OnValuesChanged(); + + if (!_isSavingCurve) + { + UpdateCurveKeyframes(); + } + } + private void OnCurveEdited() { if (_isSavingCurve) @@ -553,17 +577,6 @@ namespace FlaxEditor.Surface.Archetypes _isSavingCurve = false; } - /// - public override void OnValuesChanged() - { - base.OnValuesChanged(); - - if (!_isSavingCurve) - { - UpdateCurveKeyframes(); - } - } - private void UpdateCurveKeyframes() { var count = (int)Values[0]; @@ -1575,7 +1588,7 @@ namespace FlaxEditor.Surface.Archetypes DefaultValues = new object[] { Guid.Empty, - string.Empty + string.Empty, }, Elements = new[] { diff --git a/Source/Editor/Surface/ResizableSurfaceNode.cs b/Source/Editor/Surface/ResizableSurfaceNode.cs new file mode 100644 index 000000000..259c29836 --- /dev/null +++ b/Source/Editor/Surface/ResizableSurfaceNode.cs @@ -0,0 +1,182 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.Surface +{ + /// + /// Visject Surface node control that cna be resized. + /// + /// + [HideInEditor] + public class ResizableSurfaceNode : SurfaceNode + { + private Float2 _startResizingSize; + private Float2 _startResizingCornerOffset; + + /// + /// Indicates whether the node is currently being resized. + /// + protected bool _isResizing; + + /// + /// Index of the Float2 value in the node values list to store node size. + /// + protected int _sizeValueIndex = -1; + + /// + /// Minimum node size. + /// + protected Float2 _sizeMin = new Float2(240, 160); + + /// + /// Node resizing rectangle bounds. + /// + protected Rectangle _resizeButtonRect; + + private Float2 SizeValue + { + get => (Float2)Values[_sizeValueIndex]; + set => SetValue(_sizeValueIndex, value, false); + } + + /// + public ResizableSurfaceNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + : base(id, context, nodeArch, groupArch) + { + } + + /// + public override bool CanSelect(ref Float2 location) + { + return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location); + } + + /// + public override void OnSurfaceLoaded(SurfaceNodeActions action) + { + // Reapply the curve node size + var size = SizeValue; + if (Surface != null && Surface.GridSnappingEnabled) + size = Surface.SnapToGrid(size, true); + Resize(size.X, size.Y); + + base.OnSurfaceLoaded(action); + } + + /// + public override void OnValuesChanged() + { + base.OnValuesChanged(); + + var size = SizeValue; + Resize(size.X, size.Y); + } + + /// + public override void Draw() + { + base.Draw(); + + if (Surface.CanEdit) + { + var style = Style.Current; + if (_isResizing) + { + Render2D.FillRectangle(_resizeButtonRect, style.Selection); + Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); + } + Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); + } + } + + /// + public override void OnLostFocus() + { + if (_isResizing) + EndResizing(); + + base.OnLostFocus(); + } + + /// + public override void OnEndMouseCapture() + { + if (_isResizing) + EndResizing(); + + base.OnEndMouseCapture(); + } + + /// + public override bool OnMouseDown(Float2 location, MouseButton button) + { + if (base.OnMouseDown(location, button)) + return true; + + if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit) + { + // Start resizing + _isResizing = true; + _startResizingSize = Size; + _startResizingCornerOffset = Size - location; + StartMouseCapture(); + Cursor = CursorType.SizeNWSE; + return true; + } + + return false; + } + + /// + public override void OnMouseMove(Float2 location) + { + if (_isResizing) + { + var emptySize = CalculateNodeSize(0, 0); + var size = Float2.Max(location - emptySize + _startResizingCornerOffset, _sizeMin); + Resize(size.X, size.Y); + } + else + { + base.OnMouseMove(location); + } + } + + /// + public override bool OnMouseUp(Float2 location, MouseButton button) + { + if (button == MouseButton.Left && _isResizing) + { + EndResizing(); + return true; + } + + return base.OnMouseUp(location, button); + } + + /// + protected override void UpdateRectangles() + { + base.UpdateRectangles(); + + const float buttonMargin = Constants.NodeCloseButtonMargin; + const float buttonSize = Constants.NodeCloseButtonSize; + _resizeButtonRect = new Rectangle(_closeButtonRect.Left, Height - buttonSize - buttonMargin - 4, buttonSize, buttonSize); + } + + private void EndResizing() + { + Cursor = CursorType.Default; + EndMouseCapture(); + _isResizing = false; + if (_startResizingSize != Size) + { + var emptySize = CalculateNodeSize(0, 0); + SizeValue = Size - emptySize; + Surface.MarkAsEdited(false); + } + } + } +} diff --git a/Source/Editor/Surface/SurfaceComment.cs b/Source/Editor/Surface/SurfaceComment.cs index 10e9fc776..a76fa245d 100644 --- a/Source/Editor/Surface/SurfaceComment.cs +++ b/Source/Editor/Surface/SurfaceComment.cs @@ -14,18 +14,11 @@ namespace FlaxEditor.Surface /// /// [HideInEditor] - public class SurfaceComment : SurfaceNode + public class SurfaceComment : ResizableSurfaceNode { private Rectangle _colorButtonRect; - private Rectangle _resizeButtonRect; - private Float2 _startResizingSize; private readonly TextBox _renameTextBox; - /// - /// True if sizing tool is in use. - /// - protected bool _isResizing; - /// /// True if rename textbox is active in order to rename comment /// @@ -52,12 +45,6 @@ namespace FlaxEditor.Surface set => SetValue(1, value, false); } - private Float2 SizeValue - { - get => (Float2)Values[2]; - set => SetValue(2, value, false); - } - private int OrderValue { get => (int)Values[3]; @@ -68,6 +55,8 @@ namespace FlaxEditor.Surface public SurfaceComment(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { + _sizeValueIndex = 2; // Index of the Size stored in Values array + _sizeMin = new Float2(140.0f, Constants.NodeHeaderSize); _renameTextBox = new TextBox(false, 0, 0, Width) { Height = Constants.NodeHeaderSize, @@ -86,10 +75,6 @@ namespace FlaxEditor.Surface // Read node data Title = TitleValue; Color = ColorValue; - var size = SizeValue; - if (Surface != null && Surface.GridSnappingEnabled) - size = Surface.SnapToGrid(size, true); - Size = size; // Order // Backwards compatibility - When opening with an older version send the old comments to the back @@ -126,27 +111,6 @@ namespace FlaxEditor.Surface // Read node data Title = TitleValue; Color = ColorValue; - Size = SizeValue; - } - - private void EndResizing() - { - // Clear state - _isResizing = false; - - if (_startResizingSize != Size) - { - SizeValue = Size; - Surface.MarkAsEdited(false); - } - - EndMouseCapture(); - } - - /// - public override bool CanSelect(ref Float2 location) - { - return _headerRect.MakeOffsetted(Location).Contains(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location); } /// @@ -158,6 +122,8 @@ namespace FlaxEditor.Surface /// protected override void UpdateRectangles() { + base.UpdateRectangles(); + const float headerSize = Constants.NodeHeaderSize; const float buttonMargin = Constants.NodeCloseButtonMargin; const float buttonSize = Constants.NodeCloseButtonSize; @@ -222,16 +188,13 @@ namespace FlaxEditor.Surface // Color button Render2D.DrawSprite(style.Settings, _colorButtonRect, _colorButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); - // Check if is resizing + // Resize button if (_isResizing) { - // Draw overlay Render2D.FillRectangle(_resizeButtonRect, style.Selection); Render2D.DrawRectangle(_resizeButtonRect, style.SelectionBorder); } - - // Resize button - Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) && Surface.CanEdit ? style.Foreground : style.ForegroundGrey); + Render2D.DrawSprite(style.Scale, _resizeButtonRect, _resizeButtonRect.Contains(_mousePosition) ? style.Foreground : style.ForegroundGrey); } // Selection outline @@ -247,88 +210,28 @@ namespace FlaxEditor.Surface /// protected override Float2 CalculateNodeSize(float width, float height) { - return Size; + // No margins or headers + return new Float2(width, height); } /// public override void OnLostFocus() { - // Check if was resizing - if (_isResizing) - { - EndResizing(); - } - - // Check if was renaming if (_isRenaming) { Rename(_renameTextBox.Text); StopRenaming(); } - // Base base.OnLostFocus(); } - /// - public override void OnEndMouseCapture() - { - // Check if was resizing - if (_isResizing) - { - EndResizing(); - } - else - { - base.OnEndMouseCapture(); - } - } - /// public override bool ContainsPoint(ref Float2 location, bool precise) { return _headerRect.Contains(ref location) || _resizeButtonRect.Contains(ref location); } - /// - public override bool OnMouseDown(Float2 location, MouseButton button) - { - if (base.OnMouseDown(location, button)) - return true; - - // Check if can start resizing - if (button == MouseButton.Left && _resizeButtonRect.Contains(ref location) && Surface.CanEdit) - { - // Start sliding - _isResizing = true; - _startResizingSize = Size; - StartMouseCapture(); - - return true; - } - - return false; - } - - /// - public override void OnMouseMove(Float2 location) - { - // Check if is resizing - if (_isResizing) - { - // Update size - var size = Float2.Max(location, new Float2(140.0f, _headerRect.Bottom)); - if (Surface.GridSnappingEnabled) - size = Surface.SnapToGrid(size, true); - Size = size; - } - else - { - // Base - base.OnMouseMove(location); - } - } - /// public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { @@ -394,12 +297,6 @@ namespace FlaxEditor.Surface /// public override bool OnMouseUp(Float2 location, MouseButton button) { - if (button == MouseButton.Left && _isResizing) - { - EndResizing(); - return true; - } - if (base.OnMouseUp(location, button)) return true; diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index cbc041c09..056987e52 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -724,7 +724,12 @@ namespace FlaxEditor.Surface if (HasNodesSelection) { - var keyMoveRange = 50; + var keyMoveDelta = 50; + bool altDown = RootWindow.GetKey(KeyboardKeys.Alt); + bool shiftDown = RootWindow.GetKey(KeyboardKeys.Shift); + if (altDown || shiftDown) + keyMoveDelta = shiftDown ? 10 : 25; + switch (key) { case KeyboardKeys.Backspace: @@ -752,17 +757,18 @@ namespace FlaxEditor.Surface Box selectedBox = GetSelectedBox(SelectedNodes); if (selectedBox != null) { - Box toSelect = (key == KeyboardKeys.ArrowUp) ? selectedBox?.ParentNode.GetPreviousBox(selectedBox) : selectedBox?.ParentNode.GetNextBox(selectedBox); - if (toSelect != null && toSelect.IsOutput == selectedBox.IsOutput) - { - Select(toSelect.ParentNode); - toSelect.ParentNode.SelectBox(toSelect); - } + int delta = key == KeyboardKeys.ArrowDown ? 1 : -1; + List boxes = selectedBox.ParentNode.GetBoxes().FindAll(b => b.IsOutput == selectedBox.IsOutput); + int selectedIndex = boxes.IndexOf(selectedBox); + Box toSelect = boxes[Mathf.Wrap(selectedIndex + delta, 0, boxes.Count - 1)]; + + Select(toSelect.ParentNode); + toSelect.ParentNode.SelectBox(toSelect); } else if (!IsMovingSelection && CanEdit) { // Move selected nodes - var delta = new Float2(0, key == KeyboardKeys.ArrowUp ? -keyMoveRange : keyMoveRange); + var delta = new Float2(0, key == KeyboardKeys.ArrowUp ? -keyMoveDelta : keyMoveDelta); MoveSelectedNodes(delta); } return true; @@ -775,12 +781,8 @@ namespace FlaxEditor.Surface if (selectedBox != null) { Box toSelect = null; - if ((key == KeyboardKeys.ArrowRight && selectedBox.IsOutput) || (key == KeyboardKeys.ArrowLeft && !selectedBox.IsOutput)) + if (((key == KeyboardKeys.ArrowRight && selectedBox.IsOutput) || (key == KeyboardKeys.ArrowLeft && !selectedBox.IsOutput)) && selectedBox.HasAnyConnection) { - if (_selectedConnectionIndex < 0 || _selectedConnectionIndex >= selectedBox.Connections.Count) - { - _selectedConnectionIndex = 0; - } toSelect = selectedBox.Connections[_selectedConnectionIndex]; } else @@ -808,7 +810,7 @@ namespace FlaxEditor.Surface else if (!IsMovingSelection && CanEdit) { // Move selected nodes - var delta = new Float2(key == KeyboardKeys.ArrowLeft ? -keyMoveRange : keyMoveRange, 0); + var delta = new Float2(key == KeyboardKeys.ArrowLeft ? -keyMoveDelta : keyMoveDelta, 0); MoveSelectedNodes(delta); } return true; @@ -824,13 +826,9 @@ namespace FlaxEditor.Surface return true; if (Root.GetKey(KeyboardKeys.Shift)) - { _selectedConnectionIndex = ((_selectedConnectionIndex - 1) % connectionCount + connectionCount) % connectionCount; - } else - { _selectedConnectionIndex = (_selectedConnectionIndex + 1) % connectionCount; - } return true; } } diff --git a/Source/Editor/Tools/Terrain/EditTab.cs b/Source/Editor/Tools/Terrain/EditTab.cs index 551a47974..6a6191122 100644 --- a/Source/Editor/Tools/Terrain/EditTab.cs +++ b/Source/Editor/Tools/Terrain/EditTab.cs @@ -192,7 +192,7 @@ namespace FlaxEditor.Tools.Terrain { if (terrain.Scene && terrain.HasStaticFlag(StaticFlags.Navigation)) { - Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs); } } } diff --git a/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs b/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs index 54a6d7fa4..5fc0e894f 100644 --- a/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs +++ b/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs @@ -209,7 +209,7 @@ namespace FlaxEditor.Tools.Terrain { if (terrain.Scene && terrain.HasStaticFlag(StaticFlags.Navigation)) { - Navigation.BuildNavMesh(terrain.Scene, patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(patchBounds, editorOptions.General.AutoRebuildNavMeshTimeoutMs); } } } diff --git a/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs b/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs index 87b0c3cc9..afac0948e 100644 --- a/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs +++ b/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs @@ -172,7 +172,7 @@ namespace FlaxEditor.Tools.Terrain.Undo if (_navmeshBoundsModifications != null) { foreach (var bounds in _navmeshBoundsModifications) - Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs); + Navigation.BuildNavMesh(bounds, _dirtyNavMeshTimeoutMs); } Editor.Instance.Scene.MarkSceneEdited(scene); @@ -217,11 +217,10 @@ namespace FlaxEditor.Tools.Terrain.Undo } // Update navmesh - var scene = Terrain.Scene; if (_navmeshBoundsModifications != null) { foreach (var bounds in _navmeshBoundsModifications) - Navigation.BuildNavMesh(scene, bounds, _dirtyNavMeshTimeoutMs); + Navigation.BuildNavMesh(bounds, _dirtyNavMeshTimeoutMs); } Editor.Instance.Scene.MarkSceneEdited(Terrain.Scene); diff --git a/Source/Editor/Undo/Actions/DeleteActorsAction.cs b/Source/Editor/Undo/Actions/DeleteActorsAction.cs index 75594ecb9..19ffb1e3f 100644 --- a/Source/Editor/Undo/Actions/DeleteActorsAction.cs +++ b/Source/Editor/Undo/Actions/DeleteActorsAction.cs @@ -303,7 +303,7 @@ namespace FlaxEditor.Actions if (_nodeParents[i] is ActorNode node && node.Actor && node.Actor.Scene && node.AffectsNavigationWithChildren) { var bounds = node.Actor.BoxWithChildren; - Navigation.BuildNavMesh(node.Actor.Scene, bounds, options.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(bounds, options.General.AutoRebuildNavMeshTimeoutMs); } } } diff --git a/Source/Editor/Undo/Actions/ParentActorsAction.cs b/Source/Editor/Undo/Actions/ParentActorsAction.cs index 312ce578d..aed85cb3c 100644 --- a/Source/Editor/Undo/Actions/ParentActorsAction.cs +++ b/Source/Editor/Undo/Actions/ParentActorsAction.cs @@ -134,7 +134,8 @@ namespace FlaxEditor.Actions var obj = Object.Find(ref item.ID); if (obj != null) { - scenes.Add(obj.Parent.Scene); + if (obj.Parent != null) + scenes.Add(obj.Parent.Scene); if (obj is Actor actor) actor.SetParent(newParent, _worldPositionsStays, true); else diff --git a/Source/Editor/Undo/Actions/TransformObjectsAction.cs b/Source/Editor/Undo/Actions/TransformObjectsAction.cs index ebed61174..df013e20e 100644 --- a/Source/Editor/Undo/Actions/TransformObjectsAction.cs +++ b/Source/Editor/Undo/Actions/TransformObjectsAction.cs @@ -121,12 +121,12 @@ namespace FlaxEditor // Handle simple case where objects were moved just a little and use one navmesh build request to improve performance if (data.BeforeBounds.Intersects(ref data.AfterBounds)) { - Navigation.BuildNavMesh(data.Scene, BoundingBox.Merge(data.BeforeBounds, data.AfterBounds), options.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(BoundingBox.Merge(data.BeforeBounds, data.AfterBounds), options.General.AutoRebuildNavMeshTimeoutMs); } else { - Navigation.BuildNavMesh(data.Scene, data.BeforeBounds, options.General.AutoRebuildNavMeshTimeoutMs); - Navigation.BuildNavMesh(data.Scene, data.AfterBounds, options.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(data.BeforeBounds, options.General.AutoRebuildNavMeshTimeoutMs); + Navigation.BuildNavMesh(data.AfterBounds, options.General.AutoRebuildNavMeshTimeoutMs); } } } diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 2af065c68..1e7d2295a 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -584,7 +584,7 @@ namespace FlaxEditor.Viewport _cameraButton = new ViewportWidgetButton(string.Format(MovementSpeedTextFormat, _movementSpeed), _editor.Icons.Camera64, cameraCM, false, cameraSpeedTextWidth) { Tag = this, - TooltipText = "Camera Settings", + TooltipText = "Camera Settings.", Parent = _cameraWidget }; _cameraWidget.Parent = this; @@ -593,7 +593,7 @@ namespace FlaxEditor.Viewport _orthographicModeButton = new ViewportWidgetButton(string.Empty, _editor.Icons.CamSpeed32, null, true) { Checked = !_isOrtho, - TooltipText = "Toggle Orthographic/Perspective Mode", + TooltipText = "Toggle Orthographic/Perspective Mode.", Parent = _cameraWidget }; _orthographicModeButton.Toggled += OnOrthographicModeToggled; @@ -832,7 +832,7 @@ namespace FlaxEditor.Viewport ViewWidgetButtonMenu = new ContextMenu(); var viewModeButton = new ViewportWidgetButton("View", SpriteHandle.Invalid, ViewWidgetButtonMenu) { - TooltipText = "View properties", + TooltipText = "View properties.", Parent = viewMode }; viewMode.Parent = this; @@ -863,8 +863,10 @@ namespace FlaxEditor.Viewport { } }); - viewLayers.AddButton("Reset layers", () => Task.ViewLayersMask = LayersMask.Default).Icon = _editor.Icons.Rotate32; - viewLayers.AddButton("Disable layers", () => Task.ViewLayersMask = new LayersMask(0)); + viewLayers.AddButton("Reset layers", () => Task.ViewLayersMask = LayersMask.Default).Icon = Editor.Instance.Icons.Rotate32; + viewLayers.AddSeparator(); + viewLayers.AddButton("Enable all", () => Task.ViewLayersMask = new LayersMask(-1)).Icon = Editor.Instance.Icons.CheckBoxTick12; + viewLayers.AddButton("Disable all", () => Task.ViewLayersMask = new LayersMask(0)).Icon = Editor.Instance.Icons.Cross12; viewLayers.AddSeparator(); var layers = LayersAndTagsSettings.GetCurrentLayers(); if (layers != null && layers.Length > 0) @@ -904,8 +906,10 @@ namespace FlaxEditor.Viewport { } }); - viewFlags.AddButton("Reset flags", () => Task.ViewFlags = ViewFlags.DefaultEditor).Icon = _editor.Icons.Rotate32; - viewFlags.AddButton("Disable flags", () => Task.ViewFlags = ViewFlags.None); + viewFlags.AddButton("Reset flags", () => Task.ViewFlags = ViewFlags.DefaultEditor).Icon = Editor.Instance.Icons.Rotate32; + viewFlags.AddSeparator(); + viewFlags.AddButton("Enable all", () => Task.ViewFlags = ViewFlags.All).Icon = Editor.Instance.Icons.CheckBoxTick12; + viewFlags.AddButton("Disable all", () => Task.ViewFlags = ViewFlags.None).Icon = Editor.Instance.Icons.Cross12; viewFlags.AddSeparator(); for (int i = 0; i < ViewFlagsValues.Length; i++) { diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index 5343a1fe5..3d366809c 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -25,6 +25,7 @@ namespace FlaxEditor.Viewport private readonly Editor _editor; private readonly ContextMenuButton _showGridButton; private readonly ContextMenuButton _showNavigationButton; + private readonly ContextMenuButton _toggleGameViewButton; private SelectionOutline _customSelectionOutline; /// @@ -108,6 +109,13 @@ namespace FlaxEditor.Viewport private EditorSpritesRenderer _editorSpritesRenderer; private ViewportRubberBandSelector _rubberBandSelector; + private bool _gameViewActive; + private ViewFlags _preGameViewFlags; + private ViewMode _preGameViewViewMode; + private bool _gameViewWasGridShown; + private bool _gameViewWasFpsCounterShown; + private bool _gameViewWasNagivationShown; + /// /// Drag and drop handlers /// @@ -185,6 +193,7 @@ namespace FlaxEditor.Viewport : base(Object.New(), editor.Undo, editor.Scene.Root) { _editor = editor; + var inputOptions = _editor.Options.Options.Input; DragHandlers = new ViewportDragHandlers(this, this, ValidateDragItem, ValidateDragActorType, ValidateDragScriptItem); // Prepare rendering task @@ -232,9 +241,14 @@ namespace FlaxEditor.Viewport _showGridButton.CloseMenuOnClick = false; // Show navigation widget - _showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", () => ShowNavigation = !ShowNavigation); + _showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", inputOptions.ToggleNavMeshVisibility, () => ShowNavigation = !ShowNavigation); _showNavigationButton.CloseMenuOnClick = false; + // Game View + ViewWidgetButtonMenu.AddSeparator(); + _toggleGameViewButton = ViewWidgetButtonMenu.AddButton("Game View", inputOptions.ToggleGameView, ToggleGameView); + _toggleGameViewButton.CloseMenuOnClick = false; + // Create camera widget ViewWidgetButtonMenu.AddSeparator(); ViewWidgetButtonMenu.AddButton("Create camera here", CreateCameraAtView); @@ -259,6 +273,10 @@ namespace FlaxEditor.Viewport InputActions.Add(options => options.FocusSelection, FocusSelection); InputActions.Add(options => options.RotateSelection, RotateSelection); InputActions.Add(options => options.Delete, _editor.SceneEditing.Delete); + InputActions.Add(options => options.ToggleNavMeshVisibility, () => ShowNavigation = !ShowNavigation); + + // Game View + InputActions.Add(options => options.ToggleGameView, ToggleGameView); } /// @@ -373,9 +391,12 @@ namespace FlaxEditor.Viewport public void DrawEditorPrimitives(GPUContext context, ref RenderContext renderContext, GPUTexture target, GPUTexture targetDepth) { // Draw gizmos - for (int i = 0; i < Gizmos.Count; i++) + foreach (var gizmo in Gizmos) { - Gizmos[i].Draw(ref renderContext); + if (gizmo.Visible) + { + gizmo.Draw(ref renderContext); + } } // Draw selected objects debug shapes and visuals @@ -481,6 +502,36 @@ namespace FlaxEditor.Viewport TransformGizmo.EndTransforming(); } + /// + /// Toggles game view view mode on or off. + /// + public void ToggleGameView() + { + if (!_gameViewActive) + { + // Cache flags & values + _preGameViewFlags = Task.ViewFlags; + _preGameViewViewMode = Task.ViewMode; + _gameViewWasGridShown = Grid.Enabled; + _gameViewWasFpsCounterShown = ShowFpsCounter; + _gameViewWasNagivationShown = ShowNavigation; + } + + // Set flags & values + Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.DefaultGame; + Task.ViewMode = _gameViewActive ? _preGameViewViewMode : ViewMode.Default; + ShowFpsCounter = _gameViewActive ? _gameViewWasFpsCounterShown : false; + ShowNavigation = _gameViewActive ? _gameViewWasNagivationShown : false; + Grid.Enabled = _gameViewActive ? _gameViewWasGridShown : false; + + _gameViewActive = !_gameViewActive; + + TransformGizmo.Visible = !_gameViewActive; + SelectionOutline.ShowSelectionOutline = !_gameViewActive; + + _toggleGameViewButton.Icon = _gameViewActive ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; + } + /// public override void OnLostFocus() { @@ -620,7 +671,7 @@ namespace FlaxEditor.Viewport { base.OnLeftMouseButtonDown(); - _rubberBandSelector.TryStartingRubberBandSelection(); + _rubberBandSelector.TryStartingRubberBandSelection(_viewMousePos); } /// diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index a22b4042f..7f2b1a471 100644 --- a/Source/Editor/Viewport/PrefabWindowViewport.cs +++ b/Source/Editor/Viewport/PrefabWindowViewport.cs @@ -720,9 +720,12 @@ namespace FlaxEditor.Viewport public override void DrawEditorPrimitives(GPUContext context, ref RenderContext renderContext, GPUTexture target, GPUTexture targetDepth) { // Draw gizmos - for (int i = 0; i < Gizmos.Count; i++) + foreach (var gizmo in Gizmos) { - Gizmos[i].Draw(ref renderContext); + if (gizmo.Visible) + { + gizmo.Draw(ref renderContext); + } } base.DrawEditorPrimitives(context, ref renderContext, target, targetDepth); diff --git a/Source/Editor/Windows/Assets/AnimationWindow.cs b/Source/Editor/Windows/Assets/AnimationWindow.cs index 41fdfde65..268e18503 100644 --- a/Source/Editor/Windows/Assets/AnimationWindow.cs +++ b/Source/Editor/Windows/Assets/AnimationWindow.cs @@ -2,7 +2,6 @@ using System; using System.Globalization; -using System.IO; using System.Reflection; using System.Xml; using FlaxEditor.Content; @@ -25,7 +24,7 @@ namespace FlaxEditor.Windows.Assets /// /// /// - public sealed class AnimationWindow : AssetEditorWindowBase + public sealed class AnimationWindow : ClonedAssetEditorWindowBase { private sealed class Preview : AnimationPreview { @@ -255,6 +254,7 @@ namespace FlaxEditor.Windows.Assets private bool _isWaitingForTimelineLoad; private SkinnedModel _initialPreviewModel, _initialBaseModel; private float _initialPanel2Splitter = 0.6f; + private bool _timelineIsDirty; /// /// Gets the animation timeline editor. @@ -295,7 +295,7 @@ namespace FlaxEditor.Windows.Assets Parent = _panel1.Panel1, Enabled = false }; - _timeline.Modified += MarkAsEdited; + _timeline.Modified += OnTimelineModified; _timeline.SetNoTracksText("Loading..."); // Asset properties @@ -321,11 +321,31 @@ namespace FlaxEditor.Windows.Assets { MarkAsEdited(); UpdateToolstrip(); + _propertiesPresenter.BuildLayout(); + } + + private void OnTimelineModified() + { + _timelineIsDirty = true; + MarkAsEdited(); + } + + private bool RefreshTempAsset() + { + if (_asset == null || _isWaitingForTimelineLoad) + return true; + if (_timeline.IsModified) + { + _timeline.Save(_asset); + } + _propertiesPresenter.BuildLayoutOnUpdate(); + + return false; } private string GetPreviewModelCacheName() { - return _asset.ID + ".PreviewModel"; + return _item.ID + ".PreviewModel"; } /// @@ -361,7 +381,11 @@ namespace FlaxEditor.Windows.Assets if (!IsEdited) return; - _timeline.Save(_asset); + if (RefreshTempAsset()) + return; + if (SaveToOriginal()) + return; + ClearEditedFlag(); _item.RefreshThumbnail(); } @@ -414,10 +438,18 @@ namespace FlaxEditor.Windows.Assets { base.Update(deltaTime); + // Check if temporary asset need to be updated + if (_timelineIsDirty) + { + _timelineIsDirty = false; + RefreshTempAsset(); + } + + // Check if need to load timeline if (_isWaitingForTimelineLoad && _asset.IsLoaded) { _isWaitingForTimelineLoad = false; - _timeline._id = _asset.ID; + _timeline._id = _item.ID; _timeline.Load(_asset); _undo.Clear(); _timeline.Enabled = true; diff --git a/Source/Editor/Windows/EditGameWindow.cs b/Source/Editor/Windows/EditGameWindow.cs index 888dd4250..dbeab28b1 100644 --- a/Source/Editor/Windows/EditGameWindow.cs +++ b/Source/Editor/Windows/EditGameWindow.cs @@ -429,6 +429,7 @@ namespace FlaxEditor.Windows writer.WriteAttributeString("FarPlane", Viewport.FarPlane.ToString()); writer.WriteAttributeString("FieldOfView", Viewport.FieldOfView.ToString()); writer.WriteAttributeString("MovementSpeed", Viewport.MovementSpeed.ToString()); + writer.WriteAttributeString("ViewportIconsScale", ViewportIconsRenderer.Scale.ToString()); writer.WriteAttributeString("OrthographicScale", Viewport.OrthographicScale.ToString()); writer.WriteAttributeString("UseOrthographicProjection", Viewport.UseOrthographicProjection.ToString()); writer.WriteAttributeString("ViewFlags", ((ulong)Viewport.Task.View.Flags).ToString()); @@ -439,31 +440,24 @@ namespace FlaxEditor.Windows { if (bool.TryParse(node.GetAttribute("GridEnabled"), out bool value1)) Viewport.Grid.Enabled = value1; - if (bool.TryParse(node.GetAttribute("ShowFpsCounter"), out value1)) Viewport.ShowFpsCounter = value1; - if (bool.TryParse(node.GetAttribute("ShowNavigation"), out value1)) Viewport.ShowNavigation = value1; - if (float.TryParse(node.GetAttribute("NearPlane"), out float value2)) Viewport.NearPlane = value2; - if (float.TryParse(node.GetAttribute("FarPlane"), out value2)) Viewport.FarPlane = value2; - if (float.TryParse(node.GetAttribute("FieldOfView"), out value2)) Viewport.FieldOfView = value2; - if (float.TryParse(node.GetAttribute("MovementSpeed"), out value2)) Viewport.MovementSpeed = value2; - + if (float.TryParse(node.GetAttribute("ViewportIconsScale"), out value2)) + ViewportIconsRenderer.Scale = value2; if (float.TryParse(node.GetAttribute("OrthographicScale"), out value2)) Viewport.OrthographicScale = value2; - if (bool.TryParse(node.GetAttribute("UseOrthographicProjection"), out value1)) Viewport.UseOrthographicProjection = value1; - if (ulong.TryParse(node.GetAttribute("ViewFlags"), out ulong value3)) Viewport.Task.ViewFlags = (ViewFlags)value3; diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index 1e4444af0..08767728a 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -958,6 +958,21 @@ void AnimGraphExecutor::ProcessGroupParameters(Box* box, Node* node, Value& valu } break; } + // Set Parameter + case 5: + { + // Set parameter value + int32 paramIndex; + const auto param = _graph.GetParameter((Guid)node->Values[0], paramIndex); + if (param) + { + context.Data->Parameters[paramIndex].Value = tryGetValue(node->GetBox(1), 1, Value::Null); + } + + // Pass over the pose + value = tryGetValue(node->GetBox(2), Value::Null); + break; + } default: break; } diff --git a/Source/Engine/Audio/AudioClip.cpp b/Source/Engine/Audio/AudioClip.cpp index 336ddb7bd..4e258e81a 100644 --- a/Source/Engine/Audio/AudioClip.cpp +++ b/Source/Engine/Audio/AudioClip.cpp @@ -132,9 +132,7 @@ int32 AudioClip::GetFirstBufferIndex(float time, float& offset) const if (_buffersStartTimes[i + 1] > time) { offset = time - _buffersStartTimes[i]; -#if BUILD_DEBUG - ASSERT(Math::Abs(GetBufferStartTime(i) + offset - time) < 0.001f); -#endif + ASSERT_LOW_LAYER(Math::Abs(GetBufferStartTime(i) + offset - time) < 0.001f); return i; } } diff --git a/Source/Engine/Audio/AudioClip.h b/Source/Engine/Audio/AudioClip.h index d35bd18fc..ac30b4c85 100644 --- a/Source/Engine/Audio/AudioClip.h +++ b/Source/Engine/Audio/AudioClip.h @@ -18,6 +18,7 @@ class AudioSource; API_CLASS(NoSpawn) class FLAXENGINE_API AudioClip : public BinaryAsset, public StreamableResource { DECLARE_BINARY_ASSET_HEADER(AudioClip, 2); + friend class AudioBackendOAL; public: /// diff --git a/Source/Engine/Audio/AudioListener.cpp b/Source/Engine/Audio/AudioListener.cpp index 9c5898b4a..3151abafd 100644 --- a/Source/Engine/Audio/AudioListener.cpp +++ b/Source/Engine/Audio/AudioListener.cpp @@ -37,15 +37,17 @@ void AudioListener::OnEnable() { _prevPos = GetPosition(); _velocity = Vector3::Zero; + + ASSERT(!Audio::Listeners.Contains(this)); if (Audio::Listeners.Count() >= AUDIO_MAX_LISTENERS) { - LOG(Error, "Unsupported amount of the audio listeners!"); + if IF_CONSTEXPR (AUDIO_MAX_LISTENERS == 1) + LOG(Warning, "There is more than one Audio Listener active. Please make sure only exactly one is active at any given time."); + else + LOG(Warning, "Too many Audio Listener active."); } else { - ASSERT(!Audio::Listeners.Contains(this)); - if (Audio::Listeners.Count() > 0) - LOG(Warning, "There is more than one Audio Listener active. Please make sure only exactly one is active at any given time."); Audio::Listeners.Add(this); AudioBackend::Listener::Reset(); AudioBackend::Listener::TransformChanged(GetPosition(), GetOrientation()); diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp index 86e828028..715ede74f 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -14,6 +14,9 @@ #include "Engine/Audio/AudioListener.h" #include "Engine/Audio/AudioSource.h" #include "Engine/Audio/AudioSettings.h" +#include "Engine/Content/Content.h" +#include "Engine/Level/Level.h" +#include "Engine/Video/VideoPlayer.h" // Include OpenAL library // Source: https://github.com/kcat/openal-soft @@ -73,6 +76,7 @@ namespace ALC ALCdevice* Device = nullptr; ALCcontext* Context = nullptr; AudioBackend::FeatureFlags Features = AudioBackend::FeatureFlags::None; + bool Inited = false; CriticalSection Locker; Dictionary SourcesData; @@ -164,12 +168,9 @@ namespace ALC float Time; }; - void RebuildContext(const Array& states) + void RebuildContext() { - LOG(Info, "Rebuilding audio contexts"); - ClearContext(); - if (Device == nullptr) return; @@ -182,10 +183,16 @@ namespace ALC Context = alcCreateContext(Device, attrList); alcMakeContextCurrent(Context); - + } + + void RebuildListeners() + { for (AudioListener* listener : Audio::Listeners) Listener::Rebuild(listener); - + } + + void RebuildSources(const Array& states) + { for (int32 i = 0; i < states.Count(); i++) { AudioSource* source = Audio::Sources[i]; @@ -205,6 +212,13 @@ namespace ALC } } + void RebuildContext(const Array& states) + { + RebuildContext(); + RebuildListeners(); + RebuildSources(states); + } + void RebuildContext(bool isChangingDevice) { Array states; @@ -400,7 +414,7 @@ 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; + const float pan = ALC::SourcesData[sourceID].Pan; ALC::Locker.Unlock(); if (spatial) { @@ -629,6 +643,7 @@ AudioBackend::FeatureFlags AudioBackendOAL::Base_Features() void AudioBackendOAL::Base_OnActiveDeviceChanged() { + PROFILE_CPU(); PROFILE_MEM(Audio); // Cleanup @@ -659,9 +674,53 @@ void AudioBackendOAL::Base_OnActiveDeviceChanged() LOG(Fatal, "Failed to open OpenAL device ({0}).", String(name)); return; } + if (ALC::Inited) + LOG(Info, "Changed audio device to: {}", String(Audio::GetActiveDevice()->Name)); - // Setup - ALC::RebuildContext(states); + // Rebuild context + ALC::RebuildContext(); + if (ALC::Inited) + { + // Reload all audio clips to recreate their buffers + for (AudioClip* audioClip : Content::GetAssets()) + { + audioClip->WaitForLoaded(); + ScopeLock lock(audioClip->Locker); + + // Clear old buffer IDs + for (uint32& bufferID : audioClip->Buffers) + bufferID = 0; + + if (audioClip->IsStreamable()) + { + // Let the streaming recreate missing buffers + audioClip->RequestStreamingUpdate(); + } + else + { + // Reload audio clip + auto assetLock = audioClip->Storage->Lock(); + audioClip->LoadChunk(0); + audioClip->Buffers[0] = AudioBackend::Buffer::Create(); + audioClip->WriteBuffer(0); + + } + } + + // Reload all videos to recreate their buffers + for (VideoPlayer* videoPlayer : Level::GetActors(true)) + { + VideoBackendPlayer& player = videoPlayer->_player; + + // Clear audio state + for (uint32& bufferID : player.AudioBuffers) + bufferID = 0; + player.NextAudioBuffer = 0; + player.AudioSource = 0; + } + } + ALC::RebuildListeners(); + ALC::RebuildSources(states); } void AudioBackendOAL::Base_SetDopplerFactor(float value) @@ -782,6 +841,7 @@ bool AudioBackendOAL::Base_Init() if (ALC::IsExtensionSupported("AL_SOFT_source_spatialize")) ALC::Features = EnumAddFlags(ALC::Features, FeatureFlags::SpatialMultiChannel); #endif + ALC::Inited = true; // Log service info LOG(Info, "{0} ({1})", String(alGetString(AL_RENDERER)), String(alGetString(AL_VERSION))); diff --git a/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp b/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp index 9600a93fb..6cba1302b 100644 --- a/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp +++ b/Source/Engine/Audio/XAudio2/AudioBackendXAudio2.cpp @@ -672,7 +672,7 @@ bool AudioBackendXAudio2::Base_Init() HRESULT hr = XAudio2Create(&XAudio2::Instance, 0, XAUDIO2_DEFAULT_PROCESSOR); if (FAILED(hr)) { - LOG(Error, "Failed to initalize XAudio2. Error: 0x{0:x}", hr); + LOG(Error, "Failed to initialize XAudio2. Error: 0x{0:x}", hr); return true; } XAudio2::Instance->RegisterForCallbacks(&XAudio2::Callback); @@ -681,7 +681,7 @@ bool AudioBackendXAudio2::Base_Init() hr = XAudio2::Instance->CreateMasteringVoice(&XAudio2::MasteringVoice); if (FAILED(hr)) { - LOG(Error, "Failed to initalize XAudio2 mastering voice. Error: 0x{0:x}", hr); + LOG(Error, "Failed to initialize XAudio2 mastering voice. Error: 0x{0:x}", hr); return true; } XAUDIO2_VOICE_DETAILS details; diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index 9fd4cee9c..7a35d3b28 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -487,6 +487,8 @@ bool Asset::WaitForLoaded(double timeoutInMilliseconds) const const auto loadingTask = (ContentLoadTask*)Platform::AtomicRead(&_loadingTask); if (loadingTask == nullptr) { + if (IsLoaded()) + return false; LOG(Warning, "WaitForLoaded asset \'{0}\' failed. No loading task attached and asset is not loaded.", ToString()); return true; } diff --git a/Source/Engine/Content/Content.cpp b/Source/Engine/Content/Content.cpp index 4a9ea500b..59d2dfffa 100644 --- a/Source/Engine/Content/Content.cpp +++ b/Source/Engine/Content/Content.cpp @@ -684,6 +684,19 @@ Array Content::GetAssets() return assets; } +Array Content::GetAssets(const MClass* type) +{ + Array assets; + AssetsLocker.Lock(); + for (auto& e : Assets) + { + if (e.Value->Is(type)) + assets.Add(e.Value); + } + AssetsLocker.Unlock(); + return assets; +} + const Dictionary& Content::GetAssetsRaw() { AssetsLocker.Lock(); diff --git a/Source/Engine/Content/Content.h b/Source/Engine/Content/Content.h index f6dcf59f0..e286e7c7d 100644 --- a/Source/Engine/Content/Content.h +++ b/Source/Engine/Content/Content.h @@ -3,6 +3,9 @@ #pragma once #include "Engine/Scripting/ScriptingType.h" +#ifndef _MSC_VER +#include "Engine/Core/Collections/Array.h" +#endif #include "AssetInfo.h" #include "Asset.h" #include "Config.h" @@ -122,7 +125,26 @@ public: /// Gets the assets (loaded or during load). /// /// The collection of assets. - static Array GetAssets(); + API_FUNCTION() static Array GetAssets(); + + /// + /// Gets the assets (loaded or during load). + /// + /// Type of the assets to search for. Includes any assets derived from the type. + /// Found actors list. + API_FUNCTION() static Array GetAssets(API_PARAM(Attributes="TypeReference(typeof(Actor))") const MClass* type); + + /// + /// Gets the assets (loaded or during load). + /// + /// Type of the object. + /// Found actors list. + template + static Array GetAssets() + { + Array assets = GetAssets(T::GetStaticClass()); + return *(Array*) & assets; + } /// /// Gets the raw dictionary of assets (loaded or during load). diff --git a/Source/Engine/ContentImporters/ImportModel.cpp b/Source/Engine/ContentImporters/ImportModel.cpp index 91547dc8d..f3548dc5c 100644 --- a/Source/Engine/ContentImporters/ImportModel.cpp +++ b/Source/Engine/ContentImporters/ImportModel.cpp @@ -478,16 +478,23 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context) } // Check if restore local changes on asset reimport + constexpr bool RestoreModelOptionsOnReimport = true; constexpr bool RestoreAnimEventsOnReimport = true; + const bool restoreModelOptions = RestoreModelOptionsOnReimport && (options.Type == ModelTool::ModelType::Model || options.Type == ModelTool::ModelType::SkinnedModel); const bool restoreMaterials = options.RestoreMaterialsOnReimport && data->Materials.HasItems(); const bool restoreAnimEvents = RestoreAnimEventsOnReimport && options.Type == ModelTool::ModelType::Animation && data->Animations.HasItems(); - if ((restoreMaterials || restoreAnimEvents) && FileSystem::FileExists(context.TargetAssetPath)) + if ((restoreModelOptions || restoreMaterials || restoreAnimEvents) && FileSystem::FileExists(context.TargetAssetPath)) { AssetReference asset = Content::LoadAsync(context.TargetAssetPath); if (asset && !asset->WaitForLoaded()) { auto* model = ScriptingObject::Cast(asset); auto* animation = ScriptingObject::Cast(asset); + if (restoreModelOptions && model) + { + // Copy general properties + data->MinScreenSize = model->MinScreenSize; + } if (restoreMaterials && model) { // Copy material settings diff --git a/Source/Engine/Core/Config.h b/Source/Engine/Core/Config.h index 810217050..95319b885 100644 --- a/Source/Engine/Core/Config.h +++ b/Source/Engine/Core/Config.h @@ -57,5 +57,5 @@ #define API_PARAM(...) #define API_TYPEDEF(...) #define API_INJECT_CODE(...) -#define API_AUTO_SERIALIZATION(...) public: void Serialize(SerializeStream& stream, const void* otherObj) override; void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; +#define API_AUTO_SERIALIZATION(...) public: bool ShouldSerialize(const void* otherObj) const override; void Serialize(SerializeStream& stream, const void* otherObj) override; void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; #define DECLARE_SCRIPTING_TYPE_MINIMAL(type) public: friend class type##Internal; static struct ScriptingTypeInitializer TypeInitializer; diff --git a/Source/Engine/Core/ISerializable.h b/Source/Engine/Core/ISerializable.h index c99051fc0..7500eff8b 100644 --- a/Source/Engine/Core/ISerializable.h +++ b/Source/Engine/Core/ISerializable.h @@ -36,6 +36,13 @@ public: /// virtual ~ISerializable() = default; + /// + /// Compares with other instance to decide whether serialize this instance (eg. any field orp property is modified). Used to skip object serialization if not needed. + /// + /// The instance of the object (always valid) to compare with to decide whether serialize this instance. + /// True if any field or property is modified compared to the other object instance, otherwise false. + virtual bool ShouldSerialize(const void* otherObj) const { return true; } + /// /// Serializes object to the output stream compared to the values of the other object instance (eg. default class object). If other object is null then serialize all properties. /// diff --git a/Source/Engine/Core/Math/Color.cs b/Source/Engine/Core/Math/Color.cs index 2d779dcfa..be4a12789 100644 --- a/Source/Engine/Core/Math/Color.cs +++ b/Source/Engine/Core/Math/Color.cs @@ -11,7 +11,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.ColorConverter))] #endif - partial struct Color + partial struct Color : Json.ICustomValueEquals { /// /// The size of the type, in bytes. @@ -196,6 +196,13 @@ namespace FlaxEngine A = values[3]; } + /// + public bool ValueEquals(object other) + { + var o = (Color)other; + return Equals(ref o); + } + /// public override bool Equals(object value) { diff --git a/Source/Engine/Core/Math/Double2.cs b/Source/Engine/Core/Math/Double2.cs index 9594b22cb..51fcf32d5 100644 --- a/Source/Engine/Core/Math/Double2.cs +++ b/Source/Engine/Core/Math/Double2.cs @@ -65,7 +65,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Double2Converter))] #endif - partial struct Double2 : IEquatable, IFormattable + partial struct Double2 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2}"; @@ -1574,6 +1574,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Double2)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Double3.cs b/Source/Engine/Core/Math/Double3.cs index cb26cf071..4dccc1fb3 100644 --- a/Source/Engine/Core/Math/Double3.cs +++ b/Source/Engine/Core/Math/Double3.cs @@ -66,7 +66,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Double3Converter))] #endif - partial struct Double3 : IEquatable, IFormattable + partial struct Double3 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2}"; @@ -1872,6 +1872,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Double3)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Double4.cs b/Source/Engine/Core/Math/Double4.cs index 70d27cb28..bf176e6ff 100644 --- a/Source/Engine/Core/Math/Double4.cs +++ b/Source/Engine/Core/Math/Double4.cs @@ -66,7 +66,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Double4Converter))] #endif - partial struct Double4 : IEquatable, IFormattable + partial struct Double4 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2} W:{3:F2}"; @@ -1372,6 +1372,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Double4)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Float2.cs b/Source/Engine/Core/Math/Float2.cs index 1b70dd0a6..5bb81ec3a 100644 --- a/Source/Engine/Core/Math/Float2.cs +++ b/Source/Engine/Core/Math/Float2.cs @@ -60,7 +60,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Float2Converter))] #endif - partial struct Float2 : IEquatable, IFormattable + partial struct Float2 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2}"; @@ -1650,6 +1650,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Float2)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Float3.cs b/Source/Engine/Core/Math/Float3.cs index 5e8dceed6..50554345b 100644 --- a/Source/Engine/Core/Math/Float3.cs +++ b/Source/Engine/Core/Math/Float3.cs @@ -60,7 +60,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Float3Converter))] #endif - partial struct Float3 : IEquatable, IFormattable + partial struct Float3 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2}"; @@ -1904,6 +1904,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Float3)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Float4.cs b/Source/Engine/Core/Math/Float4.cs index b6eb6dd9e..26abf4b2e 100644 --- a/Source/Engine/Core/Math/Float4.cs +++ b/Source/Engine/Core/Math/Float4.cs @@ -60,7 +60,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Float4Converter))] #endif - partial struct Float4 : IEquatable, IFormattable + partial struct Float4 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2} W:{3:F2}"; @@ -1412,6 +1412,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Float4)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Half.h b/Source/Engine/Core/Math/Half.h index 2617649d0..9ec68a6a0 100644 --- a/Source/Engine/Core/Math/Half.h +++ b/Source/Engine/Core/Math/Half.h @@ -5,6 +5,7 @@ #include "Math.h" #include "Vector2.h" #include "Vector3.h" +#include "Vector4.h" /// /// Half-precision 16 bit floating point number consisting of a sign bit, a 5 bit biased exponent, and a 10 bit mantissa @@ -248,6 +249,19 @@ public: explicit Half4(const Color& c); explicit Half4(const Rectangle& rect); + operator Float2() const + { + return ToFloat2(); + } + operator Float3() const + { + return ToFloat3(); + } + operator Float4() const + { + return ToFloat4(); + } + public: Float2 ToFloat2() const; Float3 ToFloat3() const; diff --git a/Source/Engine/Core/Math/Int2.cs b/Source/Engine/Core/Math/Int2.cs index 4a4107252..32a273307 100644 --- a/Source/Engine/Core/Math/Int2.cs +++ b/Source/Engine/Core/Math/Int2.cs @@ -14,7 +14,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Int2Converter))] #endif - partial struct Int2 : IEquatable, IFormattable + partial struct Int2 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0} Y:{1}"; @@ -940,6 +940,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Int2)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Int3.cs b/Source/Engine/Core/Math/Int3.cs index 78e4600c3..81bb8026e 100644 --- a/Source/Engine/Core/Math/Int3.cs +++ b/Source/Engine/Core/Math/Int3.cs @@ -14,7 +14,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Int3Converter))] #endif - partial struct Int3 : IEquatable, IFormattable + partial struct Int3 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0} Y:{1} Z:{2}"; @@ -1023,6 +1023,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Int3)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Int4.cs b/Source/Engine/Core/Math/Int4.cs index e180ccb31..bbccadab4 100644 --- a/Source/Engine/Core/Math/Int4.cs +++ b/Source/Engine/Core/Math/Int4.cs @@ -14,7 +14,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Int4Converter))] #endif - partial struct Int4 : IEquatable, IFormattable + partial struct Int4 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0} Y:{1} Z:{2} W:{3}"; @@ -881,6 +881,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Int4)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Packed.cpp b/Source/Engine/Core/Math/Packed.cpp index 55ebd9ccf..b22e65d3e 100644 --- a/Source/Engine/Core/Math/Packed.cpp +++ b/Source/Engine/Core/Math/Packed.cpp @@ -41,16 +41,6 @@ FloatR10G10B10A2::FloatR10G10B10A2(const float* values) { } -FloatR10G10B10A2::operator Float3() const -{ - return ToFloat3(); -} - -FloatR10G10B10A2::operator Float4() const -{ - return ToFloat4(); -} - Float3 FloatR10G10B10A2::ToFloat3() const { Float3 vectorOut; diff --git a/Source/Engine/Core/Math/Packed.h b/Source/Engine/Core/Math/Packed.h index 8e3aad41f..c5b5e827f 100644 --- a/Source/Engine/Core/Math/Packed.h +++ b/Source/Engine/Core/Math/Packed.h @@ -40,9 +40,14 @@ struct FLAXENGINE_API FloatR10G10B10A2 { return Value; } - - operator Float3() const; - operator Float4() const; + operator Float3() const + { + return ToFloat3(); + } + operator Float4() const + { + return ToFloat4(); + } FloatR10G10B10A2& operator=(const FloatR10G10B10A2& other) { diff --git a/Source/Engine/Core/Math/Quaternion.cs b/Source/Engine/Core/Math/Quaternion.cs index d89b71488..cf51fac50 100644 --- a/Source/Engine/Core/Math/Quaternion.cs +++ b/Source/Engine/Core/Math/Quaternion.cs @@ -60,7 +60,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.QuaternionConverter))] #endif - partial struct Quaternion : IEquatable, IFormattable + partial struct Quaternion : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2} W:{3:F2}"; @@ -1681,6 +1681,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Quaternion)other; + return Equals(ref o); + } + /// /// Tests whether one quaternion is near another quaternion. /// diff --git a/Source/Engine/Core/Math/Rectangle.cs b/Source/Engine/Core/Math/Rectangle.cs index 81c689d48..8e3c2b6c4 100644 --- a/Source/Engine/Core/Math/Rectangle.cs +++ b/Source/Engine/Core/Math/Rectangle.cs @@ -6,7 +6,7 @@ using System.Runtime.CompilerServices; namespace FlaxEngine { - partial struct Rectangle : IEquatable + partial struct Rectangle : IEquatable, Json.ICustomValueEquals { /// /// A which represents an empty space. @@ -523,6 +523,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Rectangle)other; + return Equals(ref o); + } + /// public override string ToString() { diff --git a/Source/Engine/Core/Math/Transform.cs b/Source/Engine/Core/Math/Transform.cs index fc16a501b..90c0c36c9 100644 --- a/Source/Engine/Core/Math/Transform.cs +++ b/Source/Engine/Core/Math/Transform.cs @@ -16,7 +16,7 @@ using System.Runtime.InteropServices; namespace FlaxEngine { [Serializable] - partial struct Transform : IEquatable, IFormattable + partial struct Transform : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "Translation:{0} Orientation:{1} Scale:{2}"; @@ -673,6 +673,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Transform)other; + return Equals(ref o); + } + /// /// Tests whether one transform is near another transform. /// diff --git a/Source/Engine/Core/Math/Vector2.cs b/Source/Engine/Core/Math/Vector2.cs index 8e1599513..77c52035d 100644 --- a/Source/Engine/Core/Math/Vector2.cs +++ b/Source/Engine/Core/Math/Vector2.cs @@ -73,7 +73,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Vector2Converter))] #endif - public unsafe partial struct Vector2 : IEquatable, IFormattable + public unsafe partial struct Vector2 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2}"; @@ -954,6 +954,33 @@ namespace FlaxEngine return result; } + /// + /// Performs a spherical linear interpolation between two vectors. + /// + /// Start vector. + /// End vector. + /// Value between 0 and 1 indicating the weight of . + /// >When the method completes, contains the linear interpolation of the two vectors. + public static void Slerp(ref Vector2 start, ref Vector2 end, float amount, out Vector2 result) + { + var dot = Mathr.Clamp(Dot(start, end), -1.0f, 1.0f); + var theta = Mathr.Acos(dot) * amount; + Vector2 relativeVector = (end - start * dot).Normalized; + result = ((start * Mathr.Cos(theta)) + (relativeVector * Mathr.Sin(theta))); + } + + /// + /// Performs a spherical linear interpolation between two vectors. + /// + /// Start vector. + /// End vector. + /// Value between 0 and 1 indicating the weight of . + public static Vector2 Slerp(Vector2 start, Vector2 end, float amount) + { + Slerp(ref start, ref end, amount, out Vector2 result); + return result; + } + /// /// Performs a gradual change of a vector towards a specified target over time /// @@ -1774,6 +1801,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Vector2)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Vector2.h b/Source/Engine/Core/Math/Vector2.h index 34bd1a59c..cee013840 100644 --- a/Source/Engine/Core/Math/Vector2.h +++ b/Source/Engine/Core/Math/Vector2.h @@ -558,6 +558,24 @@ public: return result; } + // Performs a spherical linear interpolation between two vectors. + static void Slerp(const Vector2Base& start, const Vector2Base& end, T amount, Vector2Base& result) + { + T dot = Math::Clamp(Dot(start, end), -1.0f, 1.0f); + T theta = Math::Acos(dot) * amount; + Vector2Base relativeVector = end - start * dot; + relativeVector.Normalize(); + result = ((start * Math::Cos(theta)) + (relativeVector * Math::Sin(theta))); + } + + // Performs a spherical linear interpolation between two vectors. + static Vector2Base Slerp(const Vector2Base& start, const Vector2Base& end, T amount) + { + Vector2Base result; + Slerp(start, end, amount, result); + return result; + } + public: /// /// Calculates the area of the triangle. diff --git a/Source/Engine/Core/Math/Vector3.cs b/Source/Engine/Core/Math/Vector3.cs index 5e01a7a6c..ce7492e55 100644 --- a/Source/Engine/Core/Math/Vector3.cs +++ b/Source/Engine/Core/Math/Vector3.cs @@ -73,7 +73,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Vector3Converter))] #endif - public unsafe partial struct Vector3 : IEquatable, IFormattable + public unsafe partial struct Vector3 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2}"; @@ -1043,6 +1043,33 @@ namespace FlaxEngine return result; } + /// + /// Performs a spherical linear interpolation between two vectors. + /// + /// Start vector. + /// End vector. + /// Value between 0 and 1 indicating the weight of . + /// When the method completes, contains the linear interpolation of the two vectors. + public static void Slerp(ref Vector3 start, ref Vector3 end, float amount, out Vector3 result) + { + var dot = Mathr.Clamp(Dot(start, end), -1.0f, 1.0f); + var theta = Mathr.Acos(dot) * amount; + Vector3 relativeVector = (end - start * dot).Normalized; + result = ((start * Mathr.Cos(theta)) + (relativeVector * Mathr.Sin(theta))); + } + + /// + /// Performs a spherical linear interpolation between two vectors. + /// + /// Start vector. + /// End vector. + /// Value between 0 and 1 indicating the weight of . + public static Vector3 Slerp(Vector3 start, Vector3 end, float amount) + { + Slerp(ref start, ref end, amount, out var result); + return result; + } + /// /// Performs a gradual change of a vector towards a specified target over time /// @@ -2133,6 +2160,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Vector3)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Vector3.h b/Source/Engine/Core/Math/Vector3.h index a19253b00..bf737877b 100644 --- a/Source/Engine/Core/Math/Vector3.h +++ b/Source/Engine/Core/Math/Vector3.h @@ -686,6 +686,24 @@ public: return result; } + // Performs a spherical linear interpolation between two vectors. + static void Slerp(const Vector3Base& start, const Vector3Base& end, T amount, Vector3Base& result) + { + T dot = Math::Clamp(Dot(start, end), -1.0f, 1.0f); + T theta = Math::Acos(dot) * amount; + Vector3Base relativeVector = end - start * dot; + relativeVector.Normalize(); + result = ((start * Math::Cos(theta)) + (relativeVector * Math::Sin(theta))); + } + + // Performs a spherical linear interpolation between two vectors. + static Vector3Base Slerp(const Vector3Base& start, const Vector3Base& end, T amount) + { + Vector3Base result; + Slerp(start, end, amount, result); + return result; + } + // Performs a cubic interpolation between two vectors. static void SmoothStep(const Vector3Base& start, const Vector3Base& end, T amount, Vector3Base& result) { diff --git a/Source/Engine/Core/Math/Vector4.cs b/Source/Engine/Core/Math/Vector4.cs index e50d03ca0..b08a08f50 100644 --- a/Source/Engine/Core/Math/Vector4.cs +++ b/Source/Engine/Core/Math/Vector4.cs @@ -72,7 +72,7 @@ namespace FlaxEngine #if FLAX_EDITOR [System.ComponentModel.TypeConverter(typeof(TypeConverters.Vector4Converter))] #endif - public partial struct Vector4 : IEquatable, IFormattable + public partial struct Vector4 : IEquatable, IFormattable, Json.ICustomValueEquals { private static readonly string _formatString = "X:{0:F2} Y:{1:F2} Z:{2:F2} W:{3:F2}"; @@ -260,6 +260,19 @@ namespace FlaxEngine /// public bool IsNormalized => Mathr.Abs((X * X + Y * Y + Z * Z + W * W) - 1.0f) < 1e-4f; + /// + /// Gets the normalized vector. Returned vector has length equal 1. + /// + public Vector4 Normalized + { + get + { + Vector4 vector4 = this; + vector4.Normalize(); + return vector4; + } + } + /// /// Gets a value indicting whether this vector is zero /// @@ -878,6 +891,33 @@ namespace FlaxEngine return result; } + /// + /// Performs a spherical linear interpolation between two vectors. + /// + /// Start vector. + /// End vector. + /// Value between 0 and 1 indicating the weight of . + /// When the method completes, contains the linear interpolation of the two vectors. + public static void Slerp(ref Vector4 start, ref Vector4 end, Real amount, out Vector4 result) + { + var dot = Mathr.Clamp(Dot(start, end), -1.0f, 1.0f); + var theta = Mathr.Acos(dot) * amount; + Vector4 relativeVector = (end - start * dot).Normalized; + result = ((start * Mathr.Cos(theta)) + (relativeVector * Mathr.Sin(theta))); + } + + /// + /// Performs a spherical linear interpolation between two vectors. + /// + /// Start vector. + /// End vector. + /// Value between 0 and 1 indicating the weight of . + public static Vector4 Slerp(Vector4 start, Vector4 end, Real amount) + { + Slerp(ref start, ref end, amount, out var result); + return result; + } + /// /// Performs a cubic interpolation between two vectors. /// @@ -1486,6 +1526,13 @@ namespace FlaxEngine } } + /// + public bool ValueEquals(object other) + { + var o = (Vector4)other; + return Equals(ref o); + } + /// /// Determines whether the specified is equal to this instance. /// diff --git a/Source/Engine/Core/Math/Vector4.h b/Source/Engine/Core/Math/Vector4.h index cc7b59730..5f5e436ad 100644 --- a/Source/Engine/Core/Math/Vector4.h +++ b/Source/Engine/Core/Math/Vector4.h @@ -129,6 +129,12 @@ public: FLAXENGINE_API String ToString() const; public: + // Gets a value indicting whether this instance is normalized. + bool IsNormalized() const + { + return Math::Abs((X * X + Y * Y + Z * Z + W * W) - 1.0f) < 1e-4f; + } + // Gets a value indicting whether this vector is zero. bool IsZero() const { @@ -219,6 +225,45 @@ public: return Vector4Base(-X, -Y, -Z, -W); } + /// + /// Calculates a normalized vector that has length equal to 1. + /// + Vector4Base GetNormalized() const + { + Vector4Base result(X, Y, Z, W); + result.Normalize(); + return result; + } + +public: + /// + /// Performs vector normalization (scales vector up to unit length). + /// + void Normalize() + { + const T length = Math::Sqrt(X * X + Y * Y + Z * Z + W * W); + if (length >= ZeroTolerance) + { + const T inv = (T)1.0f / length; + X *= inv; + Y *= inv; + Z *= inv; + W *= inv; + } + } + + /// + /// Performs fast vector normalization (scales vector up to unit length). + /// + void NormalizeFast() + { + const T inv = 1.0f / Math::Sqrt(X * X + Y * Y + Z * Z + W * W); + X *= inv; + Y *= inv; + Z *= inv; + W *= inv; + } + public: Vector4Base operator+(const Vector4Base& b) const { @@ -469,6 +514,41 @@ public: result = Vector4Base(Math::Clamp(v.X, min.X, max.X), Math::Clamp(v.Y, min.Y, max.Y), Math::Clamp(v.Z, min.Z, max.Z), Math::Clamp(v.W, min.W, max.W)); } + // Performs vector normalization (scales vector up to unit length). + static Vector4Base Normalize(const Vector4Base& v) + { + Vector4Base r = v; + const T length = Math::Sqrt(r.X * r.X + r.Y * r.Y + r.Z * r.Z + r.W * r.W); + if (length >= ZeroTolerance) + { + const T inv = (T)1.0f / length; + r.X *= inv; + r.Y *= inv; + r.Z *= inv; + r.W *= inv; + } + return r; + } + + // Performs vector normalization (scales vector up to unit length). This is a faster version that does not perform check for length equal 0 (it assumes that input vector is not empty). + static Vector4Base NormalizeFast(const Vector4Base& v) + { + const T inv = 1.0f / v.Length(); + return Vector4Base(v.X * inv, v.Y * inv, v.Z * inv, v.W * inv); + } + + // Performs vector normalization (scales vector up to unit length). + static FORCE_INLINE void Normalize(const Vector4Base& input, Vector4Base& result) + { + result = Normalize(input); + } + + // Calculates the dot product of two vectors. + FORCE_INLINE static T Dot(const Vector4Base& a, const Vector4Base& b) + { + return a.X * b.X + a.Y * b.Y + a.Z * b.Z + a.W * b.W; + } + // Performs a linear interpolation between two vectors. static void Lerp(const Vector4Base& start, const Vector4Base& end, T amount, Vector4Base& result) { @@ -486,6 +566,24 @@ public: return result; } + // Performs a spherical linear interpolation between two vectors. + static void Slerp(const Vector4Base& start, const Vector4Base& end, T amount, Vector4Base& result) + { + T dot = Math::Clamp(Dot(start, end), -1.0f, 1.0f); + T theta = Math::Acos(dot) * amount; + Vector4Base relativeVector = end - start * dot; + relativeVector.Normalize(); + result = ((start * Math::Cos(theta)) + (relativeVector * Math::Sin(theta))); + } + + // Performs a spherical linear interpolation between two vectors. + static Vector4Base Slerp(const Vector4Base& start, const Vector4Base& end, T amount) + { + Vector4Base result; + Slerp(start, end, amount, result); + return result; + } + FLAXENGINE_API static Vector4Base Transform(const Vector4Base& v, const Matrix& m); }; diff --git a/Source/Engine/Debug/DebugCommands.cpp b/Source/Engine/Debug/DebugCommands.cpp index fa171d5dd..58cf2894b 100644 --- a/Source/Engine/Debug/DebugCommands.cpp +++ b/Source/Engine/Debug/DebugCommands.cpp @@ -8,6 +8,7 @@ #include "Engine/Threading/Threading.h" #include "Engine/Threading/Task.h" #include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Profiler/ProfilerMemory.h" #include "Engine/Scripting/BinaryModule.h" #include "Engine/Scripting/Scripting.h" #include "Engine/Scripting/ManagedCLR/MAssembly.h" @@ -219,6 +220,7 @@ namespace if (module == GetBinaryModuleCorlib()) return; PROFILE_CPU(); + PROFILE_MEM(EngineDebug); #if USE_CSHARP if (auto* managedModule = dynamic_cast(module)) @@ -381,6 +383,7 @@ DebugCommandsService DebugCommandsServiceInstance; void DebugCommands::Execute(StringView command) { + PROFILE_MEM(EngineDebug); // TODO: fix missing string handle on 1st command execution (command gets invalid after InitCommands due to dotnet GC or dotnet interop handles flush) String commandCopy = command; command = commandCopy; @@ -423,6 +426,7 @@ void DebugCommands::Search(StringView searchText, Array& matches, bo { if (searchText.IsEmpty()) return; + PROFILE_MEM(EngineDebug); // TODO: fix missing string handle on 1st command execution (command gets invalid after InitCommands due to dotnet GC or dotnet interop handles flush) String searchTextCopy = searchText; searchText = searchTextCopy; diff --git a/Source/Engine/Debug/DebugDraw.cpp b/Source/Engine/Debug/DebugDraw.cpp index bea9e76f4..18552dcec 100644 --- a/Source/Engine/Debug/DebugDraw.cpp +++ b/Source/Engine/Debug/DebugDraw.cpp @@ -480,6 +480,7 @@ DebugDrawCall WriteLists(int32& vertexCounter, const Array& listA, const Arra FORCE_INLINE DebugTriangle* AppendTriangles(int32 count, float duration, bool depthTest) { + PROFILE_MEM(EngineDebug); Array* list; if (depthTest) list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultTriangles : &Context->DebugDrawDepthTest.OneFrameTriangles; @@ -492,6 +493,7 @@ FORCE_INLINE DebugTriangle* AppendTriangles(int32 count, float duration, bool de FORCE_INLINE DebugTriangle* AppendWireTriangles(int32 count, float duration, bool depthTest) { + PROFILE_MEM(EngineDebug); Array* list; if (depthTest) list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultWireTriangles : &Context->DebugDrawDepthTest.OneFrameWireTriangles; @@ -539,7 +541,7 @@ DebugDrawService DebugDrawServiceInstance; bool DebugDrawService::Init() { - PROFILE_MEM(Graphics); + PROFILE_MEM(EngineDebug); Context = &GlobalContext; // Init wireframe sphere cache @@ -658,7 +660,7 @@ void DebugDrawService::Update() } PROFILE_CPU(); - PROFILE_MEM(Graphics); + PROFILE_MEM(EngineDebug); // Update lists float deltaTime = Time::Update.DeltaTime.GetTotalSeconds(); @@ -1114,6 +1116,7 @@ void DebugDraw::DrawRay(const Ray& ray, const Color& color, float length, float void DebugDraw::DrawLine(const Vector3& start, const Vector3& end, const Color& color, float duration, bool depthTest) { + PROFILE_MEM(EngineDebug); const Float3 startF = start - Context->Origin, endF = end - Context->Origin; auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) @@ -1132,6 +1135,7 @@ void DebugDraw::DrawLine(const Vector3& start, const Vector3& end, const Color& void DebugDraw::DrawLine(const Vector3& start, const Vector3& end, const Color& startColor, const Color& endColor, float duration, bool depthTest) { + PROFILE_MEM(EngineDebug); const Float3 startF = start - Context->Origin, endF = end - Context->Origin; auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) @@ -1161,6 +1165,7 @@ void DebugDraw::DrawLines(const Span& lines, const Matrix& transform, co } // Draw lines + PROFILE_MEM(EngineDebug); const Float3* p = lines.Get(); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; const Matrix transformF = transform * Matrix::Translation(-Context->Origin); @@ -1200,6 +1205,7 @@ void DebugDraw::DrawLines(GPUBuffer* lines, const Matrix& transform, float durat } // Draw lines + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; auto& geometry = debugDrawData.GeometryBuffers.AddOne(); geometry.Buffer = lines; @@ -1224,6 +1230,7 @@ void DebugDraw::DrawLines(const Span& lines, const Matrix& transform, c } // Draw lines + PROFILE_MEM(EngineDebug); const Double3* p = lines.Get(); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; const Matrix transformF = transform * Matrix::Translation(-Context->Origin); @@ -1270,6 +1277,7 @@ void DebugDraw::DrawBezier(const Vector3& p1, const Vector3& p2, const Vector3& const float segmentCountInv = 1.0f / (float)segmentCount; // Draw segmented curve from lines + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) { @@ -1310,6 +1318,7 @@ void DebugDraw::DrawWireBox(const BoundingBox& box, const Color& color, float du c -= Context->Origin; // Draw lines + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) { @@ -1344,6 +1353,7 @@ void DebugDraw::DrawWireFrustum(const BoundingFrustum& frustum, const Color& col c -= Context->Origin; // Draw lines + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) { @@ -1378,6 +1388,7 @@ void DebugDraw::DrawWireBox(const OrientedBoundingBox& box, const Color& color, c -= Context->Origin; // Draw lines + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) { @@ -1419,6 +1430,7 @@ void DebugDraw::DrawWireSphere(const BoundingSphere& sphere, const Color& color, auto& cache = SphereCache[index]; // Draw lines of the unit sphere after linear transform + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; if (duration > 0) { @@ -1454,6 +1466,7 @@ void DebugDraw::DrawSphere(const BoundingSphere& sphere, const Color& color, flo list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultTriangles : &Context->DebugDrawDepthTest.OneFrameTriangles; else list = duration > 0 ? &Context->DebugDrawDefault.DefaultTriangles : &Context->DebugDrawDefault.OneFrameTriangles; + PROFILE_MEM(EngineDebug); list->EnsureCapacity(list->Count() + SphereTriangleCache.Count()); const Float3 centerF = sphere.Center - Context->Origin; @@ -1485,6 +1498,7 @@ void DebugDraw::DrawCircle(const Vector3& position, const Float3& normal, float Matrix::Multiply(scale, world, matrix); // Draw lines of the unit circle after linear transform + PROFILE_MEM(EngineDebug); Float3 prev = Float3::Transform(CircleCache[0], matrix); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; for (int32 i = 1; i < DEBUG_DRAW_CIRCLE_VERTICES;) @@ -1515,6 +1529,7 @@ void DebugDraw::DrawWireTriangle(const Vector3& v0, const Vector3& v1, const Vec void DebugDraw::DrawTriangle(const Vector3& v0, const Vector3& v1, const Vector3& v2, const Color& color, float duration, bool depthTest) { + PROFILE_MEM(EngineDebug); DebugTriangle t; t.Color = Color32(color); t.TimeLeft = duration; @@ -1570,6 +1585,7 @@ void DebugDraw::DrawTriangles(GPUBuffer* triangles, const Matrix& transform, flo DebugLog::ThrowException("Cannot draw debug lines with incorrect amount of items in array"); return; } + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; auto& geometry = debugDrawData.GeometryBuffers.AddOne(); geometry.Buffer = triangles; @@ -1859,6 +1875,7 @@ void DebugDraw::DrawWireCapsule(const Vector3& position, const Quaternion& orien Matrix::Multiply(rotation, translation, world); // Write vertices + PROFILE_MEM(EngineDebug); auto& debugDrawData = depthTest ? Context->DebugDrawDepthTest : Context->DebugDrawDefault; Color32 color32(color); if (duration > 0) @@ -1953,6 +1970,7 @@ namespace void DrawCylinder(Array* list, const Vector3& position, const Quaternion& orientation, float radius, float height, const Color& color, float duration) { // Setup cache + PROFILE_MEM(EngineDebug); Float3 CylinderCache[DEBUG_DRAW_CYLINDER_VERTICES]; const float angleBetweenFacets = TWO_PI / DEBUG_DRAW_CYLINDER_RESOLUTION; const float verticalOffset = height * 0.5f; @@ -2024,6 +2042,7 @@ namespace void DrawCone(Array* list, const Vector3& position, const Quaternion& orientation, float radius, float angleXY, float angleXZ, const Color& color, float duration) { + PROFILE_MEM(EngineDebug); const float tolerance = 0.001f; const float angle1 = Math::Clamp(angleXY, tolerance, PI - tolerance); const float angle2 = Math::Clamp(angleXZ, tolerance, PI - tolerance); @@ -2113,6 +2132,7 @@ void DebugDraw::DrawArc(const Vector3& position, const Quaternion& orientation, { if (angle <= 0) return; + PROFILE_MEM(EngineDebug); if (angle > TWO_PI) angle = TWO_PI; Array* list; @@ -2145,6 +2165,7 @@ void DebugDraw::DrawWireArc(const Vector3& position, const Quaternion& orientati { if (angle <= 0) return; + PROFILE_MEM(EngineDebug); if (angle > TWO_PI) angle = TWO_PI; const int32 resolution = Math::CeilToInt((float)DEBUG_DRAW_CONE_RESOLUTION / TWO_PI * angle); @@ -2211,6 +2232,7 @@ void DebugDraw::DrawBox(const BoundingBox& box, const Color& color, float durati list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultTriangles : &Context->DebugDrawDepthTest.OneFrameTriangles; else list = duration > 0 ? &Context->DebugDrawDefault.DefaultTriangles : &Context->DebugDrawDefault.OneFrameTriangles; + PROFILE_MEM(EngineDebug); list->EnsureCapacity(list->Count() + 36); for (int i0 = 0; i0 < 36;) { @@ -2239,6 +2261,7 @@ void DebugDraw::DrawBox(const OrientedBoundingBox& box, const Color& color, floa list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultTriangles : &Context->DebugDrawDepthTest.OneFrameTriangles; else list = duration > 0 ? &Context->DebugDrawDefault.DefaultTriangles : &Context->DebugDrawDefault.OneFrameTriangles; + PROFILE_MEM(EngineDebug); list->EnsureCapacity(list->Count() + 36); for (int i0 = 0; i0 < 36;) { @@ -2254,6 +2277,7 @@ void DebugDraw::DrawText(const StringView& text, const Float2& position, const C { if (text.Length() == 0 || size < 4) return; + PROFILE_MEM(EngineDebug); Array* list = duration > 0 ? &Context->DebugDrawDefault.DefaultText2D : &Context->DebugDrawDefault.OneFrameText2D; auto& t = list->AddOne(); t.Text.Resize(text.Length() + 1); @@ -2269,6 +2293,7 @@ void DebugDraw::DrawText(const StringView& text, const Vector3& position, const { if (text.Length() == 0 || size < 4) return; + PROFILE_MEM(EngineDebug); Array* list = duration > 0 ? &Context->DebugDrawDefault.DefaultText3D : &Context->DebugDrawDefault.OneFrameText3D; auto& t = list->AddOne(); t.Text.Resize(text.Length() + 1); @@ -2286,6 +2311,7 @@ void DebugDraw::DrawText(const StringView& text, const Transform& transform, con { if (text.Length() == 0 || size < 4) return; + PROFILE_MEM(EngineDebug); Array* list = duration > 0 ? &Context->DebugDrawDefault.DefaultText3D : &Context->DebugDrawDefault.OneFrameText3D; auto& t = list->AddOne(); t.Text.Resize(text.Length() + 1); diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index ddc3468f7..f8b9c7b0f 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -44,20 +44,39 @@ void Foliage::AddToCluster(ChunkedArray ZeroTolerance); ASSERT(cluster->Bounds.Intersects(instance.Bounds)); - // Find target cluster - while (cluster->Children[0]) + // Minor clusters don't use bounds intersection but try to find the first free cluster instead + if (cluster->IsMinor) { + // Insert into the first non-full child cluster or subdivide 1st child +#define CHECK_CHILD(idx) \ + if (cluster->Children[idx]->Instances.Count() < FOLIAGE_CLUSTER_CAPACITY) \ + { \ + cluster->Children[idx]->Instances.Add(&instance); \ + return; \ + } + CHECK_CHILD(3); + CHECK_CHILD(2); + CHECK_CHILD(1); + cluster = cluster->Children[0]; +#undef CHECK_CHILD + } + else + { + // Find target cluster + while (cluster->Children[0]) + { #define CHECK_CHILD(idx) \ if (cluster->Children[idx]->Bounds.Intersects(instance.Bounds)) \ { \ cluster = cluster->Children[idx]; \ continue; \ } - CHECK_CHILD(0); - CHECK_CHILD(1); - CHECK_CHILD(2); - CHECK_CHILD(3); + CHECK_CHILD(0); + CHECK_CHILD(1); + CHECK_CHILD(2); + CHECK_CHILD(3); #undef CHECK_CHILD + } } // Check if it's not full @@ -79,11 +98,20 @@ void Foliage::AddToCluster(ChunkedArrayBounds.Minimum; const Vector3 max = cluster->Bounds.Maximum; - const Vector3 size = cluster->Bounds.GetSize(); + const Vector3 size = max - min; cluster->Children[0]->Init(BoundingBox(min, min + size * Vector3(0.5f, 1.0f, 0.5f))); cluster->Children[1]->Init(BoundingBox(min + size * Vector3(0.5f, 0.0f, 0.5f), max)); cluster->Children[2]->Init(BoundingBox(min + size * Vector3(0.5f, 0.0f, 0.0f), min + size * Vector3(1.0f, 1.0f, 0.5f))); cluster->Children[3]->Init(BoundingBox(min + size * Vector3(0.0f, 0.0f, 0.5f), min + size * Vector3(0.5f, 1.0f, 1.0f))); + if (cluster->IsMinor || size.MinValue() < 1.0f) + { + // Mark children as minor to avoid infinite subdivision + cluster->IsMinor = true; + cluster->Children[0]->IsMinor = true; + cluster->Children[1]->IsMinor = true; + cluster->Children[2]->IsMinor = true; + cluster->Children[3]->IsMinor = true; + } // Move instances to a proper cells for (int32 i = 0; i < cluster->Instances.Count(); i++) diff --git a/Source/Engine/Foliage/FoliageCluster.cpp b/Source/Engine/Foliage/FoliageCluster.cpp index 1f76e5086..fd4c0f753 100644 --- a/Source/Engine/Foliage/FoliageCluster.cpp +++ b/Source/Engine/Foliage/FoliageCluster.cpp @@ -9,6 +9,7 @@ void FoliageCluster::Init(const BoundingBox& bounds) Bounds = bounds; TotalBounds = bounds; MaxCullDistance = 0.0f; + IsMinor = false; Children[0] = nullptr; Children[1] = nullptr; diff --git a/Source/Engine/Foliage/FoliageCluster.h b/Source/Engine/Foliage/FoliageCluster.h index 55cbeb027..c55305c5d 100644 --- a/Source/Engine/Foliage/FoliageCluster.h +++ b/Source/Engine/Foliage/FoliageCluster.h @@ -33,6 +33,11 @@ public: /// float MaxCullDistance; + /// + /// Flag used by clusters that are not typical quad-tree nodes but have no volume (eg. lots of instances placed on top of each other). + /// + int32 IsMinor : 1; + /// /// The child clusters. If any element is valid then all are created. /// diff --git a/Source/Engine/Graphics/Enums.h b/Source/Engine/Graphics/Enums.h index 107fe3533..64e49e18c 100644 --- a/Source/Engine/Graphics/Enums.h +++ b/Source/Engine/Graphics/Enums.h @@ -1094,6 +1094,11 @@ API_ENUM(Attributes="Flags") enum class ViewFlags : uint64 /// Default flags for materials/models previews generating. /// DefaultAssetPreview = Reflections | Decals | DirectionalLights | PointLights | SpotLights | SkyLights | SpecularLight | AntiAliasing | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | Sky | Particles, + + /// + /// All flags enabled. + /// + All = None | DebugDraw | EditorSprites | Reflections | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | Decals | DepthOfField | PhysicsDebug | Fog | MotionBlur | ContactShadows | GlobalSDF | Sky | LightsDebug | Particles, }; DECLARE_ENUM_OPERATORS(ViewFlags); diff --git a/Source/Engine/Graphics/Models/CollisionProxy.h b/Source/Engine/Graphics/Models/CollisionProxy.h index 5dc021867..2ecdce756 100644 --- a/Source/Engine/Graphics/Models/CollisionProxy.h +++ b/Source/Engine/Graphics/Models/CollisionProxy.h @@ -6,7 +6,9 @@ #include "Engine/Core/Math/Transform.h" #include "Engine/Core/Math/Ray.h" #include "Engine/Core/Math/CollisionsHelper.h" +#include "Engine/Core/Math/Half.h" #include "Engine/Core/Collections/Array.h" +#include "Engine/Graphics/PixelFormat.h" /// /// Helper container used for detailed triangle mesh intersections tests. @@ -31,23 +33,38 @@ public: } template - void Init(uint32 vertices, uint32 triangles, const Float3* positions, const IndexType* indices, uint32 positionsStride = sizeof(Float3)) + void Init(uint32 vertices, uint32 triangles, const Float3* positions, const IndexType* indices, uint32 positionsStride = sizeof(Float3), PixelFormat positionsFormat = PixelFormat::R32G32B32_Float) { Triangles.Clear(); Triangles.EnsureCapacity(triangles, false); const IndexType* it = indices; - for (uint32 i = 0; i < triangles; i++) +#define LOOP_BEGIN() \ + for (uint32 i = 0; i < triangles; i++) \ + { \ + const IndexType i0 = *(it++); \ + const IndexType i1 = *(it++); \ + const IndexType i2 = *(it++); \ + if (i0 < vertices && i1 < vertices && i2 < vertices) \ { - const IndexType i0 = *(it++); - const IndexType i1 = *(it++); - const IndexType i2 = *(it++); - if (i0 < vertices && i1 < vertices && i2 < vertices) - { +#define LOOP_END() } } + if (positionsFormat == PixelFormat::R32G32B32_Float) + { + LOOP_BEGIN() #define GET_POS(idx) *(const Float3*)((const byte*)positions + positionsStride * idx) Triangles.Add({ GET_POS(i0), GET_POS(i1), GET_POS(i2) }); #undef GET_POS - } + LOOP_END() } + else if (positionsFormat == PixelFormat::R16G16B16A16_Float) + { + LOOP_BEGIN() +#define GET_POS(idx) ((const Half4*)((const byte*)positions + positionsStride * idx))->ToFloat3() + Triangles.Add({ GET_POS(i0), GET_POS(i1), GET_POS(i2) }); +#undef GET_POS + LOOP_END() + } +#undef LOOP_BEGIN +#undef LOOP_END } void Clear() diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs index 228e43a8c..29aa86c18 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.cs +++ b/Source/Engine/Graphics/Models/MeshAccessor.cs @@ -265,6 +265,39 @@ namespace FlaxEngine } } + /// + /// Copies the contents of the input into the elements of this stream. + /// + /// The source . + public void Set(Span src) + { + if (IsLinear(PixelFormat.R32_UInt)) + { + src.CopyTo(MemoryMarshal.Cast(_data)); + } + else if (IsLinear(PixelFormat.R16_UInt)) + { + var count = Count; + fixed (byte* data = _data) + { + for (int i = 0; i < count; i++) + ((ushort*)data)[i] = (ushort)src[i]; + } + } + else + { + var count = Count; + fixed (byte* data = _data) + { + for (int i = 0; i < count; i++) + { + var v = new Float4(src[i]); + _sampler.Write(data + i * _stride, ref v); + } + } + } + } + /// /// Copies the contents of this stream into a destination . /// @@ -281,9 +314,7 @@ namespace FlaxEngine fixed (byte* data = _data) { for (int i = 0; i < count; i++) - { dst[i] = new Float2(_sampler.Read(data + i * _stride)); - } } } } @@ -304,9 +335,7 @@ namespace FlaxEngine fixed (byte* data = _data) { for (int i = 0; i < count; i++) - { dst[i] = new Float3(_sampler.Read(data + i * _stride)); - } } } } @@ -327,9 +356,37 @@ namespace FlaxEngine fixed (byte* data = _data) { for (int i = 0; i < count; i++) - { dst[i] = (Color)_sampler.Read(data + i * _stride); - } + } + } + } + + /// + /// Copies the contents of this stream into a destination . + /// + /// The destination . + public void CopyTo(Span dst) + { + if (IsLinear(PixelFormat.R32_UInt)) + { + _data.CopyTo(MemoryMarshal.Cast(dst)); + } + else if (IsLinear(PixelFormat.R16_UInt)) + { + var count = Count; + fixed (byte* data = _data) + { + for (int i = 0; i < count; i++) + dst[i] = ((ushort*)data)[i]; + } + } + else + { + var count = Count; + fixed (byte* data = _data) + { + for (int i = 0; i < count; i++) + dst[i] = (uint)_sampler.Read(data + i * _stride).X; } } } @@ -619,6 +676,16 @@ namespace FlaxEngine return Attribute((VertexElement.Types)((byte)VertexElement.Types.TexCoord0 + channel)); } + /// + /// Gets or sets the index buffer with triangle indices. + /// + /// Uses stream to read or write data to the index buffer. + public uint[] Triangles + { + get => GetStreamUInt(Index()); + set => SetStreamUInt(Index(), value); + } + /// /// Gets or sets the vertex positions. Null if does not exist in vertex buffers of the mesh. /// @@ -659,6 +726,25 @@ namespace FlaxEngine set => SetStreamFloat2(VertexElement.Types.TexCoord, value); } + private uint[] GetStreamUInt(Stream stream) + { + uint[] result = null; + if (stream.IsValid) + { + result = new uint[stream.Count]; + stream.CopyTo(result); + } + return result; + } + + private void SetStreamUInt(Stream stream, uint[] value) + { + if (stream.IsValid) + { + stream.Set(value); + } + } + private delegate void TransformDelegate3(ref Float3 value); private Float3[] GetStreamFloat3(VertexElement.Types attribute, TransformDelegate3 transform = null) diff --git a/Source/Engine/Graphics/Models/MeshBase.cpp b/Source/Engine/Graphics/Models/MeshBase.cpp index 3434cd91a..eadbfcba9 100644 --- a/Source/Engine/Graphics/Models/MeshBase.cpp +++ b/Source/Engine/Graphics/Models/MeshBase.cpp @@ -441,6 +441,9 @@ bool MeshBase::Init(uint32 vertices, uint32 triangles, const ArrayFindElement(VertexElement::Types::Position); if (use16BitIndexBuffer) - _collisionProxy.Init(vertices, triangles, (const Float3*)vbData[0], (const uint16*)ibData); + _collisionProxy.Init(vertices, triangles, (const Float3*)vbData[0], (const uint16*)ibData, vertexBuffer0->GetStride(), positionsElement.Format); else - _collisionProxy.Init(vertices, triangles, (const Float3*)vbData[0], (const uint32*)ibData); + _collisionProxy.Init(vertices, triangles, (const Float3*)vbData[0], (const uint32*)ibData, vertexBuffer0->GetStride(), positionsElement.Format); #endif // Free old buffers diff --git a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp index 23382673f..05c6d605a 100644 --- a/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp +++ b/Source/Engine/Graphics/Shaders/GPUVertexLayout.cpp @@ -59,7 +59,7 @@ namespace elements.Get()[j].Slot = (byte)slot; } } - GPUVertexLayout* result = anyValid ? GPUVertexLayout::Get(elements) : nullptr; + GPUVertexLayout* result = anyValid ? GPUVertexLayout::Get(elements, true) : nullptr; VertexBufferCache.Add(key, result); return result; } @@ -97,6 +97,7 @@ GPUVertexLayout::GPUVertexLayout() void GPUVertexLayout::SetElements(const Elements& elements, bool explicitOffsets) { uint32 offsets[GPU_MAX_VB_BINDED + 1] = {}; + uint32 maxOffset[GPU_MAX_VB_BINDED + 1] = {}; _elements = elements; for (int32 i = 0; i < _elements.Count(); i++) { @@ -108,9 +109,10 @@ void GPUVertexLayout::SetElements(const Elements& elements, bool explicitOffsets else e.Offset = (byte)offset; offset += PixelFormatExtensions::SizeInBytes(e.Format); + maxOffset[e.Slot] = Math::Max(maxOffset[e.Slot], offset); } _stride = 0; - for (uint32 offset : offsets) + for (uint32 offset : maxOffset) _stride += offset; } @@ -139,7 +141,7 @@ VertexElement GPUVertexLayout::FindElement(VertexElement::Types type) const GPUVertexLayout* GPUVertexLayout::Get(const Elements& elements, bool explicitOffsets) { // Hash input layout - uint32 hash = 0; + uint32 hash = explicitOffsets ? 131 : 0; for (const VertexElement& element : elements) { CombineHash(hash, GetHash(element)); diff --git a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderProgramDX11.h b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderProgramDX11.h index 319e1a939..567cbb618 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderProgramDX11.h +++ b/Source/Engine/GraphicsDevice/DirectX/DX11/GPUShaderProgramDX11.h @@ -4,6 +4,7 @@ #include "Engine/Graphics/Shaders/GPUShaderProgram.h" #include "Engine/Core/Types/DataContainer.h" +#include "Engine/Core/Collections/Dictionary.h" #include "../IncludeDirectXHeaders.h" #if GRAPHICS_API_DIRECTX11 diff --git a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp index 98143c7c3..4dc923234 100644 --- a/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp +++ b/Source/Engine/GraphicsDevice/DirectX/DX12/GPUContextDX12.cpp @@ -3,16 +3,9 @@ #if GRAPHICS_API_DIRECTX12 #include "Engine/Graphics/Config.h" +#include "Engine/Platform/Platform.h" +#include "../IncludeDirectXHeaders.h" #if USE_PIX && GPU_ALLOW_PROFILE_EVENTS -// Include these header files before pix3 -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#define NOGDI -#define NODRAWTEXT -//#define NOCTLMGR -#define NOFLATSBAPIS -#include -#include #include #endif #include "GPUContextDX12.h" diff --git a/Source/Engine/Level/Level.h b/Source/Engine/Level/Level.h index 110b9ac61..e3d43b7c5 100644 --- a/Source/Engine/Level/Level.h +++ b/Source/Engine/Level/Level.h @@ -473,6 +473,19 @@ public: /// Found actors list. API_FUNCTION() static Array GetActors(API_PARAM(Attributes="TypeReference(typeof(Actor))") const MClass* type, bool activeOnly = false); + /// + /// Finds all the actors of the given type in all the loaded scenes. + /// + /// Type of the object. + /// Finds only active actors. + /// Found actors list. + template + static Array GetActors(bool activeOnly = false) + { + Array actors = GetActors(T::GetStaticClass(), activeOnly); + return *(Array*)&actors; + } + /// /// Finds all the scripts of the given type in an actor or all the loaded scenes. /// diff --git a/Source/Engine/Level/MeshReference.cs b/Source/Engine/Level/MeshReference.cs index 14ef0c72b..cb87e9769 100644 --- a/Source/Engine/Level/MeshReference.cs +++ b/Source/Engine/Level/MeshReference.cs @@ -13,7 +13,7 @@ namespace FlaxEngine public bool ValueEquals(object other) { var o = (MeshReference)other; - return JsonSerializer.ValueEquals(Actor, o.Actor) && + return JsonSerializer.SceneObjectEquals(Actor, o.Actor) && LODIndex == o.LODIndex && MeshIndex == o.MeshIndex; } diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp index 0bed02f0b..378b706ed 100644 --- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp @@ -227,9 +227,9 @@ public: void PrefabInstanceData::CollectPrefabInstances(PrefabInstancesData& prefabInstancesData, const Guid& prefabId, Actor* defaultInstance, Actor* targetActor) { ScopeLock lock(PrefabManager::PrefabsReferencesLocker); - if (PrefabManager::PrefabsReferences.ContainsKey(prefabId)) + if (auto instancesPtr = PrefabManager::PrefabsReferences.TryGet(prefabId)) { - auto& instances = PrefabManager::PrefabsReferences[prefabId]; + auto& instances = *instancesPtr; int32 usedCount = 0; for (int32 instanceIndex = 0; instanceIndex < instances.Count(); instanceIndex++) { diff --git a/Source/Engine/Navigation/NavMesh.cpp b/Source/Engine/Navigation/NavMesh.cpp index 5593d732a..5932607a0 100644 --- a/Source/Engine/Navigation/NavMesh.cpp +++ b/Source/Engine/Navigation/NavMesh.cpp @@ -25,6 +25,7 @@ NavMesh::NavMesh(const SpawnParams& params) void NavMesh::SaveNavMesh() { #if COMPILE_WITH_ASSETS_IMPORTER + PROFILE_MEM(NavigationMesh); // Skip if scene is missing const auto scene = GetScene(); @@ -111,7 +112,7 @@ void NavMesh::OnAssetLoaded(Asset* asset, void* caller) if (Data.Tiles.HasItems()) return; ScopeLock lock(DataAsset->Locker); - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationMesh); // Remove added tiles if (_navMeshActive) diff --git a/Source/Engine/Navigation/NavMeshBoundsVolume.cpp b/Source/Engine/Navigation/NavMeshBoundsVolume.cpp index 56351fded..c54f2f072 100644 --- a/Source/Engine/Navigation/NavMeshBoundsVolume.cpp +++ b/Source/Engine/Navigation/NavMeshBoundsVolume.cpp @@ -6,7 +6,7 @@ #if USE_EDITOR #include "Editor/Editor.h" #include "Editor/Managed/ManagedEditor.h" -#include "NavMeshBuilder.h" +#include "Navigation.h" #endif NavMeshBoundsVolume::NavMeshBoundsVolume(const SpawnParams& params) @@ -55,9 +55,30 @@ void NavMeshBoundsVolume::OnBoundsChanged(const BoundingBox& prevBounds) // Auto-rebuild modified navmesh area if (IsDuringPlay() && IsActiveInHierarchy() && !Editor::IsPlayMode && Editor::Managed->CanAutoBuildNavMesh()) { - BoundingBox dirtyBounds; - BoundingBox::Merge(prevBounds, _box, dirtyBounds); - NavMeshBuilder::Build(GetScene(), dirtyBounds, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs); + if (_box.Intersects(prevBounds)) + { + // Bounds were moved a bit so merge into a single request (for performance reasons) + BoundingBox dirtyBounds; + BoundingBox::Merge(prevBounds, _box, dirtyBounds); + Navigation::BuildNavMesh(dirtyBounds, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs); + } + else + { + // Dirty each bounds in separate + Navigation::BuildNavMesh(prevBounds, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs); + Navigation::BuildNavMesh(_box, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs); + } + } +} + +void NavMeshBoundsVolume::OnActiveInTreeChanged() +{ + BoxVolume::OnActiveInTreeChanged(); + + // Auto-rebuild + if (IsDuringPlay() && !Editor::IsPlayMode && Editor::Managed->CanAutoBuildNavMesh()) + { + Navigation::BuildNavMesh(_box, ManagedEditor::ManagedEditorOptions.AutoRebuildNavMeshTimeoutMs); } } diff --git a/Source/Engine/Navigation/NavMeshBoundsVolume.h b/Source/Engine/Navigation/NavMeshBoundsVolume.h index c04bc0483..80df5035a 100644 --- a/Source/Engine/Navigation/NavMeshBoundsVolume.h +++ b/Source/Engine/Navigation/NavMeshBoundsVolume.h @@ -30,6 +30,7 @@ protected: void OnDisable() override; #if USE_EDITOR void OnBoundsChanged(const BoundingBox& prevBounds) override; + void OnActiveInTreeChanged() override; Color GetWiresColor() override; #endif }; diff --git a/Source/Engine/Navigation/NavMeshBuilder.cpp b/Source/Engine/Navigation/NavMeshBuilder.cpp index e92173846..896cf4217 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.cpp +++ b/Source/Engine/Navigation/NavMeshBuilder.cpp @@ -3,6 +3,7 @@ #if COMPILE_WITH_NAV_MESH_BUILDER #include "NavMeshBuilder.h" +#include "Navigation.h" #include "NavMesh.h" #include "NavigationSettings.h" #include "NavMeshBoundsVolume.h" @@ -706,6 +707,7 @@ struct BuildRequest ScriptingObjectReference Scene; DateTime Time; BoundingBox DirtyBounds; + bool SpecificScene; }; CriticalSection NavBuildQueueLocker; @@ -713,6 +715,7 @@ Array NavBuildQueue; CriticalSection NavBuildTasksLocker; int32 NavBuildTasksMaxCount = 0; +bool NavBuildCheckMissingNavMeshes = false; Array NavBuildTasks; class NavMeshTileBuildTask : public ThreadPoolTask @@ -733,7 +736,7 @@ public: bool Run() override { PROFILE_CPU_NAMED("BuildNavMeshTile"); - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationBuilding); const auto navMesh = NavMesh.Get(); if (!navMesh) return false; @@ -776,13 +779,13 @@ void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime) NavBuildTasksLocker.Unlock(); } -void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime, int32 x, int32 y) +void CancelNavMeshTileBuildTasks(NavMeshRuntime* runtime, int32 x, int32 y, NavMesh* navMesh) { NavBuildTasksLocker.Lock(); for (int32 i = 0; i < NavBuildTasks.Count(); i++) { auto task = NavBuildTasks[i]; - if (task->Runtime == runtime && task->X == x && task->Y == y) + if (task->Runtime == runtime && task->X == x && task->Y == y && task->NavMesh == navMesh) { NavBuildTasksLocker.Unlock(); @@ -838,7 +841,7 @@ void NavMeshBuilder::Init() Level::SceneUnloading.Bind(); } -bool NavMeshBuilder::IsBuildingNavMesh() +bool Navigation::IsBuildingNavMesh() { NavBuildTasksLocker.Lock(); const bool hasAnyTask = NavBuildTasks.HasItems(); @@ -847,7 +850,7 @@ bool NavMeshBuilder::IsBuildingNavMesh() return hasAnyTask; } -float NavMeshBuilder::GetNavMeshBuildingProgress() +float Navigation::GetNavMeshBuildingProgress() { NavBuildTasksLocker.Lock(); float result = 1.0f; @@ -907,15 +910,13 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo // Align dirty bounds to tile size BoundingBox dirtyBoundsNavMesh; BoundingBox::Transform(dirtyBounds, worldToNavMesh, dirtyBoundsNavMesh); - BoundingBox dirtyBoundsAligned; - dirtyBoundsAligned.Minimum = Float3::Floor(dirtyBoundsNavMesh.Minimum / tileSize) * tileSize; - dirtyBoundsAligned.Maximum = Float3::Ceil(dirtyBoundsNavMesh.Maximum / tileSize) * tileSize; + dirtyBoundsNavMesh.Minimum = Float3::Floor(dirtyBoundsNavMesh.Minimum / tileSize) * tileSize; + dirtyBoundsNavMesh.Maximum = Float3::Ceil(dirtyBoundsNavMesh.Maximum / tileSize) * tileSize; // Calculate tiles range for the given navigation dirty bounds (aligned to tiles size) - const Int3 tilesMin(dirtyBoundsAligned.Minimum / tileSize); - const Int3 tilesMax(dirtyBoundsAligned.Maximum / tileSize); - const int32 tilesX = tilesMax.X - tilesMin.X; - const int32 tilesY = tilesMax.Z - tilesMin.Z; + const Int3 tilesMin(dirtyBoundsNavMesh.Minimum / tileSize); + const Int3 tilesMax(dirtyBoundsNavMesh.Maximum / tileSize); + const int32 tilesXZ = (tilesMax.X - tilesMin.X) * (tilesMax.Z - tilesMin.Z); { PROFILE_CPU_NAMED("Prepare"); @@ -932,18 +933,18 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo // Remove all tiles from navmesh runtime runtime->RemoveTiles(navMesh); runtime->SetTileSize(tileSize); - runtime->EnsureCapacity(tilesX * tilesY); + runtime->EnsureCapacity(tilesXZ); // Remove all tiles from navmesh data navMesh->Data.TileSize = tileSize; navMesh->Data.Tiles.Clear(); - navMesh->Data.Tiles.EnsureCapacity(tilesX * tilesX); + navMesh->Data.Tiles.EnsureCapacity(tilesXZ); navMesh->IsDataDirty = true; } else { // Ensure to have enough memory for tiles - runtime->EnsureCapacity(tilesX * tilesY); + runtime->EnsureCapacity(tilesXZ); } runtime->Locker.Unlock(); @@ -959,11 +960,10 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo // Cache navmesh volumes Array> volumes; - for (int32 i = 0; i < scene->Navigation.Volumes.Count(); i++) + for (const NavMeshBoundsVolume* volume : scene->Navigation.Volumes) { - const auto volume = scene->Navigation.Volumes.Get()[i]; if (!volume->AgentsMask.IsNavMeshSupported(navMesh->Properties) || - !volume->GetBox().Intersects(dirtyBoundsAligned)) + !volume->GetBox().Intersects(dirtyBoundsNavMesh)) continue; auto& bounds = volumes.AddOne(); BoundingBox::Transform(volume->GetBox(), worldToNavMesh, bounds); @@ -1026,7 +1026,7 @@ void BuildDirtyBounds(Scene* scene, NavMesh* navMesh, const BoundingBox& dirtyBo for (const auto& tile : unusedTiles) { // Wait for any async tasks that are producing this tile - CancelNavMeshTileBuildTasks(runtime, tile.X, tile.Y); + CancelNavMeshTileBuildTasks(runtime, tile.X, tile.Y, navMesh); } runtime->Locker.Lock(); for (const auto& tile : unusedTiles) @@ -1095,6 +1095,7 @@ void BuildDirtyBounds(Scene* scene, const BoundingBox& dirtyBounds, bool rebuild else if (settings->AutoAddMissingNavMeshes) { // Spawn missing navmesh + PROFILE_MEM(Navigation); navMesh = New(); navMesh->SetStaticFlags(StaticFlags::FullyStatic); navMesh->SetName(TEXT("NavMesh.") + navMeshProperties.Name); @@ -1108,39 +1109,6 @@ void BuildDirtyBounds(Scene* scene, const BoundingBox& dirtyBounds, bool rebuild { BuildDirtyBounds(scene, navMesh, dirtyBounds, rebuild); } - - // Remove unused navmeshes - if (settings->AutoRemoveMissingNavMeshes) - { - for (NavMesh* navMesh : scene->Navigation.Meshes) - { - // Skip used navmeshes - if (navMesh->Data.Tiles.HasItems()) - continue; - - // Skip navmeshes during async building - int32 usageCount = 0; - NavBuildTasksLocker.Lock(); - for (int32 i = 0; i < NavBuildTasks.Count(); i++) - { - if (NavBuildTasks.Get()[i]->NavMesh == navMesh) - usageCount++; - } - NavBuildTasksLocker.Unlock(); - if (usageCount != 0) - continue; - - navMesh->DeleteObject(); - } - } -} - -void BuildWholeScene(Scene* scene) -{ - // Compute total navigation area bounds - const BoundingBox worldBounds = scene->Navigation.GetNavigationBounds(); - - BuildDirtyBounds(scene, worldBounds, true); } void ClearNavigation(Scene* scene) @@ -1154,22 +1122,58 @@ void ClearNavigation(Scene* scene) } } +void BuildNavigation(BuildRequest& request) +{ + // If scene is not specified then build all loaded scenes + if (!request.Scene) + { + for (Scene* scene : Level::Scenes) + { + request.Scene = scene; + BuildNavigation(request); + } + return; + } + + // Early out if scene is not using navigation + if (request.Scene->Navigation.Volumes.IsEmpty()) + { + ClearNavigation(request.Scene); + return; + } + + // Check if similar request is already in a queue + for (auto& e : NavBuildQueue) + { + if (e.Scene == request.Scene && (e.DirtyBounds == request.DirtyBounds || request.DirtyBounds == BoundingBox::Empty)) + { + e = request; + return; + } + } + + // Enqueue request + NavBuildQueue.Add(request); +} + void NavMeshBuilder::Update() { - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationBuilding); ScopeLock lock(NavBuildQueueLocker); // Process nav mesh building requests and kick the tasks const auto now = DateTime::NowUTC(); + bool didRebuild = false; for (int32 i = 0; NavBuildQueue.HasItems() && i < NavBuildQueue.Count(); i++) { auto req = NavBuildQueue.Get()[i]; if (now - req.Time >= 0) { NavBuildQueue.RemoveAt(i--); - const auto scene = req.Scene.Get(); + Scene* scene = req.Scene.Get(); if (!scene) continue; + bool rebuild = req.DirtyBounds == BoundingBox::Empty; // Early out if scene has no bounds volumes to define nav mesh area if (scene->Navigation.Volumes.IsEmpty()) @@ -1179,80 +1183,69 @@ void NavMeshBuilder::Update() } // Check if build a custom dirty bounds or whole scene - if (req.DirtyBounds == BoundingBox::Empty) - { - BuildWholeScene(scene); - } + if (rebuild) + req.DirtyBounds = scene->Navigation.GetNavigationBounds(); // Compute total navigation area bounds + if (didRebuild) + rebuild = false; // When rebuilding navmesh for multiple scenes, rebuild only the first one (other scenes will use additive update) else + didRebuild = true; + BuildDirtyBounds(scene, req.DirtyBounds, rebuild); + NavBuildCheckMissingNavMeshes = true; + } + } + + // Remove unused navmeshes (when all active tasks are done) + // TODO: ignore AutoRemoveMissingNavMeshes in game and make it editor-only? + if (NavBuildCheckMissingNavMeshes && NavBuildTasksMaxCount == 0 && NavigationSettings::Get()->AutoRemoveMissingNavMeshes) + { + NavBuildCheckMissingNavMeshes = false; + NavBuildTasksLocker.Lock(); + int32 taskCount = NavBuildTasks.Count(); + NavBuildTasksLocker.Unlock(); + if (taskCount == 0) + { + for (Scene* scene : Level::Scenes) { - BuildDirtyBounds(scene, req.DirtyBounds, false); + for (NavMesh* navMesh : scene->Navigation.Meshes) + { + if (!navMesh->Data.Tiles.HasItems()) + { + navMesh->DeleteObject(); + } + } } } } } -void NavMeshBuilder::Build(Scene* scene, float timeoutMs) +void Navigation::BuildNavMesh(Scene* scene, float timeoutMs) { - if (!scene) - { - LOG(Warning, "Could not generate navmesh without scene."); - return; - } - - // Early out if scene is not using navigation - if (scene->Navigation.Volumes.IsEmpty()) - { - ClearNavigation(scene); - return; - } - - PROFILE_CPU_NAMED("NavMeshBuilder"); - PROFILE_MEM(Navigation); + PROFILE_CPU(); + PROFILE_MEM(NavigationBuilding); ScopeLock lock(NavBuildQueueLocker); BuildRequest req; req.Scene = scene; req.Time = DateTime::NowUTC() + TimeSpan::FromMilliseconds(timeoutMs); req.DirtyBounds = BoundingBox::Empty; - - for (int32 i = 0; i < NavBuildQueue.Count(); i++) - { - auto& e = NavBuildQueue.Get()[i]; - if (e.Scene == scene && e.DirtyBounds == req.DirtyBounds) - { - e = req; - return; - } - } - - NavBuildQueue.Add(req); + req.SpecificScene = scene != nullptr; + BuildNavigation(req); } -void NavMeshBuilder::Build(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs) +void Navigation::BuildNavMesh(const BoundingBox& dirtyBounds, Scene* scene, float timeoutMs) { - if (!scene) - { - LOG(Warning, "Could not generate navmesh without scene."); - return; - } - - // Early out if scene is not using navigation - if (scene->Navigation.Volumes.IsEmpty()) - { - ClearNavigation(scene); - return; - } - - PROFILE_CPU_NAMED("NavMeshBuilder"); - PROFILE_MEM(Navigation); + if (dirtyBounds.GetVolume() <= ZeroTolerance) + return; // Skip updating empty bounds + PROFILE_CPU(); + PROFILE_MEM(NavigationBuilding); ScopeLock lock(NavBuildQueueLocker); BuildRequest req; req.Scene = scene; req.Time = DateTime::NowUTC() + TimeSpan::FromMilliseconds(timeoutMs); req.DirtyBounds = dirtyBounds; - - NavBuildQueue.Add(req); + req.SpecificScene = scene != nullptr; + BuildNavigation(req); } #endif diff --git a/Source/Engine/Navigation/NavMeshBuilder.h b/Source/Engine/Navigation/NavMeshBuilder.h index a3477db27..355bac7de 100644 --- a/Source/Engine/Navigation/NavMeshBuilder.h +++ b/Source/Engine/Navigation/NavMeshBuilder.h @@ -15,11 +15,7 @@ class FLAXENGINE_API NavMeshBuilder { public: static void Init(); - static bool IsBuildingNavMesh(); - static float GetNavMeshBuildingProgress(); static void Update(); - static void Build(Scene* scene, float timeoutMs); - static void Build(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs); }; #endif diff --git a/Source/Engine/Navigation/NavMeshRuntime.cpp b/Source/Engine/Navigation/NavMeshRuntime.cpp index 2758077c6..0ace29415 100644 --- a/Source/Engine/Navigation/NavMeshRuntime.cpp +++ b/Source/Engine/Navigation/NavMeshRuntime.cpp @@ -5,6 +5,9 @@ #include "NavMesh.h" #include "Engine/Core/Log.h" #include "Engine/Core/Random.h" +#if COMPILE_WITH_DEBUG_DRAW +#include "Engine/Level/Scene/Scene.h" +#endif #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Profiler/ProfilerMemory.h" #include "Engine/Threading/Threading.h" @@ -326,7 +329,7 @@ void NavMeshRuntime::EnsureCapacity(int32 tilesToAddCount) if (newTilesCount <= capacity) return; PROFILE_CPU_NAMED("NavMeshRuntime.EnsureCapacity"); - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationMesh); // Navmesh tiles capacity growing rule int32 newCapacity = capacity ? capacity : 32; @@ -387,7 +390,7 @@ void NavMeshRuntime::AddTiles(NavMesh* navMesh) return; auto& data = navMesh->Data; PROFILE_CPU_NAMED("NavMeshRuntime.AddTiles"); - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationMesh); ScopeLock lock(Locker); // Validate data (must match navmesh) or init navmesh to match the tiles options @@ -419,7 +422,7 @@ void NavMeshRuntime::AddTile(NavMesh* navMesh, NavMeshTileData& tileData) ASSERT(navMesh); auto& data = navMesh->Data; PROFILE_CPU_NAMED("NavMeshRuntime.AddTile"); - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationMesh); ScopeLock lock(Locker); // Validate data (must match navmesh) or init navmesh to match the tiles options @@ -603,7 +606,21 @@ void NavMeshRuntime::DebugDraw() if (!tile->header) continue; - //DebugDraw::DrawWireBox(*(BoundingBox*)&tile->header->bmin[0], Color::CadetBlue); +#if 0 + // Debug draw tile bounds and owner scene name + BoundingBox tileBounds = *(BoundingBox*)&tile->header->bmin[0]; + DebugDraw::DrawWireBox(tileBounds, Color::CadetBlue); + // TODO: build map from tile coords to tile data to avoid this loop + for (const auto& e : _tiles) + { + if (e.X == tile->header->x && e.Y == tile->header->y && e.Layer == tile->header->layer) + { + if (e.NavMesh && e.NavMesh->GetScene()) + DebugDraw::DrawText(e.NavMesh->GetScene()->GetName(), tileBounds.Minimum + tileBounds.GetSize() * Float3(0.5f, 0.8f, 0.5f), Color::CadetBlue); + break; + } + } +#endif for (int i = 0; i < tile->header->polyCount; i++) { diff --git a/Source/Engine/Navigation/NavMeshRuntime.h b/Source/Engine/Navigation/NavMeshRuntime.h index 1ca6607b9..9e1165196 100644 --- a/Source/Engine/Navigation/NavMeshRuntime.h +++ b/Source/Engine/Navigation/NavMeshRuntime.h @@ -111,7 +111,7 @@ public: /// The start position. /// The result hit information. Valid only when query succeed. /// The maximum distance to search for wall (search radius). - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool FindDistanceToWall(const Vector3& startPosition, NavMeshHit& hitInfo, float maxDistance = MAX_float) const; /// @@ -187,7 +187,7 @@ public: /// The start position. /// The end position. /// The result hit information. Valid only when query succeed. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool RayCast(const Vector3& startPosition, const Vector3& endPosition, API_PARAM(Out) NavMeshHit& hitInfo) const; public: diff --git a/Source/Engine/Navigation/NavModifierVolume.cpp b/Source/Engine/Navigation/NavModifierVolume.cpp index 9e1295f70..aa71e7aa1 100644 --- a/Source/Engine/Navigation/NavModifierVolume.cpp +++ b/Source/Engine/Navigation/NavModifierVolume.cpp @@ -2,7 +2,7 @@ #include "NavModifierVolume.h" #include "NavigationSettings.h" -#include "NavMeshBuilder.h" +#include "Navigation.h" #include "Engine/Level/Scene/Scene.h" #include "Engine/Serialization/Serialization.h" #if USE_EDITOR @@ -83,7 +83,7 @@ void NavModifierVolume::OnBoundsChanged(const BoundingBox& prevBounds) #else const float timeoutMs = 0.0f; #endif - NavMeshBuilder::Build(GetScene(), dirtyBounds, timeoutMs); + Navigation::BuildNavMesh(dirtyBounds, GetScene(), timeoutMs); } #endif } diff --git a/Source/Engine/Navigation/Navigation.cpp b/Source/Engine/Navigation/Navigation.cpp index 34983652f..908819765 100644 --- a/Source/Engine/Navigation/Navigation.cpp +++ b/Source/Engine/Navigation/Navigation.cpp @@ -180,7 +180,7 @@ NavigationService NavigationServiceInstance; void* dtAllocDefault(size_t size, dtAllocHint) { - PROFILE_MEM(Navigation); + PROFILE_MEM(NavigationMesh); return Allocator::Allocate(size); } @@ -382,30 +382,6 @@ bool Navigation::RayCast(const Vector3& startPosition, const Vector3& endPositio return NavMeshes.First()->RayCast(startPosition, endPosition, hitInfo); } -#if COMPILE_WITH_NAV_MESH_BUILDER - -bool Navigation::IsBuildingNavMesh() -{ - return NavMeshBuilder::IsBuildingNavMesh(); -} - -float Navigation::GetNavMeshBuildingProgress() -{ - return NavMeshBuilder::GetNavMeshBuildingProgress(); -} - -void Navigation::BuildNavMesh(Scene* scene, float timeoutMs) -{ - NavMeshBuilder::Build(scene, timeoutMs); -} - -void Navigation::BuildNavMesh(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs) -{ - NavMeshBuilder::Build(scene, dirtyBounds, timeoutMs); -} - -#endif - #if COMPILE_WITH_DEBUG_DRAW void Navigation::DrawNavMesh() diff --git a/Source/Engine/Navigation/Navigation.h b/Source/Engine/Navigation/Navigation.h index 4d8b181e7..80c8eb84a 100644 --- a/Source/Engine/Navigation/Navigation.h +++ b/Source/Engine/Navigation/Navigation.h @@ -19,7 +19,7 @@ public: /// The start position. /// The result hit information. Valid only when query succeed. /// The maximum distance to search for wall (search radius). - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool FindDistanceToWall(const Vector3& startPosition, API_PARAM(Out) NavMeshHit& hitInfo, float maxDistance = MAX_float); /// @@ -81,12 +81,10 @@ public: /// The start position. /// The end position. /// The result hit information. Valid only when query succeed. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool RayCast(const Vector3& startPosition, const Vector3& endPosition, API_PARAM(Out) NavMeshHit& hitInfo); -public: #if COMPILE_WITH_NAV_MESH_BUILDER - /// /// Returns true if navigation system is during navmesh building (any request is valid or async task active). /// @@ -100,32 +98,49 @@ public: /// /// Builds the Nav Mesh for the given scene (discards all its tiles). /// - /// - /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls. - /// - /// The scene. + /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls. + /// The scene. Pass null to build navmesh for all loaded scenes. /// The timeout to wait before building Nav Mesh (in milliseconds). - API_FUNCTION() static void BuildNavMesh(Scene* scene, float timeoutMs = 50); + API_FUNCTION() static void BuildNavMesh(Scene* scene = nullptr, float timeoutMs = 50); /// /// Builds the Nav Mesh for the given scene (builds only the tiles overlapping the given bounding box). /// - /// - /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls. - /// + /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls. + /// The bounds in world-space to build overlapping tiles. + /// The scene. Pass null to build navmesh for all loaded scenes that intersect with a given bounds. + /// The timeout to wait before building Nav Mesh (in milliseconds). + API_FUNCTION() static void BuildNavMesh(const BoundingBox& dirtyBounds, Scene* scene = nullptr, float timeoutMs = 50); + + /// + /// Builds the Nav Mesh for all the loaded scenes (builds only the tiles overlapping the given bounding box). + /// + /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls. + /// The bounds in world-space to build overlapping tiles. + /// The timeout to wait before building Nav Mesh (in milliseconds). + API_FUNCTION() static void BuildNavMesh(const BoundingBox& dirtyBounds, float timeoutMs = 50) + { + BuildNavMesh(dirtyBounds, nullptr, timeoutMs); + } + + /// + /// Builds the Nav Mesh for the given scene (builds only the tiles overlapping the given bounding box). + /// [Deprecated in v1.12] + /// + /// Requests are enqueued till the next game scripts update. Actual navmesh building in done via Thread Pool tasks in a background to prevent game thread stalls. /// The scene. /// The bounds in world-space to build overlapping tiles. /// The timeout to wait before building Nav Mesh (in milliseconds). - API_FUNCTION() static void BuildNavMesh(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs = 50); - + API_FUNCTION() DEPRECATED("Use BuildNavMesh with reordered arguments instead") static void BuildNavMesh(Scene* scene, const BoundingBox& dirtyBounds, float timeoutMs = 50) + { + BuildNavMesh(dirtyBounds, scene, timeoutMs); + } #endif #if COMPILE_WITH_DEBUG_DRAW - /// /// Draws the navigation for all the scenes (uses DebugDraw interface). /// static void DrawNavMesh(); - #endif }; diff --git a/Source/Engine/Particles/ParticleEffect.cpp b/Source/Engine/Particles/ParticleEffect.cpp index 9592147a7..14467664c 100644 --- a/Source/Engine/Particles/ParticleEffect.cpp +++ b/Source/Engine/Particles/ParticleEffect.cpp @@ -11,6 +11,10 @@ #include "Engine/Level/Scene/Scene.h" #include "Engine/Engine/Time.h" #include "Engine/Engine/Engine.h" +#if USE_EDITOR +#include "Editor/Editor.h" +#include "Editor/Managed/ManagedEditor.h" +#endif ParticleEffect::ParticleEffect(const SpawnParams& params) : Actor(params) @@ -465,7 +469,12 @@ void ParticleEffect::Update() if (UpdateMode == SimulationUpdateMode::FixedTimestep) { // Check if last simulation update was past enough to kick a new on - const float time = Time::Update.Time.GetTotalSeconds(); + bool useTimeScale = UseTimeScale; +#if USE_EDITOR + if (!Editor::IsPlayMode && IsDuringPlay()) + useTimeScale = false; +#endif + const float time = (useTimeScale ? Time::Update.Time : Time::Update.UnscaledTime).GetTotalSeconds(); if (time - Instance.LastUpdateTime < FixedTimestep) return; } @@ -475,9 +484,6 @@ void ParticleEffect::Update() #if USE_EDITOR -#include "Editor/Editor.h" -#include "Editor/Managed/ManagedEditor.h" - void ParticleEffect::UpdateExecuteInEditor() { // Auto-play in Editor diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp index 3edfaa0c6..8c3d3610e 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -2784,6 +2784,69 @@ float PhysicsBackend::ComputeShapeSqrDistanceToPoint(void* shape, const Vector3& { auto shapePhysX = (PxShape*)shape; const PxTransform trans(C2P(position), C2P(orientation)); + + // Special case for heightfield collider (not implemented in PhysX) + if (shapePhysX->getGeometryType() == PxGeometryType::eHEIGHTFIELD) + { + // Do a bunch of raycasts in all directions to find the closest point on the heightfield + PxVec3 origin = C2P(point); + Array unitDirections; + constexpr int32 resolution = 32; + unitDirections.EnsureCapacity((resolution + 1) * (resolution + 1)); + for (int32 i = 0; i <= resolution; i++) + { + float phi = PI * (float)i / resolution; + float sinPhi = Math::Sin(phi); + float cosPhi = Math::Cos(phi); + for (int32 j = 0; j <= resolution; j++) + { + float theta = 2.0f * PI * (float)j / resolution; + float cosTheta = Math::Cos(theta); + float sinTheta = Math::Sin(theta); + + PxVec3 v; + v.x = cosTheta * sinPhi; + v.y = cosPhi; + v.z = sinTheta * sinPhi; + + // All generated vectors are unit vectors (length 1) + unitDirections.Add(v); + } + } + + PxReal maxDistance = PX_MAX_REAL; // Search indefinitely + PxQueryFilterData filterData; + filterData.data.word0 = (PxU32)shapePhysX->getSimulationFilterData().word0; + PxHitFlags hitFlags = PxHitFlag::ePOSITION | PxHitFlag::eMESH_BOTH_SIDES; // Both sides added for if it is underneath the height field + PxRaycastBuffer buffer; + auto scene = shapePhysX->getActor()->getScene(); + + PxReal closestDistance = maxDistance; + PxVec3 tempClosestPoint; + for (PxVec3& unitDir : unitDirections) + { + bool hitResult = scene->raycast(origin, unitDir, maxDistance, buffer, hitFlags, filterData); + if (hitResult) + { + auto& hit = buffer.getAnyHit(0); + if (hit.distance < closestDistance && hit.distance > 0.0f) + { + tempClosestPoint = hit.position; + closestDistance = hit.distance; + } + } + } + + if (closestDistance < maxDistance) + { + *closestPoint = P2C(tempClosestPoint); + return closestDistance * closestDistance; // Result is squared distance + } + + return -1.0f; + } + + // Default point distance for other collider queries #if USE_LARGE_WORLDS PxVec3 closestPointPx; float result = PxGeometryQuery::pointDistance(C2P(point), shapePhysX->getGeometry(), trans, &closestPointPx); diff --git a/Source/Engine/Physics/Physics.h b/Source/Engine/Physics/Physics.h index 2fc116020..85cd5e77b 100644 --- a/Source/Engine/Physics/Physics.h +++ b/Source/Engine/Physics/Physics.h @@ -102,7 +102,7 @@ public: /// The end position of the line. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool LineCast(const Vector3& start, const Vector3& end, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -113,18 +113,18 @@ public: /// The result hit information. Valid only when method returns true. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool LineCast(const Vector3& start, const Vector3& end, API_PARAM(Out) RayCastHit& hitInfo, uint32 layerMask = MAX_uint32, bool hitTriggers = true); // - /// Performs a line between two points in the scene, returns all hitpoints infos. + /// Performs a line between two points in the scene, returns all hit points info. /// /// The origin of the ray. /// The end position of the line. /// The result hits. Valid only when method returns true. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool LineCastAll(const Vector3& start, const Vector3& end, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -135,7 +135,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool RayCast(const Vector3& origin, const Vector3& direction, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -147,7 +147,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool RayCast(const Vector3& origin, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -159,7 +159,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() static bool RayCastAll(const Vector3& origin, const Vector3& direction, API_PARAM(Out) Array& results, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -172,7 +172,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if box hits an matching object, otherwise false. + /// True if box hits a matching object, otherwise false. API_FUNCTION() static bool BoxCast(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -186,7 +186,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if box hits an matching object, otherwise false. + /// True if box hits a matching object, otherwise false. API_FUNCTION() static bool BoxCast(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -200,7 +200,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if box hits an matching object, otherwise false. + /// True if box hits a matching object, otherwise false. API_FUNCTION() static bool BoxCastAll(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -212,7 +212,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if sphere hits an matching object, otherwise false. + /// True if sphere hits a matching object, otherwise false. API_FUNCTION() static bool SphereCast(const Vector3& center, float radius, const Vector3& direction, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -225,7 +225,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if sphere hits an matching object, otherwise false. + /// True if sphere hits a matching object, otherwise false. API_FUNCTION() static bool SphereCast(const Vector3& center, float radius, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -238,7 +238,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if sphere hits an matching object, otherwise false. + /// True if sphere hits a matching object, otherwise false. API_FUNCTION() static bool SphereCastAll(const Vector3& center, float radius, const Vector3& direction, API_PARAM(Out) Array& results, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -252,7 +252,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if capsule hits an matching object, otherwise false. + /// True if capsule hits a matching object, otherwise false. API_FUNCTION() static bool CapsuleCast(const Vector3& center, float radius, float height, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -267,7 +267,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if capsule hits an matching object, otherwise false. + /// True if capsule hits a matching object, otherwise false. API_FUNCTION() static bool CapsuleCast(const Vector3& center, float radius, float height, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -282,7 +282,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if capsule hits an matching object, otherwise false. + /// True if capsule hits a matching object, otherwise false. API_FUNCTION() static bool CapsuleCastAll(const Vector3& center, float radius, float height, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -296,7 +296,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if convex mesh hits an matching object, otherwise false. + /// True if convex mesh hits a matching object, otherwise false. API_FUNCTION() static bool ConvexCast(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -311,7 +311,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if convex mesh hits an matching object, otherwise false. + /// True if convex mesh hits a matching object, otherwise false. API_FUNCTION() static bool ConvexCast(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -326,7 +326,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if convex mesh hits an matching object, otherwise false. + /// True if convex mesh hits a matching object, otherwise false. API_FUNCTION() static bool ConvexCastAll(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -375,7 +375,7 @@ public: API_FUNCTION() static bool CheckConvex(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given box. + /// Finds all colliders touching or inside the given box. /// /// The box center. /// The half size of the box in each direction. @@ -387,7 +387,7 @@ public: API_FUNCTION() static bool OverlapBox(const Vector3& center, const Vector3& halfExtents, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given sphere. + /// Finds all colliders touching or inside the given sphere. /// /// The sphere center. /// The radius of the sphere. @@ -398,7 +398,7 @@ public: API_FUNCTION() static bool OverlapSphere(const Vector3& center, float radius, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given capsule. + /// Finds all colliders touching or inside the given capsule. /// /// The capsule center. /// The radius of the capsule. @@ -411,7 +411,7 @@ public: API_FUNCTION() static bool OverlapCapsule(const Vector3& center, float radius, float height, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given convex mesh. + /// Finds all colliders touching or inside the given convex mesh. /// /// The convex mesh center. /// Collision data of the convex mesh. @@ -424,7 +424,7 @@ public: API_FUNCTION() static bool OverlapConvex(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given box. + /// Finds all colliders touching or inside the given box. /// /// The box center. /// The half size of the box in each direction. @@ -436,7 +436,7 @@ public: API_FUNCTION() static bool OverlapBox(const Vector3& center, const Vector3& halfExtents, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given sphere. + /// Finds all colliders touching or inside the given sphere. /// /// The sphere center. /// The radius of the sphere. @@ -447,7 +447,7 @@ public: API_FUNCTION() static bool OverlapSphere(const Vector3& center, float radius, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given capsule. + /// Finds all colliders touching or inside the given capsule. /// /// The capsule center. /// The radius of the capsule. @@ -460,7 +460,7 @@ public: API_FUNCTION() static bool OverlapCapsule(const Vector3& center, float radius, float height, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given convex mesh. + /// Finds all colliders touching or inside the given convex mesh. /// /// The convex mesh center. /// Collision data of the convex mesh. diff --git a/Source/Engine/Physics/PhysicsScene.h b/Source/Engine/Physics/PhysicsScene.h index 602e6f713..a7cb91cbe 100644 --- a/Source/Engine/Physics/PhysicsScene.h +++ b/Source/Engine/Physics/PhysicsScene.h @@ -140,7 +140,7 @@ public: /// The end position of the line. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool LineCast(const Vector3& start, const Vector3& end, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -151,18 +151,18 @@ public: /// The result hit information. Valid only when method returns true. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool LineCast(const Vector3& start, const Vector3& end, API_PARAM(Out) RayCastHit& hitInfo, uint32 layerMask = MAX_uint32, bool hitTriggers = true); // - /// Performs a line between two points in the scene, returns all hitpoints infos. + /// Performs a line between two points in the scene, returns all hit points info. /// /// The origin of the ray. /// The normalized direction of the ray. /// The result hits. Valid only when method returns true. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool LineCastAll(const Vector3& start, const Vector3& end, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -173,7 +173,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool RayCast(const Vector3& origin, const Vector3& direction, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -185,7 +185,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool RayCast(const Vector3& origin, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -197,7 +197,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if ray hits an matching object, otherwise false. + /// True if ray hits a matching object, otherwise false. API_FUNCTION() bool RayCastAll(const Vector3& origin, const Vector3& direction, API_PARAM(Out) Array& results, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -210,7 +210,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if box hits an matching object, otherwise false. + /// True if box hits a matching object, otherwise false. API_FUNCTION() bool BoxCast(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -224,7 +224,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if box hits an matching object, otherwise false. + /// True if box hits a matching object, otherwise false. API_FUNCTION() bool BoxCast(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -238,7 +238,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if box hits an matching object, otherwise false. + /// True if box hits a matching object, otherwise false. API_FUNCTION() bool BoxCastAll(const Vector3& center, const Vector3& halfExtents, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -250,7 +250,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if sphere hits an matching object, otherwise false. + /// True if sphere hits a matching object, otherwise false. API_FUNCTION() bool SphereCast(const Vector3& center, float radius, const Vector3& direction, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -263,7 +263,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if sphere hits an matching object, otherwise false. + /// True if sphere hits a matching object, otherwise false. API_FUNCTION() bool SphereCast(const Vector3& center, float radius, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -276,7 +276,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if sphere hits an matching object, otherwise false. + /// True if sphere hits a matching object, otherwise false. API_FUNCTION() bool SphereCastAll(const Vector3& center, float radius, const Vector3& direction, API_PARAM(Out) Array& results, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -290,7 +290,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if capsule hits an matching object, otherwise false. + /// True if capsule hits a matching object, otherwise false. API_FUNCTION() bool CapsuleCast(const Vector3& center, float radius, float height, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -305,7 +305,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if capsule hits an matching object, otherwise false. + /// True if capsule hits a matching object, otherwise false. API_FUNCTION() bool CapsuleCast(const Vector3& center, float radius, float height, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -320,7 +320,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if capsule hits an matching object, otherwise false. + /// True if capsule hits a matching object, otherwise false. API_FUNCTION() bool CapsuleCastAll(const Vector3& center, float radius, float height, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -334,7 +334,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if convex mesh hits an matching object, otherwise false. + /// True if convex mesh hits a matching object, otherwise false. API_FUNCTION() bool ConvexCast(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -349,7 +349,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if convex mesh hits an matching object, otherwise false. + /// True if convex mesh hits a matching object, otherwise false. API_FUNCTION() bool ConvexCast(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, API_PARAM(Out) RayCastHit& hitInfo, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -364,7 +364,7 @@ public: /// The maximum distance the ray should check for collisions. /// The layer mask used to filter the results. /// If set to true triggers will be hit, otherwise will skip them. - /// True if convex mesh hits an matching object, otherwise false. + /// True if convex mesh hits a matching object, otherwise false. API_FUNCTION() bool ConvexCastAll(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Vector3& direction, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, float maxDistance = MAX_float, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// @@ -413,7 +413,7 @@ public: API_FUNCTION() bool CheckConvex(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given box. + /// Finds all colliders touching or inside the given box. /// /// The box center. /// The half size of the box in each direction. @@ -425,7 +425,7 @@ public: API_FUNCTION() bool OverlapBox(const Vector3& center, const Vector3& halfExtents, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given sphere. + /// Finds all colliders touching or inside the given sphere. /// /// The sphere center. /// The radius of the sphere. @@ -436,7 +436,7 @@ public: API_FUNCTION() bool OverlapSphere(const Vector3& center, float radius, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given capsule. + /// Finds all colliders touching or inside the given capsule. /// /// The capsule center. /// The radius of the capsule. @@ -449,7 +449,7 @@ public: API_FUNCTION() bool OverlapCapsule(const Vector3& center, float radius, float height, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given convex mesh. + /// Finds all colliders touching or inside the given convex mesh. /// /// The convex mesh center. /// Collision data of the convex mesh. @@ -462,7 +462,7 @@ public: API_FUNCTION() bool OverlapConvex(const Vector3& center, const CollisionData* convexMesh, const Vector3& scale, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given box. + /// Finds all colliders touching or inside the given box. /// /// The box center. /// The half size of the box in each direction. @@ -474,7 +474,7 @@ public: API_FUNCTION() bool OverlapBox(const Vector3& center, const Vector3& halfExtents, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given sphere. + /// Finds all colliders touching or inside the given sphere. /// /// The sphere center. /// The radius of the sphere. @@ -485,7 +485,7 @@ public: API_FUNCTION() bool OverlapSphere(const Vector3& center, float radius, API_PARAM(Out) Array& results, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given capsule. + /// Finds all colliders touching or inside the given capsule. /// /// The capsule center. /// The radius of the capsule. @@ -498,7 +498,7 @@ public: API_FUNCTION() bool OverlapCapsule(const Vector3& center, float radius, float height, API_PARAM(Out) Array& results, const Quaternion& rotation = Quaternion::Identity, uint32 layerMask = MAX_uint32, bool hitTriggers = true); /// - /// Finds all colliders touching or inside of the given convex mesh. + /// Finds all colliders touching or inside the given convex mesh. /// /// The convex mesh center. /// Collision data of the convex mesh. diff --git a/Source/Engine/Profiler/ProfilerMemory.cpp b/Source/Engine/Profiler/ProfilerMemory.cpp index c936ff5b2..6b8f18ce3 100644 --- a/Source/Engine/Profiler/ProfilerMemory.cpp +++ b/Source/Engine/Profiler/ProfilerMemory.cpp @@ -243,6 +243,7 @@ void InitProfilerMemory(const Char* cmdLine, int32 stage) #define INIT_PARENT(parent, child) GroupParents[(int32)ProfilerMemory::Groups::child] = (uint8)ProfilerMemory::Groups::parent INIT_PARENT(Engine, EngineThreading); INIT_PARENT(Engine, EngineDelegate); + INIT_PARENT(Engine, EngineDebug); INIT_PARENT(Malloc, MallocArena); INIT_PARENT(Graphics, GraphicsTextures); INIT_PARENT(Graphics, GraphicsRenderTargets); @@ -260,6 +261,8 @@ void InitProfilerMemory(const Char* cmdLine, int32 stage) INIT_PARENT(Content, ContentFiles); INIT_PARENT(Level, LevelFoliage); INIT_PARENT(Level, LevelTerrain); + INIT_PARENT(Navigation, NavigationMesh); + INIT_PARENT(Navigation, NavigationBuilding); INIT_PARENT(Scripting, ScriptingVisual); INIT_PARENT(Scripting, ScriptingCSharp); INIT_PARENT(ScriptingCSharp, ScriptingCSharpGCCommitted); diff --git a/Source/Engine/Profiler/ProfilerMemory.h b/Source/Engine/Profiler/ProfilerMemory.h index 5dddb912b..9177ae6e7 100644 --- a/Source/Engine/Profiler/ProfilerMemory.h +++ b/Source/Engine/Profiler/ProfilerMemory.h @@ -44,6 +44,8 @@ public: EngineThreading, // Memory used by Delegate (engine events system to store all references). EngineDelegate, + // Memory used by debug tools (eg. DebugDraw, DebugCommands or DebugLog). + EngineDebug, // Total graphics memory usage. Graphics, @@ -105,6 +107,10 @@ public: // Total navigation system memory. Navigation, + // Navigation mesh memory. + NavigationMesh, + // Navigation mesh builder memory. + NavigationBuilding, // Total networking system memory. Networking, diff --git a/Source/Engine/Serialization/JsonSerializer.cs b/Source/Engine/Serialization/JsonSerializer.cs index 4321ffa36..c8d00567a 100644 --- a/Source/Engine/Serialization/JsonSerializer.cs +++ b/Source/Engine/Serialization/JsonSerializer.cs @@ -270,8 +270,8 @@ namespace FlaxEngine.Json // Special case when saving reference to prefab object and the objects are different but the point to the same prefab object // In that case, skip saving reference as it's defined in prefab (will be populated via IdsMapping during deserialization) - if (objA is SceneObject sceneA && objB is SceneObject sceneB && sceneA && sceneB && sceneA.HasPrefabLink && sceneB.HasPrefabLink) - return sceneA.PrefabObjectID == sceneB.PrefabObjectID; + if (objA is SceneObject sceneObjA && objB is SceneObject sceneObjB && sceneObjA && sceneObjB && sceneObjA.HasPrefabLink && sceneObjB.HasPrefabLink) + return sceneObjA.PrefabObjectID == sceneObjB.PrefabObjectID; // Comparing an Int32 and Int64 both of the same value returns false, make types the same then compare if (objA.GetType() != objB.GetType()) @@ -286,7 +286,6 @@ namespace FlaxEngine.Json type == typeof(Int32) || type == typeof(UInt32) || type == typeof(Int64) || - type == typeof(SByte) || type == typeof(UInt64); } if (IsInteger(objA) && IsInteger(objB)) @@ -301,6 +300,12 @@ namespace FlaxEngine.Json { if (aList.Count != bList.Count) return false; + for (int i = 0; i < aList.Count; i++) + { + if (!ValueEquals(aList[i], bList[i])) + return false; + } + return true; } if (objA is IEnumerable aEnumerable && objB is IEnumerable bEnumerable) { @@ -316,8 +321,30 @@ namespace FlaxEngine.Json return !bEnumerator.MoveNext(); } - if (objA is ICustomValueEquals customValueEquals && objA.GetType() == objB.GetType()) + // Custom comparer + if (objA is ICustomValueEquals customValueEquals) return customValueEquals.ValueEquals(objB); + + // If type contains SceneObject references then it needs to use custom comparision that handles prefab links (see SceneObjectEquals) + var typeA = objA.GetType(); + if (typeA.IsValueType && !typeA.IsEnum && !typeA.IsPrimitive) + { + var contract = Settings.ContractResolver.ResolveContract(typeA); + if (contract is JsonObjectContract objContract) + { + foreach (var property in objContract.Properties) + { + var valueProvider = property.ValueProvider; + var propA = valueProvider.GetValue(objA); + var propB = valueProvider.GetValue(objB); + if (!ValueEquals(propA, propB)) + return false; + } + return true; + } + } + + // Generic fallback return objA.Equals(objB); #endif } diff --git a/Source/Engine/Serialization/Serialization.h b/Source/Engine/Serialization/Serialization.h index d3205ad83..9af6d7be1 100644 --- a/Source/Engine/Serialization/Serialization.h +++ b/Source/Engine/Serialization/Serialization.h @@ -415,7 +415,7 @@ namespace Serialization inline bool ShouldSerialize(const ISerializable& v, const void* otherObj) { - return true; + return !otherObj || v.ShouldSerialize(otherObj); } inline void Serialize(ISerializable::SerializeStream& stream, const ISerializable& v, const void* otherObj) { @@ -431,7 +431,7 @@ namespace Serialization template inline typename TEnableIf::Value, bool>::Type ShouldSerialize(const ISerializable& v, const void* otherObj) { - return true; + return !otherObj || v.ShouldSerialize(otherObj); } template inline typename TEnableIf::Value>::Type Serialize(ISerializable::SerializeStream& stream, const ISerializable& v, const void* otherObj) diff --git a/Source/Engine/Threading/Task.cpp b/Source/Engine/Threading/Task.cpp index a640019d1..cef08b0bc 100644 --- a/Source/Engine/Threading/Task.cpp +++ b/Source/Engine/Threading/Task.cpp @@ -148,9 +148,8 @@ Task* Task::StartNew(Function::Signature& action, Object* target) void Task::Execute() { - if (IsCanceled()) + if (!IsQueued()) return; - ASSERT(IsQueued()); SetState(TaskState::Running); // Perform an operation diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp index c48a4c569..13b835185 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp @@ -534,7 +534,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) } // Sample Texture case 9: - // Procedural Texture Sample + // Procedural Sample Texture case 17: { // Get input boxes @@ -585,7 +585,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) const auto offset = useOffset ? eatBox(offsetBox->GetParent(), offsetBox->FirstConnection()) : Value::Zero; const Char* samplerName; const int32 samplerIndex = node->Values[0].AsInt; - if (samplerIndex == TextureGroup) + if (samplerIndex == TextureGroup && node->Values.Count() > 2) { auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[2].AsInt); samplerName = *textureGroupSampler.ShaderName; @@ -739,7 +739,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) const int32 samplerIndex = node->Values.Count() >= 4 ? node->Values[3].AsInt : LinearWrap; if (samplerIndex == TextureGroup) { - auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[3].AsInt); + auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[5].AsInt); samplerName = *textureGroupSampler.ShaderName; } else if (samplerIndex >= 0 && samplerIndex < ARRAY_COUNT(SamplerNames)) @@ -828,7 +828,7 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) const int32 samplerIndex = node->Values[3].AsInt; if (samplerIndex == TextureGroup) { - auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[3].AsInt); + auto& textureGroupSampler = findOrAddTextureGroupSampler(node->Values[5].AsInt); samplerName = *textureGroupSampler.ShaderName; } else if (samplerIndex >= 0 && samplerIndex < ARRAY_COUNT(SamplerNames)) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 843822b98..57afeb7a5 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -567,6 +567,7 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj SERIALIZE(CalculateBoneOffsetMatrices); SERIALIZE(LightmapUVsSource); SERIALIZE(CollisionMeshesPrefix); + SERIALIZE(CollisionMeshesPostfix); SERIALIZE(CollisionType); SERIALIZE(PositionFormat); SERIALIZE(TexCoordFormat); @@ -621,6 +622,7 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi DESERIALIZE(CalculateBoneOffsetMatrices); DESERIALIZE(LightmapUVsSource); DESERIALIZE(CollisionMeshesPrefix); + DESERIALIZE(CollisionMeshesPostfix); DESERIALIZE(CollisionType); DESERIALIZE(PositionFormat); DESERIALIZE(TexCoordFormat); @@ -1830,7 +1832,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option } // Collision mesh output - if (options.CollisionMeshesPrefix.HasChars()) + if (options.CollisionMeshesPrefix.HasChars() || options.CollisionMeshesPostfix.HasChars()) { // Extract collision meshes from the model ModelData collisionModel; @@ -1839,7 +1841,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option for (int32 i = lod.Meshes.Count() - 1; i >= 0; i--) { auto mesh = lod.Meshes[i]; - if (mesh->Name.StartsWith(options.CollisionMeshesPrefix, StringSearchCase::IgnoreCase)) + if ((options.CollisionMeshesPrefix.HasChars() && mesh->Name.StartsWith(options.CollisionMeshesPrefix, StringSearchCase::IgnoreCase)) || + (options.CollisionMeshesPostfix.HasChars() && mesh->Name.EndsWith(options.CollisionMeshesPostfix, StringSearchCase::IgnoreCase))) { // Remove material slot used by this mesh (if no other mesh else uses it) int32 materialSlotUsageCount = 0; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index d7545b92e..bc96e8308 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -221,6 +221,9 @@ public: // If specified, all meshes that name starts with this prefix in the name will be imported as a separate collision data asset (excluded used for rendering). API_FIELD(Attributes="EditorOrder(100), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))") String CollisionMeshesPrefix = TEXT(""); + // If specified, all meshes that name ends with this postfix in the name will be imported as a separate collision data asset (excluded used for rendering). + API_FIELD(Attributes="EditorOrder(101), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))") + String CollisionMeshesPostfix = TEXT(""); // The type of collision that should be generated if the mesh has a collision prefix specified. API_FIELD(Attributes="EditorOrder(105), EditorDisplay(\"Geometry\"), VisibleIf(nameof(ShowGeometry))") CollisionDataType CollisionType = CollisionDataType::ConvexMesh; diff --git a/Source/Engine/UI/GUI/CanvasScaler.cs b/Source/Engine/UI/GUI/CanvasScaler.cs index 6bd18ea51..1e30fd22f 100644 --- a/Source/Engine/UI/GUI/CanvasScaler.cs +++ b/Source/Engine/UI/GUI/CanvasScaler.cs @@ -449,8 +449,7 @@ namespace FlaxEngine.GUI /// public override bool RayCast(ref Float2 location, out Control hit) { - var p = location / _scale; - if (RayCastChildren(ref p, out hit)) + if (RayCastChildren(ref location, out hit)) return true; return base.RayCast(ref location, out hit); } diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs index 20ef1c401..bb6ee22a5 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBox.Parsing.cs @@ -143,6 +143,40 @@ namespace FlaxEngine.GUI context.Caret.X = 0; OnLineAdded(ref context, _text.Length - 1); } + + // Organize lines vertically + if (_textBlocks.Count != 0) + { + var lastBlock = _textBlocks[_textBlocks.Count - 1]; + + // Get style (global or leftover from style stack or the last lime) + var verticalAlignments = _textStyle.Alignment; + if (context.StyleStack.Count > 1) + verticalAlignments = context.StyleStack.Peek().Alignment; + else if ((lastBlock.Style.Alignment & TextBlockStyle.Alignments.VerticalMask) != TextBlockStyle.Alignments.Baseline) + verticalAlignments = lastBlock.Style.Alignment; + + var totalSize = lastBlock.Bounds.BottomRight; + var sizeOffset = Size - totalSize; + var textBlocks = CollectionsMarshal.AsSpan(_textBlocks); + if ((verticalAlignments & TextBlockStyle.Alignments.Middle) == TextBlockStyle.Alignments.Middle) + { + sizeOffset.Y *= 0.5f; + for (int i = 0; i < _textBlocks.Count; i++) + { + ref TextBlock textBlock = ref textBlocks[i]; + textBlock.Bounds.Location.Y += sizeOffset.Y; + } + } + else if ((verticalAlignments & TextBlockStyle.Alignments.Bottom) == TextBlockStyle.Alignments.Bottom) + { + for (int i = 0; i < _textBlocks.Count; i++) + { + ref TextBlock textBlock = ref textBlocks[i]; + textBlock.Bounds.Location.Y += sizeOffset.Y; + } + } + } } /// @@ -239,14 +273,15 @@ namespace FlaxEngine.GUI } // Organize text blocks within line - var horizontalAlignments = TextBlockStyle.Alignments.Baseline; - var verticalAlignments = TextBlockStyle.Alignments.Baseline; + var lineAlignments = TextBlockStyle.Alignments.Baseline; for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++) { ref TextBlock textBlock = ref textBlocks[i]; var vOffset = lineSize.Y - textBlock.Bounds.Height; - horizontalAlignments |= textBlock.Style.Alignment & TextBlockStyle.Alignments.HorizontalMask; - verticalAlignments |= textBlock.Style.Alignment & TextBlockStyle.Alignments.VerticalMask; + if (i == context.LineStartTextBlockIndex) + lineAlignments = textBlock.Style.Alignment; + else + lineAlignments &= textBlock.Style.Alignment; switch (textBlock.Style.Alignment & TextBlockStyle.Alignments.VerticalMask) { case TextBlockStyle.Alignments.Baseline: @@ -275,9 +310,9 @@ namespace FlaxEngine.GUI } } - // Organize blocks within whole container + // Organize whole line horizontally var sizeOffset = Size - lineSize; - if ((horizontalAlignments & TextBlockStyle.Alignments.Center) == TextBlockStyle.Alignments.Center) + if ((lineAlignments & TextBlockStyle.Alignments.Center) == TextBlockStyle.Alignments.Center) { sizeOffset.X *= 0.5f; for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++) @@ -286,7 +321,7 @@ namespace FlaxEngine.GUI textBlock.Bounds.Location.X += sizeOffset.X; } } - else if ((horizontalAlignments & TextBlockStyle.Alignments.Right) == TextBlockStyle.Alignments.Right) + else if ((lineAlignments & TextBlockStyle.Alignments.Right) == TextBlockStyle.Alignments.Right) { for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++) { @@ -294,23 +329,6 @@ namespace FlaxEngine.GUI textBlock.Bounds.Location.X += sizeOffset.X; } } - if ((verticalAlignments & TextBlockStyle.Alignments.Middle) == TextBlockStyle.Alignments.Middle) - { - sizeOffset.Y *= 0.5f; - for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++) - { - ref TextBlock textBlock = ref textBlocks[i]; - textBlock.Bounds.Location.Y += sizeOffset.Y; - } - } - else if ((verticalAlignments & TextBlockStyle.Alignments.Bottom) == TextBlockStyle.Alignments.Bottom) - { - for (int i = context.LineStartTextBlockIndex; i < _textBlocks.Count; i++) - { - ref TextBlock textBlock = ref textBlocks[i]; - textBlock.Bounds.Location.Y += sizeOffset.Y; - } - } // Move to the next line context.LineStartCharacterIndex = lineEnd + 1; diff --git a/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs b/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs index b57fac47d..3bb99762f 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBox.Tags.cs @@ -175,7 +175,7 @@ namespace FlaxEngine.GUI // Setup size var font = imageBlock.Style.Font.GetFont(); if (font) - imageBlock.Bounds.Size = new Float2(font.Height); + imageBlock.Bounds.Size = new Float2(font.Ascender); var imageSize = image.Size; imageBlock.Bounds.Size.X *= imageSize.X / imageSize.Y; // Keep original aspect ratio bool hasWidth = TryParseNumberTag(ref tag, "width", imageBlock.Bounds.Width, out var width); @@ -215,16 +215,16 @@ namespace FlaxEngine.GUI switch (valign) { case "top": - style.Alignment = TextBlockStyle.Alignments.Top; + style.Alignment |= TextBlockStyle.Alignments.Top; break; case "bottom": - style.Alignment = TextBlockStyle.Alignments.Bottom; + style.Alignment |= TextBlockStyle.Alignments.Bottom; break; case "middle": - style.Alignment = TextBlockStyle.Alignments.Middle; + style.Alignment |= TextBlockStyle.Alignments.Middle; break; case "baseline": - style.Alignment = TextBlockStyle.Alignments.Baseline; + style.Alignment |= TextBlockStyle.Alignments.Baseline; break; } } @@ -243,17 +243,17 @@ namespace FlaxEngine.GUI var style = context.StyleStack.Peek(); if (tag.Attributes.TryGetValue(string.Empty, out var valign)) { - style.Alignment &= ~TextBlockStyle.Alignments.VerticalMask; + style.Alignment &= ~TextBlockStyle.Alignments.HorizontalMask; switch (valign) { case "left": - style.Alignment = TextBlockStyle.Alignments.Left; + style.Alignment |= TextBlockStyle.Alignments.Left; break; case "right": - style.Alignment = TextBlockStyle.Alignments.Right; + style.Alignment |= TextBlockStyle.Alignments.Right; break; case "center": - style.Alignment = TextBlockStyle.Alignments.Center; + style.Alignment |= TextBlockStyle.Alignments.Center; break; } } @@ -270,7 +270,8 @@ namespace FlaxEngine.GUI else { var style = context.StyleStack.Peek(); - style.Alignment = TextBlockStyle.Alignments.Center; + style.Alignment &= ~TextBlockStyle.Alignments.HorizontalMask; + style.Alignment |= TextBlockStyle.Alignments.Center; context.StyleStack.Push(style); } } diff --git a/Source/Engine/UI/GUI/ContainerControl.cs b/Source/Engine/UI/GUI/ContainerControl.cs index ada93ff1e..55fcecf80 100644 --- a/Source/Engine/UI/GUI/ContainerControl.cs +++ b/Source/Engine/UI/GUI/ContainerControl.cs @@ -901,6 +901,15 @@ namespace FlaxEngine.GUI internal bool RayCastChildren(ref Float2 location, out Control hit) { + if (_clipChildren) + { + GetDesireClientArea(out var clientArea); + if (!clientArea.Contains(ref location)) + { + hit = null; + return false; + } + } for (int i = _children.Count - 1; i >= 0 && _children.Count > 0; i--) { var child = _children[i]; diff --git a/Source/Engine/Video/MF/VideoBackendMF.cpp b/Source/Engine/Video/MF/VideoBackendMF.cpp index df24f5eed..3e738d09f 100644 --- a/Source/Engine/Video/MF/VideoBackendMF.cpp +++ b/Source/Engine/Video/MF/VideoBackendMF.cpp @@ -47,6 +47,8 @@ struct VideoPlayerMF namespace MF { Array Players; + TimeSpan UpdateDeltaTime; + double UpdateTime; bool Configure(VideoBackendPlayer& player, VideoPlayerMF& playerMF, DWORD streamIndex) { @@ -395,13 +397,6 @@ namespace MF if (!playerMF.Playing && !playerMF.Seek) return; - bool useTimeScale = true; -#if USE_EDITOR - if (!Editor::IsPlayMode) - useTimeScale = false; -#endif - TimeSpan dt = useTimeScale ? Time::Update.DeltaTime : Time::Update.UnscaledDeltaTime; - // Update playback time if (playerMF.FirstFrame) { @@ -410,9 +405,9 @@ namespace MF } else if (playerMF.Playing) { - playerMF.Time += dt; + playerMF.Time += UpdateDeltaTime; } - if (playerMF.Time > player.Duration) + if (playerMF.Time > player.Duration && player.Duration.Ticks != 0) { if (playerMF.Loop) { @@ -452,7 +447,7 @@ namespace MF } // Update streams - if (ReadStream(player, playerMF, MF_SOURCE_READER_FIRST_VIDEO_STREAM, dt)) + if (ReadStream(player, playerMF, MF_SOURCE_READER_FIRST_VIDEO_STREAM, UpdateDeltaTime)) { // Failed to pick a valid sample so try again with seeking playerMF.Seek = 1; @@ -464,7 +459,7 @@ namespace MF } } if (player.AudioInfo.BitDepth != 0) - ReadStream(player, playerMF, MF_SOURCE_READER_FIRST_AUDIO_STREAM, dt); + ReadStream(player, playerMF, MF_SOURCE_READER_FIRST_AUDIO_STREAM, UpdateDeltaTime); player.Tick(); } @@ -610,12 +605,17 @@ bool VideoBackendMF::Base_Init() VIDEO_API_MF_ERROR(MFStartup, hr); return true; } + MF::UpdateTime = Platform::GetTimeSeconds(); return false; } void VideoBackendMF::Base_Update(TaskGraph* graph) { + double time = Platform::GetTimeSeconds(); + MF::UpdateDeltaTime = TimeSpan::FromSeconds(time - MF::UpdateTime); + MF::UpdateTime = time; + // Schedule work to update all videos in async Function job; job.Bind(MF::UpdatePlayer); diff --git a/Source/Engine/Video/VideoPlayer.cpp b/Source/Engine/Video/VideoPlayer.cpp index 59f8d2438..363779038 100644 --- a/Source/Engine/Video/VideoPlayer.cpp +++ b/Source/Engine/Video/VideoPlayer.cpp @@ -133,7 +133,9 @@ void VideoPlayer::SetTime(float time) if (_state == States::Stopped || _player.Backend == nullptr) return; TimeSpan timeSpan = TimeSpan::FromSeconds(time); - timeSpan.Ticks = Math::Clamp(timeSpan.Ticks, 0, _player.Duration.Ticks); + timeSpan.Ticks = Math::Max(timeSpan.Ticks, 0); + if (_player.Duration.Ticks > 0) + timeSpan.Ticks = Math::Min(timeSpan.Ticks, _player.Duration.Ticks); _player.Backend->Player_Seek(_player, timeSpan); } diff --git a/Source/Engine/Video/VideoPlayer.h b/Source/Engine/Video/VideoPlayer.h index e1b7e877d..4447337ea 100644 --- a/Source/Engine/Video/VideoPlayer.h +++ b/Source/Engine/Video/VideoPlayer.h @@ -15,6 +15,7 @@ class FLAXENGINE_API VideoPlayer : public Actor { DECLARE_SCENE_OBJECT(VideoPlayer); API_AUTO_SERIALIZATION(); + friend class AudioBackendOAL; public: /// diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 46f0ec245..3bdb16815 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -1892,13 +1892,13 @@ namespace Flax.Build.Bindings CppAutoSerializeProperties.Clear(); CppIncludeFiles.Add("Engine/Serialization/Serialization.h"); + // Serialize contents.AppendLine(); contents.Append($"void {typeNameNative}::Serialize(SerializeStream& stream, const void* otherObj)").AppendLine(); contents.Append('{').AppendLine(); if (baseType != null) contents.Append($" {baseType.FullNameNative}::Serialize(stream, otherObj);").AppendLine(); contents.Append($" SERIALIZE_GET_OTHER_OBJ({typeNameNative});").AppendLine(); - if (classInfo != null) { foreach (var fieldInfo in classInfo.Fields) @@ -1910,7 +1910,6 @@ namespace Flax.Build.Bindings contents.Append($" SERIALIZE{typeHint}({fieldInfo.Name});").AppendLine(); CppAutoSerializeFields.Add(fieldInfo); } - foreach (var propertyInfo in classInfo.Properties) { if (propertyInfo.Getter == null || propertyInfo.Setter == null) @@ -1952,21 +1951,19 @@ namespace Flax.Build.Bindings CppAutoSerializeFields.Add(fieldInfo); } } - contents.Append('}').AppendLine(); + // Deserialize contents.AppendLine(); contents.Append($"void {typeNameNative}::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)").AppendLine(); contents.Append('{').AppendLine(); if (baseType != null) contents.Append($" {baseType.FullNameNative}::Deserialize(stream, modifier);").AppendLine(); - foreach (var fieldInfo in CppAutoSerializeFields) { var typeHint = GenerateCppAutoSerializationDefineType(buildData, contents, moduleInfo, typeInfo, fieldInfo.Type, fieldInfo); contents.Append($" DESERIALIZE{typeHint}({fieldInfo.Name});").AppendLine(); } - foreach (var propertyInfo in CppAutoSerializeProperties) { contents.AppendLine(" {"); @@ -1978,7 +1975,43 @@ namespace Flax.Build.Bindings contents.AppendLine(" }"); contents.AppendLine(" }"); } + contents.Append('}').AppendLine(); + // ShouldSerialize + contents.AppendLine(); + contents.Append($"bool {typeNameNative}::ShouldSerialize(const void* otherObj) const").AppendLine(); + contents.Append('{').AppendLine(); + if (!typeInfo.IsScriptingObject) + { + contents.Append($" SERIALIZE_GET_OTHER_OBJ({typeNameNative});").AppendLine(); + contents.AppendLine(" bool result = false;"); + if (baseType != null) + contents.Append($" result |= {baseType.FullNameNative}::ShouldSerialize(otherObj);").AppendLine(); + foreach (var fieldInfo in CppAutoSerializeFields) + { + contents.Append($" result |= Serialization::ShouldSerialize({fieldInfo.Name}, &other->{fieldInfo.Name});").AppendLine(); + } + foreach (var propertyInfo in CppAutoSerializeProperties) + { + contents.Append(" {"); + contents.Append(" const auto"); + if (propertyInfo.Getter.ReturnType.IsConstRef) + contents.Append('&'); + contents.Append($" value = {propertyInfo.Getter.Name}();"); + contents.Append(" const auto"); + if (propertyInfo.Getter.ReturnType.IsConstRef) + contents.Append('&'); + contents.Append($" otherValue = other->{propertyInfo.Getter.Name}();"); + contents.Append(" result |= Serialization::ShouldSerialize(value, &otherValue);").AppendLine(); + contents.Append('}').AppendLine(); + } + contents.AppendLine(" return result;"); + } + else + { + // Not needed to generate + contents.AppendLine(" return true;"); + } contents.Append('}').AppendLine(); }