Merge remote-tracking branch 'origin/1.12' into 1.12

This commit is contained in:
Wojtek Figat
2026-03-27 11:22:32 +01:00
129 changed files with 1493 additions and 402 deletions

View File

@@ -175,15 +175,13 @@ namespace FlaxEditor.Content
}
}
bool isExpanded = isAnyChildVisible;
if (isExpanded)
if (!noFilter)
{
Expand(true);
}
else
{
Collapse(true);
bool isExpanded = isAnyChildVisible;
if (isExpanded)
Expand(true);
else
Collapse(true);
}
Visible = isThisVisible | isAnyChildVisible;

View File

@@ -52,6 +52,7 @@ namespace FlaxEditor.CustomEditors
private readonly List<CustomEditor> _children = new List<CustomEditor>();
private ValueContainer _values;
private bool _isSetBlocked;
private bool _isRebuilding;
private bool _skipChildrenRefresh;
private bool _hasValueDirty;
private bool _rebuildOnRefresh;
@@ -178,7 +179,7 @@ namespace FlaxEditor.CustomEditors
public void RebuildLayout()
{
// Skip rebuilding during init
if (CurrentCustomEditor == this)
if (CurrentCustomEditor == this || _isRebuilding)
return;
// Special case for root objects to run normal layout build
@@ -197,6 +198,7 @@ namespace FlaxEditor.CustomEditors
_parent?.RebuildLayout();
return;
}
_isRebuilding = true;
var control = layout.ContainerControl;
var parent = _parent;
var parentScrollV = (_presenter?.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1;
@@ -216,6 +218,7 @@ namespace FlaxEditor.CustomEditors
// Restore scroll value
if (parentScrollV > -1 && _presenter != null && _presenter.Panel.Parent is Panel panel && panel.VScrollBar != null)
panel.VScrollBar.Value = parentScrollV;
_isRebuilding = false;
}
/// <summary>

View File

@@ -61,6 +61,7 @@ public class Editor : EditorModule
options.PrivateDependencies.Add("Renderer");
options.PrivateDependencies.Add("TextureTool");
options.PrivateDependencies.Add("Particles");
options.PrivateDependencies.Add("Terrain");
var platformToolsRoot = Path.Combine(FolderPath, "Cooker", "Platform");
var platformToolsRootExternal = Path.Combine(Globals.EngineRoot, "Source", "Platforms");

View File

@@ -135,11 +135,7 @@ namespace FlaxEditor.GUI.Docking
settings.MaximumSize = Float2.Zero; // Unlimited size
settings.Fullscreen = false;
settings.HasBorder = true;
#if PLATFORM_SDL
settings.SupportsTransparency = true;
#else
settings.SupportsTransparency = false;
#endif
settings.ActivateWhenFirstShown = true;
settings.AllowInput = true;
settings.AllowMinimize = true;
@@ -211,6 +207,7 @@ namespace FlaxEditor.GUI.Docking
{
if (ChildPanelsCount > 0)
return;
// Close window
_window?.Close();
}

View File

@@ -269,8 +269,9 @@ namespace FlaxEditor.GUI.Docking
if (_toDock == null)
return;
if (_toDock.RootWindow.Window != _dragSourceWindow)
_toDock.RootWindow.Window.MouseUp -= OnMouseUp;
var window = _toDock.RootWindow?.Window;
if (window != null && window != _dragSourceWindow)
window.MouseUp -= OnMouseUp;
_dockHintDown?.Parent.RemoveChild(_dockHintDown);
_dockHintUp?.Parent.RemoveChild(_dockHintUp);
@@ -327,10 +328,10 @@ namespace FlaxEditor.GUI.Docking
_toDock?.RootWindow.Window.BringToFront();
//_toDock?.RootWindow.Window.Focus();
#if PLATFORM_SDL
// Make the dragged window transparent when dock hints are visible
_toMove.Window.Window.Opacity = _toDock == null ? 1.0f : DragWindowOpacity;
#else
#if !PLATFORM_SDL
// Bring the drop source always to the top
if (_dragSourceWindow != null)
_dragSourceWindow.BringToFront();

View File

@@ -0,0 +1,267 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using FlaxEditor.Viewport;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.Gizmo;
[HideInEditor]
internal class DirectionGizmo : ContainerControl
{
private IGizmoOwner _owner;
private ViewportProjection _viewportProjection;
private EditorViewport _viewport;
private Vector3 _gizmoCenter;
private float _axisLength = 75.0f;
private float _textAxisLength = 95.0f;
private float _spriteRadius = 12.0f;
private AxisData _xAxisData;
private AxisData _yAxisData;
private AxisData _zAxisData;
private AxisData _negXAxisData;
private AxisData _negYAxisData;
private AxisData _negZAxisData;
private List<AxisData> _axisData = new List<AxisData>();
private int _hoveredAxisIndex = -1;
private SpriteHandle _posHandle;
private SpriteHandle _negHandle;
private FontReference _fontReference;
// Store sprite positions for hover detection
private List<(Float2 position, AxisDirection direction)> _spritePositions = new List<(Float2, AxisDirection)>();
private struct ViewportProjection
{
private Matrix _viewProjection;
private BoundingFrustum _frustum;
private FlaxEngine.Viewport _viewport;
private Vector3 _origin;
public void Init(EditorViewport editorViewport)
{
// Inline EditorViewport.ProjectPoint to save on calculation for large set of points
_viewport = new FlaxEngine.Viewport(0, 0, editorViewport.Width, editorViewport.Height);
_frustum = editorViewport.ViewFrustum;
_viewProjection = _frustum.Matrix;
_origin = editorViewport.Task.View.Origin;
}
public void ProjectPoint(Vector3 worldSpaceLocation, out Float2 viewportSpaceLocation)
{
worldSpaceLocation -= _origin;
_viewport.Project(ref worldSpaceLocation, ref _viewProjection, out var projected);
viewportSpaceLocation = new Float2((float)projected.X, (float)projected.Y);
}
}
private struct AxisData
{
public Float2 Delta;
public float Distance;
public string Label;
public Color AxisColor;
public bool Negative;
public AxisDirection Direction;
}
private enum AxisDirection
{
PosX,
PosY,
PosZ,
NegX,
NegY,
NegZ
}
/// <summary>
/// Constructor of the Direction Gizmo
/// </summary>
/// <param name="owner">The owner of this object.</param>
public DirectionGizmo(IGizmoOwner owner)
{
_owner = owner;
_viewport = owner.Viewport;
_viewportProjection.Init(owner.Viewport);
_xAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "X", AxisColor = new Color(1.0f, 0.0f, 0.02745f, 1.0f), Negative = false, Direction = AxisDirection.PosX };
_yAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "Y", AxisColor = new Color(0.239215f, 1.0f, 0.047058f, 1.0f), Negative = false, Direction = AxisDirection.PosY };
_zAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "Z", AxisColor = new Color(0.0f, 0.0235294f, 1.0f, 1.0f), Negative = false, Direction = AxisDirection.PosZ };
_negXAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "-X", AxisColor = new Color(1.0f, 0.0f, 0.02745f, 1.0f), Negative = true, Direction = AxisDirection.NegX };
_negYAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "-Y", AxisColor = new Color(0.239215f, 1.0f, 0.047058f, 1.0f), Negative = true, Direction = AxisDirection.NegY };
_negZAxisData = new AxisData { Delta = new Float2(0, 0), Distance = 0, Label = "-Z", AxisColor = new Color(0.0f, 0.0235294f, 1.0f, 1.0f), Negative = true, Direction = AxisDirection.NegZ };
_axisData.EnsureCapacity(6);
_spritePositions.EnsureCapacity(6);
_posHandle = Editor.Instance.Icons.VisjectBoxClosed32;
_negHandle = Editor.Instance.Icons.VisjectBoxOpen32;
_fontReference = new FontReference(Style.Current.FontSmall);
_fontReference.Size = 8;
}
private bool IsPointInSprite(Float2 point, Float2 spriteCenter)
{
Float2 delta = point - spriteCenter;
float distanceSq = delta.LengthSquared;
float radiusSq = _spriteRadius * _spriteRadius;
return distanceSq <= radiusSq;
}
/// <inheritdoc />
public override void OnMouseMove(Float2 location)
{
_hoveredAxisIndex = -1;
// Check which axis is being hovered - check from closest to farthest for proper layering
for (int i = _spritePositions.Count - 1; i >= 0; i--)
{
if (IsPointInSprite(location, _spritePositions[i].position))
{
_hoveredAxisIndex = i;
break;
}
}
base.OnMouseMove(location);
}
/// <inheritdoc />
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (base.OnMouseDown(location, button))
return true;
// Check which axis is being clicked - check from closest to farthest for proper layering
for (int i = _spritePositions.Count - 1; i >= 0; i--)
{
if (IsPointInSprite(location, _spritePositions[i].position))
{
OrientViewToAxis(_spritePositions[i].direction);
return true;
}
}
return false;
}
private void OrientViewToAxis(AxisDirection direction)
{
Quaternion orientation = direction switch
{
AxisDirection.PosX => Quaternion.Euler(0, 90, 0),
AxisDirection.NegX => Quaternion.Euler(0, -90, 0),
AxisDirection.PosY => Quaternion.Euler(-90, 0, 0),
AxisDirection.NegY => Quaternion.Euler(90, 0, 0),
AxisDirection.PosZ => Quaternion.Euler(0, 0, 0),
AxisDirection.NegZ => Quaternion.Euler(0, 180, 0),
_ => Quaternion.Identity
};
_viewport.OrientViewport(ref orientation);
}
/// <summary>
/// Used to Draw the gizmo.
/// </summary>
public override void DrawSelf()
{
base.DrawSelf();
var features = Render2D.Features;
Render2D.Features = features & ~Render2D.RenderingFeatures.VertexSnapping;
_viewportProjection.Init(_owner.Viewport);
_gizmoCenter = _viewport.Task.View.WorldPosition + _viewport.Task.View.Direction * 1500;
_viewportProjection.ProjectPoint(_gizmoCenter, out var gizmoCenterScreen);
var relativeCenter = Size * 0.5f;
// Project unit vectors
_viewportProjection.ProjectPoint(_gizmoCenter + Vector3.Right, out var xProjected);
_viewportProjection.ProjectPoint(_gizmoCenter + Vector3.Up, out var yProjected);
_viewportProjection.ProjectPoint(_gizmoCenter + Vector3.Forward, out var zProjected);
_viewportProjection.ProjectPoint(_gizmoCenter - Vector3.Right, out var negXProjected);
_viewportProjection.ProjectPoint(_gizmoCenter - Vector3.Up, out var negYProjected);
_viewportProjection.ProjectPoint(_gizmoCenter - Vector3.Forward, out var negZProjected);
// Normalize by viewport height to keep size independent of FOV and viewport dimensions
float heightNormalization = _viewport.Height / 720.0f; // 720 = reference height
if (_owner.Viewport.UseOrthographicProjection)
heightNormalization /= _owner.Viewport.OrthographicScale * 0.5f; // Fix in ortho view to keep consistent size regardless of zoom level
Float2 xDelta = (xProjected - gizmoCenterScreen) / heightNormalization;
Float2 yDelta = (yProjected - gizmoCenterScreen) / heightNormalization;
Float2 zDelta = (zProjected - gizmoCenterScreen) / heightNormalization;
Float2 negXDelta = (negXProjected - gizmoCenterScreen) / heightNormalization;
Float2 negYDelta = (negYProjected - gizmoCenterScreen) / heightNormalization;
Float2 negZDelta = (negZProjected - gizmoCenterScreen) / heightNormalization;
// Calculate distances from camera to determine draw order
Vector3 cameraPosition = _viewport.Task.View.Position;
float xDistance = (float)Vector3.Distance(cameraPosition, _gizmoCenter + Vector3.Right);
float yDistance = (float)Vector3.Distance(cameraPosition, _gizmoCenter + Vector3.Up);
float zDistance = (float)Vector3.Distance(cameraPosition, _gizmoCenter + Vector3.Forward);
float negXDistance = (float)Vector3.Distance(cameraPosition, _gizmoCenter - Vector3.Right);
float negYDistance = (float)Vector3.Distance(cameraPosition, _gizmoCenter - Vector3.Up);
float negZDistance = (float)Vector3.Distance(cameraPosition, _gizmoCenter - Vector3.Forward);
_xAxisData.Delta = xDelta;
_xAxisData.Distance = xDistance;
_yAxisData.Delta = yDelta;
_yAxisData.Distance = yDistance;
_zAxisData.Delta = zDelta;
_zAxisData.Distance = zDistance;
_negXAxisData.Delta = negXDelta;
_negXAxisData.Distance = negXDistance;
_negYAxisData.Delta = negYDelta;
_negYAxisData.Distance = negYDistance;
_negZAxisData.Delta = negZDelta;
_negZAxisData.Distance = negZDistance;
// Sort for correct draw order.
_axisData.Clear();
_axisData.AddRange([_xAxisData, _yAxisData, _zAxisData, _negXAxisData, _negYAxisData, _negZAxisData]);
_axisData.Sort((a, b) => -a.Distance.CompareTo(b.Distance));
// Rebuild sprite positions list for hover detection
_spritePositions.Clear();
Render2D.DrawSprite(_posHandle, new Rectangle(0, 0, Size), Color.Black.AlphaMultiplied(0.1f));
// Draw in order from farthest to closest
for (int i = 0; i < _axisData.Count; i++)
{
var axis = _axisData[i];
Float2 tipScreen = relativeCenter + axis.Delta * _axisLength;
Float2 tipTextScreen = relativeCenter + axis.Delta * _textAxisLength;
bool isHovered = _hoveredAxisIndex == i;
// Store sprite position for hover detection
_spritePositions.Add((tipTextScreen, axis.Direction));
var axisColor = isHovered ? new Color(1.0f, 0.8980392f, 0.039215688f) : axis.AxisColor;
var font = _fontReference.GetFont();
if (!axis.Negative)
{
Render2D.DrawLine(relativeCenter, tipScreen, axisColor, 2.0f);
Render2D.DrawSprite(_posHandle, new Rectangle(tipTextScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor);
Render2D.DrawText(font, axis.Label, isHovered ? Color.Gray : Color.Black, tipTextScreen - font.MeasureText(axis.Label) * 0.5f);
}
else
{
Render2D.DrawSprite(_posHandle, new Rectangle(tipTextScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor.RGBMultiplied(0.65f));
Render2D.DrawSprite(_negHandle, new Rectangle(tipTextScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor);
if (isHovered)
Render2D.DrawText(font, axis.Label, Color.Black, tipTextScreen - font.MeasureText(axis.Label) * 0.5f);
}
}
Render2D.Features = features;
}
}

View File

@@ -710,6 +710,10 @@ namespace FlaxEditor.Options
[EditorDisplay("Node Editors"), EditorOrder(4580)]
public InputBinding NodesDistributeVertical = new InputBinding(KeyboardKeys.A, KeyboardKeys.Alt);
[DefaultValue(typeof(InputBinding), "Shift+F")]
[EditorDisplay("Node Editors"), EditorOrder(4590)]
public InputBinding FocusSelectedNodes = new InputBinding(KeyboardKeys.F, KeyboardKeys.Shift);
#endregion
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.SceneGraph.Actors
{
/// <summary>
/// Scene tree node for <see cref="RigidBody"/> actor type.
/// </summary>
[HideInEditor]
public sealed class RigidBodyNode : ActorNode
{
/// <inheritdoc />
public RigidBodyNode(Actor actor)
: base(actor)
{
}
/// <inheritdoc />
public override void PostSpawn()
{
base.PostSpawn();
if (HasPrefabLink)
return;
Actor.StaticFlags = StaticFlags.None;
}
}
}

View File

@@ -324,13 +324,12 @@ namespace FlaxEditor.SceneGraph.GUI
isExpanded = Editor.Instance.ProjectCache.IsExpandedActor(ref id);
}
if (isExpanded)
if (!noFilter)
{
Expand(true);
}
else
{
Collapse(true);
if (isExpanded)
Expand(true);
else
Collapse(true);
}
Visible = isThisVisible | isAnyChildVisible;

View File

@@ -74,6 +74,7 @@ namespace FlaxEditor.SceneGraph
CustomNodesTypes.Add(typeof(NavMesh), typeof(ActorNode));
CustomNodesTypes.Add(typeof(SpriteRender), typeof(SpriteRenderNode));
CustomNodesTypes.Add(typeof(Joint), typeof(JointNode));
CustomNodesTypes.Add(typeof(RigidBody), typeof(RigidBodyNode));
}
/// <summary>

View File

@@ -52,6 +52,7 @@ namespace FlaxEditor.Surface.Archetypes
},
new NodeArchetype
{
// [Deprecated]
TypeID = 3,
Title = "Pack Material Layer",
Description = "Pack material properties",
@@ -75,6 +76,7 @@ namespace FlaxEditor.Surface.Archetypes
},
new NodeArchetype
{
// [Deprecated]
TypeID = 4,
Title = "Unpack Material Layer",
Description = "Unpack material properties",
@@ -120,6 +122,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 6,
Title = "Pack Material Layer",
Description = "Pack material properties",
AlternativeTitles = new[] { "Make Material Layer", "Construct Material Layer", "Compose Material Layer" },
Flags = NodeFlags.MaterialGraph,
Size = new Float2(200, 280),
Elements = new[]
@@ -146,6 +149,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 7,
Title = "Unpack Material Layer",
Description = "Unpack material properties",
AlternativeTitles = new[] { "Break Material Layer", "Deconstruct Material Layer", "Decompose Material Layer", "Split Material Layer" },
Flags = NodeFlags.MaterialGraph,
Size = new Float2(210, 280),
Elements = new[]

View File

@@ -342,6 +342,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 20,
Title = "Pack Float2",
Description = "Pack components to Float2",
AlternativeTitles = new[] { "Make Float2", "Construct Float2", "Compose Float2" },
Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 40),
DefaultValues = new object[]
@@ -361,6 +362,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 21,
Title = "Pack Float3",
Description = "Pack components to Float3",
AlternativeTitles = new[] { "Make Float3", "Construct Float3", "Compose Float3" },
Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 60),
DefaultValues = new object[]
@@ -382,6 +384,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 22,
Title = "Pack Float4",
Description = "Pack components to Float4",
AlternativeTitles = new[] { "Make Float4", "Construct Float4", "Compose Float4" },
Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 80),
DefaultValues = new object[]
@@ -405,6 +408,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 23,
Title = "Pack Rotation",
Description = "Pack components to Rotation",
AlternativeTitles = new[] { "Make Rotation", "Construct Rotation", "Compose Rotation" },
Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 60),
DefaultValues = new object[]
@@ -426,6 +430,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 24,
Title = "Pack Transform",
Description = "Pack components to Transform",
AlternativeTitles = new[] { "Make Transform", "Construct Transform", "Compose Transform" },
Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 80),
Elements = new[]
@@ -441,6 +446,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 25,
Title = "Pack Box",
Description = "Pack components to BoundingBox",
AlternativeTitles = new[] { "Make Box", "Construct Box", "Compose Box" },
Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 40),
Elements = new[]
@@ -454,6 +460,7 @@ namespace FlaxEditor.Surface.Archetypes
{
TypeID = 26,
Title = "Pack Structure",
AlternativeTitles = new[] { "Make Structure", "Construct Structure", "Compose Structure" },
Create = (id, context, arch, groupArch) => new PackStructureNode(id, context, arch, groupArch),
IsInputCompatible = PackStructureNode.IsInputCompatible,
IsOutputCompatible = PackStructureNode.IsOutputCompatible,
@@ -479,6 +486,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 30,
Title = "Unpack Float2",
Description = "Unpack components from Float2",
AlternativeTitles = new[] { "Break Float2", "Deconstruct Float2", "Decompose Float2", "Split Float2" },
Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 40),
Elements = new[]
@@ -493,6 +501,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 31,
Title = "Unpack Float3",
Description = "Unpack components from Float3",
AlternativeTitles = new[] { "Break Float3", "Deconstruct Float3", "Decompose Float3", "Split Float3" },
Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 60),
Elements = new[]
@@ -508,6 +517,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 32,
Title = "Unpack Float4",
Description = "Unpack components from Float4",
AlternativeTitles = new[] { "Break Float4", "Deconstruct Float4", "Decompose Float4", "Split Float4" },
Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 80),
Elements = new[]
@@ -524,6 +534,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 33,
Title = "Unpack Rotation",
Description = "Unpack components from Rotation",
AlternativeTitles = new[] { "Break Rotation", "Deconstruct Rotation", "Decompose Rotation", "Split Rotation" },
Flags = NodeFlags.AllGraphs,
Size = new Float2(170, 60),
Elements = new[]
@@ -539,6 +550,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 34,
Title = "Unpack Transform",
Description = "Unpack components from Transform",
AlternativeTitles = new[] { "Break Transform", "Deconstruct Transform", "Decompose Transform", "Split Transform" },
Flags = NodeFlags.AllGraphs,
Size = new Float2(170, 60),
Elements = new[]
@@ -554,6 +566,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 35,
Title = "Unpack Box",
Description = "Unpack components from BoundingBox",
AlternativeTitles = new[] { "Break BoundingBox", "Deconstruct BoundingBox", "Decompose BoundingBox", "Split BoundingBox" },
Flags = NodeFlags.AllGraphs,
Size = new Float2(170, 40),
Elements = new[]
@@ -572,6 +585,7 @@ namespace FlaxEditor.Surface.Archetypes
IsOutputCompatible = UnpackStructureNode.IsOutputCompatible,
GetInputOutputDescription = UnpackStructureNode.GetInputOutputDescription,
Description = "Breaks the structure data to allow extracting components from it.",
AlternativeTitles = new[] { "Break Structure", "Deconstruct Structure", "Decompose Structure", "Split Structure" },
Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI,
Size = new Float2(180, 20),
DefaultValues = new object[]

View File

@@ -585,7 +585,7 @@ namespace FlaxEditor.Surface.ContextMenu
private void UpdateFilters()
{
if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBoxes[0] == null)
if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBoxes.Count == 0)
{
ResetView();
Profiler.EndEvent();

View File

@@ -34,11 +34,6 @@ namespace FlaxEditor.Surface.Elements
/// </summary>
public const float DefaultConnectionOffset = 24f;
/// <summary>
/// Distance for the mouse to be considered above the connection
/// </summary>
public float MouseOverConnectionDistance => 100f / Surface.ViewScale;
/// <inheritdoc />
public OutputBox(SurfaceNode parentNode, NodeElementArchetype archetype)
: base(parentNode, archetype, archetype.Position + new Float2(parentNode.Archetype.Size.X, 0))
@@ -109,12 +104,13 @@ namespace FlaxEditor.Surface.Elements
/// </summary>
/// <param name="targetBox">The other box.</param>
/// <param name="mousePosition">The mouse position</param>
public bool IntersectsConnection(Box targetBox, ref Float2 mousePosition)
/// <param name="distance">Distance at which its an intersection</param>
public bool IntersectsConnection(Box targetBox, ref Float2 mousePosition, float distance)
{
float connectionOffset = Mathf.Max(0f, DefaultConnectionOffset * (1 - Editor.Instance.Options.Options.Interface.ConnectionCurvature));
Float2 start = new Float2(ConnectionOrigin.X + connectionOffset, ConnectionOrigin.Y);
Float2 end = new Float2(targetBox.ConnectionOrigin.X - connectionOffset, targetBox.ConnectionOrigin.Y);
return IntersectsConnection(ref start, ref end, ref mousePosition, MouseOverConnectionDistance);
return IntersectsConnection(ref start, ref end, ref mousePosition, distance);
}
/// <summary>
@@ -182,7 +178,7 @@ namespace FlaxEditor.Surface.Elements
{
// Draw all the connections
var style = Surface.Style;
var mouseOverDistance = MouseOverConnectionDistance;
var mouseOverDistance = Surface.MouseOverConnectionDistance;
var startPos = ConnectionOrigin;
var startHighlight = ConnectionsHighlightIntensity;
for (int i = 0; i < Connections.Count; i++)

View File

@@ -574,13 +574,13 @@ namespace FlaxEditor.Surface
var showSearch = () => editor.ContentFinding.ShowSearch(window);
// Toolstrip
saveButton = toolStrip.AddButton(editor.Icons.Save64, window.Save).LinkTooltip("Save", ref inputOptions.Save);
saveButton = toolStrip.AddButton(editor.Icons.Save64, window.Save).LinkTooltip("Save.", ref inputOptions.Save);
toolStrip.AddSeparator();
undoButton = toolStrip.AddButton(editor.Icons.Undo64, undo.PerformUndo).LinkTooltip("Undo", ref inputOptions.Undo);
redoButton = toolStrip.AddButton(editor.Icons.Redo64, undo.PerformRedo).LinkTooltip("Redo", ref inputOptions.Redo);
undoButton = toolStrip.AddButton(editor.Icons.Undo64, undo.PerformUndo).LinkTooltip("Undo.", ref inputOptions.Undo);
redoButton = toolStrip.AddButton(editor.Icons.Redo64, undo.PerformRedo).LinkTooltip("Redo.", ref inputOptions.Redo);
toolStrip.AddSeparator();
toolStrip.AddButton(editor.Icons.Search64, showSearch).LinkTooltip("Open content search tool", ref inputOptions.Search);
toolStrip.AddButton(editor.Icons.CenterView64, surface.ShowWholeGraph).LinkTooltip("Show whole graph");
toolStrip.AddButton(editor.Icons.Search64, showSearch).LinkTooltip("Open content search tool.", ref inputOptions.Search);
toolStrip.AddButton(editor.Icons.CenterView64, surface.ShowWholeGraph).LinkTooltip("Show whole graph.");
var gridSnapButton = toolStrip.AddButton(editor.Icons.Grid32, surface.ToggleGridSnapping);
gridSnapButton.LinkTooltip("Toggle grid snapping for nodes.");
gridSnapButton.AutoCheck = true;

View File

@@ -410,8 +410,11 @@ namespace FlaxEditor.Surface
}
menu.AddSeparator();
_cmFormatNodesMenu = menu.AddChildMenu("Format node(s)");
_cmFormatNodesMenu.Enabled = CanEdit && HasNodesSelection;
bool allNodesNoMove = SelectedNodes.All(n => n.Archetype.Flags.HasFlag(NodeFlags.NoMove));
bool clickedNodeNoMove = ((SelectedNodes.Count == 1 && controlUnderMouse is SurfaceNode n && n.Archetype.Flags.HasFlag(NodeFlags.NoMove)));
_cmFormatNodesMenu = menu.AddChildMenu("Format nodes");
_cmFormatNodesMenu.Enabled = CanEdit && HasNodesSelection && !(allNodesNoMove || clickedNodeNoMove);
_cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Auto format", Editor.Instance.Options.Options.Input.NodesAutoFormat, () => { FormatGraph(SelectedNodes); });
_cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Straighten connections", Editor.Instance.Options.Options.Input.NodesStraightenConnections, () => { StraightenGraphConnections(SelectedNodes); });

View File

@@ -213,6 +213,44 @@ namespace FlaxEditor.Surface
}
}
/// <summary>
/// Draw connection hints for lazy connect feature.
/// </summary>
protected virtual void DrawLazyConnect()
{
var style = FlaxEngine.GUI.Style.Current;
if (_lazyConnectStartNode != null)
{
Float2 upperLeft = _rootControl.PointToParent(_lazyConnectStartNode.UpperLeft);
Rectangle startNodeOutline = new Rectangle(upperLeft + 1f, _lazyConnectStartNode.Size - 1f);
startNodeOutline.Size *= ViewScale;
Render2D.DrawRectangle(startNodeOutline.MakeExpanded(4f), style.BackgroundSelected, 4f);
}
if (_lazyConnectEndNode != null)
{
Float2 upperLeft = _rootControl.PointToParent(_lazyConnectEndNode.UpperLeft);
Rectangle startNodeOutline = new Rectangle(upperLeft + 1f, _lazyConnectEndNode.Size - 1f);
startNodeOutline.Size *= ViewScale;
Render2D.DrawRectangle(startNodeOutline.MakeExpanded(4f), style.BackgroundSelected, 4f);
}
Rectangle startRect = new Rectangle(_rightMouseDownPos - 6f, new Float2(12f));
Rectangle endRect = new Rectangle(_mousePos - 6f, new Float2(12f));
// Start and end shadows/ outlines
Render2D.FillRectangle(startRect.MakeExpanded(2.5f), Color.Black);
Render2D.FillRectangle(endRect.MakeExpanded(2.5f), Color.Black);
Render2D.DrawLine(_rightMouseDownPos, _mousePos, Color.Black, 7.5f);
Render2D.DrawLine(_rightMouseDownPos, _mousePos, style.ForegroundGrey, 5f);
// Draw start and end boxes over the lines to hide ugly artifacts at the ends
Render2D.FillRectangle(startRect, style.ForegroundGrey);
Render2D.FillRectangle(endRect, style.ForegroundGrey);
}
/// <summary>
/// Draws the contents of the surface (nodes, connections, comments, etc.).
/// </summary>
@@ -260,6 +298,9 @@ namespace FlaxEditor.Surface
DrawContents();
if (_isLazyConnecting)
DrawLazyConnect();
//Render2D.DrawText(style.FontTitle, string.Format("Scale: {0}", _rootControl.Scale), rect, Enabled ? Color.Red : Color.Black);
// Draw border

View File

@@ -39,6 +39,8 @@ namespace FlaxEditor.Surface
if (nodes.Count <= 1)
return;
List<MoveNodesAction> undoActions = new List<MoveNodesAction>();
var nodesToVisit = new HashSet<SurfaceNode>(nodes);
// While we haven't formatted every node
@@ -73,18 +75,23 @@ namespace FlaxEditor.Surface
}
}
FormatConnectedGraph(connectedNodes);
undoActions.AddRange(FormatConnectedGraph(connectedNodes));
}
Undo?.AddAction(new MultiUndoAction(undoActions, "Format nodes"));
MarkAsEdited(false);
}
/// <summary>
/// Formats a graph where all nodes are connected.
/// </summary>
/// <param name="nodes">List of connected nodes.</param>
protected void FormatConnectedGraph(List<SurfaceNode> nodes)
private List<MoveNodesAction> FormatConnectedGraph(List<SurfaceNode> nodes)
{
List<MoveNodesAction> undoActions = new List<MoveNodesAction>();
if (nodes.Count <= 1)
return;
return undoActions;
var boundingBox = GetNodesBounds(nodes);
@@ -140,7 +147,6 @@ namespace FlaxEditor.Surface
}
// Set the node positions
var undoActions = new List<MoveNodesAction>();
var topRightPosition = endNodes[0].Location;
for (int i = 0; i < nodes.Count; i++)
{
@@ -155,16 +161,18 @@ namespace FlaxEditor.Surface
}
}
MarkAsEdited(false);
Undo?.AddAction(new MultiUndoAction(undoActions, "Format nodes"));
return undoActions;
}
/// <summary>
/// Straightens every connection between nodes in <paramref name="nodes"/>.
/// </summary>
/// <param name="nodes">List of nodes.</param>
/// <returns>List of undo actions.</returns>
public void StraightenGraphConnections(List<SurfaceNode> nodes)
{
{
nodes = nodes.Where(n => !n.Archetype.Flags.HasFlag(NodeFlags.NoMove)).ToList();
if (nodes.Count <= 1)
return;
@@ -350,8 +358,10 @@ namespace FlaxEditor.Surface
/// <param name="nodes">List of nodes.</param>
/// <param name="alignmentType">Alignemnt type.</param>
public void AlignNodes(List<SurfaceNode> nodes, NodeAlignmentType alignmentType)
{
if(nodes.Count <= 1)
{
nodes = nodes.Where(n => !n.Archetype.Flags.HasFlag(NodeFlags.NoMove)).ToList();
if (nodes.Count <= 1)
return;
var undoActions = new List<MoveNodesAction>();
@@ -392,6 +402,8 @@ namespace FlaxEditor.Surface
/// <param name="vertically">If false will be done horizontally, if true will be done vertically.</param>
public void DistributeNodes(List<SurfaceNode> nodes, bool vertically)
{
nodes = nodes.Where(n => !n.Archetype.Flags.HasFlag(NodeFlags.NoMove)).ToList();
if(nodes.Count <= 1)
return;

View File

@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using static FlaxEditor.Surface.Archetypes.Particles;
using FlaxEditor.Options;
using FlaxEditor.Surface.Elements;
using FlaxEditor.Surface.Undo;
@@ -23,12 +24,26 @@ namespace FlaxEditor.Surface
/// </summary>
public bool PanWithMiddleMouse = false;
/// <summary>
/// Distance for the mouse to be considered above the connection.
/// </summary>
public float MouseOverConnectionDistance => 100f / ViewScale;
/// <summary>
/// Distance of a node from which it is able to be slotted into an existing connection.
/// </summary>
public float SlotNodeIntoConnectionDistance => 250f / ViewScale;
private string _currentInputText = string.Empty;
private Float2 _movingNodesDelta;
private Float2 _gridRoundingDelta;
private HashSet<SurfaceNode> _movingNodes;
private HashSet<SurfaceNode> _temporarySelectedNodes;
private readonly Stack<InputBracket> _inputBrackets = new Stack<InputBracket>();
private bool _isLazyConnecting;
private SurfaceNode _lazyConnectStartNode;
private SurfaceNode _lazyConnectEndNode;
private InputBinding _focusSelectedNodeBinding;
private class InputBracket
{
@@ -250,8 +265,13 @@ namespace FlaxEditor.Surface
// Cache mouse location
_mousePos = location;
if (_isLazyConnecting && GetControlUnderMouse() is SurfaceNode nodeUnderMouse && !(nodeUnderMouse is SurfaceComment || nodeUnderMouse is ParticleEmitterNode))
_lazyConnectEndNode = nodeUnderMouse;
else if (_isLazyConnecting && Nodes.Count > 0)
_lazyConnectEndNode = GetClosestNodeAtLocation(location);
// Moving around surface with mouse
if (_rightMouseDown)
if (_rightMouseDown && !_isLazyConnecting)
{
// Calculate delta
var delta = location - _rightMouseDownPos;
@@ -321,6 +341,33 @@ namespace FlaxEditor.Surface
foreach (var node in _movingNodes)
{
// Allow ripping the node from its current connection
if (RootWindow.GetKey(KeyboardKeys.Alt))
{
InputBox nodeConnectedInput = null;
OutputBox nodeConnectedOuput = null;
var boxes = node.GetBoxes();
foreach (var box in boxes)
{
if (!box.IsOutput && box.Connections.Count > 0)
{
nodeConnectedInput = (InputBox)box;
continue;
}
if (box.IsOutput && box.Connections.Count > 0)
{
nodeConnectedOuput = (OutputBox)box;
continue;
}
}
if (nodeConnectedInput != null && nodeConnectedOuput != null)
TryConnect(nodeConnectedOuput.Connections[0], nodeConnectedInput.Connections[0]);
node.RemoveConnections();
}
if (gridSnap)
{
Float2 unroundedLocation = node.Location;
@@ -420,7 +467,7 @@ namespace FlaxEditor.Surface
if (!handled && CanEdit && CanUseNodeType(7, 29))
{
var mousePos = _rootControl.PointFromParent(ref _mousePos);
if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox) && GetControlUnderMouse() == null)
if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox, MouseOverConnectionDistance) && GetControlUnderMouse() == null)
{
if (Undo != null)
{
@@ -515,11 +562,17 @@ namespace FlaxEditor.Surface
_middleMouseDownPos = location;
}
if (root.GetKey(KeyboardKeys.Alt) && button == MouseButton.Right)
_isLazyConnecting = true;
// Check if any node is under the mouse
SurfaceControl controlUnderMouse = GetControlUnderMouse();
var cLocation = _rootControl.PointFromParent(ref location);
if (controlUnderMouse != null)
{
if (controlUnderMouse is SurfaceNode node && _isLazyConnecting && !(controlUnderMouse is SurfaceComment || controlUnderMouse is ParticleEmitterNode))
_lazyConnectStartNode = node;
// Check if mouse is over header and user is pressing mouse left button
if (_leftMouseDown && controlUnderMouse.CanSelect(ref cLocation))
{
@@ -554,6 +607,9 @@ namespace FlaxEditor.Surface
}
else
{
if (_isLazyConnecting && Nodes.Count > 0)
_lazyConnectStartNode = GetClosestNodeAtLocation(location);
// Cache flags and state
if (_leftMouseDown)
{
@@ -602,8 +658,71 @@ namespace FlaxEditor.Surface
{
if (_movingNodes != null && _movingNodes.Count > 0)
{
// Allow dropping a single node onto an existing connection and connect it
if (_movingNodes.Count == 1)
{
var mousePos = _rootControl.PointFromParent(ref _mousePos);
InputBox intersectedConnectionInputBox;
OutputBox intersectedConnectionOutputBox;
if (IntersectsConnection(mousePos, out intersectedConnectionInputBox, out intersectedConnectionOutputBox, SlotNodeIntoConnectionDistance))
{
SurfaceNode node = _movingNodes.First();
InputBox nodeInputBox = (InputBox)node.GetBoxes().First(b => !b.IsOutput);
OutputBox nodeOutputBox = (OutputBox)node.GetBoxes().First(b => b.IsOutput);
TryConnect(intersectedConnectionOutputBox, nodeInputBox);
TryConnect(nodeOutputBox, intersectedConnectionInputBox);
float intersectedConnectionNodesXDistance = intersectedConnectionInputBox.ParentNode.Left - intersectedConnectionOutputBox.ParentNode.Right;
float paddedNodeWidth = node.Width + 2f;
if (intersectedConnectionNodesXDistance < paddedNodeWidth)
{
List<SurfaceNode> visitedNodes = new List<SurfaceNode>{ node };
List<SurfaceNode> movedNodes = new List<SurfaceNode>();
Float2 locationDelta = new Float2(paddedNodeWidth, 0f);
MoveConnectedNodes(intersectedConnectionInputBox.ParentNode);
void MoveConnectedNodes(SurfaceNode node)
{
// Only move node if it is to the right of the node we have connected the moved node to
if (node.Right > intersectedConnectionInputBox.ParentNode.Left + 15f && !node.Archetype.Flags.HasFlag(NodeFlags.NoMove))
{
node.Location += locationDelta;
movedNodes.Add(node);
}
visitedNodes.Add(node);
foreach (var box in node.GetBoxes())
{
if (!box.HasAnyConnection || box == intersectedConnectionInputBox)
continue;
foreach (var connectedBox in box.Connections)
{
SurfaceNode nextNode = connectedBox.ParentNode;
if (visitedNodes.Contains(nextNode))
continue;
MoveConnectedNodes(nextNode);
}
}
}
Float2 nodeMoveOffset = new Float2(node.Width * 0.5f, 0f);
node.Location += nodeMoveOffset;
var moveNodesAction = new MoveNodesAction(Context, movedNodes.Select(n => n.ID).ToArray(), locationDelta);
var moveNodeAction = new MoveNodesAction(Context, [node.ID], nodeMoveOffset);
var multiAction = new MultiUndoAction(moveNodeAction, moveNodesAction);
AddBatchedUndoAction(multiAction);
}
}
}
if (Undo != null && !_movingNodesDelta.IsZero && CanEdit)
Undo.AddAction(new MoveNodesAction(Context, _movingNodes.Select(x => x.ID).ToArray(), _movingNodesDelta));
AddBatchedUndoAction(new MoveNodesAction(Context, _movingNodes.Select(x => x.ID).ToArray(), _movingNodesDelta));
_movingNodes.Clear();
}
_movingNodesDelta = Float2.Zero;
@@ -630,12 +749,36 @@ namespace FlaxEditor.Surface
{
// Check if any control is under the mouse
_cmStartPos = location;
if (controlUnderMouse == null)
if (controlUnderMouse == null && !_isLazyConnecting)
{
showPrimaryMenu = true;
}
}
_mouseMoveAmount = 0;
if (_isLazyConnecting)
{
if (_lazyConnectStartNode != null && _lazyConnectEndNode != null && _lazyConnectStartNode != _lazyConnectEndNode)
{
// First check if there is a type matching input and output where input
OutputBox startNodeOutput = (OutputBox)_lazyConnectStartNode.GetBoxes().FirstOrDefault(b => b.IsOutput, null);
InputBox endNodeInput = null;
if (startNodeOutput != null)
endNodeInput = (InputBox)_lazyConnectEndNode.GetBoxes().FirstOrDefault(b => !b.IsOutput && b.CurrentType == startNodeOutput.CurrentType && !b.HasAnyConnection && b.IsActive && b.CanConnectWith(startNodeOutput), null);
// Perform less strict checks (less ideal conditions for connection but still good) if the first checks failed
if (endNodeInput == null)
endNodeInput = (InputBox)_lazyConnectEndNode.GetBoxes().FirstOrDefault(b => !b.IsOutput && !b.HasAnyConnection && b.CanConnectWith(startNodeOutput), null);
if (startNodeOutput != null && endNodeInput != null)
TryConnect(startNodeOutput, endNodeInput);
}
_isLazyConnecting = false;
_lazyConnectStartNode = null;
_lazyConnectEndNode = null;
}
}
if (_middleMouseDown && button == MouseButton.Middle)
{
@@ -651,7 +794,7 @@ namespace FlaxEditor.Surface
{
// Surface was not moved with MMB so try to remove connection underneath
var mousePos = _rootControl.PointFromParent(ref location);
if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox))
if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox, MouseOverConnectionDistance))
{
var action = new EditNodeConnections(inputBox.ParentNode.Context, inputBox.ParentNode);
inputBox.BreakConnection(outputBox);
@@ -704,13 +847,21 @@ namespace FlaxEditor.Surface
private void MoveSelectedNodes(Float2 delta)
{
// TODO: undo
List<MoveNodesAction> undoActions = new List<MoveNodesAction>();
delta /= _targetScale;
OnGetNodesToMove();
foreach (var node in _movingNodes)
{
node.Location += delta;
if (Undo != null)
undoActions.Add(new MoveNodesAction(Context, new[] { node.ID }, delta));
}
_isMovingSelection = false;
MarkAsEdited(false);
if (undoActions.Count > 0)
Undo?.AddAction(new MultiUndoAction(undoActions, "Moved "));
}
/// <inheritdoc />
@@ -837,6 +988,29 @@ namespace FlaxEditor.Surface
return false;
}
private SurfaceNode GetClosestNodeAtLocation(Float2 location)
{
SurfaceNode currentClosestNode = null;
float currentClosestDistanceSquared = float.MaxValue;
foreach (var node in Nodes)
{
if (node is SurfaceComment || node is ParticleEmitterNode)
continue;
Float2 nodeSurfaceLocation = _rootControl.PointToParent(node.Center);
float distanceSquared = Float2.DistanceSquared(location, nodeSurfaceLocation);
if (distanceSquared < currentClosestDistanceSquared)
{
currentClosestNode = node;
currentClosestDistanceSquared = distanceSquared;
}
}
return currentClosestNode;
}
private void ResetInput()
{
InputText = "";
@@ -845,7 +1019,8 @@ namespace FlaxEditor.Surface
private void CurrentInputTextChanged(string currentInputText)
{
if (string.IsNullOrEmpty(currentInputText))
// Check if focus selected nodes binding is being pressed to prevent it triggering primary menu
if (string.IsNullOrEmpty(currentInputText) || _focusSelectedNodeBinding.Process(RootWindow))
return;
if (IsPrimaryMenuOpened || !CanEdit)
{
@@ -1025,7 +1200,7 @@ namespace FlaxEditor.Surface
return new Float2(xLocation, yLocation);
}
private bool IntersectsConnection(Float2 mousePosition, out InputBox inputBox, out OutputBox outputBox)
private bool IntersectsConnection(Float2 mousePosition, out InputBox inputBox, out OutputBox outputBox, float distance)
{
for (int i = 0; i < Nodes.Count; i++)
{
@@ -1035,7 +1210,7 @@ namespace FlaxEditor.Surface
{
for (int k = 0; k < ob.Connections.Count; k++)
{
if (ob.IntersectsConnection(ob.Connections[k], ref mousePosition))
if (ob.IntersectsConnection(ob.Connections[k], ref mousePosition, distance))
{
outputBox = ob;
inputBox = ob.Connections[k] as InputBox;

View File

@@ -423,8 +423,9 @@ namespace FlaxEditor.Surface
new InputActionsContainer.Binding(options => options.NodesAlignLeft, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Left); }),
new InputActionsContainer.Binding(options => options.NodesAlignCenter, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Center); }),
new InputActionsContainer.Binding(options => options.NodesAlignRight, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Right); }),
new InputActionsContainer.Binding(options => options.NodesDistributeHorizontal, () => { DistributeNodes(SelectedNodes, false); }),
new InputActionsContainer.Binding(options => options.NodesDistributeVertical, () => { DistributeNodes(SelectedNodes, true); }),
new InputActionsContainer.Binding(options => options.NodesDistributeHorizontal, () => { DistributeNodes(SelectedNodes, false); }),
new InputActionsContainer.Binding(options => options.NodesDistributeVertical, () => { DistributeNodes(SelectedNodes, true); }),
new InputActionsContainer.Binding(options => options.FocusSelectedNodes, () => { FocusSelectionOrWholeGraph(); }),
});
Context.ControlSpawned += OnSurfaceControlSpawned;
@@ -436,7 +437,10 @@ namespace FlaxEditor.Surface
DragHandlers.Add(_dragAssets = new DragAssets<DragDropEventArgs>(ValidateDragItem));
DragHandlers.Add(_dragParameters = new DragNames<DragDropEventArgs>(SurfaceParameter.DragPrefix, ValidateDragParameter));
OnEditorOptionsChanged(Editor.Instance.Options.Options);
ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin;
Editor.Instance.Options.OptionsChanged += OnEditorOptionsChanged;
}
private void OnScriptsReloadBegin()
@@ -446,6 +450,11 @@ namespace FlaxEditor.Surface
_cmPrimaryMenu = null;
}
private void OnEditorOptionsChanged(EditorOptions options)
{
_focusSelectedNodeBinding = options.Input.FocusSelectedNodes;
}
/// <summary>
/// Gets the display name of the connection type used in the surface.
/// </summary>
@@ -648,6 +657,37 @@ namespace FlaxEditor.Surface
ViewCenterPosition = areaRect.Center;
}
/// <summary>
/// Adjusts the view to focus on the currently selected nodes, or the entire graph if no nodes are selected.
/// </summary>
public void FocusSelectionOrWholeGraph()
{
if (SelectedNodes.Count > 0)
ShowSelection();
else
ShowWholeGraph();
}
/// <summary>
/// Shows the selected controls by changing the view scale and the position.
/// </summary>
public void ShowSelection()
{
var selection = SelectedControls;
if (selection.Count == 0)
return;
// Calculate the bounds of all selected controls
Rectangle bounds = selection[0].Bounds;
for (int i = 1; i < selection.Count; i++)
bounds = Rectangle.Union(bounds, selection[i].Bounds);
// Add margin
bounds = bounds.MakeExpanded(250.0f);
ShowArea(bounds);
}
/// <summary>
/// Shows the given surface node by changing the view scale and the position and focuses the node.
/// </summary>
@@ -1071,6 +1111,7 @@ namespace FlaxEditor.Surface
_cmPrimaryMenu?.Dispose();
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
Editor.Instance.Options.OptionsChanged += OnEditorOptionsChanged;
base.OnDestroy();
}

View File

@@ -89,7 +89,7 @@ namespace FlaxEditor.Tools.Terrain
if (!terrain.HasPatch(ref patchCoord) && _planeModel)
{
var planeSize = 100.0f;
var patchSize = terrain.ChunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount;
var patchSize = terrain.PatchSize;
Matrix world = Matrix.RotationX(-Mathf.PiOverTwo) *
Matrix.Scaling(patchSize / planeSize) *
Matrix.Translation(patchSize * (0.5f + patchCoord.X), 0, patchSize * (0.5f + patchCoord.Y)) *

View File

@@ -69,9 +69,9 @@ namespace FlaxEditor.Tools.Terrain.Paint
var splatmapIndex = ActiveSplatmapIndex;
var splatmapIndexOther = (splatmapIndex + 1) % 2;
var chunkSize = terrain.ChunkSize;
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1;
var heightmapSize = terrain.HeightmapSize;
var heightmapLength = heightmapSize * heightmapSize;
var patchSize = chunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount;
var patchSize = terrain.PatchSize;
var tempBuffer = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes, splatmapIndex).ToPointer();
var tempBufferOther = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes, (splatmapIndex + 1) % 2).ToPointer();
var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex;

View File

@@ -70,9 +70,9 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
// Prepare
var chunkSize = terrain.ChunkSize;
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1;
var heightmapSize = terrain.HeightmapSize;
var heightmapLength = heightmapSize * heightmapSize;
var patchSize = chunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount;
var patchSize = terrain.PatchSize;
var tempBuffer = (float*)gizmo.GetHeightmapTempBuffer(heightmapLength * sizeof(float)).ToPointer();
var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex;

View File

@@ -382,7 +382,8 @@ bool TerrainTools::ExportTerrain(Terrain* terrain, String outputFolder)
const Int2 heightmapSize = size * Terrain::ChunksCountEdge * terrain->GetChunkSize() + 1;
Array<float> heightmap;
heightmap.Resize(heightmapSize.X * heightmapSize.Y);
heightmap.SetAll(firstPatch->GetHeightmapData()[0]);
if (const float* heightmapData = firstPatch->GetHeightmapData())
heightmap.SetAll(heightmapData[0]);
// Fill heightmap with data from all patches
const int32 rowSize = terrain->GetChunkSize() * Terrain::ChunksCountEdge + 1;
@@ -392,8 +393,16 @@ bool TerrainTools::ExportTerrain(Terrain* terrain, String outputFolder)
const Int2 pos(patch->GetX() - start.X, patch->GetZ() - start.Y);
const float* src = patch->GetHeightmapData();
float* dst = heightmap.Get() + pos.X * (rowSize - 1) + pos.Y * heightmapSize.X * (rowSize - 1);
for (int32 row = 0; row < rowSize; row++)
Platform::MemoryCopy(dst + row * heightmapSize.X, src + row * rowSize, rowSize * sizeof(float));
if (src)
{
for (int32 row = 0; row < rowSize; row++)
Platform::MemoryCopy(dst + row * heightmapSize.X, src + row * rowSize, rowSize * sizeof(float));
}
else
{
for (int32 row = 0; row < rowSize; row++)
Platform::MemoryClear(dst + row * heightmapSize.X, rowSize * sizeof(float));
}
}
// Interpolate to 16-bit int

View File

@@ -85,8 +85,7 @@ namespace FlaxEditor.Tools.Terrain.Undo
{
_terrain = terrain.ID;
_patches = new List<PatchData>(4);
var chunkSize = terrain.ChunkSize;
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1;
var heightmapSize = terrain.HeightmapSize;
_heightmapLength = heightmapSize * heightmapSize;
_heightmapDataSize = _heightmapLength * stride;

View File

@@ -1229,7 +1229,7 @@ namespace FlaxEditor.Viewport
/// Orients the viewport.
/// </summary>
/// <param name="orientation">The orientation.</param>
protected void OrientViewport(Quaternion orientation)
public void OrientViewport(Quaternion orientation)
{
OrientViewport(ref orientation);
}
@@ -1238,7 +1238,7 @@ namespace FlaxEditor.Viewport
/// Orients the viewport.
/// </summary>
/// <param name="orientation">The orientation.</param>
protected virtual void OrientViewport(ref Quaternion orientation)
public virtual void OrientViewport(ref Quaternion orientation)
{
if (ViewportCamera is FPSCamera fpsCamera)
{

View File

@@ -108,13 +108,14 @@ namespace FlaxEditor.Viewport
private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32);
private EditorSpritesRenderer _editorSpritesRenderer;
private ViewportRubberBandSelector _rubberBandSelector;
private DirectionGizmo _directionGizmo;
private bool _gameViewActive;
private ViewFlags _preGameViewFlags;
private ViewMode _preGameViewViewMode;
private bool _gameViewWasGridShown;
private bool _gameViewWasFpsCounterShown;
private bool _gameViewWasNagivationShown;
private bool _gameViewWasNavigationShown;
/// <summary>
/// Drag and drop handlers
@@ -225,6 +226,12 @@ namespace FlaxEditor.Viewport
// Add rubber band selector
_rubberBandSelector = new ViewportRubberBandSelector(this);
_directionGizmo = new DirectionGizmo(this);
_directionGizmo.AnchorPreset = AnchorPresets.TopRight;
_directionGizmo.Parent = this;
_directionGizmo.LocalY += 25;
_directionGizmo.LocalX -= 150;
_directionGizmo.Size = new Float2(150, 150);
// Add grid
Grid = new GridGizmo(this);
@@ -244,6 +251,12 @@ namespace FlaxEditor.Viewport
_showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", inputOptions.ToggleNavMeshVisibility, () => ShowNavigation = !ShowNavigation);
_showNavigationButton.CloseMenuOnClick = false;
// Show direction gizmo widget
var showDirectionGizmoButton = ViewWidgetShowMenu.AddButton("Direction Gizmo", () => _directionGizmo.Visible = !_directionGizmo.Visible);
showDirectionGizmoButton.AutoCheck = true;
showDirectionGizmoButton.CloseMenuOnClick = false;
showDirectionGizmoButton.Checked = _directionGizmo.Visible;
// Game View
ViewWidgetButtonMenu.AddSeparator();
_toggleGameViewButton = ViewWidgetButtonMenu.AddButton("Game View", inputOptions.ToggleGameView, ToggleGameView);
@@ -514,14 +527,14 @@ namespace FlaxEditor.Viewport
_preGameViewViewMode = Task.ViewMode;
_gameViewWasGridShown = Grid.Enabled;
_gameViewWasFpsCounterShown = ShowFpsCounter;
_gameViewWasNagivationShown = ShowNavigation;
_gameViewWasNavigationShown = ShowNavigation;
}
// Set flags & values
Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.DefaultGame;
Task.ViewMode = _gameViewActive ? _preGameViewViewMode : ViewMode.Default;
ShowFpsCounter = _gameViewActive ? _gameViewWasFpsCounterShown : false;
ShowNavigation = _gameViewActive ? _gameViewWasNagivationShown : false;
ShowNavigation = _gameViewActive ? _gameViewWasNavigationShown : false;
Grid.Enabled = _gameViewActive ? _gameViewWasGridShown : false;
_gameViewActive = !_gameViewActive;
@@ -647,7 +660,7 @@ namespace FlaxEditor.Viewport
}
/// <inheritdoc />
protected override void OrientViewport(ref Quaternion orientation)
public override void OrientViewport(ref Quaternion orientation)
{
if (TransformGizmo.SelectedParents.Count != 0)
FocusSelection(ref orientation);

View File

@@ -681,7 +681,7 @@ namespace FlaxEditor.Viewport
}
/// <inheritdoc />
protected override void OrientViewport(ref Quaternion orientation)
public override void OrientViewport(ref Quaternion orientation)
{
if (TransformGizmo.SelectedParents.Count != 0)
FocusSelection(ref orientation);

View File

@@ -303,8 +303,7 @@ namespace FlaxEditor.Viewport.Previews
{
_terrain = new Terrain();
_terrain.Setup(1, 63);
var chunkSize = _terrain.ChunkSize;
var heightMapSize = chunkSize * Terrain.PatchEdgeChunksCount + 1;
var heightMapSize = _terrain.HeightmapSize;
var heightMapLength = heightMapSize * heightMapSize;
var heightmap = new float[heightMapLength];
var patchCoord = new Int2(0, 0);

View File

@@ -431,6 +431,9 @@ namespace FlaxEditor.Windows.Assets
_isWaitingForTimelineLoad = true;
base.OnItemReimported(item);
// Drop virtual asset state and get a new one from the reimported file
LoadFromOriginal();
}
/// <inheritdoc />

View File

@@ -53,7 +53,7 @@ namespace FlaxEditor.Windows.Assets
{
Parent = this
};
_toolstrip.AddButton(editor.Icons.Search64, () => Editor.Windows.ContentWin.Select(_item)).LinkTooltip("Show and select in Content Window");
_toolstrip.AddButton(editor.Icons.Search64, () => Editor.Windows.ContentWin.Select(_item)).LinkTooltip("Show and select in Content Window.");
InputActions.Add(options => options.Save, Save);
@@ -527,6 +527,16 @@ namespace FlaxEditor.Windows.Assets
return false;
}
/// <summary>
/// Loads the asset from the original location to reflect the state (eg. after original asset reimport).
/// </summary>
protected virtual void LoadFromOriginal()
{
_asset = LoadAsset();
OnAssetLoaded();
ClearEditedFlag();
}
/// <inheritdoc />
protected override T LoadAsset()
{

View File

@@ -115,6 +115,7 @@ namespace FlaxEditor.Windows
var root = _root;
root.LockChildrenRecursive();
PerformLayout();
// Update tree
var query = _foldersSearchBox.Text;

View File

@@ -1126,6 +1126,8 @@ namespace FlaxEditor.Windows
if (Editor.ContentDatabase.Find(_lastViewedFolderBeforeReload) is ContentFolder folder)
_tree.Select(folder.Node);
}
OnFoldersSearchBoxTextChanged();
}
private void Refresh()

View File

@@ -67,6 +67,7 @@ namespace FlaxEditor.Windows
TooltipText = "Search the scene tree.\n\nYou can prefix your search with different search operators:\ns: -> Actor with script of type\na: -> Actor type\nc: -> Control type",
};
_searchBox.TextChanged += OnSearchBoxTextChanged;
ScriptsBuilder.ScriptsReloadEnd += OnSearchBoxTextChanged;
// Scene tree panel
_sceneTreePanel = new Panel
@@ -111,7 +112,7 @@ namespace FlaxEditor.Windows
InputActions.Add(options => options.LockFocusSelection, () => Editor.Windows.EditWin.Viewport.LockFocusSelection());
InputActions.Add(options => options.Rename, RenameSelection);
}
/// <inheritdoc />
public override void OnPlayBeginning()
{
@@ -124,6 +125,7 @@ namespace FlaxEditor.Windows
{
base.OnPlayBegin();
_blockSceneTreeScroll = false;
OnSearchBoxTextChanged();
}
/// <inheritdoc />
@@ -138,6 +140,7 @@ namespace FlaxEditor.Windows
{
base.OnPlayEnd();
_blockSceneTreeScroll = true;
OnSearchBoxTextChanged();
}
/// <summary>
@@ -173,6 +176,7 @@ namespace FlaxEditor.Windows
return;
_tree.LockChildrenRecursive();
PerformLayout();
// Update tree
var query = _searchBox.Text;
@@ -586,6 +590,7 @@ namespace FlaxEditor.Windows
_dragHandlers = null;
_tree = null;
_searchBox = null;
ScriptsBuilder.ScriptsReloadEnd -= OnSearchBoxTextChanged;
base.OnDestroy();
}

View File

@@ -8,6 +8,8 @@
#include "Loading/Tasks/LoadAssetTask.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/LogContext.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Physics/Physics.h"
#include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Scripting/ManagedCLR/MCore.h"
@@ -703,6 +705,38 @@ void Asset::onUnload_MainThread()
OnUnloaded(this);
}
bool Asset::WaitForInitGraphics()
{
#define IS_GPU_NOT_READY() (GPUDevice::Instance == nullptr || GPUDevice::Instance->GetState() != GPUDevice::DeviceState::Ready)
if (!IsInMainThread() && IS_GPU_NOT_READY())
{
PROFILE_CPU();
ZoneColor(TracyWaitZoneColor);
int32 timeout = 1000;
while (IS_GPU_NOT_READY() && timeout-- > 0)
Platform::Sleep(1);
if (IS_GPU_NOT_READY())
return true;
}
#undef IS_GPU_NOT_READY
return false;
}
bool Asset::WaitForInitPhysics()
{
if (!IsInMainThread() && !Physics::DefaultScene)
{
PROFILE_CPU();
ZoneColor(TracyWaitZoneColor);
int32 timeout = 1000;
while (!Physics::DefaultScene && timeout-- > 0)
Platform::Sleep(1);
if (!Physics::DefaultScene)
return true;
}
return false;
}
#if USE_EDITOR
bool Asset::OnCheckSave(const StringView& path) const

View File

@@ -285,6 +285,10 @@ protected:
virtual void onRename(const StringView& newPath) = 0;
#endif
// Utilities to ensure specific engine systems are initialized before loading asset (eg. assets can be loaded during engine startup).
static bool WaitForInitGraphics();
static bool WaitForInitPhysics();
public:
// [ManagedScriptingObject]
String ToString() const override;

View File

@@ -6,7 +6,6 @@
#include "Engine/Content/Deprecated.h"
#include "Engine/Content/Upgraders/ShaderAssetUpgrader.h"
#include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/Materials/MaterialShader.h"
#include "Engine/Graphics/Shaders/Cache/ShaderCacheManager.h"
@@ -157,16 +156,8 @@ Asset::LoadResult Material::load()
FlaxChunk* materialParamsChunk;
// Wait for the GPU Device to be ready (eg. case when loading material before GPU init)
#define IS_GPU_NOT_READY() (GPUDevice::Instance == nullptr || GPUDevice::Instance->GetState() != GPUDevice::DeviceState::Ready)
if (!IsInMainThread() && IS_GPU_NOT_READY())
{
int32 timeout = 1000;
while (IS_GPU_NOT_READY() && timeout-- > 0)
Platform::Sleep(1);
if (IS_GPU_NOT_READY())
return LoadResult::InvalidData;
}
#undef IS_GPU_NOT_READY
if (WaitForInitGraphics())
return LoadResult::CannotLoadData;
// If engine was compiled with shaders compiling service:
// - Material should be changed in need to convert it to the newer version (via Visject Surface)

View File

@@ -19,10 +19,10 @@ Variant MaterialBase::GetParameterValue(const StringView& name)
if (!IsLoaded() && WaitForLoaded())
return Variant::Null;
const auto param = Params.Get(name);
if (IsMaterialInstance() && param && !param->IsOverride() && ((MaterialInstance*)this)->GetBaseMaterial())
return ((MaterialInstance*)this)->GetBaseMaterial()->GetParameterValue(name);
if (param)
{
return param->GetValue();
}
LOG(Warning, "Missing material parameter '{0}' in material {1}", String(name), ToString());
return Variant::Null;
}

View File

@@ -57,6 +57,8 @@ public:
/// <summary>
/// Gets the material parameter value.
/// </summary>
/// <remarks>For material instances that inherit a base material, returned value might come from base material if the current one doesn't override it.</remarks>
/// <param name="name">The parameter name.</param>
/// <returns>The parameter value.</returns>
API_FUNCTION() Variant GetParameterValue(const StringView& name);

View File

@@ -46,6 +46,7 @@ namespace
ContentStorageService ContentStorageServiceInstance;
TimeSpan ContentStorageManager::UnusedStorageLifetime = TimeSpan::FromSeconds(0.5f);
TimeSpan ContentStorageManager::UnusedDataChunksLifetime = TimeSpan::FromSeconds(10);
FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, bool loadIt)

View File

@@ -15,7 +15,12 @@ class FLAXENGINE_API ContentStorageManager
{
public:
/// <summary>
/// Auto-release timeout for unused asset chunks.
/// Auto-release timeout for unused asset files.
/// </summary>
static TimeSpan UnusedStorageLifetime;
/// <summary>
/// Auto-release timeout for unused asset data chunks.
/// </summary>
static TimeSpan UnusedDataChunksLifetime;

View File

@@ -286,14 +286,14 @@ FlaxStorage::LockData FlaxStorage::LockSafe()
uint32 FlaxStorage::GetRefCount() const
{
return (uint32)Platform::AtomicRead((intptr*)&_refCount);
return (uint32)Platform::AtomicRead(&_refCount);
}
bool FlaxStorage::ShouldDispose() const
{
return Platform::AtomicRead((intptr*)&_refCount) == 0 &&
Platform::AtomicRead((intptr*)&_chunksLock) == 0 &&
Platform::GetTimeSeconds() - _lastRefLostTime >= 0.5; // TTL in seconds
return Platform::AtomicRead(&_refCount) == 0 &&
Platform::AtomicRead(&_chunksLock) == 0 &&
Platform::GetTimeSeconds() - _lastRefLostTime >= ContentStorageManager::UnusedStorageLifetime.GetTotalSeconds();
}
uint32 FlaxStorage::GetMemoryUsage() const

View File

@@ -19,6 +19,7 @@
#include "Engine/Animations/AnimEvent.h"
#include "Engine/Level/Actors/EmptyActor.h"
#include "Engine/Level/Actors/StaticModel.h"
#include "Engine/Level/Actors/AnimatedModel.h"
#include "Engine/Level/Prefabs/Prefab.h"
#include "Engine/Level/Prefabs/PrefabManager.h"
#include "Engine/Level/Scripts/ModelPrefab.h"
@@ -82,6 +83,11 @@ bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
struct PrefabObject
{
enum
{
Model,
SkinnedModel,
} Type;
int32 NodeIndex;
String Name;
String AssetPath;
@@ -280,7 +286,7 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context)
options.SplitObjects = false;
options.ObjectIndex = -1;
// Import all of the objects recursive but use current model data to skip loading file again
// Import all the objects recursive but use current model data to skip loading file again
options.Cached = &cached;
HashSet<String> objectNames;
Function<bool(Options& splitOptions, const StringView& objectName, String& outputPath, MeshData* meshData)> splitImport = [&context, &autoImportOutput, &objectNames](Options& splitOptions, const StringView& objectName, String& outputPath, MeshData* meshData)
@@ -335,12 +341,24 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context)
auto& group = meshesByName[groupIndex];
// Cache object options (nested sub-object import removes the meshes)
prefabObject.NodeIndex = group.First()->NodeIndex;
prefabObject.Name = group.First()->Name;
MeshData* firstMesh = group.First();
prefabObject.NodeIndex = firstMesh->NodeIndex;
prefabObject.Name = firstMesh->Name;
splitOptions.Type = ModelTool::ModelType::Model;
// Detect model type
if ((firstMesh->BlendIndices.HasItems() && firstMesh->BlendWeights.HasItems()) || firstMesh->BlendShapes.HasItems())
{
splitOptions.Type = ModelTool::ModelType::SkinnedModel;
prefabObject.Type = PrefabObject::SkinnedModel;
}
else
{
splitOptions.Type = ModelTool::ModelType::Model;
prefabObject.Type = PrefabObject::Model;
}
splitOptions.ObjectIndex = groupIndex;
if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath, group.First()))
if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath, firstMesh))
{
prefabObjects.Add(prefabObject);
}
@@ -734,24 +752,38 @@ CreateAssetResult ImportModel::CreatePrefab(CreateAssetContext& context, const M
nodeActors.Clear();
for (const PrefabObject& e : prefabObjects)
{
if (e.NodeIndex == nodeIndex)
if (e.NodeIndex != nodeIndex)
continue;
Actor* a = nullptr;
switch (e.Type)
{
case PrefabObject::Model:
{
auto* actor = New<StaticModel>();
actor->SetName(e.Name);
if (auto* model = Content::LoadAsync<Model>(e.AssetPath))
{
actor->Model = model;
}
nodeActors.Add(actor);
a = actor;
break;
}
case PrefabObject::SkinnedModel:
{
auto* actor = New<AnimatedModel>();
if (auto* skinnedModel = Content::LoadAsync<SkinnedModel>(e.AssetPath))
actor->SkinnedModel = skinnedModel;
a = actor;
break;
}
default:
continue;
}
a->SetName(e.Name);
nodeActors.Add(a);
}
Actor* nodeActor = nodeActors.Count() == 1 ? nodeActors[0] : New<EmptyActor>();
if (nodeActors.Count() > 1)
{
for (Actor* e : nodeActors)
{
e->SetParent(nodeActor);
}
}
if (nodeActors.Count() != 1)
{

View File

@@ -155,6 +155,7 @@ void Screen::SetCursorLock(CursorLockMode mode)
bool inRelativeMode = Input::Mouse->IsRelative();
if (mode == CursorLockMode::Clipped)
win->StartClippingCursor(bounds);
#if PLATFORM_SDL
else if (mode == CursorLockMode::Locked)
{
// Use mouse clip region to restrict the cursor in one spot
@@ -162,6 +163,10 @@ void Screen::SetCursorLock(CursorLockMode mode)
}
else if (CursorLock == CursorLockMode::Locked || CursorLock == CursorLockMode::Clipped)
win->EndClippingCursor();
#else
else if (CursorLock == CursorLockMode::Clipped)
win->EndClippingCursor();
#endif
// Enable relative mode when cursor is restricted
if (mode != CursorLockMode::None)

View File

@@ -88,9 +88,8 @@ void TerrainMaterialShader::Bind(BindParameters& params)
}
// Bind terrain textures
const auto heightmap = drawCall.Terrain.Patch->Heightmap->GetTexture();
const auto splatmap0 = drawCall.Terrain.Patch->Splatmap[0] ? drawCall.Terrain.Patch->Splatmap[0]->GetTexture() : nullptr;
const auto splatmap1 = drawCall.Terrain.Patch->Splatmap[1] ? drawCall.Terrain.Patch->Splatmap[1]->GetTexture() : nullptr;
GPUTexture* heightmap, *splatmap0, *splatmap1;
drawCall.Terrain.Patch->GetTextures(heightmap, splatmap0, splatmap1);
context->BindSR(0, heightmap);
context->BindSR(1, splatmap0);
context->BindSR(2, splatmap1);

View File

@@ -936,7 +936,9 @@ void InputService::Update()
break;
}
}
#if PLATFORM_SDL
WindowsManager::WindowsLocker.Unlock();
#endif
// Send input events for the focused window
for (const auto& e : InputEvents)
@@ -990,6 +992,9 @@ void InputService::Update()
break;
}
}
#if !PLATFORM_SDL
WindowsManager::WindowsLocker.Unlock();
#endif
// Skip if game has no focus to handle the input
if (!Engine::HasGameViewportFocus())

View File

@@ -448,8 +448,7 @@ void SceneObjectsFactory::PrefabSyncData::InitNewObjects()
void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyncData& data)
{
PROFILE_CPU_NAMED("SetupPrefabInstances");
const int32 count = data.Data.Size();
ASSERT(count <= data.SceneObjects.Count());
const int32 count = Math::Min<int32>(data.Data.Size(), data.SceneObjects.Count());
Dictionary<Guid, Guid> parentIdsLookup;
for (int32 i = 0; i < count; i++)
{

View File

@@ -209,6 +209,13 @@ void Collider::CreateShape()
// Create shape
const bool isTrigger = _isTrigger && CanBeTrigger();
_shape = PhysicsBackend::CreateShape(this, shape, Material, IsActiveInHierarchy(), isTrigger);
if (!_shape)
{
LOG(Error, "Failed to create physics shape for actor '{}'", GetNamePath());
if (shape.Type == CollisionShape::Types::ConvexMesh && Float3(shape.ConvexMesh.Scale).MinValue() <= 0)
LOG(Warning, "Convex Mesh colliders cannot have negative scale");
return;
}
PhysicsBackend::SetShapeContactOffset(_shape, _contactOffset);
UpdateLayerBits();
}
@@ -293,18 +300,20 @@ void Collider::BeginPlay(SceneBeginData* data)
if (_shape == nullptr)
{
CreateShape();
// Check if parent is a rigidbody
const auto rigidBody = dynamic_cast<RigidBody*>(GetParent());
if (rigidBody && CanAttach(rigidBody))
if (_shape)
{
// Attach to the rigidbody
Attach(rigidBody);
}
else
{
// Be a static collider
CreateStaticActor();
// Check if parent is a rigidbody
const auto rigidBody = dynamic_cast<RigidBody*>(GetParent());
if (rigidBody && CanAttach(rigidBody))
{
// Attach to the rigidbody
Attach(rigidBody);
}
else
{
// Be a static collider
CreateStaticActor();
}
}
}

View File

@@ -257,6 +257,8 @@ Asset::LoadResult CollisionData::load()
CollisionData::LoadResult CollisionData::load(const SerializedOptions* options, byte* dataPtr, int32 dataSize)
{
if (WaitForInitPhysics())
return LoadResult::CannotLoadData;
PROFILE_MEM(Physics);
// Load options

View File

@@ -1204,6 +1204,8 @@ void ScenePhysX::PreSimulateCloth(int32 i)
PROFILE_MEM(PhysicsCloth);
auto clothPhysX = ClothsList[i];
auto& clothSettings = Cloths[clothPhysX];
if (!clothSettings.Actor)
return;
if (clothSettings.Actor->OnPreUpdate())
{
@@ -2686,10 +2688,13 @@ void* PhysicsBackend::CreateShape(PhysicsColliderActor* collider, const Collisio
PxGeometryHolder geometryPhysX;
GetShapeGeometry(geometry, geometryPhysX);
PxShape* shapePhysX = PhysX->createShape(geometryPhysX.any(), materialsPhysX.Get(), materialsPhysX.Count(), true, shapeFlags);
shapePhysX->userData = collider;
if (shapePhysX)
{
shapePhysX->userData = collider;
#if PHYSX_DEBUG_NAMING
shapePhysX->setName("Shape");
shapePhysX->setName("Shape");
#endif
}
return shapePhysX;
}

View File

@@ -178,6 +178,27 @@ void RenderAntiAliasingPass(RenderContext& renderContext, GPUTexture* input, GPU
}
}
void RenderLightBuffer(const SceneRenderTask* task, GPUContext* context, RenderContext& renderContext, GPUTexture* lightBuffer, const GPUTextureDescription& tempDesc)
{
context->ResetRenderTarget();
auto colorGradingLUT = ColorGradingPass::Instance()->RenderLUT(renderContext);
auto tempBuffer = RenderTargetPool::Get(tempDesc);
RENDER_TARGET_POOL_SET_NAME(tempBuffer, "TempBuffer");
EyeAdaptationPass::Instance()->Render(renderContext, lightBuffer);
PostProcessingPass::Instance()->Render(renderContext, lightBuffer, tempBuffer, colorGradingLUT);
context->ResetRenderTarget();
if (renderContext.List->Settings.AntiAliasing.Mode == AntialiasingMode::TemporalAntialiasing)
{
TAA::Instance()->Render(renderContext, tempBuffer, lightBuffer->View());
Swap(lightBuffer, tempBuffer);
}
RenderTargetPool::Release(lightBuffer);
context->SetRenderTarget(task->GetOutputView());
context->SetViewportAndScissors(task->GetOutputViewport());
context->Draw(tempBuffer);
RenderTargetPool::Release(tempBuffer);
}
bool Renderer::IsReady()
{
// Warm up first (state getters initialize content loading so do it for all first)
@@ -350,10 +371,12 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
// Perform postFx volumes blending and query before rendering
task->CollectPostFxVolumes(renderContext);
renderContext.List->BlendSettings();
auto aaMode = EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::AntiAliasing) ? renderContext.List->Settings.AntiAliasing.Mode : AntialiasingMode::None;
if (aaMode == AntialiasingMode::TemporalAntialiasing && view.IsOrthographicProjection())
aaMode = AntialiasingMode::None; // TODO: support TAA in ortho projection (see RenderView::Prepare to jitter projection matrix better)
renderContext.List->Settings.AntiAliasing.Mode = aaMode;
{
auto aaMode = EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::AntiAliasing) ? renderContext.List->Settings.AntiAliasing.Mode : AntialiasingMode::None;
if (aaMode == AntialiasingMode::TemporalAntialiasing && view.IsOrthographicProjection())
aaMode = AntialiasingMode::None; // TODO: support TAA in ortho projection (see RenderView::Prepare to jitter projection matrix better)
renderContext.List->Settings.AntiAliasing.Mode = aaMode;
}
// Initialize setup
RenderSetup& setup = renderContext.List->Setup;
@@ -375,7 +398,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
(ssrSettings.Intensity > ZeroTolerance && ssrSettings.TemporalEffect && EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::SSR)) ||
renderContext.List->Settings.AntiAliasing.Mode == AntialiasingMode::TemporalAntialiasing;
}
setup.UseTemporalAAJitter = aaMode == AntialiasingMode::TemporalAntialiasing;
setup.UseTemporalAAJitter = renderContext.List->Settings.AntiAliasing.Mode == AntialiasingMode::TemporalAntialiasing;
setup.UseGlobalSurfaceAtlas = renderContext.View.Mode == ViewMode::GlobalSurfaceAtlas ||
(EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::GI) && renderContext.List->Settings.GlobalIllumination.Mode == GlobalIlluminationMode::DDGI);
setup.UseGlobalSDF = (graphicsSettings->EnableGlobalSDF && EnumHasAnyFlags(view.Flags, ViewFlags::GlobalSDF)) ||
@@ -630,22 +653,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
}
if (renderContext.View.Mode == ViewMode::LightBuffer)
{
auto colorGradingLUT = ColorGradingPass::Instance()->RenderLUT(renderContext);
auto tempBuffer = RenderTargetPool::Get(tempDesc);
RENDER_TARGET_POOL_SET_NAME(tempBuffer, "TempBuffer");
EyeAdaptationPass::Instance()->Render(renderContext, lightBuffer);
PostProcessingPass::Instance()->Render(renderContext, lightBuffer, tempBuffer, colorGradingLUT);
context->ResetRenderTarget();
if (aaMode == AntialiasingMode::TemporalAntialiasing)
{
TAA::Instance()->Render(renderContext, tempBuffer, lightBuffer->View());
Swap(lightBuffer, tempBuffer);
}
RenderTargetPool::Release(lightBuffer);
context->SetRenderTarget(task->GetOutputView());
context->SetViewportAndScissors(task->GetOutputViewport());
context->Draw(tempBuffer);
RenderTargetPool::Release(tempBuffer);
RenderLightBuffer(task, context, renderContext, lightBuffer, tempDesc);
return;
}
@@ -656,11 +664,13 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
ReflectionsPass::Instance()->Render(renderContext, *lightBuffer);
if (renderContext.View.Mode == ViewMode::Reflections)
{
context->ResetRenderTarget();
context->SetRenderTarget(task->GetOutputView());
context->SetViewportAndScissors(task->GetOutputViewport());
context->Draw(lightBuffer);
RenderTargetPool::Release(lightBuffer);
renderContext.List->Settings.ToneMapping.Mode = ToneMappingMode::Neutral;
renderContext.List->Settings.Bloom.Enabled = false;
renderContext.List->Settings.LensFlares.Intensity = 0.0f;
renderContext.List->Settings.CameraArtifacts.GrainAmount = 0.0f;
renderContext.List->Settings.CameraArtifacts.ChromaticDistortion = 0.0f;
renderContext.List->Settings.CameraArtifacts.VignetteIntensity = 0.0f;
RenderLightBuffer(task, context, renderContext, lightBuffer, tempDesc);
return;
}
@@ -716,7 +726,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
renderContext.List->RunCustomPostFxPass(context, renderContext, PostProcessEffectLocation::BeforePostProcessingPass, frameBuffer, tempBuffer);
// Temporal Anti-Aliasing (goes before post processing)
if (aaMode == AntialiasingMode::TemporalAntialiasing)
if (renderContext.List->Settings.AntiAliasing.Mode == AntialiasingMode::TemporalAntialiasing)
{
TAA::Instance()->Render(renderContext, frameBuffer, tempBuffer->View());
Swap(frameBuffer, tempBuffer);

View File

@@ -20,6 +20,7 @@
#define RAPIDJSON_NEW(x) New<x>
#define RAPIDJSON_DELETE(x) Delete(x)
#define RAPIDJSON_NOMEMBERITERATORCLASS
#define RAPIDJSON_PARSE_DEFAULT_FLAGS kParseTrailingCommasFlag
//#define RAPIDJSON_MALLOC(size) ::malloc(size)
//#define RAPIDJSON_REALLOC(ptr, new_size) ::realloc(ptr, new_size)
//#define RAPIDJSON_FREE(ptr) ::free(ptr)

View File

@@ -8,13 +8,22 @@ using Flax.Build.NativeCpp;
/// </summary>
public class Terrain : EngineModule
{
/// <summary>
/// Enables terrain editing and changing at runtime. If your game doesn't use procedural terrain in game then disable this option to reduce build size.
/// </summary>
public static bool WithEditing = true;
/// <inheritdoc />
public override void Setup(BuildOptions options)
{
base.Setup(options);
options.PrivateDependencies.Add("Physics");
if (!WithEditing)
{
options.PublicDefinitions.Add("TERRAIN_EDITING=0");
}
options.PrivateDependencies.Add("Physics");
if (options.Target.IsEditor)
{
options.PrivateDependencies.Add("ContentImporters");

View File

@@ -306,6 +306,16 @@ void Terrain::SetPhysicalMaterials(const Array<JsonAssetReference<PhysicalMateri
}
}
int32 Terrain::GetHeightmapSize() const
{
return GetChunkSize() * ChunksCountEdge + 1;
}
float Terrain::GetPatchSize() const
{
return TERRAIN_UNITS_PER_VERTEX * ChunksCountEdge * GetChunkSize();
}
TerrainPatch* Terrain::GetPatch(const Int2& patchCoord) const
{
return GetPatch(patchCoord.X, patchCoord.Y);

View File

@@ -21,11 +21,14 @@ struct RenderView;
// Amount of units per terrain geometry vertex (can be adjusted per terrain instance using non-uniform scale factor)
#define TERRAIN_UNITS_PER_VERTEX 100.0f
// Enable/disable terrain editing and changing at runtime. If your game doesn't use procedural terrain in game then disable this option to reduce build size.
#ifndef TERRAIN_EDITING
// Enables terrain editing and changing at runtime. If your game doesn't use procedural terrain in game then disable this option to reduce build size.
#define TERRAIN_EDITING 1
#endif
// Enable/disable terrain heightmap samples modification and gather. Used by the editor to modify the terrain with the brushes.
#define TERRAIN_UPDATING 1
// [Deprecated in 1.12, use TERRAIN_EDITING instead]
#define TERRAIN_UPDATING (TERRAIN_EDITING)
// Enable/disable terrain physics collision drawing
#define TERRAIN_USE_PHYSICS_DEBUG (USE_EDITOR && 1)
@@ -240,6 +243,18 @@ public:
return static_cast<int32>(_chunkSize);
}
/// <summary>
/// Gets the heightmap texture size (square) used by a single patch (shared by all chunks within that patch).
/// </summary>
/// <remarks>ChunkSize * ChunksCountEdge + 1</remarks>
API_PROPERTY() int32 GetHeightmapSize() const;
/// <summary>
/// Gets the size of the patch in world-units (square) without actor scale.
/// </summary>
/// <remarks>UnitsPerVertex * ChunksCountEdge * ChunkSize</remarks>
API_PROPERTY() float GetPatchSize() const;
/// <summary>
/// Gets the terrain patches count. Each patch contains 16 chunks arranged into a 4x4 square.
/// </summary>
@@ -329,7 +344,6 @@ public:
API_FUNCTION() void SetChunkOverrideMaterial(API_PARAM(Ref) const Int2& patchCoord, API_PARAM(Ref) const Int2& chunkCoord, MaterialBase* value);
#if TERRAIN_EDITING
/// <summary>
/// Setups the terrain patch using the specified heightmap data.
/// </summary>
@@ -352,10 +366,6 @@ public:
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool SetupPatchSplatMap(API_PARAM(Ref) const Int2& patchCoord, int32 index, int32 splatMapLength, const Color32* splatMap, bool forceUseVirtualStorage = false);
#endif
public:
#if TERRAIN_EDITING
/// <summary>
/// Setups the terrain. Clears the existing data.
/// </summary>

View File

@@ -17,6 +17,7 @@
#include "Engine/Threading/Threading.h"
#if TERRAIN_EDITING
#include "Engine/Core/Math/Packed.h"
#include "Engine/Core/Collections/ArrayExtensions.h"
#include "Engine/Graphics/PixelFormatExtensions.h"
#include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/RenderView.h"
@@ -27,11 +28,6 @@
#include "Editor/Editor.h"
#include "Engine/ContentImporters/AssetsImportingManager.h"
#endif
#endif
#if TERRAIN_EDITING || TERRAIN_UPDATING
#include "Engine/Core/Collections/ArrayExtensions.h"
#endif
#if USE_EDITOR
#include "Engine/Debug/DebugDraw.h"
#endif
#if TERRAIN_USE_PHYSICS_DEBUG
@@ -90,7 +86,7 @@ void TerrainPatch::Init(Terrain* terrain, int16 x, int16 z)
Splatmap[i] = nullptr;
}
_heightfield = nullptr;
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
_cachedHeightMap.Resize(0);
_cachedHolesMask.Resize(0);
_wasHeightModified = false;
@@ -114,7 +110,7 @@ void TerrainPatch::Init(Terrain* terrain, int16 x, int16 z)
TerrainPatch::~TerrainPatch()
{
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
SAFE_DELETE(_dataHeightmap);
for (int32 i = 0; i < TERRAIN_MAX_SPLATMAPS_COUNT; i++)
{
@@ -134,6 +130,13 @@ RawDataAsset* TerrainPatch::GetHeightfield() const
return _heightfield.Get();
}
void TerrainPatch::GetTextures(GPUTexture*& heightmap, GPUTexture*& splatmap0, GPUTexture*& splatmap1) const
{
heightmap = Heightmap->GetTexture();
splatmap0 = Splatmap[0] ? Splatmap[0]->GetTexture() : nullptr;
splatmap1 = Splatmap[1] ? Splatmap[1]->GetTexture() : nullptr;
}
void TerrainPatch::RemoveLightmap()
{
for (auto& chunk : Chunks)
@@ -178,7 +181,7 @@ void TerrainPatch::UpdateTransform()
_collisionVertices.Resize(0);
}
#if TERRAIN_EDITING || TERRAIN_UPDATING
#if TERRAIN_EDITING
bool IsValidMaterial(const JsonAssetReference<PhysicalMaterial>& e)
{
@@ -217,7 +220,7 @@ struct TerrainDataUpdateInfo
// When using physical materials, then get splatmaps data required for per-triangle material indices
void GetSplatMaps()
{
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
if (SplatMaps[0])
return;
if (UsePhysicalMaterials())
@@ -1021,7 +1024,7 @@ bool TerrainPatch::SetupHeightMap(int32 heightMapLength, const float* heightMap,
_terrain->UpdateBounds();
_terrain->UpdateLayerBits();
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
// Invalidate cache
_cachedHeightMap.Resize(0);
_cachedHolesMask.Resize(0);
@@ -1169,7 +1172,7 @@ bool TerrainPatch::SetupSplatMap(int32 index, int32 splatMapLength, const Color3
}
#endif
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
// Invalidate cache
_cachedSplatMap[index].Resize(0);
_wasSplatmapModified[index] = false;
@@ -1191,7 +1194,7 @@ bool TerrainPatch::InitializeHeightMap()
return SetupHeightMap(heightmap.Count(), heightmap.Get());
}
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
float* TerrainPatch::GetHeightmapData()
{
@@ -2631,7 +2634,7 @@ void TerrainPatch::Serialize(SerializeStream& stream, const void* otherObj)
}
stream.EndArray();
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
SaveHeightData();
SaveSplatData();
#endif

View File

@@ -12,6 +12,10 @@
struct RayCastHit;
class TerrainMaterialShader;
#ifndef TERRAIN_EDITING
#define TERRAIN_EDITING 1
#endif
/// <summary>
/// Represents single terrain patch made of 16 terrain chunks.
/// </summary>
@@ -34,7 +38,7 @@ private:
void* _physicsHeightField;
CriticalSection _collisionLocker;
float _collisionScaleXZ;
#if TERRAIN_UPDATING
#if TERRAIN_EDITING
Array<float> _cachedHeightMap;
Array<byte> _cachedHolesMask;
Array<Color32> _cachedSplatMap[TERRAIN_MAX_SPLATMAPS_COUNT];
@@ -189,6 +193,8 @@ public:
return _bounds;
}
void GetTextures(GPUTexture*& heightmap, GPUTexture*& splatmap0, GPUTexture*& splatmap1) const;
public:
/// <summary>
/// Removes the lightmap data from the terrain patch.
@@ -220,7 +226,7 @@ public:
/// <param name="holesMask">The holes mask (optional). Normalized to 0-1 range values with holes mask per-vertex. Must match the heightmap dimensions.</param>
/// <param name="forceUseVirtualStorage">If set to <c>true</c> patch will use virtual storage by force. Otherwise it can use normal texture asset storage on drive (valid only during Editor). Runtime-created terrain can only use virtual storage (in RAM).</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool SetupHeightMap(int32 heightMapLength, API_PARAM(Ref) const float* heightMap, API_PARAM(Ref) const byte* holesMask = nullptr, bool forceUseVirtualStorage = false);
API_FUNCTION() bool SetupHeightMap(int32 heightMapLength, const float* heightMap, const byte* holesMask = nullptr, bool forceUseVirtualStorage = false);
/// <summary>
/// Setups the terrain patch layer weights using the specified splatmaps data.
@@ -230,14 +236,12 @@ public:
/// <param name="splatMap">The splat map. Each array item contains 4 layer weights.</param>
/// <param name="forceUseVirtualStorage">If set to <c>true</c> patch will use virtual storage by force. Otherwise it can use normal texture asset storage on drive (valid only during Editor). Runtime-created terrain can only use virtual storage (in RAM).</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool SetupSplatMap(int32 index, int32 splatMapLength, API_PARAM(Ref) const Color32* splatMap, bool forceUseVirtualStorage = false);
#endif
API_FUNCTION() bool SetupSplatMap(int32 index, int32 splatMapLength, const Color32* splatMap, bool forceUseVirtualStorage = false);
#if TERRAIN_UPDATING
/// <summary>
/// Gets the raw pointer to the heightmap data.
/// Gets the raw pointer to the heightmap data. Array size is square of Terrain.HeightmapSize.
/// </summary>
/// <returns>The heightmap data.</returns>
/// <returns>The heightmap data. Null if empty or failed to access it.</returns>
API_FUNCTION() float* GetHeightmapData();
/// <summary>
@@ -246,9 +250,9 @@ public:
API_FUNCTION() void ClearHeightmapCache();
/// <summary>
/// Gets the raw pointer to the holes mask data.
/// Gets the raw pointer to the holes mask data. Array size is square of Terrain.HeightmapSize.
/// </summary>
/// <returns>The holes mask data.</returns>
/// <returns>The holes mask data. Null if empty/unused or failed to access it.</returns>
API_FUNCTION() byte* GetHolesMaskData();
/// <summary>
@@ -257,10 +261,10 @@ public:
API_FUNCTION() void ClearHolesMaskCache();
/// <summary>
/// Gets the raw pointer to the splat map data.
/// Gets the raw pointer to the splat map data. Array size is square of Terrain.HeightmapSize.
/// </summary>
/// <param name="index">The zero-based index of the splatmap texture.</param>
/// <returns>The splat map data.</returns>
/// <returns>The splat map data. Null if empty/unused or failed to access it.</returns>
API_FUNCTION() Color32* GetSplatMapData(int32 index);
/// <summary>
@@ -280,7 +284,7 @@ public:
/// <param name="modifiedOffset">The offset from the first row and column of the heightmap data (offset destination x and z start position).</param>
/// <param name="modifiedSize">The size of the heightmap to modify (x and z). Amount of samples in each direction.</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool ModifyHeightMap(API_PARAM(Ref) const float* samples, API_PARAM(Ref) const Int2& modifiedOffset, API_PARAM(Ref) const Int2& modifiedSize);
API_FUNCTION() bool ModifyHeightMap(const float* samples, const Int2& modifiedOffset, const Int2& modifiedSize);
/// <summary>
/// Modifies the terrain patch holes mask with the given samples.
@@ -289,7 +293,7 @@ public:
/// <param name="modifiedOffset">The offset from the first row and column of the holes map data (offset destination x and z start position).</param>
/// <param name="modifiedSize">The size of the holes map to modify (x and z). Amount of samples in each direction.</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool ModifyHolesMask(API_PARAM(Ref) const byte* samples, API_PARAM(Ref) const Int2& modifiedOffset, API_PARAM(Ref) const Int2& modifiedSize);
API_FUNCTION() bool ModifyHolesMask(const byte* samples, const Int2& modifiedOffset, const Int2& modifiedSize);
/// <summary>
/// Modifies the terrain patch splat map (layers mask) with the given samples.
@@ -299,7 +303,7 @@ public:
/// <param name="modifiedOffset">The offset from the first row and column of the splat map data (offset destination x and z start position).</param>
/// <param name="modifiedSize">The size of the splat map to modify (x and z). Amount of samples in each direction.</param>
/// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool ModifySplatMap(int32 index, API_PARAM(Ref) const Color32* samples, API_PARAM(Ref) const Int2& modifiedOffset, API_PARAM(Ref) const Int2& modifiedSize);
API_FUNCTION() bool ModifySplatMap(int32 index, const Color32* samples, const Int2& modifiedOffset, const Int2& modifiedSize);
private:
bool UpdateHeightData(struct TerrainDataUpdateInfo& info, const Int2& modifiedOffset, const Int2& modifiedSize, bool wasHeightRangeChanged, bool wasHeightChanged);

View File

@@ -588,6 +588,7 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj
SERIALIZE(SloppyOptimization);
SERIALIZE(LODTargetError);
SERIALIZE(ImportMaterials);
SERIALIZE(CreateEmptyMaterialSlots);
SERIALIZE(ImportMaterialsAsInstances);
SERIALIZE(InstanceToImportAs);
SERIALIZE(ImportTextures);
@@ -643,6 +644,7 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi
DESERIALIZE(SloppyOptimization);
DESERIALIZE(LODTargetError);
DESERIALIZE(ImportMaterials);
DESERIALIZE(CreateEmptyMaterialSlots);
DESERIALIZE(ImportMaterialsAsInstances);
DESERIALIZE(InstanceToImportAs);
DESERIALIZE(ImportTextures);
@@ -1019,7 +1021,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
options.ImportTypes |= ImportDataTypes::Skeleton;
break;
case ModelType::Prefab:
options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Animations;
options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Skeleton | ImportDataTypes::Animations;
if (options.ImportMaterials)
options.ImportTypes |= ImportDataTypes::Materials;
if (options.ImportTextures)
@@ -1045,6 +1047,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
{
for (auto& mesh : lod.Meshes)
{
if (mesh->BlendShapes.IsEmpty())
continue;
for (int32 blendShapeIndex = mesh->BlendShapes.Count() - 1; blendShapeIndex >= 0; blendShapeIndex--)
{
auto& blendShape = mesh->BlendShapes[blendShapeIndex];
@@ -1209,7 +1213,9 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
for (int32 i = 0; i < meshesCount; i++)
{
const auto mesh = data.LODs[0].Meshes[i];
if (mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty())
// If imported mesh has skeleton but no indices or weights then need to setup those (except in Prefab mode when we conditionally import meshes based on type)
if ((mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty()) && data.Skeleton.Bones.HasItems() && (options.Type != ModelType::Prefab))
{
auto indices = Int4::Zero;
auto weights = Float4::UnitX;
@@ -1326,7 +1332,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
auto& texture = data.Textures[i];
// Auto-import textures
if (autoImportOutput.IsEmpty() || EnumHasNoneFlags(options.ImportTypes, ImportDataTypes::Textures) || texture.FilePath.IsEmpty())
if (autoImportOutput.IsEmpty() || EnumHasNoneFlags(options.ImportTypes, ImportDataTypes::Textures) || texture.FilePath.IsEmpty() || options.CreateEmptyMaterialSlots)
continue;
String assetPath = GetAdditionalImportPath(autoImportOutput, importedFileNames, StringUtils::GetFileNameWithoutExtension(texture.FilePath));
#if COMPILE_WITH_ASSETS_IMPORTER
@@ -1384,6 +1390,10 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
}
}
// The rest of the steps this function performs become irrelevant when we're only creating slots.
if (options.CreateEmptyMaterialSlots)
continue;
if (options.ImportMaterialsAsInstances)
{
// Create material instance
@@ -2021,12 +2031,11 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
#undef REMAP_VERTEX_BUFFER
// Remap blend shapes
dstMesh->BlendShapes.Resize(srcMesh->BlendShapes.Count());
dstMesh->BlendShapes.EnsureCapacity(srcMesh->BlendShapes.Count(), false);
for (int32 blendShapeIndex = 0; blendShapeIndex < srcMesh->BlendShapes.Count(); blendShapeIndex++)
{
const auto& srcBlendShape = srcMesh->BlendShapes[blendShapeIndex];
auto& dstBlendShape = dstMesh->BlendShapes[blendShapeIndex];
BlendShape dstBlendShape;
dstBlendShape.Name = srcBlendShape.Name;
dstBlendShape.Weight = srcBlendShape.Weight;
dstBlendShape.Vertices.EnsureCapacity(srcBlendShape.Vertices.Count());
@@ -2035,17 +2044,12 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
auto v = srcBlendShape.Vertices[i];
v.VertexIndex = remap[v.VertexIndex];
if (v.VertexIndex != ~0u)
{
dstBlendShape.Vertices.Add(v);
}
}
}
// Remove empty blend shapes
for (int32 blendShapeIndex = dstMesh->BlendShapes.Count() - 1; blendShapeIndex >= 0; blendShapeIndex--)
{
if (dstMesh->BlendShapes[blendShapeIndex].Vertices.IsEmpty())
dstMesh->BlendShapes.RemoveAt(blendShapeIndex);
// Add only valid blend shapes
if (dstBlendShape.Vertices.HasItems())
dstMesh->BlendShapes.Add(dstBlendShape);
}
// Optimize generated LOD
@@ -2092,6 +2096,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
{
for (auto& mesh : lod.Meshes)
{
if (mesh->BlendShapes.IsEmpty())
continue;
for (auto& blendShape : mesh->BlendShapes)
{
// Compute min/max for used vertex indices

View File

@@ -311,16 +311,19 @@ public:
public: // Materials
// If checked, the importer will create materials for model meshes as specified in the file.
API_FIELD(Attributes="EditorOrder(400), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))")
API_FIELD(Attributes="EditorOrder(399), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))")
bool ImportMaterials = true;
// If checked, the importer will create empty material slots for every material without importing materials nor textures.
API_FIELD(Attributes="EditorOrder(400), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))")
bool CreateEmptyMaterialSlots = false;
// If checked, the importer will create the model's materials as instances of a base material.
API_FIELD(Attributes="EditorOrder(401), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterials)), VisibleIf(nameof(ShowGeometry))")
API_FIELD(Attributes="EditorOrder(401), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterials)), VisibleIf(nameof(ShowGeometry)), VisibleIf(nameof(CreateEmptyMaterialSlots), true)")
bool ImportMaterialsAsInstances = false;
// The material used as the base material that will be instanced as the imported model's material.
API_FIELD(Attributes="EditorOrder(402), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterialsAsInstances)), VisibleIf(nameof(ShowGeometry))")
API_FIELD(Attributes="EditorOrder(402), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterialsAsInstances)), VisibleIf(nameof(ShowGeometry)), VisibleIf(nameof(CreateEmptyMaterialSlots), true)")
AssetReference<MaterialBase> InstanceToImportAs;
// If checked, the importer will import texture files used by the model and any embedded texture resources.
API_FIELD(Attributes="EditorOrder(410), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))")
API_FIELD(Attributes="EditorOrder(410), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry)), VisibleIf(nameof(CreateEmptyMaterialSlots), true)")
bool ImportTextures = true;
// If checked, the importer will try to keep the model's current overridden material slots, instead of importing materials from the source file.
API_FIELD(Attributes="EditorOrder(420), EditorDisplay(\"Materials\", \"Keep Overridden Materials\"), VisibleIf(nameof(ShowGeometry))")

View File

@@ -414,6 +414,7 @@ bool TextureTool::UpdateTexture(GPUContext* context, GPUTexture* texture, int32
Array<byte> tempData;
if (textureFormat != dataFormat)
{
PROFILE_CPU_NAMED("ConvertTexture");
auto dataSampler = PixelFormatSampler::Get(dataFormat);
auto textureSampler = PixelFormatSampler::Get(textureFormat);
if (!dataSampler || !textureSampler)

View File

@@ -5,14 +5,14 @@ using System;
namespace FlaxEngine.GUI
{
/// <summary>
/// Radial menu control that arranges child controls (of type Image) in a circle.
/// Radial menu control that arranges child controls (of type <see cref="FlaxEngine.GUI.Image"/>) in a circle.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
public class RadialMenu : ContainerControl
{
private bool _materialIsDirty = true;
private float _angle;
private float _selectedSegment;
private int _selectedSegment;
private int _highlightSegment = -1;
private MaterialBase _material;
private MaterialInstance _materialInstance;
@@ -27,7 +27,7 @@ namespace FlaxEngine.GUI
private bool ShowMatProp => _material != null;
/// <summary>
/// The material to use for menu background drawing.
/// The material used for menu background drawing.
/// </summary>
[EditorOrder(1)]
public MaterialBase Material
@@ -44,7 +44,7 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// Gets or sets the edge offset.
/// Gets or sets the offset of the outer edge from the bounds of the Control.
/// </summary>
[EditorOrder(2), Range(0, 1)]
public float EdgeOffset
@@ -59,7 +59,7 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// Gets or sets the thickness.
/// Gets or sets the thickness of the menu.
/// </summary>
[EditorOrder(3), Range(0, 1), VisibleIf(nameof(ShowMatProp))]
public float Thickness
@@ -74,7 +74,7 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// Gets or sets control background color (transparent color (alpha=0) means no background rendering).
/// Gets or sets control background color (transparent color means no background rendering).
/// </summary>
[VisibleIf(nameof(ShowMatProp))]
public new Color BackgroundColor
@@ -88,7 +88,7 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// Gets or sets the color of the highlight.
/// Gets or sets the color of the outer edge highlight.
/// </summary>
[VisibleIf(nameof(ShowMatProp))]
public Color HighlightColor
@@ -130,19 +130,43 @@ namespace FlaxEngine.GUI
}
/// <summary>
/// The selected callback
/// The material instance of <see cref="Material"/> used to draw the menu.
/// </summary>
[HideInEditor]
public MaterialInstance MaterialInstance => _materialInstance;
/// <summary>
/// The selected callback.
/// </summary>
[HideInEditor]
public Action<int> Selected;
/// <summary>
/// The allow change selection when inside
/// Invoked when the hovered segment is changed.
/// </summary>
[HideInEditor]
public Action<int> HoveredSelectionChanged;
/// <summary>
/// The selected segment.
/// </summary>
[HideInEditor]
public int SelectedSegment => _selectedSegment;
/// <summary>
/// Allows the selected to change when the mouse is moved in the empty center of the menu.
/// </summary>
[VisibleIf(nameof(ShowMatProp))]
public bool AllowChangeSelectionWhenInside;
/// <summary>
/// The center as button
/// Allows the selected to change when the mouse is moved outside of the menu.
/// </summary>
[VisibleIf(nameof(ShowMatProp))]
public bool AllowChangeSelectionWhenOutside;
/// <summary>
/// Wether the center is a button.
/// </summary>
[VisibleIf(nameof(ShowMatProp))]
public bool CenterAsButton;
@@ -225,7 +249,7 @@ namespace FlaxEngine.GUI
var min = ((1 - _edgeOffset) - _thickness) * USize * 0.5f;
var max = (1 - _edgeOffset) * USize * 0.5f;
var val = ((USize * 0.5f) - location).Length;
if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside)
if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside || val > max && AllowChangeSelectionWhenOutside)
{
UpdateAngle(ref location);
}
@@ -276,7 +300,7 @@ namespace FlaxEngine.GUI
var min = ((1 - _edgeOffset) - _thickness) * USize * 0.5f;
var max = (1 - _edgeOffset) * USize * 0.5f;
var val = ((USize * 0.5f) - location).Length;
if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside)
if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside || val > max && AllowChangeSelectionWhenOutside)
{
UpdateAngle(ref location);
}
@@ -347,6 +371,28 @@ namespace FlaxEngine.GUI
base.PerformLayout(force);
}
/// <summary>
/// Updates the current angle and selected segment of the radial menu based on the specified location inside of the control.
/// </summary>
/// <param name="location">The position used to determine the angle and segment selection within the radial menu.</param>
public void UpdateAngle(ref Float2 location)
{
float previousSelectedSegment = _selectedSegment;
var size = new Float2(USize);
var p = (size * 0.5f) - location;
var sa = (1.0f / _segmentCount) * Mathf.TwoPi;
_angle = Mathf.Atan2(p.X, p.Y);
_angle = Mathf.Ceil((_angle - (sa * 0.5f)) / sa) * sa;
_selectedSegment = Mathf.RoundToInt((_angle < 0 ? Mathf.TwoPi + _angle : _angle) / sa);
if (float.IsNaN(_angle) || float.IsInfinity(_angle))
_angle = 0;
_materialInstance.SetParameterValue("RadialMenu_Rotation", -_angle + Mathf.Pi);
if (previousSelectedSegment != _selectedSegment)
HoveredSelectionChanged?.Invoke((int)_selectedSegment);
}
private void UpdateSelectionColor()
{
Color color;
@@ -368,20 +414,6 @@ namespace FlaxEngine.GUI
_materialInstance.SetParameterValue("RadialMenu_SelectionColor", color);
}
private void UpdateAngle(ref Float2 location)
{
var size = new Float2(USize);
var p = (size * 0.5f) - location;
var sa = (1.0f / _segmentCount) * Mathf.TwoPi;
_angle = Mathf.Atan2(p.X, p.Y);
_angle = Mathf.Ceil((_angle - (sa * 0.5f)) / sa) * sa;
_selectedSegment = _angle;
_selectedSegment = Mathf.RoundToInt((_selectedSegment < 0 ? Mathf.TwoPi + _selectedSegment : _selectedSegment) / sa);
if (float.IsNaN(_angle) || float.IsInfinity(_angle))
_angle = 0;
_materialInstance.SetParameterValue("RadialMenu_Rotation", -_angle + Mathf.Pi);
}
private static Float2 Rotate2D(Float2 point, float angle)
{
return new Float2(Mathf.Cos(angle) * point.X + Mathf.Sin(angle) * point.Y,

View File

@@ -6,6 +6,8 @@ using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Flax.Build
{
@@ -407,6 +409,15 @@ namespace Flax.Build
Configure(GetMembers(obj), obj, configuration);
}
internal static void ConfigureChild(Type type, Dictionary<string, string> configuration, string name = null)
{
if (configuration.TryGetValue(name ?? type.Name, out var subConfig) && subConfig?.Length != 0)
{
var child = JsonSerializer.Deserialize<Dictionary<string, string>>(subConfig.AsSpan(), ProjectInfo.JsonOptions);
Configure(type, child);
}
}
private static void Configure(Dictionary<CommandLineAttribute, MemberInfo> members, object instance, string commandLine)
{
if (commandLine == null)

View File

@@ -259,6 +259,42 @@ namespace Flax.Build
}
}
/// <summary>
/// Platform-specific configuration for Windows.
/// </summary>
public static partial class WindowsConfiguration
{
/// <summary>
/// [Windows] True if SDL support should be enabled.
/// </summary>
[CommandLine("useSdl", "1 to enable SDL support in build on Windows")]
public static bool UseSDL = false;
}
/// <summary>
/// Platform-specific configuration for Linux.
/// </summary>
public static partial class LinuxConfiguration
{
/// <summary>
/// [Linux] True if SDL support should be enabled.
/// </summary>
[CommandLine("useSdl", "1 to enable SDL support in build on Linux")]
public static bool UseSDL = false;
}
/// <summary>
/// Platform-specific configuration for Mac.
/// </summary>
public static partial class MacConfiguration
{
/// <summary>
/// [Mac] True if SDL support should be enabled.
/// </summary>
[CommandLine("useSdl", "1 to enable SDL support in build on Mac")]
public static bool UseSDL = false;
}
/// <summary>
/// The engine configuration options.
/// </summary>
@@ -317,9 +353,11 @@ namespace Flax.Build
switch (options.Platform.Target)
{
case TargetPlatform.Windows:
case TargetPlatform.Linux:
return UseSDL && WindowsConfiguration.UseSDL;
case TargetPlatform.Mac:
return UseSDL;
return UseSDL && MacConfiguration.UseSDL;
case TargetPlatform.Linux:
return UseSDL && LinuxConfiguration.UseSDL;
case TargetPlatform.Web:
return true;
default: return false;

View File

@@ -201,7 +201,7 @@ namespace Flax.Deploy
Log.Info("Building disk image...");
if (File.Exists(dmgPath))
File.Delete(dmgPath);
Utilities.Run("hdiutil", $"create -srcFolder \"{appPath}\" -o \"{dmgPath}\"", null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError);
Utilities.Run("hdiutil", $"create -srcFolder \"{appPath}\" -o \"{dmgPath}\" -force", null, null, Utilities.RunOptions.Default | Utilities.RunOptions.ThrowExceptionOnError);
CodeSign(dmgPath);
Log.Info("Output disk image size: " + Utilities.GetFileSize(dmgPath));

View File

@@ -153,9 +153,9 @@ namespace Flax.Deps.Dependencies
{
var envVars = new Dictionary<string, string>
{
{ "CC", "clang-" + Configuration.LinuxClangMinVer },
{ "CC_FOR_BUILD", "clang-" + Configuration.LinuxClangMinVer },
{ "CXX", "clang-" + Configuration.LinuxClangMinVer },
{ "CC", "clang-" + LinuxConfiguration.ClangMinVer },
{ "CC_FOR_BUILD", "clang-" + LinuxConfiguration.ClangMinVer },
{ "CXX", "clang-" + LinuxConfiguration.ClangMinVer },
{ "CMAKE_BUILD_PARALLEL_LEVEL", CmakeBuildParallel },
};

Some files were not shown because too many files have changed in this diff Show More