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