Merge remote-tracking branch 'origin/master' into sdl_platform
# Conflicts: # Source/Editor/Viewport/MainEditorGizmoViewport.cs
This commit is contained in:
BIN
Content/Editor/Camera/M_Camera.flax
(Stored with Git LFS)
BIN
Content/Editor/Camera/M_Camera.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/CubeTexturePreviewMaterial.flax
(Stored with Git LFS)
BIN
Content/Editor/CubeTexturePreviewMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/DebugMaterials/DDGIDebugProbes.flax
(Stored with Git LFS)
BIN
Content/Editor/DebugMaterials/DDGIDebugProbes.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/DebugMaterials/SingleColor/Decal.flax
(Stored with Git LFS)
BIN
Content/Editor/DebugMaterials/SingleColor/Decal.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/DebugMaterials/SingleColor/Particle.flax
(Stored with Git LFS)
BIN
Content/Editor/DebugMaterials/SingleColor/Particle.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/DebugMaterials/SingleColor/Surface.flax
(Stored with Git LFS)
BIN
Content/Editor/DebugMaterials/SingleColor/Surface.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax
(Stored with Git LFS)
BIN
Content/Editor/DebugMaterials/SingleColor/SurfaceAdditive.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/DebugMaterials/SingleColor/Terrain.flax
(Stored with Git LFS)
BIN
Content/Editor/DebugMaterials/SingleColor/Terrain.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/DefaultFontMaterial.flax
(Stored with Git LFS)
BIN
Content/Editor/DefaultFontMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/Gizmo/FoliageBrushMaterial.flax
(Stored with Git LFS)
BIN
Content/Editor/Gizmo/FoliageBrushMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/Gizmo/Material.flax
(Stored with Git LFS)
BIN
Content/Editor/Gizmo/Material.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/Gizmo/MaterialWire.flax
(Stored with Git LFS)
BIN
Content/Editor/Gizmo/MaterialWire.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/Gizmo/SelectionOutlineMaterial.flax
(Stored with Git LFS)
BIN
Content/Editor/Gizmo/SelectionOutlineMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax
(Stored with Git LFS)
BIN
Content/Editor/Gizmo/VertexColorsPreviewMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/Highlight Material.flax
(Stored with Git LFS)
BIN
Content/Editor/Highlight Material.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/Icons/IconsMaterial.flax
(Stored with Git LFS)
BIN
Content/Editor/Icons/IconsMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/IesProfilePreviewMaterial.flax
(Stored with Git LFS)
BIN
Content/Editor/IesProfilePreviewMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/Particles/Particle Material Color.flax
(Stored with Git LFS)
BIN
Content/Editor/Particles/Particle Material Color.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/Particles/Smoke Material.flax
(Stored with Git LFS)
BIN
Content/Editor/Particles/Smoke Material.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/SpriteMaterial.flax
(Stored with Git LFS)
BIN
Content/Editor/SpriteMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/Terrain/Circle Brush Material.flax
(Stored with Git LFS)
BIN
Content/Editor/Terrain/Circle Brush Material.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/Terrain/Highlight Terrain Material.flax
(Stored with Git LFS)
BIN
Content/Editor/Terrain/Highlight Terrain Material.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/TexturePreviewMaterial.flax
(Stored with Git LFS)
BIN
Content/Editor/TexturePreviewMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Editor/Wires Debug Material.flax
(Stored with Git LFS)
BIN
Content/Editor/Wires Debug Material.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Engine/DefaultDeformableMaterial.flax
(Stored with Git LFS)
BIN
Content/Engine/DefaultDeformableMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Engine/DefaultMaterial.flax
(Stored with Git LFS)
BIN
Content/Engine/DefaultMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Engine/DefaultRadialMenu.flax
(Stored with Git LFS)
BIN
Content/Engine/DefaultRadialMenu.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Engine/DefaultTerrainMaterial.flax
(Stored with Git LFS)
BIN
Content/Engine/DefaultTerrainMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Engine/SingleColorMaterial.flax
(Stored with Git LFS)
BIN
Content/Engine/SingleColorMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Engine/SkyboxMaterial.flax
(Stored with Git LFS)
BIN
Content/Engine/SkyboxMaterial.flax
(Stored with Git LFS)
Binary file not shown.
BIN
Content/Shaders/GI/DDGI.flax
(Stored with Git LFS)
BIN
Content/Shaders/GI/DDGI.flax
(Stored with Git LFS)
Binary file not shown.
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:0f34bf867df5f4296ca66ac691c2bca4efa168fb9e21ca4e613e8086669575cf
|
||||
size 13296
|
||||
oid sha256:615dff65b01507be6c4de722e126324aba20fc197f8e12dafaa94a05e46cba6e
|
||||
size 13222
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:064f54786958f109222c49cbc0358ff4f345b30010fcd5e8cc1fab7bdc68c4fe
|
||||
size 13349
|
||||
oid sha256:1f07ebb16820897e8598ae7a0627cb75b3d28e9dceea3ad4bd9ff543d5cdd01c
|
||||
size 13979
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"Major": 1,
|
||||
"Minor": 11,
|
||||
"Revision": 0,
|
||||
"Build": 6805
|
||||
"Build": 6806
|
||||
},
|
||||
"Company": "Flax",
|
||||
"Copyright": "Copyright (c) 2012-2025 Wojciech Figat. All rights reserved.",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEngine;
|
||||
|
||||
namespace FlaxEditor.CustomEditors.Dedicated
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom editor for <see cref="NavMeshBoundsVolume"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="ActorEditor" />
|
||||
[CustomEditor(typeof(NavMeshBoundsVolume)), DefaultEditor]
|
||||
internal class NavMeshBoundsVolumeEditor : ActorEditor
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -909,7 +909,8 @@ namespace FlaxEditor.CustomEditors.Dedicated
|
||||
settingsButton.Tag = script;
|
||||
settingsButton.Clicked += OnSettingsButtonClicked;
|
||||
|
||||
group.Panel.HeaderTextMargin = new Margin(scriptDrag.Right - 12, 15, 2, 2);
|
||||
// Adjust margin to not overlap with other ui elements in the header
|
||||
group.Panel.HeaderTextMargin = group.Panel.HeaderTextMargin with { Left = scriptDrag.Right - 12, Right = settingsButton.Width + Utilities.Constants.UIMargin };
|
||||
group.Object(values, editor);
|
||||
// Remove drop down arrows and containment lines if no objects in the group
|
||||
if (group.Children.Count == 0)
|
||||
|
||||
@@ -450,6 +450,7 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
protected bool NotNullItems;
|
||||
|
||||
private IntValueBox _sizeBox;
|
||||
private Label _label;
|
||||
private Color _background;
|
||||
private int _elementsCount, _minCount, _maxCount;
|
||||
private bool _readOnly;
|
||||
@@ -566,7 +567,7 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
Parent = dropPanel,
|
||||
};
|
||||
|
||||
var label = new Label
|
||||
_label = new Label
|
||||
{
|
||||
Text = "Size",
|
||||
AnchorPreset = AnchorPresets.TopRight,
|
||||
@@ -672,8 +673,10 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
Resize(Count + 1);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Layout.ContainerControl.SizeChanged += OnLayoutSizeChanged;
|
||||
}
|
||||
|
||||
private void OnSetupContextMenu(ContextMenu menu, DropPanel panel)
|
||||
{
|
||||
if (menu.Items.Any(x => x is ContextMenuButton b && b.Text.Equals("Open All", StringComparison.Ordinal)))
|
||||
@@ -696,10 +699,24 @@ namespace FlaxEditor.CustomEditors.Editors
|
||||
});
|
||||
}
|
||||
|
||||
private void OnLayoutSizeChanged(Control control)
|
||||
{
|
||||
if (Layout.ContainerControl is DropPanel dropPanel)
|
||||
{
|
||||
// Hide "Size" text when array editor title overlaps
|
||||
var headerTextSize = dropPanel.HeaderTextFont.GetFont().MeasureText(dropPanel.HeaderText);
|
||||
if (headerTextSize.X + DropPanel.DropDownIconSize >= _label.Left)
|
||||
_label.TextColor = _label.TextColorHighlighted = Color.Transparent;
|
||||
else
|
||||
_label.TextColor = _label.TextColorHighlighted = FlaxEngine.GUI.Style.Current.Foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Deinitialize()
|
||||
{
|
||||
_sizeBox = null;
|
||||
Layout.ContainerControl.SizeChanged -= OnLayoutSizeChanged;
|
||||
|
||||
base.Deinitialize();
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ namespace FlaxEditor.CustomEditors.Elements
|
||||
{
|
||||
var style = Style.Current;
|
||||
var settingsButtonSize = Panel.HeaderHeight;
|
||||
return new Image
|
||||
Panel.HeaderTextMargin = Panel.HeaderTextMargin with { Right = settingsButtonSize + Utilities.Constants.UIMargin };
|
||||
; return new Image
|
||||
{
|
||||
TooltipText = "Settings",
|
||||
AutoFocus = true,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -75,6 +75,11 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
/// </summary>
|
||||
public bool HasChildCMOpened => _childCM != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parent context menu (if exists).
|
||||
/// </summary>
|
||||
public ContextMenuBase ParentCM => _parentCM;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the topmost context menu.
|
||||
/// </summary>
|
||||
@@ -84,9 +89,7 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
{
|
||||
var cm = this;
|
||||
while (cm._parentCM != null && cm._isSubMenu)
|
||||
{
|
||||
cm = cm._parentCM;
|
||||
}
|
||||
return cm;
|
||||
}
|
||||
}
|
||||
@@ -111,6 +114,11 @@ namespace FlaxEditor.GUI.ContextMenu
|
||||
/// </summary>
|
||||
public bool UseInput = true;
|
||||
|
||||
/// <summary>
|
||||
/// Optional flag that can disable UI navigation (tab/enter).
|
||||
/// </summary>
|
||||
public bool UseNavigation = true;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ContextMenuBase"/> class.
|
||||
/// </summary>
|
||||
@@ -622,6 +630,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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<T> && (preset != CurvePreset.Constant && preset != CurvePreset.Linear));
|
||||
}
|
||||
|
||||
_editor.OnShowContextMenu(cm, selectionCount);
|
||||
cm.Show(this, location);
|
||||
}
|
||||
@@ -619,6 +629,33 @@ namespace FlaxEditor.GUI
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A list of avaliable curve presets for the <see cref="CurveEditor{T}"/>.
|
||||
/// </summary>
|
||||
public enum CurvePreset
|
||||
{
|
||||
/// <summary>
|
||||
/// A curve where every point has the same value.
|
||||
/// </summary>
|
||||
Constant,
|
||||
/// <summary>
|
||||
/// A curve linear curve.
|
||||
/// </summary>
|
||||
Linear,
|
||||
/// <summary>
|
||||
/// A curve that starts a slowly and then accelerates until the end.
|
||||
/// </summary>
|
||||
EaseIn,
|
||||
/// <summary>
|
||||
/// A curve that starts a steep and then flattens until the end.
|
||||
/// </summary>
|
||||
EaseOut,
|
||||
/// <summary>
|
||||
/// A combination of the <see cref="CurvePreset.EaseIn"/> and <see cref="CurvePreset.EaseOut"/> preset.
|
||||
/// </summary>
|
||||
Smoothstep
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnKeyframesDeselect(IKeyframesEditor editor)
|
||||
{
|
||||
|
||||
@@ -19,6 +19,48 @@ namespace FlaxEditor.GUI
|
||||
/// <seealso cref="CurveEditorBase" />
|
||||
public abstract partial class CurveEditor<T> : CurveEditorBase where T : new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single point in a <see cref="CurveEditorPreset"/>.
|
||||
/// </summary>
|
||||
protected struct CurvePresetPoint
|
||||
{
|
||||
/// <summary>
|
||||
/// The time.
|
||||
/// </summary>
|
||||
public float Time;
|
||||
|
||||
/// <summary>
|
||||
/// The value.
|
||||
/// </summary>
|
||||
public float Value;
|
||||
|
||||
/// <summary>
|
||||
/// The in tangent. Will be ignored in <see cref="LinearCurveEditor{T}"/>
|
||||
/// </summary>
|
||||
public float TangentIn;
|
||||
|
||||
/// <summary>
|
||||
/// The out tangent. Will be ignored in <see cref="LinearCurveEditor{T}"/>
|
||||
/// </summary>
|
||||
public float TangentOut;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A curve preset.
|
||||
/// </summary>
|
||||
protected struct CurveEditorPreset()
|
||||
{
|
||||
/// <summary>
|
||||
/// If the tangents will be linear or smooth.
|
||||
/// </summary>
|
||||
public bool LinearTangents;
|
||||
|
||||
/// <summary>
|
||||
/// The points of the preset.
|
||||
/// </summary>
|
||||
public List<CurvePresetPoint> Points;
|
||||
}
|
||||
|
||||
private class Popup : ContextMenuBase
|
||||
{
|
||||
private CustomEditorPresenter _presenter;
|
||||
@@ -26,11 +68,12 @@ namespace FlaxEditor.GUI
|
||||
private List<int> _keyframeIndices;
|
||||
private bool _isDirty;
|
||||
|
||||
public Popup(CurveEditor<T> editor, object[] selection, List<int> keyframeIndices = null, float height = 140.0f)
|
||||
: this(editor, height)
|
||||
public Popup(CurveEditor<T> editor, object[] selection, List<int> 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
|
||||
/// <summary>
|
||||
/// The keyframes size.
|
||||
/// </summary>
|
||||
protected static readonly Float2 KeyframesSize = new Float2(7.0f);
|
||||
protected static readonly Float2 KeyframesSize = new Float2(8.0f);
|
||||
|
||||
/// <summary>
|
||||
/// The colors for the keyframe points.
|
||||
@@ -326,6 +369,63 @@ namespace FlaxEditor.GUI
|
||||
private Color _labelsColor;
|
||||
private Font _labelsFont;
|
||||
|
||||
/// <summary>
|
||||
/// Preset values for <see cref="CurvePreset"/> to be applied to a <see cref="CurveEditor{T}"/>.
|
||||
/// </summary>
|
||||
protected Dictionary<CurvePreset, CurveEditorPreset> Presets = new Dictionary<CurvePreset, CurveEditorPreset>
|
||||
{
|
||||
{ CurvePreset.Constant, new CurveEditorPreset
|
||||
{
|
||||
LinearTangents = true,
|
||||
Points = new List<CurvePresetPoint>
|
||||
{
|
||||
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<CurvePresetPoint>
|
||||
{
|
||||
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<CurvePresetPoint>
|
||||
{
|
||||
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<CurvePresetPoint>
|
||||
{
|
||||
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<CurvePresetPoint>
|
||||
{
|
||||
new CurvePresetPoint { Time = 0f, Value = 0f, TangentIn = 0f, TangentOut = 0f },
|
||||
new CurvePresetPoint { Time = 1f, Value = 1f, TangentIn = 0f, TangentOut = 0f },
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The keyframe UI points.
|
||||
/// </summary>
|
||||
@@ -568,6 +668,28 @@ namespace FlaxEditor.GUI
|
||||
/// <param name="indicesToRemove">The list of indices of the keyframes to remove.</param>
|
||||
protected abstract void RemoveKeyframesInternal(HashSet<int> indicesToRemove);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to convert a float to the type of the type wildcard of the curve editor.
|
||||
/// </summary>
|
||||
/// <param name="value">The float.</param>
|
||||
/// <returns>The converted value.</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when showing a context menu. Can be used to add custom buttons with actions.
|
||||
/// </summary>
|
||||
@@ -752,6 +874,17 @@ namespace FlaxEditor.GUI
|
||||
ShowCurve(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a <see cref="CurvePreset"/> to the curve editor.
|
||||
/// </summary>
|
||||
/// <param name="preset">The preset.</param>
|
||||
public virtual void ApplyPreset(CurvePreset preset)
|
||||
{
|
||||
// Remove existing keyframes
|
||||
SelectAll();
|
||||
RemoveKeyframes();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void DrawCurve(ref Rectangle viewRect)
|
||||
{
|
||||
@@ -2312,6 +2486,30 @@ namespace FlaxEditor.GUI
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void SetScaleInternal(ref Float2 scale)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -99,6 +99,11 @@ namespace FlaxEditor.GUI.Input
|
||||
/// </summary>
|
||||
public event Action SlidingEnd;
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, pressing the arrow up or down key increments/ decrements the value.
|
||||
/// </summary>
|
||||
public bool ArrowKeysIncrement = true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the slider speed. Use value 0 to disable and hide slider UI.
|
||||
/// </summary>
|
||||
@@ -239,6 +244,27 @@ namespace FlaxEditor.GUI.Input
|
||||
ResetViewOffset();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnKeyDown(KeyboardKeys key)
|
||||
{
|
||||
if (ArrowKeysIncrement && (key == KeyboardKeys.ArrowUp || key == KeyboardKeys.ArrowDown))
|
||||
{
|
||||
bool altDown = Root.GetKey(KeyboardKeys.Alt);
|
||||
bool shiftDown = Root.GetKey(KeyboardKeys.Shift);
|
||||
bool controlDown = Root.GetKey(KeyboardKeys.Control);
|
||||
float deltaValue = altDown ? 0.1f : (shiftDown ? 10f : (controlDown ? 100f : 1f));
|
||||
float slideDelta = key == KeyboardKeys.ArrowUp ? deltaValue : -deltaValue;
|
||||
|
||||
_startSlideValue = Value;
|
||||
ApplySliding(slideDelta);
|
||||
EndSliding();
|
||||
Focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnKeyDown(key);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseDown(Float2 location, MouseButton button)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -36,11 +36,12 @@ public sealed class ViewportRubberBandSelector
|
||||
/// Triggers the start of a rubber band selection.
|
||||
/// </summary>
|
||||
/// <returns>True if selection started, otherwise false.</returns>
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -158,6 +158,7 @@ namespace FlaxEditor.Modules
|
||||
private ContextMenuButton _menuToolsProfilerWindow;
|
||||
private ContextMenuButton _menuToolsSetTheCurrentSceneViewAsDefault;
|
||||
private ContextMenuButton _menuToolsTakeScreenshot;
|
||||
private ContextMenuButton _menuToolsOpenLocalFolder;
|
||||
private ContextMenuChildMenu _menuWindowApplyWindowLayout;
|
||||
|
||||
private ToolStripButton _toolStripSaveAll;
|
||||
@@ -733,6 +734,16 @@ namespace FlaxEditor.Modules
|
||||
_menuToolsTakeScreenshot = cm.AddButton("Take screenshot", inputOptions.TakeScreenshot, Editor.Windows.TakeScreenshot);
|
||||
cm.AddSeparator();
|
||||
cm.AddButton("Plugins", () => Editor.Windows.PluginsWin.Show());
|
||||
cm.AddSeparator();
|
||||
var childMenu = cm.AddChildMenu("Open Product Local folder");
|
||||
childMenu.ContextMenu.AddButton("Editor", () => FileSystem.ShowFileExplorer(Globals.ProductLocalFolder));
|
||||
_menuToolsOpenLocalFolder = childMenu.ContextMenu.AddButton("Game", () =>
|
||||
{
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
GameSettings settings = GameSettings.Load<GameSettings>();
|
||||
string path = Path.Combine(localAppData, settings.CompanyName, settings.ProductName);
|
||||
FileSystem.ShowFileExplorer(path);
|
||||
});
|
||||
|
||||
// Window
|
||||
MenuWindow = MainMenu.AddButton("Window");
|
||||
@@ -1084,6 +1095,10 @@ namespace FlaxEditor.Modules
|
||||
_menuToolsBuildNavMesh.Enabled = canEdit;
|
||||
_menuToolsCancelBuilding.Enabled = GameCooker.IsRunning;
|
||||
_menuToolsSetTheCurrentSceneViewAsDefault.Enabled = Level.ScenesCount > 0;
|
||||
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
GameSettings settings = GameSettings.Load<GameSettings>();
|
||||
string path = Path.Combine(localAppData, settings.CompanyName, settings.ProductName);
|
||||
_menuToolsOpenLocalFolder.Enabled = Directory.Exists(path);
|
||||
|
||||
c.PerformLayout();
|
||||
}
|
||||
|
||||
@@ -490,10 +490,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)
|
||||
|
||||
@@ -571,6 +571,10 @@ namespace FlaxEditor.Options
|
||||
[EditorDisplay("View Flags"), EditorOrder(3260)]
|
||||
public InputBinding DebugDraw = new InputBinding(KeyboardKeys.Alpha4, KeyboardKeys.Control, KeyboardKeys.Shift);
|
||||
|
||||
[DefaultValue(typeof(InputBinding), "None")]
|
||||
[EditorDisplay("View Flags"), EditorOrder(3270)]
|
||||
public InputBinding Particles = new InputBinding(KeyboardKeys.None);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Interface
|
||||
|
||||
@@ -42,6 +42,7 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
if (value is BoxCollider collider)
|
||||
collider.AutoResize(!_keepLocalOrientation);
|
||||
}
|
||||
Presenter.OnModified();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,11 @@ namespace FlaxEditor.SceneGraph.Actors
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the model used by this actor.
|
||||
/// </summary>
|
||||
public Model Model => ((StaticModel)Actor).Model;
|
||||
|
||||
/// <inheritdoc />
|
||||
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<SceneGraphNode>();
|
||||
}
|
||||
|
||||
private static bool TryCollisionData(Model model, BinaryAssetItem assetItem, out CollisionData collisionData)
|
||||
{
|
||||
collisionData = FlaxEngine.Content.Load<CollisionData>(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<CollisionData>())
|
||||
{
|
||||
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<CollisionData>())
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[]
|
||||
{
|
||||
|
||||
@@ -453,7 +453,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
}
|
||||
}
|
||||
|
||||
private class CurveNode<T> : SurfaceNode where T : struct
|
||||
private class CurveNode<T> : ResizableSurfaceNode where T : struct
|
||||
{
|
||||
private BezierCurveEditor<T> _curve;
|
||||
private bool _isSavingCurve;
|
||||
@@ -467,7 +467,7 @@ namespace FlaxEditor.Surface.Archetypes
|
||||
Create = (id, context, arch, groupArch) => new CurveNode<T>(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
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
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<T>
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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[]
|
||||
{
|
||||
|
||||
182
Source/Editor/Surface/ResizableSurfaceNode.cs
Normal file
182
Source/Editor/Surface/ResizableSurfaceNode.cs
Normal file
@@ -0,0 +1,182 @@
|
||||
// Copyright (c) Wojciech Figat. All rights reserved.
|
||||
|
||||
using FlaxEngine;
|
||||
using FlaxEngine.GUI;
|
||||
|
||||
namespace FlaxEditor.Surface
|
||||
{
|
||||
/// <summary>
|
||||
/// Visject Surface node control that cna be resized.
|
||||
/// </summary>
|
||||
/// <seealso cref="SurfaceNode" />
|
||||
[HideInEditor]
|
||||
public class ResizableSurfaceNode : SurfaceNode
|
||||
{
|
||||
private Float2 _startResizingSize;
|
||||
private Float2 _startResizingCornerOffset;
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the node is currently being resized.
|
||||
/// </summary>
|
||||
protected bool _isResizing;
|
||||
|
||||
/// <summary>
|
||||
/// Index of the Float2 value in the node values list to store node size.
|
||||
/// </summary>
|
||||
protected int _sizeValueIndex = -1;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum node size.
|
||||
/// </summary>
|
||||
protected Float2 _sizeMin = new Float2(240, 160);
|
||||
|
||||
/// <summary>
|
||||
/// Node resizing rectangle bounds.
|
||||
/// </summary>
|
||||
protected Rectangle _resizeButtonRect;
|
||||
|
||||
private Float2 SizeValue
|
||||
{
|
||||
get => (Float2)Values[_sizeValueIndex];
|
||||
set => SetValue(_sizeValueIndex, value, false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ResizableSurfaceNode(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch)
|
||||
: base(id, context, nodeArch, groupArch)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanSelect(ref Float2 location)
|
||||
{
|
||||
return base.CanSelect(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnValuesChanged()
|
||||
{
|
||||
base.OnValuesChanged();
|
||||
|
||||
var size = SizeValue;
|
||||
Resize(size.X, size.Y);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLostFocus()
|
||||
{
|
||||
if (_isResizing)
|
||||
EndResizing();
|
||||
|
||||
base.OnLostFocus();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnEndMouseCapture()
|
||||
{
|
||||
if (_isResizing)
|
||||
EndResizing();
|
||||
|
||||
base.OnEndMouseCapture();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseUp(Float2 location, MouseButton button)
|
||||
{
|
||||
if (button == MouseButton.Left && _isResizing)
|
||||
{
|
||||
EndResizing();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnMouseUp(location, button);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,18 +14,11 @@ namespace FlaxEditor.Surface
|
||||
/// </summary>
|
||||
/// <seealso cref="SurfaceNode" />
|
||||
[HideInEditor]
|
||||
public class SurfaceComment : SurfaceNode
|
||||
public class SurfaceComment : ResizableSurfaceNode
|
||||
{
|
||||
private Rectangle _colorButtonRect;
|
||||
private Rectangle _resizeButtonRect;
|
||||
private Float2 _startResizingSize;
|
||||
private readonly TextBox _renameTextBox;
|
||||
|
||||
/// <summary>
|
||||
/// True if sizing tool is in use.
|
||||
/// </summary>
|
||||
protected bool _isResizing;
|
||||
|
||||
/// <summary>
|
||||
/// True if rename textbox is active in order to rename comment
|
||||
/// </summary>
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanSelect(ref Float2 location)
|
||||
{
|
||||
return _headerRect.MakeOffsetted(Location).Contains(ref location) && !_resizeButtonRect.MakeOffsetted(Location).Contains(ref location);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -158,6 +122,8 @@ namespace FlaxEditor.Surface
|
||||
/// <inheritdoc />
|
||||
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
|
||||
/// <inheritdoc />
|
||||
protected override Float2 CalculateNodeSize(float width, float height)
|
||||
{
|
||||
return Size;
|
||||
// No margins or headers
|
||||
return new Float2(width, height);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnLostFocus()
|
||||
{
|
||||
// Check if was resizing
|
||||
if (_isResizing)
|
||||
{
|
||||
EndResizing();
|
||||
}
|
||||
|
||||
// Check if was renaming
|
||||
if (_isRenaming)
|
||||
{
|
||||
Rename(_renameTextBox.Text);
|
||||
StopRenaming();
|
||||
}
|
||||
|
||||
// Base
|
||||
base.OnLostFocus();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnEndMouseCapture()
|
||||
{
|
||||
// Check if was resizing
|
||||
if (_isResizing)
|
||||
{
|
||||
EndResizing();
|
||||
}
|
||||
else
|
||||
{
|
||||
base.OnEndMouseCapture();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool ContainsPoint(ref Float2 location, bool precise)
|
||||
{
|
||||
return _headerRect.Contains(ref location) || _resizeButtonRect.Contains(ref location);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
|
||||
{
|
||||
@@ -394,12 +297,6 @@ namespace FlaxEditor.Surface
|
||||
/// <inheritdoc />
|
||||
public override bool OnMouseUp(Float2 location, MouseButton button)
|
||||
{
|
||||
if (button == MouseButton.Left && _isResizing)
|
||||
{
|
||||
EndResizing();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (base.OnMouseUp(location, button))
|
||||
return true;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,6 +444,9 @@ namespace FlaxEditor.Utilities
|
||||
/// <returns>The result value.</returns>
|
||||
public static double Parse(string text)
|
||||
{
|
||||
// Hack to allow parsing numbers while using "_" as a separator (like this: 1_000)
|
||||
text = text.Replace("_", string.Empty);
|
||||
|
||||
var tokens = Tokenize(text);
|
||||
var rpn = OrderTokens(tokens);
|
||||
return EvaluateRPN(rpn);
|
||||
|
||||
@@ -1069,6 +1069,7 @@ namespace FlaxEditor.Viewport
|
||||
InputActions.Add(options => options.Fog, () => Task.ViewFlags ^= ViewFlags.Fog);
|
||||
InputActions.Add(options => options.SpecularLight, () => Task.ViewFlags ^= ViewFlags.SpecularLight);
|
||||
InputActions.Add(options => options.Decals, () => Task.ViewFlags ^= ViewFlags.Decals);
|
||||
InputActions.Add(options => options.Particles, () => Task.ViewFlags ^= ViewFlags.Particles);
|
||||
InputActions.Add(options => options.CustomPostProcess, () => Task.ViewFlags ^= ViewFlags.CustomPostProcess);
|
||||
InputActions.Add(options => options.Bloom, () => Task.ViewFlags ^= ViewFlags.Bloom);
|
||||
InputActions.Add(options => options.ToneMapping, () => Task.ViewFlags ^= ViewFlags.ToneMapping);
|
||||
@@ -2147,6 +2148,7 @@ namespace FlaxEditor.Viewport
|
||||
new ViewFlagOptions(ViewFlags.Fog, "Fog", Editor.Instance.Options.Options.Input.Fog),
|
||||
new ViewFlagOptions(ViewFlags.SpecularLight, "Specular Light", Editor.Instance.Options.Options.Input.SpecularLight),
|
||||
new ViewFlagOptions(ViewFlags.Decals, "Decals", Editor.Instance.Options.Options.Input.Decals),
|
||||
new ViewFlagOptions(ViewFlags.Particles, "Particles", Editor.Instance.Options.Options.Input.Particles),
|
||||
new ViewFlagOptions(ViewFlags.CustomPostProcess, "Custom Post Process", Editor.Instance.Options.Options.Input.CustomPostProcess),
|
||||
new ViewFlagOptions(ViewFlags.Bloom, "Bloom", Editor.Instance.Options.Options.Input.Bloom),
|
||||
new ViewFlagOptions(ViewFlags.ToneMapping, "Tone Mapping", Editor.Instance.Options.Options.Input.ToneMapping),
|
||||
@@ -2166,12 +2168,13 @@ namespace FlaxEditor.Viewport
|
||||
if (cm.Visible == false)
|
||||
return;
|
||||
var ccm = (ContextMenu)cm;
|
||||
var flags = Task.View.Flags;
|
||||
foreach (var e in ccm.Items)
|
||||
{
|
||||
if (e is ContextMenuButton b && b.Tag != null)
|
||||
{
|
||||
var v = (ViewFlags)b.Tag;
|
||||
b.Icon = (Task.View.Flags & v) != 0 ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
|
||||
b.Icon = (flags & v) != 0 ? Style.Current.CheckBoxTick : SpriteHandle.Invalid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,7 +629,7 @@ namespace FlaxEditor.Viewport
|
||||
base.OnLeftMouseButtonDown();
|
||||
|
||||
if (!IsAltKeyDown)
|
||||
_rubberBandSelector.TryStartingRubberBandSelection();
|
||||
_rubberBandSelector.TryStartingRubberBandSelection(_viewMousePos);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace FlaxEditor.Windows
|
||||
{
|
||||
Parent = this
|
||||
};
|
||||
_saveButton = (ToolStripButton)toolstrip.AddButton(editor.Icons.Save64, SaveData).LinkTooltip("Save");
|
||||
_saveButton = (ToolStripButton)toolstrip.AddButton(editor.Icons.Save64, SaveData).LinkTooltip("Save.");
|
||||
_saveButton.Enabled = false;
|
||||
|
||||
_tabs = new Tabs
|
||||
@@ -104,6 +104,8 @@ namespace FlaxEditor.Windows
|
||||
{
|
||||
_saveButton.Enabled = true;
|
||||
_isDataDirty = true;
|
||||
if (!Title.EndsWith('*'))
|
||||
Title += "*";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +115,8 @@ namespace FlaxEditor.Windows
|
||||
{
|
||||
_saveButton.Enabled = false;
|
||||
_isDataDirty = false;
|
||||
if (Title.EndsWith('*'))
|
||||
Title = Title.Remove(Title.Length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,35 @@ bool Material::IsMaterialInstance() const
|
||||
return false;
|
||||
}
|
||||
|
||||
#if USE_EDITOR
|
||||
|
||||
void Material::GetReferences(Array<Guid>& assets, Array<String>& files) const
|
||||
{
|
||||
ShaderAssetTypeBase<MaterialBase>::GetReferences(assets, files);
|
||||
|
||||
// Collect references from material graph (needs to load it)
|
||||
if (!WaitForLoaded() && HasChunk(SHADER_FILE_CHUNK_VISJECT_SURFACE))
|
||||
{
|
||||
ScopeLock lock(Locker);
|
||||
if (!LoadChunks(GET_CHUNK_FLAG(SHADER_FILE_CHUNK_VISJECT_SURFACE)))
|
||||
{
|
||||
const auto surfaceChunk = GetChunk(SHADER_FILE_CHUNK_VISJECT_SURFACE);
|
||||
if (surfaceChunk)
|
||||
{
|
||||
MemoryReadStream stream(surfaceChunk->Get(), surfaceChunk->Size());
|
||||
MaterialGraph graph;
|
||||
if (!graph.Load(&stream, false))
|
||||
{
|
||||
graph.GetReferences(assets);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
const MaterialInfo& Material::GetInfo() const
|
||||
{
|
||||
if (_materialShader)
|
||||
|
||||
@@ -38,6 +38,9 @@ public:
|
||||
public:
|
||||
// [MaterialBase]
|
||||
bool IsMaterialInstance() const override;
|
||||
#if USE_EDITOR
|
||||
void GetReferences(Array<Guid>& assets, Array<String>& files) const override;
|
||||
#endif
|
||||
|
||||
// [IMaterial]
|
||||
const MaterialInfo& GetInfo() const override;
|
||||
|
||||
@@ -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> asset = Content::LoadAsync<Asset>(context.TargetAssetPath);
|
||||
if (asset && !asset->WaitForLoaded())
|
||||
{
|
||||
auto* model = ScriptingObject::Cast<ModelBase>(asset);
|
||||
auto* animation = ScriptingObject::Cast<Animation>(asset);
|
||||
if (restoreModelOptions && model)
|
||||
{
|
||||
// Copy general properties
|
||||
data->MinScreenSize = model->MinScreenSize;
|
||||
}
|
||||
if (restoreMaterials && model)
|
||||
{
|
||||
// Copy material settings
|
||||
|
||||
@@ -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<ManagedBinaryModule*>(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<StringView>& 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;
|
||||
|
||||
@@ -480,6 +480,7 @@ DebugDrawCall WriteLists(int32& vertexCounter, const Array<T>& listA, const Arra
|
||||
|
||||
FORCE_INLINE DebugTriangle* AppendTriangles(int32 count, float duration, bool depthTest)
|
||||
{
|
||||
PROFILE_MEM(EngineDebug);
|
||||
Array<DebugTriangle>* list;
|
||||
if (depthTest)
|
||||
list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultTriangles : &Context->DebugDrawDepthTest.OneFrameTriangles;
|
||||
@@ -490,6 +491,19 @@ FORCE_INLINE DebugTriangle* AppendTriangles(int32 count, float duration, bool de
|
||||
return list->Get() + startIndex;
|
||||
}
|
||||
|
||||
FORCE_INLINE DebugTriangle* AppendWireTriangles(int32 count, float duration, bool depthTest)
|
||||
{
|
||||
PROFILE_MEM(EngineDebug);
|
||||
Array<DebugTriangle>* list;
|
||||
if (depthTest)
|
||||
list = duration > 0 ? &Context->DebugDrawDepthTest.DefaultWireTriangles : &Context->DebugDrawDepthTest.OneFrameWireTriangles;
|
||||
else
|
||||
list = duration > 0 ? &Context->DebugDrawDefault.DefaultWireTriangles : &Context->DebugDrawDefault.OneFrameWireTriangles;
|
||||
const int32 startIndex = list->Count();
|
||||
list->AddUninitialized(count);
|
||||
return list->Get() + startIndex;
|
||||
}
|
||||
|
||||
inline void DrawText3D(const DebugText3D& t, const RenderContext& renderContext, const Float3& viewUp, const Matrix& f, const Matrix& vp, const Viewport& viewport, GPUContext* context, GPUTextureView* target, GPUTextureView* depthBuffer)
|
||||
{
|
||||
Matrix w, fw, m;
|
||||
@@ -527,7 +541,7 @@ DebugDrawService DebugDrawServiceInstance;
|
||||
|
||||
bool DebugDrawService::Init()
|
||||
{
|
||||
PROFILE_MEM(Graphics);
|
||||
PROFILE_MEM(EngineDebug);
|
||||
Context = &GlobalContext;
|
||||
|
||||
// Init wireframe sphere cache
|
||||
@@ -646,7 +660,7 @@ void DebugDrawService::Update()
|
||||
}
|
||||
|
||||
PROFILE_CPU();
|
||||
PROFILE_MEM(Graphics);
|
||||
PROFILE_MEM(EngineDebug);
|
||||
|
||||
// Update lists
|
||||
float deltaTime = Time::Update.DeltaTime.GetTotalSeconds();
|
||||
@@ -1102,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)
|
||||
@@ -1120,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)
|
||||
@@ -1149,6 +1165,7 @@ void DebugDraw::DrawLines(const Span<Float3>& 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);
|
||||
@@ -1188,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;
|
||||
@@ -1212,6 +1230,7 @@ void DebugDraw::DrawLines(const Span<Double3>& 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);
|
||||
@@ -1258,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)
|
||||
{
|
||||
@@ -1298,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)
|
||||
{
|
||||
@@ -1332,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)
|
||||
{
|
||||
@@ -1366,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)
|
||||
{
|
||||
@@ -1407,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)
|
||||
{
|
||||
@@ -1442,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;
|
||||
@@ -1473,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;)
|
||||
@@ -1503,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;
|
||||
@@ -1558,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;
|
||||
@@ -1714,7 +1742,7 @@ void DebugDraw::DrawWireTriangles(const Span<Float3>& vertices, const Color& col
|
||||
DebugTriangle t;
|
||||
t.Color = Color32(color);
|
||||
t.TimeLeft = duration;
|
||||
auto dst = AppendTriangles(vertices.Length() / 3, duration, depthTest);
|
||||
auto dst = AppendWireTriangles(vertices.Length() / 3, duration, depthTest);
|
||||
const Float3 origin = Context->Origin;
|
||||
for (int32 i = 0; i < vertices.Length();)
|
||||
{
|
||||
@@ -1736,7 +1764,7 @@ void DebugDraw::DrawWireTriangles(const Span<Float3>& vertices, const Span<int32
|
||||
DebugTriangle t;
|
||||
t.Color = Color32(color);
|
||||
t.TimeLeft = duration;
|
||||
auto dst = AppendTriangles(indices.Length() / 3, duration, depthTest);
|
||||
auto dst = AppendWireTriangles(indices.Length() / 3, duration, depthTest);
|
||||
const Float3 origin = Context->Origin;
|
||||
for (int32 i = 0; i < indices.Length();)
|
||||
{
|
||||
@@ -1758,7 +1786,7 @@ void DebugDraw::DrawWireTriangles(const Span<Double3>& vertices, const Color& co
|
||||
DebugTriangle t;
|
||||
t.Color = Color32(color);
|
||||
t.TimeLeft = duration;
|
||||
auto dst = AppendTriangles(vertices.Length() / 3, duration, depthTest);
|
||||
auto dst = AppendWireTriangles(vertices.Length() / 3, duration, depthTest);
|
||||
const Double3 origin = Context->Origin;
|
||||
for (int32 i = 0; i < vertices.Length();)
|
||||
{
|
||||
@@ -1780,7 +1808,7 @@ void DebugDraw::DrawWireTriangles(const Span<Double3>& vertices, const Span<int3
|
||||
DebugTriangle t;
|
||||
t.Color = Color32(color);
|
||||
t.TimeLeft = duration;
|
||||
auto dst = AppendTriangles(indices.Length() / 3, duration, depthTest);
|
||||
auto dst = AppendWireTriangles(indices.Length() / 3, duration, depthTest);
|
||||
const Double3 origin = Context->Origin;
|
||||
for (int32 i = 0; i < indices.Length();)
|
||||
{
|
||||
@@ -1847,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)
|
||||
@@ -1941,6 +1970,7 @@ namespace
|
||||
void DrawCylinder(Array<DebugTriangle>* 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;
|
||||
@@ -2012,6 +2042,7 @@ namespace
|
||||
|
||||
void DrawCone(Array<DebugTriangle>* 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);
|
||||
@@ -2101,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<DebugTriangle>* list;
|
||||
@@ -2133,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);
|
||||
@@ -2199,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;)
|
||||
{
|
||||
@@ -2227,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;)
|
||||
{
|
||||
@@ -2242,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<DebugText2D>* list = duration > 0 ? &Context->DebugDrawDefault.DefaultText2D : &Context->DebugDrawDefault.OneFrameText2D;
|
||||
auto& t = list->AddOne();
|
||||
t.Text.Resize(text.Length() + 1);
|
||||
@@ -2257,6 +2293,7 @@ void DebugDraw::DrawText(const StringView& text, const Vector3& position, const
|
||||
{
|
||||
if (text.Length() == 0 || size < 4)
|
||||
return;
|
||||
PROFILE_MEM(EngineDebug);
|
||||
Array<DebugText3D>* list = duration > 0 ? &Context->DebugDrawDefault.DefaultText3D : &Context->DebugDrawDefault.OneFrameText3D;
|
||||
auto& t = list->AddOne();
|
||||
t.Text.Resize(text.Length() + 1);
|
||||
@@ -2274,6 +2311,7 @@ void DebugDraw::DrawText(const StringView& text, const Transform& transform, con
|
||||
{
|
||||
if (text.Length() == 0 || size < 4)
|
||||
return;
|
||||
PROFILE_MEM(EngineDebug);
|
||||
Array<DebugText3D>* list = duration > 0 ? &Context->DebugDrawDefault.DefaultText3D : &Context->DebugDrawDefault.OneFrameText3D;
|
||||
auto& t = list->AddOne();
|
||||
t.Text.Resize(text.Length() + 1);
|
||||
|
||||
@@ -44,20 +44,39 @@ void Foliage::AddToCluster(ChunkedArray<FoliageCluster, FOLIAGE_CLUSTER_CHUNKS_S
|
||||
ASSERT(instance.Bounds.Radius > 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(ChunkedArray<FoliageCluster, FOLIAGE_CLUSTER_CHUNKS_S
|
||||
// Setup children
|
||||
const Vector3 min = cluster->Bounds.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++)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -33,6 +33,11 @@ public:
|
||||
/// </summary>
|
||||
float MaxCullDistance;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
int32 IsMinor : 1;
|
||||
|
||||
/// <summary>
|
||||
/// The child clusters. If any element is valid then all are created.
|
||||
/// </summary>
|
||||
|
||||
@@ -1075,20 +1075,25 @@ API_ENUM(Attributes="Flags") enum class ViewFlags : uint64
|
||||
/// </summary>
|
||||
LightsDebug = 1 << 27,
|
||||
|
||||
/// <summary>
|
||||
/// Shows/hides particle effects.
|
||||
/// </summary>
|
||||
Particles = 1 << 28,
|
||||
|
||||
/// <summary>
|
||||
/// Default flags for Game.
|
||||
/// </summary>
|
||||
DefaultGame = Reflections | DepthOfField | Fog | Decals | MotionBlur | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | GlobalSDF | Sky,
|
||||
DefaultGame = Reflections | DepthOfField | Fog | Decals | MotionBlur | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | GlobalSDF | Sky | Particles,
|
||||
|
||||
/// <summary>
|
||||
/// Default flags for Editor.
|
||||
/// </summary>
|
||||
DefaultEditor = Reflections | Fog | Decals | DebugDraw | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | EditorSprites | ContactShadows | GlobalSDF | Sky,
|
||||
DefaultEditor = Reflections | Fog | Decals | DebugDraw | SSR | AO | GI | DirectionalLights | PointLights | SpotLights | SkyLights | Shadows | SpecularLight | AntiAliasing | CustomPostProcess | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | EditorSprites | ContactShadows | GlobalSDF | Sky | Particles,
|
||||
|
||||
/// <summary>
|
||||
/// Default flags for materials/models previews generating.
|
||||
/// </summary>
|
||||
DefaultAssetPreview = Reflections | Decals | DirectionalLights | PointLights | SpotLights | SkyLights | SpecularLight | AntiAliasing | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | Sky,
|
||||
DefaultAssetPreview = Reflections | Decals | DirectionalLights | PointLights | SpotLights | SkyLights | SpecularLight | AntiAliasing | Bloom | ToneMapping | EyeAdaptation | CameraArtifacts | LensFlares | ContactShadows | Sky | Particles,
|
||||
};
|
||||
|
||||
DECLARE_ENUM_OPERATORS(ViewFlags);
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
/// <summary>
|
||||
/// Current materials shader version.
|
||||
/// </summary>
|
||||
#define MATERIAL_GRAPH_VERSION 178
|
||||
#define MATERIAL_GRAPH_VERSION 179
|
||||
|
||||
class Material;
|
||||
class GPUShader;
|
||||
|
||||
@@ -191,7 +191,7 @@ bool GlobalIlluminationFeature::Bind(MaterialShader::BindParameters& params, Spa
|
||||
{
|
||||
// Unbind SRVs to prevent issues
|
||||
data.DDGI.CascadesCount = 0;
|
||||
data.DDGI.FallbackIrradiance = Float3::Zero;
|
||||
data.DDGI.FallbackIrradiance = Float4::Zero;
|
||||
params.GPUContext->UnBindSR(srv + 0);
|
||||
params.GPUContext->UnBindSR(srv + 1);
|
||||
params.GPUContext->UnBindSR(srv + 2);
|
||||
|
||||
@@ -17,7 +17,7 @@ public:
|
||||
/// <summary>
|
||||
/// Mesh data stream.
|
||||
/// </summary>
|
||||
struct Stream
|
||||
struct FLAXENGINE_API Stream
|
||||
{
|
||||
friend MeshAccessor;
|
||||
|
||||
|
||||
@@ -378,7 +378,7 @@ API_STRUCT() struct FLAXENGINE_API GlobalIlluminationSettings : ISerializable
|
||||
/// The irradiance lighting outside the GI range used as a fallback to prevent pure-black scene outside the Global Illumination range.
|
||||
/// </summary>
|
||||
API_FIELD(Attributes="EditorOrder(40), PostProcessSetting((int)GlobalIlluminationSettingsOverride.FallbackIrradiance)")
|
||||
Color FallbackIrradiance = Color::Black;
|
||||
Color FallbackIrradiance = Color::Transparent;
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
|
||||
@@ -620,6 +620,40 @@ void RenderTools::ComputeSphereModelDrawMatrix(const RenderView& view, const Flo
|
||||
resultIsViewInside = Float3::DistanceSquared(view.Position, position) < Math::Square(radius * 1.1f); // Manually tweaked bias
|
||||
}
|
||||
|
||||
Float3 RenderTools::GetColorQuantizationError(PixelFormat format)
|
||||
{
|
||||
Float3 mantissaBits;
|
||||
switch (format)
|
||||
{
|
||||
case PixelFormat::R11G11B10_Float:
|
||||
mantissaBits = Float3(6, 6, 5);
|
||||
break;
|
||||
case PixelFormat::R10G10B10A2_UNorm:
|
||||
mantissaBits = Float3(10, 10, 10);
|
||||
break;
|
||||
case PixelFormat::R16G16B16A16_Float:
|
||||
mantissaBits = Float3(16, 16, 16);
|
||||
break;
|
||||
case PixelFormat::R32G32B32A32_Float:
|
||||
mantissaBits = Float3(23, 23, 23);
|
||||
break;
|
||||
case PixelFormat::R9G9B9E5_SharedExp:
|
||||
mantissaBits = Float3(5, 6, 5);
|
||||
break;
|
||||
case PixelFormat::R8G8B8A8_UNorm:
|
||||
case PixelFormat::B8G8R8A8_UNorm:
|
||||
mantissaBits = Float3(8, 8, 8);
|
||||
break;
|
||||
default:
|
||||
return Float3::Zero;
|
||||
}
|
||||
return {
|
||||
Math::Pow(0.5f, mantissaBits.X),
|
||||
Math::Pow(0.5f, mantissaBits.Y),
|
||||
Math::Pow(0.5f, mantissaBits.Z)
|
||||
};
|
||||
}
|
||||
|
||||
int32 MipLevelsCount(int32 width)
|
||||
{
|
||||
int32 result = 1;
|
||||
|
||||
@@ -140,6 +140,9 @@ public:
|
||||
static void CalculateTangentFrame(Float3& resultNormal, Float4& resultTangent, const Float3& normal, const Float3& tangent);
|
||||
|
||||
static void ComputeSphereModelDrawMatrix(const RenderView& view, const Float3& position, float radius, Matrix& resultWorld, bool& resultIsViewInside);
|
||||
|
||||
// Calculates error for a given render target format to reduce floating-point precision artifacts via QuantizeColor (from Noise.hlsl).
|
||||
static Float3 GetColorQuantizationError(PixelFormat format);
|
||||
};
|
||||
|
||||
// Calculates mip levels count for a texture 1D.
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <Windows.h>
|
||||
#include <d3d12.h>
|
||||
#include <ThirdParty/WinPixEventRuntime/pix3.h>
|
||||
#endif
|
||||
#include "GPUContextDX12.h"
|
||||
|
||||
@@ -81,6 +81,8 @@ Delegate<const Float2&, float> Input::MouseWheel;
|
||||
Delegate<const Float2&> Input::MouseMove;
|
||||
Delegate<const Float2&> Input::MouseMoveRelative;
|
||||
Action Input::MouseLeave;
|
||||
Delegate<InputGamepadIndex, GamepadButton> Input::GamepadButtonDown;
|
||||
Delegate<InputGamepadIndex, GamepadButton> Input::GamepadButtonUp;
|
||||
Delegate<const Float2&, int32> Input::TouchDown;
|
||||
Delegate<const Float2&, int32> Input::TouchMove;
|
||||
Delegate<const Float2&, int32> Input::TouchUp;
|
||||
@@ -1045,6 +1047,19 @@ void InputService::Update()
|
||||
break;
|
||||
}
|
||||
}
|
||||
// TODO: route gamepad button events into global InputEvents queue to improve processing
|
||||
for (int32 i = 0; i < Input::Gamepads.Count(); i++)
|
||||
{
|
||||
auto gamepad = Input::Gamepads[i];
|
||||
for (int32 buttonIdx = 1; buttonIdx < (int32)GamepadButton::MAX; buttonIdx++)
|
||||
{
|
||||
GamepadButton button = (GamepadButton)buttonIdx;
|
||||
if (gamepad->GetButtonDown(button))
|
||||
Input::GamepadButtonDown((InputGamepadIndex)i, button);
|
||||
else if (gamepad->GetButtonUp(button))
|
||||
Input::GamepadButtonUp((InputGamepadIndex)i, button);
|
||||
}
|
||||
}
|
||||
|
||||
// Update all actions
|
||||
for (int32 i = 0; i < Input::ActionMappings.Count(); i++)
|
||||
|
||||
@@ -118,6 +118,16 @@ public:
|
||||
/// </summary>
|
||||
API_EVENT() static Action MouseLeave;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when gamepad button goes down.
|
||||
/// </summary>
|
||||
API_EVENT() static Delegate<InputGamepadIndex, GamepadButton> GamepadButtonDown;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when gamepad button goes up.
|
||||
/// </summary>
|
||||
API_EVENT() static Delegate<InputGamepadIndex, GamepadButton> GamepadButtonUp;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when touch action begins.
|
||||
/// </summary>
|
||||
|
||||
@@ -107,6 +107,7 @@ void LightWithShadow::Serialize(SerializeStream& stream, const void* otherObj)
|
||||
SERIALIZE(ContactShadowsLength);
|
||||
SERIALIZE(ShadowsUpdateRate);
|
||||
SERIALIZE(ShadowsUpdateRateAtDistance);
|
||||
SERIALIZE(ShadowsResolution);
|
||||
}
|
||||
|
||||
void LightWithShadow::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier)
|
||||
@@ -125,4 +126,5 @@ void LightWithShadow::Deserialize(DeserializeStream& stream, ISerializeModifier*
|
||||
DESERIALIZE(ContactShadowsLength);
|
||||
DESERIALIZE(ShadowsUpdateRate);
|
||||
DESERIALIZE(ShadowsUpdateRateAtDistance);
|
||||
DESERIALIZE(ShadowsResolution);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ protected:
|
||||
void OnDisable() override;
|
||||
#if USE_EDITOR
|
||||
void OnBoundsChanged(const BoundingBox& prevBounds) override;
|
||||
void OnActiveInTreeChanged() override;
|
||||
Color GetWiresColor() override;
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -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> Scene;
|
||||
DateTime Time;
|
||||
BoundingBox DirtyBounds;
|
||||
bool SpecificScene;
|
||||
};
|
||||
|
||||
CriticalSection NavBuildQueueLocker;
|
||||
@@ -713,6 +715,7 @@ Array<BuildRequest> NavBuildQueue;
|
||||
|
||||
CriticalSection NavBuildTasksLocker;
|
||||
int32 NavBuildTasksMaxCount = 0;
|
||||
bool NavBuildCheckMissingNavMeshes = false;
|
||||
Array<class NavMeshTileBuildTask*> 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<OnSceneUnloading>();
|
||||
}
|
||||
|
||||
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<BoundingBox, InlinedAllocation<8>> 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>();
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -111,7 +111,7 @@ public:
|
||||
/// <param name="startPosition">The start position.</param>
|
||||
/// <param name="hitInfo">The result hit information. Valid only when query succeed.</param>
|
||||
/// <param name="maxDistance">The maximum distance to search for wall (search radius).</param>
|
||||
/// <returns>True if ray hits an matching object, otherwise false.</returns>
|
||||
/// <returns>True if ray hits a matching object, otherwise false.</returns>
|
||||
API_FUNCTION() bool FindDistanceToWall(const Vector3& startPosition, NavMeshHit& hitInfo, float maxDistance = MAX_float) const;
|
||||
|
||||
/// <summary>
|
||||
@@ -187,7 +187,7 @@ public:
|
||||
/// <param name="startPosition">The start position.</param>
|
||||
/// <param name="endPosition">The end position.</param>
|
||||
/// <param name="hitInfo">The result hit information. Valid only when query succeed.</param>
|
||||
/// <returns>True if ray hits an matching object, otherwise false.</returns>
|
||||
/// <returns>True if ray hits a matching object, otherwise false.</returns>
|
||||
API_FUNCTION() bool RayCast(const Vector3& startPosition, const Vector3& endPosition, API_PARAM(Out) NavMeshHit& hitInfo) const;
|
||||
|
||||
public:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -19,7 +19,7 @@ public:
|
||||
/// <param name="startPosition">The start position.</param>
|
||||
/// <param name="hitInfo">The result hit information. Valid only when query succeed.</param>
|
||||
/// <param name="maxDistance">The maximum distance to search for wall (search radius).</param>
|
||||
/// <returns>True if ray hits an matching object, otherwise false.</returns>
|
||||
/// <returns>True if ray hits a matching object, otherwise false.</returns>
|
||||
API_FUNCTION() static bool FindDistanceToWall(const Vector3& startPosition, API_PARAM(Out) NavMeshHit& hitInfo, float maxDistance = MAX_float);
|
||||
|
||||
/// <summary>
|
||||
@@ -81,12 +81,10 @@ public:
|
||||
/// <param name="startPosition">The start position.</param>
|
||||
/// <param name="endPosition">The end position.</param>
|
||||
/// <param name="hitInfo">The result hit information. Valid only when query succeed.</param>
|
||||
/// <returns>True if ray hits an matching object, otherwise false.</returns>
|
||||
/// <returns>True if ray hits a matching object, otherwise false.</returns>
|
||||
API_FUNCTION() static bool RayCast(const Vector3& startPosition, const Vector3& endPosition, API_PARAM(Out) NavMeshHit& hitInfo);
|
||||
|
||||
public:
|
||||
#if COMPILE_WITH_NAV_MESH_BUILDER
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if navigation system is during navmesh building (any request is valid or async task active).
|
||||
/// </summary>
|
||||
@@ -100,32 +98,49 @@ public:
|
||||
/// <summary>
|
||||
/// Builds the Nav Mesh for the given scene (discards all its tiles).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <param name="scene">The scene.</param>
|
||||
/// <remarks>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.</remarks>
|
||||
/// <param name="scene">The scene. Pass null to build navmesh for all loaded scenes.</param>
|
||||
/// <param name="timeoutMs">The timeout to wait before building Nav Mesh (in milliseconds).</param>
|
||||
API_FUNCTION() static void BuildNavMesh(Scene* scene, float timeoutMs = 50);
|
||||
API_FUNCTION() static void BuildNavMesh(Scene* scene = nullptr, float timeoutMs = 50);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the Nav Mesh for the given scene (builds only the tiles overlapping the given bounding box).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
/// <remarks>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.</remarks>
|
||||
/// <param name="dirtyBounds">The bounds in world-space to build overlapping tiles.</param>
|
||||
/// <param name="scene">The scene. Pass null to build navmesh for all loaded scenes that intersect with a given bounds.</param>
|
||||
/// <param name="timeoutMs">The timeout to wait before building Nav Mesh (in milliseconds).</param>
|
||||
API_FUNCTION() static void BuildNavMesh(const BoundingBox& dirtyBounds, Scene* scene = nullptr, float timeoutMs = 50);
|
||||
|
||||
/// <summary>
|
||||
/// Builds the Nav Mesh for all the loaded scenes (builds only the tiles overlapping the given bounding box).
|
||||
/// </summary>
|
||||
/// <remarks>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.</remarks>
|
||||
/// <param name="dirtyBounds">The bounds in world-space to build overlapping tiles.</param>
|
||||
/// <param name="timeoutMs">The timeout to wait before building Nav Mesh (in milliseconds).</param>
|
||||
API_FUNCTION() static void BuildNavMesh(const BoundingBox& dirtyBounds, float timeoutMs = 50)
|
||||
{
|
||||
BuildNavMesh(dirtyBounds, nullptr, timeoutMs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builds the Nav Mesh for the given scene (builds only the tiles overlapping the given bounding box).
|
||||
/// [Deprecated in v1.12]
|
||||
/// </summary>
|
||||
/// <remarks>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.</remarks>
|
||||
/// <param name="scene">The scene.</param>
|
||||
/// <param name="dirtyBounds">The bounds in world-space to build overlapping tiles.</param>
|
||||
/// <param name="timeoutMs">The timeout to wait before building Nav Mesh (in milliseconds).</param>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Draws the navigation for all the scenes (uses DebugDraw interface).
|
||||
/// </summary>
|
||||
static void DrawNavMesh();
|
||||
|
||||
#endif
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user