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/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 1579d3bd4..a77fe9485 100644 --- a/Flax.flaxproj +++ b/Flax.flaxproj @@ -4,7 +4,7 @@ "Major": 1, "Minor": 12, "Revision": 0, - "Build": 6907 + "Build": 6908 }, "Company": "Flax", "Copyright": "Copyright (c) 2012-2026 Wojciech Figat. All rights reserved.", diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs index 1a308e08a..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); @@ -824,7 +825,7 @@ namespace FlaxEditor.CustomEditors.Dedicated 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; 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..c1179e393 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.0235294f, 0.9f, 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/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.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/GUI/Input/ValueBox.cs b/Source/Editor/GUI/Input/ValueBox.cs index 8e3e4f4e7..68f5e8f03 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/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/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/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index a996fa628..5413a25b0 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -252,8 +252,8 @@ namespace FlaxEditor.Viewport { _movementSpeed = value; - if (_cameraButton != null) - _cameraButton.Text = string.Format(MovementSpeedTextFormat, _movementSpeed); + if (_orthographicModeButton != null) + _orthographicModeButton.Text = string.Format(MovementSpeedTextFormat, _movementSpeed); } } @@ -587,7 +587,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.", @@ -596,7 +596,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.", diff --git a/Source/Editor/Viewport/PrefabWindowViewport.cs b/Source/Editor/Viewport/PrefabWindowViewport.cs index 7f2b1a471..a8bdc8f20 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; 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 ec5db8301..6756cabf6 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-2026 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/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/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 0441e27a0..3fea63ceb 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/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/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/Engine/CommandLine.cpp b/Source/Engine/Engine/CommandLine.cpp index 4dad47971..6221d17bf 100644 --- a/Source/Engine/Engine/CommandLine.cpp +++ b/Source/Engine/Engine/CommandLine.cpp @@ -166,6 +166,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 1733afc1a..e60772bc1 100644 --- a/Source/Engine/Engine/CommandLine.h +++ b/Source/Engine/Engine/CommandLine.h @@ -148,6 +148,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 7a9ae0969..4b037db37 100644 --- a/Source/Engine/Engine/Engine.cpp +++ b/Source/Engine/Engine/Engine.cpp @@ -581,7 +581,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 efe89bf5c..459f1b662 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -22,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) @@ -360,7 +360,7 @@ void Foliage::DrawCluster(DrawContext& context, FoliageCluster* cluster, Mesh::D 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(context.RenderContext, draw); @@ -597,14 +597,22 @@ void Foliage::DrawType(RenderContext& renderContext, const FoliageType& type, Me void Foliage::InitType(const RenderView& view, FoliageType& type) { - const DrawPass drawModes = type.DrawModes & view.Pass & view.GetShadowsDrawPassMask(type.ShadowsMode); + 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 @@ -1250,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; 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 224ed0bd8..8b36f618b 100644 --- a/Source/Engine/Foliage/FoliageType.h +++ b/Source/Engine/Foliage/FoliageType.h @@ -48,6 +48,8 @@ API_CLASS(Sealed, NoSpawn) class FLAXENGINE_API FoliageType : public ScriptingOb private: uint8 _isReady : 1; uint8 _canDraw : 1; + uint8 _drawModesDirty : 1; + DrawPass _drawModes = DrawPass::Depth | DrawPass::GBuffer | DrawPass::Forward; public: /// @@ -124,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. @@ -184,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/Models/MeshBase.cpp b/Source/Engine/Graphics/Models/MeshBase.cpp index eadbfcba9..bbec523a2 100644 --- a/Source/Engine/Graphics/Models/MeshBase.cpp +++ b/Source/Engine/Graphics/Models/MeshBase.cpp @@ -158,6 +158,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++) 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 6860c3463..8b9bd40bd 100644 --- a/Source/Engine/Input/Input.cpp +++ b/Source/Engine/Input/Input.cpp @@ -121,6 +121,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 @@ -513,24 +514,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; } @@ -556,13 +557,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; } } @@ -570,18 +571,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; } } @@ -589,18 +590,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; } } @@ -608,7 +609,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; } @@ -1083,26 +1084,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 964a247d9..0f619c85f 100644 --- a/Source/Engine/Input/Input.h +++ b/Source/Engine/Input/Input.h @@ -243,24 +243,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. @@ -275,24 +278,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/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..19e4e3f34 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(); } } } diff --git a/Source/Engine/Level/Actors/StaticModel.h b/Source/Engine/Level/Actors/StaticModel.h index e6ed701cc..4b575a0ed 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. /// diff --git a/Source/Engine/Level/Level.cpp b/Source/Engine/Level/Level.cpp index a873ae734..629c44096 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" @@ -960,9 +961,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; } @@ -1125,6 +1123,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()) { @@ -1138,27 +1162,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; } @@ -1166,6 +1170,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/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/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 e642adbea..82a589390 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" @@ -79,6 +80,7 @@ enum class DrawCallType : byte FillTexture, FillTexturePoint, DrawChar, + DrawCharMSDF, DrawCharMaterial, Custom, Material, @@ -169,6 +171,7 @@ struct CachedPSO GPUPipelineState* PS_Color_NoAlpha; GPUPipelineState* PS_Font; + GPUPipelineState* PS_FontMSDF; GPUPipelineState* PS_BlurH; GPUPipelineState* PS_BlurV; @@ -461,6 +464,7 @@ CanDrawCallCallback CanDrawCallBatch[] = CanDrawCallCallbackTexture, // FillTexture, CanDrawCallCallbackTexture, // FillTexturePoint, CanDrawCallCallbackChar, // DrawChar, + CanDrawCallCallbackChar, // DrawCharMSDF, CanDrawCallCallbackCharMaterial, // DrawCharMaterial, CanDrawCallCallbackFalse, // Custom, CanDrawCallCallbackMaterial, // Material, @@ -523,6 +527,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)) @@ -1001,6 +1011,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 @@ -1193,7 +1207,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; @@ -1312,7 +1326,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++) @@ -1447,8 +1461,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/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 6d2dde57d..5b109fd55 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/Variant.h" #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Core/Math/Vector2.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.cpp b/Source/Engine/Terrain/Terrain.cpp index 77b4ee8a6..f0ad65894 100644 --- a/Source/Engine/Terrain/Terrain.cpp +++ b/Source/Engine/Terrain/Terrain.cpp @@ -549,19 +549,22 @@ bool Terrain::DrawSetup(RenderContext& renderContext) const DrawPass drawModes = DrawModes & renderContext.View.Pass; if (drawModes == DrawPass::GlobalSDF) { - const float chunkSize = TERRAIN_UNITS_PER_VERTEX * (float)_chunkSize; - const float posToUV = 0.25f / chunkSize; - Float4 localToUV(posToUV, posToUV, 0.0f, 0.0f); + const float chunkScale = 0.25f / (TERRAIN_UNITS_PER_VERTEX * (float)_chunkSize); // Patch heightfield is divided into 4x4 chunks for (const TerrainPatch* patch : _patches) { if (!patch->Heightmap) 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/Tools/ModelTool/MeshAccelerationStructure.cpp b/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp index 6e1b5ae6f..297a6f868 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,25 +336,19 @@ 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()); - } + 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 = mesh.Use16BitIndexBuffer(); meshData.Bounds = mesh.GetBox(); } diff --git a/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp b/Source/Engine/Tools/TextureTool/TextureTool.DirectXTex.cpp index 3250242ec..c046a4b8d 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 3d3e38d55..28aa1e9f7 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp +++ b/Source/Engine/Tools/TextureTool/TextureTool.stb.cpp @@ -627,6 +627,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..6488bb14a 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 = TextColorHighlighted.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/TextBoxBase.cs b/Source/Engine/UI/GUI/Common/TextBoxBase.cs index 489e4c93d..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,6 +227,16 @@ 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. /// @@ -395,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(); } /// 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/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/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/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 f28c087fc..809ed94f0 100644 --- a/Source/Shaders/Common.hlsl +++ b/Source/Shaders/Common.hlsl @@ -159,6 +159,7 @@ float4 LoadTextureWGSL(Texture2D tex, float2 uv) #endif #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 16f961839..dfce289e3 100644 --- a/Source/Shaders/GUI.shader +++ b/Source/Shaders/GUI.shader @@ -89,7 +89,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 1aca05862..a96d2c7de 100644 --- a/Source/Shaders/GUICommon.hlsl +++ b/Source/Shaders/GUICommon.hlsl @@ -69,4 +69,52 @@ float4 GetImageColor(float4 color, float2 customData) return color; } +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/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