diff --git a/.github/ISSUE_TEMPLATE/1-bug.yaml b/.github/ISSUE_TEMPLATE/1-bug.yaml index a75003f63..7e6150557 100644 --- a/.github/ISSUE_TEMPLATE/1-bug.yaml +++ b/.github/ISSUE_TEMPLATE/1-bug.yaml @@ -5,7 +5,7 @@ body: - type: markdown attributes: value: | - Thanks for taking the time to fill out this bug report! Please attach any minimal reproduction projects! + Thanks for taking the time to fill out this bug report! Please attach a minimal reproduction project if available! - type: textarea id: description-area attributes: @@ -17,19 +17,19 @@ body: id: steps-area attributes: label: Steps to reproduce - description: Please provide reproduction steps if possible. + description: Please provide reproduction steps if available. validations: required: true - type: dropdown id: version attributes: label: Version - description: What version of Flax are you running? + description: What version of Flax did you experience the bug in? options: - - '1.8' - '1.9' - '1.10' - '1.11' + - '1.12' - master branch default: 3 validations: diff --git a/.github/ISSUE_TEMPLATE/2-feature-request.yaml b/.github/ISSUE_TEMPLATE/2-feature-request.yaml index 338c9aea0..7ad43bf5b 100644 --- a/.github/ISSUE_TEMPLATE/2-feature-request.yaml +++ b/.github/ISSUE_TEMPLATE/2-feature-request.yaml @@ -5,7 +5,7 @@ body: - type: markdown attributes: value: | - Thanks for taking the time to fill out a feature request! + Thank you for taking the time to submit this feature request! - type: textarea id: description-area attributes: @@ -17,6 +17,6 @@ body: id: benefits-area attributes: label: Benefits - description: Please provide what benefits this feature would provide to the engine! + description: Please list what benefits this feature would provide to the engine! validations: required: true \ No newline at end of file diff --git a/.github/workflows/build_android.yml b/.github/workflows/build_android.yml index 772e3f67c..741d446f7 100644 --- a/.github/workflows/build_android.yml +++ b/.github/workflows/build_android.yml @@ -4,6 +4,7 @@ on: [push, pull_request] env: DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: false + DOTNET_ROLL_FORWARD: 'minor' jobs: @@ -19,7 +20,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 8.0.419 - name: Setup .NET Workload run: | dotnet workload install android @@ -33,4 +34,7 @@ jobs: git lfs pull - name: Build 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 diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml index 85f4e0c79..f64e800b3 100644 --- a/.github/workflows/build_windows.yml +++ b/.github/workflows/build_windows.yml @@ -4,6 +4,7 @@ on: [push, pull_request] env: DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: false + DOTNET_ROLL_FORWARD: 'minor' jobs: @@ -19,7 +20,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 8.0.419 - name: Print .NET info run: | dotnet --info @@ -30,6 +31,9 @@ jobs: git lfs pull - name: Build 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 # Game @@ -44,7 +48,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 8.0.419 - name: Print .NET info run: | dotnet --info @@ -55,4 +59,7 @@ jobs: git lfs pull - name: Build 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 diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9b7c5c965..4155dca71 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -7,6 +7,7 @@ on: env: DOTNET_NOLOGO: true 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' jobs: @@ -27,13 +28,16 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 8.0.419 - name: Print .NET info run: | dotnet --info dotnet workload --info - name: Build 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 - name: Upload uses: actions/upload-artifact@v4 @@ -60,13 +64,16 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 8.0.419 - name: Print .NET info run: | dotnet --info dotnet workload --info - name: Build 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 - name: Upload uses: actions/upload-artifact@v4 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fdc8e80c9..5202bbb61 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,6 +4,7 @@ on: [push, pull_request] env: DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: false + DOTNET_ROLL_FORWARD: 'minor' jobs: @@ -59,7 +60,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 8.0.x + dotnet-version: 8.0.419 - name: Print .NET info run: | dotnet --info @@ -70,6 +71,9 @@ jobs: git lfs pull - name: Build 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 .\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 diff --git a/Content/Editor/Fonts/Inconsolata-Regular.flax b/Content/Editor/Fonts/Inconsolata-Regular.flax index a2fdf0626..d85ef610a 100644 --- a/Content/Editor/Fonts/Inconsolata-Regular.flax +++ b/Content/Editor/Fonts/Inconsolata-Regular.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:36995fa880bf47331618b1e96f613c6925e72484fe76e98d6e44c1985ecab017 -size 93015 +oid sha256:139e2f54a268f4febc753aaa9807a33a6eb2dd84e788c5b8b4f50ca8683d2b3b +size 93116 diff --git a/Content/Editor/Fonts/NotoSansSC-Regular.flax b/Content/Editor/Fonts/NotoSansSC-Regular.flax index 39964e6c5..8f8f00157 100644 --- a/Content/Editor/Fonts/NotoSansSC-Regular.flax +++ b/Content/Editor/Fonts/NotoSansSC-Regular.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19fa43eb7b31ee3936348b1f271c464c79d7020a21d33e3cdbe54f98c3b14304 -size 10560899 +oid sha256:2ddc7af5fab1dd4ad858ad2abf11c6c243d908345720892cd56242fbc9c33b02 +size 10560900 diff --git a/Content/Editor/Fonts/Roboto-Regular.flax b/Content/Editor/Fonts/Roboto-Regular.flax index a1e416f77..01251cac6 100644 --- a/Content/Editor/Fonts/Roboto-Regular.flax +++ b/Content/Editor/Fonts/Roboto-Regular.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:574b86d8e7439e88c3d7b4265cbc5ad7089c49368c47661b2c6f4c997cb53d86 -size 172093 +oid sha256:c0584b3a3c186364464de155edece5cb6a8c46000676ac82a45f951f3a67fac5 +size 172194 diff --git a/Content/Editor/Fonts/SegMDL2.flax b/Content/Editor/Fonts/SegMDL2.flax index d06bac811..b627849d9 100644 --- a/Content/Editor/Fonts/SegMDL2.flax +++ b/Content/Editor/Fonts/SegMDL2.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0c4a17c412fce35275af5be883c784b57d192eb188fb3568990b0ae8e0e6d34e -size 204049 +oid sha256:f7a9d723d6342b8a484dfb2ee784cef506b3311a6092be0e9de35f941d698ff5 +size 204150 diff --git a/Content/Editor/Fonts/Segoe Media Center Regular.flax b/Content/Editor/Fonts/Segoe Media Center Regular.flax index e9038cd65..6c17d96bf 100644 --- a/Content/Editor/Fonts/Segoe Media Center Regular.flax +++ b/Content/Editor/Fonts/Segoe Media Center Regular.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5d277b4abbdc1c91f056dd0d838631135242643abb41ce5751b1be3aaca72401 -size 72697 +oid sha256:c21443a111daede658cb634f56b4ae0838dbb2eaa75d4ba8ba74a4c77df44de8 +size 72798 diff --git a/Content/Editor/HSWheel.flax b/Content/Editor/HSWheel.flax new file mode 100644 index 000000000..7fb2f0600 --- /dev/null +++ b/Content/Editor/HSWheel.flax @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:946a92d15b50e01a67d9e0a9d71493b222dce2b7cc464383b07450ef3afa6888 +size 15068 diff --git a/Content/Shaders/GI/DDGI.flax b/Content/Shaders/GI/DDGI.flax index 257953bf9..98cfbb80a 100644 --- a/Content/Shaders/GI/DDGI.flax +++ b/Content/Shaders/GI/DDGI.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5b017cf857f443553020e4bc7c8c8c5da3a826a2514322664a023ffa6005f7a5 -size 38217 +oid sha256:50753dbd37cb2a6fd6f48a597e90c8b8b689c7e433e66a5f21bc4ffa94244895 +size 38260 diff --git a/Content/Shaders/GUI.flax b/Content/Shaders/GUI.flax index d5e3f59fa..79948533e 100644 --- a/Content/Shaders/GUI.flax +++ b/Content/Shaders/GUI.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4fed6a05104322f61a77f6cf69b64478518b829165a2a3ab7fc151098dcd48be -size 4526 +oid sha256:324f564e3b0324dbdf5712f3f1e11f54f276d11bc203aecabeef93953b2ab904 +size 4719 diff --git a/Content/Shaders/GlobalSignDistanceField.flax b/Content/Shaders/GlobalSignDistanceField.flax index 5afcb4bf4..01dae76b1 100644 --- a/Content/Shaders/GlobalSignDistanceField.flax +++ b/Content/Shaders/GlobalSignDistanceField.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1f07ebb16820897e8598ae7a0627cb75b3d28e9dceea3ad4bd9ff543d5cdd01c -size 13979 +oid sha256:0506a5485a0107f67714ceb8c1714f18cb5718bacfde36fad91d53ea3cb60de9 +size 14044 diff --git a/Flax.flaxproj b/Flax.flaxproj index c9c27281b..dc126772a 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -4,7 +4,7 @@ "Major": 1, "Minor": 11, "Revision": 0, - "Build": 6807 + "Build": 6809 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2026 Wojciech Figat. All rights reserved.", diff --git a/Source/Editor/Content/Tree/ContentFolderTreeNode.cs b/Source/Editor/Content/Tree/ContentFolderTreeNode.cs index 93fa1f0d3..129cc1473 100644 --- a/Source/Editor/Content/Tree/ContentFolderTreeNode.cs +++ b/Source/Editor/Content/Tree/ContentFolderTreeNode.cs @@ -184,15 +184,13 @@ public class ContentFolderTreeNode : TreeNode } } - bool isExpanded = isAnyChildVisible; - - if (isExpanded) + if (!noFilter) { - Expand(true); - } - else - { - Collapse(true); + bool isExpanded = isAnyChildVisible; + if (isExpanded) + Expand(true); + else + Collapse(true); } Visible = isThisVisible | isAnyChildVisible; diff --git a/Source/Editor/CustomEditors/CustomEditor.cs b/Source/Editor/CustomEditors/CustomEditor.cs index ff9a14ea3..253fbc590 100644 --- a/Source/Editor/CustomEditors/CustomEditor.cs +++ b/Source/Editor/CustomEditors/CustomEditor.cs @@ -52,6 +52,7 @@ namespace FlaxEditor.CustomEditors private readonly List _children = new List(); private ValueContainer _values; private bool _isSetBlocked; + private bool _isRebuilding; private bool _skipChildrenRefresh; private bool _hasValueDirty; private bool _rebuildOnRefresh; @@ -178,7 +179,7 @@ namespace FlaxEditor.CustomEditors public void RebuildLayout() { // Skip rebuilding during init - if (CurrentCustomEditor == this) + if (CurrentCustomEditor == this || _isRebuilding) return; // Special case for root objects to run normal layout build @@ -197,6 +198,7 @@ namespace FlaxEditor.CustomEditors _parent?.RebuildLayout(); return; } + _isRebuilding = true; var control = layout.ContainerControl; var parent = _parent; var parentScrollV = (_presenter?.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1; @@ -216,6 +218,7 @@ namespace FlaxEditor.CustomEditors // Restore scroll value if (parentScrollV > -1 && _presenter != null && _presenter.Panel.Parent is Panel panel && panel.VScrollBar != null) panel.VScrollBar.Value = parentScrollV; + _isRebuilding = false; } /// diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs index 9844f3fda..efcd1bec3 100644 --- a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs @@ -103,11 +103,12 @@ namespace FlaxEditor.CustomEditors.Dedicated var actors = ScriptsEditor.ParentEditor.Values; foreach (var a in actors) { - if (a.GetType() != requireActor.RequiredType) - { - item.Enabled = false; - break; - } + if (a.GetType() == requireActor.RequiredType) + continue; + if (requireActor.IncludeInheritedTypes && a.GetType().IsSubclassOf(requireActor.RequiredType)) + continue; + item.Enabled = false; + break; } } cm.AddItem(item); @@ -739,6 +740,8 @@ namespace FlaxEditor.CustomEditors.Dedicated /// public override void Initialize(LayoutElementsContainer layout) { + var style = FlaxEngine.GUI.Style.Current; + // Area for drag&drop scripts var dragArea = layout.CustomContainer(); dragArea.CustomControl.ScriptsEditor = this; @@ -800,17 +803,10 @@ namespace FlaxEditor.CustomEditors.Dedicated bool hasAllRequirements = true; if (scriptType.HasAttribute(typeof(RequireScriptAttribute), false)) { - RequireScriptAttribute scriptAttribute = null; - foreach (var e in scriptType.GetAttributes(false)) + var attribute = (RequireScriptAttribute)scriptType.GetAttributes(false).FirstOrDefault(x => x is RequireScriptAttribute); + if (attribute != null) { - if (e is not RequireScriptAttribute requireScriptAttribute) - continue; - scriptAttribute = requireScriptAttribute; - } - - if (scriptAttribute != null) - { - foreach (var type in scriptAttribute.RequiredTypes) + foreach (var type in attribute.RequiredTypes) { if (!type.IsSubclassOf(typeof(Script))) continue; @@ -825,19 +821,11 @@ namespace FlaxEditor.CustomEditors.Dedicated } if (scriptType.HasAttribute(typeof(RequireActorAttribute), false)) { - RequireActorAttribute attribute = null; - foreach (var e in scriptType.GetAttributes(false)) - { - if (e is not RequireActorAttribute requireActorAttribute) - continue; - attribute = requireActorAttribute; - break; - } - + var attribute = (RequireActorAttribute)scriptType.GetAttributes(false).FirstOrDefault(x => x is RequireActorAttribute); if (attribute != null) { var actor = script.Actor; - if (actor.GetType() != attribute.RequiredType && !actor.GetType().IsSubclassOf(attribute.RequiredType)) + if (actor.GetType() != attribute.RequiredType && (attribute.IncludeInheritedTypes && !actor.GetType().IsSubclassOf(attribute.RequiredType))) { Editor.LogWarning($"`{Utilities.Utils.GetPropertyNameUI(scriptType.Name)}` on `{script.Actor}` is missing a required Actor of type `{attribute.RequiredType}`."); hasAllRequirements = false; @@ -850,7 +838,7 @@ namespace FlaxEditor.CustomEditors.Dedicated var title = Utilities.Utils.GetPropertyNameUI(scriptType.Name); var group = layout.Group(title, editor); if (!hasAllRequirements) - group.Panel.HeaderTextColor = FlaxEngine.GUI.Style.Current.Statusbar.Failed; + group.Panel.HeaderTextColor = style.Statusbar.Failed; if ((Presenter.Features & FeatureFlags.CacheExpandedGroups) != 0) { if (Editor.Instance.ProjectCache.IsGroupToggled(title)) @@ -863,9 +851,10 @@ namespace FlaxEditor.CustomEditors.Dedicated group.Panel.Open(); // Customize + float totalHeaderButtonsOffset = 0f; group.Panel.TooltipText = Editor.Instance.CodeDocs.GetTooltip(scriptType); if (script.HasPrefabLink) - group.Panel.HeaderTextColor = FlaxEngine.GUI.Style.Current.ProgressNormal; + group.Panel.HeaderTextColor = style.ProgressNormal; // Add toggle button to the group var headerHeight = group.Panel.HeaderHeight; @@ -889,7 +878,7 @@ namespace FlaxEditor.CustomEditors.Dedicated TooltipText = "Script reference.", AutoFocus = true, IsScrollable = false, - Color = FlaxEngine.GUI.Style.Current.ForegroundGrey, + Color = style.ForegroundGrey, Parent = group.Panel, Bounds = new Rectangle(scriptToggle.Right, 0.5f, headerHeight, headerHeight), Margin = new Margin(1), @@ -908,10 +897,35 @@ namespace FlaxEditor.CustomEditors.Dedicated var settingsButton = group.AddSettingsButton(); settingsButton.Tag = script; settingsButton.Clicked += OnSettingsButtonClicked; + totalHeaderButtonsOffset += settingsButton.Width + FlaxEditor.Utilities.Constants.UIMargin; + + // Add script obsolete icon to the group + if (scriptType.HasAttribute(typeof(ObsoleteAttribute), false)) + { + var attribute = (ObsoleteAttribute)scriptType.GetAttributes(false).First(x => x is ObsoleteAttribute); + var tooltip = "Script marked as obsolete." + + (string.IsNullOrEmpty(attribute.Message) ? "" : $"\n{attribute.Message}") + + (string.IsNullOrEmpty(attribute.DiagnosticId) ? "" : $"\n{attribute.DiagnosticId}"); + var obsoleteButton = group.AddHeaderButton(tooltip, totalHeaderButtonsOffset, Editor.Instance.Icons.Info32); + obsoleteButton.Color = Color.Orange; + obsoleteButton.MouseOverColor = Color.DarkOrange; + totalHeaderButtonsOffset += obsoleteButton.Width; + } + + // Show visual indicator if script only exists in prefab instance and is not part of the prefab + bool isPrefabActor = scripts.Any(s => s.Actor.HasPrefabLink); + if (isPrefabActor && script.PrefabID == Guid.Empty) + { + var prefabInstanceButton = group.AddHeaderButton("Script only exists in this prefab instance.", totalHeaderButtonsOffset, Editor.Instance.Icons.Add32); + prefabInstanceButton.Color = style.ProgressNormal; + prefabInstanceButton.MouseOverColor = style.ProgressNormal * 0.9f; + totalHeaderButtonsOffset += prefabInstanceButton.Width; + } // Adjust margin to not overlap with other ui elements in the header group.Panel.HeaderTextMargin = group.Panel.HeaderTextMargin with { Left = scriptDrag.Right - 12, Right = settingsButton.Width + Utilities.Constants.UIMargin }; group.Object(values, editor); + // Remove drop down arrows and containment lines if no objects in the group if (group.Children.Count == 0) { diff --git a/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs b/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs index 3e0332b09..a8bfe4f5f 100644 --- a/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/SplineEditor.cs @@ -18,7 +18,7 @@ namespace FlaxEditor.CustomEditors.Dedicated public class SplineEditor : ActorEditor { /// - /// Storage undo spline data + /// Stores undo spline data. /// private struct UndoData { @@ -83,7 +83,7 @@ namespace FlaxEditor.CustomEditors.Dedicated } /// - /// Edit curve options manipulate the curve as free mode + /// Edit curve options manipulate the curve as free mode. /// private sealed class FreeTangentMode : EditTangentOptionBase { @@ -98,7 +98,7 @@ namespace FlaxEditor.CustomEditors.Dedicated } /// - /// Edit curve options to set tangents to linear + /// Edit curve options to set tangents to linear. /// private sealed class LinearTangentMode : EditTangentOptionBase { @@ -107,13 +107,13 @@ namespace FlaxEditor.CustomEditors.Dedicated { SetKeyframeLinear(spline, index); - // change the selection to tangent parent (a spline point / keyframe) + // Change the selection to tangent parent (a spline point / keyframe) Editor.SetSelectSplinePointNode(spline, index); } } /// - /// Edit curve options to align tangents of selected spline + /// Edit curve options to align tangents of selected spline. /// private sealed class AlignedTangentMode : EditTangentOptionBase { @@ -168,8 +168,7 @@ namespace FlaxEditor.CustomEditors.Dedicated } /// - /// Edit curve options manipulate the curve setting selected point - /// tangent in as smoothed but tangent out as linear + /// Edit curve options manipulate the curve setting selected point tangent in as smoothed but tangent out as linear. /// private sealed class SmoothInTangentMode : EditTangentOptionBase { @@ -182,8 +181,7 @@ namespace FlaxEditor.CustomEditors.Dedicated } /// - /// Edit curve options manipulate the curve setting selected point - /// tangent in as linear but tangent out as smoothed + /// Edit curve options manipulate the curve setting selected point tangent in as linear but tangent out as smoothed. /// private sealed class SmoothOutTangentMode : EditTangentOptionBase { @@ -219,7 +217,8 @@ namespace FlaxEditor.CustomEditors.Dedicated var enabled = EnabledInHierarchy && tab.EnabledInHierarchy; var style = FlaxEngine.GUI.Style.Current; var size = Size; - var textHeight = 16.0f; + var textHeight = 30.0f; + // Make icons smaller when tabs get thinner var iconSize = size.Y - textHeight; var iconRect = new Rectangle((Width - iconSize) / 2, 0, iconSize, iconSize); if (tab._mirrorIcon) @@ -230,8 +229,11 @@ namespace FlaxEditor.CustomEditors.Dedicated var color = style.Foreground; if (!enabled) color *= 0.6f; + var textRect = new Rectangle(0, size.Y - textHeight, size.X, textHeight); + Render2D.PushClip(new Rectangle(Float2.Zero, Size)); Render2D.DrawSprite(tab._customIcon, iconRect, color); - Render2D.DrawText(style.FontMedium, tab._customText, new Rectangle(0, iconSize, size.X, textHeight), color, TextAlignment.Center, TextAlignment.Center); + Render2D.DrawText(style.FontMedium, tab._customText, textRect, color, TextAlignment.Center, TextAlignment.Center, TextWrapping.WrapWords, 0.65f); + Render2D.PopClip(); } } @@ -291,18 +293,21 @@ namespace FlaxEditor.CustomEditors.Dedicated return; _selectedSpline = spline; - layout.Space(10); - var tabSize = 46; + //var tabSize = 46; + var tabSize = 60; var icons = Editor.Instance.Icons; - layout.Header("Selected spline point"); + var group = layout.Group("Selected Point"); _selectedPointsTabs = new Tabs { Height = tabSize, TabsSize = new Float2(tabSize), AutoTabsSize = true, - Parent = layout.ContainerControl, + Parent = group.ContainerControl, }; + // Move the group above the group containing spline points + group.Control.IndexInParent = 3; + _linearTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedLinear, "Linear", icons.SplineLinear64)); _freeTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedFree, "Free", icons.SplineFree64)); _alignedTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedAligned, "Aligned", icons.SplineAligned64)); @@ -310,13 +315,13 @@ namespace FlaxEditor.CustomEditors.Dedicated _smoothOutTangentTab = _selectedPointsTabs.AddTab(new IconTab(OnSetSelectedSmoothOut, "Smooth Out", icons.SplineSmoothIn64, true)); _selectedPointsTabs.SelectedTabIndex = -1; - layout.Header("All spline points"); + group = layout.Group("All Points"); _allPointsTabs = new Tabs { Height = tabSize, TabsSize = new Float2(tabSize), AutoTabsSize = true, - Parent = layout.ContainerControl, + Parent = group.ContainerControl, }; _setLinearAllTangentsTab = _allPointsTabs.AddTab(new IconTab(OnSetTangentsLinear, "Set Linear Tangents", icons.SplineLinear64)); _setSmoothAllTangentsTab = _allPointsTabs.AddTab(new IconTab(OnSetTangentsSmooth, "Set Smooth Tangents", icons.SplineAligned64)); diff --git a/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs b/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs index 614d2c160..1628f69ec 100644 --- a/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/UIControlEditor.cs @@ -690,7 +690,7 @@ namespace FlaxEditor.CustomEditors.Dedicated return grid; } - private CustomElementsContainer UniformPanelCapsuleForObjectWithText(LayoutElementsContainer el, string text, ValueContainer values, Color borderColor, out FloatValueBox valueBox) + private CustomElementsContainer UniformPanelCapsuleForObjectWithText(LayoutElementsContainer el, string text, ValueContainer values, Color highlightColor, out FloatValueBox valueBox) { valueBox = null; var grid = UniformGridTwoByOne(el); @@ -701,8 +701,8 @@ namespace FlaxEditor.CustomEditors.Dedicated { valueBox = floatEditorElement.ValueBox; var back = FlaxEngine.GUI.Style.Current.TextBoxBackground; - valueBox.BorderColor = Color.Lerp(borderColor, back, ActorTransformEditor.AxisGreyOutFactor); - valueBox.BorderSelectedColor = borderColor; + valueBox.HighlightColor = highlightColor; + valueBox.BorderSelectedColor = highlightColor; } return grid; } diff --git a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs index 02f5ca7ec..131a9b10a 100644 --- a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs @@ -14,22 +14,17 @@ namespace FlaxEditor.CustomEditors.Editors /// /// The X axis color. /// - public static Color AxisColorX = new Color(1.0f, 0.0f, 0.02745f, 1.0f); + public static Color AxisColorX = new Color(0.8f, 0.0f, 0.027f, 1.0f); /// /// The Y axis color. /// - public static Color AxisColorY = new Color(0.239215f, 1.0f, 0.047058f, 1.0f); + public static Color AxisColorY = new Color(0.239215f, 0.65f, 0.047058f, 1.0f); /// /// The Z axis color. /// - public static Color AxisColorZ = new Color(0.0f, 0.0235294f, 1.0f, 1.0f); - - /// - /// The axes colors grey out scale when input field is not focused. - /// - public static float AxisGreyOutFactor = 0.6f; + public static Color AxisColorZ = new Color(0.0f, 0.42352f, 0.8f, 1.0f); /// /// Custom editor for actor position property. @@ -43,18 +38,20 @@ namespace FlaxEditor.CustomEditors.Editors base.Initialize(layout); if (XElement.ValueBox.Parent is UniformGridPanel ug) + { + ug.SlotPadding = new Margin(3.0f, 0.0f, 0.0f, 0.0f); CheckLayout(ug); + } // Override colors - var back = FlaxEngine.GUI.Style.Current.TextBoxBackground; - XElement.ValueBox.BorderColor = Color.Lerp(AxisColorX, back, AxisGreyOutFactor); XElement.ValueBox.BorderSelectedColor = AxisColorX; - XElement.ValueBox.Category = Utils.ValueCategory.Distance; - YElement.ValueBox.BorderColor = Color.Lerp(AxisColorY, back, AxisGreyOutFactor); YElement.ValueBox.BorderSelectedColor = AxisColorY; - YElement.ValueBox.Category = Utils.ValueCategory.Distance; - ZElement.ValueBox.BorderColor = Color.Lerp(AxisColorZ, back, AxisGreyOutFactor); ZElement.ValueBox.BorderSelectedColor = AxisColorZ; + XElement.ValueBox.HighlightColor = AxisColorX; + XElement.ValueBox.Category = Utils.ValueCategory.Distance; + YElement.ValueBox.HighlightColor = AxisColorY; + YElement.ValueBox.Category = Utils.ValueCategory.Distance; + ZElement.ValueBox.HighlightColor = AxisColorZ; ZElement.ValueBox.Category = Utils.ValueCategory.Distance; } } @@ -71,18 +68,20 @@ namespace FlaxEditor.CustomEditors.Editors base.Initialize(layout); if (XElement.ValueBox.Parent is UniformGridPanel ug) + { + ug.SlotPadding = new Margin(3.0f, 0.0f, 0.0f, 0.0f); CheckLayout(ug); + } // Override colors - var back = FlaxEngine.GUI.Style.Current.TextBoxBackground; - XElement.ValueBox.BorderColor = Color.Lerp(AxisColorX, back, AxisGreyOutFactor); XElement.ValueBox.BorderSelectedColor = AxisColorX; - XElement.ValueBox.Category = Utils.ValueCategory.Angle; - YElement.ValueBox.BorderColor = Color.Lerp(AxisColorY, back, AxisGreyOutFactor); YElement.ValueBox.BorderSelectedColor = AxisColorY; - YElement.ValueBox.Category = Utils.ValueCategory.Angle; - ZElement.ValueBox.BorderColor = Color.Lerp(AxisColorZ, back, AxisGreyOutFactor); ZElement.ValueBox.BorderSelectedColor = AxisColorZ; + XElement.ValueBox.HighlightColor = AxisColorX; + XElement.ValueBox.Category = Utils.ValueCategory.Angle; + YElement.ValueBox.HighlightColor = AxisColorY; + YElement.ValueBox.Category = Utils.ValueCategory.Angle; + ZElement.ValueBox.HighlightColor = AxisColorZ; ZElement.ValueBox.Category = Utils.ValueCategory.Angle; } } @@ -129,17 +128,19 @@ namespace FlaxEditor.CustomEditors.Editors } if (XElement.ValueBox.Parent is UniformGridPanel ug) + { + ug.SlotPadding = new Margin(3.0f, 0.0f, 0.0f, 0.0f); CheckLayout(ug); + } // Override colors var back = FlaxEngine.GUI.Style.Current.TextBoxBackground; - var grayOutFactor = 0.6f; - XElement.ValueBox.BorderColor = Color.Lerp(AxisColorX, back, grayOutFactor); XElement.ValueBox.BorderSelectedColor = AxisColorX; - YElement.ValueBox.BorderColor = Color.Lerp(AxisColorY, back, grayOutFactor); YElement.ValueBox.BorderSelectedColor = AxisColorY; - ZElement.ValueBox.BorderColor = Color.Lerp(AxisColorZ, back, grayOutFactor); ZElement.ValueBox.BorderSelectedColor = AxisColorZ; + XElement.ValueBox.HighlightColor = AxisColorX; + YElement.ValueBox.HighlightColor = AxisColorY; + ZElement.ValueBox.HighlightColor = AxisColorZ; } /// diff --git a/Source/Editor/CustomEditors/Elements/Container/GroupElement.cs b/Source/Editor/CustomEditors/Elements/Container/GroupElement.cs index 055c6a29d..a3397d10a 100644 --- a/Source/Editor/CustomEditors/Elements/Container/GroupElement.cs +++ b/Source/Editor/CustomEditors/Elements/Container/GroupElement.cs @@ -37,25 +37,35 @@ namespace FlaxEditor.CustomEditors.Elements public override ContainerControl ContainerControl => Panel; /// - /// Adds utility settings button to the group header. + /// Add utility settings button to the group header. /// /// The created control. public Image AddSettingsButton() + { + return AddHeaderButton("Settings", 0, Style.Current.Settings); + } + + /// + /// Adds a button to the group header. + /// + /// The created control. + public Image AddHeaderButton(string tooltipText, float xOffset, SpriteHandle sprite) { var style = Style.Current; + const float padding = 2.0f; var settingsButtonSize = Panel.HeaderHeight; Panel.HeaderTextMargin = Panel.HeaderTextMargin with { Right = settingsButtonSize + Utilities.Constants.UIMargin }; ; return new Image { - TooltipText = "Settings", + TooltipText = tooltipText, AutoFocus = true, AnchorPreset = AnchorPresets.TopRight, Parent = Panel, - Bounds = new Rectangle(Panel.Width - settingsButtonSize, 0, settingsButtonSize, settingsButtonSize), + Bounds = new Rectangle(Panel.Width - settingsButtonSize - xOffset, padding * 0.5f, settingsButtonSize - padding, settingsButtonSize - padding), IsScrollable = false, Color = style.ForegroundGrey, Margin = new Margin(1), - Brush = new SpriteBrush(style.Settings), + Brush = new SpriteBrush(sprite), }; } } diff --git a/Source/Editor/CustomEditors/GUI/PropertiesList.cs b/Source/Editor/CustomEditors/GUI/PropertiesList.cs index 3847a8f36..f430bae06 100644 --- a/Source/Editor/CustomEditors/GUI/PropertiesList.cs +++ b/Source/Editor/CustomEditors/GUI/PropertiesList.cs @@ -70,9 +70,9 @@ namespace FlaxEditor.CustomEditors.GUI UpdateSplitRect(); } - private void AutoSizeSplitter() + private void AutoSizeSplitter(bool ignoreCustomSplitterValue = false) { - if (_hasCustomSplitterValue || !Editor.Instance.Options.Options.Interface.AutoSizePropertiesPanelSplitter) + if (_hasCustomSplitterValue && !ignoreCustomSplitterValue) return; Font font = Style.Current.FontMedium; @@ -178,6 +178,21 @@ namespace FlaxEditor.CustomEditors.GUI return base.OnMouseDown(location, button); } + /// + public override bool OnMouseDoubleClick(Float2 location, MouseButton button) + { + if (button == MouseButton.Left && _splitterRect.Contains(location)) + { + if (_splitterClicked) + EndTracking(); + + AutoSizeSplitter(true); + return true; + } + + return base.OnMouseDoubleClick(location, button); + } + /// public override bool OnMouseUp(Float2 location, MouseButton button) { @@ -220,7 +235,8 @@ namespace FlaxEditor.CustomEditors.GUI // Refresh UpdateSplitRect(); PerformLayout(true); - AutoSizeSplitter(); + if (Editor.Instance.Options.Options.Interface.AutoSizePropertiesPanelSplitter) + AutoSizeSplitter(); } /// diff --git a/Source/Editor/Editor.Build.cs b/Source/Editor/Editor.Build.cs index e75954909..3fd8ea98c 100644 --- a/Source/Editor/Editor.Build.cs +++ b/Source/Editor/Editor.Build.cs @@ -61,6 +61,7 @@ public class Editor : EditorModule options.PrivateDependencies.Add("Renderer"); options.PrivateDependencies.Add("TextureTool"); options.PrivateDependencies.Add("Particles"); + options.PrivateDependencies.Add("Terrain"); var platformToolsRoot = Path.Combine(FolderPath, "Cooker", "Platform"); var platformToolsRootExternal = Path.Combine(Globals.EngineRoot, "Source", "Platforms"); diff --git a/Source/Editor/Editor.cpp b/Source/Editor/Editor.cpp index 58739917e..c08648505 100644 --- a/Source/Editor/Editor.cpp +++ b/Source/Editor/Editor.cpp @@ -526,6 +526,23 @@ int32 Editor::LoadProduct() return 12; } + // Get the last opened project path + String localCachePath; + FileSystem::GetSpecialFolderPath(SpecialFolder::AppData, localCachePath); + String editorConfigPath = localCachePath / TEXT("Flax"); + String lastProjectSettingPath = editorConfigPath / TEXT("LastProject.txt"); + if (!FileSystem::DirectoryExists(editorConfigPath)) + FileSystem::CreateDirectory(editorConfigPath); + String lastProjectPath; + if (FileSystem::FileExists(lastProjectSettingPath)) + File::ReadAllText(lastProjectSettingPath, lastProjectPath); + if (!FileSystem::DirectoryExists(lastProjectPath)) + lastProjectPath = String::Empty; + + // Try to open the last project when requested + if (projectPath.IsEmpty() && CommandLine::Options.LastProject.IsTrue() && !lastProjectPath.IsEmpty()) + projectPath = lastProjectPath; + // Missing project case if (projectPath.IsEmpty()) { @@ -541,7 +558,7 @@ int32 Editor::LoadProduct() Array files; if (FileSystem::ShowOpenFileDialog( nullptr, - StringView::Empty, + lastProjectPath, TEXT("Project files (*.flaxproj)\0*.flaxproj\0All files (*.*)\0*.*\0"), false, TEXT("Select project to open in Editor"), @@ -625,6 +642,10 @@ int32 Editor::LoadProduct() } } + // Update the last opened project path + if (lastProjectPath.Compare(Project->ProjectFolderPath) != 0) + File::WriteAllText(lastProjectSettingPath, Project->ProjectFolderPath, Encoding::UTF8); + return 0; } diff --git a/Source/Editor/EditorAssets.cs b/Source/Editor/EditorAssets.cs index d9b3d7f18..447dc3375 100644 --- a/Source/Editor/EditorAssets.cs +++ b/Source/Editor/EditorAssets.cs @@ -69,6 +69,11 @@ namespace FlaxEditor /// public static string WindowIcon = "Editor/EditorIcon"; + /// + /// The material used for the HS color wheel. + /// + public static string HSWheelMaterial = "Editor/HSWheel"; + /// /// The window icons font. /// diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs index 2d6a3882b..ab002505f 100644 --- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs +++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs @@ -1,10 +1,11 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System.Collections.Generic; using FlaxEditor.GUI.Input; +using FlaxEditor.GUI.Tabs; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Json; -using System.Collections.Generic; namespace FlaxEditor.GUI.Dialogs { @@ -25,15 +26,16 @@ namespace FlaxEditor.GUI.Dialogs /// public class ColorPickerDialog : Dialog, IColorPickerDialog { - private const float ButtonsWidth = 60.0f; private const float PickerMargin = 6.0f; - private const float EyedropperMargin = 8.0f; - private const float RGBAMargin = 12.0f; - private const float HSVMargin = 0.0f; private const float ChannelsMargin = 4.0f; private const float ChannelTextWidth = 12.0f; private const float SavedColorButtonWidth = 20.0f; private const float SavedColorButtonHeight = 20.0f; + private const float TabHeight = 20; + private const float ValueBoxesWidth = 100.0f; + private const float HSVRGBTextWidth = 15.0f; + private const float ColorPreviewHeight = 50.0f; + private const int SavedColorsAmount = 10; private Color _initialValue; private Color _value; @@ -45,16 +47,19 @@ namespace FlaxEditor.GUI.Dialogs private ColorValueBox.ColorPickerClosedEvent _onClosed; private ColorSelectorWithSliders _cSelector; + private Tabs.Tabs _hsvRGBTabs; + private Tab _RGBTab; + private Panel _rgbPanel; private FloatValueBox _cRed; private FloatValueBox _cGreen; private FloatValueBox _cBlue; - private FloatValueBox _cAlpha; + private Tab _hsvTab; + private Panel _hsvPanel; private FloatValueBox _cHue; private FloatValueBox _cSaturation; private FloatValueBox _cValue; private TextBox _cHex; - private Button _cCancel; - private Button _cOK; + private FloatValueBox _cAlpha; private Button _cEyedropper; private List _savedColors = new List(); @@ -124,97 +129,110 @@ namespace FlaxEditor.GUI.Dialogs _savedColors = JsonSerializer.Deserialize>(savedColors); // Selector - _cSelector = new ColorSelectorWithSliders(180, 18) + _cSelector = new ColorSelectorWithSliders(180, 21) { Location = new Float2(PickerMargin, PickerMargin), Parent = this }; _cSelector.ColorChanged += x => SelectedColor = x; - // Red - _cRed = new FloatValueBox(0, _cSelector.Right + PickerMargin + RGBAMargin + ChannelTextWidth, PickerMargin, 100, 0, float.MaxValue, 0.001f) + _hsvRGBTabs = new Tabs.Tabs { - Parent = this + Location = new Float2(_cSelector.Right + 30.0f, PickerMargin), + TabsTextHorizontalAlignment = TextAlignment.Center, + Width = ValueBoxesWidth + HSVRGBTextWidth * 2.0f + ChannelsMargin * 2.0f, + Height = (FloatValueBox.DefaultHeight + ChannelsMargin) * 4 + ChannelsMargin, + Parent = this, + }; + _hsvRGBTabs.TabsSize = new Float2(_hsvRGBTabs.Width * 0.5f, TabHeight); + _hsvRGBTabs.SelectedTabChanged += SelectedTabChanged; + + // RGB Tab + _RGBTab = _hsvRGBTabs.AddTab(new Tab("RGB")); + _rgbPanel = new Panel(ScrollBars.Vertical) + { + AnchorPreset = AnchorPresets.StretchAll, + Offsets = Margin.Zero, + Parent = _RGBTab, + }; + + // HSV Tab + _hsvTab = _hsvRGBTabs.AddTab(new Tab("HSV")); + _hsvPanel = new Panel(ScrollBars.Vertical) + { + AnchorPreset = AnchorPresets.StretchAll, + Offsets = Margin.Zero, + Parent = _hsvTab, + }; + + // Red + _cRed = new FloatValueBox(0, HSVRGBTextWidth + ChannelsMargin, PickerMargin, ValueBoxesWidth, 0, float.MaxValue, 0.001f) + { + Parent = _rgbPanel, }; _cRed.ValueChanged += OnRGBAChanged; // Green _cGreen = new FloatValueBox(0, _cRed.X, _cRed.Bottom + ChannelsMargin, _cRed.Width, 0, float.MaxValue, 0.001f) { - Parent = this + Parent = _rgbPanel, }; _cGreen.ValueChanged += OnRGBAChanged; // Blue _cBlue = new FloatValueBox(0, _cRed.X, _cGreen.Bottom + ChannelsMargin, _cRed.Width, 0, float.MaxValue, 0.001f) { - Parent = this + Parent = _rgbPanel, }; _cBlue.ValueChanged += OnRGBAChanged; - // Alpha - _cAlpha = new FloatValueBox(0, _cRed.X, _cBlue.Bottom + ChannelsMargin, _cRed.Width, 0, float.MaxValue, 0.001f) - { - Parent = this - }; - _cAlpha.ValueChanged += OnRGBAChanged; - // Hue - _cHue = new FloatValueBox(0, PickerMargin + HSVMargin + ChannelTextWidth, _cSelector.Bottom + PickerMargin, 100, 0, 360) + _cHue = new FloatValueBox(0, HSVRGBTextWidth + ChannelsMargin, PickerMargin, ValueBoxesWidth) { - Parent = this + Parent = _hsvPanel, + Category = Utils.ValueCategory.Angle, }; _cHue.ValueChanged += OnHSVChanged; // Saturation _cSaturation = new FloatValueBox(0, _cHue.X, _cHue.Bottom + ChannelsMargin, _cHue.Width, 0, 100.0f, 0.1f) { - Parent = this + Parent = _hsvPanel, }; _cSaturation.ValueChanged += OnHSVChanged; // Value _cValue = new FloatValueBox(0, _cHue.X, _cSaturation.Bottom + ChannelsMargin, _cHue.Width, 0, float.MaxValue, 0.1f) { - Parent = this + Parent = _hsvPanel, }; _cValue.ValueChanged += OnHSVChanged; - // Set valid dialog size based on UI content - _dialogSize = Size = new Float2(_cRed.Right + PickerMargin, 300); + // Alpha + _cAlpha = new FloatValueBox(0, _hsvRGBTabs.Left + HSVRGBTextWidth + ChannelsMargin, _hsvRGBTabs.Bottom + ChannelsMargin * 4.0f, ValueBoxesWidth, 0, float.MaxValue, 0.001f) + { + Parent = this, + }; + _cAlpha.ValueChanged += OnRGBAChanged; // Hex - const float hexTextBoxWidth = 80; - _cHex = new TextBox(false, Width - hexTextBoxWidth - PickerMargin, _cSelector.Bottom + PickerMargin, hexTextBoxWidth) + _cHex = new TextBox(false, _hsvRGBTabs.Left + HSVRGBTextWidth + ChannelsMargin, _cAlpha.Bottom + ChannelsMargin * 2.0f, ValueBoxesWidth) { - Parent = this + Parent = this, }; _cHex.EditEnd += OnHexChanged; - // Cancel - _cCancel = new Button(Width - ButtonsWidth - PickerMargin, Height - Button.DefaultHeight - PickerMargin, ButtonsWidth) - { - Text = "Cancel", - Parent = this - }; - _cCancel.Clicked += OnCancel; - - // OK - _cOK = new Button(_cCancel.Left - ButtonsWidth - PickerMargin, _cCancel.Y, ButtonsWidth) - { - Text = "Ok", - Parent = this - }; - _cOK.Clicked += OnSubmit; + // Set valid dialog size based on UI content + _dialogSize = Size = new Float2(_hsvRGBTabs.Right + PickerMargin, _cHex.Bottom + 40.0f + ColorPreviewHeight + PickerMargin); // Create saved color buttons - CreateAllSaveButtons(); + CreateAllSavedColorsButtons(); // Eyedropper button var style = Style.Current; - _cEyedropper = new Button(_cOK.X - EyedropperMargin, _cHex.Bottom + PickerMargin) + _cEyedropper = new Button(_cSelector.BottomLeft.X, _cSelector.BottomLeft.Y - 25.0f, 25.0f, 25.0f) { - TooltipText = "Eyedropper tool to pick a color directly from the screen", + TooltipText = "Eyedropper tool to pick a color directly from the screen.", BackgroundBrush = new SpriteBrush(Editor.Instance.Icons.Search32), BackgroundColor = style.Foreground, BackgroundColorHighlighted = style.Foreground.RGBMultiplied(0.9f), @@ -223,9 +241,6 @@ namespace FlaxEditor.GUI.Dialogs Parent = this, }; _cEyedropper.Clicked += OnEyedropStart; - _cEyedropper.Height = (_cValue.Bottom - _cEyedropper.Y) * 0.5f; - _cEyedropper.Width = _cEyedropper.Height; - _cEyedropper.X -= _cEyedropper.Width; // Set initial color SelectedColor = initialValue; @@ -244,7 +259,7 @@ namespace FlaxEditor.GUI.Dialogs } } - // Set color of button to current value; + // Set color of button to current value button.BackgroundColor = _value; button.BackgroundColorHighlighted = _value; button.BackgroundColorSelected = _value.RGBMultiplied(0.8f); @@ -256,10 +271,10 @@ namespace FlaxEditor.GUI.Dialogs var savedColors = JsonSerializer.Serialize(_savedColors, typeof(List)); Editor.Instance.ProjectCache.SetCustomData("ColorPickerSavedColors", savedColors); - // create new + button - if (_savedColorButtons.Count < 8) + // Create new + button + if (_savedColorButtons.Count < SavedColorsAmount) { - var savedColorButton = new Button(PickerMargin * (_savedColorButtons.Count + 1) + SavedColorButtonWidth * _savedColorButtons.Count, Height - SavedColorButtonHeight - PickerMargin, SavedColorButtonWidth, SavedColorButtonHeight) + var savedColorButton = new Button(PickerMargin * (_savedColorButtons.Count + 1) + SavedColorButtonWidth * _savedColorButtons.Count, _cHex.Bottom + 40.0f + ColorPreviewHeight * 0.5f, SavedColorButtonWidth, SavedColorButtonHeight) { Text = "+", Parent = this, @@ -276,11 +291,32 @@ namespace FlaxEditor.GUI.Dialogs } } + private void SelectedTabChanged(Tabs.Tabs tabs) + { + if (_rgbPanel == null || _hsvPanel == null) + return; + + switch (tabs.SelectedTabIndex) + { + // RGB + case 0: + _rgbPanel.Visible = true; + _hsvPanel.Visible = false; + break; + // HSV + case 1: + _rgbPanel.Visible = false; + _hsvPanel.Visible = true; + break; + } + } + private void OnColorPicked(Color32 colorPicked) { if (_activeEyedropper) { _activeEyedropper = false; + _cEyedropper.BackgroundColor = _cEyedropper.BackgroundColorHighlighted = Style.Current.Foreground; SelectedColor = colorPicked; ScreenUtilities.PickColorDone -= OnColorPicked; } @@ -289,6 +325,7 @@ namespace FlaxEditor.GUI.Dialogs private void OnEyedropStart() { _activeEyedropper = true; + _cEyedropper.BackgroundColor = _cEyedropper.BackgroundColorHighlighted = Style.Current.BackgroundHighlighted; ScreenUtilities.PickColor(); ScreenUtilities.PickColorDone += OnColorPicked; } @@ -306,6 +343,7 @@ namespace FlaxEditor.GUI.Dialogs if (_disableEvents) return; + _cHue.Value = Mathf.Wrap(_cHue.Value, 0f, 360f); SelectedColor = Color.FromHSV(_cHue.Value, _cSaturation.Value / 100.0f, _cValue.Value / 100.0f, _cAlpha.Value); } @@ -339,64 +377,76 @@ namespace FlaxEditor.GUI.Dialogs base.Draw(); - // RGBA - var rgbaR = new Rectangle(_cRed.Left - ChannelTextWidth, _cRed.Y, 10000, _cRed.Height); - Render2D.DrawText(style.FontMedium, "R", rgbaR, textColor, TextAlignment.Near, TextAlignment.Center); - rgbaR.Location.Y = _cGreen.Y; - Render2D.DrawText(style.FontMedium, "G", rgbaR, textColor, TextAlignment.Near, TextAlignment.Center); - rgbaR.Location.Y = _cBlue.Y; - Render2D.DrawText(style.FontMedium, "B", rgbaR, textColor, TextAlignment.Near, TextAlignment.Center); - rgbaR.Location.Y = _cAlpha.Y; - Render2D.DrawText(style.FontMedium, "A", rgbaR, textColor, TextAlignment.Near, TextAlignment.Center); + switch (_hsvRGBTabs.SelectedTabIndex) + { + // RGB + case 0: + var rgbRect = new Rectangle(_hsvRGBTabs.Left + PickerMargin, _hsvRGBTabs.Top + TabHeight + PickerMargin, 10000, _cRed.Height); + Render2D.DrawText(style.FontMedium, "R", rgbRect, textColor, TextAlignment.Near, TextAlignment.Center); + rgbRect.Location.Y += _cRed.Height + ChannelsMargin; + Render2D.DrawText(style.FontMedium, "G", rgbRect, textColor, TextAlignment.Near, TextAlignment.Center); + rgbRect.Location.Y += _cRed.Height + ChannelsMargin; + Render2D.DrawText(style.FontMedium, "B", rgbRect, textColor, TextAlignment.Near, TextAlignment.Center); + break; + // HSV + case 1: + // Left + var hsvLeftRect = new Rectangle(_hsvRGBTabs.Left + PickerMargin, _hsvRGBTabs.Top + TabHeight + PickerMargin, 10000, _cHue.Height); + Render2D.DrawText(style.FontMedium, "H", hsvLeftRect, textColor, TextAlignment.Near, TextAlignment.Center); + hsvLeftRect.Location.Y += _cHue.Height + ChannelsMargin; + Render2D.DrawText(style.FontMedium, "S", hsvLeftRect, textColor, TextAlignment.Near, TextAlignment.Center); + hsvLeftRect.Location.Y += _cHue.Height + ChannelsMargin; + Render2D.DrawText(style.FontMedium, "V", hsvLeftRect, textColor, TextAlignment.Near, TextAlignment.Center); - // HSV left - var hsvHl = new Rectangle(_cHue.Left - ChannelTextWidth, _cHue.Y, 10000, _cHue.Height); - Render2D.DrawText(style.FontMedium, "H", hsvHl, textColor, TextAlignment.Near, TextAlignment.Center); - hsvHl.Location.Y = _cSaturation.Y; - Render2D.DrawText(style.FontMedium, "S", hsvHl, textColor, TextAlignment.Near, TextAlignment.Center); - hsvHl.Location.Y = _cValue.Y; - Render2D.DrawText(style.FontMedium, "V", hsvHl, textColor, TextAlignment.Near, TextAlignment.Center); + // Right + var hsvRightRect = new Rectangle(_hsvRGBTabs.Right - HSVRGBTextWidth, _hsvRGBTabs.Top + TabHeight + PickerMargin, ChannelTextWidth, _cHue.Height); + Render2D.DrawText(style.FontMedium, "°", hsvRightRect, textColor, TextAlignment.Near, TextAlignment.Center); + hsvRightRect.Location.Y += _cHue.Height + ChannelsMargin; + Render2D.DrawText(style.FontMedium, "%", hsvRightRect, textColor, TextAlignment.Near, TextAlignment.Center); + hsvRightRect.Location.Y += _cHue.Height + ChannelsMargin; + Render2D.DrawText(style.FontMedium, "%", hsvRightRect, textColor, TextAlignment.Near, TextAlignment.Center); + break; + } - // HSV right - var hsvHr = new Rectangle(_cHue.Right + 2, _cHue.Y, 10000, _cHue.Height); - Render2D.DrawText(style.FontMedium, "°", hsvHr, textColor, TextAlignment.Near, TextAlignment.Center); - hsvHr.Location.Y = _cSaturation.Y; - Render2D.DrawText(style.FontMedium, "%", hsvHr, textColor, TextAlignment.Near, TextAlignment.Center); - hsvHr.Location.Y = _cValue.Y; - Render2D.DrawText(style.FontMedium, "%", hsvHr, textColor, TextAlignment.Near, TextAlignment.Center); + // A + var alphaHexRect = new Rectangle(_hsvRGBTabs.Left + PickerMargin, _cAlpha.Top, ChannelTextWidth, _cAlpha.Height); + Render2D.DrawText(style.FontMedium, "A", alphaHexRect, textColor, TextAlignment.Near, TextAlignment.Center); // Hex - var hex = new Rectangle(_cHex.Left - 26, _cHex.Y, 10000, _cHex.Height); - Render2D.DrawText(style.FontMedium, "Hex", hex, textColor, TextAlignment.Near, TextAlignment.Center); + alphaHexRect.Y += _cAlpha.Height + ChannelsMargin * 2.0f; + alphaHexRect.X -= 5.0f; // "Hex" is two characters wider than the other labels so we need to adjust for that + Render2D.DrawText(style.FontMedium, "Hex", alphaHexRect, textColor, TextAlignment.Far, TextAlignment.Center); // Color difference - var newRect = new Rectangle(_cOK.X - 3, _cHex.Bottom + PickerMargin, 130, 0); - newRect.Size.Y = 50; - Render2D.FillRectangle(newRect, Color.White); - var smallRectSize = 10; - var numHor = Mathf.FloorToInt(newRect.Width / smallRectSize); - var numVer = Mathf.FloorToInt(newRect.Height / smallRectSize); + var differenceRect = new Rectangle(_hsvRGBTabs.Left, _cHex.Bottom + 40.0f, _hsvRGBTabs.Width, ColorPreviewHeight); + // Draw checkerboard for background of color to help with transparency + Render2D.FillRectangle(differenceRect, Color.White); + var smallRectSize = 10; + var numHor = Mathf.CeilToInt(differenceRect.Width / smallRectSize); + var numVer = Mathf.CeilToInt(differenceRect.Height / smallRectSize); + Render2D.PushClip(differenceRect); for (int i = 0; i < numHor; i++) { for (int j = 0; j < numVer; j++) { if ((i + j) % 2 == 0) { - var rect = new Rectangle(newRect.X + smallRectSize * i, newRect.Y + smallRectSize * j, new Float2(smallRectSize)); + var rect = new Rectangle(differenceRect.X + smallRectSize * i, differenceRect.Y + smallRectSize * j, new Float2(smallRectSize)); Render2D.FillRectangle(rect, Color.Gray); } } } - Render2D.FillRectangle(newRect, _value); + Render2D.PopClip(); + Render2D.FillRectangle(differenceRect, _value); } /// protected override void OnShow() { - // Auto cancel on lost focus + // Apply changes on lost focus #if !PLATFORM_LINUX - ((WindowRootControl)Root).Window.LostFocus += OnWindowLostFocus; + ((WindowRootControl)Root).Window.LostFocus += OnSubmit; #endif base.OnShow(); @@ -409,6 +459,7 @@ namespace FlaxEditor.GUI.Dialogs { // Cancel eye dropping _activeEyedropper = false; + _cEyedropper.BackgroundColor = _cEyedropper.BackgroundColorHighlighted = Style.Current.Foreground; ScreenUtilities.PickColorDone -= OnColorPicked; return true; } @@ -420,9 +471,7 @@ namespace FlaxEditor.GUI.Dialogs public override bool OnMouseUp(Float2 location, MouseButton button) { if (base.OnMouseUp(location, button)) - { return true; - } var child = GetChildAtRecursive(location); if (button == MouseButton.Right && child is Button b && b.Tag is Color c) @@ -484,20 +533,20 @@ namespace FlaxEditor.GUI.Dialogs } _savedColorButtons.Clear(); - CreateAllSaveButtons(); + CreateAllSavedColorsButtons(); // Save new colors var savedColors = JsonSerializer.Serialize(_savedColors, typeof(List)); Editor.Instance.ProjectCache.SetCustomData("ColorPickerSavedColors", savedColors); } - private void CreateAllSaveButtons() + private void CreateAllSavedColorsButtons() { // Create saved color buttons for (int i = 0; i < _savedColors.Count; i++) { var savedColor = _savedColors[i]; - var savedColorButton = new Button(PickerMargin * (i + 1) + SavedColorButtonWidth * i, Height - SavedColorButtonHeight - PickerMargin, SavedColorButtonWidth, SavedColorButtonHeight) + var savedColorButton = new Button(PickerMargin * (i + 1) + SavedColorButtonWidth * i, _cHex.Bottom + 40.0f + ColorPreviewHeight * 0.5f, SavedColorButtonWidth, SavedColorButtonHeight) { Parent = this, Tag = savedColor, @@ -508,9 +557,9 @@ namespace FlaxEditor.GUI.Dialogs savedColorButton.ButtonClicked += OnSavedColorButtonClicked; _savedColorButtons.Add(savedColorButton); } - if (_savedColors.Count < 8) + if (_savedColors.Count < SavedColorsAmount) { - var savedColorButton = new Button(PickerMargin * (_savedColors.Count + 1) + SavedColorButtonWidth * _savedColors.Count, Height - SavedColorButtonHeight - PickerMargin, SavedColorButtonWidth, SavedColorButtonHeight) + var savedColorButton = new Button(PickerMargin * (_savedColors.Count + 1) + SavedColorButtonWidth * _savedColors.Count, _cHex.Bottom + 40.0f + ColorPreviewHeight * 0.5f, SavedColorButtonWidth, SavedColorButtonHeight) { Text = "+", Parent = this, @@ -522,19 +571,6 @@ namespace FlaxEditor.GUI.Dialogs } } - private void OnWindowLostFocus() - { - // Auto apply color on defocus - var autoAcceptColorPickerChange = Editor.Instance.Options.Options.Interface.AutoAcceptColorPickerChange; - if (_useDynamicEditing && _initialValue != _value && _canPassLastChangeEvent && autoAcceptColorPickerChange) - { - _canPassLastChangeEvent = false; - _onChanged?.Invoke(_value, false); - } - - OnCancel(); - } - /// public override void OnSubmit() { @@ -542,6 +578,9 @@ namespace FlaxEditor.GUI.Dialogs return; _disableEvents = true; + // Ensure the cursor is restored + Cursor = CursorType.Default; + // Send color event if modified if (_value != _initialValue) { @@ -558,6 +597,9 @@ namespace FlaxEditor.GUI.Dialogs return; _disableEvents = true; + // Ensure the cursor is restored + Cursor = CursorType.Default; + // Restore color if modified if (_useDynamicEditing && _initialValue != _value && _canPassLastChangeEvent) { diff --git a/Source/Editor/GUI/Dialogs/ColorSelector.cs b/Source/Editor/GUI/Dialogs/ColorSelector.cs index f556e8cd6..3d781a8ec 100644 --- a/Source/Editor/GUI/Dialogs/ColorSelector.cs +++ b/Source/Editor/GUI/Dialogs/ColorSelector.cs @@ -12,6 +12,13 @@ namespace FlaxEditor.GUI.Dialogs /// public class ColorSelector : ContainerControl { + private const String GrayedOutParamName = "GrayedOut"; + + /// + /// Offset value applied to mouse cursor position when the user lets go of wheel or sliders. + /// + protected const float MouseCursorOffset = 6.0f; + /// /// The color. /// @@ -22,9 +29,22 @@ namespace FlaxEditor.GUI.Dialogs /// protected Rectangle _wheelRect; - private readonly SpriteHandle _colorWheelSprite; + private readonly MaterialBase _hsWheelMaterial; private bool _isMouseDownWheel; + private Rectangle wheelDragRect + { + get + { + var hsv = _color.ToHSV(); + float hAngle = hsv.X * Mathf.DegreesToRadians; + float hRadius = hsv.Y * _wheelRect.Width * 0.5f; + var hsPos = new Float2(hRadius * Mathf.Cos(hAngle), -hRadius * Mathf.Sin(hAngle)); + float wheelBoxSize = IsSliding ? 9.0f : 5.0f; + return new Rectangle(hsPos - (wheelBoxSize * 0.5f) + _wheelRect.Center, new Float2(wheelBoxSize)); + } + } + /// /// Occurs when selected color gets changed. /// @@ -78,7 +98,8 @@ namespace FlaxEditor.GUI.Dialogs { AutoFocus = true; - _colorWheelSprite = Editor.Instance.Icons.ColorWheel128; + _hsWheelMaterial = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.HSWheelMaterial); + _hsWheelMaterial = _hsWheelMaterial.CreateVirtualInstance(); _wheelRect = new Rectangle(0, 0, wheelSize, wheelSize); } @@ -165,17 +186,19 @@ namespace FlaxEditor.GUI.Dialogs { base.Draw(); - var hsv = _color.ToHSV(); bool enabled = EnabledInHierarchy; + _hsWheelMaterial.SetParameterValue(GrayedOutParamName, enabled ? 1.0f : 0.5f); + Render2D.DrawMaterial(_hsWheelMaterial, _wheelRect, enabled ? Color.White : Color.Gray); + // Wheel float boxExpand = (2.0f * 4.0f / 128.0f) * _wheelRect.Width; - Render2D.DrawSprite(_colorWheelSprite, _wheelRect.MakeExpanded(boxExpand), enabled ? Color.White : Color.Gray); - float hAngle = hsv.X * Mathf.DegreesToRadians; - float hRadius = hsv.Y * _wheelRect.Width * 0.5f; - var hsPos = new Float2(hRadius * Mathf.Cos(hAngle), -hRadius * Mathf.Sin(hAngle)); - const float wheelBoxSize = 4.0f; - Render2D.DrawRectangle(new Rectangle(hsPos - (wheelBoxSize * 0.5f) + _wheelRect.Center, new Float2(wheelBoxSize)), _isMouseDownWheel ? Color.Gray : Color.Black); + Render2D.DrawMaterial(_hsWheelMaterial, _wheelRect, enabled ? Color.White : Color.Gray); + Float3 hsv = _color.ToHSV(); + Color hsColor = Color.FromHSV(new Float3(hsv.X, hsv.Y, 1)); + Rectangle wheelRect = wheelDragRect; + Render2D.FillRectangle(wheelRect, hsColor); + Render2D.DrawRectangle(wheelRect, Color.Black, _isMouseDownWheel ? 2.0f : 1.0f); } /// @@ -202,6 +225,7 @@ namespace FlaxEditor.GUI.Dialogs if (!_isMouseDownWheel) { _isMouseDownWheel = true; + Cursor = CursorType.Hidden; StartMouseCapture(); SlidingStart?.Invoke(); } @@ -218,6 +242,10 @@ namespace FlaxEditor.GUI.Dialogs if (button == MouseButton.Left && _isMouseDownWheel) { EndMouseCapture(); + // Make the cursor appear where the user expects it to be (position of selection rectangle) + Rectangle dragRect = wheelDragRect; + Root.MousePosition = dragRect.Center + MouseCursorOffset; + Cursor = CursorType.Default; EndSliding(); return true; } @@ -246,10 +274,10 @@ namespace FlaxEditor.GUI.Dialogs /// public class ColorSelectorWithSliders : ColorSelector { - private Rectangle _slider1Rect; - private Rectangle _slider2Rect; - private bool _isMouseDownSlider1; - private bool _isMouseDownSlider2; + private Rectangle _valueSliderRect; + private Rectangle _alphaSliderRect; + private bool _isMouseDownValueSlider; + private bool _isMouseDownAlphaSlider; /// /// Initializes a new instance of the class. @@ -260,26 +288,26 @@ namespace FlaxEditor.GUI.Dialogs : base(wheelSize) { // Setup dimensions - const float slidersMargin = 8.0f; - _slider1Rect = new Rectangle(wheelSize + slidersMargin, 0, slidersThickness, wheelSize); - _slider2Rect = new Rectangle(_slider1Rect.Right + slidersMargin, _slider1Rect.Y, slidersThickness, _slider1Rect.Height); - Size = new Float2(_slider2Rect.Right, wheelSize); + const float slidersMargin = 10.0f; + _valueSliderRect = new Rectangle(wheelSize + slidersMargin, 0, slidersThickness, wheelSize); + _alphaSliderRect = new Rectangle(_valueSliderRect.Right + slidersMargin * 1.5f, _valueSliderRect.Y, slidersThickness, _valueSliderRect.Height); + Size = new Float2(_alphaSliderRect.Right, wheelSize); } /// protected override void UpdateMouse(ref Float2 location) { - if (_isMouseDownSlider1) + if (_isMouseDownValueSlider) { var hsv = _color.ToHSV(); - hsv.Z = 1.0f - Mathf.Saturate((location.Y - _slider1Rect.Y) / _slider1Rect.Height); + hsv.Z = 1.0f - Mathf.Saturate((location.Y - _valueSliderRect.Y) / _valueSliderRect.Height); Color = Color.FromHSV(hsv, _color.A); } - else if (_isMouseDownSlider2) + else if (_isMouseDownAlphaSlider) { var color = _color; - color.A = 1.0f - Mathf.Saturate((location.Y - _slider2Rect.Y) / _slider2Rect.Height); + color.A = 1.0f - Mathf.Saturate((location.Y - _alphaSliderRect.Y) / _alphaSliderRect.Height); Color = color; } @@ -300,32 +328,61 @@ namespace FlaxEditor.GUI.Dialogs var hs = hsv; hs.Z = 1.0f; Color hsC = Color.FromHSV(hs); - const float slidersOffset = 3.0f; - const float slidersThickness = 4.0f; - // Value - float valueY = _slider2Rect.Height * (1 - hsv.Z); - var valueR = new Rectangle(_slider1Rect.X - slidersOffset, _slider1Rect.Y + valueY - slidersThickness / 2, _slider1Rect.Width + slidersOffset * 2, slidersThickness); - Render2D.FillRectangle(_slider1Rect, hsC, hsC, Color.Black, Color.Black); - Render2D.DrawRectangle(_slider1Rect, _isMouseDownSlider1 ? style.BackgroundSelected : Color.Black); - Render2D.DrawRectangle(valueR, _isMouseDownSlider1 ? Color.White : Color.Gray); + // Value slider + float valueKnobExpand = _isMouseDownValueSlider ? 10.0f : 4.0f; + float valueY = _valueSliderRect.Height * (1 - hsv.Z); + float valueKnobWidth = _valueSliderRect.Width + valueKnobExpand; + float valueKnobHeight = _isMouseDownValueSlider ? 7.0f : 4.0f; + float valueKnobX = _valueSliderRect.X - valueKnobExpand * 0.5f; + float valueKnobY = _valueSliderRect.Y + valueY - valueKnobHeight * 0.5f; + Rectangle valueKnobRect = new Rectangle(valueKnobX, valueKnobY, valueKnobWidth, valueKnobHeight); + Render2D.FillRectangle(_valueSliderRect, hsC, hsC, Color.Black, Color.Black); + // Draw one black and one white border to make the knob visible at any saturation level + Render2D.DrawRectangle(valueKnobRect, Color.White, _isMouseDownValueSlider ? 3.0f : 2.0f); + Render2D.DrawRectangle(valueKnobRect, Color.Black, _isMouseDownValueSlider ? 2.0f : 1.0f); - // Alpha - float alphaY = _slider2Rect.Height * (1 - _color.A); - var alphaR = new Rectangle(_slider2Rect.X - slidersOffset, _slider2Rect.Y + alphaY - slidersThickness / 2, _slider2Rect.Width + slidersOffset * 2, slidersThickness); + // Draw checkerboard pattern as background of alpha slider + Render2D.FillRectangle(_alphaSliderRect, Color.White); + var smallRectSize = _alphaSliderRect.Width / 2.0f; + var numHor = Mathf.CeilToInt(_alphaSliderRect.Width / smallRectSize); + var numVer = Mathf.CeilToInt(_alphaSliderRect.Height / smallRectSize); + Render2D.PushClip(_alphaSliderRect); + for (int i = 0; i < numHor; i++) + { + for (int j = 0; j < numVer; j++) + { + if ((i + j) % 2 == 0) + { + var rect = new Rectangle(_alphaSliderRect.X + smallRectSize * i, _alphaSliderRect.Y + smallRectSize * j, new Float2(smallRectSize)); + Render2D.FillRectangle(rect, Color.Gray); + } + } + } + Render2D.PopClip(); + + // Alpha slider + float alphaKnobExpand = _isMouseDownAlphaSlider ? 10.0f : 4.0f; + float alphaY = _alphaSliderRect.Height * (1 - _color.A); + float alphaKnobWidth = _alphaSliderRect.Width + alphaKnobExpand; + float alphaKnobHeight = _isMouseDownAlphaSlider ? 7.0f : 4.0f; + float alphaKnobX = _alphaSliderRect.X - alphaKnobExpand * 0.5f; + float alphaKnobY = _alphaSliderRect.Y + alphaY - alphaKnobExpand * 0.5f; + Rectangle alphaKnobRect = new Rectangle(alphaKnobX, alphaKnobY, alphaKnobWidth, alphaKnobHeight); var color = _color; - color.A = 1; // Keep slider 2 fill rect from changing color alpha while selecting. - Render2D.FillRectangle(_slider2Rect, color, color, Color.Transparent, Color.Transparent); - Render2D.DrawRectangle(_slider2Rect, _isMouseDownSlider2 ? style.BackgroundSelected : Color.Black); - Render2D.DrawRectangle(alphaR, _isMouseDownSlider2 ? Color.White : Color.Gray); + color.A = 1; // Prevent alpha slider fill from becoming transparent + Render2D.FillRectangle(_alphaSliderRect, color, color, Color.Transparent, Color.Transparent); + // Draw one black and one white border to make the knob visible at any saturation level + Render2D.DrawRectangle(alphaKnobRect, Color.White, _isMouseDownAlphaSlider ? 3.0f : 2.0f); + Render2D.DrawRectangle(alphaKnobRect, Color.Black, _isMouseDownAlphaSlider ? 2.0f : 1.0f); } /// public override void OnLostFocus() { // Clear flags - _isMouseDownSlider1 = false; - _isMouseDownSlider2 = false; + _isMouseDownValueSlider = false; + _isMouseDownAlphaSlider = false; base.OnLostFocus(); } @@ -333,15 +390,17 @@ namespace FlaxEditor.GUI.Dialogs /// public override bool OnMouseDown(Float2 location, MouseButton button) { - if (button == MouseButton.Left && _slider1Rect.Contains(location)) + if (button == MouseButton.Left && _valueSliderRect.Contains(location)) { - _isMouseDownSlider1 = true; + _isMouseDownValueSlider = true; + Cursor = CursorType.Hidden; StartMouseCapture(); UpdateMouse(ref location); } - if (button == MouseButton.Left && _slider2Rect.Contains(location)) + if (button == MouseButton.Left && _alphaSliderRect.Contains(location)) { - _isMouseDownSlider2 = true; + _isMouseDownAlphaSlider = true; + Cursor = CursorType.Hidden; StartMouseCapture(); UpdateMouse(ref location); } @@ -352,10 +411,16 @@ namespace FlaxEditor.GUI.Dialogs /// public override bool OnMouseUp(Float2 location, MouseButton button) { - if (button == MouseButton.Left && (_isMouseDownSlider1 || _isMouseDownSlider2)) + if (button == MouseButton.Left && (_isMouseDownValueSlider || _isMouseDownAlphaSlider)) { - _isMouseDownSlider1 = false; - _isMouseDownSlider2 = false; + // Make the cursor appear where the user expects it to be (center of slider horizontally and slider knob position vertically) + float sliderCenter = _isMouseDownValueSlider ? _valueSliderRect.Center.X : _alphaSliderRect.Center.X; + // Calculate y position based on the slider knob to avoid incrementing value by a small amount when the user repeatedly clicks the slider (f.e. when moving in small steps) + float mouseSliderPosition = _isMouseDownValueSlider ? _valueSliderRect.Y + _valueSliderRect.Height * (1 - _color.ToHSV().Z) + MouseCursorOffset : _alphaSliderRect.Y + _alphaSliderRect.Height * (1 - _color.A) + MouseCursorOffset; + Root.MousePosition = new Float2(sliderCenter + MouseCursorOffset, Mathf.Clamp(mouseSliderPosition, _valueSliderRect.Top, _valueSliderRect.Bottom)); + Cursor = CursorType.Default; + _isMouseDownValueSlider = false; + _isMouseDownAlphaSlider = false; EndMouseCapture(); return true; } @@ -367,8 +432,8 @@ namespace FlaxEditor.GUI.Dialogs public override void OnEndMouseCapture() { // Clear flags - _isMouseDownSlider1 = false; - _isMouseDownSlider2 = false; + _isMouseDownValueSlider = false; + _isMouseDownAlphaSlider = false; base.OnEndMouseCapture(); } diff --git a/Source/Editor/GUI/Input/ValueBox.cs b/Source/Editor/GUI/Input/ValueBox.cs index 88ec9a4ee..c26761683 100644 --- a/Source/Editor/GUI/Input/ValueBox.cs +++ b/Source/Editor/GUI/Input/ValueBox.cs @@ -89,6 +89,11 @@ namespace FlaxEditor.GUI.Input /// public bool IsSliding => _isSliding; + /// + /// The color of the highlight to the left of the value box. + /// + public Color HighlightColor; + /// /// Occurs when sliding starts. /// @@ -211,6 +216,12 @@ namespace FlaxEditor.GUI.Input Render2D.DrawRectangle(bounds, style.SelectionBorder); } } + + if (HighlightColor != Color.Transparent) + { + var highlightRect = new Rectangle(-3.0f, 0.0f, 3.0f, Height); + Render2D.FillRectangle(highlightRect, HighlightColor); + } } /// diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs index 15e7eb953..1fb19e9ec 100644 --- a/Source/Editor/GUI/Timeline/Timeline.cs +++ b/Source/Editor/GUI/Timeline/Timeline.cs @@ -2148,10 +2148,9 @@ namespace FlaxEditor.GUI.Timeline /// public void ShowWholeTimeline() { - var viewWidth = Width; - var timelineWidth = Duration * UnitsPerSecond * Zoom + 8 * StartOffset; - _backgroundArea.ViewOffset = Float2.Zero; - Zoom = viewWidth / timelineWidth; + const float padding = 40f; + Zoom = (_backgroundArea.Width - padding * 2f) / (Duration * UnitsPerSecond); + _backgroundArea.ViewOffset = new Float2(-_leftEdge.X + padding, _backgroundArea.ViewOffset.Y); } /// diff --git a/Source/Editor/Gizmo/DirectionGizmo.cs b/Source/Editor/Gizmo/DirectionGizmo.cs new file mode 100644 index 000000000..b04eb8d8d --- /dev/null +++ b/Source/Editor/Gizmo/DirectionGizmo.cs @@ -0,0 +1,289 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using System.Collections.Generic; +using FlaxEditor.Options; +using FlaxEditor.Viewport; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.Gizmo; + +[HideInEditor] +internal class DirectionGizmo : ContainerControl +{ + public const float DefaultGizmoSize = 112.5f; + + private const float AxisLength = 71.25f; + private const float SpriteRadius = 10.85f; + + private IGizmoOwner _owner; + private ViewportProjection _viewportProjection; + private EditorViewport _viewport; + private Vector3 _gizmoCenter; + private float _gizmoBrightness; + private float _gizmoOpacity; + private float _backgroundOpacity; + private float _axisLength; + private float _spriteRadius; + + private AxisData _xAxisData; + private AxisData _yAxisData; + private AxisData _zAxisData; + private AxisData _negXAxisData; + private AxisData _negYAxisData; + private AxisData _negZAxisData; + + private List _axisData = new List(); + 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 + } + + /// + /// Constructor of the Direction Gizmo + /// + /// The owner of this object. + 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.3607f, 0.9f, 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.3607f, 0.9f, 1.0f), Negative = true, Direction = AxisDirection.NegZ }; + _axisData.EnsureCapacity(6); + _spritePositions.EnsureCapacity(6); + + var editor = Editor.Instance; + _posHandle = editor.Icons.VisjectBoxClosed32; + _negHandle = editor.Icons.VisjectBoxOpen32; + + _fontReference = new FontReference(Style.Current.FontSmall); + + editor.Options.OptionsChanged += OnEditorOptionsChanged; + OnEditorOptionsChanged(editor.Options.Options); + } + + private void OnEditorOptionsChanged(EditorOptions options) + { + float gizmoScale = options.Viewport.DirectionGizmoScale; + _axisLength = AxisLength * gizmoScale; + _spriteRadius = SpriteRadius * gizmoScale; + _gizmoBrightness = options.Viewport.DirectionGizmoBrightness; + _gizmoOpacity = options.Viewport.DirectionGizmoOpacity; + _backgroundOpacity = options.Viewport.DirectionGizmoBackgroundOpacity; + + _fontReference.Size = 8.25f * gizmoScale; + } + + private bool IsPointInSprite(Float2 point, Float2 spriteCenter) + { + Float2 delta = point - spriteCenter; + float distanceSq = delta.LengthSquared; + float radiusSq = _spriteRadius * _spriteRadius; + return distanceSq <= radiusSq; + } + + /// + 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); + } + + /// + 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, 180, 0), + AxisDirection.NegZ => Quaternion.Euler(0, 0, 0), + _ => Quaternion.Identity + }; + _viewport.OrientViewport(ref orientation); + } + + /// + /// Used to Draw the gizmo. + /// + 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(_backgroundOpacity)); + + // 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; + bool isHovered = _hoveredAxisIndex == i; + + // Store sprite position for hover detection + _spritePositions.Add((tipScreen, axis.Direction)); + + var axisColor = isHovered ? new Color(1.0f, 0.8980392f, 0.039215688f) : axis.AxisColor; + axisColor = axisColor.RGBMultiplied(_gizmoBrightness).AlphaMultiplied(_gizmoOpacity); + var font = _fontReference.GetFont(); + if (!axis.Negative) + { + Render2D.DrawLine(relativeCenter, tipScreen, axisColor, 1.5f); + Render2D.DrawSprite(_posHandle, new Rectangle(tipScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor); + Render2D.DrawText(font, axis.Label, Color.Black, tipScreen - font.MeasureText(axis.Label) * 0.5f); + } + else + { + Render2D.DrawSprite(_posHandle, new Rectangle(tipScreen - new Float2(_spriteRadius), new Float2(_spriteRadius * 2)), axisColor.RGBMultiplied(0.85f).AlphaMultiplied(0.8f)); + if (isHovered) + Render2D.DrawText(font, axis.Label, Color.Black, tipScreen - font.MeasureText(axis.Label) * 0.5f); + } + } + + Render2D.Features = features; + } +} diff --git a/Source/Editor/Gizmo/UIEditorGizmo.cs b/Source/Editor/Gizmo/UIEditorGizmo.cs index 765893bed..61109556c 100644 --- a/Source/Editor/Gizmo/UIEditorGizmo.cs +++ b/Source/Editor/Gizmo/UIEditorGizmo.cs @@ -180,6 +180,11 @@ namespace FlaxEditor /// public bool EnableCamera => _view != null && EnableBackground; + /// + /// True if enable grid drawing. + /// + public bool ShowGrid { get; set; } = true; + /// /// Transform gizmo to use sync with (selection, snapping, transformation settings). /// @@ -387,19 +392,53 @@ namespace FlaxEditor if (_mouseMovesWidget && _activeWidget.UIControl) { // Calculate transform delta - var resizeAxisAbs = _activeWidget.ResizeAxis.Absolute; - var resizeAxisPos = Float2.Clamp(_activeWidget.ResizeAxis, Float2.Zero, Float2.One); - var resizeAxisNeg = Float2.Clamp(-_activeWidget.ResizeAxis, Float2.Zero, Float2.One); var delta = location - _mouseMovesPos; // TODO: scale/size snapping? - delta *= resizeAxisAbs; // Resize control via widget var moveLocation = _mouseMovesPos + delta; var control = _activeWidget.UIControl.Control; var uiControlDelta = GetControlDelta(control, ref _mouseMovesPos, ref moveLocation); - control.LocalLocation += uiControlDelta * resizeAxisNeg; - control.Size += uiControlDelta * resizeAxisPos - uiControlDelta * resizeAxisNeg; + + // Transform delta to control local space + var rotation = GetTotalRotation(control) * Mathf.DegreesToRadians; + var cos = Mathf.Cos(rotation); + var sin = Mathf.Sin(rotation); + var localDeltaX = uiControlDelta.X * cos + uiControlDelta.Y * sin; + var localDeltaY = uiControlDelta.Y * cos - uiControlDelta.X * sin; + var localDelta = new Float2(localDeltaX, localDeltaY); + localDelta *= _activeWidget.ResizeAxis.Absolute; + + // Calculate size change + var resizeAxisPos = Float2.Clamp(_activeWidget.ResizeAxis, Float2.Zero, Float2.One); + var resizeAxisNeg = Float2.Clamp(-_activeWidget.ResizeAxis, Float2.Zero, Float2.One); + var dSizeScaled = localDelta * resizeAxisPos - localDelta * resizeAxisNeg; + var scale = control.Scale; + var dSize = new Float2( + Mathf.Abs(scale.X) > Mathf.Epsilon ? dSizeScaled.X / scale.X : 0, + Mathf.Abs(scale.Y) > Mathf.Epsilon ? dSizeScaled.Y / scale.Y : 0); + + // Apply size change + control.Size += dSize; + + // Calculate location offset to keep the opposite edge stationary + // When PivotRelative is false, resizing keeps Top-Left (Location) constant, + // so we only need to slide back if we are resizing Left or Top edges. + if (!control.PivotRelative) + { + var pivotOffset = Float2.Zero; + if (_activeWidget.ResizeAxis.X < 0 && Mathf.Abs(dSize.X) > Mathf.Epsilon) + pivotOffset.X = -dSize.X * scale.X; + if (_activeWidget.ResizeAxis.Y < 0 && Mathf.Abs(dSize.Y) > Mathf.Epsilon) + pivotOffset.Y = -dSize.Y * scale.Y; + + // Transform offset back to parent space and apply + var dLocationX = pivotOffset.X * cos - pivotOffset.Y * sin; + var dLocationY = pivotOffset.X * sin + pivotOffset.Y * cos; + var dLocation = new Float2(dLocationX, dLocationY); + + control.LocalLocation += dLocation; + } // Don't move if layout doesn't allow it if (control.Parent != null) @@ -492,17 +531,20 @@ namespace FlaxEditor // Draw background Surface.VisjectSurface.DrawBackgroundDefault(Editor.Instance.UI.VisjectSurfaceBackground, Width, Height); - // Draw grid - var viewRect = GetClientArea(); - var upperLeft = _view.PointFromParent(viewRect.Location); - var bottomRight = _view.PointFromParent(viewRect.Size); - var min = Float2.Min(upperLeft, bottomRight); - var max = Float2.Max(upperLeft, bottomRight); - var pixelRange = (max - min) * ViewScale; - Render2D.PushClip(ref viewRect); - DrawAxis(Float2.UnitX, viewRect, min.X, max.X, pixelRange.X); - DrawAxis(Float2.UnitY, viewRect, min.Y, max.Y, pixelRange.Y); - Render2D.PopClip(); + if (ShowGrid) + { + // Draw grid + var viewRect = GetClientArea(); + var upperLeft = _view.PointFromParent(viewRect.Location); + var bottomRight = _view.PointFromParent(viewRect.Size); + var min = Float2.Min(upperLeft, bottomRight); + var max = Float2.Max(upperLeft, bottomRight); + var pixelRange = (max - min) * ViewScale; + Render2D.PushClip(ref viewRect); + DrawAxis(Float2.UnitX, viewRect, min.X, max.X, pixelRange.X); + DrawAxis(Float2.UnitY, viewRect, min.Y, max.Y, pixelRange.Y); + Render2D.PopClip(); + } } base.Draw(); @@ -634,7 +676,7 @@ namespace FlaxEditor // Draw sizing widgets if (_widgets == null) _widgets = new List(); - var widgetSize = 10.0f; + var widgetSize = 8.0f; var viewScale = ViewScale; if (viewScale < 0.7f) widgetSize *= viewScale; @@ -685,7 +727,7 @@ namespace FlaxEditor anchorRectSize *= viewScale; // Make anchor rects and rotate if parent is rotated. - var parentRotation = controlParent.Rotation * Mathf.DegreesToRadians; + var parentRotation = GetTotalRotation(controlParent) * Mathf.DegreesToRadians; var rect1Axis = new Float2(-1, -1); var rect1 = new Rectangle(anchorUpperLeft + @@ -717,17 +759,25 @@ namespace FlaxEditor } } + private float GetTotalRotation(Control control) + { + if (control.Parent != null) + return control.Rotation + GetTotalRotation(control.Parent); + return control.Rotation; + } + private void DrawControlWidget(UIControl uiControl, ref Float2 pos, ref Float2 mousePos, ref Float2 size, float scale, Float2 resizeAxis, CursorType cursor) { var style = Style.Current; var control = uiControl.Control; - var rotation = control.Rotation; + var rotation = GetTotalRotation(control); var rotationInRadians = rotation * Mathf.DegreesToRadians; - var rect = new Rectangle((pos + - new Float2(resizeAxis.X * Mathf.Cos(rotationInRadians) - resizeAxis.Y * Mathf.Sin(rotationInRadians), - resizeAxis.Y * Mathf.Cos(rotationInRadians) + resizeAxis.X * Mathf.Sin(rotationInRadians)) * 10 * scale) - size * 0.5f, - size); - + var position = (pos + new Float2(resizeAxis.X * Mathf.Cos(rotationInRadians) - resizeAxis.Y * Mathf.Sin(rotationInRadians), + resizeAxis.Y * Mathf.Cos(rotationInRadians) + resizeAxis.X * Mathf.Sin(rotationInRadians)) * 4 * (scale < 0.7f ? scale : 1)); + var halfSize = size * 0.5f; + // Keep at 0, 0 rect position until later to correctly render rotation. + var rect = new Rectangle(0, 0, size); + // Find more correct cursor at different angles var unwindRotation = Mathf.UnwindDegrees(rotation); if (unwindRotation is (>= 45 and < 135) or (> -135 and <= -45) ) @@ -749,6 +799,10 @@ namespace FlaxEditor default: break; } } + + Render2D.PushTransform(Matrix3x3.Translation2D(position)); + Render2D.PushTransform(Matrix3x3.RotationZ(rotationInRadians)); + Render2D.PushTransform(Matrix3x3.Translation2D(-1 * halfSize)); if (rect.Contains(ref mousePos)) { Render2D.FillRectangle(rect, style.Foreground); @@ -759,9 +813,14 @@ namespace FlaxEditor Render2D.FillRectangle(rect, style.ForegroundGrey); Render2D.DrawRectangle(rect, style.Foreground); } + Render2D.PopTransform(); + Render2D.PopTransform(); + Render2D.PopTransform(); + if (!_mouseMovesWidget && uiControl != null) { // Collect widget + rect.Location = position - halfSize; _widgets.Add(new Widget { UIControl = uiControl, diff --git a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs index e379e911b..af0187248 100644 --- a/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs +++ b/Source/Editor/Modules/SourceCodeEditing/CodeEditingModule.cs @@ -325,7 +325,7 @@ namespace FlaxEditor.Modules.SourceCodeEditing var codeEditor = options.SourceCode.SourceCodeEditor; if (codeEditor != "None") { - foreach (var e in Editor.Instance.CodeEditing.Editors) + foreach (var e in _editors) { if (string.Equals(codeEditor, e.Name, StringComparison.OrdinalIgnoreCase)) { @@ -334,7 +334,10 @@ namespace FlaxEditor.Modules.SourceCodeEditing } } } - Editor.Instance.CodeEditing.SelectedEditor = editor; + if (editor == null && _editors.Count != 0) + editor = _editors[0]; + + SelectedEditor = editor; } /// diff --git a/Source/Editor/Modules/SourceCodeEditing/DefaultSourceCodeEditor.cs b/Source/Editor/Modules/SourceCodeEditing/DefaultSourceCodeEditor.cs index 074dade6a..00057e9a7 100644 --- a/Source/Editor/Modules/SourceCodeEditing/DefaultSourceCodeEditor.cs +++ b/Source/Editor/Modules/SourceCodeEditing/DefaultSourceCodeEditor.cs @@ -41,9 +41,9 @@ namespace FlaxEditor.Modules.SourceCodeEditing var vsCode = codeEditing.GetInBuildEditor(CodeEditorTypes.VSCode); var rider = codeEditing.GetInBuildEditor(CodeEditorTypes.Rider); -#if PLATFORM_WINDOW +#if PLATFORM_WINDOWS // Favor the newest Visual Studio - for (int i = (int)CodeEditorTypes.VS2019; i >= (int)CodeEditorTypes.VS2008; i--) + for (int i = (int)CodeEditorTypes.VS2026; i >= (int)CodeEditorTypes.VS2008; i--) { var visualStudio = codeEditing.GetInBuildEditor((CodeEditorTypes)i); if (visualStudio != null) @@ -74,7 +74,7 @@ namespace FlaxEditor.Modules.SourceCodeEditing public string Name => "Default"; /// - public string GenerateProjectCustomArgs => null; + public string GenerateProjectCustomArgs => _currentEditor?.GenerateProjectCustomArgs; /// public void OpenSolution() diff --git a/Source/Editor/Modules/SourceCodeEditing/InBuildSourceCodeEditor.cs b/Source/Editor/Modules/SourceCodeEditing/InBuildSourceCodeEditor.cs index a2a333805..222a7a0bc 100644 --- a/Source/Editor/Modules/SourceCodeEditing/InBuildSourceCodeEditor.cs +++ b/Source/Editor/Modules/SourceCodeEditing/InBuildSourceCodeEditor.cs @@ -22,71 +22,14 @@ namespace FlaxEditor.Modules.SourceCodeEditing public InBuildSourceCodeEditor(CodeEditorTypes type) { Type = type; - switch (type) - { - case CodeEditorTypes.Custom: - Name = "Custom"; - break; - case CodeEditorTypes.SystemDefault: - Name = "System Default"; - break; - case CodeEditorTypes.VS2008: - Name = "Visual Studio 2008"; - break; - case CodeEditorTypes.VS2010: - Name = "Visual Studio 2010"; - break; - case CodeEditorTypes.VS2012: - Name = "Visual Studio 2012"; - break; - case CodeEditorTypes.VS2013: - Name = "Visual Studio 2013"; - break; - case CodeEditorTypes.VS2015: - Name = "Visual Studio 2015"; - break; - case CodeEditorTypes.VS2017: - Name = "Visual Studio 2017"; - break; - case CodeEditorTypes.VS2019: - Name = "Visual Studio 2019"; - break; - case CodeEditorTypes.VS2022: - Name = "Visual Studio 2022"; - break; - case CodeEditorTypes.VS2026: - Name = "Visual Studio 2026"; - break; - case CodeEditorTypes.VSCode: - Name = "Visual Studio Code"; - break; - case CodeEditorTypes.VSCodeInsiders: - Name = "Visual Studio Code - Insiders"; - break; - case CodeEditorTypes.Rider: - Name = "Rider"; - break; - default: throw new ArgumentOutOfRangeException(nameof(type), type, null); - } + Name = CodeEditingManager.GetName(type); } /// public string Name { get; set; } /// - public string GenerateProjectCustomArgs - { - get - { - switch (Type) - { - case CodeEditorTypes.VSCodeInsiders: - case CodeEditorTypes.VSCode: return "-vscode -vs2022"; - case CodeEditorTypes.Rider: return "-vs2022"; - default: return null; - } - } - } + public string GenerateProjectCustomArgs => CodeEditingManager.GetGenerateProjectCustomArgs(Type); /// public void OpenSolution() diff --git a/Source/Editor/Options/InputOptions.cs b/Source/Editor/Options/InputOptions.cs index 9b15c0c09..82ccc21d5 100644 --- a/Source/Editor/Options/InputOptions.cs +++ b/Source/Editor/Options/InputOptions.cs @@ -710,6 +710,10 @@ namespace FlaxEditor.Options [EditorDisplay("Node Editors"), EditorOrder(4580)] public InputBinding NodesDistributeVertical = new InputBinding(KeyboardKeys.A, KeyboardKeys.Alt); + [DefaultValue(typeof(InputBinding), "Shift+F")] + [EditorDisplay("Node Editors"), EditorOrder(4590)] + public InputBinding FocusSelectedNodes = new InputBinding(KeyboardKeys.F, KeyboardKeys.Shift); + #endregion } } diff --git a/Source/Editor/Options/InterfaceOptions.cs b/Source/Editor/Options/InterfaceOptions.cs index f26506c74..0dab5ca6e 100644 --- a/Source/Editor/Options/InterfaceOptions.cs +++ b/Source/Editor/Options/InterfaceOptions.cs @@ -159,7 +159,7 @@ namespace FlaxEditor.Options } /// - /// Options focus Game Window behaviour when play mode is entered. + /// Options for focus Game Window behaviour when play mode is entered. /// public enum PlayModeFocus { @@ -179,6 +179,22 @@ namespace FlaxEditor.Options GameWindowThenRestore, } + /// + /// Generic options for a disabled or hidden state. Used for example in create content button. + /// + public enum DisabledHidden + { + /// + /// Disabled state. + /// + Disabled, + + /// + /// Hidden state. + /// + Hidden, + } + /// /// Gets or sets the Editor User Interface scale. Applied to all UI elements, windows and text. Can be used to scale the interface up on a bigger display. Editor restart required. /// @@ -207,13 +223,6 @@ namespace FlaxEditor.Options [EditorDisplay("Interface"), EditorOrder(280), Tooltip("Editor content window orientation.")] public FlaxEngine.GUI.Orientation ContentWindowOrientation { get; set; } = FlaxEngine.GUI.Orientation.Horizontal; - /// - /// If checked, color pickers will always modify the color unless 'Cancel' if pressed, otherwise color won't change unless 'Ok' is pressed. - /// - [DefaultValue(true)] - [EditorDisplay("Interface"), EditorOrder(290)] - public bool AutoAcceptColorPickerChange { get; set; } = true; - /// /// Gets or sets the formatting option for numeric values in the editor. /// @@ -525,6 +534,13 @@ namespace FlaxEditor.Options [EditorDisplay("Visject", "Warn when deleting used parameter"), EditorOrder(552)] public bool WarnOnDeletingUsedVisjectParameter { get; set; } = true; + /// + /// Gets or sets a value indicating what should happen to unavaliable options in the content create menu. + /// + [DefaultValue(DisabledHidden.Hidden)] + [EditorDisplay("Content"), EditorOrder(600)] + public DisabledHidden UnavaliableContentCreateOptions { get; set; } = DisabledHidden.Hidden; + private static FontAsset DefaultFont => FlaxEngine.Content.LoadAsyncInternal(EditorAssets.PrimaryFont); private static FontAsset ConsoleFont => FlaxEngine.Content.LoadAsyncInternal(EditorAssets.InconsolataRegularFont); diff --git a/Source/Editor/Options/ViewportOptions.cs b/Source/Editor/Options/ViewportOptions.cs index aed633672..c7f6ba544 100644 --- a/Source/Editor/Options/ViewportOptions.cs +++ b/Source/Editor/Options/ViewportOptions.cs @@ -171,5 +171,40 @@ namespace FlaxEditor.Options [DefaultValue(1000.0f), Limit(0.0f, 20000.0f, 5.0f)] [EditorDisplay("Viewport Icons"), EditorOrder(410)] public float MaxSizeDistance { get; set; } = 1000.0f; + + /// + /// Gets or sets a value that indicates whether the main viewports is visible. + /// + [DefaultValue(true)] + [EditorDisplay("Direction Gizmo"), EditorOrder(500), Tooltip("Sets the visibility of the direction gizmo in the main editor viewport.")] + public bool ShowDirectionGizmo { get; set; } = true; + + /// + /// Gets or sets a value by which the main viewports size is multiplied with. + /// + [DefaultValue(1f), Limit(0.0f, 2.0f)] + [EditorDisplay("Direction Gizmo"), EditorOrder(501), Tooltip("The scale of the direction gizmo in the main viewport.")] + public float DirectionGizmoScale { get; set; } = 1f; + + /// + /// Gets or sets a value for the opacity of the main viewports background. + /// + [DefaultValue(0.1f), Limit(0.0f, 1.0f)] + [EditorDisplay("Direction Gizmo"), EditorOrder(502), Tooltip("The background opacity of the of the direction gizmo in the main viewport.")] + public float DirectionGizmoBackgroundOpacity { get; set; } = 0.1f; + + /// + /// Gets or sets a value for the opacity of the main viewports . + /// + [DefaultValue(0.6f), Limit(0.0f, 1.0f)] + [EditorDisplay("Direction Gizmo"), EditorOrder(503), Tooltip("The opacity of the of the direction gizmo in the main viewport.")] + public float DirectionGizmoOpacity { get; set; } = 0.6f; + + /// + /// Gets or sets a value for the opacity of the main viewports . + /// + [DefaultValue(1f), Limit(0.0f, 2.0f)] + [EditorDisplay("Direction Gizmo"), EditorOrder(504), Tooltip("The brightness of the of the direction gizmo in the main viewport.")] + public float DirectionGizmoBrightness{ get; set; } = 1f; } } diff --git a/Source/Editor/SceneGraph/Actors/ClothNode.cs b/Source/Editor/SceneGraph/Actors/ClothNode.cs new file mode 100644 index 000000000..b5caa8258 --- /dev/null +++ b/Source/Editor/SceneGraph/Actors/ClothNode.cs @@ -0,0 +1,29 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using FlaxEngine; + +namespace FlaxEditor.SceneGraph.Actors +{ + /// + /// Scene tree node for actor type. + /// + [HideInEditor] + public sealed class ClothNode : ActorNode + { + /// + public ClothNode(Actor actor) + : base(actor) + { + } + + /// + public override void PostSpawn() + { + base.PostSpawn(); + + // Snap to the parent + if (!(ParentNode is SceneNode)) + Actor.LocalTransform = Transform.Identity; + } + } +} diff --git a/Source/Editor/SceneGraph/Actors/RigidBodyNode.cs b/Source/Editor/SceneGraph/Actors/RigidBodyNode.cs new file mode 100644 index 000000000..9f9a38258 --- /dev/null +++ b/Source/Editor/SceneGraph/Actors/RigidBodyNode.cs @@ -0,0 +1,29 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using FlaxEngine; + +namespace FlaxEditor.SceneGraph.Actors +{ + /// + /// Scene tree node for actor type. + /// + [HideInEditor] + public sealed class RigidBodyNode : ActorNode + { + /// + public RigidBodyNode(Actor actor) + : base(actor) + { + } + + /// + public override void PostSpawn() + { + base.PostSpawn(); + + if (HasPrefabLink) + return; + Actor.StaticFlags = StaticFlags.None; + } + } +} diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index 5f56e918a..4607a4f63 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -324,13 +324,12 @@ namespace FlaxEditor.SceneGraph.GUI isExpanded = Editor.Instance.ProjectCache.IsExpandedActor(ref id); } - if (isExpanded) + if (!noFilter) { - Expand(true); - } - else - { - Collapse(true); + if (isExpanded) + Expand(true); + else + Collapse(true); } Visible = isThisVisible | isAnyChildVisible; diff --git a/Source/Editor/SceneGraph/SceneGraphFactory.cs b/Source/Editor/SceneGraph/SceneGraphFactory.cs index c4aecef7e..8437167aa 100644 --- a/Source/Editor/SceneGraph/SceneGraphFactory.cs +++ b/Source/Editor/SceneGraph/SceneGraphFactory.cs @@ -51,6 +51,7 @@ namespace FlaxEditor.SceneGraph CustomNodesTypes.Add(typeof(AudioSource), typeof(AudioSourceNode)); CustomNodesTypes.Add(typeof(BoneSocket), typeof(BoneSocketNode)); CustomNodesTypes.Add(typeof(Decal), typeof(DecalNode)); + CustomNodesTypes.Add(typeof(Cloth), typeof(ClothNode)); CustomNodesTypes.Add(typeof(BoxCollider), typeof(BoxColliderNode)); CustomNodesTypes.Add(typeof(SphereCollider), typeof(ColliderNode)); CustomNodesTypes.Add(typeof(CapsuleCollider), typeof(ColliderNode)); @@ -73,6 +74,7 @@ namespace FlaxEditor.SceneGraph CustomNodesTypes.Add(typeof(NavMesh), typeof(ActorNode)); CustomNodesTypes.Add(typeof(SpriteRender), typeof(SpriteRenderNode)); CustomNodesTypes.Add(typeof(Joint), typeof(JointNode)); + CustomNodesTypes.Add(typeof(RigidBody), typeof(RigidBodyNode)); } /// diff --git a/Source/Editor/Scripting/CodeEditor.cpp b/Source/Editor/Scripting/CodeEditor.cpp index b372da189..fcc8eef7c 100644 --- a/Source/Editor/Scripting/CodeEditor.cpp +++ b/Source/Editor/Scripting/CodeEditor.cpp @@ -139,6 +139,34 @@ CodeEditor* CodeEditingManager::GetCodeEditor(CodeEditorTypes editorType) return nullptr; } +String CodeEditingManager::GetName(CodeEditorTypes editorType) +{ + const auto editor = GetCodeEditor(editorType); + if (editor) + { + return editor->GetName(); + } + else + { + LOG(Warning, "Missing code editor type {0}", (int32)editorType); + return String::Empty; + } +} + +String CodeEditingManager::GetGenerateProjectCustomArgs(CodeEditorTypes editorType) +{ + const auto editor = GetCodeEditor(editorType); + if (editor) + { + return editor->GetGenerateProjectCustomArgs(); + } + else + { + LOG(Warning, "Missing code editor type {0}", (int32)editorType); + return String::Empty; + } +} + void CodeEditingManager::OpenFile(CodeEditorTypes editorType, const String& path, int32 line) { const auto editor = GetCodeEditor(editorType); diff --git a/Source/Editor/Scripting/CodeEditor.h b/Source/Editor/Scripting/CodeEditor.h index 9cc71977b..0baae21b0 100644 --- a/Source/Editor/Scripting/CodeEditor.h +++ b/Source/Editor/Scripting/CodeEditor.h @@ -109,9 +109,18 @@ public: /// /// Gets the name of the editor. /// - /// The name + /// The name. virtual String GetName() const = 0; + /// + /// Gets the custom arguments for the Flax.Build tool to add when generating project files for this code editor. + /// + /// The custom arguments to generate project files. + virtual String GetGenerateProjectCustomArgs() const + { + return String::Empty; + } + /// /// Opens the file. /// @@ -169,6 +178,20 @@ public: /// The editor object or null if not found. static CodeEditor* GetCodeEditor(CodeEditorTypes editorType); + /// + /// Gets the name of the editor. + /// + /// The code editor type. + /// The name. + API_FUNCTION() static String GetName(CodeEditorTypes editorType); + + /// + /// Gets the custom arguments for the Flax.Build tool to add when generating project files for this code editor. + /// + /// The code editor type. + /// The custom arguments to generate project files. + API_FUNCTION() static String GetGenerateProjectCustomArgs(CodeEditorTypes editorType); + /// /// Opens the file. Handles async opening. /// diff --git a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp index 8884ca322..b63815dce 100644 --- a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp +++ b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.cpp @@ -257,12 +257,17 @@ String RiderCodeEditor::GetName() const return TEXT("Rider"); } +String RiderCodeEditor::GetGenerateProjectCustomArgs() const +{ + return TEXT("-vs2022"); +} + void RiderCodeEditor::OpenFile(const String& path, int32 line) { // Generate project files if solution is missing if (!FileSystem::FileExists(_solutionPath)) { - ScriptsBuilder::GenerateProject(TEXT("-vs2022")); + ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); } // Open file @@ -290,7 +295,7 @@ void RiderCodeEditor::OpenSolution() // Generate project files if solution is missing if (!FileSystem::FileExists(_solutionPath)) { - ScriptsBuilder::GenerateProject(TEXT("-vs2022")); + ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); } // Open solution @@ -312,5 +317,5 @@ void RiderCodeEditor::OpenSolution() void RiderCodeEditor::OnFileAdded(const String& path) { - ScriptsBuilder::GenerateProject(); + ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); } diff --git a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.h b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.h index d9bf00947..f3c43b5b2 100644 --- a/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.h +++ b/Source/Editor/Scripting/CodeEditors/RiderCodeEditor.h @@ -35,6 +35,7 @@ public: // [CodeEditor] CodeEditorTypes GetType() const override; String GetName() const override; + String GetGenerateProjectCustomArgs() const override; void OpenFile(const String& path, int32 line) override; void OpenSolution() override; void OnFileAdded(const String& path) override; diff --git a/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.cpp b/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.cpp index 5c06eec9c..2d79cc02b 100644 --- a/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.cpp +++ b/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.cpp @@ -145,7 +145,46 @@ CodeEditorTypes VisualStudioEditor::GetType() const String VisualStudioEditor::GetName() const { - return String(ToString(_version)); + const Char* name; + switch (_version) + { + case VisualStudioVersion::VS2008: + name = TEXT("Visual Studio 2008"); + break; + case VisualStudioVersion::VS2010: + name = TEXT("Visual Studio 2010"); + break; + case VisualStudioVersion::VS2012: + name = TEXT("Visual Studio 2012"); + break; + case VisualStudioVersion::VS2013: + name = TEXT("Visual Studio 2013"); + break; + case VisualStudioVersion::VS2015: + name = TEXT("Visual Studio 2015"); + break; + case VisualStudioVersion::VS2017: + name = TEXT("Visual Studio 2017"); + break; + case VisualStudioVersion::VS2019: + name = TEXT("Visual Studio 2019"); + break; + case VisualStudioVersion::VS2022: + name = TEXT("Visual Studio 2022"); + break; + case VisualStudioVersion::VS2026: + name = TEXT("Visual Studio 2026"); + break; + default: + name = ToString(_version); + break; + } + return String(name); +} + +String VisualStudioEditor::GetGenerateProjectCustomArgs() const +{ + return String::Format(TEXT("-{0}"), String(ToString(_version)).ToLower()); } void VisualStudioEditor::OpenFile(const String& path, int32 line) @@ -153,7 +192,7 @@ void VisualStudioEditor::OpenFile(const String& path, int32 line) // Generate project files if solution is missing if (!FileSystem::FileExists(_solutionPath)) { - ScriptsBuilder::GenerateProject(); + ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); } // Open file @@ -172,7 +211,7 @@ void VisualStudioEditor::OpenSolution() // Generate project files if solution is missing if (!FileSystem::FileExists(_solutionPath)) { - ScriptsBuilder::GenerateProject(); + ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); } // Open solution @@ -187,7 +226,7 @@ void VisualStudioEditor::OpenSolution() void VisualStudioEditor::OnFileAdded(const String& path) { // TODO: finish dynamic files adding to the project - for now just regenerate it - ScriptsBuilder::GenerateProject(); + ScriptsBuilder::GenerateProject(GetGenerateProjectCustomArgs()); return; if (!FileSystem::FileExists(_solutionPath)) { diff --git a/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.h b/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.h index 1bf1f1433..5c32a1171 100644 --- a/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.h +++ b/Source/Editor/Scripting/CodeEditors/VisualStudio/VisualStudioEditor.h @@ -56,6 +56,7 @@ public: // [CodeEditor] CodeEditorTypes GetType() const override; String GetName() const override; + String GetGenerateProjectCustomArgs() const override; void OpenFile(const String& path, int32 line) override; void OpenSolution() override; void OnFileAdded(const String& path) override; diff --git a/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp b/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp index 5eba7f20c..bf2ef6bb6 100644 --- a/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp +++ b/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.cpp @@ -128,6 +128,11 @@ String VisualStudioCodeEditor::GetName() const return _isInsiders ? TEXT("Visual Studio Code - Insiders") : TEXT("Visual Studio Code"); } +String VisualStudioCodeEditor::GetGenerateProjectCustomArgs() const +{ + return TEXT("-vs2022 -vscode"); +} + void VisualStudioCodeEditor::OpenFile(const String& path, int32 line) { // Generate VS solution files for intellisense diff --git a/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.h b/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.h index 0212f207e..5091e1134 100644 --- a/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.h +++ b/Source/Editor/Scripting/CodeEditors/VisualStudioCodeEditor.h @@ -37,6 +37,7 @@ public: // [CodeEditor] CodeEditorTypes GetType() const override; String GetName() const override; + String GetGenerateProjectCustomArgs() const override; void OpenFile(const String& path, int32 line) override; void OpenSolution() override; bool UseAsyncForOpen() const override; diff --git a/Source/Editor/Surface/Archetypes/Layers.cs b/Source/Editor/Surface/Archetypes/Layers.cs index fe0053f7f..6c4caa6e7 100644 --- a/Source/Editor/Surface/Archetypes/Layers.cs +++ b/Source/Editor/Surface/Archetypes/Layers.cs @@ -52,6 +52,7 @@ namespace FlaxEditor.Surface.Archetypes }, new NodeArchetype { + // [Deprecated] TypeID = 3, Title = "Pack Material Layer", Description = "Pack material properties", @@ -75,6 +76,7 @@ namespace FlaxEditor.Surface.Archetypes }, new NodeArchetype { + // [Deprecated] TypeID = 4, Title = "Unpack Material Layer", Description = "Unpack material properties", @@ -120,6 +122,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 6, Title = "Pack Material Layer", Description = "Pack material properties", + AlternativeTitles = new[] { "Make Material Layer", "Construct Material Layer", "Compose Material Layer" }, Flags = NodeFlags.MaterialGraph, Size = new Float2(200, 280), Elements = new[] @@ -146,6 +149,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 7, Title = "Unpack Material Layer", Description = "Unpack material properties", + AlternativeTitles = new[] { "Break Material Layer", "Deconstruct Material Layer", "Decompose Material Layer", "Split Material Layer" }, Flags = NodeFlags.MaterialGraph, Size = new Float2(210, 280), Elements = new[] diff --git a/Source/Editor/Surface/Archetypes/Material.cs b/Source/Editor/Surface/Archetypes/Material.cs index 9ac43082e..df89dfc94 100644 --- a/Source/Editor/Surface/Archetypes/Material.cs +++ b/Source/Editor/Surface/Archetypes/Material.cs @@ -586,8 +586,9 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 13, Title = "Pre-skinned Local Position", Description = "Per vertex local position (before skinning)", + AlternativeTitles = new[] { "Vertex Position", "Pre skinning Local Vertex Position" }, Flags = NodeFlags.MaterialGraph, - Size = new Float2(230, 40), + Size = new Float2(270, 40), Elements = new[] { NodeElementArchetype.Factory.Output(0, string.Empty, typeof(Float3), 0), @@ -598,8 +599,9 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 14, Title = "Pre-skinned Local Normal", Description = "Per vertex local normal (before skinning)", + AlternativeTitles = new[] { "Vertex Normal", "Pre skinning Local Normal" }, Flags = NodeFlags.MaterialGraph, - Size = new Float2(230, 40), + Size = new Float2(270, 40), Elements = new[] { NodeElementArchetype.Factory.Output(0, string.Empty, typeof(Float3), 0), diff --git a/Source/Editor/Surface/Archetypes/Packing.cs b/Source/Editor/Surface/Archetypes/Packing.cs index 363d867f7..8d57bc81e 100644 --- a/Source/Editor/Surface/Archetypes/Packing.cs +++ b/Source/Editor/Surface/Archetypes/Packing.cs @@ -342,6 +342,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 20, Title = "Pack Float2", Description = "Pack components to Float2", + AlternativeTitles = new[] { "Make Float2", "Construct Float2", "Compose Float2" }, Flags = NodeFlags.AllGraphs, Size = new Float2(150, 40), DefaultValues = new object[] @@ -361,6 +362,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 21, Title = "Pack Float3", Description = "Pack components to Float3", + AlternativeTitles = new[] { "Make Float3", "Construct Float3", "Compose Float3" }, Flags = NodeFlags.AllGraphs, Size = new Float2(150, 60), DefaultValues = new object[] @@ -382,6 +384,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 22, Title = "Pack Float4", Description = "Pack components to Float4", + AlternativeTitles = new[] { "Make Float4", "Construct Float4", "Compose Float4" }, Flags = NodeFlags.AllGraphs, Size = new Float2(150, 80), DefaultValues = new object[] @@ -405,6 +408,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 23, Title = "Pack Rotation", Description = "Pack components to Rotation", + AlternativeTitles = new[] { "Make Rotation", "Construct Rotation", "Compose Rotation" }, Flags = NodeFlags.AllGraphs, Size = new Float2(150, 60), DefaultValues = new object[] @@ -426,6 +430,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 24, Title = "Pack Transform", Description = "Pack components to Transform", + AlternativeTitles = new[] { "Make Transform", "Construct Transform", "Compose Transform" }, Flags = NodeFlags.AllGraphs, Size = new Float2(150, 80), Elements = new[] @@ -441,6 +446,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 25, Title = "Pack Box", Description = "Pack components to BoundingBox", + AlternativeTitles = new[] { "Make Box", "Construct Box", "Compose Box" }, Flags = NodeFlags.AllGraphs, Size = new Float2(150, 40), Elements = new[] @@ -454,6 +460,7 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 26, Title = "Pack Structure", + AlternativeTitles = new[] { "Make Structure", "Construct Structure", "Compose Structure" }, Create = (id, context, arch, groupArch) => new PackStructureNode(id, context, arch, groupArch), IsInputCompatible = PackStructureNode.IsInputCompatible, IsOutputCompatible = PackStructureNode.IsOutputCompatible, @@ -479,6 +486,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 30, Title = "Unpack Float2", Description = "Unpack components from Float2", + AlternativeTitles = new[] { "Break Float2", "Deconstruct Float2", "Decompose Float2", "Split Float2" }, Flags = NodeFlags.AllGraphs, Size = new Float2(150, 40), Elements = new[] @@ -493,6 +501,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 31, Title = "Unpack Float3", Description = "Unpack components from Float3", + AlternativeTitles = new[] { "Break Float3", "Deconstruct Float3", "Decompose Float3", "Split Float3" }, Flags = NodeFlags.AllGraphs, Size = new Float2(150, 60), Elements = new[] @@ -508,6 +517,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 32, Title = "Unpack Float4", Description = "Unpack components from Float4", + AlternativeTitles = new[] { "Break Float4", "Deconstruct Float4", "Decompose Float4", "Split Float4" }, Flags = NodeFlags.AllGraphs, Size = new Float2(150, 80), Elements = new[] @@ -524,6 +534,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 33, Title = "Unpack Rotation", Description = "Unpack components from Rotation", + AlternativeTitles = new[] { "Break Rotation", "Deconstruct Rotation", "Decompose Rotation", "Split Rotation" }, Flags = NodeFlags.AllGraphs, Size = new Float2(170, 60), Elements = new[] @@ -539,6 +550,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 34, Title = "Unpack Transform", Description = "Unpack components from Transform", + AlternativeTitles = new[] { "Break Transform", "Deconstruct Transform", "Decompose Transform", "Split Transform" }, Flags = NodeFlags.AllGraphs, Size = new Float2(170, 60), Elements = new[] @@ -554,6 +566,7 @@ namespace FlaxEditor.Surface.Archetypes TypeID = 35, Title = "Unpack Box", Description = "Unpack components from BoundingBox", + AlternativeTitles = new[] { "Break BoundingBox", "Deconstruct BoundingBox", "Decompose BoundingBox", "Split BoundingBox" }, Flags = NodeFlags.AllGraphs, Size = new Float2(170, 40), Elements = new[] @@ -572,6 +585,7 @@ namespace FlaxEditor.Surface.Archetypes IsOutputCompatible = UnpackStructureNode.IsOutputCompatible, GetInputOutputDescription = UnpackStructureNode.GetInputOutputDescription, Description = "Breaks the structure data to allow extracting components from it.", + AlternativeTitles = new[] { "Break Structure", "Deconstruct Structure", "Decompose Structure", "Split Structure" }, Flags = NodeFlags.VisualScriptGraph | NodeFlags.AnimGraph | NodeFlags.NoSpawnViaGUI, Size = new Float2(180, 20), DefaultValues = new object[] diff --git a/Source/Editor/Surface/Archetypes/Parameters.cs b/Source/Editor/Surface/Archetypes/Parameters.cs index 3383e7662..905f793c9 100644 --- a/Source/Editor/Surface/Archetypes/Parameters.cs +++ b/Source/Editor/Surface/Archetypes/Parameters.cs @@ -22,15 +22,97 @@ namespace FlaxEditor.Surface.Archetypes [HideInEditor] public static class Parameters { + /// + /// Surface node type for parameters group Get/Set nodes. + /// + /// + public abstract class SurfaceNodeParamsBase : SurfaceNode + { + /// + /// The combobox for picking parameter. + /// + protected ComboBoxElement _combobox; + + /// + /// Fag used to block layout updated when updating node. + /// + protected bool _isUpdateLocked; + + /// + protected SurfaceNodeParamsBase(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + : base(id, context, nodeArch, groupArch) + { + } + + /// + /// Gets the selected parameter. + /// + /// Surface parameter object or null if nothing selected or cannot find it. + protected SurfaceParameter GetSelected() + { + if (Surface != null) + return Surface.GetParameter(_combobox.SelectedItem); + return Context.GetParameter((Guid)Values[0]); + } + + /// + /// Updates the combo box. + /// + protected void UpdateCombo() + { + if (_isUpdateLocked) + return; + _isUpdateLocked = true; + if (_combobox == null) + { + _combobox = GetChild(); + _combobox.SelectedIndexChanged += OnSelectedChanged; + } + string toSelect = null; + Guid loadedSelected = (Guid)Values[0]; + _combobox.ClearItems(); + var parameters = Surface.Parameters; + for (int i = 0; i < parameters.Count; i++) + { + var param = parameters[i]; + if (!param.IsPublic && !Surface.CanShowPrivateParameters) + continue; + _combobox.AddItem(param.Name); + if (param.ID == loadedSelected) + toSelect = param.Name; + } + _combobox.SelectedItem = toSelect; + _isUpdateLocked = false; + } + + private void OnSelectedChanged(ComboBox cb) + { + if (_isUpdateLocked) + return; + var selected = GetSelected(); + var selectedID = selected?.ID ?? Guid.Empty; + if (selectedID != (Guid)Values[0]) + Set(selected, ref selectedID); + } + + /// + /// Sets the selected parameter. + /// + /// Parameter. + /// Parameter identifier. + protected virtual void Set(SurfaceParameter selected, ref Guid selectedID) + { + SetValue(0, selectedID); + } + } + /// /// Surface node type for parameters group Get node. /// /// - public class SurfaceNodeParamsGet : SurfaceNode, IParametersDependantNode + public class SurfaceNodeParamsGet : SurfaceNodeParamsBase, IParametersDependantNode { - private ComboBoxElement _combobox; private readonly List _dynamicChildren = new List(); - private bool _isUpdateLocked; private ScriptType _layoutType; private NodeElementArchetype[] _layoutElements; @@ -306,49 +388,6 @@ namespace FlaxEditor.Surface.Archetypes UpdateTitle(); } - private void UpdateCombo() - { - if (_isUpdateLocked) - return; - _isUpdateLocked = true; - if (_combobox == null) - { - _combobox = (ComboBoxElement)_children[0]; - _combobox.SelectedIndexChanged += OnSelectedChanged; - } - int toSelect = -1; - Guid loadedSelected = (Guid)Values[0]; - _combobox.ClearItems(); - for (int i = 0; i < Surface.Parameters.Count; i++) - { - var param = Surface.Parameters[i]; - _combobox.AddItem(param.Name); - if (param.ID == loadedSelected) - toSelect = i; - } - _combobox.SelectedIndex = toSelect; - _isUpdateLocked = false; - } - - private void OnSelectedChanged(ComboBox cb) - { - if (_isUpdateLocked) - return; - var selected = GetSelected(); - var selectedID = selected?.ID ?? Guid.Empty; - SetValue(0, selectedID); - } - - private SurfaceParameter GetSelected() - { - if (Surface != null) - { - var selectedIndex = _combobox.SelectedIndex; - return selectedIndex >= 0 && selectedIndex < Surface.Parameters.Count ? Surface.Parameters[selectedIndex] : null; - } - return Context.GetParameter((Guid)Values[0]); - } - private void ClearDynamicElements() { for (int i = 0; i < _dynamicChildren.Count; i++) @@ -463,15 +502,19 @@ namespace FlaxEditor.Surface.Archetypes else if (!_isUpdateLocked) { _isUpdateLocked = true; - int toSelect = -1; + string toSelect = null; Guid loadedSelected = (Guid)Values[0]; - for (int i = 0; i < Surface.Parameters.Count; i++) + var parameters = Surface.Parameters; + for (int i = 0; i < parameters.Count; i++) { - var param = Surface.Parameters[i]; + var param = parameters[i]; if (param.ID == loadedSelected) - toSelect = i; + { + toSelect = param.Name; + break; + } } - _combobox.SelectedIndex = toSelect; + _combobox.SelectedItem = toSelect; _isUpdateLocked = false; } UpdateLayout(); @@ -817,66 +860,23 @@ namespace FlaxEditor.Surface.Archetypes /// Surface node type for parameters group Set node. /// /// - public class SurfaceNodeParamsSet : SurfaceNode, IParametersDependantNode + public class SurfaceNodeParamsSet : SurfaceNodeParamsBase, IParametersDependantNode { - private ComboBoxElement _combobox; - private bool _isUpdateLocked; - /// public SurfaceNodeParamsSet(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { } - private void UpdateCombo() + /// + protected override void Set(SurfaceParameter selected, ref Guid selectedID) { - if (_isUpdateLocked) - return; - _isUpdateLocked = true; - if (_combobox == null) + SetValues(new[] { - _combobox = GetChild(); - _combobox.SelectedIndexChanged += OnSelectedChanged; - } - int toSelect = -1; - Guid loadedSelected = (Guid)Values[0]; - _combobox.ClearItems(); - for (int i = 0; i < Surface.Parameters.Count; i++) - { - var param = Surface.Parameters[i]; - _combobox.AddItem(param.Name); - if (param.ID == loadedSelected) - toSelect = i; - } - _combobox.SelectedIndex = toSelect; - _isUpdateLocked = false; - } - - private void OnSelectedChanged(ComboBox cb) - { - if (_isUpdateLocked) - return; - var selected = GetSelected(); - var selectedID = selected?.ID ?? Guid.Empty; - if (selectedID != (Guid)Values[0]) - { - SetValues(new[] - { - selectedID, - selected != null ? TypeUtils.GetDefaultValue(selected.Type) : null, - }); - UpdateUI(); - } - } - - private SurfaceParameter GetSelected() - { - if (Surface != null) - { - var selectedIndex = _combobox.SelectedIndex; - return selectedIndex >= 0 && selectedIndex < Surface.Parameters.Count ? Surface.Parameters[selectedIndex] : null; - } - return Context.GetParameter((Guid)Values[0]); + selectedID, + selected != null ? TypeUtils.GetDefaultValue(selected.Type) : null, + }); + UpdateUI(); } /// diff --git a/Source/Editor/Surface/Archetypes/Particles.cs b/Source/Editor/Surface/Archetypes/Particles.cs index 5b8c05381..59af7af5e 100644 --- a/Source/Editor/Surface/Archetypes/Particles.cs +++ b/Source/Editor/Surface/Archetypes/Particles.cs @@ -436,10 +436,11 @@ namespace FlaxEditor.Surface.Archetypes new NodeArchetype { TypeID = 102, - Title = "Particle Lifetime", - Description = "Particle lifetime (in seconds).", + Title = "Particle Total Lifetime", + Description = "Total particle lifetime (in seconds) at the time when the particle was created. Always the same, no matter the particles age.", + AlternativeTitles = new[] { "Age" }, Flags = NodeFlags.MaterialGraph | NodeFlags.ParticleEmitterGraph, - Size = new Float2(200, 30), + Size = new Float2(250, 30), Elements = new[] { NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 0), @@ -449,9 +450,10 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 103, Title = "Particle Age", - Description = "Particle age (in seconds).", + Description = "Particle age (in seconds). How long the particle has been alive since it was created.", + AlternativeTitles = new[] { "Lifetime" }, Flags = NodeFlags.MaterialGraph | NodeFlags.ParticleEmitterGraph, - Size = new Float2(200, 30), + Size = new Float2(170, 30), Elements = new[] { NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 0), @@ -533,9 +535,10 @@ namespace FlaxEditor.Surface.Archetypes { TypeID = 110, Title = "Particle Normalized Age", - Description = "Particle normalized age to range 0-1 (age divided by lifetime).", + Description = "The normalized age of the particle, represented as 0 (max lifetime) to 1 (max age). (Same as age divided by lifetime.)", + AlternativeTitles = new[] { "Lifetime" }, Flags = NodeFlags.MaterialGraph | NodeFlags.ParticleEmitterGraph, - Size = new Float2(230, 30), + Size = new Float2(250, 30), Elements = new[] { NodeElementArchetype.Factory.Output(0, string.Empty, typeof(float), 0), diff --git a/Source/Editor/Surface/ContextMenu/VisjectCM.cs b/Source/Editor/Surface/ContextMenu/VisjectCM.cs index 21d0e7470..e1a61fd5f 100644 --- a/Source/Editor/Surface/ContextMenu/VisjectCM.cs +++ b/Source/Editor/Surface/ContextMenu/VisjectCM.cs @@ -585,7 +585,7 @@ namespace FlaxEditor.Surface.ContextMenu private void UpdateFilters() { - if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBoxes[0] == null) + if (string.IsNullOrEmpty(_searchBox.Text) && _selectedBoxes.Count == 0) { ResetView(); Profiler.EndEvent(); diff --git a/Source/Editor/Surface/Elements/OutputBox.cs b/Source/Editor/Surface/Elements/OutputBox.cs index 7e271fef0..8836dc0dc 100644 --- a/Source/Editor/Surface/Elements/OutputBox.cs +++ b/Source/Editor/Surface/Elements/OutputBox.cs @@ -34,11 +34,6 @@ namespace FlaxEditor.Surface.Elements /// public const float DefaultConnectionOffset = 24f; - /// - /// Distance for the mouse to be considered above the connection - /// - public float MouseOverConnectionDistance => 100f / Surface.ViewScale; - /// public OutputBox(SurfaceNode parentNode, NodeElementArchetype archetype) : base(parentNode, archetype, archetype.Position + new Float2(parentNode.Archetype.Size.X, 0)) @@ -109,12 +104,13 @@ namespace FlaxEditor.Surface.Elements /// /// The other box. /// The mouse position - public bool IntersectsConnection(Box targetBox, ref Float2 mousePosition) + /// Distance at which its an intersection + public bool IntersectsConnection(Box targetBox, ref Float2 mousePosition, float distance) { float connectionOffset = Mathf.Max(0f, DefaultConnectionOffset * (1 - Editor.Instance.Options.Options.Interface.ConnectionCurvature)); Float2 start = new Float2(ConnectionOrigin.X + connectionOffset, ConnectionOrigin.Y); Float2 end = new Float2(targetBox.ConnectionOrigin.X - connectionOffset, targetBox.ConnectionOrigin.Y); - return IntersectsConnection(ref start, ref end, ref mousePosition, MouseOverConnectionDistance); + return IntersectsConnection(ref start, ref end, ref mousePosition, distance); } /// @@ -182,7 +178,7 @@ namespace FlaxEditor.Surface.Elements { // Draw all the connections var style = Surface.Style; - var mouseOverDistance = MouseOverConnectionDistance; + var mouseOverDistance = Surface.MouseOverConnectionDistance; var startPos = ConnectionOrigin; var startHighlight = ConnectionsHighlightIntensity; for (int i = 0; i < Connections.Count; i++) diff --git a/Source/Editor/Surface/SurfaceUtils.cs b/Source/Editor/Surface/SurfaceUtils.cs index 7a19567fa..c30f0deae 100644 --- a/Source/Editor/Surface/SurfaceUtils.cs +++ b/Source/Editor/Surface/SurfaceUtils.cs @@ -573,13 +573,13 @@ namespace FlaxEditor.Surface var showSearch = () => editor.ContentFinding.ShowSearch(window); // Toolstrip - saveButton = toolStrip.AddButton(editor.Icons.Save64, window.Save).LinkTooltip("Save", ref inputOptions.Save); + saveButton = toolStrip.AddButton(editor.Icons.Save64, window.Save).LinkTooltip("Save.", ref inputOptions.Save); toolStrip.AddSeparator(); - undoButton = toolStrip.AddButton(editor.Icons.Undo64, undo.PerformUndo).LinkTooltip("Undo", ref inputOptions.Undo); - redoButton = toolStrip.AddButton(editor.Icons.Redo64, undo.PerformRedo).LinkTooltip("Redo", ref inputOptions.Redo); + undoButton = toolStrip.AddButton(editor.Icons.Undo64, undo.PerformUndo).LinkTooltip("Undo.", ref inputOptions.Undo); + redoButton = toolStrip.AddButton(editor.Icons.Redo64, undo.PerformRedo).LinkTooltip("Redo.", ref inputOptions.Redo); toolStrip.AddSeparator(); - toolStrip.AddButton(editor.Icons.Search64, showSearch).LinkTooltip("Open content search tool", ref inputOptions.Search); - toolStrip.AddButton(editor.Icons.CenterView64, surface.ShowWholeGraph).LinkTooltip("Show whole graph"); + toolStrip.AddButton(editor.Icons.Search64, showSearch).LinkTooltip("Open content search tool.", ref inputOptions.Search); + toolStrip.AddButton(editor.Icons.CenterView64, surface.ShowWholeGraph).LinkTooltip("Show whole graph."); var gridSnapButton = toolStrip.AddButton(editor.Icons.Grid32, surface.ToggleGridSnapping); gridSnapButton.LinkTooltip("Toggle grid snapping for nodes."); gridSnapButton.AutoCheck = true; diff --git a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs index d8dfb8ad3..ffb1c725a 100644 --- a/Source/Editor/Surface/VisjectSurface.ContextMenu.cs +++ b/Source/Editor/Surface/VisjectSurface.ContextMenu.cs @@ -410,8 +410,11 @@ namespace FlaxEditor.Surface } menu.AddSeparator(); - _cmFormatNodesMenu = menu.AddChildMenu("Format node(s)"); - _cmFormatNodesMenu.Enabled = CanEdit && HasNodesSelection; + bool allNodesNoMove = SelectedNodes.All(n => n.Archetype.Flags.HasFlag(NodeFlags.NoMove)); + bool clickedNodeNoMove = ((SelectedNodes.Count == 1 && controlUnderMouse is SurfaceNode n && n.Archetype.Flags.HasFlag(NodeFlags.NoMove))); + + _cmFormatNodesMenu = menu.AddChildMenu("Format nodes"); + _cmFormatNodesMenu.Enabled = CanEdit && HasNodesSelection && !(allNodesNoMove || clickedNodeNoMove); _cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Auto format", Editor.Instance.Options.Options.Input.NodesAutoFormat, () => { FormatGraph(SelectedNodes); }); _cmFormatNodesConnectionButton = _cmFormatNodesMenu.ContextMenu.AddButton("Straighten connections", Editor.Instance.Options.Options.Input.NodesStraightenConnections, () => { StraightenGraphConnections(SelectedNodes); }); diff --git a/Source/Editor/Surface/VisjectSurface.Draw.cs b/Source/Editor/Surface/VisjectSurface.Draw.cs index af5893907..a7d390132 100644 --- a/Source/Editor/Surface/VisjectSurface.Draw.cs +++ b/Source/Editor/Surface/VisjectSurface.Draw.cs @@ -213,6 +213,44 @@ namespace FlaxEditor.Surface } } + /// + /// Draw connection hints for lazy connect feature. + /// + 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); + } + /// /// Draws the contents of the surface (nodes, connections, comments, etc.). /// @@ -260,6 +298,9 @@ namespace FlaxEditor.Surface DrawContents(); + if (_isLazyConnecting) + DrawLazyConnect(); + //Render2D.DrawText(style.FontTitle, string.Format("Scale: {0}", _rootControl.Scale), rect, Enabled ? Color.Red : Color.Black); // Draw border diff --git a/Source/Editor/Surface/VisjectSurface.Formatting.cs b/Source/Editor/Surface/VisjectSurface.Formatting.cs index e1b9a6777..e55a4eb8a 100644 --- a/Source/Editor/Surface/VisjectSurface.Formatting.cs +++ b/Source/Editor/Surface/VisjectSurface.Formatting.cs @@ -39,6 +39,8 @@ namespace FlaxEditor.Surface if (nodes.Count <= 1) return; + List undoActions = new List(); + var nodesToVisit = new HashSet(nodes); // While we haven't formatted every node @@ -73,18 +75,23 @@ namespace FlaxEditor.Surface } } - FormatConnectedGraph(connectedNodes); + undoActions.AddRange(FormatConnectedGraph(connectedNodes)); } + + Undo?.AddAction(new MultiUndoAction(undoActions, "Format nodes")); + MarkAsEdited(false); } /// /// Formats a graph where all nodes are connected. /// /// List of connected nodes. - protected void FormatConnectedGraph(List nodes) + private List FormatConnectedGraph(List nodes) { + List undoActions = new List(); + if (nodes.Count <= 1) - return; + return undoActions; var boundingBox = GetNodesBounds(nodes); @@ -140,7 +147,6 @@ namespace FlaxEditor.Surface } // Set the node positions - var undoActions = new List(); var topRightPosition = endNodes[0].Location; for (int i = 0; i < nodes.Count; i++) { @@ -155,16 +161,18 @@ namespace FlaxEditor.Surface } } - MarkAsEdited(false); - Undo?.AddAction(new MultiUndoAction(undoActions, "Format nodes")); + return undoActions; } /// /// Straightens every connection between nodes in . /// /// List of nodes. + /// List of undo actions. public void StraightenGraphConnections(List nodes) - { + { + nodes = nodes.Where(n => !n.Archetype.Flags.HasFlag(NodeFlags.NoMove)).ToList(); + if (nodes.Count <= 1) return; @@ -350,8 +358,10 @@ namespace FlaxEditor.Surface /// List of nodes. /// Alignemnt type. public void AlignNodes(List nodes, NodeAlignmentType alignmentType) - { - if(nodes.Count <= 1) + { + nodes = nodes.Where(n => !n.Archetype.Flags.HasFlag(NodeFlags.NoMove)).ToList(); + + if (nodes.Count <= 1) return; var undoActions = new List(); @@ -392,6 +402,8 @@ namespace FlaxEditor.Surface /// If false will be done horizontally, if true will be done vertically. public void DistributeNodes(List nodes, bool vertically) { + nodes = nodes.Where(n => !n.Archetype.Flags.HasFlag(NodeFlags.NoMove)).ToList(); + if(nodes.Count <= 1) return; diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index 056987e52..c2f474865 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; +using static FlaxEditor.Surface.Archetypes.Particles; using FlaxEditor.Options; using FlaxEditor.Surface.Elements; using FlaxEditor.Surface.Undo; @@ -23,12 +24,26 @@ namespace FlaxEditor.Surface /// public bool PanWithMiddleMouse = false; + /// + /// Distance for the mouse to be considered above the connection. + /// + public float MouseOverConnectionDistance => 100f / ViewScale; + + /// + /// Distance of a node from which it is able to be slotted into an existing connection. + /// + public float SlotNodeIntoConnectionDistance => 250f / ViewScale; + private string _currentInputText = string.Empty; private Float2 _movingNodesDelta; private Float2 _gridRoundingDelta; private HashSet _movingNodes; private HashSet _temporarySelectedNodes; private readonly Stack _inputBrackets = new Stack(); + private bool _isLazyConnecting; + private SurfaceNode _lazyConnectStartNode; + private SurfaceNode _lazyConnectEndNode; + private InputBinding _focusSelectedNodeBinding; private class InputBracket { @@ -250,8 +265,13 @@ namespace FlaxEditor.Surface // Cache mouse location _mousePos = location; + if (_isLazyConnecting && GetControlUnderMouse() is SurfaceNode nodeUnderMouse && !(nodeUnderMouse is SurfaceComment || nodeUnderMouse is ParticleEmitterNode)) + _lazyConnectEndNode = nodeUnderMouse; + else if (_isLazyConnecting && Nodes.Count > 0) + _lazyConnectEndNode = GetClosestNodeAtLocation(location); + // Moving around surface with mouse - if (_rightMouseDown) + if (_rightMouseDown && !_isLazyConnecting) { // Calculate delta var delta = location - _rightMouseDownPos; @@ -321,6 +341,33 @@ namespace FlaxEditor.Surface foreach (var node in _movingNodes) { + // Allow ripping the node from its current connection + if (RootWindow.GetKey(KeyboardKeys.Alt)) + { + InputBox nodeConnectedInput = null; + OutputBox nodeConnectedOuput = null; + + var boxes = node.GetBoxes(); + foreach (var box in boxes) + { + if (!box.IsOutput && box.Connections.Count > 0) + { + nodeConnectedInput = (InputBox)box; + continue; + } + if (box.IsOutput && box.Connections.Count > 0) + { + nodeConnectedOuput = (OutputBox)box; + continue; + } + } + + if (nodeConnectedInput != null && nodeConnectedOuput != null) + TryConnect(nodeConnectedOuput.Connections[0], nodeConnectedInput.Connections[0]); + + node.RemoveConnections(); + } + if (gridSnap) { Float2 unroundedLocation = node.Location; @@ -420,7 +467,7 @@ namespace FlaxEditor.Surface if (!handled && CanEdit && CanUseNodeType(7, 29)) { var mousePos = _rootControl.PointFromParent(ref _mousePos); - if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox) && GetControlUnderMouse() == null) + if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox, MouseOverConnectionDistance) && GetControlUnderMouse() == null) { if (Undo != null) { @@ -515,11 +562,17 @@ namespace FlaxEditor.Surface _middleMouseDownPos = location; } + if (root.GetKey(KeyboardKeys.Alt) && button == MouseButton.Right) + _isLazyConnecting = true; + // Check if any node is under the mouse SurfaceControl controlUnderMouse = GetControlUnderMouse(); var cLocation = _rootControl.PointFromParent(ref location); if (controlUnderMouse != null) { + if (controlUnderMouse is SurfaceNode node && _isLazyConnecting && !(controlUnderMouse is SurfaceComment || controlUnderMouse is ParticleEmitterNode)) + _lazyConnectStartNode = node; + // Check if mouse is over header and user is pressing mouse left button if (_leftMouseDown && controlUnderMouse.CanSelect(ref cLocation)) { @@ -554,6 +607,9 @@ namespace FlaxEditor.Surface } else { + if (_isLazyConnecting && Nodes.Count > 0) + _lazyConnectStartNode = GetClosestNodeAtLocation(location); + // Cache flags and state if (_leftMouseDown) { @@ -602,8 +658,71 @@ namespace FlaxEditor.Surface { if (_movingNodes != null && _movingNodes.Count > 0) { + // Allow dropping a single node onto an existing connection and connect it + if (_movingNodes.Count == 1) + { + var mousePos = _rootControl.PointFromParent(ref _mousePos); + InputBox intersectedConnectionInputBox; + OutputBox intersectedConnectionOutputBox; + if (IntersectsConnection(mousePos, out intersectedConnectionInputBox, out intersectedConnectionOutputBox, SlotNodeIntoConnectionDistance)) + { + SurfaceNode node = _movingNodes.First(); + InputBox nodeInputBox = (InputBox)node.GetBoxes().First(b => !b.IsOutput); + OutputBox nodeOutputBox = (OutputBox)node.GetBoxes().First(b => b.IsOutput); + TryConnect(intersectedConnectionOutputBox, nodeInputBox); + TryConnect(nodeOutputBox, intersectedConnectionInputBox); + + float intersectedConnectionNodesXDistance = intersectedConnectionInputBox.ParentNode.Left - intersectedConnectionOutputBox.ParentNode.Right; + float paddedNodeWidth = node.Width + 2f; + if (intersectedConnectionNodesXDistance < paddedNodeWidth) + { + List visitedNodes = new List{ node }; + List movedNodes = new List(); + Float2 locationDelta = new Float2(paddedNodeWidth, 0f); + + MoveConnectedNodes(intersectedConnectionInputBox.ParentNode); + + void MoveConnectedNodes(SurfaceNode node) + { + // Only move node if it is to the right of the node we have connected the moved node to + if (node.Right > intersectedConnectionInputBox.ParentNode.Left + 15f && !node.Archetype.Flags.HasFlag(NodeFlags.NoMove)) + { + node.Location += locationDelta; + movedNodes.Add(node); + } + + visitedNodes.Add(node); + + foreach (var box in node.GetBoxes()) + { + if (!box.HasAnyConnection || box == intersectedConnectionInputBox) + continue; + + foreach (var connectedBox in box.Connections) + { + SurfaceNode nextNode = connectedBox.ParentNode; + if (visitedNodes.Contains(nextNode)) + continue; + + MoveConnectedNodes(nextNode); + } + } + } + + Float2 nodeMoveOffset = new Float2(node.Width * 0.5f, 0f); + node.Location += nodeMoveOffset; + + var moveNodesAction = new MoveNodesAction(Context, movedNodes.Select(n => n.ID).ToArray(), locationDelta); + var moveNodeAction = new MoveNodesAction(Context, [node.ID], nodeMoveOffset); + var multiAction = new MultiUndoAction(moveNodeAction, moveNodesAction); + + AddBatchedUndoAction(multiAction); + } + } + } + if (Undo != null && !_movingNodesDelta.IsZero && CanEdit) - Undo.AddAction(new MoveNodesAction(Context, _movingNodes.Select(x => x.ID).ToArray(), _movingNodesDelta)); + AddBatchedUndoAction(new MoveNodesAction(Context, _movingNodes.Select(x => x.ID).ToArray(), _movingNodesDelta)); _movingNodes.Clear(); } _movingNodesDelta = Float2.Zero; @@ -630,12 +749,36 @@ namespace FlaxEditor.Surface { // Check if any control is under the mouse _cmStartPos = location; - if (controlUnderMouse == null) + if (controlUnderMouse == null && !_isLazyConnecting) { showPrimaryMenu = true; } } _mouseMoveAmount = 0; + + if (_isLazyConnecting) + { + if (_lazyConnectStartNode != null && _lazyConnectEndNode != null && _lazyConnectStartNode != _lazyConnectEndNode) + { + // First check if there is a type matching input and output where input + OutputBox startNodeOutput = (OutputBox)_lazyConnectStartNode.GetBoxes().FirstOrDefault(b => b.IsOutput, null); + InputBox endNodeInput = null; + + if (startNodeOutput != null) + endNodeInput = (InputBox)_lazyConnectEndNode.GetBoxes().FirstOrDefault(b => !b.IsOutput && b.CurrentType == startNodeOutput.CurrentType && !b.HasAnyConnection && b.IsActive && b.CanConnectWith(startNodeOutput), null); + + // Perform less strict checks (less ideal conditions for connection but still good) if the first checks failed + if (endNodeInput == null) + endNodeInput = (InputBox)_lazyConnectEndNode.GetBoxes().FirstOrDefault(b => !b.IsOutput && !b.HasAnyConnection && b.CanConnectWith(startNodeOutput), null); + + if (startNodeOutput != null && endNodeInput != null) + TryConnect(startNodeOutput, endNodeInput); + } + + _isLazyConnecting = false; + _lazyConnectStartNode = null; + _lazyConnectEndNode = null; + } } if (_middleMouseDown && button == MouseButton.Middle) { @@ -651,7 +794,7 @@ namespace FlaxEditor.Surface { // Surface was not moved with MMB so try to remove connection underneath var mousePos = _rootControl.PointFromParent(ref location); - if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox)) + if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox, MouseOverConnectionDistance)) { var action = new EditNodeConnections(inputBox.ParentNode.Context, inputBox.ParentNode); inputBox.BreakConnection(outputBox); @@ -704,13 +847,21 @@ namespace FlaxEditor.Surface private void MoveSelectedNodes(Float2 delta) { - // TODO: undo + List undoActions = new List(); + delta /= _targetScale; OnGetNodesToMove(); foreach (var node in _movingNodes) + { node.Location += delta; + if (Undo != null) + undoActions.Add(new MoveNodesAction(Context, new[] { node.ID }, delta)); + } _isMovingSelection = false; MarkAsEdited(false); + + if (undoActions.Count > 0) + Undo?.AddAction(new MultiUndoAction(undoActions, "Moved ")); } /// @@ -837,6 +988,29 @@ namespace FlaxEditor.Surface return false; } + private SurfaceNode GetClosestNodeAtLocation(Float2 location) + { + SurfaceNode currentClosestNode = null; + float currentClosestDistanceSquared = float.MaxValue; + + foreach (var node in Nodes) + { + if (node is SurfaceComment || node is ParticleEmitterNode) + continue; + + Float2 nodeSurfaceLocation = _rootControl.PointToParent(node.Center); + + float distanceSquared = Float2.DistanceSquared(location, nodeSurfaceLocation); + if (distanceSquared < currentClosestDistanceSquared) + { + currentClosestNode = node; + currentClosestDistanceSquared = distanceSquared; + } + } + + return currentClosestNode; + } + private void ResetInput() { InputText = ""; @@ -845,7 +1019,8 @@ namespace FlaxEditor.Surface private void CurrentInputTextChanged(string currentInputText) { - if (string.IsNullOrEmpty(currentInputText)) + // Check if focus selected nodes binding is being pressed to prevent it triggering primary menu + if (string.IsNullOrEmpty(currentInputText) || _focusSelectedNodeBinding.Process(RootWindow)) return; if (IsPrimaryMenuOpened || !CanEdit) { @@ -1025,7 +1200,7 @@ namespace FlaxEditor.Surface return new Float2(xLocation, yLocation); } - private bool IntersectsConnection(Float2 mousePosition, out InputBox inputBox, out OutputBox outputBox) + private bool IntersectsConnection(Float2 mousePosition, out InputBox inputBox, out OutputBox outputBox, float distance) { for (int i = 0; i < Nodes.Count; i++) { @@ -1035,7 +1210,7 @@ namespace FlaxEditor.Surface { for (int k = 0; k < ob.Connections.Count; k++) { - if (ob.IntersectsConnection(ob.Connections[k], ref mousePosition)) + if (ob.IntersectsConnection(ob.Connections[k], ref mousePosition, distance)) { outputBox = ob; inputBox = ob.Connections[k] as InputBox; diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index e3bb94bcc..9f326f36c 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -423,8 +423,9 @@ namespace FlaxEditor.Surface new InputActionsContainer.Binding(options => options.NodesAlignLeft, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Left); }), new InputActionsContainer.Binding(options => options.NodesAlignCenter, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Center); }), new InputActionsContainer.Binding(options => options.NodesAlignRight, () => { AlignNodes(SelectedNodes, NodeAlignmentType.Right); }), - new InputActionsContainer.Binding(options => options.NodesDistributeHorizontal, () => { DistributeNodes(SelectedNodes, false); }), - new InputActionsContainer.Binding(options => options.NodesDistributeVertical, () => { DistributeNodes(SelectedNodes, true); }), + new InputActionsContainer.Binding(options => options.NodesDistributeHorizontal, () => { DistributeNodes(SelectedNodes, false); }), + new InputActionsContainer.Binding(options => options.NodesDistributeVertical, () => { DistributeNodes(SelectedNodes, true); }), + new InputActionsContainer.Binding(options => options.FocusSelectedNodes, () => { FocusSelectionOrWholeGraph(); }), }); Context.ControlSpawned += OnSurfaceControlSpawned; @@ -436,7 +437,10 @@ namespace FlaxEditor.Surface DragHandlers.Add(_dragAssets = new DragAssets(ValidateDragItem)); DragHandlers.Add(_dragParameters = new DragNames(SurfaceParameter.DragPrefix, ValidateDragParameter)); + OnEditorOptionsChanged(Editor.Instance.Options.Options); + ScriptsBuilder.ScriptsReloadBegin += OnScriptsReloadBegin; + Editor.Instance.Options.OptionsChanged += OnEditorOptionsChanged; } private void OnScriptsReloadBegin() @@ -446,6 +450,11 @@ namespace FlaxEditor.Surface _cmPrimaryMenu = null; } + private void OnEditorOptionsChanged(EditorOptions options) + { + _focusSelectedNodeBinding = options.Input.FocusSelectedNodes; + } + /// /// Gets the display name of the connection type used in the surface. /// @@ -583,6 +592,11 @@ namespace FlaxEditor.Surface /// public virtual bool CanSetParameters => false; + /// + /// Gets a value indicating whether surface private parameters can be used, otherwise they will remain hidden. + /// + public virtual bool CanShowPrivateParameters => false; + /// /// True of the context menu should make use of a description panel drawn at the bottom of the menu /// @@ -643,6 +657,37 @@ namespace FlaxEditor.Surface ViewCenterPosition = areaRect.Center; } + /// + /// Adjusts the view to focus on the currently selected nodes, or the entire graph if no nodes are selected. + /// + public void FocusSelectionOrWholeGraph() + { + if (SelectedNodes.Count > 0) + ShowSelection(); + else + ShowWholeGraph(); + } + + /// + /// Shows the selected controls by changing the view scale and the position. + /// + 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); + } + /// /// Shows the given surface node by changing the view scale and the position and focuses the node. /// @@ -1066,6 +1111,7 @@ namespace FlaxEditor.Surface _cmPrimaryMenu?.Dispose(); ScriptsBuilder.ScriptsReloadBegin -= OnScriptsReloadBegin; + Editor.Instance.Options.OptionsChanged += OnEditorOptionsChanged; base.OnDestroy(); } diff --git a/Source/Editor/Surface/VisjectSurfaceContext.cs b/Source/Editor/Surface/VisjectSurfaceContext.cs index 0d10aa230..560a7c1d6 100644 --- a/Source/Editor/Surface/VisjectSurfaceContext.cs +++ b/Source/Editor/Surface/VisjectSurfaceContext.cs @@ -254,9 +254,10 @@ namespace FlaxEditor.Surface public SurfaceParameter GetParameter(Guid id) { SurfaceParameter result = null; - for (int i = 0; i < Parameters.Count; i++) + var parameters = Parameters; + for (int i = 0; i < parameters.Count; i++) { - var parameter = Parameters[i]; + var parameter = parameters[i]; if (parameter.ID == id) { result = parameter; @@ -274,9 +275,10 @@ namespace FlaxEditor.Surface public SurfaceParameter GetParameter(string name) { SurfaceParameter result = null; - for (int i = 0; i < Parameters.Count; i++) + var parameters = Parameters; + for (int i = 0; i < parameters.Count; i++) { - var parameter = Parameters[i]; + var parameter = parameters[i]; if (parameter.Name == name) { result = parameter; diff --git a/Source/Editor/Surface/VisjectSurfaceWindow.cs b/Source/Editor/Surface/VisjectSurfaceWindow.cs index 7c2cb23ad..3151f9b17 100644 --- a/Source/Editor/Surface/VisjectSurfaceWindow.cs +++ b/Source/Editor/Surface/VisjectSurfaceWindow.cs @@ -708,12 +708,30 @@ namespace FlaxEditor.Surface { var index = (int)label.Tag; menu.AddSeparator(); + menu.AddButton("Copy name", () => Clipboard.Text = ((IVisjectSurfaceWindow)Values[0]).VisjectSurface.Parameters[index].Name); + // TODO: move 'Copy all names' to context menu of the Properties category (as it's not item-specific) + menu.AddButton("Copy all names", CopyAllParameterNamesAsConstantCSharpCode); + menu.AddSeparator(); menu.AddButton("Rename", () => StartParameterRenaming(index, label)); menu.AddButton("Edit attributes...", () => EditAttributesParameter(index, label)); menu.AddButton("Delete", () => DeleteParameter(index)); OnParamContextMenu(index, menu); } + private void CopyAllParameterNamesAsConstantCSharpCode() + { + string allParamNames = ""; + foreach (var param in ((IVisjectSurfaceWindow)Values[0]).VisjectSurface.Parameters) + { + string cleanParamName = param.Name.Replace(" ", ""); + // Filter out headers and other non-parameter entries that can be present in the parameters list + if (string.IsNullOrEmpty(cleanParamName)) + continue; + allParamNames += $"private const string {cleanParamName}ParameterName = \"{param.Name}\";\n"; + } + Clipboard.Text = allParamNames; + } + private void StartParameterRenaming(int index, Control label) { var window = (IVisjectSurfaceWindow)Values[0]; diff --git a/Source/Editor/Surface/VisualScriptSurface.cs b/Source/Editor/Surface/VisualScriptSurface.cs index 2f70ee340..57aa29d67 100644 --- a/Source/Editor/Surface/VisualScriptSurface.cs +++ b/Source/Editor/Surface/VisualScriptSurface.cs @@ -187,6 +187,9 @@ namespace FlaxEditor.Surface /// public override bool CanSetParameters => true; + /// + public override bool CanShowPrivateParameters => true; + /// public override bool UseContextMenuDescriptionPanel => true; diff --git a/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs b/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs index 5fc0e894f..c6926f32e 100644 --- a/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs +++ b/Source/Editor/Tools/Terrain/EditTerrainGizmo.cs @@ -89,7 +89,7 @@ namespace FlaxEditor.Tools.Terrain if (!terrain.HasPatch(ref patchCoord) && _planeModel) { var planeSize = 100.0f; - var patchSize = terrain.ChunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount; + var patchSize = terrain.PatchSize; Matrix world = Matrix.RotationX(-Mathf.PiOverTwo) * Matrix.Scaling(patchSize / planeSize) * Matrix.Translation(patchSize * (0.5f + patchCoord.X), 0, patchSize * (0.5f + patchCoord.Y)) * diff --git a/Source/Editor/Tools/Terrain/Paint/Mode.cs b/Source/Editor/Tools/Terrain/Paint/Mode.cs index cee3157d3..624a74c3f 100644 --- a/Source/Editor/Tools/Terrain/Paint/Mode.cs +++ b/Source/Editor/Tools/Terrain/Paint/Mode.cs @@ -69,9 +69,9 @@ namespace FlaxEditor.Tools.Terrain.Paint var splatmapIndex = ActiveSplatmapIndex; var splatmapIndexOther = (splatmapIndex + 1) % 2; var chunkSize = terrain.ChunkSize; - var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1; + var heightmapSize = terrain.HeightmapSize; var heightmapLength = heightmapSize * heightmapSize; - var patchSize = chunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount; + var patchSize = terrain.PatchSize; var tempBuffer = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes, splatmapIndex).ToPointer(); var tempBufferOther = (Color32*)gizmo.GetSplatmapTempBuffer(heightmapLength * Color32.SizeInBytes, (splatmapIndex + 1) % 2).ToPointer(); var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex; diff --git a/Source/Editor/Tools/Terrain/Sculpt/Mode.cs b/Source/Editor/Tools/Terrain/Sculpt/Mode.cs index f37902f39..55d11b21a 100644 --- a/Source/Editor/Tools/Terrain/Sculpt/Mode.cs +++ b/Source/Editor/Tools/Terrain/Sculpt/Mode.cs @@ -70,9 +70,9 @@ namespace FlaxEditor.Tools.Terrain.Sculpt // Prepare var chunkSize = terrain.ChunkSize; - var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1; + var heightmapSize = terrain.HeightmapSize; var heightmapLength = heightmapSize * heightmapSize; - var patchSize = chunkSize * FlaxEngine.Terrain.UnitsPerVertex * FlaxEngine.Terrain.PatchEdgeChunksCount; + var patchSize = terrain.PatchSize; var tempBuffer = (float*)gizmo.GetHeightmapTempBuffer(heightmapLength * sizeof(float)).ToPointer(); var unitsPerVertexInv = 1.0f / FlaxEngine.Terrain.UnitsPerVertex; diff --git a/Source/Editor/Tools/Terrain/TerrainTools.cpp b/Source/Editor/Tools/Terrain/TerrainTools.cpp index 861cb975b..8e83e77cf 100644 --- a/Source/Editor/Tools/Terrain/TerrainTools.cpp +++ b/Source/Editor/Tools/Terrain/TerrainTools.cpp @@ -382,7 +382,8 @@ bool TerrainTools::ExportTerrain(Terrain* terrain, String outputFolder) const Int2 heightmapSize = size * Terrain::ChunksCountEdge * terrain->GetChunkSize() + 1; Array heightmap; heightmap.Resize(heightmapSize.X * heightmapSize.Y); - heightmap.SetAll(firstPatch->GetHeightmapData()[0]); + if (const float* heightmapData = firstPatch->GetHeightmapData()) + heightmap.SetAll(heightmapData[0]); // Fill heightmap with data from all patches const int32 rowSize = terrain->GetChunkSize() * Terrain::ChunksCountEdge + 1; @@ -392,8 +393,16 @@ bool TerrainTools::ExportTerrain(Terrain* terrain, String outputFolder) const Int2 pos(patch->GetX() - start.X, patch->GetZ() - start.Y); const float* src = patch->GetHeightmapData(); float* dst = heightmap.Get() + pos.X * (rowSize - 1) + pos.Y * heightmapSize.X * (rowSize - 1); - for (int32 row = 0; row < rowSize; row++) - Platform::MemoryCopy(dst + row * heightmapSize.X, src + row * rowSize, rowSize * sizeof(float)); + if (src) + { + for (int32 row = 0; row < rowSize; row++) + Platform::MemoryCopy(dst + row * heightmapSize.X, src + row * rowSize, rowSize * sizeof(float)); + } + else + { + for (int32 row = 0; row < rowSize; row++) + Platform::MemoryClear(dst + row * heightmapSize.X, rowSize * sizeof(float)); + } } // Interpolate to 16-bit int diff --git a/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs b/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs index afac0948e..93fc0470e 100644 --- a/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs +++ b/Source/Editor/Tools/Terrain/Undo/EditTerrainMapAction.cs @@ -85,8 +85,7 @@ namespace FlaxEditor.Tools.Terrain.Undo { _terrain = terrain.ID; _patches = new List(4); - var chunkSize = terrain.ChunkSize; - var heightmapSize = chunkSize * FlaxEngine.Terrain.PatchEdgeChunksCount + 1; + var heightmapSize = terrain.HeightmapSize; _heightmapLength = heightmapSize * heightmapSize; _heightmapDataSize = _heightmapLength * stride; diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index 1e7d2295a..ddcee7084 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -163,7 +163,6 @@ namespace FlaxEditor.Viewport private bool _useMouseAcceleration; // Input - internal bool _disableInputUpdate; private bool _isControllingMouse, _isViewportControllingMouse, _wasVirtualMouseRightDown, _isVirtualMouseRightDown; private int _deltaFilteringStep; @@ -247,8 +246,8 @@ namespace FlaxEditor.Viewport { _movementSpeed = value; - if (_cameraButton != null) - _cameraButton.Text = string.Format(MovementSpeedTextFormat, _movementSpeed); + if (_orthographicModeButton != null) + _orthographicModeButton.Text = string.Format(MovementSpeedTextFormat, _movementSpeed); } } @@ -581,7 +580,7 @@ namespace FlaxEditor.Viewport // Camera Settings Menu var cameraCM = new ContextMenu(); - _cameraButton = new ViewportWidgetButton(string.Format(MovementSpeedTextFormat, _movementSpeed), _editor.Icons.Camera64, cameraCM, false, cameraSpeedTextWidth) + _cameraButton = new ViewportWidgetButton("", _editor.Icons.Camera64, cameraCM) { Tag = this, TooltipText = "Camera Settings.", @@ -590,7 +589,7 @@ namespace FlaxEditor.Viewport _cameraWidget.Parent = this; // Orthographic/Perspective Mode Widget - _orthographicModeButton = new ViewportWidgetButton(string.Empty, _editor.Icons.CamSpeed32, null, true) + _orthographicModeButton = new OrthoCamToggleViewportWidgetButton(cameraSpeedTextWidth) { Checked = !_isOrtho, TooltipText = "Toggle Orthographic/Perspective Mode.", @@ -1223,7 +1222,7 @@ namespace FlaxEditor.Viewport /// Orients the viewport. /// /// The orientation. - protected void OrientViewport(Quaternion orientation) + public void OrientViewport(Quaternion orientation) { OrientViewport(ref orientation); } @@ -1232,7 +1231,7 @@ namespace FlaxEditor.Viewport /// Orients the viewport. /// /// The orientation. - protected virtual void OrientViewport(ref Quaternion orientation) + public virtual void OrientViewport(ref Quaternion orientation) { if (ViewportCamera is FPSCamera fpsCamera) { diff --git a/Source/Editor/Viewport/MainEditorGizmoViewport.cs b/Source/Editor/Viewport/MainEditorGizmoViewport.cs index 3d366809c..6a2a78543 100644 --- a/Source/Editor/Viewport/MainEditorGizmoViewport.cs +++ b/Source/Editor/Viewport/MainEditorGizmoViewport.cs @@ -1,18 +1,19 @@ // Copyright (c) Wojciech Figat. All rights reserved. -using System; using System.Collections.Generic; +using Object = FlaxEngine.Object; using FlaxEditor.Content; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.Options; using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; using FlaxEditor.Viewport.Modes; +using FlaxEditor.Viewport.Widgets; using FlaxEditor.Windows; using FlaxEngine; using FlaxEngine.Gizmo; using FlaxEngine.GUI; -using Object = FlaxEngine.Object; namespace FlaxEditor.Viewport { @@ -26,6 +27,7 @@ namespace FlaxEditor.Viewport private readonly ContextMenuButton _showGridButton; private readonly ContextMenuButton _showNavigationButton; private readonly ContextMenuButton _toggleGameViewButton; + private readonly ContextMenuButton _showDirectionGizmoButton; private SelectionOutline _customSelectionOutline; /// @@ -108,13 +110,14 @@ namespace FlaxEditor.Viewport private readonly ViewportDebugDrawData _debugDrawData = new ViewportDebugDrawData(32); private EditorSpritesRenderer _editorSpritesRenderer; private ViewportRubberBandSelector _rubberBandSelector; + private DirectionGizmo _directionGizmo; private bool _gameViewActive; private ViewFlags _preGameViewFlags; private ViewMode _preGameViewViewMode; private bool _gameViewWasGridShown; private bool _gameViewWasFpsCounterShown; - private bool _gameViewWasNagivationShown; + private bool _gameViewWasNavigationShown; /// /// Drag and drop handlers @@ -226,6 +229,13 @@ namespace FlaxEditor.Viewport // Add rubber band selector _rubberBandSelector = new ViewportRubberBandSelector(this); + // Add direction gizmo + _directionGizmo = new DirectionGizmo(this) + { + AnchorPreset = AnchorPresets.TopRight, + Parent = this, + }; + // Add grid Grid = new GridGizmo(this); Grid.EnabledChanged += gizmo => _showGridButton.Icon = gizmo.Enabled ? Style.Current.CheckBoxTick : SpriteHandle.Invalid; @@ -244,6 +254,11 @@ namespace FlaxEditor.Viewport _showNavigationButton = ViewWidgetShowMenu.AddButton("Navigation", inputOptions.ToggleNavMeshVisibility, () => ShowNavigation = !ShowNavigation); _showNavigationButton.CloseMenuOnClick = false; + // Show direction gizmo widget + _showDirectionGizmoButton = ViewWidgetShowMenu.AddButton("Direction Gizmo", () => _directionGizmo.Visible = !_directionGizmo.Visible); + _showDirectionGizmoButton.AutoCheck = true; + _showDirectionGizmoButton.CloseMenuOnClick = false; + // Game View ViewWidgetButtonMenu.AddSeparator(); _toggleGameViewButton = ViewWidgetButtonMenu.AddButton("Game View", inputOptions.ToggleGameView, ToggleGameView); @@ -277,6 +292,18 @@ namespace FlaxEditor.Viewport // Game View InputActions.Add(options => options.ToggleGameView, ToggleGameView); + + editor.Options.OptionsChanged += OnEditorOptionsChanged; + OnEditorOptionsChanged(editor.Options.Options); + } + + private void OnEditorOptionsChanged(EditorOptions options) + { + _directionGizmo.Visible = options.Viewport.ShowDirectionGizmo; + _showDirectionGizmoButton.Checked = _directionGizmo.Visible; + _directionGizmo.Size = new Float2(DirectionGizmo.DefaultGizmoSize * options.Viewport.DirectionGizmoScale); + _directionGizmo.LocalX = -_directionGizmo.Size.X * 0.5f; + _directionGizmo.LocalY = _directionGizmo.Size.Y * 0.5f + ViewportWidgetsContainer.WidgetsHeight; } /// @@ -514,14 +541,14 @@ namespace FlaxEditor.Viewport _preGameViewViewMode = Task.ViewMode; _gameViewWasGridShown = Grid.Enabled; _gameViewWasFpsCounterShown = ShowFpsCounter; - _gameViewWasNagivationShown = ShowNavigation; + _gameViewWasNavigationShown = ShowNavigation; } // Set flags & values Task.ViewFlags = _gameViewActive ? _preGameViewFlags : ViewFlags.DefaultGame; Task.ViewMode = _gameViewActive ? _preGameViewViewMode : ViewMode.Default; ShowFpsCounter = _gameViewActive ? _gameViewWasFpsCounterShown : false; - ShowNavigation = _gameViewActive ? _gameViewWasNagivationShown : false; + ShowNavigation = _gameViewActive ? _gameViewWasNavigationShown : false; Grid.Enabled = _gameViewActive ? _gameViewWasGridShown : false; _gameViewActive = !_gameViewActive; @@ -647,7 +674,7 @@ namespace FlaxEditor.Viewport } /// - protected override void OrientViewport(ref Quaternion orientation) + public override void OrientViewport(ref Quaternion orientation) { if (TransformGizmo.SelectedParents.Count != 0) FocusSelection(ref orientation); diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index 7f2b1a471..a599615de 100644 --- a/Source/Editor/Viewport/PrefabWindowViewport.cs +++ b/Source/Editor/Viewport/PrefabWindowViewport.cs @@ -6,7 +6,6 @@ using System.Linq; using FlaxEditor.Content; using FlaxEditor.Gizmo; using FlaxEditor.GUI.ContextMenu; -using FlaxEditor.GUI.Input; using FlaxEditor.Modules; using FlaxEditor.SceneGraph; using FlaxEditor.Scripting; @@ -15,7 +14,6 @@ using FlaxEditor.Viewport.Previews; using FlaxEditor.Windows.Assets; using FlaxEngine; using FlaxEngine.GUI; -using FlaxEngine.Json; using Utils = FlaxEditor.Utilities.Utils; namespace FlaxEditor.Viewport @@ -185,6 +183,7 @@ namespace FlaxEditor.Viewport showGridButton.Clicked += () => { _gridGizmo.Enabled = !_gridGizmo.Enabled; + _uiRoot.ShowGrid = _gridGizmo.Enabled; showGridButton.Checked = _gridGizmo.Enabled; }; showGridButton.Checked = true; @@ -682,7 +681,7 @@ namespace FlaxEditor.Viewport } /// - protected override void OrientViewport(ref Quaternion orientation) + public override void OrientViewport(ref Quaternion orientation) { if (TransformGizmo.SelectedParents.Count != 0) FocusSelection(ref orientation); diff --git a/Source/Editor/Viewport/Previews/MaterialPreview.cs b/Source/Editor/Viewport/Previews/MaterialPreview.cs index 8ba8a00ee..e494df14a 100644 --- a/Source/Editor/Viewport/Previews/MaterialPreview.cs +++ b/Source/Editor/Viewport/Previews/MaterialPreview.cs @@ -303,8 +303,7 @@ namespace FlaxEditor.Viewport.Previews { _terrain = new Terrain(); _terrain.Setup(1, 63); - var chunkSize = _terrain.ChunkSize; - var heightMapSize = chunkSize * Terrain.PatchEdgeChunksCount + 1; + var heightMapSize = _terrain.HeightmapSize; var heightMapLength = heightMapSize * heightMapSize; var heightmap = new float[heightMapLength]; var patchCoord = new Int2(0, 0); diff --git a/Source/Editor/Viewport/ViewportDraggingHelper.cs b/Source/Editor/Viewport/ViewportDraggingHelper.cs index 21287fc94..3e6fce6ad 100644 --- a/Source/Editor/Viewport/ViewportDraggingHelper.cs +++ b/Source/Editor/Viewport/ViewportDraggingHelper.cs @@ -198,6 +198,11 @@ namespace FlaxEditor.Viewport actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation); _owner.Spawn(actor); _viewport.Focus(); + + // Scroll to the new actor in the hierarchy + var actorNode = Editor.Instance.Scene.GetActorNode(actor); + if (actorNode?.TreeNode.ParentTree.Parent is Panel treePanel) + FlaxEngine.Scripting.InvokeOnUpdate(() => treePanel.ScrollViewTo(actorNode.TreeNode)); } private void Spawn(ScriptItem item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal) diff --git a/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs b/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs index 55c3e9c71..dd09dc24c 100644 --- a/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs +++ b/Source/Editor/Viewport/Widgets/ViewportWidgetButton.cs @@ -7,6 +7,100 @@ using FlaxEngine.GUI; namespace FlaxEditor.Viewport.Widgets { + /// + /// Otrhographic view toggle viewport Widget Button class. + /// Will draw a custom camera frustum to represent an orthographic or perspective camera. + /// + /// + [HideInEditor] + public class OrthoCamToggleViewportWidgetButton : ViewportWidgetButton + { + private const int iconPointCount = 4; + private const float iconRenderScale = 4.0f; + + private readonly Float2 iconDrawOffset = new Float2(3.0f, 3.0f); + + private readonly Float2[] iconPointsPerspective = new[] + { + new Float2(0.0f, 1.0f), // Top left + new Float2(4.0f, 0.0f), // Top right + new Float2(4.0f, 3.0f), // Bottom right + new Float2(0.0f, 2.0f), // Bottom left + }; + + private readonly Float2[] iconPointsOrtho = new[] + { + new Float2(0.0f, 0.0f), // Top left + new Float2(4.0f, 0.0f), // Top right + new Float2(4.0f, 3.0f), // Bottom right + new Float2(0.0f, 3.0f), // Bottom left + }; + + private bool wasChecked; + private float lerpWeight; + + /// + /// Initializes a new instance of the class. + /// + public OrthoCamToggleViewportWidgetButton(float speedTextWidth) + : base("00.0", SpriteHandle.Invalid, null, true, 20.0f + speedTextWidth) + { + wasChecked = Checked; + } + + /// + public override void Update(float deltaTime) + { + if (wasChecked != Checked) + { + lerpWeight = 0.0f; + wasChecked = Checked; + } + + if (lerpWeight <= 1.0f) + lerpWeight += deltaTime * 4.2f; + + base.Update(deltaTime); + } + + /// + public override void Draw() + { + // Cache data + var style = Style.Current; + var textRect = new Rectangle(0.0f, 0.0f, Width - 2.0f, Height); + var backgroundRect = textRect with { Width = Width - 1.0f }; + + // Check if is checked or mouse is over + if (Checked) + Render2D.FillRectangle(backgroundRect, style.BackgroundSelected * (IsMouseOver ? 0.9f : 0.6f)); + else if (IsMouseOver) + Render2D.FillRectangle(backgroundRect, style.BackgroundHighlighted); + + // Draw text + Render2D.DrawText(style.FontMedium, Text, textRect, style.ForegroundViewport * (IsMouseOver ? 1.0f : 0.9f), TextAlignment.Far, TextAlignment.Center); + + // Draw camera frustum icon + Float2[] currentStart = Checked ? iconPointsOrtho : iconPointsPerspective; + Float2[] currentTarget = Checked ? iconPointsPerspective : iconPointsOrtho; + + var features = Render2D.Features; + Render2D.Features = features & ~Render2D.RenderingFeatures.VertexSnapping; + for (int i = 1; i < iconPointCount + 1; i++) + { + int endPointIndex = Mathf.Wrap(i, 0, iconPointCount - 1); + Float2 lineStart = Float2.Lerp(currentStart[i - 1], currentTarget[i - 1], lerpWeight); + Float2 lineEnd = Float2.Lerp(currentStart[endPointIndex], currentTarget[endPointIndex], lerpWeight); + + lineStart = lineStart * iconRenderScale + iconDrawOffset; + lineEnd = lineEnd * iconRenderScale + iconDrawOffset; + + Render2D.DrawLine(lineStart, lineEnd, Color.White); + } + Render2D.Features = features; + } + } + /// /// Viewport Widget Button class. /// diff --git a/Source/Editor/Windows/AboutDialog.cs b/Source/Editor/Windows/AboutDialog.cs index 1d9afbdfe..bd34802ce 100644 --- a/Source/Editor/Windows/AboutDialog.cs +++ b/Source/Editor/Windows/AboutDialog.cs @@ -2,6 +2,7 @@ //#define USE_AUTODESK_FBX_SDK using System.Collections.Generic; +using System.Reflection; using FlaxEditor.GUI.Dialogs; using FlaxEngine; using FlaxEngine.GUI; @@ -45,9 +46,16 @@ namespace FlaxEditor.Windows VerticalAlignment = TextAlignment.Center, Parent = this }; + var assembly = typeof(Editor).Assembly; + var assemblyCopyright = assembly.GetCustomAttribute(); + var assemblyInformationalVersion = assembly.GetCustomAttribute(); + var versionParts = assemblyInformationalVersion.InformationalVersion.Split('+'); + string versionInfo = string.Empty; + if (versionParts.Length == 3) + versionInfo = $"\nBranch: {versionParts[1]}+{(versionParts[2].Length == 40 ? versionParts[2].Substring(0, 8) : versionParts[2])}"; new Label(nameLabel.Left, nameLabel.Bottom + 4, nameLabel.Width, 50) { - Text = string.Format("Version: {0}\nCopyright (c) 2012-2025 Wojciech Figat.\nAll rights reserved.", Globals.EngineVersion), + Text = $"Version: {Globals.EngineVersion}{versionInfo}\n{assemblyCopyright.Copyright.Replace(". ", ".\n")}", HorizontalAlignment = TextAlignment.Near, VerticalAlignment = TextAlignment.Near, Parent = this @@ -98,6 +106,7 @@ namespace FlaxEditor.Windows "Chandler Cox", "Ari Vuollet", "Vincent Saarmann", + "Michael Salvini", }); authors.Sort(); var authorsLabel = new Label(4, topParentControl.Bottom + 20, Width - 8, 70) diff --git a/Source/Editor/Windows/Assets/AnimationWindow.cs b/Source/Editor/Windows/Assets/AnimationWindow.cs index 268e18503..69da1f9e9 100644 --- a/Source/Editor/Windows/Assets/AnimationWindow.cs +++ b/Source/Editor/Windows/Assets/AnimationWindow.cs @@ -431,6 +431,9 @@ namespace FlaxEditor.Windows.Assets _isWaitingForTimelineLoad = true; base.OnItemReimported(item); + + // Drop virtual asset state and get a new one from the reimported file + LoadFromOriginal(); } /// @@ -455,6 +458,7 @@ namespace FlaxEditor.Windows.Assets _timeline.Enabled = true; _timeline.SetNoTracksText(null); ClearEditedFlag(); + _timeline.ShowWholeTimeline(); } } diff --git a/Source/Editor/Windows/Assets/AssetEditorWindow.cs b/Source/Editor/Windows/Assets/AssetEditorWindow.cs index 646112441..93d6c850c 100644 --- a/Source/Editor/Windows/Assets/AssetEditorWindow.cs +++ b/Source/Editor/Windows/Assets/AssetEditorWindow.cs @@ -53,7 +53,7 @@ namespace FlaxEditor.Windows.Assets { Parent = this }; - _toolstrip.AddButton(editor.Icons.Search64, () => Editor.Windows.ContentWin.Select(_item)).LinkTooltip("Show and select in Content Window"); + _toolstrip.AddButton(editor.Icons.Search64, () => Editor.Windows.ContentWin.Select(_item)).LinkTooltip("Show and select in Content Window."); InputActions.Add(options => options.Save, Save); @@ -527,6 +527,16 @@ namespace FlaxEditor.Windows.Assets return false; } + /// + /// Loads the asset from the original location to reflect the state (eg. after original asset reimport). + /// + protected virtual void LoadFromOriginal() + { + _asset = LoadAsset(); + OnAssetLoaded(); + ClearEditedFlag(); + } + /// protected override T LoadAsset() { diff --git a/Source/Editor/Windows/Assets/FontWindow.cs b/Source/Editor/Windows/Assets/FontWindow.cs index 3f4a7a681..e08a87b05 100644 --- a/Source/Editor/Windows/Assets/FontWindow.cs +++ b/Source/Editor/Windows/Assets/FontWindow.cs @@ -21,6 +21,10 @@ namespace FlaxEditor.Windows.Assets /// private sealed class PropertiesProxy { + [DefaultValue(FontRasterMode.Bitmap)] + [EditorOrder(5), EditorDisplay("Properties"), Tooltip("The rasterization mode used when generating font atlases.")] + public FontRasterMode RasterMode; + [DefaultValue(FontHinting.Default)] [EditorOrder(10), EditorDisplay("Properties"), Tooltip("The font hinting used when rendering characters.")] public FontHinting Hinting; @@ -41,7 +45,8 @@ namespace FlaxEditor.Windows.Assets { options = new FontOptions { - Hinting = Hinting + Hinting = Hinting, + RasterMode = RasterMode, }; if (AntiAliasing) options.Flags |= FontFlags.AntiAliasing; @@ -57,6 +62,7 @@ namespace FlaxEditor.Windows.Assets AntiAliasing = (options.Flags & FontFlags.AntiAliasing) == FontFlags.AntiAliasing; Bold = (options.Flags & FontFlags.Bold) == FontFlags.Bold; Italic = (options.Flags & FontFlags.Italic) == FontFlags.Italic; + RasterMode = options.RasterMode; } } diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 28364e6dd..ad5a8caf2 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -39,8 +39,7 @@ namespace FlaxEditor.Windows folder = CurrentViewFolder; } Assert.IsNotNull(folder); - bool isRootFolder = CurrentViewFolder == _root.Folder; - + // Create context menu ContextMenuButton b; ContextMenu cm = new ContextMenu @@ -178,19 +177,61 @@ namespace FlaxEditor.Windows cm.AddSeparator(); + CreateNewModuleMenu(cm, folder); + CreateNewFolderMenu(cm, folder, false, item); + CreateNewContentItemMenu(cm, folder); + + if (folder.CanHaveAssets) + { + cm.AddButton("Import file", () => + { + _view.ClearSelection(); + Editor.ContentImporting.ShowImportFileDialog(CurrentViewFolder); + }); + } + + // Remove any leftover separator + if (cm.ItemsContainer.Children.LastOrDefault() is ContextMenuSeparator) + cm.ItemsContainer.Children.Last().Dispose(); + + // Show it + cm.Show(this, location); + } + + private void CreateNewModuleMenu(ContextMenu menu, ContentFolder folder, bool disableUncreatable = false) + { // Check if is source folder to add new module if (folder?.ParentFolder?.Node is ProjectFolderTreeNode parentFolderNode && folder.Node == parentFolderNode.Source) { - var button = cm.AddButton("New module"); + var button = menu.AddButton("New module"); button.CloseMenuOnClick = false; button.Clicked += () => NewModule(button, parentFolderNode.Source.Path); } - - if (!isRootFolder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectFolderTreeNode)) + else if (disableUncreatable) { - cm.AddButton("New folder", NewFolder); + var button = menu.AddButton("New module"); + button.Enabled = false; } + } + private bool CanCreateFolder(ContentItem item = null) + { + bool canCreateFolder = CurrentViewFolder != _root.Folder && !(item is ContentFolder projectFolder && projectFolder.Node is ProjectFolderTreeNode); + return canCreateFolder; + } + + private void CreateNewFolderMenu(ContextMenu menu, ContentFolder folder, bool disableUncreatable = false, ContentItem item = null) + { + bool canCreateFolder = CanCreateFolder(item); + if (canCreateFolder || disableUncreatable) + { + var b = menu.AddButton("New folder", NewFolder); + b.Enabled = canCreateFolder; + } + } + + private void CreateNewContentItemMenu(ContextMenu menu, ContentFolder folder, bool showNew = true, bool disableUncreatable = false) + { // Loop through each proxy and user defined json type and add them to the context menu var actorType = new ScriptType(typeof(Actor)); var scriptType = new ScriptType(typeof(Script)); @@ -230,7 +271,8 @@ namespace FlaxEditor.Windows if (p == null) continue; - if (p.CanCreate(folder)) + bool canCreate = p.CanCreate(folder); + if (canCreate || disableUncreatable) { var parts = attribute.Path.Split('/'); ContextMenuChildMenu childCM = null; @@ -238,16 +280,20 @@ namespace FlaxEditor.Windows for (int i = 0; i < parts?.Length; i++) { var part = parts[i].Trim(); + if (part == "New" && !showNew) + continue; if (i == parts.Length - 1) { if (mainCM) { - cm.AddButton(part, () => NewItem(p)); + var b = menu.AddButton(part, () => NewItem(p)); + b.Enabled = canCreate; mainCM = false; } else if (childCM != null) { - childCM.ContextMenu.AddButton(part, () => NewItem(p)); + var b = childCM.ContextMenu.AddButton(part, () => NewItem(p)); + b.Enabled = canCreate; childCM.ContextMenu.AutoSort = true; } } @@ -255,35 +301,21 @@ namespace FlaxEditor.Windows { if (mainCM) { - childCM = cm.GetOrAddChildMenu(part); + childCM = menu.GetOrAddChildMenu(part); childCM.ContextMenu.AutoSort = true; + childCM.Enabled = canCreate; mainCM = false; } else if (childCM != null) { childCM = childCM.ContextMenu.GetOrAddChildMenu(part); childCM.ContextMenu.AutoSort = true; + childCM.Enabled = canCreate; } } } } } - - if (folder.CanHaveAssets) - { - cm.AddButton("Import file", () => - { - _view.ClearSelection(); - Editor.ContentImporting.ShowImportFileDialog(CurrentViewFolder); - }); - } - - // Remove any leftover separator - if (cm.ItemsContainer.Children.LastOrDefault() is ContextMenuSeparator) - cm.ItemsContainer.Children.Last().Dispose(); - - // Show it - cm.Show(this, location); } private void OnExpandAllClicked(ContextMenuButton button) diff --git a/Source/Editor/Windows/ContentWindow.Search.cs b/Source/Editor/Windows/ContentWindow.Search.cs index 87564597a..af6cc67ef 100644 --- a/Source/Editor/Windows/ContentWindow.Search.cs +++ b/Source/Editor/Windows/ContentWindow.Search.cs @@ -116,6 +116,7 @@ namespace FlaxEditor.Windows var root = _root; root.LockChildrenRecursive(); _suppressExpandedStateSave = true; + PerformLayout(); // Update tree var query = _foldersSearchBox.Text; diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index 780be5c04..1a4b3667d 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -42,6 +42,7 @@ namespace FlaxEditor.Windows private readonly ToolStrip _toolStrip; private readonly ToolStripButton _importButton; + private readonly ToolStripButton _createNewButton; private readonly ToolStripButton _navigateBackwardButton; private readonly ToolStripButton _navigateForwardButton; private readonly ToolStripButton _navigateUpButton; @@ -167,11 +168,12 @@ namespace FlaxEditor.Windows { Parent = this, }; - _importButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Import64, () => Editor.ContentImporting.ShowImportFileDialog(CurrentViewFolder)).LinkTooltip("Import content"); + _importButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Import64, () => Editor.ContentImporting.ShowImportFileDialog(CurrentViewFolder)).LinkTooltip("Import content."); + _createNewButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Add64, OnCreateNewItemButtonClicked).LinkTooltip("Create a new asset. Shift + left click to create a new folder."); _toolStrip.AddSeparator(); - _navigateBackwardButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Left64, NavigateBackward).LinkTooltip("Navigate backward"); - _navigateForwardButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Right64, NavigateForward).LinkTooltip("Navigate forward"); - _navigateUpButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Up64, NavigateUp).LinkTooltip("Navigate up"); + _navigateBackwardButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Left64, NavigateBackward).LinkTooltip("Navigate backward."); + _navigateForwardButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Right64, NavigateForward).LinkTooltip("Navigate forward."); + _navigateUpButton = (ToolStripButton)_toolStrip.AddButton(Editor.Icons.Up64, NavigateUp).LinkTooltip("Navigate up."); _toolStrip.AddSeparator(); // Split panel @@ -319,6 +321,42 @@ namespace FlaxEditor.Windows ApplyTreeViewScale(); } + private void OnCreateNewItemButtonClicked() + { + if (Input.GetKey(KeyboardKeys.Shift) && CanCreateFolder()) + { + NewFolder(); + return; + } + + var menu = new ContextMenu(); + + InterfaceOptions interfaceOptions = Editor.Instance.Options.Options.Interface; + bool disableUnavaliable = interfaceOptions.UnavaliableContentCreateOptions == InterfaceOptions.DisabledHidden.Disabled; + + CreateNewFolderMenu(menu, CurrentViewFolder, disableUnavaliable); + CreateNewModuleMenu(menu, CurrentViewFolder, disableUnavaliable); + menu.AddSeparator(); + CreateNewContentItemMenu(menu, CurrentViewFolder, false, disableUnavaliable); + // Hack: Show the menu once to get the direction, then show it above or below the button depending on the direction. + menu.Show(this, _createNewButton.UpperLeft); + var direction = menu.Direction; + menu.Hide(); + bool below = false; + switch (direction) + { + case ContextMenuDirection.RightDown: + case ContextMenuDirection.LeftDown: + below = true; + break; + case ContextMenuDirection.RightUp: + case ContextMenuDirection.LeftUp: + below = false; + break; + } + menu.Show(this, below ? _createNewButton.BottomLeft : _createNewButton.UpperLeft, direction); + } + private ContextMenu OnViewDropdownPopupCreate(ComboBox comboBox) { var menu = new ContextMenu(); @@ -1592,6 +1630,8 @@ namespace FlaxEditor.Windows if (Editor.ContentDatabase.Find(_lastViewedFolderBeforeReload) is ContentFolder folder) _tree.Select(folder.Node); } + + OnFoldersSearchBoxTextChanged(); } private void Refresh() diff --git a/Source/Editor/Windows/EditGameWindow.cs b/Source/Editor/Windows/EditGameWindow.cs index dbeab28b1..5f91aebe6 100644 --- a/Source/Editor/Windows/EditGameWindow.cs +++ b/Source/Editor/Windows/EditGameWindow.cs @@ -257,7 +257,7 @@ namespace FlaxEditor.Windows private void UpdateCameraPreview() { // Disable rendering preview during GI baking - if (Editor.StateMachine.CurrentState.IsPerformanceHeavy) + if (Editor == null || Editor.StateMachine.CurrentState.IsPerformanceHeavy) { HideAllCameraPreviews(); return; @@ -406,6 +406,14 @@ namespace FlaxEditor.Windows } } + /// + protected override void OnSizeChanged() + { + base.OnSizeChanged(); + + UpdateCameraPreview(); + } + /// public override void OnDestroy() { diff --git a/Source/Editor/Windows/SceneTreeWindow.cs b/Source/Editor/Windows/SceneTreeWindow.cs index d9ba67aa3..72b7c0f57 100644 --- a/Source/Editor/Windows/SceneTreeWindow.cs +++ b/Source/Editor/Windows/SceneTreeWindow.cs @@ -68,6 +68,7 @@ namespace FlaxEditor.Windows TooltipText = "Search the scene tree.\n\nYou can prefix your search with different search operators:\ns: -> Actor with script of type\na: -> Actor type\nc: -> Control type", }; _searchBox.TextChanged += OnSearchBoxTextChanged; + ScriptsBuilder.ScriptsReloadEnd += OnSearchBoxTextChanged; // Scene tree panel _sceneTreePanel = new Panel @@ -112,7 +113,7 @@ namespace FlaxEditor.Windows InputActions.Add(options => options.LockFocusSelection, () => Editor.Windows.EditWin.Viewport.LockFocusSelection()); InputActions.Add(options => options.Rename, RenameSelection); } - + /// public override void OnPlayBeginning() { @@ -125,6 +126,7 @@ namespace FlaxEditor.Windows { base.OnPlayBegin(); _blockSceneTreeScroll = false; + OnSearchBoxTextChanged(); } /// @@ -139,6 +141,7 @@ namespace FlaxEditor.Windows { base.OnPlayEnd(); _blockSceneTreeScroll = true; + OnSearchBoxTextChanged(); } /// @@ -174,6 +177,7 @@ namespace FlaxEditor.Windows return; _tree.LockChildrenRecursive(); + PerformLayout(); // Update tree var query = _searchBox.Text; @@ -600,6 +604,7 @@ namespace FlaxEditor.Windows _dragHandlers = null; _tree = null; _searchBox = null; + ScriptsBuilder.ScriptsReloadEnd -= OnSearchBoxTextChanged; base.OnDestroy(); } diff --git a/Source/Engine/AI/Behavior.cpp b/Source/Engine/AI/Behavior.cpp index 442051d73..5eee07dbf 100644 --- a/Source/Engine/AI/Behavior.cpp +++ b/Source/Engine/AI/Behavior.cpp @@ -109,7 +109,14 @@ void Behavior::UpdateAsync() const BehaviorUpdateResult result = tree->Graph.Root->InvokeUpdate(context); if (result != BehaviorUpdateResult::Running) _result = result; - if (_result != BehaviorUpdateResult::Running) + if (_result != BehaviorUpdateResult::Running && tree->Graph.Root->Loop) + { + // Reset State + _result = BehaviorUpdateResult::Running; + _accumulatedTime = 0.0f; + _totalTime = 0; + } + else if (_result != BehaviorUpdateResult::Running) { Finished(); } diff --git a/Source/Engine/AI/BehaviorTreeNodes.h b/Source/Engine/AI/BehaviorTreeNodes.h index 7aaab5f3c..05a472da7 100644 --- a/Source/Engine/AI/BehaviorTreeNodes.h +++ b/Source/Engine/AI/BehaviorTreeNodes.h @@ -96,6 +96,10 @@ API_CLASS(Sealed) class FLAXENGINE_API BehaviorTreeRootNode : public BehaviorTre // The target amount of the behavior logic updates per second. API_FIELD(Attributes="EditorOrder(100)") float UpdateFPS = 10.0f; + + // Whether to loop the root node. + API_FIELD(Attributes="EditorOrder(200)") + bool Loop = true; }; /// diff --git a/Source/Engine/Audio/Audio.cpp b/Source/Engine/Audio/Audio.cpp index 5574919c4..12cde01e8 100644 --- a/Source/Engine/Audio/Audio.cpp +++ b/Source/Engine/Audio/Audio.cpp @@ -3,7 +3,6 @@ #include "Audio.h" #include "AudioBackend.h" #include "AudioSettings.h" -#include "FlaxEngine.Gen.h" #include "Engine/Scripting/ScriptingType.h" #include "Engine/Scripting/BinaryModule.h" #include "Engine/Level/Level.h" diff --git a/Source/Engine/Audio/AudioSource.h b/Source/Engine/Audio/AudioSource.h index 58903912c..07762be2f 100644 --- a/Source/Engine/Audio/AudioSource.h +++ b/Source/Engine/Audio/AudioSource.h @@ -207,16 +207,16 @@ public: API_PROPERTY() void SetAttenuation(float value); /// - /// Gets the doppler effect factor. Scale for source velocity. Default is 1. + /// Gets the doppler effect factor. Scale for source velocity. Default is 1. Used by spatial sources only. Cannot scale the effect up (only dim it). /// - API_PROPERTY(Attributes="EditorOrder(75), DefaultValue(1.0f), Limit(0, float.MaxValue, 0.1f), EditorDisplay(\"Audio Source\")") + API_PROPERTY(Attributes="EditorOrder(75), DefaultValue(1.0f), Limit(0, 1.0f, 0.1f), EditorDisplay(\"Audio Source\")") FORCE_INLINE float GetDopplerFactor() const { return _dopplerFactor; } /// - /// Sets the doppler effect factor. Scale for source velocity. Default is 1. + /// Sets the doppler effect factor. Scale for source velocity. Default is 1. Used by spatial sources only. Cannot scale the effect up (only dim it). /// API_PROPERTY() void SetDopplerFactor(float value); diff --git a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp index 715ede74f..8795cc6f9 100644 --- a/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp +++ b/Source/Engine/Audio/OpenAL/AudioBackendOAL.cpp @@ -829,7 +829,6 @@ bool AudioBackendOAL::Base_Init() } // Init - Base_SetDopplerFactor(AudioSettings::Get()->DopplerFactor); alDistanceModel(AL_INVERSE_DISTANCE_CLAMPED); // Default attenuation model int32 clampedIndex = Math::Clamp(activeDeviceIndex, -1, Audio::Devices.Count() - 1); if (clampedIndex == Audio::GetActiveDeviceIndex()) @@ -841,6 +840,7 @@ bool AudioBackendOAL::Base_Init() if (ALC::IsExtensionSupported("AL_SOFT_source_spatialize")) ALC::Features = EnumAddFlags(ALC::Features, FeatureFlags::SpatialMultiChannel); #endif + Base_SetDopplerFactor(AudioSettings::Get()->DopplerFactor); ALC::Inited = true; // Log service info diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index 7a35d3b28..a9b1a7fc9 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -8,6 +8,8 @@ #include "Loading/Tasks/LoadAssetTask.h" #include "Engine/Core/Log.h" #include "Engine/Core/LogContext.h" +#include "Engine/Graphics/GPUDevice.h" +#include "Engine/Physics/Physics.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Profiler/ProfilerMemory.h" #include "Engine/Scripting/ManagedCLR/MCore.h" @@ -703,6 +705,38 @@ void Asset::onUnload_MainThread() OnUnloaded(this); } +bool Asset::WaitForInitGraphics() +{ +#define IS_GPU_NOT_READY() (GPUDevice::Instance == nullptr || GPUDevice::Instance->GetState() != GPUDevice::DeviceState::Ready) + if (!IsInMainThread() && IS_GPU_NOT_READY()) + { + PROFILE_CPU(); + ZoneColor(TracyWaitZoneColor); + int32 timeout = 1000; + while (IS_GPU_NOT_READY() && timeout-- > 0) + Platform::Sleep(1); + if (IS_GPU_NOT_READY()) + return true; + } +#undef IS_GPU_NOT_READY + return false; +} + +bool Asset::WaitForInitPhysics() +{ + if (!IsInMainThread() && !Physics::DefaultScene) + { + PROFILE_CPU(); + ZoneColor(TracyWaitZoneColor); + int32 timeout = 1000; + while (!Physics::DefaultScene && timeout-- > 0) + Platform::Sleep(1); + if (!Physics::DefaultScene) + return true; + } + return false; +} + #if USE_EDITOR bool Asset::OnCheckSave(const StringView& path) const diff --git a/Source/Engine/Content/Asset.h b/Source/Engine/Content/Asset.h index e6607e62c..a7913a5a5 100644 --- a/Source/Engine/Content/Asset.h +++ b/Source/Engine/Content/Asset.h @@ -285,6 +285,10 @@ protected: virtual void onRename(const StringView& newPath) = 0; #endif + // Utilities to ensure specific engine systems are initialized before loading asset (eg. assets can be loaded during engine startup). + static bool WaitForInitGraphics(); + static bool WaitForInitPhysics(); + public: // [ManagedScriptingObject] String ToString() const override; diff --git a/Source/Engine/Content/Assets/Animation.cpp b/Source/Engine/Content/Assets/Animation.cpp index 917db28ac..86fa41f89 100644 --- a/Source/Engine/Content/Assets/Animation.cpp +++ b/Source/Engine/Content/Assets/Animation.cpp @@ -691,11 +691,7 @@ Asset::LoadResult Animation::load() continue; } #if USE_EDITOR - if (!_registeredForScriptingReload) - { - _registeredForScriptingReload = true; - Level::ScriptsReloadStart.Bind(this); - } + _registerForScriptingReload = true; #endif } } @@ -733,6 +729,7 @@ void Animation::unload(bool isReloading) { ScopeWriteLock systemScope(Animations::SystemLocker); #if USE_EDITOR + _registerForScriptingReload = false; if (_registeredForScriptingReload) { _registeredForScriptingReload = false; @@ -752,6 +749,22 @@ void Animation::unload(bool isReloading) NestedAnims.Clear(); } +#if USE_EDITOR + +void Animation::onLoaded_MainThread() +{ + if (_registerForScriptingReload && !_registeredForScriptingReload) + { + _registeredForScriptingReload = true; + Level::ScriptsReloadStart.Bind(this); + } + _registerForScriptingReload = false; + + BinaryAsset::onLoaded_MainThread(); +} + +#endif + AssetChunksFlag Animation::getChunksToPreload() const { return GET_CHUNK_FLAG(0); diff --git a/Source/Engine/Content/Assets/Animation.h b/Source/Engine/Content/Assets/Animation.h index 4f4a773b4..c342cc64c 100644 --- a/Source/Engine/Content/Assets/Animation.h +++ b/Source/Engine/Content/Assets/Animation.h @@ -78,6 +78,7 @@ API_CLASS(NoSpawn) class FLAXENGINE_API Animation : public BinaryAsset private: #if USE_EDITOR + bool _registerForScriptingReload = false; bool _registeredForScriptingReload = false; void OnScriptsReloadStart(); #endif @@ -163,5 +164,8 @@ protected: // [BinaryAsset] LoadResult load() override; void unload(bool isReloading) override; +#if USE_EDITOR + void onLoaded_MainThread() override; +#endif AssetChunksFlag getChunksToPreload() const override; }; diff --git a/Source/Engine/Content/Assets/Material.cpp b/Source/Engine/Content/Assets/Material.cpp index b4cf55d4d..e46060397 100644 --- a/Source/Engine/Content/Assets/Material.cpp +++ b/Source/Engine/Content/Assets/Material.cpp @@ -5,7 +5,6 @@ #include "Engine/Core/Types/DataContainer.h" #include "Engine/Content/Upgraders/ShaderAssetUpgrader.h" #include "Engine/Content/Factories/BinaryAssetFactory.h" -#include "Engine/Graphics/GPUDevice.h" #include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/Materials/MaterialShader.h" #include "Engine/Graphics/Shaders/Cache/ShaderCacheManager.h" @@ -156,16 +155,8 @@ Asset::LoadResult Material::load() FlaxChunk* materialParamsChunk; // Wait for the GPU Device to be ready (eg. case when loading material before GPU init) -#define IS_GPU_NOT_READY() (GPUDevice::Instance == nullptr || GPUDevice::Instance->GetState() != GPUDevice::DeviceState::Ready) - if (!IsInMainThread() && IS_GPU_NOT_READY()) - { - int32 timeout = 1000; - while (IS_GPU_NOT_READY() && timeout-- > 0) - Platform::Sleep(1); - if (IS_GPU_NOT_READY()) - return LoadResult::InvalidData; - } -#undef IS_GPU_NOT_READY + if (WaitForInitGraphics()) + return LoadResult::CannotLoadData; // If engine was compiled with shaders compiling service: // - Material should be changed in need to convert it to the newer version (via Visject Surface) diff --git a/Source/Engine/Content/Assets/MaterialBase.cpp b/Source/Engine/Content/Assets/MaterialBase.cpp index 6ef5f9429..1613ef59a 100644 --- a/Source/Engine/Content/Assets/MaterialBase.cpp +++ b/Source/Engine/Content/Assets/MaterialBase.cpp @@ -19,10 +19,10 @@ Variant MaterialBase::GetParameterValue(const StringView& name) if (!IsLoaded() && WaitForLoaded()) return Variant::Null; const auto param = Params.Get(name); + if (IsMaterialInstance() && param && !param->IsOverride() && ((MaterialInstance*)this)->GetBaseMaterial()) + return ((MaterialInstance*)this)->GetBaseMaterial()->GetParameterValue(name); if (param) - { return param->GetValue(); - } LOG(Warning, "Missing material parameter '{0}' in material {1}", String(name), ToString()); return Variant::Null; } diff --git a/Source/Engine/Content/Assets/MaterialBase.h b/Source/Engine/Content/Assets/MaterialBase.h index f0c32d66a..78f244cb7 100644 --- a/Source/Engine/Content/Assets/MaterialBase.h +++ b/Source/Engine/Content/Assets/MaterialBase.h @@ -57,6 +57,8 @@ public: /// /// Gets the material parameter value. /// + /// For material instances that inherit a base material, returned value might come from base material if the current one doesn't override it. + /// The parameter name. /// The parameter value. API_FUNCTION() Variant GetParameterValue(const StringView& name); diff --git a/Source/Engine/Content/Storage/ContentStorageManager.cpp b/Source/Engine/Content/Storage/ContentStorageManager.cpp index 80744ae45..8aa36c7be 100644 --- a/Source/Engine/Content/Storage/ContentStorageManager.cpp +++ b/Source/Engine/Content/Storage/ContentStorageManager.cpp @@ -46,6 +46,7 @@ namespace ContentStorageService ContentStorageServiceInstance; +TimeSpan ContentStorageManager::UnusedStorageLifetime = TimeSpan::FromSeconds(0.5f); TimeSpan ContentStorageManager::UnusedDataChunksLifetime = TimeSpan::FromSeconds(10); FlaxStorageReference ContentStorageManager::GetStorage(const StringView& path, bool loadIt) diff --git a/Source/Engine/Content/Storage/ContentStorageManager.h b/Source/Engine/Content/Storage/ContentStorageManager.h index 0d56855ea..fac27aa7f 100644 --- a/Source/Engine/Content/Storage/ContentStorageManager.h +++ b/Source/Engine/Content/Storage/ContentStorageManager.h @@ -15,7 +15,12 @@ class FLAXENGINE_API ContentStorageManager { public: /// - /// Auto-release timeout for unused asset chunks. + /// Auto-release timeout for unused asset files. + /// + static TimeSpan UnusedStorageLifetime; + + /// + /// Auto-release timeout for unused asset data chunks. /// static TimeSpan UnusedDataChunksLifetime; diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index 6967bb5ad..e286ee326 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.cpp +++ b/Source/Engine/Content/Storage/FlaxStorage.cpp @@ -286,14 +286,14 @@ FlaxStorage::LockData FlaxStorage::LockSafe() uint32 FlaxStorage::GetRefCount() const { - return (uint32)Platform::AtomicRead((int64*)&_refCount); + return (uint32)Platform::AtomicRead(&_refCount); } bool FlaxStorage::ShouldDispose() const { - return Platform::AtomicRead((int64*)&_refCount) == 0 && - Platform::AtomicRead((int64*)&_chunksLock) == 0 && - Platform::GetTimeSeconds() - _lastRefLostTime >= 0.5; // TTL in seconds + return Platform::AtomicRead(&_refCount) == 0 && + Platform::AtomicRead(&_chunksLock) == 0 && + Platform::GetTimeSeconds() - _lastRefLostTime >= ContentStorageManager::UnusedStorageLifetime.GetTotalSeconds(); } uint32 FlaxStorage::GetMemoryUsage() const diff --git a/Source/Engine/Content/Upgraders/FontAssetUpgrader.h b/Source/Engine/Content/Upgraders/FontAssetUpgrader.h index 80c2e6c39..4ca6e6d73 100644 --- a/Source/Engine/Content/Upgraders/FontAssetUpgrader.h +++ b/Source/Engine/Content/Upgraders/FontAssetUpgrader.h @@ -5,6 +5,7 @@ #if USE_EDITOR #include "BinaryAssetUpgrader.h" +#include "Engine/Render2D/FontAsset.h" /// /// Font Asset Upgrader @@ -17,10 +18,33 @@ public: { const Upgrader upgraders[] = { - {}, + { 3, 4, &Upgrade_3_To_4 }, }; setup(upgraders, ARRAY_COUNT(upgraders)); } + +private: + struct FontOptionsOld + { + FontHinting Hinting; + FontFlags Flags; + }; + + static bool Upgrade_3_To_4(AssetMigrationContext& context) + { + ASSERT(context.Input.SerializedVersion == 3 && context.Output.SerializedVersion == 4); + + FontOptionsOld optionsOld; + Platform::MemoryCopy(&optionsOld, context.Input.CustomData.Get(), sizeof(FontOptionsOld)); + + FontOptions options; + options.Hinting = optionsOld.Hinting; + options.Flags = optionsOld.Flags; + options.RasterMode = FontRasterMode::Bitmap; + context.Output.CustomData.Copy(&options); + + return CopyChunk(context, 0); + } }; #endif diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.cpp b/Source/Engine/ContentImporters/AssetsImportingManager.cpp index 9cad287dc..696f43a9e 100644 --- a/Source/Engine/ContentImporters/AssetsImportingManager.cpp +++ b/Source/Engine/ContentImporters/AssetsImportingManager.cpp @@ -36,6 +36,30 @@ #include "CreateAnimation.h" #include "CreateBehaviorTree.h" #include "CreateJson.h" +#include "Engine/Content/Assets/Model.h" + +namespace +{ + bool IsAssetTypeNameTextureFile(const String& typeName) + { + return typeName == Texture::TypeName || typeName == SpriteAtlas::TypeName; + } + + bool IsAssetTypeNameModelFile(const String& typeName) + { + return typeName == Model::TypeName || typeName == SkinnedModel::TypeName || typeName == Animation::TypeName; + } + + bool IsAssetTypeNameMatch(const String& a, const String& b) + { + // Special case when reimporting model/texture but different type + if (IsAssetTypeNameTextureFile(a) && IsAssetTypeNameTextureFile(b)) + return true; + if (IsAssetTypeNameModelFile(a) && IsAssetTypeNameModelFile(b)) + return true; + return a == b; + } +} // Tags used to detect asset creation mode const String AssetsImportingManager::CreateTextureTag(TEXT("Texture")); @@ -84,8 +108,6 @@ CreateAssetContext::CreateAssetContext(const StringView& inputPath, const String CustomArg = arg; Data.Header.ID = id; SkipMetadata = false; - - // TODO: we should use ASNI only chars path (Assimp can use only that kind) OutputPath = Content::CreateTemporaryAssetPath(); } @@ -122,6 +144,24 @@ CreateAssetResult CreateAssetContext::Run(const CreateAssetFunction& callback) Data.Metadata.Copy((const byte*)buffer.GetString(), (uint32)buffer.GetSize()); } + // Check if target asset already exists but has different type + AssetInfo targetAssetInfo; + if (Content::GetAssetInfo(TargetAssetPath, targetAssetInfo) && !IsAssetTypeNameMatch(targetAssetInfo.TypeName, Data.Header.TypeName)) + { + // Change path + int32 index = 0; + String newTargetAssetPath; + do + { + newTargetAssetPath = StringUtils::GetDirectoryName(TargetAssetPath); + newTargetAssetPath /= StringUtils::GetFileNameWithoutExtension(TargetAssetPath) + String::Format(TEXT(" ({})."), index++) + FileSystem::GetExtension(TargetAssetPath); + } while (index < 100 && FileSystem::FileExists(newTargetAssetPath)); + TargetAssetPath = newTargetAssetPath; + + // Change id + Data.Header.ID = Guid::New(); + } + // Save file result = FlaxStorage::Create(OutputPath, Data) ? CreateAssetResult::CannotSaveFile : CreateAssetResult::Ok; if (result == CreateAssetResult::Ok) diff --git a/Source/Engine/ContentImporters/ImportFont.cpp b/Source/Engine/ContentImporters/ImportFont.cpp index c7dc01fe6..964f3907e 100644 --- a/Source/Engine/ContentImporters/ImportFont.cpp +++ b/Source/Engine/ContentImporters/ImportFont.cpp @@ -12,12 +12,13 @@ CreateAssetResult ImportFont::Import(CreateAssetContext& context) { // Base - IMPORT_SETUP(FontAsset, 3); + IMPORT_SETUP(FontAsset, 4); // Setup header FontOptions options; options.Hinting = FontHinting::Default; options.Flags = FontFlags::AntiAliasing; + options.RasterMode = FontRasterMode::Bitmap; context.Data.CustomData.Copy(&options); // Open the file diff --git a/Source/Engine/ContentImporters/ImportModel.cpp b/Source/Engine/ContentImporters/ImportModel.cpp index f3548dc5c..b2a9434c9 100644 --- a/Source/Engine/ContentImporters/ImportModel.cpp +++ b/Source/Engine/ContentImporters/ImportModel.cpp @@ -19,6 +19,7 @@ #include "Engine/Animations/AnimEvent.h" #include "Engine/Level/Actors/EmptyActor.h" #include "Engine/Level/Actors/StaticModel.h" +#include "Engine/Level/Actors/AnimatedModel.h" #include "Engine/Level/Prefabs/Prefab.h" #include "Engine/Level/Prefabs/PrefabManager.h" #include "Engine/Level/Scripts/ModelPrefab.h" @@ -82,6 +83,11 @@ bool ImportModel::TryGetImportOptions(const StringView& path, Options& options) struct PrefabObject { + enum + { + Model, + SkinnedModel, + } Type; int32 NodeIndex; String Name; String AssetPath; @@ -280,7 +286,7 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context) options.SplitObjects = false; options.ObjectIndex = -1; - // Import all of the objects recursive but use current model data to skip loading file again + // Import all the objects recursive but use current model data to skip loading file again options.Cached = &cached; HashSet objectNames; Function splitImport = [&context, &autoImportOutput, &objectNames](Options& splitOptions, const StringView& objectName, String& outputPath, MeshData* meshData) @@ -335,12 +341,24 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context) auto& group = meshesByName[groupIndex]; // Cache object options (nested sub-object import removes the meshes) - prefabObject.NodeIndex = group.First()->NodeIndex; - prefabObject.Name = group.First()->Name; + MeshData* firstMesh = group.First(); + prefabObject.NodeIndex = firstMesh->NodeIndex; + prefabObject.Name = firstMesh->Name; - splitOptions.Type = ModelTool::ModelType::Model; + // Detect model type + if ((firstMesh->BlendIndices.HasItems() && firstMesh->BlendWeights.HasItems()) || firstMesh->BlendShapes.HasItems()) + { + splitOptions.Type = ModelTool::ModelType::SkinnedModel; + prefabObject.Type = PrefabObject::SkinnedModel; + } + else + { + splitOptions.Type = ModelTool::ModelType::Model; + prefabObject.Type = PrefabObject::Model; + } + splitOptions.ObjectIndex = groupIndex; - if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath, group.First())) + if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath, firstMesh)) { prefabObjects.Add(prefabObject); } @@ -734,24 +752,38 @@ CreateAssetResult ImportModel::CreatePrefab(CreateAssetContext& context, const M nodeActors.Clear(); for (const PrefabObject& e : prefabObjects) { - if (e.NodeIndex == nodeIndex) + if (e.NodeIndex != nodeIndex) + continue; + Actor* a = nullptr; + switch (e.Type) + { + case PrefabObject::Model: { auto* actor = New(); - actor->SetName(e.Name); if (auto* model = Content::LoadAsync(e.AssetPath)) - { actor->Model = model; - } - nodeActors.Add(actor); + a = actor; + break; } + case PrefabObject::SkinnedModel: + { + auto* actor = New(); + if (auto* skinnedModel = Content::LoadAsync(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(); if (nodeActors.Count() > 1) { for (Actor* e : nodeActors) - { e->SetParent(nodeActor); - } } if (nodeActors.Count() != 1) { diff --git a/Source/Engine/Core/Math/Color.cs b/Source/Engine/Core/Math/Color.cs index be4a12789..dbfd70c7e 100644 --- a/Source/Engine/Core/Math/Color.cs +++ b/Source/Engine/Core/Math/Color.cs @@ -82,6 +82,11 @@ namespace FlaxEngine } } + /// + /// Gets the brightness of the color + /// + public float Brightness => R * 0.299f + G * 0.587f + B * 0.114f; + /// /// Returns the minimum color component value: Min(r,g,b). /// diff --git a/Source/Engine/Core/Math/Vector4.h b/Source/Engine/Core/Math/Vector4.h index 5f5e436ad..167937c81 100644 --- a/Source/Engine/Core/Math/Vector4.h +++ b/Source/Engine/Core/Math/Vector4.h @@ -153,6 +153,12 @@ public: return Math::IsOne(X) && Math::IsOne(Y) && Math::IsOne(Z) && Math::IsOne(W); } + // Calculates the length of the vector. + T Length() const + { + return Math::Sqrt(X * X + Y * Y + Z * Z + W * W); + } + /// /// Returns the average arithmetic of all the components. /// diff --git a/Source/Engine/Core/Types/Variant.cpp b/Source/Engine/Core/Types/Variant.cpp index dcabe8e48..96db06656 100644 --- a/Source/Engine/Core/Types/Variant.cpp +++ b/Source/Engine/Core/Types/Variant.cpp @@ -247,7 +247,7 @@ VariantType::VariantType(VariantType&& other) noexcept VariantType& VariantType::operator=(const Types& type) { Type = type; - if (StaticName) + if (!StaticName) Allocator::Free(TypeName); TypeName = nullptr; StaticName = 0; @@ -266,7 +266,7 @@ VariantType& VariantType::operator=(const VariantType& other) { ASSERT(this != &other); Type = other.Type; - if (StaticName) + if (!StaticName) Allocator::Free(TypeName); StaticName = other.StaticName; if (StaticName) @@ -316,7 +316,7 @@ void VariantType::SetTypeName(const StringView& typeName) { if (StringUtils::Length(TypeName) != typeName.Length()) { - if (StaticName) + if (!StaticName) Allocator::Free(TypeName); StaticName = 0; TypeName = static_cast(Allocator::Allocate(typeName.Length() + 1)); @@ -329,7 +329,7 @@ void VariantType::SetTypeName(const StringAnsiView& typeName, bool staticName) { if (StringUtils::Length(TypeName) != typeName.Length() || StaticName != staticName) { - if (StaticName) + if (!StaticName) Allocator::Free(TypeName); StaticName = staticName; if (staticName) diff --git a/Source/Engine/Engine/CommandLine.cpp b/Source/Engine/Engine/CommandLine.cpp index e0f9441eb..ad2c9c903 100644 --- a/Source/Engine/Engine/CommandLine.cpp +++ b/Source/Engine/Engine/CommandLine.cpp @@ -149,6 +149,7 @@ bool CommandLine::Parse(const Char* cmdLine) PARSE_BOOL_SWITCH("-clearcache ", ClearCache); PARSE_BOOL_SWITCH("-clearcooker ", ClearCookerCache); PARSE_ARG_SWITCH("-project ", Project); + PARSE_BOOL_SWITCH("-lastproject ", LastProject); PARSE_BOOL_SWITCH("-new ", NewProject); PARSE_BOOL_SWITCH("-genprojectfiles ", GenProjectFiles); PARSE_ARG_SWITCH("-build ", Build); diff --git a/Source/Engine/Engine/CommandLine.h b/Source/Engine/Engine/CommandLine.h index ad49bee04..0464a478f 100644 --- a/Source/Engine/Engine/CommandLine.h +++ b/Source/Engine/Engine/CommandLine.h @@ -133,6 +133,11 @@ public: /// String Project; + /// + /// -lastproject (Opens the last project) + /// + Nullable LastProject; + /// /// -new (generates the project files inside the specified project folder or uses current workspace folder) /// diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp index 5e8224b4e..b53b69741 100644 --- a/Source/Engine/Engine/Engine.cpp +++ b/Source/Engine/Engine/Engine.cpp @@ -595,7 +595,11 @@ void EngineImpl::InitLog() #if COMPILE_WITH_DEV_ENV LOG(Info, "Compiled for Dev Environment"); #endif +#if defined(FLAXENGINE_BRANCH) && defined(FLAXENGINE_COMMIT) + LOG(Info, "Version " FLAXENGINE_VERSION_TEXT ", {}, {}", StringAsUTF16<>(FLAXENGINE_BRANCH).Get(), StringAsUTF16<>(FLAXENGINE_COMMIT).Get()); +#else LOG(Info, "Version " FLAXENGINE_VERSION_TEXT); +#endif const Char* cpp = TEXT("?"); if (__cplusplus == 202101L) cpp = TEXT("C++23"); else if (__cplusplus == 202002L) cpp = TEXT("C++20"); diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index 116866848..459f1b662 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -6,14 +6,13 @@ #include "Engine/Core/Log.h" #include "Engine/Core/Random.h" #include "Engine/Engine/Engine.h" +#include "Engine/Graphics/Graphics.h" #include "Engine/Graphics/RenderTask.h" +#include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/GPUDevice.h" #include "Engine/Content/Deprecated.h" #if !FOLIAGE_USE_SINGLE_QUAD_TREE #include "Engine/Threading/JobSystem.h" -#if FOLIAGE_USE_DRAW_CALLS_BATCHING -#include "Engine/Graphics/RenderTools.h" -#endif #endif #include "Engine/Level/SceneQuery.h" #include "Engine/Profiler/ProfilerCPU.h" @@ -23,7 +22,7 @@ #include "Engine/Serialization/Serialization.h" #include "Engine/Utilities/Encryption.h" -#define FOLIAGE_GET_DRAW_MODES(renderContext, type) (type.DrawModes & renderContext.View.Pass & renderContext.View.GetShadowsDrawPassMask(type.ShadowsMode)) +#define FOLIAGE_GET_DRAW_MODES(renderContext, type) (type._drawModes & renderContext.View.Pass & renderContext.View.GetShadowsDrawPassMask(type.ShadowsMode)) #define FOLIAGE_CAN_DRAW(renderContext, type) (type.IsReady() && FOLIAGE_GET_DRAW_MODES(renderContext, type) != DrawPass::None && type.Model->CanBeRendered()) Foliage::Foliage(const SpawnParams& params) @@ -125,7 +124,7 @@ void Foliage::AddToCluster(ChunkedArrayLODs.Get()[lod].Meshes; for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++) @@ -141,7 +140,7 @@ void Foliage::DrawInstance(RenderContext& renderContext, FoliageInstance& instan auto* e = result.TryGet(key); if (!e) { - e = &result.Add(key, BatchedDrawCall(renderContext.List))->Value; + e = &result.Add(key, BatchedDrawCall(context.RenderContext.List))->Value; ASSERT_LOW_LAYER(key.Mat); e->DrawCall.Material = key.Mat; e->DrawCall.Surface.Lightmap = EnumHasAnyFlags(_staticFlags, StaticFlags::Lightmap) && _scene ? _scene->LightmapsData.GetReadyLightmap(key.Lightmap) : nullptr; @@ -152,21 +151,18 @@ void Foliage::DrawInstance(RenderContext& renderContext, FoliageInstance& instan auto& instanceData = e->Instances.AddOne(); Matrix world; const Transform transform = _transform.LocalToWorld(instance.Transform); - const Float3 translation = transform.Translation - renderContext.View.Origin; + const Float3 translation = transform.Translation - context.ViewOrigin; Matrix::Transformation(transform.Scale, transform.Orientation, translation, world); constexpr float worldDeterminantSign = 1.0f; instanceData.Store(world, world, instance.Lightmap.UVsArea, drawCall.Surface.GeometrySize, instance.Random, worldDeterminantSign, lodDitherFactor); } } -void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, const FoliageType& type, DrawCallsList* drawCallsLists, BatchedDrawCalls& result) const +void Foliage::DrawCluster(DrawContext& context, FoliageCluster* cluster, DrawCallsList* drawCallsLists, BatchedDrawCalls& result) const { // Skip clusters that around too far from view - const auto lodView = (renderContext.LodProxyView ? renderContext.LodProxyView : &renderContext.View); - if (Float3::Distance(lodView->Position, cluster->TotalBoundsSphere.Center - lodView->Origin) - (float)cluster->TotalBoundsSphere.Radius > cluster->MaxCullDistance) + if (Float3::Distance(context.LodView.Position, cluster->TotalBoundsSphere.Center - context.LodView.Origin) - (float)cluster->TotalBoundsSphere.Radius > cluster->MaxCullDistance) return; - const Vector3 viewOrigin = renderContext.View.Origin; - //DebugDraw::DrawBox(cluster->Bounds, Color::Red); // Draw visible children @@ -178,10 +174,10 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, BoundingBox box; #define DRAW_CLUSTER(idx) \ box = cluster->Children[idx]->TotalBounds; \ - box.Minimum -= viewOrigin; \ - box.Maximum -= viewOrigin; \ - if (renderContext.View.CullingFrustum.Intersects(box)) \ - DrawCluster(renderContext, cluster->Children[idx], type, drawCallsLists, result) + box.Minimum -= context.ViewOrigin; \ + box.Maximum -= context.ViewOrigin; \ + if (context.RenderContext.View.CullingFrustum.Intersects(box)) \ + DrawCluster(context, cluster->Children[idx], drawCallsLists, result) DRAW_CLUSTER(0); DRAW_CLUSTER(1); DRAW_CLUSTER(2); @@ -192,21 +188,22 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, { // Draw visible instances const auto frame = Engine::FrameCount; - const auto model = type.Model.Get(); - const auto transitionLOD = renderContext.View.Pass != DrawPass::Depth; // Let the main view pass update LOD transitions + const auto model = context.FoliageType.Model.Get(); + const auto transitionLOD = context.RenderContext.View.Pass != DrawPass::Depth; // Let the main view pass update LOD transitions // TODO: move DrawState to be stored per-view (so shadows can fade objects on their own) for (int32 i = 0; i < cluster->Instances.Count(); i++) { auto& instance = *cluster->Instances.Get()[i]; BoundingSphere sphere = instance.Bounds; - sphere.Center -= viewOrigin; - if (Float3::Distance(lodView->Position, sphere.Center) - (float)sphere.Radius < instance.CullDistance && - renderContext.View.CullingFrustum.Intersects(sphere)) + sphere.Center -= context.ViewOrigin; + if (Float3::Distance(context.LodView.Position, sphere.Center) - (float)sphere.Radius < instance.CullDistance && + context.RenderContext.View.CullingFrustum.Intersects(sphere) && + RenderTools::ComputeBoundsScreenRadiusSquared(sphere.Center, sphere.Radius, context.RenderContext.View) * context.ViewScreenSizeSq >= context.MinObjectPixelSizeSq) { const auto modelFrame = instance.DrawState.PrevFrame + 1; // Select a proper LOD index (model may be culled) - int32 lodIndex = RenderTools::ComputeModelLOD(model, sphere.Center, (float)sphere.Radius, renderContext); + int32 lodIndex = RenderTools::ComputeModelLOD(model, sphere.Center, (float)sphere.Radius, context.RenderContext); if (lodIndex == -1) { // Handling model fade-out transition @@ -231,20 +228,20 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, { const auto prevLOD = model->ClampLODIndex(instance.DrawState.PrevLOD); const float normalizedProgress = static_cast(instance.DrawState.LODTransition) * (1.0f / 255.0f); - DrawInstance(renderContext, instance, type, model, prevLOD, normalizedProgress, drawCallsLists, result); + DrawInstance(context, instance, model, prevLOD, normalizedProgress, drawCallsLists, result); } } else if (instance.DrawState.LODTransition < 255) { const auto prevLOD = model->ClampLODIndex(instance.DrawState.PrevLOD); const float normalizedProgress = static_cast(instance.DrawState.LODTransition) * (1.0f / 255.0f); - DrawInstance(renderContext, instance, type, model, prevLOD, normalizedProgress, drawCallsLists, result); + DrawInstance(context, instance, model, prevLOD, normalizedProgress, drawCallsLists, result); } } instance.DrawState.PrevFrame = frame; continue; } - lodIndex += renderContext.View.ModelLODBias; + lodIndex += context.RenderContext.View.ModelLODBias; lodIndex = model->ClampLODIndex(lodIndex); if (transitionLOD) @@ -278,19 +275,19 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, // Draw if (instance.DrawState.PrevLOD == lodIndex) { - DrawInstance(renderContext, instance, type, model, lodIndex, 0.0f, drawCallsLists, result); + DrawInstance(context, instance, model, lodIndex, 0.0f, drawCallsLists, result); } else if (instance.DrawState.PrevLOD == -1) { const float normalizedProgress = static_cast(instance.DrawState.LODTransition) * (1.0f / 255.0f); - DrawInstance(renderContext, instance, type, model, lodIndex, 1.0f - normalizedProgress, drawCallsLists, result); + DrawInstance(context, instance, model, lodIndex, 1.0f - normalizedProgress, drawCallsLists, result); } else { const auto prevLOD = model->ClampLODIndex(instance.DrawState.PrevLOD); const float normalizedProgress = static_cast(instance.DrawState.LODTransition) * (1.0f / 255.0f); - DrawInstance(renderContext, instance, type, model, prevLOD, normalizedProgress, drawCallsLists, result); - DrawInstance(renderContext, instance, type, model, lodIndex, normalizedProgress - 1.0f, drawCallsLists, result); + DrawInstance(context, instance, model, prevLOD, normalizedProgress, drawCallsLists, result); + DrawInstance(context, instance, model, lodIndex, normalizedProgress - 1.0f, drawCallsLists, result); } //DebugDraw::DrawSphere(instance.Bounds, Color::YellowGreen); @@ -304,14 +301,11 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, #else -void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, Mesh::DrawInfo& draw) +void Foliage::DrawCluster(DrawContext& context, FoliageCluster* cluster, Mesh::DrawInfo& draw) { // Skip clusters that around too far from view - const auto lodView = (renderContext.LodProxyView ? renderContext.LodProxyView : &renderContext.View); - if (Float3::Distance(lodView->Position, cluster->TotalBoundsSphere.Center - lodView->Origin) - (float)cluster->TotalBoundsSphere.Radius > cluster->MaxCullDistance) + if (Float3::Distance(context.LodView.Position, cluster->TotalBoundsSphere.Center - context.LodView.Origin) - (float)cluster->TotalBoundsSphere.Radius > cluster->MaxCullDistance) return; - const Vector3 viewOrigin = renderContext.View.Origin; - //DebugDraw::DrawBox(cluster->Bounds, Color::Red); // Draw visible children @@ -323,10 +317,10 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, BoundingBox box; #define DRAW_CLUSTER(idx) \ box = cluster->Children[idx]->TotalBounds; \ - box.Minimum -= viewOrigin; \ - box.Maximum -= viewOrigin; \ - if (renderContext.View.CullingFrustum.Intersects(box)) \ - DrawCluster(renderContext, cluster->Children[idx], draw) + box.Minimum -= context.ViewOrigin; \ + box.Maximum -= context.ViewOrigin; \ + if (context.RenderContext.View.CullingFrustum.Intersects(box)) \ + DrawCluster(context, cluster->Children[idx], draw) DRAW_CLUSTER(0); DRAW_CLUSTER(1); DRAW_CLUSTER(2); @@ -342,16 +336,17 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, auto& instance = *cluster->Instances[i]; auto& type = FoliageTypes[instance.Type]; BoundingSphere sphere = instance.Bounds; - sphere.Center -= viewOrigin; + sphere.Center -= context.ViewOrigin; // Check if can draw this instance if (type._canDraw && - Float3::Distance(lodView->Position, sphere.Center) - (float)sphere.Radius < instance.CullDistance && - renderContext.View.CullingFrustum.Intersects(sphere)) + Float3::Distance(context.LodView.Position, sphere.Center) - (float)sphere.Radius < instance.CullDistance && + context.RenderContext.View.CullingFrustum.Intersects(sphere) && + RenderTools::ComputeBoundsScreenRadiusSquared(sphere.Center, sphere.Radius, context.RenderContext.View) * context.ViewScreenSizeSq >= context.MinObjectPixelSizeSq) { Matrix world; const Transform transform = _transform.LocalToWorld(instance.Transform); - const Float3 translation = transform.Translation - renderContext.View.Origin; + const Float3 translation = transform.Translation - context.ViewOrigin; Matrix::Transformation(transform.Scale, transform.Orientation, translation, world); // Disable motion blur @@ -365,9 +360,9 @@ void Foliage::DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, draw.DrawState = &instance.DrawState; draw.Bounds = sphere; draw.PerInstanceRandom = instance.Random; - draw.DrawModes = type.DrawModes; + draw.DrawModes = type._drawModes; draw.SetStencilValue(_layer); - type.Model->Draw(renderContext, draw); + type.Model->Draw(context.RenderContext, draw); //DebugDraw::DrawSphere(instance.Bounds, Color::YellowGreen); @@ -446,22 +441,50 @@ void Foliage::DrawFoliageJob(int32 i) PROFILE_CPU(); PROFILE_MEM(Graphics); const FoliageType& type = FoliageTypes[i]; - if (type.IsReady() && type.Model->CanBeRendered()) + if (type._canDraw) { DrawCallsList drawCallsLists[MODEL_MAX_LODS]; for (RenderContext& renderContext : _renderContextBatch->Contexts) + { +#if !FOLIAGE_USE_SINGLE_QUAD_TREE && FOLIAGE_USE_DRAW_CALLS_BATCHING DrawType(renderContext, type, drawCallsLists); +#else + Mesh::DrawInfo draw; + draw.Flags = GetStaticFlags(); + draw.DrawModes = (DrawPass)(DrawPass::Default & renderContext.View.Pass); + draw.LODBias = 0; + draw.ForcedLOD = -1; + draw.VertexColors = nullptr; + draw.Deformation = nullptr; + DrawType(renderContext, type, draw); +#endif + } } } #endif +#if !FOLIAGE_USE_SINGLE_QUAD_TREE && FOLIAGE_USE_DRAW_CALLS_BATCHING void Foliage::DrawType(RenderContext& renderContext, const FoliageType& type, DrawCallsList* drawCallsLists) +#else +void Foliage::DrawType(RenderContext& renderContext, const FoliageType& type, Mesh::DrawInfo& draw) +#endif { if (!type.Root || !FOLIAGE_CAN_DRAW(renderContext, type)) return; const DrawPass typeDrawModes = FOLIAGE_GET_DRAW_MODES(renderContext, type); PROFILE_CPU_ASSET(type.Model); + DrawContext context + { + renderContext, + renderContext.LodProxyView ? *renderContext.LodProxyView : renderContext.View, + type, + renderContext.View.Origin, + Math::Square(Graphics::Shadows::MinObjectPixelSize), + renderContext.View.ScreenSize.X * renderContext.View.ScreenSize.Y, + }; + if (context.RenderContext.View.Pass != DrawPass::Depth) + context.MinObjectPixelSizeSq = 0.0f; // Don't use it in main view #if FOLIAGE_USE_DRAW_CALLS_BATCHING // Initialize draw calls for foliage type all LODs meshes for (int32 lod = 0; lod < type.Model->LODs.Count(); lod++) @@ -506,7 +529,7 @@ void Foliage::DrawType(RenderContext& renderContext, const FoliageType& type, Dr // Draw instances of the foliage type BatchedDrawCalls result(&renderContext.List->Memory); - DrawCluster(renderContext, type.Root, type, drawCallsLists, result); + DrawCluster(context, type.Root, drawCallsLists, result); // Submit draw calls with valid instances added for (auto& e : result) @@ -568,10 +591,30 @@ void Foliage::DrawType(RenderContext& renderContext, const FoliageType& type, Dr } } #else - DrawCluster(renderContext, type.Root, draw); + DrawCluster(context, type.Root, draw); #endif } +void Foliage::InitType(const RenderView& view, FoliageType& type) +{ + const DrawPass drawModes = type._drawModes & view.Pass & view.GetShadowsDrawPassMask(type.ShadowsMode); + type._canDraw = type.IsReady() && drawModes != DrawPass::None && type.Model && type.Model->CanBeRendered(); + bool drawModesDirty = false; + for (int32 j = 0; j < type.Entries.Count(); j++) + { + auto& e = type.Entries[j]; + e.ReceiveDecals = type.ReceiveDecals != 0; + e.ShadowsMode = type.ShadowsMode; + if (type._drawModesDirty) + { + type._drawModesDirty = 0; + drawModesDirty = true; + } + } + if (drawModesDirty) + GetSceneRendering()->UpdateActor(this, _sceneRenderingKey, ISceneRenderingListener::DrawModes); +} + int32 Foliage::GetInstancesCount() const { return Instances.Count(); @@ -1131,12 +1174,7 @@ void Foliage::Draw(RenderContext& renderContext) // Cache data per foliage instance type for (auto& type : FoliageTypes) { - for (int32 j = 0; j < type.Entries.Count(); j++) - { - auto& e = type.Entries[j]; - e.ReceiveDecals = type.ReceiveDecals != 0; - e.ShadowsMode = type.ShadowsMode; - } + InitType(renderContext.View, type); } if (renderContext.View.Pass == DrawPass::GlobalSDF) @@ -1202,12 +1240,7 @@ void Foliage::Draw(RenderContext& renderContext) // Draw single foliage instance projection into Global Surface Atlas auto& instance = *(FoliageInstance*)GlobalSurfaceAtlasPass::Instance()->GetCurrentActorObject(); auto& type = FoliageTypes[instance.Type]; - for (int32 i = 0; i < type.Entries.Count(); i++) - { - auto& e = type.Entries[i]; - e.ReceiveDecals = type.ReceiveDecals != 0; - e.ShadowsMode = type.ShadowsMode; - } + InitType(renderContext.View, type); Matrix world; const Transform transform = _transform.LocalToWorld(instance.Transform); renderContext.View.GetWorldMatrix(transform, world); @@ -1225,7 +1258,7 @@ void Foliage::Draw(RenderContext& renderContext) draw.Deformation = nullptr; draw.Bounds = instance.Bounds; draw.PerInstanceRandom = instance.Random; - draw.DrawModes = type.DrawModes & view.Pass & view.GetShadowsDrawPassMask(type.ShadowsMode); + draw.DrawModes = type._drawModes & view.Pass & view.GetShadowsDrawPassMask(type.ShadowsMode); draw.SetStencilValue(_layer); type.Model->Draw(renderContext, draw); return; @@ -1239,8 +1272,9 @@ void Foliage::Draw(RenderContext& renderContext) draw.LODBias = 0; draw.ForcedLOD = -1; draw.VertexColors = nullptr; + draw.Deformation = nullptr; #else - DrawCallsList drawCallsLists[MODEL_MAX_LODS]; + DrawCallsList draw[MODEL_MAX_LODS]; #endif #if FOLIAGE_USE_SINGLE_QUAD_TREE if (Root) @@ -1248,7 +1282,7 @@ void Foliage::Draw(RenderContext& renderContext) #else for (auto& type : FoliageTypes) { - DrawType(renderContext, type, drawCallsLists); + DrawType(renderContext, type, draw); } #endif } @@ -1265,14 +1299,7 @@ void Foliage::Draw(RenderContextBatch& renderContextBatch) { // Cache data per foliage instance type for (FoliageType& type : FoliageTypes) - { - for (int32 j = 0; j < type.Entries.Count(); j++) - { - auto& e = type.Entries[j]; - e.ReceiveDecals = type.ReceiveDecals != 0; - e.ShadowsMode = type.ShadowsMode; - } - } + InitType(view, type); // Run async job for each foliage type _renderContextBatch = &renderContextBatch; diff --git a/Source/Engine/Foliage/Foliage.h b/Source/Engine/Foliage/Foliage.h index 6f8b36cf4..83ab6206c 100644 --- a/Source/Engine/Foliage/Foliage.h +++ b/Source/Engine/Foliage/Foliage.h @@ -158,6 +158,15 @@ public: private: void AddToCluster(ChunkedArray& clusters, FoliageCluster* cluster, FoliageInstance& instance); + struct DrawContext + { + RenderContext& RenderContext; + const RenderView& LodView; + const FoliageType& FoliageType; + Vector3 ViewOrigin; + float MinObjectPixelSizeSq; + float ViewScreenSizeSq; + }; #if !FOLIAGE_USE_SINGLE_QUAD_TREE && FOLIAGE_USE_DRAW_CALLS_BATCHING struct DrawKey { @@ -181,10 +190,12 @@ private: typedef Array> DrawCallsList; typedef Dictionary BatchedDrawCalls; - void DrawInstance(RenderContext& renderContext, FoliageInstance& instance, const FoliageType& type, Model* model, int32 lod, float lodDitherFactor, DrawCallsList* drawCallsLists, BatchedDrawCalls& result) const; - void DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, const FoliageType& type, DrawCallsList* drawCallsLists, BatchedDrawCalls& result) const; + void DrawInstance(DrawContext& context, FoliageInstance& instance, Model* model, int32 lod, float lodDitherFactor, DrawCallsList* drawCallsLists, BatchedDrawCalls& result) const; + void DrawCluster(DrawContext& context, FoliageCluster* cluster, DrawCallsList* drawCallsLists, BatchedDrawCalls& result) const; + void DrawType(RenderContext& renderContext, const FoliageType& type, DrawCallsList* drawCallsLists); #else - void DrawCluster(RenderContext& renderContext, FoliageCluster* cluster, Mesh::DrawInfo& draw); + void DrawCluster(DrawContext& context, FoliageCluster* cluster, Mesh::DrawInfo& draw); + void DrawType(RenderContext& renderContext, const FoliageType& type, Mesh::DrawInfo& draw); #endif #if !FOLIAGE_USE_SINGLE_QUAD_TREE void DrawClusterGlobalSDF(class GlobalSignDistanceFieldPass* globalSDF, const BoundingBox& globalSDFBounds, FoliageCluster* cluster, const FoliageType& type); @@ -192,7 +203,8 @@ private: void DrawFoliageJob(int32 i); RenderContextBatch* _renderContextBatch; #endif - void DrawType(RenderContext& renderContext, const FoliageType& type, DrawCallsList* drawCallsLists); + + void InitType(const RenderView& view, FoliageType& type); public: /// diff --git a/Source/Engine/Foliage/FoliageType.cpp b/Source/Engine/Foliage/FoliageType.cpp index 8b8c84420..e9eb63b2d 100644 --- a/Source/Engine/Foliage/FoliageType.cpp +++ b/Source/Engine/Foliage/FoliageType.cpp @@ -13,6 +13,7 @@ FoliageType::FoliageType() , Index(-1) { _isReady = 0; + _drawModesDirty = 0; ReceiveDecals = true; UseDensityScaling = false; @@ -32,7 +33,7 @@ FoliageType& FoliageType::operator=(const FoliageType& other) CullDistance = other.CullDistance; CullDistanceRandomRange = other.CullDistanceRandomRange; ScaleInLightmap = other.ScaleInLightmap; - DrawModes = other.DrawModes; + SetDrawModes(other._drawModes); ShadowsMode = other.ShadowsMode; PaintDensity = other.PaintDensity; PaintRadius = other.PaintRadius; @@ -69,6 +70,19 @@ void FoliageType::SetMaterials(const Array& value) Entries[i].Material = value[i]; } +DrawPass FoliageType::GetDrawModes() const +{ + return _drawModes; +} + +void FoliageType::SetDrawModes(DrawPass value) +{ + if (_drawModes == value) + return; + _drawModes = value; + _drawModesDirty = 1; +} + Float3 FoliageType::GetRandomScale() const { Float3 result; @@ -150,7 +164,7 @@ void FoliageType::Serialize(SerializeStream& stream, const void* otherObj) SERIALIZE(CullDistance); SERIALIZE(CullDistanceRandomRange); SERIALIZE(ScaleInLightmap); - SERIALIZE(DrawModes); + SERIALIZE_MEMBER(DrawModes, _drawModes); SERIALIZE(ShadowsMode); SERIALIZE_BIT(ReceiveDecals); SERIALIZE_BIT(UseDensityScaling); @@ -191,7 +205,7 @@ void FoliageType::Deserialize(DeserializeStream& stream, ISerializeModifier* mod DESERIALIZE(CullDistance); DESERIALIZE(CullDistanceRandomRange); DESERIALIZE(ScaleInLightmap); - DESERIALIZE(DrawModes); + DESERIALIZE_MEMBER(DrawModes, _drawModes); DESERIALIZE(ShadowsMode); DESERIALIZE_BIT(ReceiveDecals); DESERIALIZE_BIT(UseDensityScaling); diff --git a/Source/Engine/Foliage/FoliageType.h b/Source/Engine/Foliage/FoliageType.h index bbe7e7739..8b36f618b 100644 --- a/Source/Engine/Foliage/FoliageType.h +++ b/Source/Engine/Foliage/FoliageType.h @@ -47,6 +47,9 @@ API_CLASS(Sealed, NoSpawn) class FLAXENGINE_API FoliageType : public ScriptingOb friend Foliage; private: uint8 _isReady : 1; + uint8 _canDraw : 1; + uint8 _drawModesDirty : 1; + DrawPass _drawModes = DrawPass::Depth | DrawPass::GBuffer | DrawPass::Forward; public: /// @@ -123,9 +126,15 @@ public: API_FIELD() float ScaleInLightmap = 1.0f; /// - /// The draw passes to use for rendering this foliage type. + /// Gets the draw passes to use for rendering this foliage type. /// - API_FIELD() DrawPass DrawModes = DrawPass::Depth | DrawPass::GBuffer | DrawPass::Forward; + API_PROPERTY(Attributes="DefaultValue(DrawPass.Depth | DrawPass.GBuffer | DrawPass.Forward)") + DrawPass GetDrawModes() const; + + /// + /// Sets the draw passes to use for rendering this foliage type. + /// + API_PROPERTY() void SetDrawModes(DrawPass value); /// /// The shadows casting mode. @@ -183,7 +192,7 @@ public: API_FIELD() float PlacementRandomRollAngle = 0.0f; /// - /// The density scaling scale applied to the global scale for the foliage instances of this type. Can be used to boost or reduce density scaling effect on this foliage type. Default is 1. + /// The density scale factor applied to the global scale for the foliage instances of this type. Can be used to boost or reduce density scaling effect on this foliage type. Default is 1. Lower to reduce density scaling effect when downscaling foliage via global quality/scalability. /// API_FIELD() float DensityScalingScale = 1.0f; diff --git a/Source/Engine/Graphics/GPUBufferDescription.cs b/Source/Engine/Graphics/GPUBufferDescription.cs index 107d17b3e..43fcf94bf 100644 --- a/Source/Engine/Graphics/GPUBufferDescription.cs +++ b/Source/Engine/Graphics/GPUBufferDescription.cs @@ -428,6 +428,20 @@ namespace FlaxEngine partial struct VertexElement : IEquatable { + /// + /// Creates the vertex element description. + /// + /// Element type. + /// Data format. + public VertexElement(Types type, PixelFormat format) + { + Type = type; + Slot = 0; + Offset = 0; + PerInstance = 0; + Format = format; + } + /// /// Creates the vertex element description. /// diff --git a/Source/Engine/Graphics/Graphics.cpp b/Source/Engine/Graphics/Graphics.cpp index dd3344f5d..97af4f089 100644 --- a/Source/Engine/Graphics/Graphics.cpp +++ b/Source/Engine/Graphics/Graphics.cpp @@ -28,6 +28,7 @@ Quality Graphics::GIQuality = Quality::High; bool Graphics::GICascadesBlending = false; PostProcessSettings Graphics::PostProcessSettings; bool Graphics::SpreadWorkload = true; +float Graphics::Shadows::MinObjectPixelSize = 2.0f; bool Graphics::PostProcessing::ColorGradingVolumeLUT = true; #if GRAPHICS_API_NULL diff --git a/Source/Engine/Graphics/Graphics.h b/Source/Engine/Graphics/Graphics.h index d328431b0..571a7f086 100644 --- a/Source/Engine/Graphics/Graphics.h +++ b/Source/Engine/Graphics/Graphics.h @@ -85,8 +85,17 @@ public: API_FIELD() static bool SpreadWorkload; public: + // Shadows rendering configuration. + API_CLASS(Static, Attributes="DebugCommand") class FLAXENGINE_API Shadows + { + DECLARE_SCRIPTING_TYPE_MINIMAL(Shadows); + + // The minimum size in pixels of objects to cast shadows. Improves performance by skipping too small objects (eg. sub-pixel) from rendering into shadow maps. + API_FIELD() static float MinObjectPixelSize; + }; + // Post Processing effects rendering configuration. - API_CLASS(Static, Attributes = "DebugCommand") class FLAXENGINE_API PostProcessing + API_CLASS(Static, Attributes="DebugCommand") class FLAXENGINE_API PostProcessing { DECLARE_SCRIPTING_TYPE_MINIMAL(PostProcessing); diff --git a/Source/Engine/Graphics/Materials/TerrainMaterialShader.cpp b/Source/Engine/Graphics/Materials/TerrainMaterialShader.cpp index b6b455f20..30aa8b280 100644 --- a/Source/Engine/Graphics/Materials/TerrainMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/TerrainMaterialShader.cpp @@ -88,9 +88,8 @@ void TerrainMaterialShader::Bind(BindParameters& params) } // Bind terrain textures - const auto heightmap = drawCall.Terrain.Patch->Heightmap->GetTexture(); - const auto splatmap0 = drawCall.Terrain.Patch->Splatmap[0] ? drawCall.Terrain.Patch->Splatmap[0]->GetTexture() : nullptr; - const auto splatmap1 = drawCall.Terrain.Patch->Splatmap[1] ? drawCall.Terrain.Patch->Splatmap[1]->GetTexture() : nullptr; + GPUTexture* heightmap, *splatmap0, *splatmap1; + drawCall.Terrain.Patch->GetTextures(heightmap, splatmap0, splatmap1); context->BindSR(0, heightmap); context->BindSR(1, splatmap0); context->BindSR(2, splatmap1); diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs index 29aa86c18..cd216cdf5 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.cs +++ b/Source/Engine/Graphics/Models/MeshAccessor.cs @@ -1,6 +1,7 @@ // Copyright (c) Wojciech Figat. All rights reserved. using System; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace FlaxEngine @@ -17,13 +18,14 @@ namespace FlaxEngine { private Span _data; private PixelFormat _format; - private int _stride; + private int _stride, _count; private readonly PixelFormatSampler _sampler; - internal Stream(Span data, PixelFormat format, int stride) + internal Stream(Span data, PixelFormat format, int stride, int count) { _data = data; _stride = stride; + _count = count; if (PixelFormatSampler.Get(format, out _sampler)) { _format = format; @@ -52,7 +54,7 @@ namespace FlaxEngine /// /// Gets the count of the items in the stride. /// - public int Count => _data.Length / _stride; + public int Count => _count; /// /// Returns true if stream is valid. @@ -76,6 +78,10 @@ namespace FlaxEngine /// Loaded value. public int GetInt(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return (int)_sampler.Read(data + index * _stride).X; } @@ -87,6 +93,10 @@ namespace FlaxEngine /// Loaded value. public float GetFloat(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return _sampler.Read(data + index * _stride).X; } @@ -98,6 +108,10 @@ namespace FlaxEngine /// Loaded value. public Float2 GetFloat2(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return new Float2(_sampler.Read(data + index * _stride)); } @@ -109,6 +123,10 @@ namespace FlaxEngine /// Loaded value. public Float3 GetFloat3(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return new Float3(_sampler.Read(data + index * _stride)); } @@ -120,6 +138,10 @@ namespace FlaxEngine /// Loaded value. public Float4 GetFloat4(int index) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) return _sampler.Read(data + index * _stride); } @@ -131,9 +153,13 @@ namespace FlaxEngine /// Value to assign. public void SetInt(int index, int value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif var v = new Float4(value); fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref v); + _sampler.Write(data + index * _stride, &v); } /// @@ -143,9 +169,13 @@ namespace FlaxEngine /// Value to assign. public void SetFloat(int index, float value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif var v = new Float4(value); fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref v); + _sampler.Write(data + index * _stride, &v); } /// @@ -155,9 +185,13 @@ namespace FlaxEngine /// Value to assign. public void SetFloat2(int index, Float2 value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif var v = new Float4(value, 0.0f, 0.0f); fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref v); + _sampler.Write(data + index * _stride, &v); } /// @@ -167,9 +201,13 @@ namespace FlaxEngine /// Value to assign. public void SetFloat3(int index, Float3 value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif var v = new Float4(value, 0.0f); fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref v); + _sampler.Write(data + index * _stride, &v); } /// @@ -179,8 +217,12 @@ namespace FlaxEngine /// Value to assign. public void SetFloat4(int index, Float4 value) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif fixed (byte* data = _data) - _sampler.Write(data + index * _stride, ref value); + _sampler.Write(data + index * _stride, &value); } /// @@ -190,6 +232,10 @@ namespace FlaxEngine /// Pointer to the source data. public void SetLinear(IntPtr data) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif new Span(data.ToPointer(), _data.Length).CopyTo(_data); } @@ -199,6 +245,10 @@ namespace FlaxEngine /// The source . public void Set(Span src) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32_Float)) { src.CopyTo(MemoryMarshal.Cast(_data)); @@ -211,7 +261,7 @@ namespace FlaxEngine for (int i = 0; i < count; i++) { var v = new Float4(src[i], 0.0f, 0.0f); - _sampler.Write(data + i * _stride, ref v); + _sampler.Write(data + i * _stride, &v); } } } @@ -223,6 +273,10 @@ namespace FlaxEngine /// The source . public void Set(Span src) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32B32_Float)) { src.CopyTo(MemoryMarshal.Cast(_data)); @@ -235,7 +289,7 @@ namespace FlaxEngine for (int i = 0; i < count; i++) { var v = new Float4(src[i], 0.0f); - _sampler.Write(data + i * _stride, ref v); + _sampler.Write(data + i * _stride, &v); } } } @@ -247,6 +301,10 @@ namespace FlaxEngine /// The source . public void Set(Span src) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32B32A32_Float)) { src.CopyTo(MemoryMarshal.Cast(_data)); @@ -259,7 +317,7 @@ namespace FlaxEngine for (int i = 0; i < count; i++) { var v = (Float4)src[i]; - _sampler.Write(data + i * _stride, ref v); + _sampler.Write(data + i * _stride, &v); } } } @@ -271,6 +329,10 @@ namespace FlaxEngine /// The source . public void Set(Span src) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32_UInt)) { src.CopyTo(MemoryMarshal.Cast(_data)); @@ -292,7 +354,7 @@ namespace FlaxEngine for (int i = 0; i < count; i++) { var v = new Float4(src[i]); - _sampler.Write(data + i * _stride, ref v); + _sampler.Write(data + i * _stride, &v); } } } @@ -304,6 +366,10 @@ namespace FlaxEngine /// The destination . public void CopyTo(Span dst) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32_Float)) { _data.CopyTo(MemoryMarshal.Cast(dst)); @@ -325,6 +391,10 @@ namespace FlaxEngine /// The destination . public void CopyTo(Span dst) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32B32_Float)) { _data.CopyTo(MemoryMarshal.Cast(dst)); @@ -346,6 +416,10 @@ namespace FlaxEngine /// The destination . public void CopyTo(Span dst) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32G32B32A32_Float)) { _data.CopyTo(MemoryMarshal.Cast(dst)); @@ -367,6 +441,10 @@ namespace FlaxEngine /// The destination . public void CopyTo(Span dst) { +#if !BUILD_RELEASE + if (_data == null) + throw new NullReferenceException("Missing stream data. Ensure to allocate mesh buffer or check for its existence."); +#endif if (IsLinear(PixelFormat.R32_UInt)) { _data.CopyTo(MemoryMarshal.Cast(dst)); @@ -390,6 +468,17 @@ namespace FlaxEngine } } } + + /// + /// Checks if stream is valid. + /// + /// The stream to check. + /// True if stream is valid, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator bool(Stream stream) + { + return stream.IsValid; + } } private byte[][] _data = new byte[(int)MeshBufferType.MAX][]; @@ -413,10 +502,16 @@ namespace FlaxEngine return true; for (int i = 0; i < buffersLocal.Length; i++) { - _data[(int)buffersLocal[i]] = meshBuffers[i]; - _layouts[(int)buffersLocal[i]] = meshLayouts[i]; + int buffer = (int)buffersLocal[i]; + _data[buffer] = meshBuffers[i]; + _layouts[buffer] = meshLayouts[i]; + + // Get format if using a single item (eg. index buffer type) + var format = PixelFormat.Unknown; + if (meshLayouts[i] && meshLayouts[i].Elements.Length == 1) + format = meshLayouts[i].Elements[0].Format; + _formats[buffer] = format; } - _formats[(int)MeshBufferType.Index] = mesh.Use16BitIndexBuffer ? PixelFormat.R16_UInt : PixelFormat.R32_UInt; return false; } @@ -504,19 +599,19 @@ namespace FlaxEngine bool use16BitIndexBuffer = false; IntPtr[] vbData = new IntPtr[3]; GPUVertexLayout[] vbLayout = new GPUVertexLayout[3]; - if (_data[VB0] != null) + if (_data[VB0] != null && _data[VB0].Length != 0) { vbData[0] = dataPtr[VB0]; vbLayout[0] = _layouts[VB0]; vertices = (uint)_data[VB0].Length / _layouts[VB0].Stride; } - if (_data[VB1] != null) + if (_data[VB1] != null && _data[VB1].Length != 0) { vbData[1] = dataPtr[VB1]; vbLayout[1] = _layouts[VB1]; vertices = (uint)_data[VB1].Length / _layouts[VB1].Stride; } - if (_data[VB2] != null) + if (_data[VB2] != null && _data[VB2].Length != 0) { vbData[2] = dataPtr[VB2]; vbLayout[2] = _layouts[VB2]; @@ -570,15 +665,16 @@ namespace FlaxEngine { Span data = new Span(); PixelFormat format = PixelFormat.Unknown; - int stride = 0; + int stride = 0, count = 0; var ib = _data[(int)MeshBufferType.Index]; if (ib != null) { data = ib; format = _formats[(int)MeshBufferType.Index]; stride = PixelFormatExtensions.SizeInBytes(format); + count = data.Length / stride; } - return new Stream(data, format, stride); + return new Stream(data, format, stride, count); } /// @@ -590,7 +686,7 @@ namespace FlaxEngine { Span data = new Span(); PixelFormat format = PixelFormat.Unknown; - int stride = 0; + int stride = 0, count = 0; for (int vbIndex = 0; vbIndex < 3 && format == PixelFormat.Unknown; vbIndex++) { int idx = vbIndex + 1; @@ -605,11 +701,12 @@ namespace FlaxEngine data = new Span(vb).Slice(e.Offset); format = e.Format; stride = (int)layout.Stride; + count = vb.Length / stride; break; } } } - return new Stream(data, format, stride); + return new Stream(data, format, stride, count); } /// @@ -716,6 +813,16 @@ namespace FlaxEngine set => SetStreamFloat3(VertexElement.Types.Normal, value, PackNormal); } + /// + /// Gets or sets the vertex tangent vectors (unpacked, normalized). Null if does not exist in vertex buffers of the mesh. + /// + /// Uses stream to read or write data to the vertex buffer. + public Float3[] Tangents + { + get => GetStreamFloat3(VertexElement.Types.Tangent, UnpackNormal); + set => SetStreamFloat3(VertexElement.Types.Tangent, value, PackNormal); + } + /// /// Gets or sets the vertex UVs (texcoord channel 0). Null if does not exist in vertex buffers of the mesh. /// @@ -726,6 +833,70 @@ namespace FlaxEngine set => SetStreamFloat2(VertexElement.Types.TexCoord, value); } + /// + /// Recalculates normal vectors for all vertices. + /// + public void ComputeNormals() + { + var positions = Position(); + var indices = Index(); + if (!positions) + throw new Exception("Cannot compute tangents without positions."); + if (!indices) + throw new Exception("Cannot compute tangents without indices."); + if (!Normal()) + throw new Exception("Cannot compute tangents without Normal vertex element."); + var vertexCount = positions.Count; + if (vertexCount == 0) + return; + var indexCount = indices.Count; + + // Compute per-face normals but store them per-vertex + var normals = new Float3[vertexCount]; + for (int i = 0; i < indexCount; i += 3) + { + var i1 = indices.GetInt(i + 0); + var i2 = indices.GetInt(i + 1); + var i3 = indices.GetInt(i + 2); + Float3 v1 = positions.GetFloat3(i1); + Float3 v2 = positions.GetFloat3(i2); + Float3 v3 = positions.GetFloat3(i3); + Float3 n = Float3.Cross((v2 - v1), (v3 - v1)).Normalized; + + normals[i1] += n; + normals[i2] += n; + normals[i3] += n; + } + + // Average normals + for (int i = 0; i < normals.Length; i++) + normals[i] = normals[i].Normalized; + + // Write back to the buffer + Normals = normals; + } + + /// + /// Recalculates tangent vectors for all vertices based on normals. + /// + public void ComputeTangents() + { + var normals = Normal(); + var tangents = Tangent(); + if (!normals) + throw new Exception("Cannot compute tangents without normals."); + if (!tangents) + throw new Exception("Cannot compute tangents without Tangent vertex element."); + var count = normals.Count; + for (int i = 0; i < count; i++) + { + Float3 normal = normals.GetFloat3(i); + UnpackNormal(ref normal); + RenderTools.CalculateTangentFrame(out var n, out var t, ref normal); + tangents.SetFloat4(i, t); + } + } + private uint[] GetStreamUInt(Stream stream) { uint[] result = null; diff --git a/Source/Engine/Graphics/Models/MeshAccessor.h b/Source/Engine/Graphics/Models/MeshAccessor.h index 25fc01a1a..f0b40b623 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.h +++ b/Source/Engine/Graphics/Models/MeshAccessor.h @@ -4,6 +4,7 @@ #include "Types.h" #include "Engine/Core/Types/DataContainer.h" +#include "Engine/Core/Collections/Array.h" #include "Engine/Graphics/PixelFormat.h" #include "Engine/Graphics/PixelFormatSampler.h" #include "Engine/Graphics/Shaders/VertexElement.h" @@ -24,10 +25,10 @@ public: private: Span _data; PixelFormat _format; - int32 _stride; + int32 _stride, _count; PixelFormatSampler _sampler; - Stream(Span data, PixelFormat format, int32 stride); + Stream(Span data, PixelFormat format, int32 stride, int32 count); public: Span GetData() const; @@ -37,53 +38,68 @@ public: bool IsValid() const; bool IsLinear(PixelFormat expectedFormat) const; + FORCE_INLINE operator bool() const + { + return IsValid(); + } + FORCE_INLINE int32 GetInt(int32 index) const { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); return (int32)_sampler.Read(_data.Get() + index * _stride).X; } FORCE_INLINE float GetFloat(int32 index) const { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); return _sampler.Read(_data.Get() + index * _stride).X; } FORCE_INLINE Float2 GetFloat2(int32 index) const { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); return Float2(_sampler.Read(_data.Get() + index * _stride)); } FORCE_INLINE Float3 GetFloat3(int32 index) const { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); return Float3(_sampler.Read(_data.Get() + index * _stride)); } FORCE_INLINE Float4 GetFloat4(int32 index) const { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); return _sampler.Read(_data.Get() + index * _stride); } FORCE_INLINE void SetInt(int32 index, const int32 value) { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); _sampler.Write(_data.Get() + index * _stride, Float4((float)value)); } FORCE_INLINE void SetFloat(int32 index, const float value) { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); _sampler.Write(_data.Get() + index * _stride, Float4(value)); } FORCE_INLINE void SetFloat2(int32 index, const Float2& value) { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); _sampler.Write(_data.Get() + index * _stride, Float4(value)); } FORCE_INLINE void SetFloat3(int32 index, const Float3& value) { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); _sampler.Write(_data.Get() + index * _stride, Float4(value)); } FORCE_INLINE void SetFloat4(int32 index, const Float4& value) { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); _sampler.Write(_data.Get() + index * _stride, value); } @@ -94,19 +110,33 @@ public: void Set(Span src); void Set(Span src); void Set(Span src); + template + void Set(const Array& dst) const + { + Set(Span(dst.Get(), dst.Count())); + } // Copies the contents of this stream into a destination data span. void CopyTo(Span dst) const; void CopyTo(Span dst) const; void CopyTo(Span dst) const; + template + void CopyTo(Array& dst) const + { + dst.Resize(GetCount()); + CopyTo(Span(dst.Get(), dst.Count())); + } }; private: BytesContainer _data[(int32)MeshBufferType::MAX]; PixelFormat _formats[(int32)MeshBufferType::MAX] = {}; GPUVertexLayout* _layouts[(int32)MeshBufferType::MAX] = {}; + Array> _usedModels; public: + ~MeshAccessor(); + /// /// Loads the data from the mesh. /// diff --git a/Source/Engine/Graphics/Models/MeshBase.cpp b/Source/Engine/Graphics/Models/MeshBase.cpp index eadbfcba9..435f6d0c8 100644 --- a/Source/Engine/Graphics/Models/MeshBase.cpp +++ b/Source/Engine/Graphics/Models/MeshBase.cpp @@ -38,10 +38,11 @@ namespace #endif } -MeshAccessor::Stream::Stream(Span data, PixelFormat format, int32 stride) +MeshAccessor::Stream::Stream(Span data, PixelFormat format, int32 stride, int32 count) : _data(data) , _format(PixelFormat::Unknown) , _stride(stride) + , _count(count) { auto sampler = PixelFormatSampler::Get(format); if (sampler) @@ -72,7 +73,7 @@ int32 MeshAccessor::Stream::GetStride() const int32 MeshAccessor::Stream::GetCount() const { - return _data.Length() / _stride; + return _count; } bool MeshAccessor::Stream::IsValid() const @@ -158,6 +159,14 @@ void MeshAccessor::Stream::CopyTo(Span dst) const { Platform::MemoryCopy(dst.Get(), _data.Get(), _data.Length()); } + else if (IsLinear(PixelFormat::R16G16B16A16_Float)) + { + for (int32 i = 0; i < count; i++) + { + auto v = *(Half4*)(_data.Get() + i * _stride); + dst.Get()[i] = Float3(Float16Compressor::Decompress(v.X), Float16Compressor::Decompress(v.Y), Float16Compressor::Decompress(v.Z)); + } + } else { for (int32 i = 0; i < count; i++) @@ -180,6 +189,16 @@ void MeshAccessor::Stream::CopyTo(Span<::Color> dst) const } } +MeshAccessor::~MeshAccessor() +{ + for (ModelBase* model : _usedModels) + { + if (model->Storage) + model->Storage->UnlockChunks(); + model->RemoveReference(); + } +} + bool MeshAccessor::LoadMesh(const MeshBase* mesh, bool forceGpu, Span buffers) { CHECK_RETURN(mesh, true); @@ -188,14 +207,28 @@ bool MeshAccessor::LoadMesh(const MeshBase* mesh, bool forceGpu, Span(allBuffers, ARRAY_COUNT(allBuffers)); Array> meshBuffers; Array> meshLayouts; + if (ModelBase* model = mesh->GetModelBase()) + { + // Maintain reference to mesh data (it's buffers might be referenced, not copied) + model->AddReference(); + if (model->Storage) + model->Storage->LockChunks(); + _usedModels.Add(model); + } if (mesh->DownloadData(buffers, meshBuffers, meshLayouts, forceGpu)) return true; for (int32 i = 0; i < buffers.Length(); i++) { - _data[(int32)buffers[i]] = MoveTemp(meshBuffers[i]); - _layouts[(int32)buffers[i]] = meshLayouts[i]; + const int32 buffer = (int32)buffers[i]; + _data[buffer] = MoveTemp(meshBuffers[i]); + _layouts[buffer] = meshLayouts[i]; + + // Get format if using a single item (eg. index buffer type) + PixelFormat format = PixelFormat::Unknown; + if (meshLayouts[i] && meshLayouts[i]->GetElements().Count() == 1) + format = meshLayouts[i]->GetElements()[0].Format; + _formats[buffer] = format; } - _formats[(int32)MeshBufferType::Index] = mesh->Use16BitIndexBuffer() ? PixelFormat::R16_UInt : PixelFormat::R32_UInt; return false; } @@ -336,22 +369,23 @@ MeshAccessor::Stream MeshAccessor::Index() { Span data; PixelFormat format = PixelFormat::Unknown; - int32 stride = 0; + int32 stride = 0, count = 0; auto& ib = _data[(int32)MeshBufferType::Index]; if (ib.IsValid()) { data = ib; format = _formats[(int32)MeshBufferType::Index]; stride = PixelFormatExtensions::SizeInBytes(format); + count = data.Length() / stride; } - return Stream(data, format, stride); + return Stream(data, format, stride, count); } MeshAccessor::Stream MeshAccessor::Attribute(VertexElement::Types attribute) { Span data; PixelFormat format = PixelFormat::Unknown; - int32 stride = 0; + int32 stride = 0, count = 0; for (int32 vbIndex = 0; vbIndex < 3 && format == PixelFormat::Unknown; vbIndex++) { static_assert((int32)MeshBufferType::Vertex0 == 1, "Update code."); @@ -367,11 +401,12 @@ MeshAccessor::Stream MeshAccessor::Attribute(VertexElement::Types attribute) data = vb.Slice(e.Offset); format = e.Format; stride = layout->GetStride(); + count = vb.Length() / stride; break; } } } - return Stream(data, format, stride); + return Stream(data, format, stride, count); } MeshBase::~MeshBase() @@ -706,6 +741,9 @@ bool MeshBase::DownloadDataCPU(MeshBufferType type, BytesContainer& result, int3 _cachedVertexBufferCount = meshData.Vertices; _cachedIndexBufferCount = (int32)meshData.Triangles * 3; _cachedIndexBuffer.Copy((const byte*)meshData.IBData, _cachedIndexBufferCount * (int32)meshData.IBStride); + GPUVertexLayout::Elements ibLayout; + ibLayout.Add({ VertexElement::Types::Attribute, 0, 0, 0, meshData.IBStride == sizeof(uint16) ? PixelFormat::R16_UInt : PixelFormat::R32_UInt }); + _cachedVertexLayouts[3] = GPUVertexLayout::Get(ibLayout); for (int32 vb = 0; vb < meshData.VBData.Count(); vb++) { _cachedVertexBuffers[vb].Copy((const byte*)meshData.VBData[vb], (int32)(meshData.VBLayout[vb]->GetStride() * meshData.Vertices)); @@ -720,6 +758,8 @@ bool MeshBase::DownloadDataCPU(MeshBufferType type, BytesContainer& result, int3 case MeshBufferType::Index: result.Link(_cachedIndexBuffer); count = _cachedIndexBufferCount; + if (layout) + *layout = _cachedVertexLayouts[3]; break; case MeshBufferType::Vertex0: result.Link(_cachedVertexBuffers[0]); diff --git a/Source/Engine/Graphics/Models/MeshBase.h b/Source/Engine/Graphics/Models/MeshBase.h index 1c76df166..cb51f466b 100644 --- a/Source/Engine/Graphics/Models/MeshBase.h +++ b/Source/Engine/Graphics/Models/MeshBase.h @@ -50,7 +50,7 @@ protected: GPUBuffer* _indexBuffer = nullptr; mutable BytesContainer _cachedVertexBuffers[MODEL_MAX_VB]; - mutable GPUVertexLayout* _cachedVertexLayouts[MODEL_MAX_VB] = {}; + mutable GPUVertexLayout* _cachedVertexLayouts[MODEL_MAX_VB + 1] = {}; mutable BytesContainer _cachedIndexBuffer; mutable int32 _cachedIndexBufferCount = 0, _cachedVertexBufferCount = 0; diff --git a/Source/Engine/Graphics/PixelFormatSampler.cs b/Source/Engine/Graphics/PixelFormatSampler.cs index 8ff5a32fb..62046b2a5 100644 --- a/Source/Engine/Graphics/PixelFormatSampler.cs +++ b/Source/Engine/Graphics/PixelFormatSampler.cs @@ -22,7 +22,7 @@ namespace FlaxEngine /// /// Write data function. /// - public delegate* unmanaged Write; + public delegate* unmanaged Write; /// /// Tries to get a sampler tool for the specified format to read pixels. @@ -38,7 +38,7 @@ namespace FlaxEngine Format = format, PixelSize = pixelSize, Read = (delegate* unmanaged)read.ToPointer(), - Write = (delegate* unmanaged)write.ToPointer(), + Write = (delegate* unmanaged)write.ToPointer(), }; return pixelSize != 0; } diff --git a/Source/Engine/Graphics/RenderTools.cpp b/Source/Engine/Graphics/RenderTools.cpp index effbe6e1b..2cf1c9707 100644 --- a/Source/Engine/Graphics/RenderTools.cpp +++ b/Source/Engine/Graphics/RenderTools.cpp @@ -548,6 +548,13 @@ void RenderTools::ComputeCascadeUpdateFrequency(int32 cascadeIndex, int32 cascad } } +bool RenderTools::ShouldUpdateCascade(int32 frameIndex, int32 cascadeIndex, int32 cascadeCount, int32 updateMaxCountPerFrame, bool updateForce) +{ + int32 updateFrequency, updatePhrase; + ComputeCascadeUpdateFrequency(cascadeIndex, cascadeCount, updateFrequency, updatePhrase, updateMaxCountPerFrame); + return (frameIndex % updateFrequency == updatePhrase) || updateForce; +} + float RenderTools::ComputeTemporalTime() { const float time = Time::Draw.UnscaledTime.GetTotalSeconds(); diff --git a/Source/Engine/Graphics/RenderTools.h b/Source/Engine/Graphics/RenderTools.h index 5f0dc23dc..0608c4338 100644 --- a/Source/Engine/Graphics/RenderTools.h +++ b/Source/Engine/Graphics/RenderTools.h @@ -119,12 +119,7 @@ public: static void ComputeCascadeUpdateFrequency(int32 cascadeIndex, int32 cascadeCount, int32& updateFrequency, int32& updatePhrase, int32 updateMaxCountPerFrame = 1); // Checks if cached data should be updated during the given frame. - FORCE_INLINE static bool ShouldUpdateCascade(int32 frameIndex, int32 cascadeIndex, int32 cascadeCount, int32 updateMaxCountPerFrame = 1, bool updateForce = false) - { - int32 updateFrequency, updatePhrase; - ComputeCascadeUpdateFrequency(cascadeIndex, cascadeCount, updateFrequency, updatePhrase, updateMaxCountPerFrame); - return (frameIndex % updateFrequency == updatePhrase) || updateForce; - } + static bool ShouldUpdateCascade(int32 frameIndex, int32 cascadeIndex, int32 cascadeCount, int32 updateMaxCountPerFrame = 1, bool updateForce = false); // Calculates temporal offset in the dithering factor that gets cleaned out by TAA. // Returns 0-1 value based on unscaled draw time for temporal effects to reduce artifacts from screen-space dithering when using Temporal Anti-Aliasing. @@ -136,8 +131,9 @@ public: DEPRECATED("Use CalculateTangentFrame with unpacked Float3/Float4.") static void CalculateTangentFrame(FloatR10G10B10A2& resultNormal, FloatR10G10B10A2& resultTangent, const Float3& normal, const Float3& tangent); // Result normal/tangent are already packed into [0;1] range. - static void CalculateTangentFrame(Float3& resultNormal, Float4& resultTangent, const Float3& normal); - static void CalculateTangentFrame(Float3& resultNormal, Float4& resultTangent, const Float3& normal, const Float3& tangent); + API_FUNCTION() static void CalculateTangentFrame(API_PARAM(Out) Float3& resultNormal, API_PARAM(Out) Float4& resultTangent, API_PARAM(Ref) const Float3& normal); + // Result normal/tangent are already packed into [0;1] range. + API_FUNCTION() static void CalculateTangentFrame(API_PARAM(Out) Float3& resultNormal, API_PARAM(Out) Float4& resultTangent, API_PARAM(Ref) const Float3& normal, API_PARAM(Ref) const Float3& tangent); static void ComputeSphereModelDrawMatrix(const RenderView& view, const Float3& position, float radius, Matrix& resultWorld, bool& resultIsViewInside); diff --git a/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.cpp index 36eacccf9..737c147d9 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.cpp @@ -136,6 +136,15 @@ void CmdBufferVulkan::EndEvent() #endif +void CmdBufferVulkan::Wait(float timeoutSeconds) +{ + PROFILE_CPU(); + ASSERT(IsSubmitted()); + const bool failed = _device->FenceManager.WaitForFence(GetFence(), timeoutSeconds); + ASSERT(!failed); + RefreshFenceStatus(); +} + void CmdBufferVulkan::RefreshFenceStatus() { if (_state == State::Submitted) @@ -186,9 +195,8 @@ CmdBufferVulkan::~CmdBufferVulkan() auto& fenceManager = _device->FenceManager; if (_state == State::Submitted) { - // Wait 60ms - const uint64 waitForCmdBufferInNanoSeconds = 60 * 1000 * 1000LL; - fenceManager.WaitAndReleaseFence(_fence, waitForCmdBufferInNanoSeconds); + // Wait + fenceManager.WaitAndReleaseFence(_fence); } else { @@ -281,15 +289,6 @@ void CmdBufferManagerVulkan::SubmitActiveCmdBuffer(SemaphoreVulkan* signalSemaph _activeCmdBuffer = nullptr; } -void CmdBufferManagerVulkan::WaitForCmdBuffer(CmdBufferVulkan* cmdBuffer, float timeInSecondsToWait) -{ - PROFILE_CPU(); - ASSERT(cmdBuffer->IsSubmitted()); - const bool failed = _device->FenceManager.WaitForFence(cmdBuffer->GetFence(), (uint64)(timeInSecondsToWait * 1e9)); - ASSERT(!failed); - cmdBuffer->RefreshFenceStatus(); -} - void CmdBufferManagerVulkan::PrepareForNewActiveCommandBuffer() { PROFILE_CPU(); diff --git a/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.h b/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.h index 7cb3ee104..c8e096cdd 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.h +++ b/Source/Engine/GraphicsDevice/Vulkan/CmdBufferVulkan.h @@ -136,6 +136,7 @@ public: void EndEvent(); #endif + void Wait(float timeoutSeconds = VULKAN_WAIT_TIMEOUT); void RefreshFenceStatus(); }; @@ -206,7 +207,6 @@ public: public: void SubmitActiveCmdBuffer(SemaphoreVulkan* signalSemaphore = nullptr); - void WaitForCmdBuffer(CmdBufferVulkan* cmdBuffer, float timeInSecondsToWait = 1.0f); void RefreshFenceStatus(CmdBufferVulkan* skipCmdBuffer = nullptr) { _pool.RefreshFenceStatus(skipCmdBuffer); diff --git a/Source/Engine/GraphicsDevice/Vulkan/Config.h b/Source/Engine/GraphicsDevice/Vulkan/Config.h index 1f30c301a..7b4e54137 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/Config.h +++ b/Source/Engine/GraphicsDevice/Vulkan/Config.h @@ -49,4 +49,9 @@ #define VULKAN_USE_QUERIES 1 #endif +// Fence wait operation timeout in seconds +#ifndef VULKAN_WAIT_TIMEOUT +#define VULKAN_WAIT_TIMEOUT 5.0f +#endif + #endif diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp index e96f7b3b3..71ef42005 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp @@ -2184,11 +2184,12 @@ FenceVulkan* FenceManagerVulkan::AllocateFence(bool createSignaled) return fence; } -bool FenceManagerVulkan::WaitForFence(FenceVulkan* fence, uint64 timeInNanoseconds) const +bool FenceManagerVulkan::WaitForFence(FenceVulkan* fence, float timeoutSeconds) const { ASSERT(_usedFences.Contains(fence)); ASSERT(!fence->IsSignaled); - const VkResult result = vkWaitForFences(_device->Device, 1, &fence->Handle, true, timeInNanoseconds); + uint64 timeNanoseconds = (uint64)((double)timeoutSeconds * 1000000000.0); + const VkResult result = vkWaitForFences(_device->Device, 1, &fence->Handle, true, timeNanoseconds); LOG_VULKAN_RESULT(result); if (result == VK_SUCCESS) { @@ -2216,11 +2217,11 @@ void FenceManagerVulkan::ReleaseFence(FenceVulkan*& fence) fence = nullptr; } -void FenceManagerVulkan::WaitAndReleaseFence(FenceVulkan*& fence, uint64 timeInNanoseconds) +void FenceManagerVulkan::WaitAndReleaseFence(FenceVulkan*& fence, float timeoutSeconds) { ScopeLock lock(_device->_fenceLock); if (!fence->IsSignaled) - WaitForFence(fence, timeInNanoseconds); + WaitForFence(fence, timeoutSeconds); ResetFence(fence); _usedFences.Remove(fence); _freeFences.Add(fence); diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.h b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.h index 09fa93f3e..a22cd9cc6 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.h +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.h @@ -88,7 +88,7 @@ public: } // Returns true if waiting timed out or failed, false otherwise. - bool WaitForFence(FenceVulkan* fence, uint64 timeInNanoseconds) const; + bool WaitForFence(FenceVulkan* fence, float timeoutSeconds = VULKAN_WAIT_TIMEOUT) const; void ResetFence(FenceVulkan* fence) const; @@ -96,7 +96,7 @@ public: void ReleaseFence(FenceVulkan*& fence); // Sets the fence handle to null - void WaitAndReleaseFence(FenceVulkan*& fence, uint64 timeInNanoseconds); + void WaitAndReleaseFence(FenceVulkan*& fence, float timeoutSeconds = VULKAN_WAIT_TIMEOUT); private: // Returns true if fence was signaled, otherwise false. diff --git a/Source/Engine/GraphicsDevice/Vulkan/QueueVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/QueueVulkan.cpp index f2d2521d0..39b0d2691 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/QueueVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/QueueVulkan.cpp @@ -62,11 +62,7 @@ void QueueVulkan::Submit(CmdBufferVulkan* cmdBuffer, uint32 signalSemaphoresCoun const bool WaitForIdleOnSubmit = false; if (WaitForIdleOnSubmit) { - // Use 200ms timeout - bool success = _device->FenceManager.WaitForFence(fence, 200 * 1000 * 1000); - ASSERT(success); - ASSERT(_device->FenceManager.IsFenceSignaled(fence)); - cmdBuffer->GetOwner()->RefreshFenceStatus(); + cmdBuffer->Wait(); } #endif diff --git a/Source/Engine/Input/Gamepad.cpp b/Source/Engine/Input/Gamepad.cpp index f4a2ef4ab..32856839b 100644 --- a/Source/Engine/Input/Gamepad.cpp +++ b/Source/Engine/Input/Gamepad.cpp @@ -2,6 +2,71 @@ #include "Gamepad.h" +namespace +{ + GamepadAxis GetButtonAxis(GamepadButton button, bool& positive) + { + positive = true; + switch (button) + { + case GamepadButton::LeftTrigger: + return GamepadAxis::LeftTrigger; + case GamepadButton::RightTrigger: + return GamepadAxis::RightTrigger; + case GamepadButton::LeftStickUp: + return GamepadAxis::LeftStickY; + case GamepadButton::LeftStickDown: + positive = false; + return GamepadAxis::LeftStickY; + case GamepadButton::LeftStickLeft: + positive = false; + return GamepadAxis::LeftStickX; + case GamepadButton::LeftStickRight: + return GamepadAxis::LeftStickX; + case GamepadButton::RightStickUp: + return GamepadAxis::RightStickY; + case GamepadButton::RightStickDown: + positive = false; + return GamepadAxis::RightStickY; + case GamepadButton::RightStickLeft: + positive = false; + return GamepadAxis::RightStickX; + case GamepadButton::RightStickRight: + return GamepadAxis::RightStickX; + default: + return GamepadAxis::None; + } + } + + bool GetButtonState(const Gamepad::State& state, GamepadButton button, float deadZone) + { + if (deadZone > 0.01f) + { + switch (button) + { + case GamepadButton::LeftTrigger: + case GamepadButton::RightTrigger: + case GamepadButton::LeftStickUp: + case GamepadButton::LeftStickDown: + case GamepadButton::LeftStickLeft: + case GamepadButton::LeftStickRight: + case GamepadButton::RightStickUp: + case GamepadButton::RightStickDown: + case GamepadButton::RightStickLeft: + case GamepadButton::RightStickRight: + { + bool positive; + float axis = state.Axis[(int32)GetButtonAxis(button, positive)]; + return positive ? axis >= deadZone : axis <= -deadZone; + } + default: + break; + } + } + return state.Buttons[(int32)button]; + } +} + void GamepadLayout::Init() { for (int32 i = 0; i < (int32)GamepadButton::MAX; i++) @@ -31,6 +96,21 @@ void Gamepad::ResetState() _mappedPrevState.Clear(); } +bool Gamepad::GetButton(GamepadButton button, float deadZone) const +{ + return GetButtonState(_mappedState, button, deadZone); +} + +bool Gamepad::GetButtonDown(GamepadButton button, float deadZone) const +{ + return GetButtonState(_mappedState, button, deadZone) && !GetButtonState(_mappedPrevState, button, deadZone); +} + +bool Gamepad::GetButtonUp(GamepadButton button, float deadZone) const +{ + return !GetButtonState(_mappedState, button, deadZone) && GetButtonState(_mappedPrevState, button, deadZone); +} + bool Gamepad::IsAnyButtonDown() const { // TODO: optimize with SIMD diff --git a/Source/Engine/Input/Gamepad.h b/Source/Engine/Input/Gamepad.h index 20994d85a..dc95c67ae 100644 --- a/Source/Engine/Input/Gamepad.h +++ b/Source/Engine/Input/Gamepad.h @@ -148,36 +148,30 @@ public: /// Gets the gamepad button state (true if being pressed during the current frame). /// /// Gamepad button to check + /// Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered. /// True if user holds down the button, otherwise false. - API_FUNCTION() FORCE_INLINE bool GetButton(const GamepadButton button) const - { - return _mappedState.Buttons[static_cast(button)]; - } + API_FUNCTION() bool GetButton(GamepadButton button, float deadZone = 0.0f) const; /// /// Gets the gamepad button down state (true if was pressed during the current frame). /// /// Gamepad button to check + /// Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered. /// True if user starts pressing down the button, otherwise false. - API_FUNCTION() FORCE_INLINE bool GetButtonDown(const GamepadButton button) const - { - return _mappedState.Buttons[static_cast(button)] && !_mappedPrevState.Buttons[static_cast(button)]; - } - - /// - /// Checks if any gamepad button is currently pressed. - /// - API_PROPERTY() bool IsAnyButtonDown() const; + API_FUNCTION() bool GetButtonDown(GamepadButton button, float deadZone = 0.0f) const; /// /// Gets the gamepad button up state (true if was released during the current frame). /// /// Gamepad button to check + /// Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered. /// True if user releases the button, otherwise false. - API_FUNCTION() FORCE_INLINE bool GetButtonUp(const GamepadButton button) const - { - return !_mappedState.Buttons[static_cast(button)] && _mappedPrevState.Buttons[static_cast(button)]; - } + API_FUNCTION() bool GetButtonUp(GamepadButton button, float deadZone = 0.0f) const; + + /// + /// Checks if any gamepad button is currently pressed. + /// + API_PROPERTY() bool IsAnyButtonDown() const; public: /// diff --git a/Source/Engine/Input/Input.cpp b/Source/Engine/Input/Input.cpp index 7048140ef..c95d7b03e 100644 --- a/Source/Engine/Input/Input.cpp +++ b/Source/Engine/Input/Input.cpp @@ -120,6 +120,7 @@ void InputSettings::Deserialize(DeserializeStream& stream, ISerializeModifier* m config.MouseButton = JsonTools::GetEnum(v, "MouseButton", MouseButton::None); config.GamepadButton = JsonTools::GetEnum(v, "GamepadButton", GamepadButton::None); config.Gamepad = JsonTools::GetEnum(v, "Gamepad", InputGamepadIndex::All); + config.DeadZone = JsonTools::GetFloat(v, "DeadZone", 0.5f); } } else @@ -499,24 +500,24 @@ float Input::GetGamepadAxis(int32 gamepadIndex, GamepadAxis axis) return 0.0f; } -bool Input::GetGamepadButton(int32 gamepadIndex, GamepadButton button) +bool Input::GetGamepadButton(int32 gamepadIndex, GamepadButton button, float deadZone) { if (gamepadIndex >= 0 && gamepadIndex < Gamepads.Count()) - return Gamepads[gamepadIndex]->GetButton(button); + return Gamepads[gamepadIndex]->GetButton(button, deadZone); return false; } -bool Input::GetGamepadButtonDown(int32 gamepadIndex, GamepadButton button) +bool Input::GetGamepadButtonDown(int32 gamepadIndex, GamepadButton button, float deadZone) { if (gamepadIndex >= 0 && gamepadIndex < Gamepads.Count()) - return Gamepads[gamepadIndex]->GetButtonDown(button); + return Gamepads[gamepadIndex]->GetButtonDown(button, deadZone); return false; } -bool Input::GetGamepadButtonUp(int32 gamepadIndex, GamepadButton button) +bool Input::GetGamepadButtonUp(int32 gamepadIndex, GamepadButton button, float deadZone) { if (gamepadIndex >= 0 && gamepadIndex < Gamepads.Count()) - return Gamepads[gamepadIndex]->GetButtonUp(button); + return Gamepads[gamepadIndex]->GetButtonUp(button, deadZone); return false; } @@ -542,13 +543,13 @@ float Input::GetGamepadAxis(InputGamepadIndex gamepad, GamepadAxis axis) return false; } -bool Input::GetGamepadButton(InputGamepadIndex gamepad, GamepadButton button) +bool Input::GetGamepadButton(InputGamepadIndex gamepad, GamepadButton button, float deadZone) { if (gamepad == InputGamepadIndex::All) { for (auto g : Gamepads) { - if (g->GetButton(button)) + if (g->GetButton(button, deadZone)) return true; } } @@ -556,18 +557,18 @@ bool Input::GetGamepadButton(InputGamepadIndex gamepad, GamepadButton button) { const auto index = static_cast(gamepad); if (index < Gamepads.Count()) - return Gamepads[index]->GetButton(button); + return Gamepads[index]->GetButton(button, deadZone); } return false; } -bool Input::GetGamepadButtonDown(InputGamepadIndex gamepad, GamepadButton button) +bool Input::GetGamepadButtonDown(InputGamepadIndex gamepad, GamepadButton button, float deadZone) { if (gamepad == InputGamepadIndex::All) { for (auto g : Gamepads) { - if (g->GetButtonDown(button)) + if (g->GetButtonDown(button, deadZone)) return true; } } @@ -575,18 +576,18 @@ bool Input::GetGamepadButtonDown(InputGamepadIndex gamepad, GamepadButton button { const auto index = static_cast(gamepad); if (index < Gamepads.Count()) - return Gamepads[index]->GetButtonDown(button); + return Gamepads[index]->GetButtonDown(button, deadZone); } return false; } -bool Input::GetGamepadButtonUp(InputGamepadIndex gamepad, GamepadButton button) +bool Input::GetGamepadButtonUp(InputGamepadIndex gamepad, GamepadButton button, float deadZone) { if (gamepad == InputGamepadIndex::All) { for (auto g : Gamepads) { - if (g->GetButtonUp(button)) + if (g->GetButtonUp(button, deadZone)) return true; } } @@ -594,7 +595,7 @@ bool Input::GetGamepadButtonUp(InputGamepadIndex gamepad, GamepadButton button) { const auto index = static_cast(gamepad); if (index < Gamepads.Count()) - return Gamepads[index]->GetButtonUp(button); + return Gamepads[index]->GetButtonUp(button, deadZone); } return false; } @@ -1065,26 +1066,26 @@ void InputService::Update() bool isActive; if (config.Mode == InputActionMode::Pressing) { - isActive = Input::GetKey(config.Key) || Input::GetMouseButton(config.MouseButton) || Input::GetGamepadButton(config.Gamepad, config.GamepadButton); + isActive = Input::GetKey(config.Key) || Input::GetMouseButton(config.MouseButton) || Input::GetGamepadButton(config.Gamepad, config.GamepadButton, config.DeadZone); } else if (config.Mode == InputActionMode::Press) { - isActive = Input::GetKeyDown(config.Key) || Input::GetMouseButtonDown(config.MouseButton) || Input::GetGamepadButtonDown(config.Gamepad, config.GamepadButton); + isActive = Input::GetKeyDown(config.Key) || Input::GetMouseButtonDown(config.MouseButton) || Input::GetGamepadButtonDown(config.Gamepad, config.GamepadButton, config.DeadZone); } else { - isActive = Input::GetKeyUp(config.Key) || Input::GetMouseButtonUp(config.MouseButton) || Input::GetGamepadButtonUp(config.Gamepad, config.GamepadButton); + isActive = Input::GetKeyUp(config.Key) || Input::GetMouseButtonUp(config.MouseButton) || Input::GetGamepadButtonUp(config.Gamepad, config.GamepadButton, config.DeadZone); } - if (Input::GetKeyDown(config.Key) || Input::GetMouseButtonDown(config.MouseButton) || Input::GetGamepadButtonDown(config.Gamepad, config.GamepadButton)) + if (Input::GetKeyDown(config.Key) || Input::GetMouseButtonDown(config.MouseButton) || Input::GetGamepadButtonDown(config.Gamepad, config.GamepadButton, config.DeadZone)) { data.State = InputActionState::Press; } - else if (Input::GetKey(config.Key) || Input::GetMouseButton(config.MouseButton) || Input::GetGamepadButton(config.Gamepad, config.GamepadButton)) + else if (Input::GetKey(config.Key) || Input::GetMouseButton(config.MouseButton) || Input::GetGamepadButton(config.Gamepad, config.GamepadButton, config.DeadZone)) { data.State = InputActionState::Pressing; } - else if (Input::GetKeyUp(config.Key) || Input::GetMouseButtonUp(config.MouseButton) || Input::GetGamepadButtonUp(config.Gamepad, config.GamepadButton)) + else if (Input::GetKeyUp(config.Key) || Input::GetMouseButtonUp(config.MouseButton) || Input::GetGamepadButtonUp(config.Gamepad, config.GamepadButton, config.DeadZone)) { data.State = InputActionState::Release; } diff --git a/Source/Engine/Input/Input.h b/Source/Engine/Input/Input.h index 73e87f5f0..dca26a5f4 100644 --- a/Source/Engine/Input/Input.h +++ b/Source/Engine/Input/Input.h @@ -238,24 +238,27 @@ public: /// /// The gamepad index /// Gamepad button to check + /// Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered. /// True if user holds down the button, otherwise false. - API_FUNCTION() static bool GetGamepadButton(int32 gamepadIndex, GamepadButton button); + API_FUNCTION() static bool GetGamepadButton(int32 gamepadIndex, GamepadButton button, float deadZone = 0.0f); /// /// Gets the gamepad button down state (true if was pressed during the current frame). /// /// The gamepad index /// Gamepad button to check + /// Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered. /// True if user starts pressing down the button, otherwise false. - API_FUNCTION() static bool GetGamepadButtonDown(int32 gamepadIndex, GamepadButton button); + API_FUNCTION() static bool GetGamepadButtonDown(int32 gamepadIndex, GamepadButton button, float deadZone = 0.0f); /// /// Gets the gamepad button up state (true if was released during the current frame). /// /// The gamepad index /// Gamepad button to check + /// Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered. /// True if user releases the button, otherwise false. - API_FUNCTION() static bool GetGamepadButtonUp(int32 gamepadIndex, GamepadButton button); + API_FUNCTION() static bool GetGamepadButtonUp(int32 gamepadIndex, GamepadButton button, float deadZone = 0.0f); /// /// Gets the gamepad axis value. @@ -270,24 +273,27 @@ public: /// /// The gamepad /// Gamepad button to check + /// Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered. /// True if user holds down the button, otherwise false. - API_FUNCTION() static bool GetGamepadButton(InputGamepadIndex gamepad, GamepadButton button); + API_FUNCTION() static bool GetGamepadButton(InputGamepadIndex gamepad, GamepadButton button, float deadZone = 0.0f); /// /// Gets the gamepad button down state (true if was pressed during the current frame). /// /// The gamepad /// Gamepad button to check + /// Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered. /// True if user starts pressing down the button, otherwise false. - API_FUNCTION() static bool GetGamepadButtonDown(InputGamepadIndex gamepad, GamepadButton button); + API_FUNCTION() static bool GetGamepadButtonDown(InputGamepadIndex gamepad, GamepadButton button, float deadZone = 0.0f); /// /// Gets the gamepad button up state (true if was released during the current frame). /// /// The gamepad /// Gamepad button to check + /// Custom dead-zone value to detect gamepad button usage for non-binary buttons such as left/right thumbs that can move freely. By default, any movement is registered. /// True if user releases the button, otherwise false. - API_FUNCTION() static bool GetGamepadButtonUp(InputGamepadIndex gamepad, GamepadButton button); + API_FUNCTION() static bool GetGamepadButtonUp(InputGamepadIndex gamepad, GamepadButton button, float deadZone = 0.0f); public: /// diff --git a/Source/Engine/Input/VirtualInput.h b/Source/Engine/Input/VirtualInput.h index 817d7014c..a4975aaef 100644 --- a/Source/Engine/Input/VirtualInput.h +++ b/Source/Engine/Input/VirtualInput.h @@ -48,6 +48,12 @@ API_STRUCT() struct ActionConfig /// API_FIELD(Attributes="EditorOrder(40)") InputGamepadIndex Gamepad; + + /// + /// Threshold for non-binary value inputs such as gamepad stick position to decide if action was triggered. Can be sued to activate action only if input value is higher than specified number. + /// + API_FIELD(Attributes = "EditorOrder(50)") + float DeadZone = 0.5f; }; /// diff --git a/Source/Engine/Level/Actor.cpp b/Source/Engine/Level/Actor.cpp index f52fab600..8123142aa 100644 --- a/Source/Engine/Level/Actor.cpp +++ b/Source/Engine/Level/Actor.cpp @@ -5,6 +5,7 @@ #include "Level.h" #include "SceneQuery.h" #include "SceneObjectsFactory.h" +#include "FlaxEngine.Gen.h" #include "Scene/Scene.h" #include "Prefabs/Prefab.h" #include "Prefabs/PrefabManager.h" @@ -178,21 +179,20 @@ void Actor::OnDeleteObject() _scene = nullptr; } } - else if (_parent) + else { - // Unlink from the parent - _parent->Children.RemoveKeepOrder(this); - _parent->_isHierarchyDirty = true; - _parent = nullptr; - _scene = nullptr; + if (_isEnabled) + OnDisable(); + if (_parent) + { + // Unlink from the parent + _parent->Children.RemoveKeepOrder(this); + _parent->_isHierarchyDirty = true; + _parent = nullptr; + _scene = nullptr; + } } - // Ensure to exit gameplay in a valid way - ASSERT(!IsDuringPlay()); -#if BUILD_DEBUG || BUILD_DEVELOPMENT - ASSERT(!_isEnabled); -#endif - // Fire event Deleted(this); diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index 11497e558..ddafd215d 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -1397,6 +1397,16 @@ bool AnimatedModel::GetMeshData(const MeshReference& ref, MeshBufferType type, B return mesh.DownloadDataCPU(type, result, count, layout); } +MeshBase* AnimatedModel::GetMesh(const MeshReference& ref) const +{ + const auto model = SkinnedModel.Get(); + if (!model || model->WaitForLoaded()) + return nullptr; + auto& lod = model->LODs[Math::Min(ref.LODIndex, model->LODs.Count() - 1)]; + auto& mesh = lod.Meshes[Math::Min(ref.MeshIndex, lod.Meshes.Count() - 1)]; + return &mesh; +} + MeshDeformation* AnimatedModel::GetMeshDeformation() const { if (!_deformation) diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index a520d6723..f9182d115 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -479,6 +479,7 @@ public: bool IntersectsEntry(int32 entryIndex, const Ray& ray, Real& distance, Vector3& normal) override; bool IntersectsEntry(const Ray& ray, Real& distance, Vector3& normal, int32& entryIndex) override; bool GetMeshData(const MeshReference& ref, MeshBufferType type, BytesContainer& result, int32& count, GPUVertexLayout** layout) const override; + MeshBase* GetMesh(const MeshReference& ref) const override; void UpdateBounds() override; MeshDeformation* GetMeshDeformation() const override; void OnDeleteObject() override; diff --git a/Source/Engine/Level/Actors/ModelInstanceActor.cpp b/Source/Engine/Level/Actors/ModelInstanceActor.cpp index 527775bfc..b8dc3af1a 100644 --- a/Source/Engine/Level/Actors/ModelInstanceActor.cpp +++ b/Source/Engine/Level/Actors/ModelInstanceActor.cpp @@ -14,6 +14,12 @@ String ModelInstanceActor::MeshReference::ToString() const return String::Format(TEXT("Actor={},LOD={},Mesh={}"), Actor ? Actor->GetNamePath() : String::Empty, LODIndex, MeshIndex); } +MeshBase* ModelInstanceActor::MeshReference::Get() const +{ + auto actor = Actor.Get(); + return actor ? actor->GetMesh(*this) : nullptr; +} + void ModelInstanceActor::SetEntries(const Array& value) { WaitForModelLoad(); diff --git a/Source/Engine/Level/Actors/ModelInstanceActor.h b/Source/Engine/Level/Actors/ModelInstanceActor.h index d5e2498fb..d553d132a 100644 --- a/Source/Engine/Level/Actors/ModelInstanceActor.h +++ b/Source/Engine/Level/Actors/ModelInstanceActor.h @@ -29,6 +29,7 @@ API_CLASS(Abstract) class FLAXENGINE_API ModelInstanceActor : public Actor API_FIELD() int32 MeshIndex = 0; String ToString() const; + MeshBase* Get() const; }; protected: @@ -113,6 +114,7 @@ public: /// /// Extracts mesh buffer data from CPU. Might be cached internally (eg. by Model/SkinnedModel). + /// [Deprecated in 1.12] /// /// Mesh reference. /// Buffer type @@ -120,11 +122,22 @@ public: /// The amount of items inside the result buffer. /// The result layout of the result buffer (for vertex buffers). Optional, pass null to ignore it. /// True if failed, otherwise false. + DEPRECATED("Use GetMesh to resolve mesh reference and access mesh data with MeshAccessor.") virtual bool GetMeshData(const MeshReference& ref, MeshBufferType type, BytesContainer& result, int32& count, GPUVertexLayout** layout = nullptr) const { return true; } + /// + /// Resolves a given mesh reference. + /// + /// Mesh reference. + /// Mesh or null if invalid ref. + virtual MeshBase* GetMesh(const MeshReference& ref) const + { + return nullptr; + } + /// /// Gets the mesh deformation utility for this model instance (optional). /// diff --git a/Source/Engine/Level/Actors/PointLight.cpp b/Source/Engine/Level/Actors/PointLight.cpp index 607bf1bc4..b5801166e 100644 --- a/Source/Engine/Level/Actors/PointLight.cpp +++ b/Source/Engine/Level/Actors/PointLight.cpp @@ -1,6 +1,7 @@ // Copyright (c) Wojciech Figat. All rights reserved. #include "PointLight.h" +#include "Engine/Content/Deprecated.h" #include "Engine/Graphics/RenderTask.h" #include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/RenderView.h" @@ -196,6 +197,14 @@ void PointLight::Deserialize(DeserializeStream& stream, ISerializeModifier* modi DESERIALIZE(UseInverseSquaredFalloff); DESERIALIZE(UseIESBrightness); DESERIALIZE(IESBrightnessScale); + + // [Deprecated on 12.03.2026, expires on 12.03.2028] + if (modifier->EngineBuild <= 6807 && SERIALIZE_FIND_MEMBER(stream, "UseInverseSquaredFalloff") != stream.MemberEnd() && UseInverseSquaredFalloff) + { + // Convert old non-physical brightness value that was used for Inverse Squared Falloff which wasn't based on proper cm/m units calculations + MARK_CONTENT_DEPRECATED(); + Brightness = Math::Sqrt(Brightness * 0.01f); + } } bool PointLight::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) diff --git a/Source/Engine/Level/Actors/SpotLight.cpp b/Source/Engine/Level/Actors/SpotLight.cpp index 85b77647a..38502cf1b 100644 --- a/Source/Engine/Level/Actors/SpotLight.cpp +++ b/Source/Engine/Level/Actors/SpotLight.cpp @@ -1,6 +1,8 @@ // Copyright (c) Wojciech Figat. All rights reserved. #include "SpotLight.h" + +#include "Engine/Content/Deprecated.h" #include "Engine/Graphics/RenderView.h" #include "Engine/Renderer/RenderList.h" #include "Engine/Content/Assets/IESProfile.h" @@ -282,6 +284,14 @@ void SpotLight::Deserialize(DeserializeStream& stream, ISerializeModifier* modif DESERIALIZE(UseInverseSquaredFalloff); DESERIALIZE(UseIESBrightness); DESERIALIZE(IESBrightnessScale); + + // [Deprecated on 12.03.2026, expires on 12.03.2028] + if (modifier->EngineBuild <= 6807 && SERIALIZE_FIND_MEMBER(stream, "UseInverseSquaredFalloff") != stream.MemberEnd() && UseInverseSquaredFalloff) + { + // Convert old non-physical brightness value that was used for Inverse Squared Falloff which wasn't based on proper cm/m units calculations + MARK_CONTENT_DEPRECATED(); + Brightness = Math::Sqrt(Brightness * 0.01f); + } } bool SpotLight::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) diff --git a/Source/Engine/Level/Actors/StaticModel.cpp b/Source/Engine/Level/Actors/StaticModel.cpp index f41e4a805..fad068bf0 100644 --- a/Source/Engine/Level/Actors/StaticModel.cpp +++ b/Source/Engine/Level/Actors/StaticModel.cpp @@ -66,6 +66,20 @@ void StaticModel::SetBoundsScale(float value) UpdateBounds(); } +DrawPass StaticModel::GetDrawModes() const +{ + return _drawModes; +} + +void StaticModel::SetDrawModes(DrawPass value) +{ + if (_drawModes == value) + return; + _drawModes = value; + if (_sceneRenderingKey != -1) + GetSceneRendering()->UpdateActor(this, _sceneRenderingKey, ISceneRenderingListener::DrawModes); +} + int32 StaticModel::GetLODBias() const { return _lodBias; @@ -330,13 +344,13 @@ void StaticModel::Draw(RenderContext& renderContext) return; if (renderContext.View.Pass == DrawPass::GlobalSDF) { - if (EnumHasAnyFlags(DrawModes, DrawPass::GlobalSDF) && Model->SDF.Texture) + if (EnumHasAnyFlags(_drawModes, DrawPass::GlobalSDF) && Model->SDF.Texture) GlobalSignDistanceFieldPass::Instance()->RasterizeModelSDF(this, Model->SDF, _transform, _box); return; } if (renderContext.View.Pass == DrawPass::GlobalSurfaceAtlas) { - if (EnumHasAnyFlags(DrawModes, DrawPass::GlobalSurfaceAtlas) && Model->SDF.Texture) + if (EnumHasAnyFlags(_drawModes, DrawPass::GlobalSurfaceAtlas) && Model->SDF.Texture) GlobalSurfaceAtlasPass::Instance()->RasterizeActor(this, this, _sphere, _transform, Model->LODs.Last().GetBox()); return; } @@ -353,7 +367,7 @@ void StaticModel::Draw(RenderContext& renderContext) draw.Lightmap = _scene ? _scene->LightmapsData.GetReadyLightmap(Lightmap.TextureIndex) : nullptr; draw.LightmapUVs = &Lightmap.UVsArea; draw.Flags = _staticFlags; - draw.DrawModes = DrawModes; + draw.DrawModes = _drawModes; draw.Bounds = _sphere; draw.Bounds.Center -= renderContext.View.Origin; draw.PerInstanceRandom = GetPerInstanceRandom(); @@ -390,7 +404,7 @@ void StaticModel::Draw(RenderContextBatch& renderContextBatch) draw.Lightmap = _scene ? _scene->LightmapsData.GetReadyLightmap(Lightmap.TextureIndex) : nullptr; draw.LightmapUVs = &Lightmap.UVsArea; draw.Flags = _staticFlags; - draw.DrawModes = DrawModes; + draw.DrawModes = _drawModes; draw.Bounds = _sphere; draw.Bounds.Center -= renderContext.View.Origin; draw.PerInstanceRandom = GetPerInstanceRandom(); @@ -435,7 +449,7 @@ void StaticModel::Serialize(SerializeStream& stream, const void* otherObj) SERIALIZE_MEMBER(LODBias, _lodBias); SERIALIZE_MEMBER(ForcedLOD, _forcedLod); SERIALIZE_MEMBER(SortOrder, _sortOrder); - SERIALIZE(DrawModes); + SERIALIZE_MEMBER(DrawModes, _drawModes); if (HasLightmap() #if USE_EDITOR @@ -487,7 +501,7 @@ void StaticModel::Deserialize(DeserializeStream& stream, ISerializeModifier* mod DESERIALIZE_MEMBER(LODBias, _lodBias); DESERIALIZE_MEMBER(ForcedLOD, _forcedLod); DESERIALIZE_MEMBER(SortOrder, _sortOrder); - DESERIALIZE(DrawModes); + DESERIALIZE_MEMBER(DrawModes, _drawModes); DESERIALIZE_MEMBER(LightmapIndex, Lightmap.TextureIndex); DESERIALIZE_MEMBER(LightmapArea, Lightmap.UVsArea); @@ -537,27 +551,27 @@ void StaticModel::Deserialize(DeserializeStream& stream, ISerializeModifier* mod if (member != stream.MemberEnd() && member->value.IsBool() && member->value.GetBool()) { MARK_CONTENT_DEPRECATED(); - DrawModes = DrawPass::Depth; + _drawModes = DrawPass::Depth; } } // [Deprecated on 07.02.2022, expires on 07.02.2024] if (modifier->EngineBuild <= 6330) { MARK_CONTENT_DEPRECATED(); - DrawModes |= DrawPass::GlobalSDF; + _drawModes |= DrawPass::GlobalSDF; } // [Deprecated on 27.04.2022, expires on 27.04.2024] if (modifier->EngineBuild <= 6331) { MARK_CONTENT_DEPRECATED(); - DrawModes |= DrawPass::GlobalSurfaceAtlas; + _drawModes |= DrawPass::GlobalSurfaceAtlas; } { const auto member = stream.FindMember("RenderPasses"); if (member != stream.MemberEnd() && member->value.IsInt()) { - DrawModes = (DrawPass)member->value.GetInt(); + _drawModes = (DrawPass)member->value.GetInt(); } } } @@ -651,6 +665,16 @@ bool StaticModel::GetMeshData(const MeshReference& ref, MeshBufferType type, Byt return mesh.DownloadDataCPU(type, result, count, layout); } +MeshBase* StaticModel::GetMesh(const MeshReference& ref) const +{ + const auto model = Model.Get(); + if (!model || model->WaitForLoaded()) + return nullptr; + auto& lod = model->LODs[Math::Min(ref.LODIndex, model->LODs.Count() - 1)]; + auto& mesh = lod.Meshes[Math::Min(ref.MeshIndex, lod.Meshes.Count() - 1)]; + return &mesh; +} + MeshDeformation* StaticModel::GetMeshDeformation() const { if (!_deformation) diff --git a/Source/Engine/Level/Actors/StaticModel.h b/Source/Engine/Level/Actors/StaticModel.h index e6ed701cc..063638e14 100644 --- a/Source/Engine/Level/Actors/StaticModel.h +++ b/Source/Engine/Level/Actors/StaticModel.h @@ -23,6 +23,7 @@ private: bool _vertexColorsDirty; byte _vertexColorsCount; int8 _sortOrder; + DrawPass _drawModes = DrawPass::Default; Array _vertexColorsData[MODEL_MAX_LODS]; GPUBuffer* _vertexColorsBuffer[MODEL_MAX_LODS]; Model* _residencyChangedModel = nullptr; @@ -40,12 +41,6 @@ public: API_FIELD(Attributes="EditorOrder(20), DefaultValue(null), EditorDisplay(\"Model\")") AssetReference Model; - /// - /// The draw passes to use for rendering this object. - /// - API_FIELD(Attributes="EditorOrder(15), DefaultValue(DrawPass.Default), EditorDisplay(\"Model\")") - DrawPass DrawModes = DrawPass::Default; - /// /// The baked lightmap entry. /// @@ -74,6 +69,17 @@ public: /// API_PROPERTY() void SetBoundsScale(float value); + /// + /// Gets the draw passes to use for rendering this object. + /// + API_PROPERTY(Attributes="EditorOrder(15), DefaultValue(DrawPass.Default), EditorDisplay(\"Model\")") + DrawPass GetDrawModes() const; + + /// + /// Sets the draw passes to use for rendering this object. + /// + API_PROPERTY() void SetDrawModes(DrawPass value); + /// /// Gets the model Level Of Detail bias value. Allows to increase or decrease rendered model quality. /// @@ -175,6 +181,7 @@ public: bool IntersectsEntry(int32 entryIndex, const Ray& ray, Real& distance, Vector3& normal) override; bool IntersectsEntry(const Ray& ray, Real& distance, Vector3& normal, int32& entryIndex) override; bool GetMeshData(const MeshReference& ref, MeshBufferType type, BytesContainer& result, int32& count, GPUVertexLayout** layout) const override; + MeshBase* GetMesh(const MeshReference& ref) const override; MeshDeformation* GetMeshDeformation() const override; void UpdateBounds() override; diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index 9cf6794be..adea4575b 100644 --- a/Source/Engine/Level/Level.cpp +++ b/Source/Engine/Level/Level.cpp @@ -5,6 +5,7 @@ #include "LargeWorlds.h" #include "SceneQuery.h" #include "SceneObjectsFactory.h" +#include "FlaxEngine.Gen.h" #include "Scene/Scene.h" #include "Engine/Content/Content.h" #include "Engine/Content/Deprecated.h" @@ -958,9 +959,6 @@ bool LevelImpl::unloadScene(Scene* scene) // Simple enqueue scene root object to be deleted scene->DeleteObject(); - // Force flush deleted objects so we actually delete unloaded scene objects (prevent from reloading their managed objects, etc.) - ObjectsRemovalService::Flush(); - return false; } @@ -1123,6 +1121,32 @@ SceneResult SceneLoader::OnBegin(Args& args) _lastSceneLoadTime = DateTime::Now(); StartFrame = Engine::UpdateCount; + // Validate arguments + if (!args.Data.IsArray()) + { + LOG(Error, "Invalid Data member."); + CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, Guid::Empty); + return SceneResult::Failed; + } + + // Peek scene node value (it's the first actor serialized) + SceneId = JsonTools::GetGuid(args.Data[0], "ID"); + if (!SceneId.IsValid()) + { + LOG(Error, "Invalid scene id."); + CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId); + return SceneResult::Failed; + } + + // Peek meta + if (args.EngineBuild < 6000) + { + LOG(Error, "Invalid serialized engine build."); + CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId); + return SceneResult::Failed; + } + Modifier->EngineBuild = args.EngineBuild; + // Scripting backend should be loaded for the current project before loading scene if (!Scripting::HasGameModulesLoaded()) { @@ -1136,27 +1160,7 @@ SceneResult SceneLoader::OnBegin(Args& args) MessageBox::Show(TEXT("Failed to load scripts.\n\nCannot load scene without game script modules.\n\nSee logs for more info."), TEXT("Missing game modules"), MessageBoxButtons::OK, MessageBoxIcon::Error); } #endif - return SceneResult::Failed; - } - - // Peek meta - if (args.EngineBuild < 6000) - { - LOG(Error, "Invalid serialized engine build."); - return SceneResult::Failed; - } - if (!args.Data.IsArray()) - { - LOG(Error, "Invalid Data member."); - return SceneResult::Failed; - } - Modifier->EngineBuild = args.EngineBuild; - - // Peek scene node value (it's the first actor serialized) - SceneId = JsonTools::GetGuid(args.Data[0], "ID"); - if (!SceneId.IsValid()) - { - LOG(Error, "Invalid scene id."); + CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId); return SceneResult::Failed; } @@ -1164,6 +1168,7 @@ SceneResult SceneLoader::OnBegin(Args& args) if (Level::FindScene(SceneId) != nullptr) { LOG(Info, "Scene {0} is already loaded.", SceneId); + CallSceneEvent(SceneEventType::OnSceneLoadError, nullptr, SceneId); return SceneResult::Failed; } diff --git a/Source/Engine/Level/Scene/Scene.cpp b/Source/Engine/Level/Scene/Scene.cpp index f60aa8d2f..b45424a3d 100644 --- a/Source/Engine/Level/Scene/Scene.cpp +++ b/Source/Engine/Level/Scene/Scene.cpp @@ -389,11 +389,14 @@ void Scene::BeginPlay(SceneBeginData* data) if (model == nullptr) CreateCsgModel(); } + + Ticking.SetTicking(true); } void Scene::EndPlay() { // Improve scene cleanup performance by removing all data from scene rendering and ticking containers + Ticking.SetTicking(false); Ticking.Clear(); Rendering.Clear(); Navigation.Clear(); diff --git a/Source/Engine/Level/Scene/SceneRendering.h b/Source/Engine/Level/Scene/SceneRendering.h index 25dfc63fa..d522454a6 100644 --- a/Source/Engine/Level/Scene/SceneRendering.h +++ b/Source/Engine/Level/Scene/SceneRendering.h @@ -56,6 +56,7 @@ public: Layer = 4, StaticFlags = 8, AutoDelayDuringRendering = 16, // Conditionally allow updating data during rendering when writes are locked + DrawModes = 32, Auto = Visual | Bounds | Layer, }; diff --git a/Source/Engine/Level/Scene/SceneTicking.cpp b/Source/Engine/Level/Scene/SceneTicking.cpp index 30a551117..2b0ccc506 100644 --- a/Source/Engine/Level/Scene/SceneTicking.cpp +++ b/Source/Engine/Level/Scene/SceneTicking.cpp @@ -44,7 +44,7 @@ void SceneTicking::TickData::Tick() { TickScripts(Scripts); - for (int32 i = 0; i < Ticks.Count(); i++) + for (int32 i = 0; i < Ticks.Count() && _canTick; i++) Ticks.Get()[i].Call(); } @@ -66,7 +66,7 @@ void SceneTicking::TickData::TickExecuteInEditor() { TickScripts(ScriptsExecuteInEditor); - for (int32 i = 0; i < TicksExecuteInEditor.Count(); i++) + for (int32 i = 0; i < TicksExecuteInEditor.Count() && _canTick; i++) TicksExecuteInEditor.Get()[i].Call(); } @@ -89,10 +89,8 @@ SceneTicking::FixedUpdateTickData::FixedUpdateTickData() void SceneTicking::FixedUpdateTickData::TickScripts(const Array& scripts) { - for (auto* script : scripts) - { - script->OnFixedUpdate(); - } + for (int32 i = 0; i < scripts.Count() && _canTick; i++) + scripts.Get()[i]->OnFixedUpdate(); } SceneTicking::UpdateTickData::UpdateTickData() @@ -102,36 +100,30 @@ SceneTicking::UpdateTickData::UpdateTickData() void SceneTicking::UpdateTickData::TickScripts(const Array& scripts) { - for (auto* script : scripts) - { - script->OnUpdate(); - } + for (int32 i = 0; i < scripts.Count() && _canTick; i++) + scripts.Get()[i]->OnUpdate(); } SceneTicking::LateUpdateTickData::LateUpdateTickData() - : TickData(64) + : TickData(0) { } void SceneTicking::LateUpdateTickData::TickScripts(const Array& scripts) { - for (auto* script : scripts) - { - script->OnLateUpdate(); - } + for (int32 i = 0; i < scripts.Count() && _canTick; i++) + scripts.Get()[i]->OnLateUpdate(); } SceneTicking::LateFixedUpdateTickData::LateFixedUpdateTickData() - : TickData(64) + : TickData(0) { } void SceneTicking::LateFixedUpdateTickData::TickScripts(const Array& scripts) { - for (auto* script : scripts) - { - script->OnLateFixedUpdate(); - } + for (int32 i = 0; i < scripts.Count() && _canTick; i++) + scripts.Get()[i]->OnLateFixedUpdate(); } void SceneTicking::AddScript(Script* obj) @@ -167,3 +159,11 @@ void SceneTicking::Clear() LateUpdate.Clear(); LateFixedUpdate.Clear(); } + +void SceneTicking::SetTicking(bool enable) +{ + FixedUpdate._canTick = enable; + Update._canTick = enable; + LateUpdate._canTick = enable; + LateFixedUpdate._canTick = enable; +} diff --git a/Source/Engine/Level/Scene/SceneTicking.h b/Source/Engine/Level/Scene/SceneTicking.h index 78e028b8e..26040852b 100644 --- a/Source/Engine/Level/Scene/SceneTicking.h +++ b/Source/Engine/Level/Scene/SceneTicking.h @@ -46,6 +46,9 @@ public: /// class FLAXENGINE_API TickData { + protected: + friend SceneTicking; + bool _canTick = true; public: Array Scripts; Array Ticks; @@ -134,6 +137,11 @@ public: /// void Clear(); + /// + /// Changes the ability to tick. When disabled, the ticking functions won't be called. Can be called during ticking (eg. when scene is unloaded) to stp performing any more ticks. + /// + void SetTicking(bool enable); + public: /// /// The fixed update tick function. diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp index 576b95b46..59d23a994 100644 --- a/Source/Engine/Level/SceneObjectsFactory.cpp +++ b/Source/Engine/Level/SceneObjectsFactory.cpp @@ -448,8 +448,7 @@ void SceneObjectsFactory::PrefabSyncData::InitNewObjects() void SceneObjectsFactory::SetupPrefabInstances(Context& context, const PrefabSyncData& data) { PROFILE_CPU_NAMED("SetupPrefabInstances"); - const int32 count = data.Data.Size(); - ASSERT(count <= data.SceneObjects.Count()); + const int32 count = Math::Min(data.Data.Size(), data.SceneObjects.Count()); Dictionary parentIdsLookup; for (int32 i = 0; i < count; i++) { diff --git a/Source/Engine/Physics/Actors/Cloth.cpp b/Source/Engine/Physics/Actors/Cloth.cpp index 1f833fa0e..3e0eb42d4 100644 --- a/Source/Engine/Physics/Actors/Cloth.cpp +++ b/Source/Engine/Physics/Actors/Cloth.cpp @@ -133,7 +133,7 @@ Array Cloth::GetParticles() const if (_cloth) { PROFILE_CPU(); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); PhysicsBackend::LockClothParticles(_cloth); const Span particles = PhysicsBackend::GetClothParticles(_cloth); result.Resize(particles.Length()); @@ -150,7 +150,7 @@ Array Cloth::GetParticles() const void Cloth::SetParticles(Span value) { PROFILE_CPU(); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); #if USE_CLOTH_SANITY_CHECKS { // Sanity check @@ -180,7 +180,7 @@ Span Cloth::GetPaint() const void Cloth::SetPaint(Span value) { PROFILE_CPU(); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); #if USE_CLOTH_SANITY_CHECKS { // Sanity check @@ -208,8 +208,12 @@ void Cloth::SetPaint(Span value) if (_cloth) { // Update cloth particles + MeshAccessor accessor; + MeshBufferType bufferTypes[2] = { MeshBufferType::Index, MeshBufferType::Vertex0 }; + if (accessor.LoadMesh(GetMesh().Get(), false, ToSpan(bufferTypes, 2))) + return; Array invMasses; - CalculateInvMasses(invMasses); + CalculateInvMasses(accessor, invMasses); PhysicsBackend::LockClothParticles(_cloth); PhysicsBackend::SetClothParticles(_cloth, Span(), Span(), ToSpan(invMasses)); PhysicsBackend::SetClothPaint(_cloth, value); @@ -227,18 +231,20 @@ bool Cloth::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) if (_cloth) { // Precise per-triangle intersection - const ModelInstanceActor::MeshReference mesh = GetMesh(); - if (mesh.Actor == nullptr) + const ModelInstanceActor::MeshReference meshRef = GetMesh(); + if (meshRef.Actor == nullptr) return false; - BytesContainer indicesData; - int32 indicesCount; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) + MeshAccessor accessor; + MeshBufferType bufferTypes[1] = { MeshBufferType::Index }; + if (accessor.LoadMesh(meshRef.Get(), false, ToSpan(bufferTypes, 1))) return false; + auto indices = accessor.Index(); + auto indicesData = indices.GetData(); PhysicsBackend::LockClothParticles(_cloth); const Span particles = PhysicsBackend::GetClothParticles(_cloth); const Transform transform = GetTransform(); - const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); - const int32 trianglesCount = indicesCount / 3; + const bool indices16bit = indices.GetFormat() == PixelFormat::R16_UInt; + const int32 trianglesCount = indices.GetCount() / 3; bool result = false; distance = MAX_Real; for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) @@ -306,7 +312,7 @@ void Cloth::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) { Actor::Deserialize(stream, modifier); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); DESERIALIZE_MEMBER(Mesh, _mesh); _mesh.Actor = nullptr; // Don't store this reference DESERIALIZE_MEMBER(Force, _forceSettings); @@ -341,18 +347,20 @@ void Cloth::DrawPhysicsDebug(RenderView& view) if (_cloth) { PROFILE_CPU(); - const ModelInstanceActor::MeshReference mesh = GetMesh(); - if (mesh.Actor == nullptr) + const ModelInstanceActor::MeshReference meshRef = GetMesh(); + if (meshRef.Actor == nullptr) return; - BytesContainer indicesData; - int32 indicesCount; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) + MeshAccessor accessor; + MeshBufferType bufferTypes[1] = { MeshBufferType::Index }; + if (accessor.LoadMesh(meshRef.Get(), false, ToSpan(bufferTypes, 1))) return; + auto indices = accessor.Index(); + auto indicesData = indices.GetData(); PhysicsBackend::LockClothParticles(_cloth); const Span particles = PhysicsBackend::GetClothParticles(_cloth); const Transform transform = GetTransform(); - const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); - const int32 trianglesCount = indicesCount / 3; + const bool indices16bit = indices.GetFormat() == PixelFormat::R16_UInt; + const int32 trianglesCount = indices.GetCount() / 3; for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) { const int32 index = triangleIndex * 3; @@ -390,18 +398,20 @@ void Cloth::OnDebugDrawSelected() if (_cloth) { DEBUG_DRAW_WIRE_BOX(_box, Color::Violet.RGBMultiplied(0.8f), 0, true); - const ModelInstanceActor::MeshReference mesh = GetMesh(); - if (mesh.Actor == nullptr) + const ModelInstanceActor::MeshReference meshRef = GetMesh(); + if (meshRef.Actor == nullptr) return; - BytesContainer indicesData; - int32 indicesCount; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) + MeshAccessor accessor; + MeshBufferType bufferTypes[1] = { MeshBufferType::Index }; + if (accessor.LoadMesh(meshRef.Get(), false, ToSpan(bufferTypes, 1))) return; + auto indices = accessor.Index(); + auto indicesData = indices.GetData(); PhysicsBackend::LockClothParticles(_cloth); const Span particles = PhysicsBackend::GetClothParticles(_cloth); const Transform transform = GetTransform(); - const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); - const int32 trianglesCount = indicesCount / 3; + const bool indices16bit = indices.GetFormat() == PixelFormat::R16_UInt; + const int32 trianglesCount = indices.GetCount() / 3; for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) { const int32 index = triangleIndex * 3; @@ -542,7 +552,7 @@ bool Cloth::CreateCloth() { #if WITH_CLOTH PROFILE_CPU(); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); // Skip if all vertices are fixed so cloth sim doesn't make sense if (_paint.HasItems()) @@ -556,26 +566,36 @@ bool Cloth::CreateCloth() // Get mesh data // TODO: consider making it via async task so physics can wait on the cloth setup from mesh data just before next fixed update which gives more time when loading scene - const ModelInstanceActor::MeshReference mesh = GetMesh(); - if (mesh.Actor == nullptr) + const ModelInstanceActor::MeshReference meshRef = GetMesh(); + if (meshRef.Actor == nullptr) return false; PhysicsClothDesc desc; desc.Actor = this; - BytesContainer data; - int32 count; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Vertex0, data, count)) + MeshAccessor accessor; + MeshBufferType bufferTypes[2] = { MeshBufferType::Index, MeshBufferType::Vertex0 }; + if (accessor.LoadMesh(meshRef.Get(), false, ToSpan(bufferTypes, 2))) return true; - // TODO: use MeshAccessor vertex data layout descriptor instead hardcoded position data at the beginning of VB0 - desc.VerticesData = data.Get(); - desc.VerticesCount = count; - desc.VerticesStride = data.Length() / count; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, data, count)) - return true; - desc.IndicesData = data.Get(); - desc.IndicesCount = count; - desc.IndicesStride = data.Length() / count; + auto position = accessor.Position(); + Array tempPositions; + if (position.GetFormat() == PixelFormat::R32G32B32_Float) + { + desc.VerticesData = position.GetData().Get(); + desc.VerticesCount = position.GetCount(); + desc.VerticesStride = position.GetStride(); + } + else + { + position.CopyTo(tempPositions); + desc.VerticesData = tempPositions.Get(); + desc.VerticesCount = tempPositions.Count(); + desc.VerticesStride = sizeof(Float3); + } + auto indices = accessor.Index(); + desc.IndicesData = indices.GetData().Get(); + desc.IndicesCount = indices.GetCount(); + desc.IndicesStride = indices.GetStride(); Array invMasses; - CalculateInvMasses(invMasses); + CalculateInvMasses(accessor, invMasses); desc.InvMassesData = invMasses.Count() == desc.VerticesCount ? invMasses.Get() : nullptr; desc.InvMassesStride = sizeof(float); desc.MaxDistancesData = _paint.Count() == desc.VerticesCount ? _paint.Get() : nullptr; @@ -595,13 +615,13 @@ bool Cloth::CreateCloth() PhysicsBackend::ClearClothInertia(_cloth); // Add cloth mesh deformer - if (auto* deformation = mesh.Actor->GetMeshDeformation()) + if (auto* deformation = meshRef.Actor->GetMeshDeformation()) { Function deformer; deformer.Bind(this); - deformation->AddDeformer(mesh.LODIndex, mesh.MeshIndex, MeshBufferType::Vertex0, deformer); + deformation->AddDeformer(meshRef.LODIndex, meshRef.MeshIndex, MeshBufferType::Vertex0, deformer); if (_simulationSettings.ComputeNormals) - deformation->AddDeformer(mesh.LODIndex, mesh.MeshIndex, MeshBufferType::Vertex1, deformer); + deformation->AddDeformer(meshRef.LODIndex, meshRef.MeshIndex, MeshBufferType::Vertex1, deformer); _meshDeformation = deformation; } @@ -631,39 +651,32 @@ void Cloth::DestroyCloth() #endif } -void Cloth::CalculateInvMasses(Array& invMasses) +void Cloth::CalculateInvMasses(MeshAccessor& accessor, Array& invMasses) { // Use per-particle max distance to evaluate which particles are immovable #if WITH_CLOTH if (_paint.IsEmpty()) return; PROFILE_CPU(); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); // Get mesh data - const ModelInstanceActor::MeshReference mesh = GetMesh(); - if (mesh.Actor == nullptr) - return; - BytesContainer verticesData; - int32 verticesCount; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Vertex0, verticesData, verticesCount)) - return; - BytesContainer indicesData; - int32 indicesCount; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) - return; + auto positions = accessor.Position(); + auto indices = accessor.Index(); + CHECK(positions.IsValid() && indices.IsValid()); + int32 verticesCount = positions.GetCount(); if (_paint.Count() != verticesCount) { // Fix incorrect paint data - LOG(Warning, "Incorrect cloth '{}' paint size {} for mesh '{}' that has {} vertices", GetNamePath(), _paint.Count(), mesh.ToString(), verticesCount); + LOG(Warning, "Incorrect cloth '{}' paint size {} for mesh '{}' that has {} vertices", GetNamePath(), _paint.Count(), GetMesh().ToString(), verticesCount); int32 countBefore = _paint.Count(); _paint.Resize(verticesCount); for (int32 i = countBefore; i < verticesCount; i++) _paint.Get()[i] = 0.0f; } - const int32 verticesStride = verticesData.Length() / verticesCount; - const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); - const int32 trianglesCount = indicesCount / 3; + const bool indices16bit = indices.GetFormat() == PixelFormat::R16_UInt; + const int32 trianglesCount = indices.GetCount() / 3; + auto indicesData = indices.GetData(); // Sum triangle area for each influenced particle invMasses.Resize(verticesCount); @@ -676,12 +689,9 @@ void Cloth::CalculateInvMasses(Array& invMasses) const int32 i0 = indicesData.Get()[index]; const int32 i1 = indicesData.Get()[index + 1]; const int32 i2 = indicesData.Get()[index + 2]; - // TODO: use MeshAccessor vertex data layout descriptor instead hardcoded position data at the beginning of VB0 -#define GET_POS(i) *(Float3*)((byte*)verticesData.Get() + i * verticesStride) - const Float3 v0(GET_POS(i0)); - const Float3 v1(GET_POS(i1)); - const Float3 v2(GET_POS(i2)); -#undef GET_POS + const Float3 v0(positions.GetFloat3(i0)); + const Float3 v1(positions.GetFloat3(i1)); + const Float3 v2(positions.GetFloat3(i2)); const float area = Float3::TriangleArea(v0, v1, v2); invMasses.Get()[i0] += area; invMasses.Get()[i1] += area; @@ -696,12 +706,9 @@ void Cloth::CalculateInvMasses(Array& invMasses) const int32 i0 = indicesData.Get()[index]; const int32 i1 = indicesData.Get()[index + 1]; const int32 i2 = indicesData.Get()[index + 2]; - // TODO: use MeshAccessor vertex data layout descriptor instead hardcoded position data at the beginning of VB0 -#define GET_POS(i) *(Float3*)((byte*)verticesData.Get() + i * verticesStride) - const Float3 v0(GET_POS(i0)); - const Float3 v1(GET_POS(i1)); - const Float3 v2(GET_POS(i2)); -#undef GET_POS + const Float3 v0(positions.GetFloat3(i0)); + const Float3 v1(positions.GetFloat3(i1)); + const Float3 v2(positions.GetFloat3(i2)); const float area = Float3::TriangleArea(v0, v1, v2); invMasses.Get()[i0] += area; invMasses.Get()[i1] += area; @@ -787,25 +794,22 @@ bool Cloth::OnPreUpdate() { if (animatedModel->GraphInstance.NodesPose.IsEmpty() || _paint.IsEmpty()) return false; - const ModelInstanceActor::MeshReference mesh = GetMesh(); - if (mesh.Actor == nullptr) - return false; - BytesContainer verticesData; - int32 verticesCount; - GPUVertexLayout* layout; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Vertex0, verticesData, verticesCount, &layout)) + const ModelInstanceActor::MeshReference meshRef = GetMesh(); + if (meshRef.Actor == nullptr) return false; MeshAccessor accessor; - if (accessor.LoadBuffer(MeshBufferType::Vertex0, verticesData, layout)) + MeshBufferType bufferTypes[1] = { MeshBufferType::Vertex0 }; + if (accessor.LoadMesh(meshRef.Get(), false, ToSpan(bufferTypes, 1))) return false; auto positionStream = accessor.Position(); auto blendIndicesStream = accessor.BlendIndices(); auto blendWeightsStream = accessor.BlendWeights(); if (!positionStream.IsValid() || !blendIndicesStream.IsValid() || !blendWeightsStream.IsValid()) return false; + const int32 verticesCount = positionStream.GetCount(); if (verticesCount != _paint.Count()) { - LOG(Warning, "Incorrect cloth '{}' paint size {} for mesh '{}' that has {} vertices", GetNamePath(), _paint.Count(), mesh.ToString(), verticesCount); + LOG(Warning, "Incorrect cloth '{}' paint size {} for mesh '{}' that has {} vertices", GetNamePath(), _paint.Count(), meshRef.ToString(), verticesCount); return false; } PROFILE_CPU_NAMED("Skinned Pose"); @@ -925,7 +929,7 @@ void Cloth::RunClothDeformer(const MeshBase* mesh, MeshDeformationData& deformat return; #if WITH_CLOTH PROFILE_CPU_NAMED("Cloth"); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); PhysicsBackend::LockClothParticles(_cloth); const Span particles = PhysicsBackend::GetClothParticles(_cloth); auto vbCount = (uint32)mesh->GetVertexCount(); @@ -934,49 +938,53 @@ void Cloth::RunClothDeformer(const MeshBase* mesh, MeshDeformationData& deformat // Calculate normals Array normals; const ModelInstanceActor::MeshReference meshRef = GetMesh(); - BytesContainer indicesData; - int32 indicesCount; - if ((_simulationSettings.ComputeNormals || deformation.Type == MeshBufferType::Vertex1) && - meshRef.Actor && !meshRef.Actor->GetMeshData(meshRef, MeshBufferType::Index, indicesData, indicesCount)) + if ((_simulationSettings.ComputeNormals || deformation.Type == MeshBufferType::Vertex1) && meshRef.Actor) { - PROFILE_CPU_NAMED("Normals"); - // TODO: optimize memory allocs (eg. use shared allocator) - normals.Resize(vbCount); - Platform::MemoryClear(normals.Get(), vbCount * sizeof(Float3)); - const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); - const int32 trianglesCount = indicesCount / 3; - if (indices16bit) + MeshAccessor accessor; + MeshBufferType bufferTypes[1] = { MeshBufferType::Index }; + if (!accessor.LoadMesh(meshRef.Get(), false, ToSpan(bufferTypes, 1))) { - for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) + PROFILE_CPU_NAMED("Normals"); + auto indices = accessor.Index(); + auto indicesData = indices.GetData(); + // TODO: optimize memory allocs (eg. use shared allocator) + normals.Resize(vbCount); + Platform::MemoryClear(normals.Get(), vbCount * sizeof(Float3)); + const bool indices16bit = indices.GetFormat() == PixelFormat::R16_UInt; + const int32 trianglesCount = indices.GetCount() / 3; + if (indices16bit) { - const int32 index = triangleIndex * 3; - const int32 i0 = indicesData.Get()[index]; - const int32 i1 = indicesData.Get()[index + 1]; - const int32 i2 = indicesData.Get()[index + 2]; - const Float3 v0(particles.Get()[i0]); - const Float3 v1(particles.Get()[i1]); - const Float3 v2(particles.Get()[i2]); - const Float3 normal = Float3::Cross(v1 - v0, v2 - v0); - normals.Get()[i0] += normal; - normals.Get()[i1] += normal; - normals.Get()[i2] += normal; + for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) + { + const int32 index = triangleIndex * 3; + const int32 i0 = indicesData.Get()[index]; + const int32 i1 = indicesData.Get()[index + 1]; + const int32 i2 = indicesData.Get()[index + 2]; + const Float3 v0(particles.Get()[i0]); + const Float3 v1(particles.Get()[i1]); + const Float3 v2(particles.Get()[i2]); + const Float3 normal = Float3::Cross(v1 - v0, v2 - v0); + normals.Get()[i0] += normal; + normals.Get()[i1] += normal; + normals.Get()[i2] += normal; + } } - } - else - { - for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) + else { - const int32 index = triangleIndex * 3; - const int32 i0 = indicesData.Get()[index]; - const int32 i1 = indicesData.Get()[index + 1]; - const int32 i2 = indicesData.Get()[index + 2]; - const Float3 v0(particles.Get()[i0]); - const Float3 v1(particles.Get()[i1]); - const Float3 v2(particles.Get()[i2]); - const Float3 normal = Float3::Cross(v1 - v0, v2 - v0); - normals.Get()[i0] += normal; - normals.Get()[i1] += normal; - normals.Get()[i2] += normal; + for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) + { + const int32 index = triangleIndex * 3; + const int32 i0 = indicesData.Get()[index]; + const int32 i1 = indicesData.Get()[index + 1]; + const int32 i2 = indicesData.Get()[index + 2]; + const Float3 v0(particles.Get()[i0]); + const Float3 v1(particles.Get()[i1]); + const Float3 v2(particles.Get()[i2]); + const Float3 normal = Float3::Cross(v1 - v0, v2 - v0); + normals.Get()[i0] += normal; + normals.Get()[i1] += normal; + normals.Get()[i2] += normal; + } } } } diff --git a/Source/Engine/Physics/Actors/Cloth.h b/Source/Engine/Physics/Actors/Cloth.h index f83f1c499..8d6f13370 100644 --- a/Source/Engine/Physics/Actors/Cloth.h +++ b/Source/Engine/Physics/Actors/Cloth.h @@ -371,6 +371,6 @@ private: ImplementPhysicsDebug; bool CreateCloth(); void DestroyCloth(); - void CalculateInvMasses(Array& invMasses); + void CalculateInvMasses(class MeshAccessor& accessor, Array& invMasses); void RunClothDeformer(const MeshBase* mesh, struct MeshDeformationData& deformation); }; diff --git a/Source/Engine/Physics/Colliders/Collider.cpp b/Source/Engine/Physics/Colliders/Collider.cpp index 96bd9c7ae..28588e2a6 100644 --- a/Source/Engine/Physics/Colliders/Collider.cpp +++ b/Source/Engine/Physics/Colliders/Collider.cpp @@ -209,6 +209,13 @@ void Collider::CreateShape() // Create shape const bool isTrigger = _isTrigger && CanBeTrigger(); _shape = PhysicsBackend::CreateShape(this, shape, Material, IsActiveInHierarchy(), isTrigger); + if (!_shape) + { + LOG(Error, "Failed to create physics shape for actor '{}'", GetNamePath()); + if (shape.Type == CollisionShape::Types::ConvexMesh && Float3(shape.ConvexMesh.Scale).MinValue() <= 0) + LOG(Warning, "Convex Mesh colliders cannot have negative scale"); + return; + } PhysicsBackend::SetShapeContactOffset(_shape, _contactOffset); UpdateLayerBits(); } @@ -293,18 +300,20 @@ void Collider::BeginPlay(SceneBeginData* data) if (_shape == nullptr) { CreateShape(); - - // Check if parent is a rigidbody - const auto rigidBody = dynamic_cast(GetParent()); - if (rigidBody && CanAttach(rigidBody)) + if (_shape) { - // Attach to the rigidbody - Attach(rigidBody); - } - else - { - // Be a static collider - CreateStaticActor(); + // Check if parent is a rigidbody + const auto rigidBody = dynamic_cast(GetParent()); + if (rigidBody && CanAttach(rigidBody)) + { + // Attach to the rigidbody + Attach(rigidBody); + } + else + { + // Be a static collider + CreateStaticActor(); + } } } diff --git a/Source/Engine/Physics/Colliders/SphereCollider.cpp b/Source/Engine/Physics/Colliders/SphereCollider.cpp index 0576ae6cf..eda3884e4 100644 --- a/Source/Engine/Physics/Colliders/SphereCollider.cpp +++ b/Source/Engine/Physics/Colliders/SphereCollider.cpp @@ -26,7 +26,7 @@ void SphereCollider::SetRadius(const float value) void SphereCollider::DrawPhysicsDebug(RenderView& view) { - const BoundingSphere sphere(_sphere.Center - view.Origin, _sphere.Radius); + const BoundingSphere sphere(_sphere.Center - view.Origin, Math::Abs(_sphere.Radius)); if (!view.CullingFrustum.Intersects(sphere)) return; if (view.Mode == ViewMode::PhysicsColliders && !GetIsTrigger()) diff --git a/Source/Engine/Physics/CollisionData.cpp b/Source/Engine/Physics/CollisionData.cpp index c65ea8a6b..94cef111e 100644 --- a/Source/Engine/Physics/CollisionData.cpp +++ b/Source/Engine/Physics/CollisionData.cpp @@ -257,6 +257,8 @@ Asset::LoadResult CollisionData::load() CollisionData::LoadResult CollisionData::load(const SerializedOptions* options, byte* dataPtr, int32 dataSize) { + if (WaitForInitPhysics()) + return LoadResult::CannotLoadData; PROFILE_MEM(Physics); // Load options diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp index 8c3d3610e..5aba5343b 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -1177,8 +1177,11 @@ void ScenePhysX::UpdateVehicles(float dt) void ScenePhysX::PreSimulateCloth(int32 i) { PROFILE_CPU(); + PROFILE_MEM(PhysicsCloth); auto clothPhysX = ClothsList[i]; auto& clothSettings = Cloths[clothPhysX]; + if (!clothSettings.Actor) + return; if (clothSettings.Actor->OnPreUpdate()) { @@ -1379,6 +1382,7 @@ void ScenePhysX::UpdateCloths(float dt) if (!clothSolver || ClothsList.IsEmpty()) return; PROFILE_CPU_NAMED("Physics.Cloth"); + PROFILE_MEM(PhysicsCloth); { PROFILE_CPU_NAMED("Pre"); @@ -2450,7 +2454,7 @@ void PhysicsBackend::SetRigidActorPose(void* actor, const Vector3& position, con if (kinematic) { auto actorPhysX = (PxRigidDynamic*)actor; - if (actorPhysX->getActorFlags() & PxActorFlag::eDISABLE_SIMULATION) + if (actorPhysX->getActorFlags() & PxActorFlag::eDISABLE_SIMULATION || !(actorPhysX->getRigidBodyFlags() & PxRigidBodyFlag::eKINEMATIC)) { // Ensures the disabled kinematic actor ends up in the correct pose after enabling simulation actorPhysX->setGlobalPose(trans, wakeUp); @@ -2649,10 +2653,13 @@ void* PhysicsBackend::CreateShape(PhysicsColliderActor* collider, const Collisio PxGeometryHolder geometryPhysX; GetShapeGeometry(geometry, geometryPhysX); PxShape* shapePhysX = PhysX->createShape(geometryPhysX.any(), materialsPhysX.Get(), materialsPhysX.Count(), true, shapeFlags); - shapePhysX->userData = collider; + if (shapePhysX) + { + shapePhysX->userData = collider; #if PHYSX_DEBUG_NAMING - shapePhysX->setName("Shape"); + shapePhysX->setName("Shape"); #endif + } return shapePhysX; } @@ -3994,7 +4001,7 @@ void PhysicsBackend::RemoveVehicle(void* scene, WheeledVehicle* actor) void* PhysicsBackend::CreateCloth(const PhysicsClothDesc& desc) { PROFILE_CPU(); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); #if USE_CLOTH_SANITY_CHECKS { // Sanity check diff --git a/Source/Engine/Platform/Apple/AppleFileSystem.cpp b/Source/Engine/Platform/Apple/AppleFileSystem.cpp index 450507009..a38269d69 100644 --- a/Source/Engine/Platform/Apple/AppleFileSystem.cpp +++ b/Source/Engine/Platform/Apple/AppleFileSystem.cpp @@ -83,17 +83,19 @@ void AppleFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& res switch (type) { case SpecialFolder::Desktop: - result = home / TEXT("/Desktop"); + result = home / TEXT("/Desktop"); // TODO: should be NSDesktopDirectory break; case SpecialFolder::Documents: - result = home / TEXT("/Documents"); + result = home / TEXT("/Documents"); // TODO: should be NSDocumentDirectory break; case SpecialFolder::Pictures: - result = home / TEXT("/Pictures"); + result = home / TEXT("/Pictures"); // TODO: should be NSPicturesDirectory break; case SpecialFolder::AppData: + result = home / TEXT("/Library/Application Support"); // TODO: should be NSApplicationSupportDirectory + break; case SpecialFolder::LocalAppData: - result = home / TEXT("/Library/Caches"); + result = home / TEXT("/Library/Caches"); // TODO: should be NSApplicationSupportDirectory break; case SpecialFolder::ProgramData: result = home / TEXT("/Library/Application Support"); diff --git a/Source/Engine/Platform/Linux/LinuxFileSystem.cpp b/Source/Engine/Platform/Linux/LinuxFileSystem.cpp index 9b31dc28f..4293985fa 100644 --- a/Source/Engine/Platform/Linux/LinuxFileSystem.cpp +++ b/Source/Engine/Platform/Linux/LinuxFileSystem.cpp @@ -338,17 +338,35 @@ void LinuxFileSystem::GetSpecialFolderPath(const SpecialFolder type, String& res switch (type) { case SpecialFolder::Desktop: - result = home / TEXT("Desktop"); + { + String desktopDir; + if (!Platform::GetEnvironmentVariable(TEXT("XDG_DESKTOP_DIR"), desktopDir)) + result = desktopDir; + else + result = home / TEXT("Desktop"); break; + } case SpecialFolder::Documents: result = String::Empty; break; case SpecialFolder::Pictures: - result = home / TEXT("Pictures"); + { + String picturesDir; + if (!Platform::GetEnvironmentVariable(TEXT("XDG_PICTURES_DIR"), picturesDir)) + result = picturesDir; + else + result = home / TEXT("Pictures"); break; + } case SpecialFolder::AppData: - result = TEXT("/usr/share"); + { + String configHome; + if (!Platform::GetEnvironmentVariable(TEXT("XDG_CONFIG_HOME"), configHome)) + result = configHome; + else + result = home / TEXT(".config"); break; + } case SpecialFolder::LocalAppData: { String dataHome; diff --git a/Source/Engine/Profiler/ProfilerMemory.cpp b/Source/Engine/Profiler/ProfilerMemory.cpp index 6b8f18ce3..eaa6cbfcd 100644 --- a/Source/Engine/Profiler/ProfilerMemory.cpp +++ b/Source/Engine/Profiler/ProfilerMemory.cpp @@ -263,6 +263,7 @@ void InitProfilerMemory(const Char* cmdLine, int32 stage) INIT_PARENT(Level, LevelTerrain); INIT_PARENT(Navigation, NavigationMesh); INIT_PARENT(Navigation, NavigationBuilding); + INIT_PARENT(Physics, PhysicsCloth); INIT_PARENT(Scripting, ScriptingVisual); INIT_PARENT(Scripting, ScriptingCSharp); INIT_PARENT(ScriptingCSharp, ScriptingCSharpGCCommitted); diff --git a/Source/Engine/Profiler/ProfilerMemory.h b/Source/Engine/Profiler/ProfilerMemory.h index 9177ae6e7..ab33a42f3 100644 --- a/Source/Engine/Profiler/ProfilerMemory.h +++ b/Source/Engine/Profiler/ProfilerMemory.h @@ -120,6 +120,8 @@ public: // Total physics memory. Physics, + // Cloth simulation and particles data. + PhysicsCloth, // Total scripting memory allocated by game. Scripting, diff --git a/Source/Engine/Render2D/FontAsset.cs b/Source/Engine/Render2D/FontAsset.cs index 72520db5d..624daab5b 100644 --- a/Source/Engine/Render2D/FontAsset.cs +++ b/Source/Engine/Render2D/FontAsset.cs @@ -1,5 +1,7 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System; + namespace FlaxEngine { partial struct FontOptions @@ -11,7 +13,7 @@ namespace FlaxEngine /// true if this object has the same value as ; otherwise, false public bool Equals(FontOptions other) { - return Hinting == other.Hinting && Flags == other.Flags; + return Hinting == other.Hinting && Flags == other.Flags && RasterMode == other.RasterMode; } /// @@ -23,10 +25,7 @@ namespace FlaxEngine /// public override int GetHashCode() { - unchecked - { - return ((int)Hinting * 397) ^ (int)Flags; - } + return HashCode.Combine((int)Hinting, (int)Flags, (int)RasterMode); } /// @@ -37,7 +36,7 @@ namespace FlaxEngine /// true if has the same value as ; otherwise, false. public static bool operator ==(FontOptions left, FontOptions right) { - return left.Hinting == right.Hinting && left.Flags == right.Flags; + return left.Hinting == right.Hinting && left.Flags == right.Flags && left.RasterMode == right.RasterMode; } /// @@ -48,7 +47,7 @@ namespace FlaxEngine /// true if has a different value than ; otherwise,false. public static bool operator !=(FontOptions left, FontOptions right) { - return left.Hinting != right.Hinting || left.Flags != right.Flags; + return left.Hinting != right.Hinting || left.Flags != right.Flags || left.RasterMode != right.RasterMode; } } } diff --git a/Source/Engine/Render2D/FontAsset.h b/Source/Engine/Render2D/FontAsset.h index b1ea87e0a..56cc3d655 100644 --- a/Source/Engine/Render2D/FontAsset.h +++ b/Source/Engine/Render2D/FontAsset.h @@ -66,6 +66,22 @@ API_ENUM(Attributes="Flags") enum class FontFlags : byte Italic = 4, }; +/// +/// The font rasterization mode. +/// +API_ENUM() enum class FontRasterMode : byte +{ + /// + /// Use the default FreeType rasterizer to render font atlases. + /// + Bitmap, + + /// + /// Use the Multi-channel Signed Distance Field (MSDF) generator to render font atlases. Need to be rendered with a compatible material. + /// + MSDF, +}; + DECLARE_ENUM_OPERATORS(FontFlags); /// @@ -76,7 +92,7 @@ API_STRUCT() struct FontOptions DECLARE_SCRIPTING_TYPE_MINIMAL(FontOptions); /// - /// The hinting. + /// The font hinting used when rendering characters. /// API_FIELD() FontHinting Hinting; @@ -84,6 +100,11 @@ API_STRUCT() struct FontOptions /// The flags. /// API_FIELD() FontFlags Flags; + + /// + /// The font rasterization mode. + /// + API_FIELD() FontRasterMode RasterMode; }; /// @@ -91,7 +112,7 @@ API_STRUCT() struct FontOptions /// API_CLASS(NoSpawn) class FLAXENGINE_API FontAsset : public BinaryAsset { - DECLARE_BINARY_ASSET_HEADER(FontAsset, 3); + DECLARE_BINARY_ASSET_HEADER(FontAsset, 4); friend Font; private: diff --git a/Source/Engine/Render2D/FontManager.cpp b/Source/Engine/Render2D/FontManager.cpp index 0375814ae..d58394bb3 100644 --- a/Source/Engine/Render2D/FontManager.cpp +++ b/Source/Engine/Render2D/FontManager.cpp @@ -9,6 +9,7 @@ #include "Engine/Content/Content.h" #include "Engine/Engine/EngineService.h" #include "Engine/Threading/Threading.h" +#include "Engine/Render2D/MSDFGenerator.h" #include "IncludeFreeType.h" #include #include @@ -125,7 +126,11 @@ bool FontManager::AddNewEntry(Font* font, Char c, FontCharacterEntry& entry) // Set load flags uint32 glyphFlags = FT_LOAD_NO_BITMAP; const bool useAA = EnumHasAnyFlags(options.Flags, FontFlags::AntiAliasing); - if (useAA) + if (options.RasterMode == FontRasterMode::MSDF) + { + glyphFlags |= FT_LOAD_NO_AUTOHINT | FT_LOAD_NO_HINTING; + } + else if (useAA) { switch (options.Hinting) { @@ -185,75 +190,109 @@ bool FontManager::AddNewEntry(Font* font, Char c, FontCharacterEntry& entry) FT_GlyphSlot_Oblique(face->glyph); } - // Render glyph to the bitmap - FT_GlyphSlot glyph = face->glyph; - FT_Render_Glyph(glyph, useAA ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO); - - FT_Bitmap* bitmap = &glyph->bitmap; - FT_Bitmap tmpBitmap; - if (bitmap->pixel_mode != FT_PIXEL_MODE_GRAY) + int32 glyphWidth = 0; + int32 glyphHeight = 0; + if (options.RasterMode == FontRasterMode::Bitmap) { - // Convert the bitmap to 8bpp grayscale - FT_Bitmap_New(&tmpBitmap); - FT_Bitmap_Convert(Library, bitmap, &tmpBitmap, 4); - bitmap = &tmpBitmap; - } - ASSERT(bitmap && bitmap->pixel_mode == FT_PIXEL_MODE_GRAY); + // Render glyph to the bitmap + FT_GlyphSlot glyph = face->glyph; + FT_Render_Glyph(glyph, useAA ? FT_RENDER_MODE_NORMAL : FT_RENDER_MODE_MONO); - // Fill the character data - entry.AdvanceX = Convert26Dot6ToRoundedPixel(glyph->advance.x); - entry.OffsetY = glyph->bitmap_top; - entry.OffsetX = glyph->bitmap_left; - entry.IsValid = true; - entry.BearingY = Convert26Dot6ToRoundedPixel(glyph->metrics.horiBearingY); - entry.Height = Convert26Dot6ToRoundedPixel(glyph->metrics.height); + FT_Bitmap* bitmap = &glyph->bitmap; + FT_Bitmap tmpBitmap; + if (bitmap->pixel_mode != FT_PIXEL_MODE_GRAY) + { + // Convert the bitmap to 8bpp grayscale + FT_Bitmap_New(&tmpBitmap); + FT_Bitmap_Convert(Library, bitmap, &tmpBitmap, 4); + bitmap = &tmpBitmap; + } + ASSERT(bitmap && bitmap->pixel_mode == FT_PIXEL_MODE_GRAY); - // Allocate memory - const int32 glyphWidth = bitmap->width; - const int32 glyphHeight = bitmap->rows; - GlyphImageData.Clear(); - GlyphImageData.Resize(glyphWidth * glyphHeight); + // Fill the character data + entry.AdvanceX = Convert26Dot6ToRoundedPixel(glyph->advance.x); + entry.OffsetY = glyph->bitmap_top; + entry.OffsetX = glyph->bitmap_left; + entry.IsValid = true; + entry.BearingY = Convert26Dot6ToRoundedPixel(glyph->metrics.horiBearingY); + entry.Height = Convert26Dot6ToRoundedPixel(glyph->metrics.height); - // End for empty glyphs - if (GlyphImageData.IsEmpty()) - { - entry.TextureIndex = MAX_uint8; + // Allocate memory + glyphWidth = bitmap->width; + glyphHeight = bitmap->rows; + GlyphImageData.Clear(); + GlyphImageData.Resize(glyphWidth * glyphHeight); + + // End for empty glyphs + if (GlyphImageData.IsEmpty()) + { + entry.TextureIndex = MAX_uint8; + if (bitmap == &tmpBitmap) + { + FT_Bitmap_Done(Library, bitmap); + bitmap = nullptr; + } + return false; + } + + // Copy glyph data after rasterization (row by row) + for (int32 row = 0; row < glyphHeight; row++) + { + Platform::MemoryCopy(&GlyphImageData[row * glyphWidth], &bitmap->buffer[row * bitmap->pitch], glyphWidth); + } + + // Normalize gray scale images not using 256 colors + if (bitmap->num_grays != 256) + { + const int32 scale = 255 / (bitmap->num_grays - 1); + for (byte& pixel : GlyphImageData) + pixel *= scale; + } + + // Free temporary bitmap if used if (bitmap == &tmpBitmap) { FT_Bitmap_Done(Library, bitmap); bitmap = nullptr; } - return false; } - - // Copy glyph data after rasterization (row by row) - for (int32 row = 0; row < glyphHeight; row++) + else { - Platform::MemoryCopy(&GlyphImageData[row * glyphWidth], &bitmap->buffer[row * bitmap->pitch], glyphWidth); - } + // Generate bitmap for MSDF + FT_GlyphSlot glyph = face->glyph; - // Normalize gray scale images not using 256 colors - if (bitmap->num_grays != 256) - { - const int32 scale = 255 / (bitmap->num_grays - 1); - for (byte& pixel : GlyphImageData) + // Set advance in advance + entry.AdvanceX = Convert26Dot6ToRoundedPixel(glyph->advance.x); + + int16 msdf_top = 0; + int16 msdf_left = 0; + MSDFGenerator::GenerateMSDF(glyph, GlyphImageData, glyphWidth, glyphHeight, msdf_top, msdf_left); + + // End for empty glyphs + if (GlyphImageData.IsEmpty()) { - pixel *= scale; + entry.TextureIndex = MAX_uint8; + return false; } - } - // Free temporary bitmap if used - if (bitmap == &tmpBitmap) - { - FT_Bitmap_Done(Library, bitmap); - bitmap = nullptr; + // Fill the remaining character data + entry.OffsetY = msdf_top; + entry.OffsetX = msdf_left; + entry.IsValid = true; + entry.BearingY = Convert26Dot6ToRoundedPixel(glyph->metrics.horiBearingY); + entry.Height = Convert26Dot6ToRoundedPixel(glyph->metrics.height); } // Find atlas for the character texture + PixelFormat atlasFormat = options.RasterMode == FontRasterMode::MSDF ? PixelFormat::R8G8B8A8_UNorm : PixelFormat::R8_UNorm; int32 atlasIndex = 0; const FontTextureAtlasSlot* slot = nullptr; for (; atlasIndex < Atlases.Count() && slot == nullptr; atlasIndex++) + { + if (Atlases[atlasIndex]->GetFormat() != atlasFormat) + continue; slot = Atlases[atlasIndex]->AddEntry(glyphWidth, glyphHeight, GlyphImageData); + } atlasIndex--; // Check if there is no atlas for this character @@ -261,7 +300,7 @@ bool FontManager::AddNewEntry(Font* font, Char c, FontCharacterEntry& entry) { // Create new atlas auto atlas = Content::CreateVirtualAsset(); - atlas->Setup(PixelFormat::R8_UNorm, FontTextureAtlas::PaddingStyle::PadWithZero); + atlas->Setup(atlasFormat, FontTextureAtlas::PaddingStyle::PadWithZero); Atlases.Add(atlas); atlasIndex++; diff --git a/Source/Engine/Render2D/FontTextureAtlas.h b/Source/Engine/Render2D/FontTextureAtlas.h index fe92ed67c..f688644ab 100644 --- a/Source/Engine/Render2D/FontTextureAtlas.h +++ b/Source/Engine/Render2D/FontTextureAtlas.h @@ -105,6 +105,14 @@ public: return _height; } + /// + /// Gets the atlas pixel format. + /// + FORCE_INLINE PixelFormat GetFormat() const + { + return _format; + } + /// /// Gets the atlas size. /// @@ -186,8 +194,8 @@ public: /// /// Returns glyph's bitmap data of the slot. /// - /// The slot in atlas. - /// The width of the slot. + /// The slot in atlas. + /// The width of the slot. /// The height of the slot. /// The stride of the slot. /// The pointer to the bitmap data of the given slot. diff --git a/Source/Engine/Render2D/MSDFGenerator.h b/Source/Engine/Render2D/MSDFGenerator.h new file mode 100644 index 000000000..e1add4dfd --- /dev/null +++ b/Source/Engine/Render2D/MSDFGenerator.h @@ -0,0 +1,142 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Core/Collections/Array.h" +#include "Engine/Core/Math/Math.h" +#include +#include +#include + +class MSDFGenerator +{ + using Point2 = msdfgen::Point2; + using Shape = msdfgen::Shape; + using Contour = msdfgen::Contour; + using EdgeHolder = msdfgen::EdgeHolder; + + static Point2 ftPoint2(const FT_Vector& vector, double scale) + { + return Point2(scale * vector.x, scale * vector.y); + } + + struct FtContext + { + double scale; + Point2 position; + Shape* shape; + Contour* contour; + }; + + static int ftMoveTo(const FT_Vector* to, void* user) + { + FtContext* context = reinterpret_cast(user); + if (!(context->contour && context->contour->edges.empty())) + context->contour = &context->shape->addContour(); + context->position = ftPoint2(*to, context->scale); + return 0; + } + + static int ftLineTo(const FT_Vector* to, void* user) + { + FtContext* context = reinterpret_cast(user); + Point2 endpoint = ftPoint2(*to, context->scale); + if (endpoint != context->position) + { + context->contour->addEdge(EdgeHolder(context->position, endpoint)); + context->position = endpoint; + } + return 0; + } + + static int ftConicTo(const FT_Vector* control, const FT_Vector* to, void* user) + { + FtContext* context = reinterpret_cast(user); + Point2 endpoint = ftPoint2(*to, context->scale); + if (endpoint != context->position) + { + context->contour->addEdge(EdgeHolder(context->position, ftPoint2(*control, context->scale), endpoint)); + context->position = endpoint; + } + return 0; + } + + static int ftCubicTo(const FT_Vector* control1, const FT_Vector* control2, const FT_Vector* to, void* user) + { + FtContext* context = reinterpret_cast(user); + Point2 endpoint = ftPoint2(*to, context->scale); + if (endpoint != context->position || msdfgen::crossProduct(ftPoint2(*control1, context->scale) - endpoint, ftPoint2(*control2, context->scale) - endpoint)) + { + context->contour->addEdge(EdgeHolder(context->position, ftPoint2(*control1, context->scale), ftPoint2(*control2, context->scale), endpoint)); + context->position = endpoint; + } + return 0; + } + + static void correctWinding(Shape& shape, Shape::Bounds& bounds) + { + Point2 p(bounds.l - (bounds.r - bounds.l) - 1, bounds.b - (bounds.t - bounds.b) - 1); + double distance = msdfgen::SimpleTrueShapeDistanceFinder::oneShotDistance(shape, p); + if (distance > 0) + { + for (auto& contour : shape.contours) + contour.reverse(); + } + } + +public: + static void GenerateMSDF(FT_GlyphSlot glyph, Array& output, int32& outputWidth, int32& outputHeight, int16& top, int16& left) + { + Shape shape; + shape.contours.clear(); + shape.inverseYAxis = true; + + FtContext context = { }; + context.scale = 1.0 / 64.0; + context.shape = &shape; + FT_Outline_Funcs ftFunctions; + ftFunctions.move_to = &ftMoveTo; + ftFunctions.line_to = &ftLineTo; + ftFunctions.conic_to = &ftConicTo; + ftFunctions.cubic_to = &ftCubicTo; + ftFunctions.shift = 0; + ftFunctions.delta = 0; + FT_Outline_Decompose(&glyph->outline, &ftFunctions, &context); + + shape.normalize(); + edgeColoringSimple(shape, 3.0); + + // Todo: make this configurable + // Also hard-coded in material: MSDFFontMaterial + const double pxRange = 4.0; + + Shape::Bounds bounds = shape.getBounds(); + int32 width = static_cast(Math::CeilToInt(bounds.r - bounds.l + pxRange)); + int32 height = static_cast(Math::CeilToInt(bounds.t - bounds.b + pxRange)); + msdfgen::Bitmap msdf(width, height); + + auto transform = msdfgen::Vector2(Math::Ceil(-bounds.l + pxRange / 2.0), Math::Ceil(-bounds.b + pxRange / 2.0)); + + msdfgen::SDFTransformation t(msdfgen::Projection(1.0, transform), msdfgen::Range(pxRange)); + correctWinding(shape, bounds); + generateMTSDF(msdf, shape, t); + + output.Resize(width * height * 4); + + const msdfgen::BitmapConstRef& bitmap = msdf; + for (int y = 0; y < height; ++y) + { + for (int x = 0; x < width; ++x) + { + output[(y * width + x) * 4] = msdfgen::pixelFloatToByte(bitmap(x, y)[0]); + output[(y * width + x) * 4 + 1] = msdfgen::pixelFloatToByte(bitmap(x, y)[1]); + output[(y * width + x) * 4 + 2] = msdfgen::pixelFloatToByte(bitmap(x, y)[2]); + output[(y * width + x) * 4 + 3] = msdfgen::pixelFloatToByte(bitmap(x, y)[3]); + } + } + outputWidth = width; + outputHeight = height; + top = height - static_cast(transform.y); + left = -static_cast(transform.x); + } +}; diff --git a/Source/Engine/Render2D/Render2D.Build.cs b/Source/Engine/Render2D/Render2D.Build.cs index f0be43983..a8f09270c 100644 --- a/Source/Engine/Render2D/Render2D.Build.cs +++ b/Source/Engine/Render2D/Render2D.Build.cs @@ -14,6 +14,7 @@ public class Render2D : EngineModule base.Setup(options); options.PrivateDependencies.Add("freetype"); + options.PrivateDependencies.Add("msdfgen"); options.PrivateDefinitions.Add("RENDER2D_USE_LINE_AA"); } diff --git a/Source/Engine/Render2D/Render2D.cpp b/Source/Engine/Render2D/Render2D.cpp index 5ae034bc5..d9e052e6e 100644 --- a/Source/Engine/Render2D/Render2D.cpp +++ b/Source/Engine/Render2D/Render2D.cpp @@ -2,6 +2,7 @@ #include "Render2D.h" #include "Font.h" +#include "FontAsset.h" #include "FontManager.h" #include "FontTextureAtlas.h" #include "RotatedRectangle.h" @@ -74,6 +75,7 @@ enum class DrawCallType : byte FillTexture, FillTexturePoint, DrawChar, + DrawCharMSDF, DrawCharMaterial, Custom, Material, @@ -164,6 +166,7 @@ struct CachedPSO GPUPipelineState* PS_Color_NoAlpha; GPUPipelineState* PS_Font; + GPUPipelineState* PS_FontMSDF; GPUPipelineState* PS_BlurH; GPUPipelineState* PS_BlurV; @@ -455,6 +458,7 @@ CanDrawCallCallback CanDrawCallBatch[] = CanDrawCallCallbackTexture, // FillTexture, CanDrawCallCallbackTexture, // FillTexturePoint, CanDrawCallCallbackChar, // DrawChar, + CanDrawCallCallbackChar, // DrawCharMSDF, CanDrawCallCallbackCharMaterial, // DrawCharMaterial, CanDrawCallCallbackFalse, // Custom, CanDrawCallCallbackMaterial, // Material, @@ -517,6 +521,12 @@ bool CachedPSO::Init(GPUShader* shader, bool useDepth) if (PS_Font->Init(desc)) return true; // + desc.BlendMode = BlendingMode::AlphaBlend; + desc.PS = shader->GetPS("PS_FontMSDF"); + PS_FontMSDF = GPUDevice::Instance->CreatePipelineState(); + if (PS_FontMSDF->Init(desc)) + return true; + // desc.PS = shader->GetPS("PS_LineAA"); PS_LineAA = GPUDevice::Instance->CreatePipelineState(); if (PS_LineAA->Init(desc)) @@ -994,6 +1004,10 @@ void DrawBatch(int32 startIndex, int32 count) Context->BindSR(0, d.AsChar.Tex); Context->SetState(CurrentPso->PS_Font); break; + case DrawCallType::DrawCharMSDF: + Context->BindSR(0, d.AsChar.Tex); + Context->SetState(CurrentPso->PS_FontMSDF); + break; case DrawCallType::DrawCharMaterial: { // Apply and bind material @@ -1186,7 +1200,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, } else { - drawCall.Type = DrawCallType::DrawChar; + drawCall.Type = font->GetAsset()->GetOptions().RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar; drawCall.AsChar.Mat = nullptr; } Float2 pointer = location; @@ -1305,7 +1319,7 @@ void Render2D::DrawText(Font* font, const StringView& text, const Color& color, } else { - drawCall.Type = DrawCallType::DrawChar; + drawCall.Type = font->GetAsset()->GetOptions().RasterMode == FontRasterMode::MSDF ? DrawCallType::DrawCharMSDF : DrawCallType::DrawChar; drawCall.AsChar.Mat = nullptr; } for (int32 lineIndex = 0; lineIndex < Lines.Count(); lineIndex++) @@ -1440,8 +1454,28 @@ void Render2D::DrawRectangle(const Rectangle& rect, const Color& color1, const C RENDER2D_CHECK_RENDERING_STATE; const auto& mask = ClipLayersStack.Peek().Mask; + float thick = thickness; thickness *= (TransformCached.M11 + TransformCached.M22 + TransformCached.M33) * 0.3333333f; + // When lines thickness is very large, don't use corner caps and place line ends to not overlap + if (thickness > 4.0f) + { + thick *= Math::Lerp(0.6f, 1.0f, Math::Saturate(thick - 4.0f)); // Smooth transition between soft LineAA and harsh FillRect + Float2 totalMin = rect.GetUpperLeft() - Float2(thick * 0.5f); + Float2 totalMax = rect.GetBottomRight() + Float2(thick * 0.5f); + Float2 size = totalMax - totalMin; + Render2DDrawCall& drawCall = DrawCalls.AddOne(); + drawCall.Type = NeedAlphaWithTint(color1, color2, color3, color4) ? DrawCallType::FillRect : DrawCallType::FillRectNoAlpha; + drawCall.StartIB = IBIndex; + drawCall.CountIB = 6 * 4; + // TODO: interpolate colors from corners to extended rectangle edges properly + WriteRect(Rectangle(totalMin.X, totalMin.Y, size.X, thick), color1, color2, color2, color1); + WriteRect(Rectangle(totalMin.X, totalMin.Y + rect.Size.Y, size.X, thick), color4, color3, color3, color4); + WriteRect(Rectangle(totalMin.X, totalMin.Y + thick, thick, rect.Size.Y - thick), color1, color1, color4, color4); + WriteRect(Rectangle(totalMax.X - thick, totalMin.Y + thick, thick, rect.Size.Y - thick), color2, color2, color3, color3); + return; + } + Float2 points[5]; ApplyTransform(rect.GetUpperLeft(), points[0]); ApplyTransform(rect.GetUpperRight(), points[1]); diff --git a/Source/Engine/Renderer/AtmospherePreCompute.h b/Source/Engine/Renderer/AtmospherePreCompute.h index e1fd3fa93..bf72f276a 100644 --- a/Source/Engine/Renderer/AtmospherePreCompute.h +++ b/Source/Engine/Renderer/AtmospherePreCompute.h @@ -2,8 +2,6 @@ #pragma once -#include "FlaxEngine.Gen.h" - class GPUTexture; /// diff --git a/Source/Engine/Renderer/GBufferPass.cpp b/Source/Engine/Renderer/GBufferPass.cpp index 1881aac3b..e62970f83 100644 --- a/Source/Engine/Renderer/GBufferPass.cpp +++ b/Source/Engine/Renderer/GBufferPass.cpp @@ -158,6 +158,7 @@ void GBufferPass::Fill(RenderContext& renderContext, GPUTexture* lightBuffer) renderContext.Buffers->GBuffer3->View(), }; renderContext.View.Pass = DrawPass::GBuffer; + context->SetViewportAndScissors(renderContext.Buffers->GetViewport()); // Clear GBuffer { diff --git a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp index 25550ecd8..37d268991 100644 --- a/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp +++ b/Source/Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.cpp @@ -90,6 +90,7 @@ public: Float3 ProbesOrigin; float ProbesSpacing = 0.0f; Int3 ProbeScrollOffsets; + bool PendingUpdate = true; Int3 ProbeScrollClears; void Clear() @@ -97,6 +98,7 @@ public: ProbesOrigin = Float3::Zero; ProbeScrollOffsets = Int3::Zero; ProbeScrollClears = Int3::Zero; + PendingUpdate = true; } } Cascades[4]; @@ -457,9 +459,12 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont //const uint64 cascadeFrequencies[] = { 1, 1, 1, 1 }; //const uint64 cascadeFrequencies[] = { 10, 10, 10, 10 }; bool cascadeSkipUpdate[4]; + int32 maxCascadesPerFrame = renderContext.View.IsSingleFrame ? cascadesCount : 2; for (int32 cascadeIndex = 0; cascadeIndex < cascadesCount; cascadeIndex++) { - cascadeSkipUpdate[cascadeIndex] = !clear && (ddgiData.LastFrameUsed % cascadeFrequencies[cascadeIndex]) != 0 && GPU_SPREAD_WORKLOAD; + auto& cascade = ddgiData.Cascades[cascadeIndex]; + cascade.PendingUpdate |= !clear && (ddgiData.LastFrameUsed % cascadeFrequencies[cascadeIndex]) != 0 && GPU_SPREAD_WORKLOAD; + cascadeSkipUpdate[cascadeIndex] = !cascade.PendingUpdate || maxCascadesPerFrame-- <= 0; } // Compute scrolling (probes are placed around camera but are scrolling to increase stability during movement) @@ -468,6 +473,7 @@ bool DynamicDiffuseGlobalIlluminationPass::RenderInner(RenderContext& renderCont if (cascadeSkipUpdate[cascadeIndex]) continue; auto& cascade = ddgiData.Cascades[cascadeIndex]; + cascade.PendingUpdate = false; // Calculate the count of grid cells between the view origin and the scroll anchor const Float3 volumeOrigin = cascade.ProbesOrigin + Float3(cascade.ProbeScrollOffsets) * cascade.ProbesSpacing; diff --git a/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp b/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp index 010e14bf9..ae6e8a9a8 100644 --- a/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp +++ b/Source/Engine/Renderer/GlobalSignDistanceFieldPass.cpp @@ -408,7 +408,8 @@ public: if (GLOBAL_SDF_ACTOR_IS_STATIC(a) && ObjectTypes.Contains(a->GetTypeHandle())) { ScopeWriteLock lock(Locker); - OnSceneRenderingDirty(BoundingBox::FromSphere(prevBounds)); + if (flags != DrawModes && flags != Layer && flags != StaticFlags) + OnSceneRenderingDirty(BoundingBox::FromSphere(prevBounds)); OnSceneRenderingDirty(a->GetBox()); } } diff --git a/Source/Engine/Renderer/RenderList.cpp b/Source/Engine/Renderer/RenderList.cpp index ac643b4e8..2bb206d6e 100644 --- a/Source/Engine/Renderer/RenderList.cpp +++ b/Source/Engine/Renderer/RenderList.cpp @@ -695,6 +695,7 @@ void RenderList::AddDrawCall(const RenderContextBatch& renderContextBatch, DrawP DrawCallsLists[(int32)DrawCallsListType::MotionVectors].Indices.Add(index); } } + float minObjectPixelSizeSq = Math::Square(Graphics::Shadows::MinObjectPixelSize); for (int32 i = 1; i < renderContextBatch.Contexts.Count(); i++) { const RenderContext& renderContext = renderContextBatch.Contexts.Get()[i]; @@ -702,7 +703,8 @@ void RenderList::AddDrawCall(const RenderContextBatch& renderContextBatch, DrawP drawModes = modes & renderContext.View.Pass; if (drawModes != DrawPass::None && (staticFlags & renderContext.View.StaticFlagsMask) == renderContext.View.StaticFlagsCompare && - renderContext.View.CullingFrustum.Intersects(bounds)) + renderContext.View.CullingFrustum.Intersects(bounds) && + RenderTools::ComputeBoundsScreenRadiusSquared(bounds.Center, bounds.Radius, renderContext.View) * (renderContext.View.ScreenSize.X * renderContext.View.ScreenSize.Y) >= minObjectPixelSizeSq) { renderContext.List->ShadowDepthDrawCallsList.Indices.Add(index); } diff --git a/Source/Engine/Scripting/Attributes/Editor/RequireActorAttribute.cs b/Source/Engine/Scripting/Attributes/Editor/RequireActorAttribute.cs index 6e08217bf..3e5ad7f73 100644 --- a/Source/Engine/Scripting/Attributes/Editor/RequireActorAttribute.cs +++ b/Source/Engine/Scripting/Attributes/Editor/RequireActorAttribute.cs @@ -13,13 +13,20 @@ public class RequireActorAttribute : Attribute /// The required type. /// public Type RequiredType; + + /// + /// Whether to include inherited types. + /// + public bool IncludeInheritedTypes; /// /// Initializes a new instance of the class. /// /// The required type. - public RequireActorAttribute(Type type) + /// Whether to include inherited types. + public RequireActorAttribute(Type type, bool includeInheritedTypes = false) { RequiredType = type; + IncludeInheritedTypes = includeInheritedTypes; } } diff --git a/Source/Engine/Serialization/ISerializeModifier.h b/Source/Engine/Serialization/ISerializeModifier.h index 8815ac2ea..0c49e24f1 100644 --- a/Source/Engine/Serialization/ISerializeModifier.h +++ b/Source/Engine/Serialization/ISerializeModifier.h @@ -4,7 +4,6 @@ #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Core/Types/Guid.h" -#include "FlaxEngine.Gen.h" /// /// Object serialization modification base class. Allows to extend the serialization process by custom effects like object ids mapping. @@ -12,17 +11,18 @@ class FLAXENGINE_API ISerializeModifier { public: - /// /// Number of engine build when data was serialized. Useful to upgrade data from the older storage format. /// - uint32 EngineBuild = FLAXENGINE_VERSION_BUILD; + uint32 EngineBuild; // Utility for scene deserialization to track currently mapped in Prefab Instance object IDs into IdsMapping. - int32 CurrentInstance = -1; + int32 CurrentInstance; /// /// The object IDs mapping. Key is a serialized object id, value is mapped value to use. /// Dictionary IdsMapping; + + ISerializeModifier(); }; diff --git a/Source/Engine/Serialization/Serialization.cpp b/Source/Engine/Serialization/Serialization.cpp index 1eb6b0181..ec02fd65f 100644 --- a/Source/Engine/Serialization/Serialization.cpp +++ b/Source/Engine/Serialization/Serialization.cpp @@ -25,6 +25,13 @@ #include "Engine/Content/Asset.h" #include "Engine/Level/SceneObject.h" #include "Engine/Utilities/Encryption.h" +#include "FlaxEngine.Gen.h" + +ISerializeModifier::ISerializeModifier() +{ + EngineBuild = FLAXENGINE_VERSION_BUILD; + CurrentInstance = -1; +} void ISerializable::DeserializeIfExists(DeserializeStream& stream, const char* memberName, ISerializeModifier* modifier) { diff --git a/Source/Engine/Serialization/Stream.cpp b/Source/Engine/Serialization/Stream.cpp index 4c9b94042..bbf57ea30 100644 --- a/Source/Engine/Serialization/Stream.cpp +++ b/Source/Engine/Serialization/Stream.cpp @@ -5,6 +5,7 @@ #include "JsonWriters.h" #include "JsonSerializer.h" #include "MemoryReadStream.h" +#include "FlaxEngine.Gen.h" #include "Engine/Core/Types/CommonValue.h" #include "Engine/Core/Types/Variant.h" #include "Engine/Core/Collections/Dictionary.h" diff --git a/Source/Engine/ShadowsOfMordor/Builder.cpp b/Source/Engine/ShadowsOfMordor/Builder.cpp index 6f85fb950..048661617 100644 --- a/Source/Engine/ShadowsOfMordor/Builder.cpp +++ b/Source/Engine/ShadowsOfMordor/Builder.cpp @@ -148,7 +148,6 @@ void ShadowsOfMordor::Builder::Dispose() #include "Engine/Serialization/FileWriteStream.h" #include "Engine/Engine/CommandLine.h" #include "Engine/Scripting/Scripting.h" -#include "FlaxEngine.Gen.h" namespace ShadowsOfMordor { diff --git a/Source/Engine/Terrain/Terrain.Build.cs b/Source/Engine/Terrain/Terrain.Build.cs index 178938da8..55a590dd2 100644 --- a/Source/Engine/Terrain/Terrain.Build.cs +++ b/Source/Engine/Terrain/Terrain.Build.cs @@ -8,13 +8,22 @@ using Flax.Build.NativeCpp; /// public class Terrain : EngineModule { + /// + /// 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. + /// + public static bool WithEditing = true; + /// public override void Setup(BuildOptions options) { base.Setup(options); - options.PrivateDependencies.Add("Physics"); + if (!WithEditing) + { + options.PublicDefinitions.Add("TERRAIN_EDITING=0"); + } + options.PrivateDependencies.Add("Physics"); if (options.Target.IsEditor) { options.PrivateDependencies.Add("ContentImporters"); diff --git a/Source/Engine/Terrain/Terrain.cpp b/Source/Engine/Terrain/Terrain.cpp index 77b4ee8a6..9185f71a9 100644 --- a/Source/Engine/Terrain/Terrain.cpp +++ b/Source/Engine/Terrain/Terrain.cpp @@ -306,6 +306,16 @@ void Terrain::SetPhysicalMaterials(const ArrayHeightmap) continue; + GPUTexture* heightfield = patch->Heightmap->GetTexture(); + float size = (float)heightfield->Width(); + Float4 localToUV; + localToUV.X = localToUV.Y = chunkScale * (size - 1) / size; // Skip the last edge texel + localToUV.Z = localToUV.W = 0.5f / size; // Include half-texel offset Transform patchTransform; patchTransform.Translation = patch->_offset + Vector3(0, patch->_yOffset, 0); patchTransform.Orientation = Quaternion::Identity; patchTransform.Scale = Float3(1.0f, patch->_yHeight, 1.0f); patchTransform = _transform.LocalToWorld(patchTransform); - GlobalSignDistanceFieldPass::Instance()->RasterizeHeightfield(this, patch->Heightmap->GetTexture(), patchTransform, patch->_bounds, localToUV); + GlobalSignDistanceFieldPass::Instance()->RasterizeHeightfield(this, heightfield, patchTransform, patch->_bounds, localToUV); } return true; } diff --git a/Source/Engine/Terrain/Terrain.h b/Source/Engine/Terrain/Terrain.h index 5c671629d..984f3d388 100644 --- a/Source/Engine/Terrain/Terrain.h +++ b/Source/Engine/Terrain/Terrain.h @@ -21,11 +21,14 @@ struct RenderView; // Amount of units per terrain geometry vertex (can be adjusted per terrain instance using non-uniform scale factor) #define TERRAIN_UNITS_PER_VERTEX 100.0f -// Enable/disable terrain editing and changing at runtime. If your game doesn't use procedural terrain in game then disable this option to reduce build size. +#ifndef TERRAIN_EDITING +// Enables terrain editing and changing at runtime. If your game doesn't use procedural terrain in game then disable this option to reduce build size. #define TERRAIN_EDITING 1 +#endif // Enable/disable terrain heightmap samples modification and gather. Used by the editor to modify the terrain with the brushes. -#define TERRAIN_UPDATING 1 +// [Deprecated in 1.12, use TERRAIN_EDITING instead] +#define TERRAIN_UPDATING (TERRAIN_EDITING) // Enable/disable terrain physics collision drawing #define TERRAIN_USE_PHYSICS_DEBUG (USE_EDITOR && 1) @@ -240,6 +243,18 @@ public: return static_cast(_chunkSize); } + /// + /// Gets the heightmap texture size (square) used by a single patch (shared by all chunks within that patch). + /// + /// ChunkSize * ChunksCountEdge + 1 + API_PROPERTY() int32 GetHeightmapSize() const; + + /// + /// Gets the size of the patch in world-units (square) without actor scale. + /// + /// UnitsPerVertex * ChunksCountEdge * ChunkSize + API_PROPERTY() float GetPatchSize() const; + /// /// Gets the terrain patches count. Each patch contains 16 chunks arranged into a 4x4 square. /// @@ -329,7 +344,6 @@ public: API_FUNCTION() void SetChunkOverrideMaterial(API_PARAM(Ref) const Int2& patchCoord, API_PARAM(Ref) const Int2& chunkCoord, MaterialBase* value); #if TERRAIN_EDITING - /// /// Setups the terrain patch using the specified heightmap data. /// @@ -352,10 +366,6 @@ public: /// True if failed, otherwise 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 /// /// Setups the terrain. Clears the existing data. /// diff --git a/Source/Engine/Terrain/TerrainPatch.cpp b/Source/Engine/Terrain/TerrainPatch.cpp index 95e9ffd05..164fce747 100644 --- a/Source/Engine/Terrain/TerrainPatch.cpp +++ b/Source/Engine/Terrain/TerrainPatch.cpp @@ -17,6 +17,7 @@ #include "Engine/Threading/Threading.h" #if TERRAIN_EDITING #include "Engine/Core/Math/Packed.h" +#include "Engine/Core/Collections/ArrayExtensions.h" #include "Engine/Graphics/PixelFormatExtensions.h" #include "Engine/Graphics/RenderTools.h" #include "Engine/Graphics/RenderView.h" @@ -27,11 +28,6 @@ #include "Editor/Editor.h" #include "Engine/ContentImporters/AssetsImportingManager.h" #endif -#endif -#if TERRAIN_EDITING || TERRAIN_UPDATING -#include "Engine/Core/Collections/ArrayExtensions.h" -#endif -#if USE_EDITOR #include "Engine/Debug/DebugDraw.h" #endif #if TERRAIN_USE_PHYSICS_DEBUG @@ -90,7 +86,7 @@ void TerrainPatch::Init(Terrain* terrain, int16 x, int16 z) Splatmap[i] = nullptr; } _heightfield = nullptr; -#if TERRAIN_UPDATING +#if TERRAIN_EDITING _cachedHeightMap.Resize(0); _cachedHolesMask.Resize(0); _wasHeightModified = false; @@ -114,7 +110,7 @@ void TerrainPatch::Init(Terrain* terrain, int16 x, int16 z) TerrainPatch::~TerrainPatch() { -#if TERRAIN_UPDATING +#if TERRAIN_EDITING SAFE_DELETE(_dataHeightmap); for (int32 i = 0; i < TERRAIN_MAX_SPLATMAPS_COUNT; i++) { @@ -134,6 +130,13 @@ RawDataAsset* TerrainPatch::GetHeightfield() const return _heightfield.Get(); } +void TerrainPatch::GetTextures(GPUTexture*& heightmap, GPUTexture*& splatmap0, GPUTexture*& splatmap1) const +{ + heightmap = Heightmap->GetTexture(); + splatmap0 = Splatmap[0] ? Splatmap[0]->GetTexture() : nullptr; + splatmap1 = Splatmap[1] ? Splatmap[1]->GetTexture() : nullptr; +} + void TerrainPatch::RemoveLightmap() { for (auto& chunk : Chunks) @@ -178,7 +181,7 @@ void TerrainPatch::UpdateTransform() _collisionVertices.Resize(0); } -#if TERRAIN_EDITING || TERRAIN_UPDATING +#if TERRAIN_EDITING bool IsValidMaterial(const JsonAssetReference& e) { @@ -217,7 +220,7 @@ struct TerrainDataUpdateInfo // When using physical materials, then get splatmaps data required for per-triangle material indices void GetSplatMaps() { -#if TERRAIN_UPDATING +#if TERRAIN_EDITING if (SplatMaps[0]) return; if (UsePhysicalMaterials()) @@ -436,7 +439,7 @@ void UpdateNormalsAndHoles(const TerrainDataUpdateInfo& info, const float* heigh // Calculate normals for quad two vertices Float3 n0 = Float3::Normalize((v00 - v01) ^ (v01 - v10)); Float3 n1 = Float3::Normalize((v11 - v10) ^ (v10 - v01)); - Float3 n2 = n0 + n1; + Float3 n2 = Float3::Normalize(n0 + n1); // Apply normal to each vertex using it normalsPerVertex[i00] += n1; @@ -1021,7 +1024,7 @@ bool TerrainPatch::SetupHeightMap(int32 heightMapLength, const float* heightMap, _terrain->UpdateBounds(); _terrain->UpdateLayerBits(); -#if TERRAIN_UPDATING +#if TERRAIN_EDITING // Invalidate cache _cachedHeightMap.Resize(0); _cachedHolesMask.Resize(0); @@ -1169,7 +1172,7 @@ bool TerrainPatch::SetupSplatMap(int32 index, int32 splatMapLength, const Color3 } #endif -#if TERRAIN_UPDATING +#if TERRAIN_EDITING // Invalidate cache _cachedSplatMap[index].Resize(0); _wasSplatmapModified[index] = false; @@ -1191,7 +1194,7 @@ bool TerrainPatch::InitializeHeightMap() return SetupHeightMap(heightmap.Count(), heightmap.Get()); } -#if TERRAIN_UPDATING +#if TERRAIN_EDITING float* TerrainPatch::GetHeightmapData() { @@ -2241,11 +2244,11 @@ void TerrainPatch::DestroyCollision() _physicsHeightField = nullptr; #if TERRAIN_USE_PHYSICS_DEBUG _debugLinesDirty = true; - SAFE_DELETE(_debugLines); + SAFE_DELETE_GPU_RESOURCE(_debugLines); #endif #if USE_EDITOR _collisionTriangles.Resize(0); - SAFE_DELETE(_collisionTrianglesBuffer); + SAFE_DELETE_GPU_RESOURCE(_collisionTrianglesBuffer); _collisionTrianglesBufferDirty = true; #endif _collisionVertices.Resize(0); @@ -2569,9 +2572,9 @@ void TerrainPatch::ExtractCollisionGeometry(Array& vertexBuffer, Array& vertexBuffer, Array /// Represents single terrain patch made of 16 terrain chunks. /// @@ -34,7 +38,7 @@ private: void* _physicsHeightField; CriticalSection _collisionLocker; float _collisionScaleXZ; -#if TERRAIN_UPDATING +#if TERRAIN_EDITING Array _cachedHeightMap; Array _cachedHolesMask; Array _cachedSplatMap[TERRAIN_MAX_SPLATMAPS_COUNT]; @@ -189,6 +193,8 @@ public: return _bounds; } + void GetTextures(GPUTexture*& heightmap, GPUTexture*& splatmap0, GPUTexture*& splatmap1) const; + public: /// /// Removes the lightmap data from the terrain patch. @@ -220,7 +226,7 @@ public: /// The holes mask (optional). Normalized to 0-1 range values with holes mask per-vertex. Must match the heightmap dimensions. /// If set to true 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). /// True if failed, otherwise false. - 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); /// /// Setups the terrain patch layer weights using the specified splatmaps data. @@ -230,14 +236,12 @@ public: /// The splat map. Each array item contains 4 layer weights. /// If set to true 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). /// True if failed, otherwise false. - API_FUNCTION() bool SetupSplatMap(int32 index, int32 splatMapLength, API_PARAM(Ref) const Color32* splatMap, bool forceUseVirtualStorage = false); -#endif + API_FUNCTION() bool SetupSplatMap(int32 index, int32 splatMapLength, const Color32* splatMap, bool forceUseVirtualStorage = false); -#if TERRAIN_UPDATING /// - /// Gets the raw pointer to the heightmap data. + /// Gets the raw pointer to the heightmap data. Array size is square of Terrain.HeightmapSize. /// - /// The heightmap data. + /// The heightmap data. Null if empty or failed to access it. API_FUNCTION() float* GetHeightmapData(); /// @@ -246,9 +250,9 @@ public: API_FUNCTION() void ClearHeightmapCache(); /// - /// 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. /// - /// The holes mask data. + /// The holes mask data. Null if empty/unused or failed to access it. API_FUNCTION() byte* GetHolesMaskData(); /// @@ -257,10 +261,10 @@ public: API_FUNCTION() void ClearHolesMaskCache(); /// - /// 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. /// /// The zero-based index of the splatmap texture. - /// The splat map data. + /// The splat map data. Null if empty/unused or failed to access it. API_FUNCTION() Color32* GetSplatMapData(int32 index); /// @@ -280,7 +284,7 @@ public: /// The offset from the first row and column of the heightmap data (offset destination x and z start position). /// The size of the heightmap to modify (x and z). Amount of samples in each direction. /// True if failed, otherwise false. - 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); /// /// Modifies the terrain patch holes mask with the given samples. @@ -289,7 +293,7 @@ public: /// The offset from the first row and column of the holes map data (offset destination x and z start position). /// The size of the holes map to modify (x and z). Amount of samples in each direction. /// True if failed, otherwise false. - 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); /// /// Modifies the terrain patch splat map (layers mask) with the given samples. @@ -299,7 +303,7 @@ public: /// The offset from the first row and column of the splat map data (offset destination x and z start position). /// The size of the splat map to modify (x and z). Amount of samples in each direction. /// True if failed, otherwise false. - API_FUNCTION() bool ModifySplatMap(int32 index, API_PARAM(Ref) const Color32* samples, API_PARAM(Ref) const Int2& modifiedOffset, API_PARAM(Ref) const Int2& modifiedSize); + API_FUNCTION() bool ModifySplatMap(int32 index, const Color32* samples, const Int2& modifiedOffset, const Int2& modifiedSize); private: bool UpdateHeightData(struct TerrainDataUpdateInfo& info, const Int2& modifiedOffset, const Int2& modifiedSize, bool wasHeightRangeChanged, bool wasHeightChanged); diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp index c473f940c..707c24416 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.cpp @@ -456,8 +456,8 @@ bool MaterialGenerator::Generate(WriteStream& source, MaterialInfo& materialInfo _writer.Write(TEXT("#define MATERIAL_REFLECTIONS_SSR_COLOR ({0})\n"), sceneColorTexture.ShaderName); } WRITE_FEATURES(Defines); - inputs[In_Defines] = _writer.ToString(); WriteCustomGlobalCode(customGlobalCodeNodes, In_Defines); + inputs[In_Defines] = _writer.ToString(); _writer.Clear(); } diff --git a/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp b/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp index 6e1b5ae6f..7dd33c12a 100644 --- a/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp +++ b/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp @@ -9,6 +9,7 @@ #include "Engine/Content/Assets/Model.h" #include "Engine/Graphics/GPUBuffer.h" #include "Engine/Graphics/Models/ModelData.h" +#include "Engine/Graphics/Models/MeshAccessor.h" #include "Engine/Profiler/ProfilerCPU.h" PACK_STRUCT(struct GPUBVH { @@ -321,7 +322,6 @@ void MeshAccelerationStructure::Add(Model* model, int32 lodIndex) lodIndex = Math::Clamp(lodIndex, model->HighestResidentLODIndex(), model->LODs.Count() - 1); ModelLOD& lod = model->LODs[lodIndex]; _meshes.EnsureCapacity(_meshes.Count() + lod.Meshes.Count()); - bool failed = false; for (int32 i = 0; i < lod.Meshes.Count(); i++) { auto& mesh = lod.Meshes[i]; @@ -336,26 +336,20 @@ void MeshAccelerationStructure::Add(Model* model, int32 lodIndex) auto& meshData = _meshes.AddOne(); meshData.Asset = model; model->AddReference(); - if (model->IsVirtual()) - { - meshData.Indices = mesh.GetTriangleCount() * 3; - meshData.Vertices = mesh.GetVertexCount(); - failed |= mesh.DownloadDataGPU(MeshBufferType::Index, meshData.IndexBuffer); - failed |= mesh.DownloadDataGPU(MeshBufferType::Vertex0, meshData.VertexBuffer); - } - else - { - failed |= mesh.DownloadDataCPU(MeshBufferType::Index, meshData.IndexBuffer, meshData.Indices); - failed |= mesh.DownloadDataCPU(MeshBufferType::Vertex0, meshData.VertexBuffer, meshData.Vertices); - } - if (failed) + MeshAccessor accessor; + MeshBufferType bufferTypes[2] = { MeshBufferType::Index, MeshBufferType::Vertex0 }; + if (accessor.LoadMesh(&mesh, false, ToSpan(bufferTypes, 2))) return; - if (!meshData.IndexBuffer.IsAllocated() && meshData.IndexBuffer.Length() != 0) - { - // BVH nodes modifies index buffer (sorts data in-place) so clone it - meshData.IndexBuffer.Copy(meshData.IndexBuffer.Get(), meshData.IndexBuffer.Length()); - } - meshData.Use16BitIndexBuffer = mesh.Use16BitIndexBuffer(); + auto indexStream = accessor.Index(); + auto positionStream = accessor.Position(); + if (!indexStream.IsValid() || !positionStream.IsValid()) + return; + meshData.Indices = indexStream.GetCount(); + meshData.Vertices = positionStream.GetCount(); + meshData.IndexBuffer.Copy(indexStream.GetData()); + meshData.VertexBuffer.Allocate(meshData.Vertices * sizeof(Float3)); + positionStream.CopyTo(ToSpan(meshData.VertexBuffer.Get(), meshData.Vertices)); + meshData.Use16BitIndexBuffer = indexStream.GetFormat() == PixelFormat::R16_UInt; meshData.Bounds = mesh.GetBox(); } } diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 57afeb7a5..e3da92da2 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -593,6 +593,7 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj SERIALIZE(SloppyOptimization); SERIALIZE(LODTargetError); SERIALIZE(ImportMaterials); + SERIALIZE(CreateEmptyMaterialSlots); SERIALIZE(ImportMaterialsAsInstances); SERIALIZE(InstanceToImportAs); SERIALIZE(ImportTextures); @@ -648,6 +649,7 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi DESERIALIZE(SloppyOptimization); DESERIALIZE(LODTargetError); DESERIALIZE(ImportMaterials); + DESERIALIZE(CreateEmptyMaterialSlots); DESERIALIZE(ImportMaterialsAsInstances); DESERIALIZE(InstanceToImportAs); DESERIALIZE(ImportTextures); @@ -1024,7 +1026,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option options.ImportTypes |= ImportDataTypes::Skeleton; break; case ModelType::Prefab: - options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Animations; + options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Skeleton | ImportDataTypes::Animations; if (options.ImportMaterials) options.ImportTypes |= ImportDataTypes::Materials; if (options.ImportTextures) @@ -1050,6 +1052,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option { for (auto& mesh : lod.Meshes) { + if (mesh->BlendShapes.IsEmpty()) + continue; for (int32 blendShapeIndex = mesh->BlendShapes.Count() - 1; blendShapeIndex >= 0; blendShapeIndex--) { auto& blendShape = mesh->BlendShapes[blendShapeIndex]; @@ -1214,7 +1218,9 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option for (int32 i = 0; i < meshesCount; i++) { const auto mesh = data.LODs[0].Meshes[i]; - if (mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty()) + + // If imported mesh has skeleton but no indices or weights then need to setup those (except in Prefab mode when we conditionally import meshes based on type) + if ((mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty()) && data.Skeleton.Bones.HasItems() && (options.Type != ModelType::Prefab)) { auto indices = Int4::Zero; auto weights = Float4::UnitX; @@ -1331,7 +1337,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option auto& texture = data.Textures[i]; // Auto-import textures - if (autoImportOutput.IsEmpty() || EnumHasNoneFlags(options.ImportTypes, ImportDataTypes::Textures) || texture.FilePath.IsEmpty()) + if (autoImportOutput.IsEmpty() || EnumHasNoneFlags(options.ImportTypes, ImportDataTypes::Textures) || texture.FilePath.IsEmpty() || options.CreateEmptyMaterialSlots) continue; String assetPath = GetAdditionalImportPath(autoImportOutput, importedFileNames, StringUtils::GetFileNameWithoutExtension(texture.FilePath)); #if COMPILE_WITH_ASSETS_IMPORTER @@ -1387,6 +1393,10 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option } } + // The rest of the steps this function performs become irrelevant when we're only creating slots. + if (options.CreateEmptyMaterialSlots) + continue; + if (options.ImportMaterialsAsInstances) { // Create material instance @@ -2024,12 +2034,11 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option #undef REMAP_VERTEX_BUFFER // Remap blend shapes - dstMesh->BlendShapes.Resize(srcMesh->BlendShapes.Count()); + dstMesh->BlendShapes.EnsureCapacity(srcMesh->BlendShapes.Count(), false); for (int32 blendShapeIndex = 0; blendShapeIndex < srcMesh->BlendShapes.Count(); blendShapeIndex++) { const auto& srcBlendShape = srcMesh->BlendShapes[blendShapeIndex]; - auto& dstBlendShape = dstMesh->BlendShapes[blendShapeIndex]; - + BlendShape dstBlendShape; dstBlendShape.Name = srcBlendShape.Name; dstBlendShape.Weight = srcBlendShape.Weight; dstBlendShape.Vertices.EnsureCapacity(srcBlendShape.Vertices.Count()); @@ -2038,17 +2047,12 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option auto v = srcBlendShape.Vertices[i]; v.VertexIndex = remap[v.VertexIndex]; if (v.VertexIndex != ~0u) - { dstBlendShape.Vertices.Add(v); - } } - } - // Remove empty blend shapes - for (int32 blendShapeIndex = dstMesh->BlendShapes.Count() - 1; blendShapeIndex >= 0; blendShapeIndex--) - { - if (dstMesh->BlendShapes[blendShapeIndex].Vertices.IsEmpty()) - dstMesh->BlendShapes.RemoveAt(blendShapeIndex); + // Add only valid blend shapes + if (dstBlendShape.Vertices.HasItems()) + dstMesh->BlendShapes.Add(dstBlendShape); } // Optimize generated LOD @@ -2095,6 +2099,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option { for (auto& mesh : lod.Meshes) { + if (mesh->BlendShapes.IsEmpty()) + continue; for (auto& blendShape : mesh->BlendShapes) { // Compute min/max for used vertex indices diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index bc96e8308..00d7028cf 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -311,16 +311,19 @@ public: public: // Materials // If checked, the importer will create materials for model meshes as specified in the file. - API_FIELD(Attributes="EditorOrder(400), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))") + API_FIELD(Attributes="EditorOrder(399), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))") bool ImportMaterials = true; + // If checked, the importer will create empty material slots for every material without importing materials nor textures. + API_FIELD(Attributes="EditorOrder(400), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))") + bool CreateEmptyMaterialSlots = false; // If checked, the importer will create the model's materials as instances of a base material. - API_FIELD(Attributes="EditorOrder(401), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterials)), VisibleIf(nameof(ShowGeometry))") + API_FIELD(Attributes="EditorOrder(401), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterials)), VisibleIf(nameof(ShowGeometry)), VisibleIf(nameof(CreateEmptyMaterialSlots), true)") bool ImportMaterialsAsInstances = false; // The material used as the base material that will be instanced as the imported model's material. - API_FIELD(Attributes="EditorOrder(402), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterialsAsInstances)), VisibleIf(nameof(ShowGeometry))") + API_FIELD(Attributes="EditorOrder(402), EditorDisplay(\"Materials\"), VisibleIf(nameof(ImportMaterialsAsInstances)), VisibleIf(nameof(ShowGeometry)), VisibleIf(nameof(CreateEmptyMaterialSlots), true)") AssetReference InstanceToImportAs; // If checked, the importer will import texture files used by the model and any embedded texture resources. - API_FIELD(Attributes="EditorOrder(410), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry))") + API_FIELD(Attributes="EditorOrder(410), EditorDisplay(\"Materials\"), VisibleIf(nameof(ShowGeometry)), VisibleIf(nameof(CreateEmptyMaterialSlots), true)") bool ImportTextures = true; // If checked, the importer will try to keep the model's current overridden material slots, instead of importing materials from the source file. API_FIELD(Attributes="EditorOrder(420), EditorDisplay(\"Materials\", \"Keep Overridden Materials\"), VisibleIf(nameof(ShowGeometry))") diff --git a/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp b/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp index d7199a470..be38f0015 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp +++ b/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp @@ -722,6 +722,10 @@ bool TextureTool::ImportTextureDirectXTex(ImageType type, const StringView& path errorMsg = String::Format(TEXT("Imported texture has not full mip chain, loaded mips count: {0}, expected: {1}"), sourceMipLevels, mipLevels); return true; } + if (options.GenerateMipMaps && !isPowerOfTwo) + { + LOG(Warning, "Cannot generate mip maps for texture '{}' that size is not power of two. Use Resize or Max Size to change dimensions.", StringUtils::GetFileName(path), width, height); + } // Allocate memory for texture data auto& data = textureData.Items; diff --git a/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp b/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp index 7c74ef3f2..544877232 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp +++ b/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp @@ -543,6 +543,10 @@ bool TextureTool::ImportTextureStb(ImageType type, const StringView& path, Textu errorMsg = String::Format(TEXT("Imported texture has not full mip chain, loaded mips count: {0}, expected: {1}"), sourceMipLevels, mipLevels); return true; } + if (options.GenerateMipMaps && !isPowerOfTwo) + { + LOG(Warning, "Cannot generate mip maps for texture '{}' that size is not power of two. Use Resize or Max Size to change dimensions.", StringUtils::GetFileName(path), width, height); + } // Decompress if texture is compressed (next steps need decompressed input data, for eg. mip maps generation or format changing) if (PixelFormatExtensions::IsCompressed(textureDataSrc->Format)) diff --git a/Source/Engine/UI/GUI/CanvasRootControl.cs b/Source/Engine/UI/GUI/CanvasRootControl.cs index 4697c5051..db6722090 100644 --- a/Source/Engine/UI/GUI/CanvasRootControl.cs +++ b/Source/Engine/UI/GUI/CanvasRootControl.cs @@ -25,12 +25,12 @@ namespace FlaxEngine.GUI /// /// Gets a value indicating whether canvas is 2D (screen-space). /// - public bool Is2D => _canvas.RenderMode == CanvasRenderMode.ScreenSpace; + public bool Is2D => _canvas.Is2D; /// /// Gets a value indicating whether canvas is 3D (world-space or camera-space). /// - public bool Is3D => _canvas.RenderMode != CanvasRenderMode.ScreenSpace; + public bool Is3D => _canvas.Is3D; /// /// Initializes a new instance of the class. diff --git a/Source/Engine/UI/GUI/CanvasScaler.cs b/Source/Engine/UI/GUI/CanvasScaler.cs index 1e30fd22f..8dc1fcd4d 100644 --- a/Source/Engine/UI/GUI/CanvasScaler.cs +++ b/Source/Engine/UI/GUI/CanvasScaler.cs @@ -298,6 +298,7 @@ namespace FlaxEngine.GUI { case CanvasRenderMode.WorldSpace: case CanvasRenderMode.WorldSpaceFaceCamera: + case CanvasRenderMode.GPUTexture: scale = 1.0f; break; default: diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs index a227e5acd..b91d70dc9 100644 --- a/Source/Engine/UI/GUI/Common/Dropdown.cs +++ b/Source/Engine/UI/GUI/Common/Dropdown.cs @@ -595,7 +595,7 @@ namespace FlaxEngine.GUI Size = new Float2(size.X - margin, size.Y), Font = Font, TextColor = TextColor * 0.9f, - TextColorHighlighted = TextColorHighlighted, + TextColorHighlighted = BackgroundColorSelected.Brightness < 0.05f ? Color.Lerp(TextColorHighlighted, Color.White, 0.3f) : TextColorHighlighted, HorizontalAlignment = HorizontalAlignment, VerticalAlignment = VerticalAlignment, Text = _items[i], diff --git a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs index 2fc2ef4b3..7722fc94b 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs @@ -437,8 +437,8 @@ namespace FlaxEngine.GUI // Caret if (IsFocused && CaretPosition > -1) { - float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.7f); - alpha = alpha * alpha * alpha * alpha * alpha * alpha; + float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.8f); + alpha = alpha * alpha; Render2D.FillRectangle(CaretBounds, CaretColor * alpha); } diff --git a/Source/Engine/UI/GUI/Common/TextBox.cs b/Source/Engine/UI/GUI/Common/TextBox.cs index 1dfd9facc..3d7099db6 100644 --- a/Source/Engine/UI/GUI/Common/TextBox.cs +++ b/Source/Engine/UI/GUI/Common/TextBox.cs @@ -310,8 +310,8 @@ namespace FlaxEngine.GUI // Caret if (IsFocused && CaretPosition > -1) { - float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.7f); - alpha = alpha * alpha * alpha * alpha * alpha * alpha; + float alpha = Mathf.Saturate(Mathf.Cos(_animateTime * CaretFlashSpeed) * 0.5f + 0.8f); + alpha = alpha * alpha; Render2D.FillRectangle(CaretBounds, CaretColor * alpha); } diff --git a/Source/Engine/UI/GUI/Common/TextBoxBase.cs b/Source/Engine/UI/GUI/Common/TextBoxBase.cs index 243e4786e..201e91c61 100644 --- a/Source/Engine/UI/GUI/Common/TextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/TextBoxBase.cs @@ -1,6 +1,8 @@ // Copyright (c) Wojciech Figat. All rights reserved. using System; +using System.Collections.Generic; +using System.Linq; #if FLAX_EDITOR using FlaxEditor.Options; #endif @@ -42,6 +44,38 @@ namespace FlaxEngine.GUI '<', }; + /// + /// The allowable characters to use for the text. + /// + [Flags] + public enum AllowableCharacters + { + /// + /// Wether to not allow any character in the text. + /// + None = 0, + + /// + /// Whether to use letters in the text. + /// + Letters = 1 << 0, + + /// + /// Whether to use numbers in the text. + /// + Numbers = 1 << 1, + + /// + /// Whether to use symbols in the text. + /// + Symbols = 1 << 2, + + /// + /// Whether to use all characters in the text. + /// + All = Letters | Numbers | Symbols, + } + /// /// Default height of the text box /// @@ -86,6 +120,11 @@ namespace FlaxEngine.GUI /// Flag used to indicate whenever text can contain multiple lines. /// protected bool _isMultiline; + + /// + /// The characters to allow in the text. + /// + protected AllowableCharacters _charactersToAllow = AllowableCharacters.All; /// /// Flag used to indicate whenever text is read-only and cannot be modified by the user. @@ -188,17 +227,31 @@ namespace FlaxEngine.GUI } } + /// + /// The character to allow in the text. + /// + [EditorOrder(41), Tooltip("The character to allow in the text.")] + public AllowableCharacters CharactersToAllow + { + get => _charactersToAllow; + set => _charactersToAllow = value; + } + /// /// Gets or sets the maximum number of characters the user can type into the text box control. /// - [EditorOrder(50), Tooltip("The maximum number of characters the user can type into the text box control.")] + [EditorOrder(50), Limit(-1), Tooltip("The maximum number of characters the user can type into the text box control.")] public int MaxLength { get => _maxLength; set { - if (_maxLength <= 0) - throw new ArgumentOutOfRangeException(nameof(MaxLength)); + // Cap at min of -1 for no max length + if (value <= 0) + { + _maxLength = -1; + return; + } if (_maxLength != value) { @@ -275,7 +328,7 @@ namespace FlaxEngine.GUI /// Gets or sets the speed of the caret flashing animation. /// [EditorDisplay("Caret Style"), EditorOrder(2021), Tooltip("The speed of the caret flashing animation.")] - public float CaretFlashSpeed { get; set; } = 6.0f; + public float CaretFlashSpeed { get; set; } = 6.5f; /// /// Gets or sets the speed of the selection background flashing animation. @@ -381,7 +434,7 @@ namespace FlaxEngine.GUI value = value.Replace(DelChar.ToString(), ""); // Clamp length - if (value.Length > MaxLength) + if (value.Length > MaxLength && MaxLength != -1) value = value.Substring(0, MaxLength); // Ensure to use only single line @@ -391,15 +444,42 @@ namespace FlaxEngine.GUI value = value.GetLines()[0]; } - if (_text != value) + if (_text.Equals(value, StringComparison.Ordinal)) + return; + + if (CharactersToAllow != AllowableCharacters.All) { - Deselect(); - ResetViewOffset(); - - _text = value; - - OnTextChanged(); + if (CharactersToAllow == AllowableCharacters.None) + { + value = string.Empty; + } + else + { + if (!CharactersToAllow.HasFlag(AllowableCharacters.Letters)) + { + if (value != null) + value = new string(value.Where(c => !char.IsLetter(c)).ToArray()); + } + if (!CharactersToAllow.HasFlag(AllowableCharacters.Numbers)) + { + if (value != null) + value = new string(value.Where(c => !char.IsNumber(c)).ToArray()); + } + if (!CharactersToAllow.HasFlag(AllowableCharacters.Symbols)) + { + if (value != null) + value = new string(value.Where(c => !char.IsSymbol(c)).ToArray()); + } + value ??= string.Empty; + } } + + Deselect(); + ResetViewOffset(); + + _text = value; + + OnTextChanged(); } /// @@ -514,7 +594,7 @@ namespace FlaxEngine.GUI AutoFocus = true; _isMultiline = isMultiline; - _maxLength = 2147483646; + _maxLength = -1; _selectionStart = _selectionEnd = -1; var style = Style.Current; @@ -710,7 +790,7 @@ namespace FlaxEngine.GUI str = str.Replace("\n", ""); int selectionLength = SelectionLength; - int charactersLeft = MaxLength - _text.Length + selectionLength; + int charactersLeft = (MaxLength != -1 ? MaxLength : int.MaxValue) - _text.Length + selectionLength; Assert.IsTrue(charactersLeft >= 0); if (charactersLeft == 0) return; diff --git a/Source/Engine/UI/GUI/Control.Bounds.cs b/Source/Engine/UI/GUI/Control.Bounds.cs index 62bffd6f7..079530c50 100644 --- a/Source/Engine/UI/GUI/Control.Bounds.cs +++ b/Source/Engine/UI/GUI/Control.Bounds.cs @@ -166,8 +166,26 @@ namespace FlaxEngine.GUI [NoSerialize, HideInEditor] public Float2 LocalLocation { - get => _bounds.Location - (_parent != null ? _parent._bounds.Size * (_anchorMax + _anchorMin) * 0.5f : Float2.Zero) + _bounds.Size * _pivot; - set => Bounds = new Rectangle(value + (_parent != null ? _parent.Bounds.Size * (_anchorMax + _anchorMin) * 0.5f : Float2.Zero) - _bounds.Size * _pivot, _bounds.Size); + get + { + var anchor = Float2.Zero; + if (_parent != null) + { + _parent.GetDesireClientArea(out var parentBounds); + anchor = parentBounds.Location + parentBounds.Size * (_anchorMin + _anchorMax) * 0.5f; + } + return _bounds.Location - anchor + _bounds.Size * _pivot; + } + set + { + var anchor = Float2.Zero; + if (_parent != null) + { + _parent.GetDesireClientArea(out var parentBounds); + anchor = parentBounds.Location + parentBounds.Size * (_anchorMin + _anchorMax) * 0.5f; + } + Bounds = new Rectangle(value + anchor - _bounds.Size * _pivot, _bounds.Size); + } } /// diff --git a/Source/Engine/UI/GUI/ScrollableControl.cs b/Source/Engine/UI/GUI/ScrollableControl.cs index 4977ac89b..7caa211cf 100644 --- a/Source/Engine/UI/GUI/ScrollableControl.cs +++ b/Source/Engine/UI/GUI/ScrollableControl.cs @@ -6,7 +6,6 @@ namespace FlaxEngine.GUI /// Base class for container controls that can offset controls in a view (eg. scroll panels). /// /// - [HideInEditor] public class ScrollableControl : ContainerControl { /// diff --git a/Source/Engine/UI/GUI/Special/RadialMenu.cs b/Source/Engine/UI/GUI/Special/RadialMenu.cs index 060c4ef90..d52294da8 100644 --- a/Source/Engine/UI/GUI/Special/RadialMenu.cs +++ b/Source/Engine/UI/GUI/Special/RadialMenu.cs @@ -5,14 +5,14 @@ using System; namespace FlaxEngine.GUI { /// - /// Radial menu control that arranges child controls (of type Image) in a circle. + /// Radial menu control that arranges child controls (of type ) in a circle. /// /// public class RadialMenu : ContainerControl { private bool _materialIsDirty = true; private float _angle; - private float _selectedSegment; + private int _selectedSegment; private int _highlightSegment = -1; private MaterialBase _material; private MaterialInstance _materialInstance; @@ -27,7 +27,7 @@ namespace FlaxEngine.GUI private bool ShowMatProp => _material != null; /// - /// The material to use for menu background drawing. + /// The material used for menu background drawing. /// [EditorOrder(1)] public MaterialBase Material @@ -44,7 +44,7 @@ namespace FlaxEngine.GUI } /// - /// Gets or sets the edge offset. + /// Gets or sets the offset of the outer edge from the bounds of the Control. /// [EditorOrder(2), Range(0, 1)] public float EdgeOffset @@ -59,7 +59,7 @@ namespace FlaxEngine.GUI } /// - /// Gets or sets the thickness. + /// Gets or sets the thickness of the menu. /// [EditorOrder(3), Range(0, 1), VisibleIf(nameof(ShowMatProp))] public float Thickness @@ -74,7 +74,7 @@ namespace FlaxEngine.GUI } /// - /// 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). /// [VisibleIf(nameof(ShowMatProp))] public new Color BackgroundColor @@ -88,7 +88,7 @@ namespace FlaxEngine.GUI } /// - /// Gets or sets the color of the highlight. + /// Gets or sets the color of the outer edge highlight. /// [VisibleIf(nameof(ShowMatProp))] public Color HighlightColor @@ -130,19 +130,43 @@ namespace FlaxEngine.GUI } /// - /// The selected callback + /// The material instance of used to draw the menu. + /// + [HideInEditor] + public MaterialInstance MaterialInstance => _materialInstance; + + /// + /// The selected callback. /// [HideInEditor] public Action Selected; /// - /// The allow change selection when inside + /// Invoked when the hovered segment is changed. + /// + [HideInEditor] + public Action HoveredSelectionChanged; + + /// + /// The selected segment. + /// + [HideInEditor] + public int SelectedSegment => _selectedSegment; + + /// + /// Allows the selected to change when the mouse is moved in the empty center of the menu. /// [VisibleIf(nameof(ShowMatProp))] public bool AllowChangeSelectionWhenInside; /// - /// The center as button + /// Allows the selected to change when the mouse is moved outside of the menu. + /// + [VisibleIf(nameof(ShowMatProp))] + public bool AllowChangeSelectionWhenOutside; + + /// + /// Wether the center is a button. /// [VisibleIf(nameof(ShowMatProp))] public bool CenterAsButton; @@ -225,7 +249,7 @@ namespace FlaxEngine.GUI var min = ((1 - _edgeOffset) - _thickness) * USize * 0.5f; var max = (1 - _edgeOffset) * USize * 0.5f; var val = ((USize * 0.5f) - location).Length; - if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside) + if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside || val > max && AllowChangeSelectionWhenOutside) { UpdateAngle(ref location); } @@ -276,7 +300,7 @@ namespace FlaxEngine.GUI var min = ((1 - _edgeOffset) - _thickness) * USize * 0.5f; var max = (1 - _edgeOffset) * USize * 0.5f; var val = ((USize * 0.5f) - location).Length; - if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside) + if (Mathf.IsInRange(val, min, max) || val < min && AllowChangeSelectionWhenInside || val > max && AllowChangeSelectionWhenOutside) { UpdateAngle(ref location); } @@ -347,6 +371,28 @@ namespace FlaxEngine.GUI base.PerformLayout(force); } + /// + /// Updates the current angle and selected segment of the radial menu based on the specified location inside of the control. + /// + /// The position used to determine the angle and segment selection within the radial menu. + public void UpdateAngle(ref Float2 location) + { + float previousSelectedSegment = _selectedSegment; + + var size = new Float2(USize); + var p = (size * 0.5f) - location; + var sa = (1.0f / _segmentCount) * Mathf.TwoPi; + _angle = Mathf.Atan2(p.X, p.Y); + _angle = Mathf.Ceil((_angle - (sa * 0.5f)) / sa) * sa; + _selectedSegment = Mathf.RoundToInt((_angle < 0 ? Mathf.TwoPi + _angle : _angle) / sa); + if (float.IsNaN(_angle) || float.IsInfinity(_angle)) + _angle = 0; + _materialInstance.SetParameterValue("RadialMenu_Rotation", -_angle + Mathf.Pi); + + if (previousSelectedSegment != _selectedSegment) + HoveredSelectionChanged?.Invoke((int)_selectedSegment); + } + private void UpdateSelectionColor() { Color color; @@ -368,20 +414,6 @@ namespace FlaxEngine.GUI _materialInstance.SetParameterValue("RadialMenu_SelectionColor", color); } - private void UpdateAngle(ref Float2 location) - { - var size = new Float2(USize); - var p = (size * 0.5f) - location; - var sa = (1.0f / _segmentCount) * Mathf.TwoPi; - _angle = Mathf.Atan2(p.X, p.Y); - _angle = Mathf.Ceil((_angle - (sa * 0.5f)) / sa) * sa; - _selectedSegment = _angle; - _selectedSegment = Mathf.RoundToInt((_selectedSegment < 0 ? Mathf.TwoPi + _selectedSegment : _selectedSegment) / sa); - if (float.IsNaN(_angle) || float.IsInfinity(_angle)) - _angle = 0; - _materialInstance.SetParameterValue("RadialMenu_Rotation", -_angle + Mathf.Pi); - } - private static Float2 Rotate2D(Float2 point, float angle) { return new Float2(Mathf.Cos(angle) * point.X + Mathf.Sin(angle) * point.Y, diff --git a/Source/Engine/UI/UICanvas.cs b/Source/Engine/UI/UICanvas.cs index 20c81ce1c..df542aacf 100644 --- a/Source/Engine/UI/UICanvas.cs +++ b/Source/Engine/UI/UICanvas.cs @@ -33,6 +33,11 @@ namespace FlaxEngine /// The world space rendering mode that places Canvas as any other object in the scene and orients it to face the camera. The size of the Canvas can be set manually using its Transform, and UI elements will render in front of or behind other objects in the scene based on 3D placement. This is useful for UIs that are meant to be a part of the world. This is also known as a 'diegetic interface'. /// WorldSpaceFaceCamera = 3, + + /// + /// The off-screen rendering mode that draws the contents of the canvas into a GPU texture that can be used in the scene or by other systems. The size of the canvas is automatically set to the size of the texture. + /// + GPUTexture = 4, } /// @@ -105,7 +110,7 @@ namespace FlaxEngine private CanvasRenderMode _renderMode; private readonly CanvasRootControl _guiRoot; private CanvasRenderer _renderer; - private bool _isLoading, _isRegisteredForTick; + private bool _isLoading, _isRegisteredForTick, _isRegisteredForOnDraw; /// /// Gets or sets the canvas rendering mode. @@ -169,6 +174,8 @@ namespace FlaxEngine private bool Editor_IsCameraSpace => _renderMode == CanvasRenderMode.CameraSpace; + private bool Editor_IsGPUTexture => _renderMode == CanvasRenderMode.GPUTexture; + private bool Editor_UseRenderCamera => _renderMode == CanvasRenderMode.CameraSpace || _renderMode == CanvasRenderMode.WorldSpaceFaceCamera; #endif @@ -206,6 +213,12 @@ namespace FlaxEngine [EditorOrder(60), Limit(0.01f), EditorDisplay("Canvas"), VisibleIf("Editor_IsCameraSpace"), Tooltip("Distance from the RenderCamera to place the plane with GUI. If the screen is resized, changes resolution, or the camera frustum changes, the Canvas will automatically change size to match as well.")] public float Distance { get; set; } = 500; + /// + /// Gets or sets the output texture for the canvas when render mode is set to . The size of the canvas will be automatically set to the size of the texture. The canvas will render its content into this texture. + /// + [EditorOrder(70), NoSerialize, EditorDisplay("Canvas"), VisibleIf("Editor_IsGPUTexture")] + public GPUTexture OutputTexture { get; set; } + /// /// Gets the canvas GUI root control. /// @@ -329,6 +342,11 @@ namespace FlaxEngine _isRegisteredForTick = false; Scripting.Update -= OnUpdate; } + if (_isRegisteredForOnDraw) + { + _isRegisteredForOnDraw = false; + Scripting.Draw -= OnDraw; + } } /// @@ -358,7 +376,7 @@ namespace FlaxEngine /// /// Gets a value indicating whether canvas is 3D (world-space or camera-space). /// - public bool Is3D => _renderMode != CanvasRenderMode.ScreenSpace; + public bool Is3D => _renderMode != CanvasRenderMode.ScreenSpace && _renderMode != CanvasRenderMode.GPUTexture; /// /// Gets the world matrix used to transform the GUI from the local space to the world space. Handles canvas rendering mode @@ -491,6 +509,11 @@ namespace FlaxEngine { if (_isLoading) return; + if (_isRegisteredForOnDraw) + { + _isRegisteredForOnDraw = false; + Scripting.Draw -= OnDraw; + } switch (_renderMode) { @@ -563,7 +586,32 @@ namespace FlaxEngine } break; } + case CanvasRenderMode.GPUTexture: + { + if (!_isRegisteredForOnDraw) + { + _isRegisteredForOnDraw = true; + Scripting.Draw += OnDraw; + } + break; } + } + } + + private void OnDraw() + { + var outputTexture = OutputTexture; + if (!outputTexture || !outputTexture.IsAllocated) + return; + var context = GPUDevice.Instance.MainContext; + _guiRoot.Size = outputTexture.Size; + + Profiler.BeginEvent("UI Canvas"); + Profiler.BeginEventGPU("UI Canvas"); + context.Clear(outputTexture.View(), Color.Transparent); + Render2D.CallDrawing(GUI, context, outputTexture); + Profiler.EndEvent(); + Profiler.EndEventGPU(); } private void OnUpdate() diff --git a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXCharacterKinematic_static_64.a b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXCharacterKinematic_static_64.a index 884943f02..b532d98dc 100644 --- a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXCharacterKinematic_static_64.a +++ b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXCharacterKinematic_static_64.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c07afaaa62724f7f8f532a1bc65a5fc50753e266495248a608d9f4b9476aa0b -size 2025020 +oid sha256:7b0ec52b292d42f583822883a7275bf1f311bd46866dd5e9ab3489c025a4fd5b +size 2024932 diff --git a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXCommon_static_64.a b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXCommon_static_64.a index 59899248c..8ea5ddcca 100644 --- a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXCommon_static_64.a +++ b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXCommon_static_64.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9817730bdeae1cfeb5207470d721b27f448cdd36b9eba85da73dec59e8a5bf99 -size 52205712 +oid sha256:1f3e7321d444eb054d7ce10e71be4deee9d58133157557e7b839ecd9ac48fc98 +size 52204304 diff --git a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXCooking_static_64.a b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXCooking_static_64.a index 37e24dc4c..e0f395e4f 100644 --- a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXCooking_static_64.a +++ b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXCooking_static_64.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:524ded53592c687bc834c9db24c6ccfa8449f3392ec3fd99627c87cf7f069368 -size 111200 +oid sha256:d322d65623ce682730f28c0b390b36102eabc4ae453af9dfd813cc1e189c1628 +size 111192 diff --git a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXExtensions_static_64.a b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXExtensions_static_64.a index b13b12074..697046535 100644 --- a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXExtensions_static_64.a +++ b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXExtensions_static_64.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:759aa0379faa5f010c66654e46d352f24c1f837507ffa0178022546540f4ca0f -size 31683652 +oid sha256:5981d7ce59540fe829bab21485dbb83ab82f860ed15ca720f8c0d621bcd1296b +size 7873980 diff --git a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXFoundation_static_64.a b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXFoundation_static_64.a index 19216936b..7a4e611b4 100644 --- a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXFoundation_static_64.a +++ b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXFoundation_static_64.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6738a094c3dbe50a5820c83944b3992211bd3e372b1ccfbd2f3acb0541c04d2b -size 570452 +oid sha256:e915aa5a013c099fe6ced91c50f1fa8b5e46e3778b08d09d0a12d59f6252d87f +size 570404 diff --git a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXPvdSDK_static_64.a b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXPvdSDK_static_64.a deleted file mode 100644 index e681dfc1b..000000000 --- a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXPvdSDK_static_64.a +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7369e2822a705f592c67942c0ba354694b857fa9d83db6bed4dc0dcf3a4875f0 -size 3405028 diff --git a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXVehicle2_static_64.a b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXVehicle2_static_64.a index edb89c6a0..78008e4b2 100644 --- a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXVehicle2_static_64.a +++ b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXVehicle2_static_64.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7bbb5ba66f5e47ee53fd099c805e94d66f47cb66c44ec2339f06b66ea89c6100 -size 1543576 +oid sha256:f6a43c86163970f92cb49f508c175e8cbd80ee83fc193d10011eb5905f20e0ab +size 1543440 diff --git a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXVehicle_static_64.a b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXVehicle_static_64.a index 815ea1e4a..81b2061c1 100644 --- a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXVehicle_static_64.a +++ b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysXVehicle_static_64.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4eff2cf32d6bfed5f32d26b5478ec4e1eda31d3959e71f79fa6e205905b1e67c -size 12277380 +oid sha256:cd5d1083b5479d322b98b56b94dc42139c5965a5c7434fe058382f760708273b +size 2364570 diff --git a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysX_static_64.a b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysX_static_64.a index a37ba9028..7fc068ccb 100644 --- a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysX_static_64.a +++ b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libPhysX_static_64.a @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ee3f4e2cc3160b7556dd8678c49f27160d401fbef729c81568dbc184b07fbab5 -size 37483230 +oid sha256:ae10a05b256896db3868df5d9914d5ffd10efbcc64dea046ba11946514625b7e +size 34853698 diff --git a/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libmsdfgen-core.a b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libmsdfgen-core.a new file mode 100644 index 000000000..274ba3a52 --- /dev/null +++ b/Source/Platforms/Android/Binaries/ThirdParty/ARM64/libmsdfgen-core.a @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c34b3e22db047130c0f0a425197f4e27f345c458fbadb73104c7af69d0f9f2cd +size 4560474 diff --git a/Source/Platforms/Linux/Binaries/ThirdParty/x64/libmsdfgen-core.a b/Source/Platforms/Linux/Binaries/ThirdParty/x64/libmsdfgen-core.a new file mode 100644 index 000000000..b3b6c9433 --- /dev/null +++ b/Source/Platforms/Linux/Binaries/ThirdParty/x64/libmsdfgen-core.a @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8ff134eba56b2889351f7bd1cd82744c1c4b4d9c7bb96d5daa99e8e047a7c30 +size 516942 diff --git a/Source/Platforms/Mac/Binaries/ThirdParty/ARM64/libmsdfgen-core.a b/Source/Platforms/Mac/Binaries/ThirdParty/ARM64/libmsdfgen-core.a new file mode 100644 index 000000000..60b14c840 --- /dev/null +++ b/Source/Platforms/Mac/Binaries/ThirdParty/ARM64/libmsdfgen-core.a @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb5021ea29786ec0b7134c33404c565c829ebdcde71b7891ceb45fdd5c5082b6 +size 347384 diff --git a/Source/Platforms/Mac/Binaries/ThirdParty/x64/libmsdfgen-core.a b/Source/Platforms/Mac/Binaries/ThirdParty/x64/libmsdfgen-core.a new file mode 100644 index 000000000..248cfd4fb --- /dev/null +++ b/Source/Platforms/Mac/Binaries/ThirdParty/x64/libmsdfgen-core.a @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6bc9e9aa3dc131affa4ad64b46cffb0d229cfaac6253743f3cc755cfce519015 +size 422104 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/FastXml_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/FastXml_64.pdb index 43a87a79d..ab822e5d2 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/FastXml_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/FastXml_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:132883c177c47a532afd6fa33b5a2f498cd310001400062a6df33743c4d5e784 +oid sha256:dc758a8cf6f50caf75476128ceffc41afbb6b8aaa45e81cd17ef694c18bec1a3 size 118784 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/LowLevelAABB_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/LowLevelAABB_64.pdb index 819f1d896..acf9b53bc 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/LowLevelAABB_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/LowLevelAABB_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7668042f2a51ef982925d28ec57efe89f3adfc0f0bdd45c0d836ea810010d0a1 -size 593920 +oid sha256:0356328c34bc457903e70c6f3c9d984f288ff3917564cb5f5bd2efb84c8a0b9f +size 585728 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/LowLevelDynamics_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/LowLevelDynamics_64.pdb index e96418eed..5c8c6b364 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/LowLevelDynamics_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/LowLevelDynamics_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f5eb77e8531012b27e7616f097e17bd15a0d30f0314f311bacefa67610eb873c -size 1339392 +oid sha256:8a2a326575d5b6b468dc896399bb9bcb324a44f9097cb6b78457875737df7356 +size 1323008 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/LowLevel_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/LowLevel_64.pdb index a6f6b58f8..2a28827ee 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/LowLevel_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/LowLevel_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5ed568ad39c0d14de2e51b413657cc965ee2e831b4b6bc7e6418d16fa697163 -size 1257472 +oid sha256:7313a497a5a851d86d7a2f2161f5d4a01274b031bb3d3d0ccb279fc9db199e24 +size 1241088 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCharacterKinematic_static_64.lib b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCharacterKinematic_static_64.lib index c9e7b5dfe..4258cc709 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCharacterKinematic_static_64.lib +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCharacterKinematic_static_64.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:603e6025b22349137e7244bf6ca4056e09e6947660469269da1944d9f92e02fb -size 1306220 +oid sha256:46f69808637c2a42353e3935f2f6b3c3babb3bf0853e4551b70ce7e1f63a19ec +size 1319326 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCharacterKinematic_static_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCharacterKinematic_static_64.pdb index b82c855c8..2f9cdc350 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCharacterKinematic_static_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCharacterKinematic_static_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1c870f987df7407b2edc5615a5fb4d3cb942af801d0260a13de1d27e52eac4fb -size 675840 +oid sha256:4cfb89b2836af0efe36f6e062262d4720153c9c3c396bfc6adb5098eb89e46c3 +size 667648 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCommon_static_64.lib b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCommon_static_64.lib index 56a97a846..130cfe8a1 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCommon_static_64.lib +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCommon_static_64.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d2b2093cf1f72c4135f653875f0fe0fa33d6696d34c29b6ac8d4ac3bb7aa92c1 -size 26130976 +oid sha256:935f6ea3bc1162acabadeaec6b34cc81445f8039afbdc0edd284e7200cf662d8 +size 24990306 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCommon_static_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCommon_static_64.pdb index 4c3883939..24e7628a4 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCommon_static_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCommon_static_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e0c09e6550a0ab319a7cde6382cc31ba31113d54d8d779444435e56213d07285 -size 1822720 +oid sha256:65352f47a3e32ca16ae6c5cb672ff9a5ea5098a4ade33a080accf5e57054f679 +size 1781760 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCooking_static_64.lib b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCooking_static_64.lib index 7a47a2ddb..03a130cf4 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCooking_static_64.lib +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCooking_static_64.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea85451d201ceeea71fd46bb01b5dc3501a05c77538bb85ae665ec6c0385c350 -size 130200 +oid sha256:3f786c5d71caa92ad50e186da67ea1564213d593c709134a25cbced5c1184338 +size 128984 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCooking_static_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCooking_static_64.pdb index c79307c68..2ae2ecf13 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCooking_static_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXCooking_static_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7f18f710748a40b3be245f1027748ed6c9fe6058e049c3926632c0658dd983f4 -size 397312 +oid sha256:e22e8f2cf72d6c4d5fa35e426e6e0cc41e63402dc85aad5a1e65bc34bfd0146c +size 389120 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXExtensions_static_64.lib b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXExtensions_static_64.lib index 7d543561d..4207c354d 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXExtensions_static_64.lib +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXExtensions_static_64.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f880ee07970656a19ac9a811f16be3edeed06834886606f2385bbcce9389ea4c -size 17300374 +oid sha256:0fce079940f5641113cb0d27c874923ab47a2e8ee286764c5711159c0e5f5bea +size 5639208 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXExtensions_static_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXExtensions_static_64.pdb index b6401abd3..178fb18f8 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXExtensions_static_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXExtensions_static_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:abd53b7899e7bce0b2180189d8c28df8b38107bb90cdd6078069c9d869169d9a -size 5156864 +oid sha256:7f28ef88132564ade7d813a9bfa667ce4c50e407ce3d6bcf2f386e8f5da98e16 +size 1740800 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXFoundation_static_64.lib b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXFoundation_static_64.lib index dcb1935ee..3f08fa2c4 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXFoundation_static_64.lib +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXFoundation_static_64.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f84c317ca2181e564fd7f6814898f9a1c738e3f157e5974da1fcf72d6abe8f9 -size 585126 +oid sha256:8685d51d5b7b69ee945da97e026cad77b98f91c564aae73176ca86c0d5f1d1b4 +size 571940 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXFoundation_static_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXFoundation_static_64.pdb index f784a8e8e..e074d0436 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXFoundation_static_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXFoundation_static_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c4094a92b9252e8f0dee08600ef5f93fa01abb9b21fe4a2944114d41cd143ec -size 299008 +oid sha256:e0c44d3c2b487797eaefed7708d5389a366fa5217f3c5d6d06fdca00871de942 +size 315392 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXPvdSDK_static_64.lib b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXPvdSDK_static_64.lib deleted file mode 100644 index 52b0147b0..000000000 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXPvdSDK_static_64.lib +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:747ed5f9e098e9c8e0a75cdfdfd09e82b338904b5916cc60df45d585f398b215 -size 2073834 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXPvdSDK_static_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXPvdSDK_static_64.pdb deleted file mode 100644 index 3778728ef..000000000 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXPvdSDK_static_64.pdb +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:227c4cddf1a1326c6f6a9cfb106c592f75433fcf047971f63c8bc992c6f16369 -size 946176 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXTask_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXTask_64.pdb index ed9215af3..06bee0418 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXTask_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXTask_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ea9f74cc743bf588cbb9e6a5dbe544551eefe6be2ffa1c0232908159f3fd2612 +oid sha256:9898aebf247d35e2b27b0107fc7833abb5b2cc45a7bb5581f1f5d25a40e61fbf size 143360 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle2_static_64.lib b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle2_static_64.lib index 1c7cb3bc0..b5abd5493 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle2_static_64.lib +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle2_static_64.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9c0c1865cba540abc026208ebf09653bb30f1ab9f800d65d53ea716e9b21ba64 -size 1125480 +oid sha256:56a251bf1d01a66e728618bbc53c3e54a8039c40465152f76868e37802137172 +size 1142828 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle2_static_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle2_static_64.pdb index 11ae60a06..79025e37d 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle2_static_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle2_static_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f83d273d0419023214b3f5094bfc23bd567c495bcf262fb6b2d696427a390f19 -size 733184 +oid sha256:32f10a769eb31b14338b8f858b7a4d462fbc02d3b96c9f3ecc12f12aa8ccb4ce +size 724992 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle_static_64.lib b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle_static_64.lib index c71216d38..4784302b8 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle_static_64.lib +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle_static_64.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64d9bd9c981905263b5d10ab9a9b2153785b58d6405ad99ed5119d4b70b55f51 -size 5843352 +oid sha256:5ae79e8eaee91bc6db4b0b99d5c814b9609d323f7b2c0ffaeeccf8e09b4cd4be +size 1550810 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle_static_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle_static_64.pdb index 67447c121..47b31ec74 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle_static_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysXVehicle_static_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eba825400abc8140c3d496ff0ad6df25ae9b4d4d2acb63db81171250d874e8b9 -size 3862528 +oid sha256:dcecb48dfed2969626a61197f6edc95dee1bdaf598c4567278704d0babffe812 +size 602112 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysX_static_64.lib b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysX_static_64.lib index 8a0399bd7..beed86282 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysX_static_64.lib +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysX_static_64.lib @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8311b062fb2153604f3eddee2d2a5720beb7f571159e8b1ed8c9fa89d3a3d6c5 -size 34826088 +oid sha256:4d2c83f579a3c96adc604f20ad0219016cad04c21a78ac539cd38686b2556de8 +size 33511310 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysX_static_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysX_static_64.pdb index 6b3fbcb2d..1a0ffa3b8 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysX_static_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/PhysX_static_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a332f82798acafc6423c40e09223d7781abc43fbe5cbe1aed9069937ed7de180 -size 4558848 +oid sha256:0522612dd80ec1739c244136e42221e25a17db0d8d94bc860aa78469c0d0b329 +size 3502080 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/SceneQuery_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/SceneQuery_64.pdb index c6196ff06..1ce7cb454 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/SceneQuery_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/SceneQuery_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac1251babb3766e7e9bc2efc40873b43aa1ae55b331918c9dc790f82b9e77d4f +oid sha256:b3c3204aaded5ae0ea1acc502ca15597a2884658957ec36c778395e70321e853 size 544768 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/SimulationController_64.pdb b/Source/Platforms/Windows/Binaries/ThirdParty/x64/SimulationController_64.pdb index 92b08e4bc..d42fb5191 100644 --- a/Source/Platforms/Windows/Binaries/ThirdParty/x64/SimulationController_64.pdb +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/SimulationController_64.pdb @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b11081d87071515d65544c9265b9bd09bb66af0810fbd6ec03bd28512892466b -size 2699264 +oid sha256:b2986a02de98efa5ae0f1883d6ed5af91d042d3bad136c8883f6e6a47d2e91a8 +size 2674688 diff --git a/Source/Platforms/Windows/Binaries/ThirdParty/x64/msdfgen-core.lib b/Source/Platforms/Windows/Binaries/ThirdParty/x64/msdfgen-core.lib new file mode 100644 index 000000000..3b2aeffc3 --- /dev/null +++ b/Source/Platforms/Windows/Binaries/ThirdParty/x64/msdfgen-core.lib @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1bb19678e7b094a07f035bf9438fde6ec56bc0d68a192a2e719a41aee4089e03 +size 1097914 diff --git a/Source/Platforms/iOS/Binaries/ThirdParty/ARM64/libmsdfgen-core.a b/Source/Platforms/iOS/Binaries/ThirdParty/ARM64/libmsdfgen-core.a new file mode 100644 index 000000000..7fd2e753b --- /dev/null +++ b/Source/Platforms/iOS/Binaries/ThirdParty/ARM64/libmsdfgen-core.a @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:673aff1bb5180811371876fa19f8f4dc6a97e76690d27544f507136c3eb4ba0d +size 349464 diff --git a/Source/Shaders/Common.hlsl b/Source/Shaders/Common.hlsl index 66303546a..2052214b2 100644 --- a/Source/Shaders/Common.hlsl +++ b/Source/Shaders/Common.hlsl @@ -131,6 +131,7 @@ SamplerComparisonState ShadowSamplerLinear : register(s5); #define SAMPLE_RT_LINEAR(rt, texCoord) rt.SampleLevel(SamplerLinearClamp, texCoord, 0) #define HDR_CLAMP_MAX 65472.0 #define PI 3.1415926535897932 +#define UNITS_TO_METERS_SCALE 0.01f // Structure that contains information about GBuffer struct GBufferData diff --git a/Source/Shaders/GI/DDGI.shader b/Source/Shaders/GI/DDGI.shader index b080efc0b..bade984fc 100644 --- a/Source/Shaders/GI/DDGI.shader +++ b/Source/Shaders/GI/DDGI.shader @@ -351,6 +351,7 @@ META_CS(true, FEATURE_LEVEL_SM5) void CS_UpdateProbesInitArgs() { uint activeProbesCount = ActiveProbes.Load(0); // Counter at 0 + activeProbesCount = min(activeProbesCount, ProbesCount); uint arg = 0; for (uint probesOffset = 0; probesOffset < activeProbesCount; probesOffset += DDGI_TRACE_RAYS_PROBES_COUNT_LIMIT) { diff --git a/Source/Shaders/GUI.shader b/Source/Shaders/GUI.shader index e9eedd4a6..a8f9452e9 100644 --- a/Source/Shaders/GUI.shader +++ b/Source/Shaders/GUI.shader @@ -92,7 +92,17 @@ float4 PS_Font(VS2PS input) : SV_Target0 PerformClipping(input); float4 color = input.Color; - color.a *= Image.Sample(SamplerLinearClamp, input.TexCoord).r; + color.a *= SampleFont(Image, input.TexCoord); + return color; +} + +META_PS(true, FEATURE_LEVEL_ES2) +float4 PS_FontMSDF(VS2PS input) : SV_Target0 +{ + PerformClipping(input); + + float4 color = input.Color; + color.a *= SampleFontMSDF(Image, input.TexCoord); return color; } diff --git a/Source/Shaders/GUICommon.hlsl b/Source/Shaders/GUICommon.hlsl index 14c8b1327..07420389c 100644 --- a/Source/Shaders/GUICommon.hlsl +++ b/Source/Shaders/GUICommon.hlsl @@ -54,4 +54,52 @@ void PerformClipping(VS2PS input) PerformClipping(input.ClipOriginAndPos.xy, input.ClipOriginAndPos.zw, input.ClipExtents); } +float SampleFont(Texture2D font, float2 uv) +{ + return font.Sample(SamplerLinearClamp, uv).r; +} + +float GetFontMSDFMedian(Texture2D font, float2 uv) +{ + float4 msd = font.Sample(SamplerLinearClamp, uv); + return max(min(msd.r, msd.g), min(max(msd.r, msd.g), msd.b)); +} + +float GetFontMSDFMedian(float4 msd) +{ + return max(min(msd.r, msd.g), min(max(msd.r, msd.g), msd.b)); +} + +float GetFontMSDFPixelRange(Texture2D font, float2 uv) +{ + uint width, height; + font.GetDimensions(width, height); + float pxRange = 4.0f; // Must match C++ code + float unitRange = float2(pxRange, pxRange) / float2(width, height); + + float2 dx = ddx(uv); + float2 dy = ddy(uv); + float2 screenTexSize = rsqrt(dx * dx + dy * dy); + return max(0.5f * dot(screenTexSize, unitRange), 1.0f); +} + +float SampleFontMSDF(Texture2D font, float2 uv) +{ + float sd = GetFontMSDFMedian(font, uv); + float screenPxRange = GetFontMSDFPixelRange(font, uv); + float screenPxDist = screenPxRange * (sd - 0.5f); + return saturate(screenPxDist + 0.5f); +} + +float SampleFontMSDFOutline(Texture2D font, float2 uv, float thickness) +{ + float4 msd = font.Sample(SamplerLinearClamp, uv); + float sd = max(min(msd.r, msd.g), min(max(msd.r, msd.g), msd.b)); + float screenPxRange = GetFontMSDFPixelRange(font, uv); + float thick = clamp(thickness, 0.0, screenPxRange * 0.5 - 1.0) / screenPxRange; + float outline = saturate((min(sd, msd.a) - 0.5 + thick) * screenPxRange + 0.5); + outline *= 1 - saturate(screenPxRange * (sd - 0.5f) + 0.5f); + return outline; +} + #endif diff --git a/Source/Shaders/GlobalSignDistanceField.shader b/Source/Shaders/GlobalSignDistanceField.shader index fe4bafda5..e8bb1b1f7 100644 --- a/Source/Shaders/GlobalSignDistanceField.shader +++ b/Source/Shaders/GlobalSignDistanceField.shader @@ -170,15 +170,14 @@ void CS_RasterizeHeightfield(uint3 DispatchThreadId : SV_DispatchThreadID) // Convert voxel world-space position into heightfield local-space position and get heightfield UV float4x4 worldToLocal = ToMatrix4x4(objectData.WorldToVolume); float3 volumePos = mul(float4(voxelWorldPos, 1), worldToLocal).xyz; - float3 volumeUV = volumePos * objectData.VolumeToUVWMul + objectData.VolumeToUVWAdd; - float2 heightfieldUV = float2(volumeUV.x, volumeUV.z); // Sample heightfield around the voxel location (heightmap uses point sampler) Texture2D heightmap = ObjectsTextures[i]; float4 localToUV = float4(objectData.VolumeToUVWMul.xz, objectData.VolumeToUVWAdd.xz); +#if 1 float3 n00, n10, n01, n11; bool h00, h10, h01, h11; - float offset = CascadeVoxelSize * 2; + float offset = CascadeVoxelSize; float3 p00 = SampleHeightmap(heightmap, volumePos + float3(-offset, 0, 0), localToUV, n00, h00, objectData.MipOffset); float3 p10 = SampleHeightmap(heightmap, volumePos + float3(+offset, 0, 0), localToUV, n10, h10, objectData.MipOffset); float3 p01 = SampleHeightmap(heightmap, volumePos + float3(0, 0, -offset), localToUV, n01, h01, objectData.MipOffset); @@ -189,6 +188,11 @@ void CS_RasterizeHeightfield(uint3 DispatchThreadId : SV_DispatchThreadID) float3 heightfieldNormal = (n00 + n10 + n01 + n11) * 0.25f; heightfieldNormal = normalize(heightfieldNormal); bool isHole = h00 || h10 || h01 || h11; +#else + float3 heightfieldNormal; + bool isHole; + float3 heightfieldPosition = SampleHeightmap(heightmap, volumePos, localToUV, heightfieldNormal, isHole, objectData.MipOffset); +#endif // Skip holes and pixels outside the heightfield if (isHole) diff --git a/Source/Shaders/LightingCommon.hlsl b/Source/Shaders/LightingCommon.hlsl index f09572310..734b9b709 100644 --- a/Source/Shaders/LightingCommon.hlsl +++ b/Source/Shaders/LightingCommon.hlsl @@ -68,20 +68,25 @@ void GetRadialLightAttenuation( // Distance attenuation if (lightData.InverseSquared) { + // Convert scene units to meters for inverse-squared falloff + const float distanceScale = UNITS_TO_METERS_SCALE; + const float distanceScaleSqr = distanceScale * distanceScale; + float distanceSqrScaled = distanceSqr * distanceScaleSqr; + float distanceBiasSqrScaled = distanceBiasSqr * distanceScaleSqr; BRANCH if (lightData.SourceLength > 0) { - float3 l01 = lightData.Direction * lightData.SourceLength; - float3 l0 = toLight - 0.5 * l01; - float3 l1 = toLight + 0.5 * l01; + float3 l01 = lightData.Direction * (lightData.SourceLength * distanceScale); + float3 l0 = (toLight * distanceScale) - 0.5 * l01; + float3 l1 = (toLight * distanceScale) + 0.5 * l01; float lengthL0 = length(l0); float lengthL1 = length(l1); - attenuation = rcp((lengthL0 * lengthL1 + dot(l0, l1)) * 0.5 + distanceBiasSqr); + attenuation = rcp((lengthL0 * lengthL1 + dot(l0, l1)) * 0.5 + distanceBiasSqrScaled); NoL = saturate(0.5 * (dot(N, l0) / lengthL0 + dot(N, l1) / lengthL1)); } else { - attenuation = rcp(distanceSqr + distanceBiasSqr); + attenuation = rcp(distanceSqrScaled + distanceBiasSqrScaled); NoL = saturate(dot(N, L)); } attenuation *= Square(saturate(1 - Square(distanceSqr * Square(lightData.RadiusInv)))); diff --git a/Source/Shaders/TerrainCommon.hlsl b/Source/Shaders/TerrainCommon.hlsl index 0c2f57168..f883e8033 100644 --- a/Source/Shaders/TerrainCommon.hlsl +++ b/Source/Shaders/TerrainCommon.hlsl @@ -35,11 +35,11 @@ float3 SampleHeightmap(Texture2D heightmap, float3 localPosition, float4 { // Sample heightmap float2 uv = localPosition.xz * localToUV.xy + localToUV.zw; - float4 value = heightmap.SampleLevel(SamplerPointClamp, uv, mipOffset); + float4 value = heightmap.SampleLevel(SamplerLinearClamp, uv, mipOffset); // Decode heightmap normal = DecodeHeightmapNormal(value, isHole); - float height = DecodeHeightmapHeight(value);; + float height = DecodeHeightmapHeight(value); float3 position = float3(localPosition.x, height, localPosition.z); // UVs outside the heightmap are empty diff --git a/Source/ThirdParty/PhysX/extensions/PxRepXSerializer.h b/Source/ThirdParty/PhysX/extensions/PxRepXSerializer.h index 92f8211d0..0effba190 100644 --- a/Source/ThirdParty/PhysX/extensions/PxRepXSerializer.h +++ b/Source/ThirdParty/PhysX/extensions/PxRepXSerializer.h @@ -35,6 +35,8 @@ #include "common/PxBase.h" #include "extensions/PxRepXSimpleType.h" +#if PX_SERIALIZATION + #if !PX_DOXYGEN namespace physx { @@ -147,3 +149,4 @@ PX_INLINE physx::PxRepXObject PxCreateRepXObject(const TDataType* inType) /** @} */ #endif +#endif diff --git a/Source/ThirdParty/PhysX/foundation/PxHash.h b/Source/ThirdParty/PhysX/foundation/PxHash.h index 43ea55215..c85756b0a 100644 --- a/Source/ThirdParty/PhysX/foundation/PxHash.h +++ b/Source/ThirdParty/PhysX/foundation/PxHash.h @@ -86,7 +86,7 @@ PX_FORCE_INLINE uint32_t PxComputeHash(const uint64_t key) return uint32_t(UINT32_MAX & k); } -#if PX_APPLE_FAMILY +#if PX_APPLE_FAMILY || PX_EMSCRIPTEN // hash for size_t, to make gcc happy PX_INLINE uint32_t PxComputeHash(const size_t key) { diff --git a/Source/ThirdParty/PhysX/foundation/PxMath.h b/Source/ThirdParty/PhysX/foundation/PxMath.h index cff5b1795..8b071fa32 100644 --- a/Source/ThirdParty/PhysX/foundation/PxMath.h +++ b/Source/ThirdParty/PhysX/foundation/PxMath.h @@ -45,7 +45,7 @@ #pragma warning(pop) #endif -#if (PX_LINUX_FAMILY && !PX_ARM_FAMILY) +#if (PX_LINUX_FAMILY && !PX_ARM_FAMILY && !PX_EMSCRIPTEN) // Force linking against nothing newer than glibc v2.17 to remain compatible with platforms with older glibc versions __asm__(".symver expf,expf@GLIBC_2.2.5"); __asm__(".symver powf,powf@GLIBC_2.2.5"); diff --git a/Source/ThirdParty/PhysX/foundation/PxPreprocessor.h b/Source/ThirdParty/PhysX/foundation/PxPreprocessor.h index 2b23aa222..6ca05cee3 100644 --- a/Source/ThirdParty/PhysX/foundation/PxPreprocessor.h +++ b/Source/ThirdParty/PhysX/foundation/PxPreprocessor.h @@ -437,8 +437,16 @@ Use these macro definitions to create warnings for deprecated functions General defines */ +// Customization to exclude serialization code +#define PX_SERIALIZATION 0 + +// Customization to exclude soft-bodies (incl. tetrahedral meshes) +#define PX_SOFT_BODY 0 + // static assert -#if(defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7))) || (PX_APPLE_FAMILY) || (PX_SWITCH || PX_PS4 || PX_PS5) || (PX_CLANG && PX_ARM) || (PX_CLANG && PX_A64) +#if PX_EMSCRIPTEN + #define PX_COMPILE_TIME_ASSERT(exp) +#elif(defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 7))) || (PX_APPLE_FAMILY) || (PX_SWITCH || PX_PS4 || PX_PS5) || (PX_CLANG && PX_ARM) || (PX_CLANG && PX_A64) #define PX_COMPILE_TIME_ASSERT(exp) typedef char PX_CONCAT(PxCompileTimeAssert_Dummy, __COUNTER__)[(exp) ? 1 : -1] __attribute__((unused)) #else #define PX_COMPILE_TIME_ASSERT(exp) typedef char PxCompileTimeAssert_Dummy[(exp) ? 1 : -1] diff --git a/Source/ThirdParty/msdfgen/LICENSE.txt b/Source/ThirdParty/msdfgen/LICENSE.txt new file mode 100644 index 000000000..fbea359b2 --- /dev/null +++ b/Source/ThirdParty/msdfgen/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2014 - 2025 Viktor Chlumsky + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Source/ThirdParty/msdfgen/msdfgen.Build.cs b/Source/ThirdParty/msdfgen/msdfgen.Build.cs new file mode 100644 index 000000000..c5f008ac9 --- /dev/null +++ b/Source/ThirdParty/msdfgen/msdfgen.Build.cs @@ -0,0 +1,46 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using System.IO; +using Flax.Build; +using Flax.Build.NativeCpp; + +/// +/// https://github.com/Chlumsky/msdfgen +/// +public class msdfgen : DepsModule +{ + /// + public override void Init() + { + base.Init(); + + LicenseType = LicenseTypes.MIT; + LicenseFilePath = "LICENSE.txt"; + + // Merge third-party modules into engine binary + BinaryModuleName = "FlaxEngine"; + } + + /// + public override void Setup(BuildOptions options) + { + base.Setup(options); + + var depsRoot = options.DepsFolder; + switch (options.Platform.Target) + { + case TargetPlatform.Windows: + options.OutputFiles.Add(Path.Combine(depsRoot, "msdfgen-core.lib")); + break; + case TargetPlatform.Linux: + case TargetPlatform.Mac: + case TargetPlatform.iOS: + case TargetPlatform.Android: + options.OutputFiles.Add(Path.Combine(depsRoot, "libmsdfgen-core.a")); + break; + default: throw new InvalidPlatformException(options.Platform.Target); + } + + options.PublicIncludePaths.Add(Path.Combine(Globals.EngineRoot, "Source/ThirdParty/msdfgen")); + } +} diff --git a/Source/ThirdParty/msdfgen/msdfgen.h b/Source/ThirdParty/msdfgen/msdfgen.h new file mode 100644 index 000000000..a0d69e2a5 --- /dev/null +++ b/Source/ThirdParty/msdfgen/msdfgen.h @@ -0,0 +1,4 @@ + +#pragma once + +#include "msdfgen/msdfgen.h" diff --git a/Source/ThirdParty/msdfgen/msdfgen/core/Bitmap.h b/Source/ThirdParty/msdfgen/msdfgen/core/Bitmap.h new file mode 100644 index 000000000..05c305d64 --- /dev/null +++ b/Source/ThirdParty/msdfgen/msdfgen/core/Bitmap.h @@ -0,0 +1,60 @@ + +#pragma once + +#include "YAxisOrientation.h" +#include "BitmapRef.hpp" + +namespace msdfgen { + +/// A 2D image bitmap with N channels of type T. Pixel memory is managed by the class. +template +class Bitmap { + +public: + Bitmap(); + Bitmap(int width, int height, YAxisOrientation yOrientation = MSDFGEN_Y_AXIS_DEFAULT_ORIENTATION); + explicit Bitmap(const BitmapConstRef &orig); + explicit Bitmap(const BitmapConstSection &orig); + Bitmap(const Bitmap &orig); +#ifdef MSDFGEN_USE_CPP11 + Bitmap(Bitmap &&orig); +#endif + ~Bitmap(); + Bitmap &operator=(const BitmapConstRef &orig); + Bitmap &operator=(const BitmapConstSection &orig); + Bitmap &operator=(const Bitmap &orig); +#ifdef MSDFGEN_USE_CPP11 + Bitmap &operator=(Bitmap &&orig); +#endif + /// Bitmap width in pixels. + int width() const; + /// Bitmap height in pixels. + int height() const; + T *operator()(int x, int y); + const T *operator()(int x, int y) const; +#ifdef MSDFGEN_USE_CPP11 + explicit operator T *(); + explicit operator const T *() const; +#else + operator T *(); + operator const T *() const; +#endif + operator BitmapRef(); + operator BitmapConstRef() const; + operator BitmapSection(); + operator BitmapConstSection() const; + /// Returns a reference to a rectangular section of the bitmap specified by bounds (excluding xMax, yMax). + BitmapSection getSection(int xMin, int yMin, int xMax, int yMax); + /// Returns a constant reference to a rectangular section of the bitmap specified by bounds (excluding xMax, yMax). + BitmapConstSection getConstSection(int xMin, int yMin, int xMax, int yMax) const; + +private: + T *pixels; + int w, h; + YAxisOrientation yOrientation; + +}; + +} + +#include "Bitmap.hpp" diff --git a/Source/ThirdParty/msdfgen/msdfgen/core/Bitmap.hpp b/Source/ThirdParty/msdfgen/msdfgen/core/Bitmap.hpp new file mode 100644 index 000000000..afb53942f --- /dev/null +++ b/Source/ThirdParty/msdfgen/msdfgen/core/Bitmap.hpp @@ -0,0 +1,172 @@ + +#include "Bitmap.h" + +#include +#include + +namespace msdfgen { + +template +Bitmap::Bitmap() : pixels(NULL), w(0), h(0), yOrientation(MSDFGEN_Y_AXIS_DEFAULT_ORIENTATION) { } + +template +Bitmap::Bitmap(int width, int height, YAxisOrientation yOrientation) : w(width), h(height), yOrientation(yOrientation) { + pixels = new T[N*w*h]; +} + +template +Bitmap::Bitmap(const BitmapConstRef &orig) : w(orig.width), h(orig.height), yOrientation(orig.yOrientation) { + pixels = new T[N*w*h]; + memcpy(pixels, orig.pixels, sizeof(T)*N*w*h); +} + +template +Bitmap::Bitmap(const BitmapConstSection &orig) : w(orig.width), h(orig.height), yOrientation(orig.yOrientation) { + pixels = new T[N*w*h]; + T *dst = pixels; + const T *src = orig.pixels; + int rowLength = N*w; + for (int y = 0; y < h; ++y) { + memcpy(dst, src, sizeof(T)*rowLength); + dst += rowLength; + src += orig.rowStride; + } +} + +template +Bitmap::Bitmap(const Bitmap &orig) : w(orig.w), h(orig.h), yOrientation(orig.yOrientation) { + pixels = new T[N*w*h]; + memcpy(pixels, orig.pixels, sizeof(T)*N*w*h); +} + +#ifdef MSDFGEN_USE_CPP11 +template +Bitmap::Bitmap(Bitmap &&orig) : pixels(orig.pixels), w(orig.w), h(orig.h), yOrientation(orig.yOrientation) { + orig.pixels = NULL; + orig.w = 0, orig.h = 0; +} +#endif + +template +Bitmap::~Bitmap() { + delete[] pixels; +} + +template +Bitmap &Bitmap::operator=(const BitmapConstRef &orig) { + if (pixels != orig.pixels) { + delete[] pixels; + w = orig.width, h = orig.height; + yOrientation = orig.yOrientation; + pixels = new T[N*w*h]; + memcpy(pixels, orig.pixels, sizeof(T)*N*w*h); + } + return *this; +} + +template +Bitmap &Bitmap::operator=(const BitmapConstSection &orig) { + if (orig.pixels && orig.pixels >= pixels && orig.pixels < pixels+N*w*h) + return *this = Bitmap(orig); + delete[] pixels; + w = orig.width, h = orig.height; + yOrientation = orig.yOrientation; + pixels = new T[N*w*h]; + T *dst = pixels; + const T *src = orig.pixels; + int rowLength = N*w; + for (int y = 0; y < h; ++y) { + memcpy(dst, src, sizeof(T)*rowLength); + dst += rowLength; + src += orig.rowStride; + } + return *this; +} + +template +Bitmap &Bitmap::operator=(const Bitmap &orig) { + if (this != &orig) { + delete[] pixels; + w = orig.w, h = orig.h; + yOrientation = orig.yOrientation; + pixels = new T[N*w*h]; + memcpy(pixels, orig.pixels, sizeof(T)*N*w*h); + } + return *this; +} + +#ifdef MSDFGEN_USE_CPP11 +template +Bitmap &Bitmap::operator=(Bitmap &&orig) { + if (this != &orig) { + delete[] pixels; + pixels = orig.pixels; + w = orig.w, h = orig.h; + yOrientation = orig.yOrientation; + orig.pixels = NULL; + } + return *this; +} +#endif + +template +int Bitmap::width() const { + return w; +} + +template +int Bitmap::height() const { + return h; +} + +template +T *Bitmap::operator()(int x, int y) { + return pixels+N*(w*y+x); +} + +template +const T *Bitmap::operator()(int x, int y) const { + return pixels+N*(w*y+x); +} + +template +Bitmap::operator T *() { + return pixels; +} + +template +Bitmap::operator const T *() const { + return pixels; +} + +template +Bitmap::operator BitmapRef() { + return BitmapRef(pixels, w, h, yOrientation); +} + +template +Bitmap::operator BitmapConstRef() const { + return BitmapConstRef(pixels, w, h, yOrientation); +} + +template +Bitmap::operator BitmapSection() { + return BitmapSection(pixels, w, h, yOrientation); +} + +template +Bitmap::operator BitmapConstSection() const { + return BitmapConstSection(pixels, w, h, yOrientation); +} + +template +BitmapSection Bitmap::getSection(int xMin, int yMin, int xMax, int yMax) { + return BitmapSection(pixels+N*(w*yMin+xMin), xMax-xMin, yMax-yMin, N*w, yOrientation); +} + +template +BitmapConstSection Bitmap::getConstSection(int xMin, int yMin, int xMax, int yMax) const { + return BitmapConstSection(pixels+N*(w*yMin+xMin), xMax-xMin, yMax-yMin, N*w, yOrientation); +} + +} diff --git a/Source/ThirdParty/msdfgen/msdfgen/core/BitmapRef.hpp b/Source/ThirdParty/msdfgen/msdfgen/core/BitmapRef.hpp new file mode 100644 index 000000000..6e93fe983 --- /dev/null +++ b/Source/ThirdParty/msdfgen/msdfgen/core/BitmapRef.hpp @@ -0,0 +1,154 @@ + +#pragma once + +#include "YAxisOrientation.h" + +namespace msdfgen { + +/// Reference to a 2D image bitmap or a buffer acting as one. Pixel storage not owned or managed by the object. +template +struct BitmapRef; +/// Constant reference to a 2D image bitmap or a buffer acting as one. Pixel storage not owned or managed by the object. +template +struct BitmapConstRef; +/// Reference to a 2D image bitmap with non-contiguous rows of pixels. Pixel storage not owned or managed by the object. Can represent e.g. a section of a larger bitmap, bitmap with padded rows, or vertically flipped bitmap (rowStride can be negative). +template +struct BitmapSection; +/// Constant reference to a 2D image bitmap with non-contiguous rows of pixels. Pixel storage not owned or managed by the object. Can represent e.g. a section of a larger bitmap, bitmap with padded rows, or vertically flipped bitmap (rowStride can be negative). +template +struct BitmapConstSection; + +template +struct BitmapRef { + + T *pixels; + int width, height; + YAxisOrientation yOrientation; + + inline BitmapRef() : pixels(NULL), width(0), height(0), yOrientation(MSDFGEN_Y_AXIS_DEFAULT_ORIENTATION) { } + inline BitmapRef(T *pixels, int width, int height, YAxisOrientation yOrientation = MSDFGEN_Y_AXIS_DEFAULT_ORIENTATION) : pixels(pixels), width(width), height(height), yOrientation(yOrientation) { } + + inline T *operator()(int x, int y) const { + return pixels+N*(width*y+x); + } + + /// Returns a reference to a rectangular section of the bitmap specified by bounds (excluding xMax, yMax). + inline BitmapSection getSection(int xMin, int yMin, int xMax, int yMax) const { + return BitmapSection(pixels+N*(width*yMin+xMin), xMax-xMin, yMax-yMin, N*width, yOrientation); + } + + /// Returns a constant reference to a rectangular section of the bitmap specified by bounds (excluding xMax, yMax). + inline BitmapConstSection getConstSection(int xMin, int yMin, int xMax, int yMax) const { + return BitmapConstSection(pixels+N*(width*yMin+xMin), xMax-xMin, yMax-yMin, N*width, yOrientation); + } + +}; + +template +struct BitmapConstRef { + + const T *pixels; + int width, height; + YAxisOrientation yOrientation; + + inline BitmapConstRef() : pixels(NULL), width(0), height(0), yOrientation(MSDFGEN_Y_AXIS_DEFAULT_ORIENTATION) { } + inline BitmapConstRef(const T *pixels, int width, int height, YAxisOrientation yOrientation = MSDFGEN_Y_AXIS_DEFAULT_ORIENTATION) : pixels(pixels), width(width), height(height), yOrientation(yOrientation) { } + inline BitmapConstRef(const BitmapRef &orig) : pixels(orig.pixels), width(orig.width), height(orig.height), yOrientation(orig.yOrientation) { } + + inline const T *operator()(int x, int y) const { + return pixels+N*(width*y+x); + } + + /// Returns a constant reference to a rectangular section of the bitmap specified by bounds (excluding xMax, yMax). + inline BitmapConstSection getSection(int xMin, int yMin, int xMax, int yMax) const { + return BitmapConstSection(pixels+N*(width*yMin+xMin), xMax-xMin, yMax-yMin, N*width, yOrientation); + } + + /// Returns a constant reference to a rectangular section of the bitmap specified by bounds (excluding xMax, yMax). + inline BitmapConstSection getConstSection(int xMin, int yMin, int xMax, int yMax) const { + return getSection(xMin, yMin, xMax, yMax); + } + +}; + +template +struct BitmapSection { + + T *pixels; + int width, height; + /// Specifies the difference between the beginnings of adjacent pixel rows as the number of T elements, can be negative. + int rowStride; + YAxisOrientation yOrientation; + + inline BitmapSection() : pixels(NULL), width(0), height(0), rowStride(0), yOrientation(MSDFGEN_Y_AXIS_DEFAULT_ORIENTATION) { } + inline BitmapSection(T *pixels, int width, int height, YAxisOrientation yOrientation = MSDFGEN_Y_AXIS_DEFAULT_ORIENTATION) : pixels(pixels), width(width), height(height), rowStride(N*width), yOrientation(yOrientation) { } + inline BitmapSection(T *pixels, int width, int height, int rowStride, YAxisOrientation yOrientation = MSDFGEN_Y_AXIS_DEFAULT_ORIENTATION) : pixels(pixels), width(width), height(height), rowStride(rowStride), yOrientation(yOrientation) { } + inline BitmapSection(const BitmapRef &orig) : pixels(orig.pixels), width(orig.width), height(orig.height), rowStride(N*orig.width), yOrientation(orig.yOrientation) { } + + inline T *operator()(int x, int y) const { + return pixels+rowStride*y+N*x; + } + + /// Returns a reference to a rectangular subsection of the bitmap specified by bounds (excluding xMax, yMax). + inline BitmapSection getSection(int xMin, int yMin, int xMax, int yMax) const { + return BitmapSection(pixels+rowStride*yMin+N*xMin, xMax-xMin, yMax-yMin, rowStride, yOrientation); + } + + /// Returns a constant reference to a rectangular subsection of the bitmap specified by bounds (excluding xMax, yMax). + inline BitmapConstSection getConstSection(int xMin, int yMin, int xMax, int yMax) const { + return BitmapConstSection(pixels+rowStride*yMin+N*xMin, xMax-xMin, yMax-yMin, rowStride, yOrientation); + } + + /// Makes sure that the section's Y-axis orientation matches the argument by potentially reordering its rows. + inline void reorient(YAxisOrientation newYAxisOrientation) { + if (yOrientation != newYAxisOrientation) { + pixels += rowStride*(height-1); + rowStride = -rowStride; + yOrientation = newYAxisOrientation; + } + } + +}; + +template +struct BitmapConstSection { + + const T *pixels; + int width, height; + /// Specifies the difference between the beginnings of adjacent pixel rows as the number of T elements, can be negative. + int rowStride; + YAxisOrientation yOrientation; + + inline BitmapConstSection() : pixels(NULL), width(0), height(0), rowStride(0), yOrientation(MSDFGEN_Y_AXIS_DEFAULT_ORIENTATION) { } + inline BitmapConstSection(const T *pixels, int width, int height, YAxisOrientation yOrientation = MSDFGEN_Y_AXIS_DEFAULT_ORIENTATION) : pixels(pixels), width(width), height(height), rowStride(N*width), yOrientation(yOrientation) { } + inline BitmapConstSection(const T *pixels, int width, int height, int rowStride, YAxisOrientation yOrientation = MSDFGEN_Y_AXIS_DEFAULT_ORIENTATION) : pixels(pixels), width(width), height(height), rowStride(rowStride), yOrientation(yOrientation) { } + inline BitmapConstSection(const BitmapRef &orig) : pixels(orig.pixels), width(orig.width), height(orig.height), rowStride(N*orig.width), yOrientation(orig.yOrientation) { } + inline BitmapConstSection(const BitmapConstRef &orig) : pixels(orig.pixels), width(orig.width), height(orig.height), rowStride(N*orig.width), yOrientation(orig.yOrientation) { } + inline BitmapConstSection(const BitmapSection &orig) : pixels(orig.pixels), width(orig.width), height(orig.height), rowStride(orig.rowStride), yOrientation(orig.yOrientation) { } + + inline const T *operator()(int x, int y) const { + return pixels+rowStride*y+N*x; + } + + /// Returns a constant reference to a rectangular subsection of the bitmap specified by bounds (excluding xMax, yMax). + inline BitmapConstSection getSection(int xMin, int yMin, int xMax, int yMax) const { + return BitmapConstSection(pixels+rowStride*yMin+N*xMin, xMax-xMin, yMax-yMin, rowStride, yOrientation); + } + + /// Returns a constant reference to a rectangular subsection of the bitmap specified by bounds (excluding xMax, yMax). + inline BitmapConstSection getConstSection(int xMin, int yMin, int xMax, int yMax) const { + return getSection(xMin, yMin, xMax, yMax); + } + + /// Makes sure that the section's Y-axis orientation matches the argument by potentially reordering its rows. + inline void reorient(YAxisOrientation newYAxisOrientation) { + if (yOrientation != newYAxisOrientation) { + pixels += rowStride*(height-1); + rowStride = -rowStride; + yOrientation = newYAxisOrientation; + } + } + +}; + +} diff --git a/Source/ThirdParty/msdfgen/msdfgen/core/Contour.h b/Source/ThirdParty/msdfgen/msdfgen/core/Contour.h new file mode 100644 index 000000000..53ceb8eda --- /dev/null +++ b/Source/ThirdParty/msdfgen/msdfgen/core/Contour.h @@ -0,0 +1,34 @@ + +#pragma once + +#include +#include "EdgeHolder.h" + +namespace msdfgen { + +/// A single closed contour of a shape. +class Contour { + +public: + /// The sequence of edges that make up the contour. + std::vector edges; + + /// Adds an edge to the contour. + void addEdge(const EdgeHolder &edge); +#ifdef MSDFGEN_USE_CPP11 + void addEdge(EdgeHolder &&edge); +#endif + /// Creates a new edge in the contour and returns its reference. + EdgeHolder &addEdge(); + /// Adjusts the bounding box to fit the contour. + void bound(double &xMin, double &yMin, double &xMax, double &yMax) const; + /// Adjusts the bounding box to fit the contour border's mitered corners. + void boundMiters(double &xMin, double &yMin, double &xMax, double &yMax, double border, double miterLimit, int polarity) const; + /// Computes the winding of the contour. Returns 1 if positive, -1 if negative. + int winding() const; + /// Reverses the sequence of edges on the contour. + void reverse(); + +}; + +} diff --git a/Source/ThirdParty/msdfgen/msdfgen/core/DistanceMapping.h b/Source/ThirdParty/msdfgen/msdfgen/core/DistanceMapping.h new file mode 100644 index 000000000..fadbefa54 --- /dev/null +++ b/Source/ThirdParty/msdfgen/msdfgen/core/DistanceMapping.h @@ -0,0 +1,36 @@ + +#pragma once + +#include "Range.hpp" + +namespace msdfgen { + +/// Linear transformation of signed distance values. +class DistanceMapping { + +public: + /// Explicitly designates value as distance delta rather than an absolute distance. + class Delta { + public: + double value; + inline explicit Delta(double distanceDelta) : value(distanceDelta) { } + inline operator double() const { return value; } + }; + + static DistanceMapping inverse(Range range); + + DistanceMapping(); + DistanceMapping(Range range); + double operator()(double d) const; + double operator()(Delta d) const; + DistanceMapping inverse() const; + +private: + double scale; + double translate; + + inline DistanceMapping(double scale, double translate) : scale(scale), translate(translate) { } + +}; + +} diff --git a/Source/ThirdParty/msdfgen/msdfgen/core/EdgeColor.h b/Source/ThirdParty/msdfgen/msdfgen/core/EdgeColor.h new file mode 100644 index 000000000..5d3730c9a --- /dev/null +++ b/Source/ThirdParty/msdfgen/msdfgen/core/EdgeColor.h @@ -0,0 +1,20 @@ + +#pragma once + +#include "base.h" + +namespace msdfgen { + +/// Edge color specifies which color channels an edge belongs to. +enum EdgeColor { + BLACK = 0, + RED = 1, + GREEN = 2, + YELLOW = 3, + BLUE = 4, + MAGENTA = 5, + CYAN = 6, + WHITE = 7 +}; + +} diff --git a/Source/ThirdParty/msdfgen/msdfgen/core/EdgeHolder.h b/Source/ThirdParty/msdfgen/msdfgen/core/EdgeHolder.h new file mode 100644 index 000000000..5ae2dc231 --- /dev/null +++ b/Source/ThirdParty/msdfgen/msdfgen/core/EdgeHolder.h @@ -0,0 +1,41 @@ + +#pragma once + +#include "edge-segments.h" + +namespace msdfgen { + +/// Container for a single edge of dynamic type. +class EdgeHolder { + +public: + /// Swaps the edges held by a and b. + static void swap(EdgeHolder &a, EdgeHolder &b); + + inline EdgeHolder() : edgeSegment() { } + inline EdgeHolder(EdgeSegment *segment) : edgeSegment(segment) { } + inline EdgeHolder(Point2 p0, Point2 p1, EdgeColor edgeColor = WHITE) : edgeSegment(EdgeSegment::create(p0, p1, edgeColor)) { } + inline EdgeHolder(Point2 p0, Point2 p1, Point2 p2, EdgeColor edgeColor = WHITE) : edgeSegment(EdgeSegment::create(p0, p1, p2, edgeColor)) { } + inline EdgeHolder(Point2 p0, Point2 p1, Point2 p2, Point2 p3, EdgeColor edgeColor = WHITE) : edgeSegment(EdgeSegment::create(p0, p1, p2, p3, edgeColor)) { } + EdgeHolder(const EdgeHolder &orig); +#ifdef MSDFGEN_USE_CPP11 + EdgeHolder(EdgeHolder &&orig); +#endif + ~EdgeHolder(); + EdgeHolder &operator=(const EdgeHolder &orig); +#ifdef MSDFGEN_USE_CPP11 + EdgeHolder &operator=(EdgeHolder &&orig); +#endif + EdgeSegment &operator*(); + const EdgeSegment &operator*() const; + EdgeSegment *operator->(); + const EdgeSegment *operator->() const; + operator EdgeSegment *(); + operator const EdgeSegment *() const; + +private: + EdgeSegment *edgeSegment; + +}; + +} diff --git a/Source/ThirdParty/msdfgen/msdfgen/core/MSDFErrorCorrection.h b/Source/ThirdParty/msdfgen/msdfgen/core/MSDFErrorCorrection.h new file mode 100644 index 000000000..995b1d614 --- /dev/null +++ b/Source/ThirdParty/msdfgen/msdfgen/core/MSDFErrorCorrection.h @@ -0,0 +1,55 @@ + +#pragma once + +#include "SDFTransformation.h" +#include "Shape.h" +#include "BitmapRef.hpp" + +namespace msdfgen { + +/// Performs error correction on a computed MSDF to eliminate interpolation artifacts. This is a low-level class, you may want to use the API in msdf-error-correction.h instead. +class MSDFErrorCorrection { + +public: + /// Stencil flags. + enum Flags { + /// Texel marked as potentially causing interpolation errors. + ERROR = 1, + /// Texel marked as protected. Protected texels are only given the error flag if they cause inversion artifacts. + PROTECTED = 2 + }; + + MSDFErrorCorrection(); + explicit MSDFErrorCorrection(const BitmapSection &stencil, const SDFTransformation &transformation); + /// Sets the minimum ratio between the actual and maximum expected distance delta to be considered an error. + void setMinDeviationRatio(double minDeviationRatio); + /// Sets the minimum ratio between the pre-correction distance error and the post-correction distance error. + void setMinImproveRatio(double minImproveRatio); + /// Flags all texels that are interpolated at corners as protected. + void protectCorners(const Shape &shape); + /// Flags all texels that contribute to edges as protected. + template + void protectEdges(const BitmapConstSection &sdf); + /// Flags all texels as protected. + void protectAll(); + /// Flags texels that are expected to cause interpolation artifacts based on analysis of the SDF only. + template + void findErrors(const BitmapConstSection &sdf); + /// Flags texels that are expected to cause interpolation artifacts based on analysis of the SDF and comparison with the exact shape distance. + template