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

This commit is contained in:
2026-03-27 11:22:32 +01:00
129 changed files with 1493 additions and 402 deletions
+1
View File
@@ -9,6 +9,7 @@ gdb -q --batch \
-ex 'handle SIGUSR1 nostop pass' \ -ex 'handle SIGUSR1 nostop pass' \
-ex 'handle SIGUSR2 nostop pass' \ -ex 'handle SIGUSR2 nostop pass' \
-ex 'handle SIGCHLD nostop pass' \ -ex 'handle SIGCHLD nostop pass' \
-ex 'handle SIG34 nostop pass' \
-ex 'set print thread-events off' \ -ex 'set print thread-events off' \
-return-child-result \ -return-child-result \
-ex 'run' \ -ex 'run' \
+5 -1
View File
@@ -4,6 +4,7 @@ on: [push, pull_request]
env: env:
DOTNET_NOLOGO: true DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: false DOTNET_CLI_TELEMETRY_OPTOUT: false
DOTNET_ROLL_FORWARD: 'minor'
jobs: jobs:
@@ -19,7 +20,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 8.0.x dotnet-version: 8.0.419
- name: Setup .NET Workload - name: Setup .NET Workload
run: | run: |
dotnet workload install android dotnet workload install android
@@ -33,4 +34,7 @@ jobs:
git lfs pull git lfs pull
- name: Build - name: Build
run: | run: |
PowerShell "(Get-Content global.json).Replace('latestMajor', 'minor') | Set-Content global.json"
PowerShell "(Get-Content Source/Tools/Flax.Build/global.json).Replace('latestMajor', 'minor') | Set-Content Source/Tools/Flax.Build/global.json"
PowerShell "(Get-Content Source/Tools/Flax.Build/Flax.Build.csproj).Replace('LatestMajor', 'Minor') | Set-Content Source/Tools/Flax.Build/Flax.Build.csproj"
.\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -dotnet=8 -arch=ARM64 -platform=Android -configuration=Release -buildtargets=FlaxGame .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -dotnet=8 -arch=ARM64 -platform=Android -configuration=Release -buildtargets=FlaxGame
+9 -2
View File
@@ -4,6 +4,7 @@ on: [push, pull_request]
env: env:
DOTNET_NOLOGO: true DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: false DOTNET_CLI_TELEMETRY_OPTOUT: false
DOTNET_ROLL_FORWARD: 'minor'
jobs: jobs:
@@ -19,7 +20,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 8.0.x dotnet-version: 8.0.419
- name: Print .NET info - name: Print .NET info
run: | run: |
dotnet --info dotnet --info
@@ -30,6 +31,9 @@ jobs:
git lfs pull git lfs pull
- name: Build - name: Build
run: | run: |
PowerShell "(Get-Content global.json).Replace('latestMajor', 'minor') | Set-Content global.json"
PowerShell "(Get-Content Source/Tools/Flax.Build/global.json).Replace('latestMajor', 'minor') | Set-Content Source/Tools/Flax.Build/global.json"
PowerShell "(Get-Content Source/Tools/Flax.Build/Flax.Build.csproj).Replace('LatestMajor', 'Minor') | Set-Content Source/Tools/Flax.Build/Flax.Build.csproj"
.\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -dotnet=8 -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxEditor .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -dotnet=8 -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxEditor
# Game # Game
@@ -44,7 +48,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 8.0.x dotnet-version: 8.0.419
- name: Print .NET info - name: Print .NET info
run: | run: |
dotnet --info dotnet --info
@@ -55,4 +59,7 @@ jobs:
git lfs pull git lfs pull
- name: Build - name: Build
run: | run: |
PowerShell "(Get-Content global.json).Replace('latestMajor', 'minor') | Set-Content global.json"
PowerShell "(Get-Content Source/Tools/Flax.Build/global.json).Replace('latestMajor', 'minor') | Set-Content Source/Tools/Flax.Build/global.json"
PowerShell "(Get-Content Source/Tools/Flax.Build/Flax.Build.csproj).Replace('LatestMajor', 'Minor') | Set-Content Source/Tools/Flax.Build/Flax.Build.csproj"
.\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -dotnet=8 -arch=x64 -platform=Windows -configuration=Release -buildtargets=FlaxGame .\Development\Scripts\Windows\CallBuildTool.bat -build -log -printSDKs -dotnet=8 -arch=x64 -platform=Windows -configuration=Release -buildtargets=FlaxGame
+9 -2
View File
@@ -7,6 +7,7 @@ on:
env: env:
DOTNET_NOLOGO: true DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: false DOTNET_CLI_TELEMETRY_OPTOUT: false
DOTNET_ROLL_FORWARD: 'minor'
GIT_LFS_PULL_OPTIONS: '-c lfs.concurrenttransfers=1 -c lfs.transfer.maxretries=2 -c http.version="HTTP/1.1" -c lfs.activitytimeout=60' GIT_LFS_PULL_OPTIONS: '-c lfs.concurrenttransfers=1 -c lfs.transfer.maxretries=2 -c http.version="HTTP/1.1" -c lfs.activitytimeout=60'
jobs: jobs:
@@ -27,13 +28,16 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 8.0.x dotnet-version: 8.0.419
- name: Print .NET info - name: Print .NET info
run: | run: |
dotnet --info dotnet --info
dotnet workload --info dotnet workload --info
- name: Build - name: Build
run: | run: |
PowerShell "(Get-Content global.json).Replace('latestMajor', 'minor') | Set-Content global.json"
PowerShell "(Get-Content Source/Tools/Flax.Build/global.json).Replace('latestMajor', 'minor') | Set-Content Source/Tools/Flax.Build/global.json"
PowerShell "(Get-Content Source/Tools/Flax.Build/Flax.Build.csproj).Replace('LatestMajor', 'Minor') | Set-Content Source/Tools/Flax.Build/Flax.Build.csproj"
.\PackageEditor.bat -arch=x64 -platform=Windows -deployOutput=Output -dotnet=8 .\PackageEditor.bat -arch=x64 -platform=Windows -deployOutput=Output -dotnet=8
- name: Upload - name: Upload
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -60,13 +64,16 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 8.0.x dotnet-version: 8.0.419
- name: Print .NET info - name: Print .NET info
run: | run: |
dotnet --info dotnet --info
dotnet workload --info dotnet workload --info
- name: Build - name: Build
run: | run: |
PowerShell "(Get-Content global.json).Replace('latestMajor', 'minor') | Set-Content global.json"
PowerShell "(Get-Content Source/Tools/Flax.Build/global.json).Replace('latestMajor', 'minor') | Set-Content Source/Tools/Flax.Build/global.json"
PowerShell "(Get-Content Source/Tools/Flax.Build/Flax.Build.csproj).Replace('LatestMajor', 'Minor') | Set-Content Source/Tools/Flax.Build/Flax.Build.csproj"
.\PackagePlatforms.bat -arch=x64 -platform=Windows -deployOutput=Output -dotnet=8 .\PackagePlatforms.bat -arch=x64 -platform=Windows -deployOutput=Output -dotnet=8
- name: Upload - name: Upload
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
+5 -1
View File
@@ -4,6 +4,7 @@ on: [push, pull_request]
env: env:
DOTNET_NOLOGO: true DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: false DOTNET_CLI_TELEMETRY_OPTOUT: false
DOTNET_ROLL_FORWARD: 'minor'
jobs: jobs:
@@ -56,7 +57,7 @@ jobs:
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v3
with: with:
dotnet-version: 8.0.x dotnet-version: 8.0.419
- name: Print .NET info - name: Print .NET info
run: | run: |
dotnet --info dotnet --info
@@ -67,6 +68,9 @@ jobs:
git lfs pull git lfs pull
- name: Build - name: Build
run: | run: |
PowerShell "(Get-Content global.json).Replace('latestMajor', 'minor') | Set-Content global.json"
PowerShell "(Get-Content Source/Tools/Flax.Build/global.json).Replace('latestMajor', 'minor') | Set-Content Source/Tools/Flax.Build/global.json"
PowerShell "(Get-Content Source/Tools/Flax.Build/Flax.Build.csproj).Replace('LatestMajor', 'Minor') | Set-Content Source/Tools/Flax.Build/Flax.Build.csproj"
.\GenerateProjectFiles.bat -vs2022 -log -verbose -printSDKs -dotnet=8 .\GenerateProjectFiles.bat -vs2022 -log -verbose -printSDKs -dotnet=8
.\Development\Scripts\Windows\CallBuildTool.bat -build -log -dotnet=8 -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxTestsTarget .\Development\Scripts\Windows\CallBuildTool.bat -build -log -dotnet=8 -arch=x64 -platform=Windows -configuration=Development -buildtargets=FlaxTestsTarget
dotnet msbuild Source\Tools\Flax.Build.Tests\Flax.Build.Tests.csproj /m /t:Restore,Build /p:Configuration=Debug /p:Platform=AnyCPU /nologo dotnet msbuild Source\Tools\Flax.Build.Tests\Flax.Build.Tests.csproj /m /t:Restore,Build /p:Configuration=Debug /p:Platform=AnyCPU /nologo
+10 -2
View File
@@ -4,7 +4,7 @@
"Major": 1, "Major": 1,
"Minor": 12, "Minor": 12,
"Revision": 0, "Revision": 0,
"Build": 6908 "Build": 6909
}, },
"Company": "Flax", "Company": "Flax",
"Copyright": "Copyright (c) 2012-2026 Wojciech Figat. All rights reserved.", "Copyright": "Copyright (c) 2012-2026 Wojciech Figat. All rights reserved.",
@@ -14,6 +14,14 @@
"UseCSharp": true, "UseCSharp": true,
"UseLargeWorlds": false, "UseLargeWorlds": false,
"UseDotNet": true, "UseDotNet": true,
"UseSDL": true "Windows": {
"UseSDL": false,
},
"Mac": {
"UseSDL": false,
},
"Linux": {
"UseSDL": true,
},
} }
} }
@@ -175,15 +175,13 @@ namespace FlaxEditor.Content
} }
} }
bool isExpanded = isAnyChildVisible; if (!noFilter)
if (isExpanded)
{ {
Expand(true); bool isExpanded = isAnyChildVisible;
} if (isExpanded)
else Expand(true);
{ else
Collapse(true); Collapse(true);
} }
Visible = isThisVisible | isAnyChildVisible; Visible = isThisVisible | isAnyChildVisible;
+4 -1
View File
@@ -52,6 +52,7 @@ namespace FlaxEditor.CustomEditors
private readonly List<CustomEditor> _children = new List<CustomEditor>(); private readonly List<CustomEditor> _children = new List<CustomEditor>();
private ValueContainer _values; private ValueContainer _values;
private bool _isSetBlocked; private bool _isSetBlocked;
private bool _isRebuilding;
private bool _skipChildrenRefresh; private bool _skipChildrenRefresh;
private bool _hasValueDirty; private bool _hasValueDirty;
private bool _rebuildOnRefresh; private bool _rebuildOnRefresh;
@@ -178,7 +179,7 @@ namespace FlaxEditor.CustomEditors
public void RebuildLayout() public void RebuildLayout()
{ {
// Skip rebuilding during init // Skip rebuilding during init
if (CurrentCustomEditor == this) if (CurrentCustomEditor == this || _isRebuilding)
return; return;
// Special case for root objects to run normal layout build // Special case for root objects to run normal layout build
@@ -197,6 +198,7 @@ namespace FlaxEditor.CustomEditors
_parent?.RebuildLayout(); _parent?.RebuildLayout();
return; return;
} }
_isRebuilding = true;
var control = layout.ContainerControl; var control = layout.ContainerControl;
var parent = _parent; var parent = _parent;
var parentScrollV = (_presenter?.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1; var parentScrollV = (_presenter?.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1;
@@ -216,6 +218,7 @@ namespace FlaxEditor.CustomEditors
// Restore scroll value // Restore scroll value
if (parentScrollV > -1 && _presenter != null && _presenter.Panel.Parent is Panel panel && panel.VScrollBar != null) if (parentScrollV > -1 && _presenter != null && _presenter.Panel.Parent is Panel panel && panel.VScrollBar != null)
panel.VScrollBar.Value = parentScrollV; panel.VScrollBar.Value = parentScrollV;
_isRebuilding = false;
} }
/// <summary> /// <summary>
+1
View File
@@ -61,6 +61,7 @@ public class Editor : EditorModule
options.PrivateDependencies.Add("Renderer"); options.PrivateDependencies.Add("Renderer");
options.PrivateDependencies.Add("TextureTool"); options.PrivateDependencies.Add("TextureTool");
options.PrivateDependencies.Add("Particles"); options.PrivateDependencies.Add("Particles");
options.PrivateDependencies.Add("Terrain");
var platformToolsRoot = Path.Combine(FolderPath, "Cooker", "Platform"); var platformToolsRoot = Path.Combine(FolderPath, "Cooker", "Platform");
var platformToolsRootExternal = Path.Combine(Globals.EngineRoot, "Source", "Platforms"); var platformToolsRootExternal = Path.Combine(Globals.EngineRoot, "Source", "Platforms");
@@ -135,11 +135,7 @@ namespace FlaxEditor.GUI.Docking
settings.MaximumSize = Float2.Zero; // Unlimited size settings.MaximumSize = Float2.Zero; // Unlimited size
settings.Fullscreen = false; settings.Fullscreen = false;
settings.HasBorder = true; settings.HasBorder = true;
#if PLATFORM_SDL
settings.SupportsTransparency = true; settings.SupportsTransparency = true;
#else
settings.SupportsTransparency = false;
#endif
settings.ActivateWhenFirstShown = true; settings.ActivateWhenFirstShown = true;
settings.AllowInput = true; settings.AllowInput = true;
settings.AllowMinimize = true; settings.AllowMinimize = true;
@@ -211,6 +207,7 @@ namespace FlaxEditor.GUI.Docking
{ {
if (ChildPanelsCount > 0) if (ChildPanelsCount > 0)
return; return;
// Close window // Close window
_window?.Close(); _window?.Close();
} }
@@ -269,8 +269,9 @@ namespace FlaxEditor.GUI.Docking
if (_toDock == null) if (_toDock == null)
return; return;
if (_toDock.RootWindow.Window != _dragSourceWindow) var window = _toDock.RootWindow?.Window;
_toDock.RootWindow.Window.MouseUp -= OnMouseUp; if (window != null && window != _dragSourceWindow)
window.MouseUp -= OnMouseUp;
_dockHintDown?.Parent.RemoveChild(_dockHintDown); _dockHintDown?.Parent.RemoveChild(_dockHintDown);
_dockHintUp?.Parent.RemoveChild(_dockHintUp); _dockHintUp?.Parent.RemoveChild(_dockHintUp);
@@ -327,10 +328,10 @@ namespace FlaxEditor.GUI.Docking
_toDock?.RootWindow.Window.BringToFront(); _toDock?.RootWindow.Window.BringToFront();
//_toDock?.RootWindow.Window.Focus(); //_toDock?.RootWindow.Window.Focus();
#if PLATFORM_SDL
// Make the dragged window transparent when dock hints are visible // Make the dragged window transparent when dock hints are visible
_toMove.Window.Window.Opacity = _toDock == null ? 1.0f : DragWindowOpacity; _toMove.Window.Window.Opacity = _toDock == null ? 1.0f : DragWindowOpacity;
#else
#if !PLATFORM_SDL
// Bring the drop source always to the top // Bring the drop source always to the top
if (_dragSourceWindow != null) if (_dragSourceWindow != null)
_dragSourceWindow.BringToFront(); _dragSourceWindow.BringToFront();
+267
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;
}
}
+4
View File
@@ -710,6 +710,10 @@ namespace FlaxEditor.Options
[EditorDisplay("Node Editors"), EditorOrder(4580)] [EditorDisplay("Node Editors"), EditorOrder(4580)]
public InputBinding NodesDistributeVertical = new InputBinding(KeyboardKeys.A, KeyboardKeys.Alt); 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 #endregion
} }
} }
@@ -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;
}
}
}
@@ -324,13 +324,12 @@ namespace FlaxEditor.SceneGraph.GUI
isExpanded = Editor.Instance.ProjectCache.IsExpandedActor(ref id); isExpanded = Editor.Instance.ProjectCache.IsExpandedActor(ref id);
} }
if (isExpanded) if (!noFilter)
{ {
Expand(true); if (isExpanded)
} Expand(true);
else else
{ Collapse(true);
Collapse(true);
} }
Visible = isThisVisible | isAnyChildVisible; Visible = isThisVisible | isAnyChildVisible;
@@ -74,6 +74,7 @@ namespace FlaxEditor.SceneGraph
CustomNodesTypes.Add(typeof(NavMesh), typeof(ActorNode)); CustomNodesTypes.Add(typeof(NavMesh), typeof(ActorNode));
CustomNodesTypes.Add(typeof(SpriteRender), typeof(SpriteRenderNode)); CustomNodesTypes.Add(typeof(SpriteRender), typeof(SpriteRenderNode));
CustomNodesTypes.Add(typeof(Joint), typeof(JointNode)); CustomNodesTypes.Add(typeof(Joint), typeof(JointNode));
CustomNodesTypes.Add(typeof(RigidBody), typeof(RigidBodyNode));
} }
/// <summary> /// <summary>
@@ -52,6 +52,7 @@ namespace FlaxEditor.Surface.Archetypes
}, },
new NodeArchetype new NodeArchetype
{ {
// [Deprecated]
TypeID = 3, TypeID = 3,
Title = "Pack Material Layer", Title = "Pack Material Layer",
Description = "Pack material properties", Description = "Pack material properties",
@@ -75,6 +76,7 @@ namespace FlaxEditor.Surface.Archetypes
}, },
new NodeArchetype new NodeArchetype
{ {
// [Deprecated]
TypeID = 4, TypeID = 4,
Title = "Unpack Material Layer", Title = "Unpack Material Layer",
Description = "Unpack material properties", Description = "Unpack material properties",
@@ -120,6 +122,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 6, TypeID = 6,
Title = "Pack Material Layer", Title = "Pack Material Layer",
Description = "Pack material properties", Description = "Pack material properties",
AlternativeTitles = new[] { "Make Material Layer", "Construct Material Layer", "Compose Material Layer" },
Flags = NodeFlags.MaterialGraph, Flags = NodeFlags.MaterialGraph,
Size = new Float2(200, 280), Size = new Float2(200, 280),
Elements = new[] Elements = new[]
@@ -146,6 +149,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 7, TypeID = 7,
Title = "Unpack Material Layer", Title = "Unpack Material Layer",
Description = "Unpack material properties", Description = "Unpack material properties",
AlternativeTitles = new[] { "Break Material Layer", "Deconstruct Material Layer", "Decompose Material Layer", "Split Material Layer" },
Flags = NodeFlags.MaterialGraph, Flags = NodeFlags.MaterialGraph,
Size = new Float2(210, 280), Size = new Float2(210, 280),
Elements = new[] Elements = new[]
@@ -342,6 +342,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 20, TypeID = 20,
Title = "Pack Float2", Title = "Pack Float2",
Description = "Pack components to Float2", Description = "Pack components to Float2",
AlternativeTitles = new[] { "Make Float2", "Construct Float2", "Compose Float2" },
Flags = NodeFlags.AllGraphs, Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 40), Size = new Float2(150, 40),
DefaultValues = new object[] DefaultValues = new object[]
@@ -361,6 +362,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 21, TypeID = 21,
Title = "Pack Float3", Title = "Pack Float3",
Description = "Pack components to Float3", Description = "Pack components to Float3",
AlternativeTitles = new[] { "Make Float3", "Construct Float3", "Compose Float3" },
Flags = NodeFlags.AllGraphs, Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 60), Size = new Float2(150, 60),
DefaultValues = new object[] DefaultValues = new object[]
@@ -382,6 +384,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 22, TypeID = 22,
Title = "Pack Float4", Title = "Pack Float4",
Description = "Pack components to Float4", Description = "Pack components to Float4",
AlternativeTitles = new[] { "Make Float4", "Construct Float4", "Compose Float4" },
Flags = NodeFlags.AllGraphs, Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 80), Size = new Float2(150, 80),
DefaultValues = new object[] DefaultValues = new object[]
@@ -405,6 +408,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 23, TypeID = 23,
Title = "Pack Rotation", Title = "Pack Rotation",
Description = "Pack components to Rotation", Description = "Pack components to Rotation",
AlternativeTitles = new[] { "Make Rotation", "Construct Rotation", "Compose Rotation" },
Flags = NodeFlags.AllGraphs, Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 60), Size = new Float2(150, 60),
DefaultValues = new object[] DefaultValues = new object[]
@@ -426,6 +430,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 24, TypeID = 24,
Title = "Pack Transform", Title = "Pack Transform",
Description = "Pack components to Transform", Description = "Pack components to Transform",
AlternativeTitles = new[] { "Make Transform", "Construct Transform", "Compose Transform" },
Flags = NodeFlags.AllGraphs, Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 80), Size = new Float2(150, 80),
Elements = new[] Elements = new[]
@@ -441,6 +446,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 25, TypeID = 25,
Title = "Pack Box", Title = "Pack Box",
Description = "Pack components to BoundingBox", Description = "Pack components to BoundingBox",
AlternativeTitles = new[] { "Make Box", "Construct Box", "Compose Box" },
Flags = NodeFlags.AllGraphs, Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 40), Size = new Float2(150, 40),
Elements = new[] Elements = new[]
@@ -454,6 +460,7 @@ namespace FlaxEditor.Surface.Archetypes
{ {
TypeID = 26, TypeID = 26,
Title = "Pack Structure", Title = "Pack Structure",
AlternativeTitles = new[] { "Make Structure", "Construct Structure", "Compose Structure" },
Create = (id, context, arch, groupArch) => new PackStructureNode(id, context, arch, groupArch), Create = (id, context, arch, groupArch) => new PackStructureNode(id, context, arch, groupArch),
IsInputCompatible = PackStructureNode.IsInputCompatible, IsInputCompatible = PackStructureNode.IsInputCompatible,
IsOutputCompatible = PackStructureNode.IsOutputCompatible, IsOutputCompatible = PackStructureNode.IsOutputCompatible,
@@ -479,6 +486,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 30, TypeID = 30,
Title = "Unpack Float2", Title = "Unpack Float2",
Description = "Unpack components from Float2", Description = "Unpack components from Float2",
AlternativeTitles = new[] { "Break Float2", "Deconstruct Float2", "Decompose Float2", "Split Float2" },
Flags = NodeFlags.AllGraphs, Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 40), Size = new Float2(150, 40),
Elements = new[] Elements = new[]
@@ -493,6 +501,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 31, TypeID = 31,
Title = "Unpack Float3", Title = "Unpack Float3",
Description = "Unpack components from Float3", Description = "Unpack components from Float3",
AlternativeTitles = new[] { "Break Float3", "Deconstruct Float3", "Decompose Float3", "Split Float3" },
Flags = NodeFlags.AllGraphs, Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 60), Size = new Float2(150, 60),
Elements = new[] Elements = new[]
@@ -508,6 +517,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 32, TypeID = 32,
Title = "Unpack Float4", Title = "Unpack Float4",
Description = "Unpack components from Float4", Description = "Unpack components from Float4",
AlternativeTitles = new[] { "Break Float4", "Deconstruct Float4", "Decompose Float4", "Split Float4" },
Flags = NodeFlags.AllGraphs, Flags = NodeFlags.AllGraphs,
Size = new Float2(150, 80), Size = new Float2(150, 80),
Elements = new[] Elements = new[]
@@ -524,6 +534,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 33, TypeID = 33,
Title = "Unpack Rotation", Title = "Unpack Rotation",
Description = "Unpack components from Rotation", Description = "Unpack components from Rotation",
AlternativeTitles = new[] { "Break Rotation", "Deconstruct Rotation", "Decompose Rotation", "Split Rotation" },
Flags = NodeFlags.AllGraphs, Flags = NodeFlags.AllGraphs,
Size = new Float2(170, 60), Size = new Float2(170, 60),
Elements = new[] Elements = new[]
@@ -539,6 +550,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 34, TypeID = 34,
Title = "Unpack Transform", Title = "Unpack Transform",
Description = "Unpack components from Transform", Description = "Unpack components from Transform",
AlternativeTitles = new[] { "Break Transform", "Deconstruct Transform", "Decompose Transform", "Split Transform" },
Flags = NodeFlags.AllGraphs, Flags = NodeFlags.AllGraphs,
Size = new Float2(170, 60), Size = new Float2(170, 60),
Elements = new[] Elements = new[]
@@ -554,6 +566,7 @@ namespace FlaxEditor.Surface.Archetypes
TypeID = 35, TypeID = 35,
Title = "Unpack Box", Title = "Unpack Box",
Description = "Unpack components from BoundingBox", Description = "Unpack components from BoundingBox",
AlternativeTitles = new[] { "Break BoundingBox", "Deconstruct BoundingBox", "Decompose BoundingBox", "Split BoundingBox" },
Flags = NodeFlags.AllGraphs, Flags = NodeFlags.AllGraphs,
Size = new Float2(170, 40), Size = new Float2(170, 40),
Elements = new[] Elements = new[]
@@ -572,6 +585,7 @@ namespace FlaxEditor.Surface.Archetypes
IsOutputCompatible = UnpackStructureNode.IsOutputCompatible, IsOutputCompatible = UnpackStructureNode.IsOutputCompatible,
GetInputOutputDescription = UnpackStructureNode.GetInputOutputDescription, GetInputOutputDescription = UnpackStructureNode.GetInputOutputDescription,
Description = "Breaks the structure data to allow extracting components from it.", 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, Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI,
Size = new Float2(180, 20), Size = new Float2(180, 20),
DefaultValues = new object[] DefaultValues = new object[]
@@ -585,7 +585,7 @@ namespace FlaxEditor.Surface.ContextMenu
private void UpdateFilters() private void UpdateFilters()
{ {
if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBoxes[0] == null) if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBoxes.Count == 0)
{ {
ResetView(); ResetView();
Profiler.EndEvent(); Profiler.EndEvent();
+4 -8
View File
@@ -34,11 +34,6 @@ namespace FlaxEditor.Surface.Elements
/// </summary> /// </summary>
public const float DefaultConnectionOffset = 24f; public const float DefaultConnectionOffset = 24f;
/// <summary>
/// Distance for the mouse to be considered above the connection
/// </summary>
public float MouseOverConnectionDistance => 100f / Surface.ViewScale;
/// <inheritdoc /> /// <inheritdoc />
public OutputBox(SurfaceNode parentNode, NodeElementArchetype archetype) public OutputBox(SurfaceNode parentNode, NodeElementArchetype archetype)
: base(parentNode, archetype, archetype.Position + new Float2(parentNode.Archetype.Size.X, 0)) : base(parentNode, archetype, archetype.Position + new Float2(parentNode.Archetype.Size.X, 0))
@@ -109,12 +104,13 @@ namespace FlaxEditor.Surface.Elements
/// </summary> /// </summary>
/// <param name="targetBox">The other box.</param> /// <param name="targetBox">The other box.</param>
/// <param name="mousePosition">The mouse position</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)); float connectionOffset = Mathf.Max(0f, DefaultConnectionOffset * (1 - Editor.Instance.Options.Options.Interface.ConnectionCurvature));
Float2 start = new Float2(ConnectionOrigin.X + connectionOffset, ConnectionOrigin.Y); Float2 start = new Float2(ConnectionOrigin.X + connectionOffset, ConnectionOrigin.Y);
Float2 end = new Float2(targetBox.ConnectionOrigin.X - connectionOffset, targetBox.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> /// <summary>
@@ -182,7 +178,7 @@ namespace FlaxEditor.Surface.Elements
{ {
// Draw all the connections // Draw all the connections
var style = Surface.Style; var style = Surface.Style;
var mouseOverDistance = MouseOverConnectionDistance; var mouseOverDistance = Surface.MouseOverConnectionDistance;
var startPos = ConnectionOrigin; var startPos = ConnectionOrigin;
var startHighlight = ConnectionsHighlightIntensity; var startHighlight = ConnectionsHighlightIntensity;
for (int i = 0; i < Connections.Count; i++) for (int i = 0; i < Connections.Count; i++)
+5 -5
View File
@@ -574,13 +574,13 @@ namespace FlaxEditor.Surface
var showSearch = () => editor.ContentFinding.ShowSearch(window); var showSearch = () => editor.ContentFinding.ShowSearch(window);
// Toolstrip // 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(); toolStrip.AddSeparator();
undoButton = toolStrip.AddButton(editor.Icons.Undo64, undo.PerformUndo).LinkTooltip("Undo", ref inputOptions.Undo); 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); redoButton = toolStrip.AddButton(editor.Icons.Redo64, undo.PerformRedo).LinkTooltip("Redo.", ref inputOptions.Redo);
toolStrip.AddSeparator(); toolStrip.AddSeparator();
toolStrip.AddButton(editor.Icons.Search64, showSearch).LinkTooltip("Open content search tool", ref inputOptions.Search); 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.CenterView64, surface.ShowWholeGraph).LinkTooltip("Show whole graph.");
var gridSnapButton = toolStrip.AddButton(editor.Icons.Grid32, surface.ToggleGridSnapping); var gridSnapButton = toolStrip.AddButton(editor.Icons.Grid32, surface.ToggleGridSnapping);
gridSnapButton.LinkTooltip("Toggle grid snapping for nodes."); gridSnapButton.LinkTooltip("Toggle grid snapping for nodes.");
gridSnapButton.AutoCheck = true; gridSnapButton.AutoCheck = true;
@@ -410,8 +410,11 @@ namespace FlaxEditor.Surface
} }
menu.AddSeparator(); menu.AddSeparator();
_cmFormatNodesMenu = menu.AddChildMenu("Format node(s)"); bool allNodesNoMove = SelectedNodes.All(n => n.Archetype.Flags.HasFlag(NodeFlags.NoMove));
_cmFormatNodesMenu.Enabled = CanEdit && HasNodesSelection; 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("Auto format", Editor.Instance.Options.Options.Input.NodesAutoFormat, () => { FormatGraph(SelectedNodes); });
_cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Straighten connections", Editor.Instance.Options.Options.Input.NodesStraightenConnections, () => { StraightenGraphConnections(SelectedNodes); }); _cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Straighten connections", Editor.Instance.Options.Options.Input.NodesStraightenConnections, () => { StraightenGraphConnections(SelectedNodes); });
@@ -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> /// <summary>
/// Draws the contents of the surface (nodes, connections, comments, etc.). /// Draws the contents of the surface (nodes, connections, comments, etc.).
/// </summary> /// </summary>
@@ -260,6 +298,9 @@ namespace FlaxEditor.Surface
DrawContents(); DrawContents();
if (_isLazyConnecting)
DrawLazyConnect();
//Render2D.DrawText(style.FontTitle, string.Format("Scale: {0}", _rootControl.Scale), rect, Enabled ? Color.Red : Color.Black); //Render2D.DrawText(style.FontTitle, string.Format("Scale: {0}", _rootControl.Scale), rect, Enabled ? Color.Red : Color.Black);
// Draw border // Draw border
@@ -39,6 +39,8 @@ namespace FlaxEditor.Surface
if (nodes.Count <= 1) if (nodes.Count <= 1)
return; return;
List<MoveNodesAction> undoActions = new List<MoveNodesAction>();
var nodesToVisit = new HashSet<SurfaceNode>(nodes); var nodesToVisit = new HashSet<SurfaceNode>(nodes);
// While we haven't formatted every node // 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> /// <summary>
/// Formats a graph where all nodes are connected. /// Formats a graph where all nodes are connected.
/// </summary> /// </summary>
/// <param name="nodes">List of connected nodes.</param> /// <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) if (nodes.Count <= 1)
return; return undoActions;
var boundingBox = GetNodesBounds(nodes); var boundingBox = GetNodesBounds(nodes);
@@ -140,7 +147,6 @@ namespace FlaxEditor.Surface
} }
// Set the node positions // Set the node positions
var undoActions = new List<MoveNodesAction>();
var topRightPosition = endNodes[0].Location; var topRightPosition = endNodes[0].Location;
for (int i = 0; i < nodes.Count; i++) for (int i = 0; i < nodes.Count; i++)
{ {
@@ -155,16 +161,18 @@ namespace FlaxEditor.Surface
} }
} }
MarkAsEdited(false); return undoActions;
Undo?.AddAction(new MultiUndoAction(undoActions, "Format nodes"));
} }
/// <summary> /// <summary>
/// Straightens every connection between nodes in <paramref name="nodes"/>. /// Straightens every connection between nodes in <paramref name="nodes"/>.
/// </summary> /// </summary>
/// <param name="nodes">List of nodes.</param> /// <param name="nodes">List of nodes.</param>
/// <returns>List of undo actions.</returns>
public void StraightenGraphConnections(List<SurfaceNode> nodes) public void StraightenGraphConnections(List<SurfaceNode> nodes)
{ {
nodes = nodes.Where(n => !n.Archetype.Flags.HasFlag(NodeFlags.NoMove)).ToList();
if (nodes.Count <= 1) if (nodes.Count <= 1)
return; return;
@@ -351,7 +359,9 @@ namespace FlaxEditor.Surface
/// <param name="alignmentType">Alignemnt type.</param> /// <param name="alignmentType">Alignemnt type.</param>
public void AlignNodes(List<SurfaceNode> nodes, NodeAlignmentType alignmentType) 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; return;
var undoActions = new List<MoveNodesAction>(); 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> /// <param name="vertically">If false will be done horizontally, if true will be done vertically.</param>
public void DistributeNodes(List<SurfaceNode> nodes, bool vertically) public void DistributeNodes(List<SurfaceNode> nodes, bool vertically)
{ {
nodes = nodes.Where(n => !n.Archetype.Flags.HasFlag(NodeFlags.NoMove)).ToList();
if(nodes.Count <= 1) if(nodes.Count <= 1)
return; return;
+184 -9
View File
@@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using static FlaxEditor.Surface.Archetypes.Particles;
using FlaxEditor.Options; using FlaxEditor.Options;
using FlaxEditor.Surface.Elements; using FlaxEditor.Surface.Elements;
using FlaxEditor.Surface.Undo; using FlaxEditor.Surface.Undo;
@@ -23,12 +24,26 @@ namespace FlaxEditor.Surface
/// </summary> /// </summary>
public bool PanWithMiddleMouse = false; 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 string _currentInputText = string.Empty;
private Float2 _movingNodesDelta; private Float2 _movingNodesDelta;
private Float2 _gridRoundingDelta; private Float2 _gridRoundingDelta;
private HashSet<SurfaceNode> _movingNodes; private HashSet<SurfaceNode> _movingNodes;
private HashSet<SurfaceNode> _temporarySelectedNodes; private HashSet<SurfaceNode> _temporarySelectedNodes;
private readonly Stack<InputBracket> _inputBrackets = new Stack<InputBracket>(); private readonly Stack<InputBracket> _inputBrackets = new Stack<InputBracket>();
private bool _isLazyConnecting;
private SurfaceNode _lazyConnectStartNode;
private SurfaceNode _lazyConnectEndNode;
private InputBinding _focusSelectedNodeBinding;
private class InputBracket private class InputBracket
{ {
@@ -250,8 +265,13 @@ namespace FlaxEditor.Surface
// Cache mouse location // Cache mouse location
_mousePos = 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 // Moving around surface with mouse
if (_rightMouseDown) if (_rightMouseDown && !_isLazyConnecting)
{ {
// Calculate delta // Calculate delta
var delta = location - _rightMouseDownPos; var delta = location - _rightMouseDownPos;
@@ -321,6 +341,33 @@ namespace FlaxEditor.Surface
foreach (var node in _movingNodes) 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) if (gridSnap)
{ {
Float2 unroundedLocation = node.Location; Float2 unroundedLocation = node.Location;
@@ -420,7 +467,7 @@ namespace FlaxEditor.Surface
if (!handled && CanEdit && CanUseNodeType(7, 29)) if (!handled && CanEdit && CanUseNodeType(7, 29))
{ {
var mousePos = _rootControl.PointFromParent(ref _mousePos); 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) if (Undo != null)
{ {
@@ -515,11 +562,17 @@ namespace FlaxEditor.Surface
_middleMouseDownPos = location; _middleMouseDownPos = location;
} }
if (root.GetKey(KeyboardKeys.Alt) && button == MouseButton.Right)
_isLazyConnecting = true;
// Check if any node is under the mouse // Check if any node is under the mouse
SurfaceControl controlUnderMouse = GetControlUnderMouse(); SurfaceControl controlUnderMouse = GetControlUnderMouse();
var cLocation = _rootControl.PointFromParent(ref location); var cLocation = _rootControl.PointFromParent(ref location);
if (controlUnderMouse != null) 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 // Check if mouse is over header and user is pressing mouse left button
if (_leftMouseDown && controlUnderMouse.CanSelect(ref cLocation)) if (_leftMouseDown && controlUnderMouse.CanSelect(ref cLocation))
{ {
@@ -554,6 +607,9 @@ namespace FlaxEditor.Surface
} }
else else
{ {
if (_isLazyConnecting && Nodes.Count > 0)
_lazyConnectStartNode = GetClosestNodeAtLocation(location);
// Cache flags and state // Cache flags and state
if (_leftMouseDown) if (_leftMouseDown)
{ {
@@ -602,8 +658,71 @@ namespace FlaxEditor.Surface
{ {
if (_movingNodes != null && _movingNodes.Count > 0) 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) 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(); _movingNodes.Clear();
} }
_movingNodesDelta = Float2.Zero; _movingNodesDelta = Float2.Zero;
@@ -630,12 +749,36 @@ namespace FlaxEditor.Surface
{ {
// Check if any control is under the mouse // Check if any control is under the mouse
_cmStartPos = location; _cmStartPos = location;
if (controlUnderMouse == null) if (controlUnderMouse == null && !_isLazyConnecting)
{ {
showPrimaryMenu = true; showPrimaryMenu = true;
} }
} }
_mouseMoveAmount = 0; _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) if (_middleMouseDown && button == MouseButton.Middle)
{ {
@@ -651,7 +794,7 @@ namespace FlaxEditor.Surface
{ {
// Surface was not moved with MMB so try to remove connection underneath // Surface was not moved with MMB so try to remove connection underneath
var mousePos = _rootControl.PointFromParent(ref location); 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); var action = new EditNodeConnections(inputBox.ParentNode.Context, inputBox.ParentNode);
inputBox.BreakConnection(outputBox); inputBox.BreakConnection(outputBox);
@@ -704,13 +847,21 @@ namespace FlaxEditor.Surface
private void MoveSelectedNodes(Float2 delta) private void MoveSelectedNodes(Float2 delta)
{ {
// TODO: undo List<MoveNodesAction> undoActions = new List<MoveNodesAction>();
delta /= _targetScale; delta /= _targetScale;
OnGetNodesToMove(); OnGetNodesToMove();
foreach (var node in _movingNodes) foreach (var node in _movingNodes)
{
node.Location += delta; node.Location += delta;
if (Undo != null)
undoActions.Add(new MoveNodesAction(Context, new[] { node.ID }, delta));
}
_isMovingSelection = false; _isMovingSelection = false;
MarkAsEdited(false); MarkAsEdited(false);
if (undoActions.Count > 0)
Undo?.AddAction(new MultiUndoAction(undoActions, "Moved "));
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -837,6 +988,29 @@ namespace FlaxEditor.Surface
return false; 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() private void ResetInput()
{ {
InputText = ""; InputText = "";
@@ -845,7 +1019,8 @@ namespace FlaxEditor.Surface
private void CurrentInputTextChanged(string currentInputText) 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; return;
if (IsPrimaryMenuOpened || !CanEdit) if (IsPrimaryMenuOpened || !CanEdit)
{ {
@@ -1025,7 +1200,7 @@ namespace FlaxEditor.Surface
return new Float2(xLocation, yLocation); 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++) for (int i = 0; i < Nodes.Count; i++)
{ {
@@ -1035,7 +1210,7 @@ namespace FlaxEditor.Surface
{ {
for (int k = 0; k < ob.Connections.Count; k++) 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; outputBox = ob;
inputBox = ob.Connections[k] as InputBox; inputBox = ob.Connections[k] as InputBox;
+43 -2
View File
@@ -423,8 +423,9 @@ namespace FlaxEditor.Surface
new InputActionsContainer.Binding(options => options.NodesAlignLeft, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Left); }), 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.NodesAlignCenter, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Center); }),
new InputActionsContainer.Binding(options => options.NodesAlignRight, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Right); }), new InputActionsContainer.Binding(options => options.NodesAlignRight, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Right); }),
new InputActionsContainer.Binding(options => options.NodesDistributeHorizontal, () => { DistributeNodes(SelectedNodes, false); }), new InputActionsContainer.Binding(options => options.NodesDistributeHorizontal, () => { DistributeNodes(SelectedNodes, false); }),
new InputActionsContainer.Binding(options => options.NodesDistributeVertical, () => { DistributeNodes(SelectedNodes, true); }), new InputActionsContainer.Binding(options => options.NodesDistributeVertical, () => { DistributeNodes(SelectedNodes, true); }),
new InputActionsContainer.Binding(options => options.FocusSelectedNodes, () => { FocusSelectionOrWholeGraph(); }),
}); });
Context.ControlSpawned += OnSurfaceControlSpawned; Context.ControlSpawned += OnSurfaceControlSpawned;
@@ -436,7 +437,10 @@ namespace FlaxEditor.Surface
DragHandlers.Add(_dragAssets = new DragAssets<DragDropEventArgs>(ValidateDragItem)); DragHandlers.Add(_dragAssets = new DragAssets<DragDropEventArgs>(ValidateDragItem));
DragHandlers.Add(_dragParameters = new DragNames<DragDropEventArgs>(SurfaceParameter.DragPrefix, ValidateDragParameter)); DragHandlers.Add(_dragParameters = new DragNames<DragDropEventArgs>(SurfaceParameter.DragPrefix, ValidateDragParameter));
OnEditorOptionsChanged(Editor.Instance.Options.Options);
ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin;
Editor.Instance.Options.OptionsChanged += OnEditorOptionsChanged;
} }
private void OnScriptsReloadBegin() private void OnScriptsReloadBegin()
@@ -446,6 +450,11 @@ namespace FlaxEditor.Surface
_cmPrimaryMenu = null; _cmPrimaryMenu = null;
} }
private void OnEditorOptionsChanged(EditorOptions options)
{
_focusSelectedNodeBinding = options.Input.FocusSelectedNodes;
}
/// <summary> /// <summary>
/// Gets the display name of the connection type used in the surface. /// Gets the display name of the connection type used in the surface.
/// </summary> /// </summary>
@@ -648,6 +657,37 @@ namespace FlaxEditor.Surface
ViewCenterPosition = areaRect.Center; 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> /// <summary>
/// Shows the given surface node by changing the view scale and the position and focuses the node. /// Shows the given surface node by changing the view scale and the position and focuses the node.
/// </summary> /// </summary>
@@ -1071,6 +1111,7 @@ namespace FlaxEditor.Surface
_cmPrimaryMenu?.Dispose(); _cmPrimaryMenu?.Dispose();
ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin;
Editor.Instance.Options.OptionsChanged += OnEditorOptionsChanged;
base.OnDestroy(); base.OnDestroy();
} }
@@ -89,7 +89,7 @@ namespace FlaxEditor.Tools.Terrain
if (!terrain.HasPatch(ref patchCoord) && _planeModel) if (!terrain.HasPatch(ref patchCoord) && _planeModel)
{ {
var planeSize = 100.0f; 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 world = Matrix.RotationX(-Mathf.PiOverTwo) *
Matrix.Scaling(patchSize / planeSize) * Matrix.Scaling(patchSize / planeSize) *
Matrix.Translation(patchSize * (0.5f + patchCoord.X), 0, patchSize * (0.5f + patchCoord.Y)) * Matrix.Translation(patchSize * (0.5f + patchCoord.X), 0, patchSize * (0.5f + patchCoord.Y)) *
+2 -2
View File
@@ -69,9 +69,9 @@ namespace FlaxEditor.Tools.Terrain.Paint
var splatmapIndex = ActiveSplatmapIndex; var splatmapIndex = ActiveSplatmapIndex;
var splatmapIndexOther = (splatmapIndex + 1) % 2; var splatmapIndexOther = (splatmapIndex + 1) % 2;
var chunkSize = terrain.ChunkSize; var chunkSize = terrain.ChunkSize;
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1; var heightmapSize = terrain.HeightmapSize;
var heightmapLength = heightmapSize * 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 tempBuffer = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes, splatmapIndex).ToPointer();
var tempBufferOther = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes, (splatmapIndex + 1) % 2).ToPointer(); var tempBufferOther = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes, (splatmapIndex + 1) % 2).ToPointer();
var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex; var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex;
+2 -2
View File
@@ -70,9 +70,9 @@ namespace FlaxEditor.Tools.Terrain.Sculpt
// Prepare // Prepare
var chunkSize = terrain.ChunkSize; var chunkSize = terrain.ChunkSize;
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1; var heightmapSize = terrain.HeightmapSize;
var heightmapLength = heightmapSize * 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 tempBuffer = (float*)gizmo.GetHeightmapTempBuffer(heightmapLength * sizeof(float)).ToPointer();
var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex; var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex;
+12 -3
View File
@@ -382,7 +382,8 @@ bool TerrainTools::ExportTerrain(Terrain* terrain, String outputFolder)
const Int2 heightmapSize = size * Terrain::ChunksCountEdge * terrain->GetChunkSize() + 1; const Int2 heightmapSize = size * Terrain::ChunksCountEdge * terrain->GetChunkSize() + 1;
Array<float> heightmap; Array<float> heightmap;
heightmap.Resize(heightmapSize.X * heightmapSize.Y); 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 // Fill heightmap with data from all patches
const int32 rowSize = terrain->GetChunkSize() * Terrain::ChunksCountEdge + 1; 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 Int2 pos(patch->GetX() - start.X, patch->GetZ() - start.Y);
const float* src = patch->GetHeightmapData(); const float* src = patch->GetHeightmapData();
float* dst = heightmap.Get() + pos.X * (rowSize - 1) + pos.Y * heightmapSize.X * (rowSize - 1); float* dst = heightmap.Get() + pos.X * (rowSize - 1) + pos.Y * heightmapSize.X * (rowSize - 1);
for (int32 row = 0; row < rowSize; row++) if (src)
Platform::MemoryCopy(dst + row * heightmapSize.X, src + row * rowSize, rowSize * sizeof(float)); {
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 // Interpolate to 16-bit int
@@ -85,8 +85,7 @@ namespace FlaxEditor.Tools.Terrain.Undo
{ {
_terrain = terrain.ID; _terrain = terrain.ID;
_patches = new List<PatchData>(4); _patches = new List<PatchData>(4);
var chunkSize = terrain.ChunkSize; var heightmapSize = terrain.HeightmapSize;
var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1;
_heightmapLength = heightmapSize * heightmapSize; _heightmapLength = heightmapSize * heightmapSize;
_heightmapDataSize = _heightmapLength * stride; _heightmapDataSize = _heightmapLength * stride;
+2 -2
View File
@@ -1229,7 +1229,7 @@ namespace FlaxEditor.Viewport
/// Orients the viewport. /// Orients the viewport.
/// </summary> /// </summary>
/// <param name="orientation">The orientation.</param> /// <param name="orientation">The orientation.</param>
protected void OrientViewport(Quaternion orientation) public void OrientViewport(Quaternion orientation)
{ {
OrientViewport(ref orientation); OrientViewport(ref orientation);
} }
@@ -1238,7 +1238,7 @@ namespace FlaxEditor.Viewport
/// Orients the viewport. /// Orients the viewport.
/// </summary> /// </summary>
/// <param name="orientation">The orientation.</param> /// <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) if (ViewportCamera is FPSCamera fpsCamera)
{ {
@@ -108,13 +108,14 @@ namespace FlaxEditor.Viewport
private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32); private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32);
private EditorSpritesRenderer _editorSpritesRenderer; private EditorSpritesRenderer _editorSpritesRenderer;
private ViewportRubberBandSelector _rubberBandSelector; private ViewportRubberBandSelector _rubberBandSelector;
private DirectionGizmo _directionGizmo;
private bool _gameViewActive; private bool _gameViewActive;
private ViewFlags _preGameViewFlags; private ViewFlags _preGameViewFlags;
private ViewMode _preGameViewViewMode; private ViewMode _preGameViewViewMode;
private bool _gameViewWasGridShown; private bool _gameViewWasGridShown;
private bool _gameViewWasFpsCounterShown; private bool _gameViewWasFpsCounterShown;
private bool _gameViewWasNagivationShown; private bool _gameViewWasNavigationShown;
/// <summary> /// <summary>
/// Drag and drop handlers /// Drag and drop handlers
@@ -225,6 +226,12 @@ namespace FlaxEditor.Viewport
// Add rubber band selector // Add rubber band selector
_rubberBandSelector = new ViewportRubberBandSelector(this); _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 // Add grid
Grid = new GridGizmo(this); Grid = new GridGizmo(this);
@@ -244,6 +251,12 @@ namespace FlaxEditor.Viewport
_showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", inputOptions.ToggleNavMeshVisibility, () => ShowNavigation = !ShowNavigation); _showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", inputOptions.ToggleNavMeshVisibility, () => ShowNavigation = !ShowNavigation);
_showNavigationButton.CloseMenuOnClick = false; _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 // Game View
ViewWidgetButtonMenu.AddSeparator(); ViewWidgetButtonMenu.AddSeparator();
_toggleGameViewButton = ViewWidgetButtonMenu.AddButton("Game View", inputOptions.ToggleGameView, ToggleGameView); _toggleGameViewButton = ViewWidgetButtonMenu.AddButton("Game View", inputOptions.ToggleGameView, ToggleGameView);
@@ -514,14 +527,14 @@ namespace FlaxEditor.Viewport
_preGameViewViewMode = Task.ViewMode; _preGameViewViewMode = Task.ViewMode;
_gameViewWasGridShown = Grid.Enabled; _gameViewWasGridShown = Grid.Enabled;
_gameViewWasFpsCounterShown = ShowFpsCounter; _gameViewWasFpsCounterShown = ShowFpsCounter;
_gameViewWasNagivationShown = ShowNavigation; _gameViewWasNavigationShown = ShowNavigation;
} }
// Set flags & values // Set flags & values
Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.DefaultGame; Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.DefaultGame;
Task.ViewMode = _gameViewActive ? _preGameViewViewMode : ViewMode.Default; Task.ViewMode = _gameViewActive ? _preGameViewViewMode : ViewMode.Default;
ShowFpsCounter = _gameViewActive ? _gameViewWasFpsCounterShown : false; ShowFpsCounter = _gameViewActive ? _gameViewWasFpsCounterShown : false;
ShowNavigation = _gameViewActive ? _gameViewWasNagivationShown : false; ShowNavigation = _gameViewActive ? _gameViewWasNavigationShown : false;
Grid.Enabled = _gameViewActive ? _gameViewWasGridShown : false; Grid.Enabled = _gameViewActive ? _gameViewWasGridShown : false;
_gameViewActive = !_gameViewActive; _gameViewActive = !_gameViewActive;
@@ -647,7 +660,7 @@ namespace FlaxEditor.Viewport
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void OrientViewport(ref Quaternion orientation) public override void OrientViewport(ref Quaternion orientation)
{ {
if (TransformGizmo.SelectedParents.Count != 0) if (TransformGizmo.SelectedParents.Count != 0)
FocusSelection(ref orientation); FocusSelection(ref orientation);
@@ -681,7 +681,7 @@ namespace FlaxEditor.Viewport
} }
/// <inheritdoc /> /// <inheritdoc />
protected override void OrientViewport(ref Quaternion orientation) public override void OrientViewport(ref Quaternion orientation)
{ {
if (TransformGizmo.SelectedParents.Count != 0) if (TransformGizmo.SelectedParents.Count != 0)
FocusSelection(ref orientation); FocusSelection(ref orientation);
@@ -303,8 +303,7 @@ namespace FlaxEditor.Viewport.Previews
{ {
_terrain = new Terrain(); _terrain = new Terrain();
_terrain.Setup(1, 63); _terrain.Setup(1, 63);
var chunkSize = _terrain.ChunkSize; var heightMapSize = _terrain.HeightmapSize;
var heightMapSize = chunkSize * Terrain.PatchEdgeChunksCount + 1;
var heightMapLength = heightMapSize * heightMapSize; var heightMapLength = heightMapSize * heightMapSize;
var heightmap = new float[heightMapLength]; var heightmap = new float[heightMapLength];
var patchCoord = new Int2(0, 0); var patchCoord = new Int2(0, 0);
@@ -431,6 +431,9 @@ namespace FlaxEditor.Windows.Assets
_isWaitingForTimelineLoad = true; _isWaitingForTimelineLoad = true;
base.OnItemReimported(item); base.OnItemReimported(item);
// Drop virtual asset state and get a new one from the reimported file
LoadFromOriginal();
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -53,7 +53,7 @@ namespace FlaxEditor.Windows.Assets
{ {
Parent = this 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); InputActions.Add(options => options.Save, Save);
@@ -527,6 +527,16 @@ namespace FlaxEditor.Windows.Assets
return false; 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 /> /// <inheritdoc />
protected override T LoadAsset() protected override T LoadAsset()
{ {
@@ -115,6 +115,7 @@ namespace FlaxEditor.Windows
var root = _root; var root = _root;
root.LockChildrenRecursive(); root.LockChildrenRecursive();
PerformLayout();
// Update tree // Update tree
var query = _foldersSearchBox.Text; var query = _foldersSearchBox.Text;
+2
View File
@@ -1126,6 +1126,8 @@ namespace FlaxEditor.Windows
if (Editor.ContentDatabase.Find(_lastViewedFolderBeforeReload) is ContentFolder folder) if (Editor.ContentDatabase.Find(_lastViewedFolderBeforeReload) is ContentFolder folder)
_tree.Select(folder.Node); _tree.Select(folder.Node);
} }
OnFoldersSearchBoxTextChanged();
} }
private void Refresh() private void Refresh()
+5
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", 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; _searchBox.TextChanged += OnSearchBoxTextChanged;
ScriptsBuilder.ScriptsReloadEnd += OnSearchBoxTextChanged;
// Scene tree panel // Scene tree panel
_sceneTreePanel = new Panel _sceneTreePanel = new Panel
@@ -124,6 +125,7 @@ namespace FlaxEditor.Windows
{ {
base.OnPlayBegin(); base.OnPlayBegin();
_blockSceneTreeScroll = false; _blockSceneTreeScroll = false;
OnSearchBoxTextChanged();
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -138,6 +140,7 @@ namespace FlaxEditor.Windows
{ {
base.OnPlayEnd(); base.OnPlayEnd();
_blockSceneTreeScroll = true; _blockSceneTreeScroll = true;
OnSearchBoxTextChanged();
} }
/// <summary> /// <summary>
@@ -173,6 +176,7 @@ namespace FlaxEditor.Windows
return; return;
_tree.LockChildrenRecursive(); _tree.LockChildrenRecursive();
PerformLayout();
// Update tree // Update tree
var query = _searchBox.Text; var query = _searchBox.Text;
@@ -586,6 +590,7 @@ namespace FlaxEditor.Windows
_dragHandlers = null; _dragHandlers = null;
_tree = null; _tree = null;
_searchBox = null; _searchBox = null;
ScriptsBuilder.ScriptsReloadEnd -= OnSearchBoxTextChanged;
base.OnDestroy(); base.OnDestroy();
} }
+34
View File
@@ -8,6 +8,8 @@
#include "Loading/Tasks/LoadAssetTask.h" #include "Loading/Tasks/LoadAssetTask.h"
#include "Engine/Core/Log.h" #include "Engine/Core/Log.h"
#include "Engine/Core/LogContext.h" #include "Engine/Core/LogContext.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Physics/Physics.h"
#include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Profiler/ProfilerCPU.h"
#include "Engine/Profiler/ProfilerMemory.h" #include "Engine/Profiler/ProfilerMemory.h"
#include "Engine/Scripting/ManagedCLR/MCore.h" #include "Engine/Scripting/ManagedCLR/MCore.h"
@@ -703,6 +705,38 @@ void Asset::onUnload_MainThread()
OnUnloaded(this); 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 #if USE_EDITOR
bool Asset::OnCheckSave(const StringView& path) const bool Asset::OnCheckSave(const StringView& path) const
+4
View File
@@ -285,6 +285,10 @@ protected:
virtual void onRename(const StringView& newPath) = 0; virtual void onRename(const StringView& newPath) = 0;
#endif #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: public:
// [ManagedScriptingObject] // [ManagedScriptingObject]
String ToString() const override; String ToString() const override;
+2 -11
View File
@@ -6,7 +6,6 @@
#include "Engine/Content/Deprecated.h" #include "Engine/Content/Deprecated.h"
#include "Engine/Content/Upgraders/ShaderAssetUpgrader.h" #include "Engine/Content/Upgraders/ShaderAssetUpgrader.h"
#include "Engine/Content/Factories/BinaryAssetFactory.h" #include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/Graphics/GPUDevice.h"
#include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/Materials/MaterialShader.h" #include "Engine/Graphics/Materials/MaterialShader.h"
#include "Engine/Graphics/Shaders/Cache/ShaderCacheManager.h" #include "Engine/Graphics/Shaders/Cache/ShaderCacheManager.h"
@@ -157,16 +156,8 @@ Asset::LoadResult Material::load()
FlaxChunk* materialParamsChunk; FlaxChunk* materialParamsChunk;
// Wait for the GPU Device to be ready (eg. case when loading material before GPU init) // 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 (WaitForInitGraphics())
if (!IsInMainThread() && IS_GPU_NOT_READY()) return LoadResult::CannotLoadData;
{
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 engine was compiled with shaders compiling service: // If engine was compiled with shaders compiling service:
// - Material should be changed in need to convert it to the newer version (via Visject Surface) // - Material should be changed in need to convert it to the newer version (via Visject Surface)
@@ -19,10 +19,10 @@ Variant MaterialBase::GetParameterValue(const StringView& name)
if (!IsLoaded() && WaitForLoaded()) if (!IsLoaded() && WaitForLoaded())
return Variant::Null; return Variant::Null;
const auto param = Params.Get(name); const auto param = Params.Get(name);
if (IsMaterialInstance() && param && !param->IsOverride() && ((MaterialInstance*)this)->GetBaseMaterial())
return ((MaterialInstance*)this)->GetBaseMaterial()->GetParameterValue(name);
if (param) if (param)
{
return param->GetValue(); return param->GetValue();
}
LOG(Warning, "Missing material parameter '{0}' in material {1}", String(name), ToString()); LOG(Warning, "Missing material parameter '{0}' in material {1}", String(name), ToString());
return Variant::Null; return Variant::Null;
} }
@@ -57,6 +57,8 @@ public:
/// <summary> /// <summary>
/// Gets the material parameter value. /// Gets the material parameter value.
/// </summary> /// </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> /// <returns>The parameter value.</returns>
API_FUNCTION() Variant GetParameterValue(const StringView& name); API_FUNCTION() Variant GetParameterValue(const StringView& name);
@@ -46,6 +46,7 @@ namespace
ContentStorageService ContentStorageServiceInstance; ContentStorageService ContentStorageServiceInstance;
TimeSpan ContentStorageManager::UnusedStorageLifetime = TimeSpan::FromSeconds(0.5f);
TimeSpan ContentStorageManager::UnusedDataChunksLifetime = TimeSpan::FromSeconds(10); TimeSpan ContentStorageManager::UnusedDataChunksLifetime = TimeSpan::FromSeconds(10);
FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, bool loadIt) FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, bool loadIt)
@@ -15,7 +15,12 @@ class FLAXENGINE_API ContentStorageManager
{ {
public: public:
/// <summary> /// <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> /// </summary>
static TimeSpan UnusedDataChunksLifetime; static TimeSpan UnusedDataChunksLifetime;
@@ -286,14 +286,14 @@ FlaxStorage::LockData FlaxStorage::LockSafe()
uint32 FlaxStorage::GetRefCount() const uint32 FlaxStorage::GetRefCount() const
{ {
return (uint32)Platform::AtomicRead((intptr*)&_refCount); return (uint32)Platform::AtomicRead(&_refCount);
} }
bool FlaxStorage::ShouldDispose() const bool FlaxStorage::ShouldDispose() const
{ {
return Platform::AtomicRead((intptr*)&_refCount) == 0 && return Platform::AtomicRead(&_refCount) == 0 &&
Platform::AtomicRead((intptr*)&_chunksLock) == 0 && Platform::AtomicRead(&_chunksLock) == 0 &&
Platform::GetTimeSeconds() - _lastRefLostTime >= 0.5; // TTL in seconds Platform::GetTimeSeconds() - _lastRefLostTime >= ContentStorageManager::UnusedStorageLifetime.GetTotalSeconds();
} }
uint32 FlaxStorage::GetMemoryUsage() const uint32 FlaxStorage::GetMemoryUsage() const
+44 -12
View File
@@ -19,6 +19,7 @@
#include "Engine/Animations/AnimEvent.h" #include "Engine/Animations/AnimEvent.h"
#include "Engine/Level/Actors/EmptyActor.h" #include "Engine/Level/Actors/EmptyActor.h"
#include "Engine/Level/Actors/StaticModel.h" #include "Engine/Level/Actors/StaticModel.h"
#include "Engine/Level/Actors/AnimatedModel.h"
#include "Engine/Level/Prefabs/Prefab.h" #include "Engine/Level/Prefabs/Prefab.h"
#include "Engine/Level/Prefabs/PrefabManager.h" #include "Engine/Level/Prefabs/PrefabManager.h"
#include "Engine/Level/Scripts/ModelPrefab.h" #include "Engine/Level/Scripts/ModelPrefab.h"
@@ -82,6 +83,11 @@ bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
struct PrefabObject struct PrefabObject
{ {
enum
{
Model,
SkinnedModel,
} Type;
int32 NodeIndex; int32 NodeIndex;
String Name; String Name;
String AssetPath; String AssetPath;
@@ -280,7 +286,7 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context)
options.SplitObjects = false; options.SplitObjects = false;
options.ObjectIndex = -1; 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; options.Cached = &cached;
HashSet<String> objectNames; 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) 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]; auto& group = meshesByName[groupIndex];
// Cache object options (nested sub-object import removes the meshes) // Cache object options (nested sub-object import removes the meshes)
prefabObject.NodeIndex = group.First()->NodeIndex; MeshData* firstMesh = group.First();
prefabObject.Name = group.First()->Name; prefabObject.NodeIndex = firstMesh->NodeIndex;
prefabObject.Name = firstMesh->Name;
// 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.Type = ModelTool::ModelType::Model;
splitOptions.ObjectIndex = groupIndex; splitOptions.ObjectIndex = groupIndex;
if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath, group.First())) if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath, firstMesh))
{ {
prefabObjects.Add(prefabObject); prefabObjects.Add(prefabObject);
} }
@@ -734,24 +752,38 @@ CreateAssetResult ImportModel::CreatePrefab(CreateAssetContext& context, const M
nodeActors.Clear(); nodeActors.Clear();
for (const PrefabObject& e : prefabObjects) 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>(); auto* actor = New<StaticModel>();
actor->SetName(e.Name);
if (auto* model = Content::LoadAsync<Model>(e.AssetPath)) if (auto* model = Content::LoadAsync<Model>(e.AssetPath))
{
actor->Model = model; actor->Model = model;
} a = actor;
nodeActors.Add(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>(); Actor* nodeActor = nodeActors.Count() == 1 ? nodeActors[0] : New<EmptyActor>();
if (nodeActors.Count() > 1) if (nodeActors.Count() > 1)
{ {
for (Actor* e : nodeActors) for (Actor* e : nodeActors)
{
e->SetParent(nodeActor); e->SetParent(nodeActor);
}
} }
if (nodeActors.Count() != 1) if (nodeActors.Count() != 1)
{ {
+5
View File
@@ -155,6 +155,7 @@ void Screen::SetCursorLock(CursorLockMode mode)
bool inRelativeMode = Input::Mouse->IsRelative(); bool inRelativeMode = Input::Mouse->IsRelative();
if (mode == CursorLockMode::Clipped) if (mode == CursorLockMode::Clipped)
win->StartClippingCursor(bounds); win->StartClippingCursor(bounds);
#if PLATFORM_SDL
else if (mode == CursorLockMode::Locked) else if (mode == CursorLockMode::Locked)
{ {
// Use mouse clip region to restrict the cursor in one spot // 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) else if (CursorLock == CursorLockMode::Locked || CursorLock == CursorLockMode::Clipped)
win->EndClippingCursor(); win->EndClippingCursor();
#else
else if (CursorLock == CursorLockMode::Clipped)
win->EndClippingCursor();
#endif
// Enable relative mode when cursor is restricted // Enable relative mode when cursor is restricted
if (mode != CursorLockMode::None) if (mode != CursorLockMode::None)
@@ -88,9 +88,8 @@ void TerrainMaterialShader::Bind(BindParameters& params)
} }
// Bind terrain textures // Bind terrain textures
const auto heightmap = drawCall.Terrain.Patch->Heightmap->GetTexture(); GPUTexture* heightmap, *splatmap0, *splatmap1;
const auto splatmap0 = drawCall.Terrain.Patch->Splatmap[0] ? drawCall.Terrain.Patch->Splatmap[0]->GetTexture() : nullptr; drawCall.Terrain.Patch->GetTextures(heightmap, splatmap0, splatmap1);
const auto splatmap1 = drawCall.Terrain.Patch->Splatmap[1] ? drawCall.Terrain.Patch->Splatmap[1]->GetTexture() : nullptr;
context->BindSR(0, heightmap); context->BindSR(0, heightmap);
context->BindSR(1, splatmap0); context->BindSR(1, splatmap0);
context->BindSR(2, splatmap1); context->BindSR(2, splatmap1);
+5
View File
@@ -936,7 +936,9 @@ void InputService::Update()
break; break;
} }
} }
#if PLATFORM_SDL
WindowsManager::WindowsLocker.Unlock(); WindowsManager::WindowsLocker.Unlock();
#endif
// Send input events for the focused window // Send input events for the focused window
for (const auto& e : InputEvents) for (const auto& e : InputEvents)
@@ -990,6 +992,9 @@ void InputService::Update()
break; break;
} }
} }
#if !PLATFORM_SDL
WindowsManager::WindowsLocker.Unlock();
#endif
// Skip if game has no focus to handle the input // Skip if game has no focus to handle the input
if (!Engine::HasGameViewportFocus()) if (!Engine::HasGameViewportFocus())
+1 -2
View File
@@ -448,8 +448,7 @@ void SceneObjectsFactory::PrefabSyncData::InitNewObjects()
void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyncData& data) void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyncData& data)
{ {
PROFILE_CPU_NAMED("SetupPrefabInstances"); PROFILE_CPU_NAMED("SetupPrefabInstances");
const int32 count = data.Data.Size(); const int32 count = Math::Min<int32>(data.Data.Size(), data.SceneObjects.Count());
ASSERT(count <= data.SceneObjects.Count());
Dictionary<Guid, Guid> parentIdsLookup; Dictionary<Guid, Guid> parentIdsLookup;
for (int32 i = 0; i < count; i++) for (int32 i = 0; i < count; i++)
{ {
+20 -11
View File
@@ -209,6 +209,13 @@ void Collider::CreateShape()
// Create shape // Create shape
const bool isTrigger = _isTrigger && CanBeTrigger(); const bool isTrigger = _isTrigger && CanBeTrigger();
_shape = PhysicsBackend::CreateShape(this, shape, Material, IsActiveInHierarchy(), isTrigger); _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); PhysicsBackend::SetShapeContactOffset(_shape, _contactOffset);
UpdateLayerBits(); UpdateLayerBits();
} }
@@ -293,18 +300,20 @@ void Collider::BeginPlay(SceneBeginData* data)
if (_shape == nullptr) if (_shape == nullptr)
{ {
CreateShape(); CreateShape();
if (_shape)
// Check if parent is a rigidbody
const auto rigidBody = dynamic_cast<RigidBody*>(GetParent());
if (rigidBody && CanAttach(rigidBody))
{ {
// Attach to the rigidbody // Check if parent is a rigidbody
Attach(rigidBody); const auto rigidBody = dynamic_cast<RigidBody*>(GetParent());
} if (rigidBody && CanAttach(rigidBody))
else {
{ // Attach to the rigidbody
// Be a static collider Attach(rigidBody);
CreateStaticActor(); }
else
{
// Be a static collider
CreateStaticActor();
}
} }
} }
+2
View File
@@ -257,6 +257,8 @@ Asset::LoadResult CollisionData::load()
CollisionData::LoadResult CollisionData::load(const SerializedOptions* options, byte* dataPtr, int32 dataSize) CollisionData::LoadResult CollisionData::load(const SerializedOptions* options, byte* dataPtr, int32 dataSize)
{ {
if (WaitForInitPhysics())
return LoadResult::CannotLoadData;
PROFILE_MEM(Physics); PROFILE_MEM(Physics);
// Load options // Load options
@@ -1204,6 +1204,8 @@ void ScenePhysX::PreSimulateCloth(int32 i)
PROFILE_MEM(PhysicsCloth); PROFILE_MEM(PhysicsCloth);
auto clothPhysX = ClothsList[i]; auto clothPhysX = ClothsList[i];
auto& clothSettings = Cloths[clothPhysX]; auto& clothSettings = Cloths[clothPhysX];
if (!clothSettings.Actor)
return;
if (clothSettings.Actor->OnPreUpdate()) if (clothSettings.Actor->OnPreUpdate())
{ {
@@ -2686,10 +2688,13 @@ void* PhysicsBackend::CreateShape(PhysicsColliderActor* collider, const Collisio
PxGeometryHolder geometryPhysX; PxGeometryHolder geometryPhysX;
GetShapeGeometry(geometry, geometryPhysX); GetShapeGeometry(geometry, geometryPhysX);
PxShape* shapePhysX = PhysX->createShape(geometryPhysX.any(), materialsPhysX.Get(), materialsPhysX.Count(), true, shapeFlags); PxShape* shapePhysX = PhysX->createShape(geometryPhysX.any(), materialsPhysX.Get(), materialsPhysX.Count(), true, shapeFlags);
shapePhysX->userData = collider; if (shapePhysX)
{
shapePhysX->userData = collider;
#if PHYSX_DEBUG_NAMING #if PHYSX_DEBUG_NAMING
shapePhysX->setName("Shape"); shapePhysX->setName("Shape");
#endif #endif
}
return shapePhysX; return shapePhysX;
} }
+37 -27
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() bool Renderer::IsReady()
{ {
// Warm up first (state getters initialize content loading so do it for all first) // 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 // Perform postFx volumes blending and query before rendering
task->CollectPostFxVolumes(renderContext); task->CollectPostFxVolumes(renderContext);
renderContext.List->BlendSettings(); renderContext.List->BlendSettings();
auto aaMode = EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::AntiAliasing) ? renderContext.List->Settings.AntiAliasing.Mode : AntialiasingMode::None; {
if (aaMode == AntialiasingMode::TemporalAntialiasing && view.IsOrthographicProjection()) auto aaMode = EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::AntiAliasing) ? renderContext.List->Settings.AntiAliasing.Mode : AntialiasingMode::None;
aaMode = AntialiasingMode::None; // TODO: support TAA in ortho projection (see RenderView::Prepare to jitter projection matrix better) if (aaMode == AntialiasingMode::TemporalAntialiasing && view.IsOrthographicProjection())
renderContext.List->Settings.AntiAliasing.Mode = aaMode; 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 // Initialize setup
RenderSetup& setup = renderContext.List->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)) || (ssrSettings.Intensity > ZeroTolerance && ssrSettings.TemporalEffect && EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::SSR)) ||
renderContext.List->Settings.AntiAliasing.Mode == AntialiasingMode::TemporalAntialiasing; 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 || setup.UseGlobalSurfaceAtlas = renderContext.View.Mode == ViewMode::GlobalSurfaceAtlas ||
(EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::GI) && renderContext.List->Settings.GlobalIllumination.Mode == GlobalIlluminationMode::DDGI); (EnumHasAnyFlags(renderContext.View.Flags, ViewFlags::GI) && renderContext.List->Settings.GlobalIllumination.Mode == GlobalIlluminationMode::DDGI);
setup.UseGlobalSDF = (graphicsSettings->EnableGlobalSDF && EnumHasAnyFlags(view.Flags, ViewFlags::GlobalSDF)) || 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) if (renderContext.View.Mode == ViewMode::LightBuffer)
{ {
auto colorGradingLUT = ColorGradingPass::Instance()->RenderLUT(renderContext); RenderLightBuffer(task, context, renderContext, lightBuffer, tempDesc);
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);
return; return;
} }
@@ -656,11 +664,13 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
ReflectionsPass::Instance()->Render(renderContext, *lightBuffer); ReflectionsPass::Instance()->Render(renderContext, *lightBuffer);
if (renderContext.View.Mode == ViewMode::Reflections) if (renderContext.View.Mode == ViewMode::Reflections)
{ {
context->ResetRenderTarget(); renderContext.List->Settings.ToneMapping.Mode = ToneMappingMode::Neutral;
context->SetRenderTarget(task->GetOutputView()); renderContext.List->Settings.Bloom.Enabled = false;
context->SetViewportAndScissors(task->GetOutputViewport()); renderContext.List->Settings.LensFlares.Intensity = 0.0f;
context->Draw(lightBuffer); renderContext.List->Settings.CameraArtifacts.GrainAmount = 0.0f;
RenderTargetPool::Release(lightBuffer); renderContext.List->Settings.CameraArtifacts.ChromaticDistortion = 0.0f;
renderContext.List->Settings.CameraArtifacts.VignetteIntensity = 0.0f;
RenderLightBuffer(task, context, renderContext, lightBuffer, tempDesc);
return; return;
} }
@@ -716,7 +726,7 @@ void RenderInner(SceneRenderTask* task, RenderContext& renderContext, RenderCont
renderContext.List->RunCustomPostFxPass(context, renderContext, PostProcessEffectLocation::BeforePostProcessingPass, frameBuffer, tempBuffer); renderContext.List->RunCustomPostFxPass(context, renderContext, PostProcessEffectLocation::BeforePostProcessingPass, frameBuffer, tempBuffer);
// Temporal Anti-Aliasing (goes before post processing) // 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()); TAA::Instance()->Render(renderContext, frameBuffer, tempBuffer->View());
Swap(frameBuffer, tempBuffer); Swap(frameBuffer, tempBuffer);
+1
View File
@@ -20,6 +20,7 @@
#define RAPIDJSON_NEW(x) New<x> #define RAPIDJSON_NEW(x) New<x>
#define RAPIDJSON_DELETE(x) Delete(x) #define RAPIDJSON_DELETE(x) Delete(x)
#define RAPIDJSON_NOMEMBERITERATORCLASS #define RAPIDJSON_NOMEMBERITERATORCLASS
#define RAPIDJSON_PARSE_DEFAULT_FLAGS kParseTrailingCommasFlag
//#define RAPIDJSON_MALLOC(size) ::malloc(size) //#define RAPIDJSON_MALLOC(size) ::malloc(size)
//#define RAPIDJSON_REALLOC(ptr, new_size) ::realloc(ptr, new_size) //#define RAPIDJSON_REALLOC(ptr, new_size) ::realloc(ptr, new_size)
//#define RAPIDJSON_FREE(ptr) ::free(ptr) //#define RAPIDJSON_FREE(ptr) ::free(ptr)
+10 -1
View File
@@ -8,13 +8,22 @@ using Flax.Build.NativeCpp;
/// </summary> /// </summary>
public class Terrain : EngineModule 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 /> /// <inheritdoc />
public override void Setup(BuildOptions options) public override void Setup(BuildOptions options)
{ {
base.Setup(options); base.Setup(options);
options.PrivateDependencies.Add("Physics"); if (!WithEditing)
{
options.PublicDefinitions.Add("TERRAIN_EDITING=0");
}
options.PrivateDependencies.Add("Physics");
if (options.Target.IsEditor) if (options.Target.IsEditor)
{ {
options.PrivateDependencies.Add("ContentImporters"); options.PrivateDependencies.Add("ContentImporters");
+10
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 TerrainPatch* Terrain::GetPatch(const Int2& patchCoord) const
{ {
return GetPatch(patchCoord.X, patchCoord.Y); return GetPatch(patchCoord.X, patchCoord.Y);
+17 -7
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) // 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 #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 #define TERRAIN_EDITING 1
#endif
// Enable/disable terrain heightmap samples modification and gather. Used by the editor to modify the terrain with the brushes. // 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 // Enable/disable terrain physics collision drawing
#define TERRAIN_USE_PHYSICS_DEBUG (USE_EDITOR && 1) #define TERRAIN_USE_PHYSICS_DEBUG (USE_EDITOR && 1)
@@ -240,6 +243,18 @@ public:
return static_cast<int32>(_chunkSize); 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> /// <summary>
/// Gets the terrain patches count. Each patch contains 16 chunks arranged into a 4x4 square. /// Gets the terrain patches count. Each patch contains 16 chunks arranged into a 4x4 square.
/// </summary> /// </summary>
@@ -329,7 +344,6 @@ public:
API_FUNCTION() void SetChunkOverrideMaterial(API_PARAM(Ref) const Int2& patchCoord, API_PARAM(Ref) const Int2& chunkCoord, MaterialBase* value); API_FUNCTION() void SetChunkOverrideMaterial(API_PARAM(Ref) const Int2& patchCoord, API_PARAM(Ref) const Int2& chunkCoord, MaterialBase* value);
#if TERRAIN_EDITING #if TERRAIN_EDITING
/// <summary> /// <summary>
/// Setups the terrain patch using the specified heightmap data. /// Setups the terrain patch using the specified heightmap data.
/// </summary> /// </summary>
@@ -352,10 +366,6 @@ public:
/// <returns>True if failed, otherwise false.</returns> /// <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); 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> /// <summary>
/// Setups the terrain. Clears the existing data. /// Setups the terrain. Clears the existing data.
/// </summary> /// </summary>
+16 -13
View File
@@ -17,6 +17,7 @@
#include "Engine/Threading/Threading.h" #include "Engine/Threading/Threading.h"
#if TERRAIN_EDITING #if TERRAIN_EDITING
#include "Engine/Core/Math/Packed.h" #include "Engine/Core/Math/Packed.h"
#include "Engine/Core/Collections/ArrayExtensions.h"
#include "Engine/Graphics/PixelFormatExtensions.h" #include "Engine/Graphics/PixelFormatExtensions.h"
#include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/RenderTools.h"
#include "Engine/Graphics/RenderView.h" #include "Engine/Graphics/RenderView.h"
@@ -27,11 +28,6 @@
#include "Editor/Editor.h" #include "Editor/Editor.h"
#include "Engine/ContentImporters/AssetsImportingManager.h" #include "Engine/ContentImporters/AssetsImportingManager.h"
#endif #endif
#endif
#if TERRAIN_EDITING || TERRAIN_UPDATING
#include "Engine/Core/Collections/ArrayExtensions.h"
#endif
#if USE_EDITOR
#include "Engine/Debug/DebugDraw.h" #include "Engine/Debug/DebugDraw.h"
#endif #endif
#if TERRAIN_USE_PHYSICS_DEBUG #if TERRAIN_USE_PHYSICS_DEBUG
@@ -90,7 +86,7 @@ void TerrainPatch::Init(Terrain* terrain, int16 x, int16 z)
Splatmap[i] = nullptr; Splatmap[i] = nullptr;
} }
_heightfield = nullptr; _heightfield = nullptr;
#if TERRAIN_UPDATING #if TERRAIN_EDITING
_cachedHeightMap.Resize(0); _cachedHeightMap.Resize(0);
_cachedHolesMask.Resize(0); _cachedHolesMask.Resize(0);
_wasHeightModified = false; _wasHeightModified = false;
@@ -114,7 +110,7 @@ void TerrainPatch::Init(Terrain* terrain, int16 x, int16 z)
TerrainPatch::~TerrainPatch() TerrainPatch::~TerrainPatch()
{ {
#if TERRAIN_UPDATING #if TERRAIN_EDITING
SAFE_DELETE(_dataHeightmap); SAFE_DELETE(_dataHeightmap);
for (int32 i = 0; i < TERRAIN_MAX_SPLATMAPS_COUNT; i++) for (int32 i = 0; i < TERRAIN_MAX_SPLATMAPS_COUNT; i++)
{ {
@@ -134,6 +130,13 @@ RawDataAsset* TerrainPatch::GetHeightfield() const
return _heightfield.Get(); 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() void TerrainPatch::RemoveLightmap()
{ {
for (auto& chunk : Chunks) for (auto& chunk : Chunks)
@@ -178,7 +181,7 @@ void TerrainPatch::UpdateTransform()
_collisionVertices.Resize(0); _collisionVertices.Resize(0);
} }
#if TERRAIN_EDITING || TERRAIN_UPDATING #if TERRAIN_EDITING
bool IsValidMaterial(const JsonAssetReference<PhysicalMaterial>& e) 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 // When using physical materials, then get splatmaps data required for per-triangle material indices
void GetSplatMaps() void GetSplatMaps()
{ {
#if TERRAIN_UPDATING #if TERRAIN_EDITING
if (SplatMaps[0]) if (SplatMaps[0])
return; return;
if (UsePhysicalMaterials()) if (UsePhysicalMaterials())
@@ -1021,7 +1024,7 @@ bool TerrainPatch::SetupHeightMap(int32 heightMapLength, const float* heightMap,
_terrain->UpdateBounds(); _terrain->UpdateBounds();
_terrain->UpdateLayerBits(); _terrain->UpdateLayerBits();
#if TERRAIN_UPDATING #if TERRAIN_EDITING
// Invalidate cache // Invalidate cache
_cachedHeightMap.Resize(0); _cachedHeightMap.Resize(0);
_cachedHolesMask.Resize(0); _cachedHolesMask.Resize(0);
@@ -1169,7 +1172,7 @@ bool TerrainPatch::SetupSplatMap(int32 index, int32 splatMapLength, const Color3
} }
#endif #endif
#if TERRAIN_UPDATING #if TERRAIN_EDITING
// Invalidate cache // Invalidate cache
_cachedSplatMap[index].Resize(0); _cachedSplatMap[index].Resize(0);
_wasSplatmapModified[index] = false; _wasSplatmapModified[index] = false;
@@ -1191,7 +1194,7 @@ bool TerrainPatch::InitializeHeightMap()
return SetupHeightMap(heightmap.Count(), heightmap.Get()); return SetupHeightMap(heightmap.Count(), heightmap.Get());
} }
#if TERRAIN_UPDATING #if TERRAIN_EDITING
float* TerrainPatch::GetHeightmapData() float* TerrainPatch::GetHeightmapData()
{ {
@@ -2631,7 +2634,7 @@ void TerrainPatch::Serialize(SerializeStream& stream, const void* otherObj)
} }
stream.EndArray(); stream.EndArray();
#if TERRAIN_UPDATING #if TERRAIN_EDITING
SaveHeightData(); SaveHeightData();
SaveSplatData(); SaveSplatData();
#endif #endif
+18 -14
View File
@@ -12,6 +12,10 @@
struct RayCastHit; struct RayCastHit;
class TerrainMaterialShader; class TerrainMaterialShader;
#ifndef TERRAIN_EDITING
#define TERRAIN_EDITING 1
#endif
/// <summary> /// <summary>
/// Represents single terrain patch made of 16 terrain chunks. /// Represents single terrain patch made of 16 terrain chunks.
/// </summary> /// </summary>
@@ -34,7 +38,7 @@ private:
void* _physicsHeightField; void* _physicsHeightField;
CriticalSection _collisionLocker; CriticalSection _collisionLocker;
float _collisionScaleXZ; float _collisionScaleXZ;
#if TERRAIN_UPDATING #if TERRAIN_EDITING
Array<float> _cachedHeightMap; Array<float> _cachedHeightMap;
Array<byte> _cachedHolesMask; Array<byte> _cachedHolesMask;
Array<Color32> _cachedSplatMap[TERRAIN_MAX_SPLATMAPS_COUNT]; Array<Color32> _cachedSplatMap[TERRAIN_MAX_SPLATMAPS_COUNT];
@@ -189,6 +193,8 @@ public:
return _bounds; return _bounds;
} }
void GetTextures(GPUTexture*& heightmap, GPUTexture*& splatmap0, GPUTexture*& splatmap1) const;
public: public:
/// <summary> /// <summary>
/// Removes the lightmap data from the terrain patch. /// 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="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> /// <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> /// <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> /// <summary>
/// Setups the terrain patch layer weights using the specified splatmaps data. /// 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="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> /// <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> /// <returns>True if failed, otherwise false.</returns>
API_FUNCTION() bool SetupSplatMap(int32 index, int32 splatMapLength, API_PARAM(Ref) const Color32* splatMap, bool forceUseVirtualStorage = false); API_FUNCTION() bool SetupSplatMap(int32 index, int32 splatMapLength, const Color32* splatMap, bool forceUseVirtualStorage = false);
#endif
#if TERRAIN_UPDATING
/// <summary> /// <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> /// </summary>
/// <returns>The heightmap data.</returns> /// <returns>The heightmap data. Null if empty or failed to access it.</returns>
API_FUNCTION() float* GetHeightmapData(); API_FUNCTION() float* GetHeightmapData();
/// <summary> /// <summary>
@@ -246,9 +250,9 @@ public:
API_FUNCTION() void ClearHeightmapCache(); API_FUNCTION() void ClearHeightmapCache();
/// <summary> /// <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> /// </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(); API_FUNCTION() byte* GetHolesMaskData();
/// <summary> /// <summary>
@@ -257,10 +261,10 @@ public:
API_FUNCTION() void ClearHolesMaskCache(); API_FUNCTION() void ClearHolesMaskCache();
/// <summary> /// <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> /// </summary>
/// <param name="index">The zero-based index of the splatmap texture.</param> /// <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); API_FUNCTION() Color32* GetSplatMapData(int32 index);
/// <summary> /// <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="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> /// <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> /// <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> /// <summary>
/// Modifies the terrain patch holes mask with the given samples. /// 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="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> /// <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> /// <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> /// <summary>
/// Modifies the terrain patch splat map (layers mask) with the given samples. /// 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="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> /// <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> /// <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: private:
bool UpdateHeightData(struct TerrainDataUpdateInfo& info, const Int2& modifiedOffset, const Int2& modifiedSize, bool wasHeightRangeChanged, bool wasHeightChanged); bool UpdateHeightData(struct TerrainDataUpdateInfo& info, const Int2& modifiedOffset, const Int2& modifiedSize, bool wasHeightRangeChanged, bool wasHeightChanged);
+20 -14
View File
@@ -588,6 +588,7 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj
SERIALIZE(SloppyOptimization); SERIALIZE(SloppyOptimization);
SERIALIZE(LODTargetError); SERIALIZE(LODTargetError);
SERIALIZE(ImportMaterials); SERIALIZE(ImportMaterials);
SERIALIZE(CreateEmptyMaterialSlots);
SERIALIZE(ImportMaterialsAsInstances); SERIALIZE(ImportMaterialsAsInstances);
SERIALIZE(InstanceToImportAs); SERIALIZE(InstanceToImportAs);
SERIALIZE(ImportTextures); SERIALIZE(ImportTextures);
@@ -643,6 +644,7 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi
DESERIALIZE(SloppyOptimization); DESERIALIZE(SloppyOptimization);
DESERIALIZE(LODTargetError); DESERIALIZE(LODTargetError);
DESERIALIZE(ImportMaterials); DESERIALIZE(ImportMaterials);
DESERIALIZE(CreateEmptyMaterialSlots);
DESERIALIZE(ImportMaterialsAsInstances); DESERIALIZE(ImportMaterialsAsInstances);
DESERIALIZE(InstanceToImportAs); DESERIALIZE(InstanceToImportAs);
DESERIALIZE(ImportTextures); DESERIALIZE(ImportTextures);
@@ -1019,7 +1021,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
options.ImportTypes |= ImportDataTypes::Skeleton; options.ImportTypes |= ImportDataTypes::Skeleton;
break; break;
case ModelType::Prefab: case ModelType::Prefab:
options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Animations; options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Skeleton | ImportDataTypes::Animations;
if (options.ImportMaterials) if (options.ImportMaterials)
options.ImportTypes |= ImportDataTypes::Materials; options.ImportTypes |= ImportDataTypes::Materials;
if (options.ImportTextures) if (options.ImportTextures)
@@ -1045,6 +1047,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
{ {
for (auto& mesh : lod.Meshes) for (auto& mesh : lod.Meshes)
{ {
if (mesh->BlendShapes.IsEmpty())
continue;
for (int32 blendShapeIndex = mesh->BlendShapes.Count() - 1; blendShapeIndex >= 0; blendShapeIndex--) for (int32 blendShapeIndex = mesh->BlendShapes.Count() - 1; blendShapeIndex >= 0; blendShapeIndex--)
{ {
auto& blendShape = mesh->BlendShapes[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++) for (int32 i = 0; i < meshesCount; i++)
{ {
const auto mesh = data.LODs[0].Meshes[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 indices = Int4::Zero;
auto weights = Float4::UnitX; auto weights = Float4::UnitX;
@@ -1326,7 +1332,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
auto& texture = data.Textures[i]; auto& texture = data.Textures[i];
// Auto-import textures // 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; continue;
String assetPath = GetAdditionalImportPath(autoImportOutput, importedFileNames, StringUtils::GetFileNameWithoutExtension(texture.FilePath)); String assetPath = GetAdditionalImportPath(autoImportOutput, importedFileNames, StringUtils::GetFileNameWithoutExtension(texture.FilePath));
#if COMPILE_WITH_ASSETS_IMPORTER #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) if (options.ImportMaterialsAsInstances)
{ {
// Create material instance // Create material instance
@@ -2021,12 +2031,11 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
#undef REMAP_VERTEX_BUFFER #undef REMAP_VERTEX_BUFFER
// Remap blend shapes // 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++) for (int32 blendShapeIndex = 0; blendShapeIndex < srcMesh->BlendShapes.Count(); blendShapeIndex++)
{ {
const auto& srcBlendShape = srcMesh->BlendShapes[blendShapeIndex]; const auto& srcBlendShape = srcMesh->BlendShapes[blendShapeIndex];
auto& dstBlendShape = dstMesh->BlendShapes[blendShapeIndex]; BlendShape dstBlendShape;
dstBlendShape.Name = srcBlendShape.Name; dstBlendShape.Name = srcBlendShape.Name;
dstBlendShape.Weight = srcBlendShape.Weight; dstBlendShape.Weight = srcBlendShape.Weight;
dstBlendShape.Vertices.EnsureCapacity(srcBlendShape.Vertices.Count()); 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]; auto v = srcBlendShape.Vertices[i];
v.VertexIndex = remap[v.VertexIndex]; v.VertexIndex = remap[v.VertexIndex];
if (v.VertexIndex != ~0u) if (v.VertexIndex != ~0u)
{
dstBlendShape.Vertices.Add(v); dstBlendShape.Vertices.Add(v);
}
} }
}
// Remove empty blend shapes // Add only valid blend shapes
for (int32 blendShapeIndex = dstMesh->BlendShapes.Count() - 1; blendShapeIndex >= 0; blendShapeIndex--) if (dstBlendShape.Vertices.HasItems())
{ dstMesh->BlendShapes.Add(dstBlendShape);
if (dstMesh->BlendShapes[blendShapeIndex].Vertices.IsEmpty())
dstMesh->BlendShapes.RemoveAt(blendShapeIndex);
} }
// Optimize generated LOD // Optimize generated LOD
@@ -2092,6 +2096,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
{ {
for (auto& mesh : lod.Meshes) for (auto& mesh : lod.Meshes)
{ {
if (mesh->BlendShapes.IsEmpty())
continue;
for (auto& blendShape : mesh->BlendShapes) for (auto& blendShape : mesh->BlendShapes)
{ {
// Compute min/max for used vertex indices // Compute min/max for used vertex indices
+7 -4
View File
@@ -311,16 +311,19 @@ public:
public: // Materials public: // Materials
// If checked, the importer will create materials for model meshes as specified in the file. // 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; 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. // 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; bool ImportMaterialsAsInstances = false;
// The material used as the base material that will be instanced as the imported model's material. // 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; AssetReference<MaterialBase> InstanceToImportAs;
// If checked, the importer will import texture files used by the model and any embedded texture resources. // 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; 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. // 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))") API_FIELD(Attributes="EditorOrder(420), EditorDisplay(\"Materials\", \"Keep Overridden Materials\"), VisibleIf(nameof(ShowGeometry))")
@@ -414,6 +414,7 @@ bool TextureTool::UpdateTexture(GPUContext* context, GPUTexture* texture, int32
Array<byte> tempData; Array<byte> tempData;
if (textureFormat != dataFormat) if (textureFormat != dataFormat)
{ {
PROFILE_CPU_NAMED("ConvertTexture");
auto dataSampler = PixelFormatSampler::Get(dataFormat); auto dataSampler = PixelFormatSampler::Get(dataFormat);
auto textureSampler = PixelFormatSampler::Get(textureFormat); auto textureSampler = PixelFormatSampler::Get(textureFormat);
if (!dataSampler || !textureSampler) if (!dataSampler || !textureSampler)
+58 -26
View File
@@ -5,14 +5,14 @@ using System;
namespace FlaxEngine.GUI namespace FlaxEngine.GUI
{ {
/// <summary> /// <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> /// </summary>
/// <seealso cref="FlaxEngine.GUI.ContainerControl" /> /// <seealso cref="FlaxEngine.GUI.ContainerControl" />
public class RadialMenu : ContainerControl public class RadialMenu : ContainerControl
{ {
private bool _materialIsDirty = true; private bool _materialIsDirty = true;
private float _angle; private float _angle;
private float _selectedSegment; private int _selectedSegment;
private int _highlightSegment = -1; private int _highlightSegment = -1;
private MaterialBase _material; private MaterialBase _material;
private MaterialInstance _materialInstance; private MaterialInstance _materialInstance;
@@ -27,7 +27,7 @@ namespace FlaxEngine.GUI
private bool ShowMatProp => _material != null; private bool ShowMatProp => _material != null;
/// <summary> /// <summary>
/// The material to use for menu background drawing. /// The material used for menu background drawing.
/// </summary> /// </summary>
[EditorOrder(1)] [EditorOrder(1)]
public MaterialBase Material public MaterialBase Material
@@ -44,7 +44,7 @@ namespace FlaxEngine.GUI
} }
/// <summary> /// <summary>
/// Gets or sets the edge offset. /// Gets or sets the offset of the outer edge from the bounds of the Control.
/// </summary> /// </summary>
[EditorOrder(2), Range(0, 1)] [EditorOrder(2), Range(0, 1)]
public float EdgeOffset public float EdgeOffset
@@ -59,7 +59,7 @@ namespace FlaxEngine.GUI
} }
/// <summary> /// <summary>
/// Gets or sets the thickness. /// Gets or sets the thickness of the menu.
/// </summary> /// </summary>
[EditorOrder(3), Range(0, 1), VisibleIf(nameof(ShowMatProp))] [EditorOrder(3), Range(0, 1), VisibleIf(nameof(ShowMatProp))]
public float Thickness public float Thickness
@@ -74,7 +74,7 @@ namespace FlaxEngine.GUI
} }
/// <summary> /// <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> /// </summary>
[VisibleIf(nameof(ShowMatProp))] [VisibleIf(nameof(ShowMatProp))]
public new Color BackgroundColor public new Color BackgroundColor
@@ -88,7 +88,7 @@ namespace FlaxEngine.GUI
} }
/// <summary> /// <summary>
/// Gets or sets the color of the highlight. /// Gets or sets the color of the outer edge highlight.
/// </summary> /// </summary>
[VisibleIf(nameof(ShowMatProp))] [VisibleIf(nameof(ShowMatProp))]
public Color HighlightColor public Color HighlightColor
@@ -130,19 +130,43 @@ namespace FlaxEngine.GUI
} }
/// <summary> /// <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> /// </summary>
[HideInEditor] [HideInEditor]
public Action<int> Selected; public Action<int> Selected;
/// <summary> /// <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> /// </summary>
[VisibleIf(nameof(ShowMatProp))] [VisibleIf(nameof(ShowMatProp))]
public bool AllowChangeSelectionWhenInside; public bool AllowChangeSelectionWhenInside;
/// <summary> /// <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> /// </summary>
[VisibleIf(nameof(ShowMatProp))] [VisibleIf(nameof(ShowMatProp))]
public bool CenterAsButton; public bool CenterAsButton;
@@ -225,7 +249,7 @@ namespace FlaxEngine.GUI
var min = ((1 - _edgeOffset) - _thickness) * USize * 0.5f; var min = ((1 - _edgeOffset) - _thickness) * USize * 0.5f;
var max = (1 - _edgeOffset) * USize * 0.5f; var max = (1 - _edgeOffset) * USize * 0.5f;
var val = ((USize * 0.5f) - location).Length; 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); UpdateAngle(ref location);
} }
@@ -276,7 +300,7 @@ namespace FlaxEngine.GUI
var min = ((1 - _edgeOffset) - _thickness) * USize * 0.5f; var min = ((1 - _edgeOffset) - _thickness) * USize * 0.5f;
var max = (1 - _edgeOffset) * USize * 0.5f; var max = (1 - _edgeOffset) * USize * 0.5f;
var val = ((USize * 0.5f) - location).Length; 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); UpdateAngle(ref location);
} }
@@ -347,6 +371,28 @@ namespace FlaxEngine.GUI
base.PerformLayout(force); 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() private void UpdateSelectionColor()
{ {
Color color; Color color;
@@ -368,20 +414,6 @@ namespace FlaxEngine.GUI
_materialInstance.SetParameterValue("RadialMenu_SelectionColor", color); _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) private static Float2 Rotate2D(Float2 point, float angle)
{ {
return new Float2(Mathf.Cos(angle) * point.X + Mathf.Sin(angle) * point.Y, return new Float2(Mathf.Cos(angle) * point.X + Mathf.Sin(angle) * point.Y,
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

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