From feca1c7994886dec467491c51cd42f9064126c52 Mon Sep 17 00:00:00 2001 From: Saas Date: Tue, 3 Mar 2026 23:44:24 +0100 Subject: [PATCH 01/24] replace HW with material instead of sprite --- Source/Editor/EditorAssets.cs | 5 +++++ Source/Editor/EditorIcons.cs | 1 - Source/Editor/GUI/Dialogs/ColorSelector.cs | 12 +++++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/Source/Editor/EditorAssets.cs b/Source/Editor/EditorAssets.cs index d9b3d7f18..447dc3375 100644 --- a/Source/Editor/EditorAssets.cs +++ b/Source/Editor/EditorAssets.cs @@ -69,6 +69,11 @@ namespace FlaxEditor /// public static string WindowIcon = "Editor/EditorIcon"; + /// + /// The material used for the HS color wheel. + /// + public static string HSWheelMaterial = "Editor/HSWheel"; + /// /// The window icons font. /// diff --git a/Source/Editor/EditorIcons.cs b/Source/Editor/EditorIcons.cs index 8688c502a..7da1d6fb3 100644 --- a/Source/Editor/EditorIcons.cs +++ b/Source/Editor/EditorIcons.cs @@ -134,7 +134,6 @@ namespace FlaxEditor public SpriteHandle Document128; public SpriteHandle XBoxOne128; public SpriteHandle UWPStore128; - public SpriteHandle ColorWheel128; public SpriteHandle LinuxSettings128; public SpriteHandle NavigationSettings128; public SpriteHandle AudioSettings128; diff --git a/Source/Editor/GUI/Dialogs/ColorSelector.cs b/Source/Editor/GUI/Dialogs/ColorSelector.cs index f556e8cd6..79e4029f5 100644 --- a/Source/Editor/GUI/Dialogs/ColorSelector.cs +++ b/Source/Editor/GUI/Dialogs/ColorSelector.cs @@ -12,6 +12,8 @@ namespace FlaxEditor.GUI.Dialogs /// public class ColorSelector : ContainerControl { + private const String GrayedOutParamName = "GrayedOut"; + /// /// The color. /// @@ -22,7 +24,7 @@ namespace FlaxEditor.GUI.Dialogs /// protected Rectangle _wheelRect; - private readonly SpriteHandle _colorWheelSprite; + private readonly MaterialBase _hsWheelMaterial; private bool _isMouseDownWheel; /// @@ -78,7 +80,8 @@ namespace FlaxEditor.GUI.Dialogs { AutoFocus = true; - _colorWheelSprite = Editor.Instance.Icons.ColorWheel128; + _hsWheelMaterial = FlaxEngine.Content.LoadAsyncInternal(EditorAssets.HSWheelMaterial); + _hsWheelMaterial = _hsWheelMaterial.CreateVirtualInstance(); _wheelRect = new Rectangle(0, 0, wheelSize, wheelSize); } @@ -168,9 +171,12 @@ namespace FlaxEditor.GUI.Dialogs var hsv = _color.ToHSV(); bool enabled = EnabledInHierarchy; + _hsWheelMaterial.SetParameterValue(GrayedOutParamName, enabled ? 1.0f : 0.5f); + Render2D.DrawMaterial(_hsWheelMaterial, _wheelRect, enabled ? Color.White : Color.Gray); + // Wheel float boxExpand = (2.0f * 4.0f / 128.0f) * _wheelRect.Width; - Render2D.DrawSprite(_colorWheelSprite, _wheelRect.MakeExpanded(boxExpand), enabled ? Color.White : Color.Gray); + Render2D.DrawMaterial(_hsWheelMaterial, _wheelRect, enabled ? Color.White : Color.Gray); float hAngle = hsv.X * Mathf.DegreesToRadians; float hRadius = hsv.Y * _wheelRect.Width * 0.5f; var hsPos = new Float2(hRadius * Mathf.Cos(hAngle), -hRadius * Mathf.Sin(hAngle)); From b9b11b3c2a967f91dc03645bf2db5ce12e664261 Mon Sep 17 00:00:00 2001 From: Saas Date: Tue, 3 Mar 2026 23:49:16 +0100 Subject: [PATCH 02/24] loop Hue value box --- Source/Editor/GUI/Dialogs/ColorPickerDialog.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs index 2d6a3882b..46970c50e 100644 --- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs +++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs @@ -160,7 +160,7 @@ namespace FlaxEditor.GUI.Dialogs _cAlpha.ValueChanged += OnRGBAChanged; // Hue - _cHue = new FloatValueBox(0, PickerMargin + HSVMargin + ChannelTextWidth, _cSelector.Bottom + PickerMargin, 100, 0, 360) + _cHue = new FloatValueBox(0, PickerMargin + HSVMargin + ChannelTextWidth, _cSelector.Bottom + PickerMargin, 100) { Parent = this }; @@ -306,6 +306,7 @@ namespace FlaxEditor.GUI.Dialogs if (_disableEvents) return; + _cHue.Value = Mathf.Wrap(_cHue.Value, 0f, 360f); SelectedColor = Color.FromHSV(_cHue.Value, _cSaturation.Value / 100.0f, _cValue.Value / 100.0f, _cAlpha.Value); } From 66b4c64f989802ee6e599dbd0d5f8df4415a3cb6 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 4 Mar 2026 15:17:47 +0100 Subject: [PATCH 03/24] polish sliders and wheel - Hide mouse cursor when clicking on wheel or sliders - Move mouse position to knob position when letting go of slider or wheel - Show mouse again when letting go of slider or wheel (obviously) - Provide some visual feedback when the clicks on the wheel or sliders - Make sliders wider - Add alpha grid background to alpha slider --- .../Editor/GUI/Dialogs/ColorPickerDialog.cs | 4 +- Source/Editor/GUI/Dialogs/ColorSelector.cs | 146 ++++++++++++------ 2 files changed, 103 insertions(+), 47 deletions(-) diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs index 46970c50e..f528710cd 100644 --- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs +++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs @@ -124,7 +124,7 @@ namespace FlaxEditor.GUI.Dialogs _savedColors = JsonSerializer.Deserialize>(savedColors); // Selector - _cSelector = new ColorSelectorWithSliders(180, 18) + _cSelector = new ColorSelectorWithSliders(180, 21) { Location = new Float2(PickerMargin, PickerMargin), Parent = this @@ -421,9 +421,7 @@ namespace FlaxEditor.GUI.Dialogs public override bool OnMouseUp(Float2 location, MouseButton button) { if (base.OnMouseUp(location, button)) - { return true; - } var child = GetChildAtRecursive(location); if (button == MouseButton.Right && child is Button b && b.Tag is Color c) diff --git a/Source/Editor/GUI/Dialogs/ColorSelector.cs b/Source/Editor/GUI/Dialogs/ColorSelector.cs index 79e4029f5..392a8d934 100644 --- a/Source/Editor/GUI/Dialogs/ColorSelector.cs +++ b/Source/Editor/GUI/Dialogs/ColorSelector.cs @@ -14,6 +14,11 @@ namespace FlaxEditor.GUI.Dialogs { private const String GrayedOutParamName = "GrayedOut"; + /// + /// Offset value applied to mouse cursor position when the user lets go of wheel or sliders. + /// + protected const float MouseCursorOffset = 6.0f; + /// /// The color. /// @@ -27,6 +32,19 @@ namespace FlaxEditor.GUI.Dialogs private readonly MaterialBase _hsWheelMaterial; private bool _isMouseDownWheel; + private Rectangle wheelDragRect + { + get + { + var hsv = _color.ToHSV(); + float hAngle = hsv.X * Mathf.DegreesToRadians; + float hRadius = hsv.Y * _wheelRect.Width * 0.5f; + var hsPos = new Float2(hRadius * Mathf.Cos(hAngle), -hRadius * Mathf.Sin(hAngle)); + float wheelBoxSize = IsSliding ? 9.0f : 5.0f; + return new Rectangle(hsPos - (wheelBoxSize * 0.5f) + _wheelRect.Center, new Float2(wheelBoxSize)); + } + } + /// /// Occurs when selected color gets changed. /// @@ -168,7 +186,6 @@ namespace FlaxEditor.GUI.Dialogs { base.Draw(); - var hsv = _color.ToHSV(); bool enabled = EnabledInHierarchy; _hsWheelMaterial.SetParameterValue(GrayedOutParamName, enabled ? 1.0f : 0.5f); @@ -177,11 +194,10 @@ namespace FlaxEditor.GUI.Dialogs // Wheel float boxExpand = (2.0f * 4.0f / 128.0f) * _wheelRect.Width; Render2D.DrawMaterial(_hsWheelMaterial, _wheelRect, enabled ? Color.White : Color.Gray); - float hAngle = hsv.X * Mathf.DegreesToRadians; - float hRadius = hsv.Y * _wheelRect.Width * 0.5f; - var hsPos = new Float2(hRadius * Mathf.Cos(hAngle), -hRadius * Mathf.Sin(hAngle)); - const float wheelBoxSize = 4.0f; - Render2D.DrawRectangle(new Rectangle(hsPos - (wheelBoxSize * 0.5f) + _wheelRect.Center, new Float2(wheelBoxSize)), _isMouseDownWheel ? Color.Gray : Color.Black); + Color hsColor = Color.FromHSV(new Float3(Color.ToHSV().X, 1, 1)); + Rectangle wheelRect = wheelDragRect; + Render2D.FillRectangle(wheelRect, hsColor); + Render2D.DrawRectangle(wheelRect, _isMouseDownWheel ? Color.Gray : Color.Black, _isMouseDownWheel ? 3.0f : 1.0f); } /// @@ -208,6 +224,7 @@ namespace FlaxEditor.GUI.Dialogs if (!_isMouseDownWheel) { _isMouseDownWheel = true; + Cursor = CursorType.Hidden; StartMouseCapture(); SlidingStart?.Invoke(); } @@ -224,6 +241,10 @@ namespace FlaxEditor.GUI.Dialogs if (button == MouseButton.Left && _isMouseDownWheel) { EndMouseCapture(); + // Make the cursor appear where the user expects it to be (position of selection rectangle) + Rectangle dragRect = wheelDragRect; + Root.MousePosition = dragRect.Center + MouseCursorOffset; + Cursor = CursorType.Default; EndSliding(); return true; } @@ -252,10 +273,10 @@ namespace FlaxEditor.GUI.Dialogs /// public class ColorSelectorWithSliders : ColorSelector { - private Rectangle _slider1Rect; - private Rectangle _slider2Rect; - private bool _isMouseDownSlider1; - private bool _isMouseDownSlider2; + private Rectangle _valueSliderRect; + private Rectangle _alphaSliderRect; + private bool _isMouseDownValueSlider; + private bool _isMouseDownAlphaSlider; /// /// Initializes a new instance of the class. @@ -266,26 +287,26 @@ namespace FlaxEditor.GUI.Dialogs : base(wheelSize) { // Setup dimensions - const float slidersMargin = 8.0f; - _slider1Rect = new Rectangle(wheelSize + slidersMargin, 0, slidersThickness, wheelSize); - _slider2Rect = new Rectangle(_slider1Rect.Right + slidersMargin, _slider1Rect.Y, slidersThickness, _slider1Rect.Height); - Size = new Float2(_slider2Rect.Right, wheelSize); + const float slidersMargin = 10.0f; + _valueSliderRect = new Rectangle(wheelSize + slidersMargin, 0, slidersThickness, wheelSize); + _alphaSliderRect = new Rectangle(_valueSliderRect.Right + slidersMargin * 1.5f, _valueSliderRect.Y, slidersThickness, _valueSliderRect.Height); + Size = new Float2(_alphaSliderRect.Right, wheelSize); } /// protected override void UpdateMouse(ref Float2 location) { - if (_isMouseDownSlider1) + if (_isMouseDownValueSlider) { var hsv = _color.ToHSV(); - hsv.Z = 1.0f - Mathf.Saturate((location.Y - _slider1Rect.Y) / _slider1Rect.Height); + hsv.Z = 1.0f - Mathf.Saturate((location.Y - _valueSliderRect.Y) / _valueSliderRect.Height); Color = Color.FromHSV(hsv, _color.A); } - else if (_isMouseDownSlider2) + else if (_isMouseDownAlphaSlider) { var color = _color; - color.A = 1.0f - Mathf.Saturate((location.Y - _slider2Rect.Y) / _slider2Rect.Height); + color.A = 1.0f - Mathf.Saturate((location.Y - _alphaSliderRect.Y) / _alphaSliderRect.Height); Color = color; } @@ -306,32 +327,61 @@ namespace FlaxEditor.GUI.Dialogs var hs = hsv; hs.Z = 1.0f; Color hsC = Color.FromHSV(hs); - const float slidersOffset = 3.0f; - const float slidersThickness = 4.0f; - // Value - float valueY = _slider2Rect.Height * (1 - hsv.Z); - var valueR = new Rectangle(_slider1Rect.X - slidersOffset, _slider1Rect.Y + valueY - slidersThickness / 2, _slider1Rect.Width + slidersOffset * 2, slidersThickness); - Render2D.FillRectangle(_slider1Rect, hsC, hsC, Color.Black, Color.Black); - Render2D.DrawRectangle(_slider1Rect, _isMouseDownSlider1 ? style.BackgroundSelected : Color.Black); - Render2D.DrawRectangle(valueR, _isMouseDownSlider1 ? Color.White : Color.Gray); + // Value slider + float valueKnobExpand = _isMouseDownValueSlider ? 10.0f : 4.0f; + float valueY = _valueSliderRect.Height * (1 - hsv.Z); + float valueKnobWidth = _valueSliderRect.Width + valueKnobExpand; + float valueKnobHeight = _isMouseDownValueSlider ? 7.0f : 4.0f; + float valueKnobX = _valueSliderRect.X - valueKnobExpand * 0.5f; + float valueKnobY = _valueSliderRect.Y + valueY - valueKnobHeight * 0.5f; + Rectangle valueKnobRect = new Rectangle(valueKnobX, valueKnobY, valueKnobWidth, valueKnobHeight); + Render2D.FillRectangle(_valueSliderRect, hsC, hsC, Color.Black, Color.Black); + // Draw one black and one white border to make the knob visible at any saturation level + Render2D.DrawRectangle(valueKnobRect, Color.White, _isMouseDownValueSlider ? 3.0f : 2.0f); + Render2D.DrawRectangle(valueKnobRect, Color.Black, _isMouseDownValueSlider ? 2.0f : 1.0f); - // Alpha - float alphaY = _slider2Rect.Height * (1 - _color.A); - var alphaR = new Rectangle(_slider2Rect.X - slidersOffset, _slider2Rect.Y + alphaY - slidersThickness / 2, _slider2Rect.Width + slidersOffset * 2, slidersThickness); + // Draw checkerboard pattern as background of alpha slider + Render2D.FillRectangle(_alphaSliderRect, Color.White); + var smallRectSize = _alphaSliderRect.Width / 2.0f; + var numHor = Mathf.CeilToInt(_alphaSliderRect.Width / smallRectSize); + var numVer = Mathf.CeilToInt(_alphaSliderRect.Height / smallRectSize); + Render2D.PushClip(_alphaSliderRect); + for (int i = 0; i < numHor; i++) + { + for (int j = 0; j < numVer; j++) + { + if ((i + j) % 2 == 0) + { + var rect = new Rectangle(_alphaSliderRect.X + smallRectSize * i, _alphaSliderRect.Y + smallRectSize * j, new Float2(smallRectSize)); + Render2D.FillRectangle(rect, Color.Gray); + } + } + } + Render2D.PopClip(); + + // Alpha slider + float alphaKnobExpand = _isMouseDownAlphaSlider ? 10.0f : 4.0f; + float alphaY = _alphaSliderRect.Height * (1 - _color.A); + float alphaKnobWidth = _alphaSliderRect.Width + alphaKnobExpand; + float alphaKnobHeight = _isMouseDownAlphaSlider ? 7.0f : 4.0f; + float alphaKnobX = _alphaSliderRect.X - alphaKnobExpand * 0.5f; + float alphaKnobY = _alphaSliderRect.Y + alphaY - alphaKnobExpand * 0.5f; + Rectangle alphaKnobRect = new Rectangle(alphaKnobX, alphaKnobY, alphaKnobWidth, alphaKnobHeight); var color = _color; - color.A = 1; // Keep slider 2 fill rect from changing color alpha while selecting. - Render2D.FillRectangle(_slider2Rect, color, color, Color.Transparent, Color.Transparent); - Render2D.DrawRectangle(_slider2Rect, _isMouseDownSlider2 ? style.BackgroundSelected : Color.Black); - Render2D.DrawRectangle(alphaR, _isMouseDownSlider2 ? Color.White : Color.Gray); + color.A = 1; // Prevent alpha slider fill from becoming transparent + Render2D.FillRectangle(_alphaSliderRect, color, color, Color.Transparent, Color.Transparent); + // Draw one black and one white border to make the knob visible at any saturation level + Render2D.DrawRectangle(alphaKnobRect, Color.White, _isMouseDownAlphaSlider ? 3.0f : 2.0f); + Render2D.DrawRectangle(alphaKnobRect, Color.Black, _isMouseDownAlphaSlider ? 2.0f : 1.0f); } /// public override void OnLostFocus() { // Clear flags - _isMouseDownSlider1 = false; - _isMouseDownSlider2 = false; + _isMouseDownValueSlider = false; + _isMouseDownAlphaSlider = false; base.OnLostFocus(); } @@ -339,15 +389,17 @@ namespace FlaxEditor.GUI.Dialogs /// public override bool OnMouseDown(Float2 location, MouseButton button) { - if (button == MouseButton.Left && _slider1Rect.Contains(location)) + if (button == MouseButton.Left && _valueSliderRect.Contains(location)) { - _isMouseDownSlider1 = true; + _isMouseDownValueSlider = true; + Cursor = CursorType.Hidden; StartMouseCapture(); UpdateMouse(ref location); } - if (button == MouseButton.Left && _slider2Rect.Contains(location)) + if (button == MouseButton.Left && _alphaSliderRect.Contains(location)) { - _isMouseDownSlider2 = true; + _isMouseDownAlphaSlider = true; + Cursor = CursorType.Hidden; StartMouseCapture(); UpdateMouse(ref location); } @@ -358,10 +410,16 @@ namespace FlaxEditor.GUI.Dialogs /// public override bool OnMouseUp(Float2 location, MouseButton button) { - if (button == MouseButton.Left && (_isMouseDownSlider1 || _isMouseDownSlider2)) + if (button == MouseButton.Left && (_isMouseDownValueSlider || _isMouseDownAlphaSlider)) { - _isMouseDownSlider1 = false; - _isMouseDownSlider2 = false; + // Make the cursor appear where the user expects it to be (center of slider horizontally and slider knob position vertically) + float sliderCenter = _isMouseDownValueSlider ? _valueSliderRect.Center.X : _alphaSliderRect.Center.X; + // Calculate y position based on the slider knob to avoid incrementing value by a small amount when the user repeatedly clicks the slider (f.e. when moving in small steps) + float mouseSliderPosition = _isMouseDownValueSlider ? _valueSliderRect.Y + _valueSliderRect.Height * (1 - _color.ToHSV().Z) + MouseCursorOffset : _alphaSliderRect.Y + _alphaSliderRect.Height * (1 - _color.A) + MouseCursorOffset; + Root.MousePosition = new Float2(sliderCenter + MouseCursorOffset, Mathf.Clamp(mouseSliderPosition, _valueSliderRect.Top, _valueSliderRect.Bottom)); + Cursor = CursorType.Default; + _isMouseDownValueSlider = false; + _isMouseDownAlphaSlider = false; EndMouseCapture(); return true; } @@ -373,8 +431,8 @@ namespace FlaxEditor.GUI.Dialogs public override void OnEndMouseCapture() { // Clear flags - _isMouseDownSlider1 = false; - _isMouseDownSlider2 = false; + _isMouseDownValueSlider = false; + _isMouseDownAlphaSlider = false; base.OnEndMouseCapture(); } From c62b3f7624b248eb64a2366809e33ad0f813169b Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 4 Mar 2026 17:53:27 +0100 Subject: [PATCH 04/24] improve color picker layout and accept color behaviour - Color will accept on close dialog or focus loss - Color will cancel on ESC press - Increased saved colors count from 8 to 10 - Moved RGB and HSV into tabbed interface - Moved eyedropper icon - Removed "Auto Accept Color Picker Change" option since there are no "Cancel" or "OK" button anymore - Made sure to reset cursor type when the color picker is closed --- .../Editor/GUI/Dialogs/ColorPickerDialog.cs | 257 ++++++++++-------- Source/Editor/Options/InterfaceOptions.cs | 7 - 2 files changed, 150 insertions(+), 114 deletions(-) diff --git a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs index f528710cd..ab002505f 100644 --- a/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs +++ b/Source/Editor/GUI/Dialogs/ColorPickerDialog.cs @@ -1,10 +1,11 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System.Collections.Generic; using FlaxEditor.GUI.Input; +using FlaxEditor.GUI.Tabs; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Json; -using System.Collections.Generic; namespace FlaxEditor.GUI.Dialogs { @@ -25,15 +26,16 @@ namespace FlaxEditor.GUI.Dialogs /// public class ColorPickerDialog : Dialog, IColorPickerDialog { - private const float ButtonsWidth = 60.0f; private const float PickerMargin = 6.0f; - private const float EyedropperMargin = 8.0f; - private const float RGBAMargin = 12.0f; - private const float HSVMargin = 0.0f; private const float ChannelsMargin = 4.0f; private const float ChannelTextWidth = 12.0f; private const float SavedColorButtonWidth = 20.0f; private const float SavedColorButtonHeight = 20.0f; + private const float TabHeight = 20; + private const float ValueBoxesWidth = 100.0f; + private const float HSVRGBTextWidth = 15.0f; + private const float ColorPreviewHeight = 50.0f; + private const int SavedColorsAmount = 10; private Color _initialValue; private Color _value; @@ -45,16 +47,19 @@ namespace FlaxEditor.GUI.Dialogs private ColorValueBox.ColorPickerClosedEvent _onClosed; private ColorSelectorWithSliders _cSelector; + private Tabs.Tabs _hsvRGBTabs; + private Tab _RGBTab; + private Panel _rgbPanel; private FloatValueBox _cRed; private FloatValueBox _cGreen; private FloatValueBox _cBlue; - private FloatValueBox _cAlpha; + private Tab _hsvTab; + private Panel _hsvPanel; private FloatValueBox _cHue; private FloatValueBox _cSaturation; private FloatValueBox _cValue; private TextBox _cHex; - private Button _cCancel; - private Button _cOK; + private FloatValueBox _cAlpha; private Button _cEyedropper; private List _savedColors = new List(); @@ -131,90 +136,103 @@ namespace FlaxEditor.GUI.Dialogs }; _cSelector.ColorChanged += x => SelectedColor = x; - // Red - _cRed = new FloatValueBox(0, _cSelector.Right + PickerMargin + RGBAMargin + ChannelTextWidth, PickerMargin, 100, 0, float.MaxValue, 0.001f) + _hsvRGBTabs = new Tabs.Tabs { - Parent = this + Location = new Float2(_cSelector.Right + 30.0f, PickerMargin), + TabsTextHorizontalAlignment = TextAlignment.Center, + Width = ValueBoxesWidth + HSVRGBTextWidth * 2.0f + ChannelsMargin * 2.0f, + Height = (FloatValueBox.DefaultHeight + ChannelsMargin) * 4 + ChannelsMargin, + Parent = this, + }; + _hsvRGBTabs.TabsSize = new Float2(_hsvRGBTabs.Width * 0.5f, TabHeight); + _hsvRGBTabs.SelectedTabChanged += SelectedTabChanged; + + // RGB Tab + _RGBTab = _hsvRGBTabs.AddTab(new Tab("RGB")); + _rgbPanel = new Panel(ScrollBars.Vertical) + { + AnchorPreset = AnchorPresets.StretchAll, + Offsets = Margin.Zero, + Parent = _RGBTab, + }; + + // HSV Tab + _hsvTab = _hsvRGBTabs.AddTab(new Tab("HSV")); + _hsvPanel = new Panel(ScrollBars.Vertical) + { + AnchorPreset = AnchorPresets.StretchAll, + Offsets = Margin.Zero, + Parent = _hsvTab, + }; + + // Red + _cRed = new FloatValueBox(0, HSVRGBTextWidth + ChannelsMargin, PickerMargin, ValueBoxesWidth, 0, float.MaxValue, 0.001f) + { + Parent = _rgbPanel, }; _cRed.ValueChanged += OnRGBAChanged; // Green _cGreen = new FloatValueBox(0, _cRed.X, _cRed.Bottom + ChannelsMargin, _cRed.Width, 0, float.MaxValue, 0.001f) { - Parent = this + Parent = _rgbPanel, }; _cGreen.ValueChanged += OnRGBAChanged; // Blue _cBlue = new FloatValueBox(0, _cRed.X, _cGreen.Bottom + ChannelsMargin, _cRed.Width, 0, float.MaxValue, 0.001f) { - Parent = this + Parent = _rgbPanel, }; _cBlue.ValueChanged += OnRGBAChanged; - // Alpha - _cAlpha = new FloatValueBox(0, _cRed.X, _cBlue.Bottom + ChannelsMargin, _cRed.Width, 0, float.MaxValue, 0.001f) - { - Parent = this - }; - _cAlpha.ValueChanged += OnRGBAChanged; - // Hue - _cHue = new FloatValueBox(0, PickerMargin + HSVMargin + ChannelTextWidth, _cSelector.Bottom + PickerMargin, 100) + _cHue = new FloatValueBox(0, HSVRGBTextWidth + ChannelsMargin, PickerMargin, ValueBoxesWidth) { - Parent = this + Parent = _hsvPanel, + Category = Utils.ValueCategory.Angle, }; _cHue.ValueChanged += OnHSVChanged; // Saturation _cSaturation = new FloatValueBox(0, _cHue.X, _cHue.Bottom + ChannelsMargin, _cHue.Width, 0, 100.0f, 0.1f) { - Parent = this + Parent = _hsvPanel, }; _cSaturation.ValueChanged += OnHSVChanged; // Value _cValue = new FloatValueBox(0, _cHue.X, _cSaturation.Bottom + ChannelsMargin, _cHue.Width, 0, float.MaxValue, 0.1f) { - Parent = this + Parent = _hsvPanel, }; _cValue.ValueChanged += OnHSVChanged; - // Set valid dialog size based on UI content - _dialogSize = Size = new Float2(_cRed.Right + PickerMargin, 300); + // Alpha + _cAlpha = new FloatValueBox(0, _hsvRGBTabs.Left + HSVRGBTextWidth + ChannelsMargin, _hsvRGBTabs.Bottom + ChannelsMargin * 4.0f, ValueBoxesWidth, 0, float.MaxValue, 0.001f) + { + Parent = this, + }; + _cAlpha.ValueChanged += OnRGBAChanged; // Hex - const float hexTextBoxWidth = 80; - _cHex = new TextBox(false, Width - hexTextBoxWidth - PickerMargin, _cSelector.Bottom + PickerMargin, hexTextBoxWidth) + _cHex = new TextBox(false, _hsvRGBTabs.Left + HSVRGBTextWidth + ChannelsMargin, _cAlpha.Bottom + ChannelsMargin * 2.0f, ValueBoxesWidth) { - Parent = this + Parent = this, }; _cHex.EditEnd += OnHexChanged; - // Cancel - _cCancel = new Button(Width - ButtonsWidth - PickerMargin, Height - Button.DefaultHeight - PickerMargin, ButtonsWidth) - { - Text = "Cancel", - Parent = this - }; - _cCancel.Clicked += OnCancel; - - // OK - _cOK = new Button(_cCancel.Left - ButtonsWidth - PickerMargin, _cCancel.Y, ButtonsWidth) - { - Text = "Ok", - Parent = this - }; - _cOK.Clicked += OnSubmit; + // Set valid dialog size based on UI content + _dialogSize = Size = new Float2(_hsvRGBTabs.Right + PickerMargin, _cHex.Bottom + 40.0f + ColorPreviewHeight + PickerMargin); // Create saved color buttons - CreateAllSaveButtons(); + CreateAllSavedColorsButtons(); // Eyedropper button var style = Style.Current; - _cEyedropper = new Button(_cOK.X - EyedropperMargin, _cHex.Bottom + PickerMargin) + _cEyedropper = new Button(_cSelector.BottomLeft.X, _cSelector.BottomLeft.Y - 25.0f, 25.0f, 25.0f) { - TooltipText = "Eyedropper tool to pick a color directly from the screen", + TooltipText = "Eyedropper tool to pick a color directly from the screen.", BackgroundBrush = new SpriteBrush(Editor.Instance.Icons.Search32), BackgroundColor = style.Foreground, BackgroundColorHighlighted = style.Foreground.RGBMultiplied(0.9f), @@ -223,9 +241,6 @@ namespace FlaxEditor.GUI.Dialogs Parent = this, }; _cEyedropper.Clicked += OnEyedropStart; - _cEyedropper.Height = (_cValue.Bottom - _cEyedropper.Y) * 0.5f; - _cEyedropper.Width = _cEyedropper.Height; - _cEyedropper.X -= _cEyedropper.Width; // Set initial color SelectedColor = initialValue; @@ -244,7 +259,7 @@ namespace FlaxEditor.GUI.Dialogs } } - // Set color of button to current value; + // Set color of button to current value button.BackgroundColor = _value; button.BackgroundColorHighlighted = _value; button.BackgroundColorSelected = _value.RGBMultiplied(0.8f); @@ -256,10 +271,10 @@ namespace FlaxEditor.GUI.Dialogs var savedColors = JsonSerializer.Serialize(_savedColors, typeof(List)); Editor.Instance.ProjectCache.SetCustomData("ColorPickerSavedColors", savedColors); - // create new + button - if (_savedColorButtons.Count < 8) + // Create new + button + if (_savedColorButtons.Count < SavedColorsAmount) { - var savedColorButton = new Button(PickerMargin * (_savedColorButtons.Count + 1) + SavedColorButtonWidth * _savedColorButtons.Count, Height - SavedColorButtonHeight - PickerMargin, SavedColorButtonWidth, SavedColorButtonHeight) + var savedColorButton = new Button(PickerMargin * (_savedColorButtons.Count + 1) + SavedColorButtonWidth * _savedColorButtons.Count, _cHex.Bottom + 40.0f + ColorPreviewHeight * 0.5f, SavedColorButtonWidth, SavedColorButtonHeight) { Text = "+", Parent = this, @@ -276,11 +291,32 @@ namespace FlaxEditor.GUI.Dialogs } } + private void SelectedTabChanged(Tabs.Tabs tabs) + { + if (_rgbPanel == null || _hsvPanel == null) + return; + + switch (tabs.SelectedTabIndex) + { + // RGB + case 0: + _rgbPanel.Visible = true; + _hsvPanel.Visible = false; + break; + // HSV + case 1: + _rgbPanel.Visible = false; + _hsvPanel.Visible = true; + break; + } + } + private void OnColorPicked(Color32 colorPicked) { if (_activeEyedropper) { _activeEyedropper = false; + _cEyedropper.BackgroundColor = _cEyedropper.BackgroundColorHighlighted = Style.Current.Foreground; SelectedColor = colorPicked; ScreenUtilities.PickColorDone -= OnColorPicked; } @@ -289,6 +325,7 @@ namespace FlaxEditor.GUI.Dialogs private void OnEyedropStart() { _activeEyedropper = true; + _cEyedropper.BackgroundColor = _cEyedropper.BackgroundColorHighlighted = Style.Current.BackgroundHighlighted; ScreenUtilities.PickColor(); ScreenUtilities.PickColorDone += OnColorPicked; } @@ -340,64 +377,76 @@ namespace FlaxEditor.GUI.Dialogs base.Draw(); - // RGBA - var rgbaR = new Rectangle(_cRed.Left - ChannelTextWidth, _cRed.Y, 10000, _cRed.Height); - Render2D.DrawText(style.FontMedium, "R", rgbaR, textColor, TextAlignment.Near, TextAlignment.Center); - rgbaR.Location.Y = _cGreen.Y; - Render2D.DrawText(style.FontMedium, "G", rgbaR, textColor, TextAlignment.Near, TextAlignment.Center); - rgbaR.Location.Y = _cBlue.Y; - Render2D.DrawText(style.FontMedium, "B", rgbaR, textColor, TextAlignment.Near, TextAlignment.Center); - rgbaR.Location.Y = _cAlpha.Y; - Render2D.DrawText(style.FontMedium, "A", rgbaR, textColor, TextAlignment.Near, TextAlignment.Center); + switch (_hsvRGBTabs.SelectedTabIndex) + { + // RGB + case 0: + var rgbRect = new Rectangle(_hsvRGBTabs.Left + PickerMargin, _hsvRGBTabs.Top + TabHeight + PickerMargin, 10000, _cRed.Height); + Render2D.DrawText(style.FontMedium, "R", rgbRect, textColor, TextAlignment.Near, TextAlignment.Center); + rgbRect.Location.Y += _cRed.Height + ChannelsMargin; + Render2D.DrawText(style.FontMedium, "G", rgbRect, textColor, TextAlignment.Near, TextAlignment.Center); + rgbRect.Location.Y += _cRed.Height + ChannelsMargin; + Render2D.DrawText(style.FontMedium, "B", rgbRect, textColor, TextAlignment.Near, TextAlignment.Center); + break; + // HSV + case 1: + // Left + var hsvLeftRect = new Rectangle(_hsvRGBTabs.Left + PickerMargin, _hsvRGBTabs.Top + TabHeight + PickerMargin, 10000, _cHue.Height); + Render2D.DrawText(style.FontMedium, "H", hsvLeftRect, textColor, TextAlignment.Near, TextAlignment.Center); + hsvLeftRect.Location.Y += _cHue.Height + ChannelsMargin; + Render2D.DrawText(style.FontMedium, "S", hsvLeftRect, textColor, TextAlignment.Near, TextAlignment.Center); + hsvLeftRect.Location.Y += _cHue.Height + ChannelsMargin; + Render2D.DrawText(style.FontMedium, "V", hsvLeftRect, textColor, TextAlignment.Near, TextAlignment.Center); - // HSV left - var hsvHl = new Rectangle(_cHue.Left - ChannelTextWidth, _cHue.Y, 10000, _cHue.Height); - Render2D.DrawText(style.FontMedium, "H", hsvHl, textColor, TextAlignment.Near, TextAlignment.Center); - hsvHl.Location.Y = _cSaturation.Y; - Render2D.DrawText(style.FontMedium, "S", hsvHl, textColor, TextAlignment.Near, TextAlignment.Center); - hsvHl.Location.Y = _cValue.Y; - Render2D.DrawText(style.FontMedium, "V", hsvHl, textColor, TextAlignment.Near, TextAlignment.Center); + // Right + var hsvRightRect = new Rectangle(_hsvRGBTabs.Right - HSVRGBTextWidth, _hsvRGBTabs.Top + TabHeight + PickerMargin, ChannelTextWidth, _cHue.Height); + Render2D.DrawText(style.FontMedium, "°", hsvRightRect, textColor, TextAlignment.Near, TextAlignment.Center); + hsvRightRect.Location.Y += _cHue.Height + ChannelsMargin; + Render2D.DrawText(style.FontMedium, "%", hsvRightRect, textColor, TextAlignment.Near, TextAlignment.Center); + hsvRightRect.Location.Y += _cHue.Height + ChannelsMargin; + Render2D.DrawText(style.FontMedium, "%", hsvRightRect, textColor, TextAlignment.Near, TextAlignment.Center); + break; + } - // HSV right - var hsvHr = new Rectangle(_cHue.Right + 2, _cHue.Y, 10000, _cHue.Height); - Render2D.DrawText(style.FontMedium, "°", hsvHr, textColor, TextAlignment.Near, TextAlignment.Center); - hsvHr.Location.Y = _cSaturation.Y; - Render2D.DrawText(style.FontMedium, "%", hsvHr, textColor, TextAlignment.Near, TextAlignment.Center); - hsvHr.Location.Y = _cValue.Y; - Render2D.DrawText(style.FontMedium, "%", hsvHr, textColor, TextAlignment.Near, TextAlignment.Center); + // A + var alphaHexRect = new Rectangle(_hsvRGBTabs.Left + PickerMargin, _cAlpha.Top, ChannelTextWidth, _cAlpha.Height); + Render2D.DrawText(style.FontMedium, "A", alphaHexRect, textColor, TextAlignment.Near, TextAlignment.Center); // Hex - var hex = new Rectangle(_cHex.Left - 26, _cHex.Y, 10000, _cHex.Height); - Render2D.DrawText(style.FontMedium, "Hex", hex, textColor, TextAlignment.Near, TextAlignment.Center); + alphaHexRect.Y += _cAlpha.Height + ChannelsMargin * 2.0f; + alphaHexRect.X -= 5.0f; // "Hex" is two characters wider than the other labels so we need to adjust for that + Render2D.DrawText(style.FontMedium, "Hex", alphaHexRect, textColor, TextAlignment.Far, TextAlignment.Center); // Color difference - var newRect = new Rectangle(_cOK.X - 3, _cHex.Bottom + PickerMargin, 130, 0); - newRect.Size.Y = 50; - Render2D.FillRectangle(newRect, Color.White); - var smallRectSize = 10; - var numHor = Mathf.FloorToInt(newRect.Width / smallRectSize); - var numVer = Mathf.FloorToInt(newRect.Height / smallRectSize); + var differenceRect = new Rectangle(_hsvRGBTabs.Left, _cHex.Bottom + 40.0f, _hsvRGBTabs.Width, ColorPreviewHeight); + // Draw checkerboard for background of color to help with transparency + Render2D.FillRectangle(differenceRect, Color.White); + var smallRectSize = 10; + var numHor = Mathf.CeilToInt(differenceRect.Width / smallRectSize); + var numVer = Mathf.CeilToInt(differenceRect.Height / smallRectSize); + Render2D.PushClip(differenceRect); for (int i = 0; i < numHor; i++) { for (int j = 0; j < numVer; j++) { if ((i + j) % 2 == 0) { - var rect = new Rectangle(newRect.X + smallRectSize * i, newRect.Y + smallRectSize * j, new Float2(smallRectSize)); + var rect = new Rectangle(differenceRect.X + smallRectSize * i, differenceRect.Y + smallRectSize * j, new Float2(smallRectSize)); Render2D.FillRectangle(rect, Color.Gray); } } } - Render2D.FillRectangle(newRect, _value); + Render2D.PopClip(); + Render2D.FillRectangle(differenceRect, _value); } /// protected override void OnShow() { - // Auto cancel on lost focus + // Apply changes on lost focus #if !PLATFORM_LINUX - ((WindowRootControl)Root).Window.LostFocus += OnWindowLostFocus; + ((WindowRootControl)Root).Window.LostFocus += OnSubmit; #endif base.OnShow(); @@ -410,6 +459,7 @@ namespace FlaxEditor.GUI.Dialogs { // Cancel eye dropping _activeEyedropper = false; + _cEyedropper.BackgroundColor = _cEyedropper.BackgroundColorHighlighted = Style.Current.Foreground; ScreenUtilities.PickColorDone -= OnColorPicked; return true; } @@ -483,20 +533,20 @@ namespace FlaxEditor.GUI.Dialogs } _savedColorButtons.Clear(); - CreateAllSaveButtons(); + CreateAllSavedColorsButtons(); // Save new colors var savedColors = JsonSerializer.Serialize(_savedColors, typeof(List)); Editor.Instance.ProjectCache.SetCustomData("ColorPickerSavedColors", savedColors); } - private void CreateAllSaveButtons() + private void CreateAllSavedColorsButtons() { // Create saved color buttons for (int i = 0; i < _savedColors.Count; i++) { var savedColor = _savedColors[i]; - var savedColorButton = new Button(PickerMargin * (i + 1) + SavedColorButtonWidth * i, Height - SavedColorButtonHeight - PickerMargin, SavedColorButtonWidth, SavedColorButtonHeight) + var savedColorButton = new Button(PickerMargin * (i + 1) + SavedColorButtonWidth * i, _cHex.Bottom + 40.0f + ColorPreviewHeight * 0.5f, SavedColorButtonWidth, SavedColorButtonHeight) { Parent = this, Tag = savedColor, @@ -507,9 +557,9 @@ namespace FlaxEditor.GUI.Dialogs savedColorButton.ButtonClicked += OnSavedColorButtonClicked; _savedColorButtons.Add(savedColorButton); } - if (_savedColors.Count < 8) + if (_savedColors.Count < SavedColorsAmount) { - var savedColorButton = new Button(PickerMargin * (_savedColors.Count + 1) + SavedColorButtonWidth * _savedColors.Count, Height - SavedColorButtonHeight - PickerMargin, SavedColorButtonWidth, SavedColorButtonHeight) + var savedColorButton = new Button(PickerMargin * (_savedColors.Count + 1) + SavedColorButtonWidth * _savedColors.Count, _cHex.Bottom + 40.0f + ColorPreviewHeight * 0.5f, SavedColorButtonWidth, SavedColorButtonHeight) { Text = "+", Parent = this, @@ -521,19 +571,6 @@ namespace FlaxEditor.GUI.Dialogs } } - private void OnWindowLostFocus() - { - // Auto apply color on defocus - var autoAcceptColorPickerChange = Editor.Instance.Options.Options.Interface.AutoAcceptColorPickerChange; - if (_useDynamicEditing && _initialValue != _value && _canPassLastChangeEvent && autoAcceptColorPickerChange) - { - _canPassLastChangeEvent = false; - _onChanged?.Invoke(_value, false); - } - - OnCancel(); - } - /// public override void OnSubmit() { @@ -541,6 +578,9 @@ namespace FlaxEditor.GUI.Dialogs return; _disableEvents = true; + // Ensure the cursor is restored + Cursor = CursorType.Default; + // Send color event if modified if (_value != _initialValue) { @@ -557,6 +597,9 @@ namespace FlaxEditor.GUI.Dialogs return; _disableEvents = true; + // Ensure the cursor is restored + Cursor = CursorType.Default; + // Restore color if modified if (_useDynamicEditing && _initialValue != _value && _canPassLastChangeEvent) { diff --git a/Source/Editor/Options/InterfaceOptions.cs b/Source/Editor/Options/InterfaceOptions.cs index f26506c74..50f7c6f04 100644 --- a/Source/Editor/Options/InterfaceOptions.cs +++ b/Source/Editor/Options/InterfaceOptions.cs @@ -207,13 +207,6 @@ namespace FlaxEditor.Options [EditorDisplay("Interface"), EditorOrder(280), Tooltip("Editor content window orientation.")] public FlaxEngine.GUI.Orientation ContentWindowOrientation { get; set; } = FlaxEngine.GUI.Orientation.Horizontal; - /// - /// If checked, color pickers will always modify the color unless 'Cancel' if pressed, otherwise color won't change unless 'Ok' is pressed. - /// - [DefaultValue(true)] - [EditorDisplay("Interface"), EditorOrder(290)] - public bool AutoAcceptColorPickerChange { get; set; } = true; - /// /// Gets or sets the formatting option for numeric values in the editor. /// From d163064f958efdc8a9ccb10e922971daf024af48 Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 5 Mar 2026 20:33:18 +0100 Subject: [PATCH 05/24] fix hv wheel selector --- Source/Editor/GUI/Dialogs/ColorSelector.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Editor/GUI/Dialogs/ColorSelector.cs b/Source/Editor/GUI/Dialogs/ColorSelector.cs index 392a8d934..3d781a8ec 100644 --- a/Source/Editor/GUI/Dialogs/ColorSelector.cs +++ b/Source/Editor/GUI/Dialogs/ColorSelector.cs @@ -194,10 +194,11 @@ namespace FlaxEditor.GUI.Dialogs // Wheel float boxExpand = (2.0f * 4.0f / 128.0f) * _wheelRect.Width; Render2D.DrawMaterial(_hsWheelMaterial, _wheelRect, enabled ? Color.White : Color.Gray); - Color hsColor = Color.FromHSV(new Float3(Color.ToHSV().X, 1, 1)); + Float3 hsv = _color.ToHSV(); + Color hsColor = Color.FromHSV(new Float3(hsv.X, hsv.Y, 1)); Rectangle wheelRect = wheelDragRect; Render2D.FillRectangle(wheelRect, hsColor); - Render2D.DrawRectangle(wheelRect, _isMouseDownWheel ? Color.Gray : Color.Black, _isMouseDownWheel ? 3.0f : 1.0f); + Render2D.DrawRectangle(wheelRect, Color.Black, _isMouseDownWheel ? 2.0f : 1.0f); } /// From f87606808655e8e5b2a1da7777fc407436035865 Mon Sep 17 00:00:00 2001 From: Phantom Date: Fri, 13 Mar 2026 20:09:57 +0100 Subject: [PATCH 06/24] Fix Text Color Highlighted --- Source/Engine/UI/GUI/Common/Dropdown.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs index 6488bb14a..4e6a235a8 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.Brightness < 0.05f ? Color.Lerp(TextColorHighlighted, Color.White, 0.3f) : TextColorHighlighted, + TextColorHighlighted = (BackgroundColorSelected.Brightness < 0.05f && TextColorHighlighted.Brightness < 0.05f) ? Color.Lerp(TextColorHighlighted, Color.White, 0.3f) : TextColorHighlighted, HorizontalAlignment = HorizontalAlignment, VerticalAlignment = VerticalAlignment, Text = _items[i], From 80767d65ae55e9fdd713e4a995a10105a451970d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 13 Mar 2026 23:21:08 +0100 Subject: [PATCH 07/24] Fix camera preview placement in editor preview when resizing window --- Source/Editor/Windows/EditGameWindow.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Windows/EditGameWindow.cs b/Source/Editor/Windows/EditGameWindow.cs index dbeab28b1..5f91aebe6 100644 --- a/Source/Editor/Windows/EditGameWindow.cs +++ b/Source/Editor/Windows/EditGameWindow.cs @@ -257,7 +257,7 @@ namespace FlaxEditor.Windows private void UpdateCameraPreview() { // Disable rendering preview during GI baking - if (Editor.StateMachine.CurrentState.IsPerformanceHeavy) + if (Editor == null || Editor.StateMachine.CurrentState.IsPerformanceHeavy) { HideAllCameraPreviews(); return; @@ -406,6 +406,14 @@ namespace FlaxEditor.Windows } } + /// + protected override void OnSizeChanged() + { + base.OnSizeChanged(); + + UpdateCameraPreview(); + } + /// public override void OnDestroy() { From f42a9a760adee861860a73a443b8013ba660ff41 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 13 Mar 2026 23:21:30 +0100 Subject: [PATCH 08/24] Adjust new Z Axis color in Transform --- Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs index c1179e393..131a9b10a 100644 --- a/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs +++ b/Source/Editor/CustomEditors/Editors/ActorTransformEditor.cs @@ -24,7 +24,7 @@ namespace FlaxEditor.CustomEditors.Editors /// /// The Z axis color. /// - public static Color AxisColorZ = new Color(0.0f, 0.0235294f, 0.9f, 1.0f); + public static Color AxisColorZ = new Color(0.0f, 0.42352f, 0.8f, 1.0f); /// /// Custom editor for actor position property. From 14d6273a2f465176f48dc0e04e2eb87cbb7e09ce Mon Sep 17 00:00:00 2001 From: Phantom Date: Sat, 14 Mar 2026 15:29:31 +0100 Subject: [PATCH 09/24] fix #2 on Dropdown --- Source/Engine/UI/GUI/Common/Dropdown.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/UI/GUI/Common/Dropdown.cs b/Source/Engine/UI/GUI/Common/Dropdown.cs index 4e6a235a8..b91d70dc9 100644 --- a/Source/Engine/UI/GUI/Common/Dropdown.cs +++ b/Source/Engine/UI/GUI/Common/Dropdown.cs @@ -595,7 +595,7 @@ namespace FlaxEngine.GUI Size = new Float2(size.X - margin, size.Y), Font = Font, TextColor = TextColor * 0.9f, - TextColorHighlighted = (BackgroundColorSelected.Brightness < 0.05f && TextColorHighlighted.Brightness < 0.05f) ? Color.Lerp(TextColorHighlighted, Color.White, 0.3f) : TextColorHighlighted, + TextColorHighlighted = BackgroundColorSelected.Brightness < 0.05f ? Color.Lerp(TextColorHighlighted, Color.White, 0.3f) : TextColorHighlighted, HorizontalAlignment = HorizontalAlignment, VerticalAlignment = VerticalAlignment, Text = _items[i], From 13084652800b790e775b7be07c64855bb2522859 Mon Sep 17 00:00:00 2001 From: Saas Date: Tue, 17 Mar 2026 22:02:36 +0100 Subject: [PATCH 10/24] fix sphere with negative radius getting culled to early in physics collider debug draw --- Source/Engine/Physics/Colliders/SphereCollider.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Physics/Colliders/SphereCollider.cpp b/Source/Engine/Physics/Colliders/SphereCollider.cpp index 0576ae6cf..eda3884e4 100644 --- a/Source/Engine/Physics/Colliders/SphereCollider.cpp +++ b/Source/Engine/Physics/Colliders/SphereCollider.cpp @@ -26,7 +26,7 @@ void SphereCollider::SetRadius(const float value) void SphereCollider::DrawPhysicsDebug(RenderView& view) { - const BoundingSphere sphere(_sphere.Center - view.Origin, _sphere.Radius); + const BoundingSphere sphere(_sphere.Center - view.Origin, Math::Abs(_sphere.Radius)); if (!view.CullingFrustum.Intersects(sphere)) return; if (view.Mode == ViewMode::PhysicsColliders && !GetIsTrigger()) From cb9e09c21ba04e3a72d7393f636287dce2a2b9d5 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 18 Mar 2026 16:55:35 +0100 Subject: [PATCH 11/24] fix Show whole timeline button --- Source/Editor/GUI/Timeline/Timeline.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs index 15e7eb953..1fb19e9ec 100644 --- a/Source/Editor/GUI/Timeline/Timeline.cs +++ b/Source/Editor/GUI/Timeline/Timeline.cs @@ -2148,10 +2148,9 @@ namespace FlaxEditor.GUI.Timeline /// public void ShowWholeTimeline() { - var viewWidth = Width; - var timelineWidth = Duration * UnitsPerSecond * Zoom + 8 * StartOffset; - _backgroundArea.ViewOffset = Float2.Zero; - Zoom = viewWidth / timelineWidth; + const float padding = 40f; + Zoom = (_backgroundArea.Width - padding * 2f) / (Duration * UnitsPerSecond); + _backgroundArea.ViewOffset = new Float2(-_leftEdge.X + padding, _backgroundArea.ViewOffset.Y); } /// From e5d526c9af30bfa0a0c7508de255600d48b16c33 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 18 Mar 2026 16:55:45 +0100 Subject: [PATCH 12/24] show whole timeline when animation loads --- Source/Editor/Windows/Assets/AnimationWindow.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Editor/Windows/Assets/AnimationWindow.cs b/Source/Editor/Windows/Assets/AnimationWindow.cs index 268e18503..8dbcc5921 100644 --- a/Source/Editor/Windows/Assets/AnimationWindow.cs +++ b/Source/Editor/Windows/Assets/AnimationWindow.cs @@ -455,6 +455,7 @@ namespace FlaxEditor.Windows.Assets _timeline.Enabled = true; _timeline.SetNoTracksText(null); ClearEditedFlag(); + _timeline.ShowWholeTimeline(); } } From b54794255a66d6b69efeab82a50b57497656c935 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Mar 2026 17:14:30 +0100 Subject: [PATCH 13/24] Fix `.pch` files rebuilds after MSVC toolchain updates --- .../Platforms/Windows/WindowsToolchainBase.cs | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs b/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs index 583b93b55..8e9f522a8 100644 --- a/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs +++ b/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Text; using System.Xml; using Flax.Build.Graph; @@ -26,6 +25,11 @@ namespace Flax.Build.Platforms /// protected readonly string _vcToolPath; + /// + /// The VC tools version. + /// + protected readonly string _vcToolVersion; + /// /// The compiler path. /// @@ -147,6 +151,21 @@ namespace Flax.Build.Platforms _libToolPath = Path.Combine(_vcToolPath, "lib.exe"); _xdcmakePath = Path.Combine(_vcToolPath, "xdcmake.exe"); + // Find 'MSVC\XX.YY.ZZ\bin' to get version + _vcToolVersion = string.Empty; + var pathParts = _vcToolPath.Split('\\'); + if (pathParts.Length >= 3) + { + for (int i = 3; i < pathParts.Length; i++) + { + if (pathParts[i] == "bin" && pathParts[i - 2] == "MSVC") + { + _vcToolVersion = pathParts[i - 1]; + break; + } + } + } + // Add Visual C++ toolset include and library paths var vcToolChainDir = toolsets[Toolset]; SystemIncludePaths.Add(Path.Combine(vcToolChainDir, "include")); @@ -370,7 +389,7 @@ namespace Flax.Build.Platforms public override void LogInfo() { var sdkPath = WindowsPlatformBase.GetSDKs()[SDK]; - Log.Info(string.Format("Using Windows Toolset {0} ({1})", Toolset, sdkPath)); + Log.Info(string.Format("Using Windows Toolset {0}, {2} ({1})", Toolset, sdkPath, _vcToolVersion)); Log.Info(string.Format("Using Windows SDK {0} ({1})", WindowsPlatformBase.GetSDKVersion(SDK), _vcToolPath)); } @@ -663,8 +682,9 @@ namespace Flax.Build.Platforms var pchSourceFile = Path.Combine(options.IntermediateFolder, Path.ChangeExtension(pchFilName, "cpp")); var contents = Bindings.BindingsGenerator.GetStringBuilder(); contents.AppendLine("// This code was auto-generated. Do not modify it."); - // TODO: write compiler version to properly rebuild pch on Visual Studio updates contents.Append("// Compiler: ").AppendLine(_compilerPath); + contents.Append("// Toolchain: ").AppendLine(_vcToolVersion); + contents.Append("// CppVersion: ").AppendLine(compileEnvironment.CppVersion.ToString()); contents.Append("#include \"").Append(pchSource).AppendLine("\""); Utilities.WriteFileIfChanged(pchSourceFile, contents.ToString()); Bindings.BindingsGenerator.PutStringBuilder(contents); From 99d3da467e6dfc57e5ebc96606f839c5340abbe9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Mar 2026 17:45:06 +0100 Subject: [PATCH 14/24] Fix deadlock when hot-reloading scripts in Editor while `Animation` asset gets auto-saved #3942 --- Source/Engine/Content/Assets/Animation.cpp | 23 +++++++++++++++++----- Source/Engine/Content/Assets/Animation.h | 4 ++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Content/Assets/Animation.cpp b/Source/Engine/Content/Assets/Animation.cpp index 917db28ac..86fa41f89 100644 --- a/Source/Engine/Content/Assets/Animation.cpp +++ b/Source/Engine/Content/Assets/Animation.cpp @@ -691,11 +691,7 @@ Asset::LoadResult Animation::load() continue; } #if USE_EDITOR - if (!_registeredForScriptingReload) - { - _registeredForScriptingReload = true; - Level::ScriptsReloadStart.Bind(this); - } + _registerForScriptingReload = true; #endif } } @@ -733,6 +729,7 @@ void Animation::unload(bool isReloading) { ScopeWriteLock systemScope(Animations::SystemLocker); #if USE_EDITOR + _registerForScriptingReload = false; if (_registeredForScriptingReload) { _registeredForScriptingReload = false; @@ -752,6 +749,22 @@ void Animation::unload(bool isReloading) NestedAnims.Clear(); } +#if USE_EDITOR + +void Animation::onLoaded_MainThread() +{ + if (_registerForScriptingReload && !_registeredForScriptingReload) + { + _registeredForScriptingReload = true; + Level::ScriptsReloadStart.Bind(this); + } + _registerForScriptingReload = false; + + BinaryAsset::onLoaded_MainThread(); +} + +#endif + AssetChunksFlag Animation::getChunksToPreload() const { return GET_CHUNK_FLAG(0); diff --git a/Source/Engine/Content/Assets/Animation.h b/Source/Engine/Content/Assets/Animation.h index 4f4a773b4..c342cc64c 100644 --- a/Source/Engine/Content/Assets/Animation.h +++ b/Source/Engine/Content/Assets/Animation.h @@ -78,6 +78,7 @@ API_CLASS(NoSpawn) class FLAXENGINE_API Animation : public BinaryAsset private: #if USE_EDITOR + bool _registerForScriptingReload = false; bool _registeredForScriptingReload = false; void OnScriptsReloadStart(); #endif @@ -163,5 +164,8 @@ protected: // [BinaryAsset] LoadResult load() override; void unload(bool isReloading) override; +#if USE_EDITOR + void onLoaded_MainThread() override; +#endif AssetChunksFlag getChunksToPreload() const override; }; From fa17d83501d653b37f3cda8ae1d96e4b2d6d6069 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Mar 2026 18:25:40 +0100 Subject: [PATCH 15/24] Fix showing internal surface parameters (eg. `Base Model` in `Anim Graph`) #3936 --- .../Editor/Surface/Archetypes/Parameters.cs | 204 +++++++++--------- Source/Editor/Surface/VisjectSurface.cs | 5 + .../Editor/Surface/VisjectSurfaceContext.cs | 10 +- Source/Editor/Surface/VisualScriptSurface.cs | 3 + 4 files changed, 116 insertions(+), 106 deletions(-) diff --git a/Source/Editor/Surface/Archetypes/Parameters.cs b/Source/Editor/Surface/Archetypes/Parameters.cs index 3383e7662..905f793c9 100644 --- a/Source/Editor/Surface/Archetypes/Parameters.cs +++ b/Source/Editor/Surface/Archetypes/Parameters.cs @@ -22,15 +22,97 @@ namespace FlaxEditor.Surface.Archetypes [HideInEditor] public static class Parameters { + /// + /// Surface node type for parameters group Get/Set nodes. + /// + /// + public abstract class SurfaceNodeParamsBase : SurfaceNode + { + /// + /// The combobox for picking parameter. + /// + protected ComboBoxElement _combobox; + + /// + /// Fag used to block layout updated when updating node. + /// + protected bool _isUpdateLocked; + + /// + protected SurfaceNodeParamsBase(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) + : base(id, context, nodeArch, groupArch) + { + } + + /// + /// Gets the selected parameter. + /// + /// Surface parameter object or null if nothing selected or cannot find it. + protected SurfaceParameter GetSelected() + { + if (Surface != null) + return Surface.GetParameter(_combobox.SelectedItem); + return Context.GetParameter((Guid)Values[0]); + } + + /// + /// Updates the combo box. + /// + protected void UpdateCombo() + { + if (_isUpdateLocked) + return; + _isUpdateLocked = true; + if (_combobox == null) + { + _combobox = GetChild(); + _combobox.SelectedIndexChanged += OnSelectedChanged; + } + string toSelect = null; + Guid loadedSelected = (Guid)Values[0]; + _combobox.ClearItems(); + var parameters = Surface.Parameters; + for (int i = 0; i < parameters.Count; i++) + { + var param = parameters[i]; + if (!param.IsPublic && !Surface.CanShowPrivateParameters) + continue; + _combobox.AddItem(param.Name); + if (param.ID == loadedSelected) + toSelect = param.Name; + } + _combobox.SelectedItem = toSelect; + _isUpdateLocked = false; + } + + private void OnSelectedChanged(ComboBox cb) + { + if (_isUpdateLocked) + return; + var selected = GetSelected(); + var selectedID = selected?.ID ?? Guid.Empty; + if (selectedID != (Guid)Values[0]) + Set(selected, ref selectedID); + } + + /// + /// Sets the selected parameter. + /// + /// Parameter. + /// Parameter identifier. + protected virtual void Set(SurfaceParameter selected, ref Guid selectedID) + { + SetValue(0, selectedID); + } + } + /// /// Surface node type for parameters group Get node. /// /// - public class SurfaceNodeParamsGet : SurfaceNode, IParametersDependantNode + public class SurfaceNodeParamsGet : SurfaceNodeParamsBase, IParametersDependantNode { - private ComboBoxElement _combobox; private readonly List _dynamicChildren = new List(); - private bool _isUpdateLocked; private ScriptType _layoutType; private NodeElementArchetype[] _layoutElements; @@ -306,49 +388,6 @@ namespace FlaxEditor.Surface.Archetypes UpdateTitle(); } - private void UpdateCombo() - { - if (_isUpdateLocked) - return; - _isUpdateLocked = true; - if (_combobox == null) - { - _combobox = (ComboBoxElement)_children[0]; - _combobox.SelectedIndexChanged += OnSelectedChanged; - } - int toSelect = -1; - Guid loadedSelected = (Guid)Values[0]; - _combobox.ClearItems(); - for (int i = 0; i < Surface.Parameters.Count; i++) - { - var param = Surface.Parameters[i]; - _combobox.AddItem(param.Name); - if (param.ID == loadedSelected) - toSelect = i; - } - _combobox.SelectedIndex = toSelect; - _isUpdateLocked = false; - } - - private void OnSelectedChanged(ComboBox cb) - { - if (_isUpdateLocked) - return; - var selected = GetSelected(); - var selectedID = selected?.ID ?? Guid.Empty; - SetValue(0, selectedID); - } - - private SurfaceParameter GetSelected() - { - if (Surface != null) - { - var selectedIndex = _combobox.SelectedIndex; - return selectedIndex >= 0 && selectedIndex < Surface.Parameters.Count ? Surface.Parameters[selectedIndex] : null; - } - return Context.GetParameter((Guid)Values[0]); - } - private void ClearDynamicElements() { for (int i = 0; i < _dynamicChildren.Count; i++) @@ -463,15 +502,19 @@ namespace FlaxEditor.Surface.Archetypes else if (!_isUpdateLocked) { _isUpdateLocked = true; - int toSelect = -1; + string toSelect = null; Guid loadedSelected = (Guid)Values[0]; - for (int i = 0; i < Surface.Parameters.Count; i++) + var parameters = Surface.Parameters; + for (int i = 0; i < parameters.Count; i++) { - var param = Surface.Parameters[i]; + var param = parameters[i]; if (param.ID == loadedSelected) - toSelect = i; + { + toSelect = param.Name; + break; + } } - _combobox.SelectedIndex = toSelect; + _combobox.SelectedItem = toSelect; _isUpdateLocked = false; } UpdateLayout(); @@ -817,66 +860,23 @@ namespace FlaxEditor.Surface.Archetypes /// Surface node type for parameters group Set node. /// /// - public class SurfaceNodeParamsSet : SurfaceNode, IParametersDependantNode + public class SurfaceNodeParamsSet : SurfaceNodeParamsBase, IParametersDependantNode { - private ComboBoxElement _combobox; - private bool _isUpdateLocked; - /// public SurfaceNodeParamsSet(uint id, VisjectSurfaceContext context, NodeArchetype nodeArch, GroupArchetype groupArch) : base(id, context, nodeArch, groupArch) { } - private void UpdateCombo() + /// + protected override void Set(SurfaceParameter selected, ref Guid selectedID) { - if (_isUpdateLocked) - return; - _isUpdateLocked = true; - if (_combobox == null) + SetValues(new[] { - _combobox = GetChild(); - _combobox.SelectedIndexChanged += OnSelectedChanged; - } - int toSelect = -1; - Guid loadedSelected = (Guid)Values[0]; - _combobox.ClearItems(); - for (int i = 0; i < Surface.Parameters.Count; i++) - { - var param = Surface.Parameters[i]; - _combobox.AddItem(param.Name); - if (param.ID == loadedSelected) - toSelect = i; - } - _combobox.SelectedIndex = toSelect; - _isUpdateLocked = false; - } - - private void OnSelectedChanged(ComboBox cb) - { - if (_isUpdateLocked) - return; - var selected = GetSelected(); - var selectedID = selected?.ID ?? Guid.Empty; - if (selectedID != (Guid)Values[0]) - { - SetValues(new[] - { - selectedID, - selected != null ? TypeUtils.GetDefaultValue(selected.Type) : null, - }); - UpdateUI(); - } - } - - private SurfaceParameter GetSelected() - { - if (Surface != null) - { - var selectedIndex = _combobox.SelectedIndex; - return selectedIndex >= 0 && selectedIndex < Surface.Parameters.Count ? Surface.Parameters[selectedIndex] : null; - } - return Context.GetParameter((Guid)Values[0]); + selectedID, + selected != null ? TypeUtils.GetDefaultValue(selected.Type) : null, + }); + UpdateUI(); } /// diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index e3bb94bcc..c4a8d31d3 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -583,6 +583,11 @@ namespace FlaxEditor.Surface /// public virtual bool CanSetParameters => false; + /// + /// Gets a value indicating whether surface private parameters can be used, otherwise they will remain hidden. + /// + public virtual bool CanShowPrivateParameters => false; + /// /// True of the context menu should make use of a description panel drawn at the bottom of the menu /// diff --git a/Source/Editor/Surface/VisjectSurfaceContext.cs b/Source/Editor/Surface/VisjectSurfaceContext.cs index 0d10aa230..560a7c1d6 100644 --- a/Source/Editor/Surface/VisjectSurfaceContext.cs +++ b/Source/Editor/Surface/VisjectSurfaceContext.cs @@ -254,9 +254,10 @@ namespace FlaxEditor.Surface public SurfaceParameter GetParameter(Guid id) { SurfaceParameter result = null; - for (int i = 0; i < Parameters.Count; i++) + var parameters = Parameters; + for (int i = 0; i < parameters.Count; i++) { - var parameter = Parameters[i]; + var parameter = parameters[i]; if (parameter.ID == id) { result = parameter; @@ -274,9 +275,10 @@ namespace FlaxEditor.Surface public SurfaceParameter GetParameter(string name) { SurfaceParameter result = null; - for (int i = 0; i < Parameters.Count; i++) + var parameters = Parameters; + for (int i = 0; i < parameters.Count; i++) { - var parameter = Parameters[i]; + var parameter = parameters[i]; if (parameter.Name == name) { result = parameter; diff --git a/Source/Editor/Surface/VisualScriptSurface.cs b/Source/Editor/Surface/VisualScriptSurface.cs index 2f70ee340..57aa29d67 100644 --- a/Source/Editor/Surface/VisualScriptSurface.cs +++ b/Source/Editor/Surface/VisualScriptSurface.cs @@ -187,6 +187,9 @@ namespace FlaxEditor.Surface /// public override bool CanSetParameters => true; + /// + public override bool CanShowPrivateParameters => true; + /// public override bool UseContextMenuDescriptionPanel => true; From 7f1add4bddcc386d1b34ffe3c230f57e276c5814 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 14 Feb 2026 00:07:34 +0100 Subject: [PATCH 16/24] Fix missing `Length` in `Vector4` --- Source/Engine/Core/Math/Vector4.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Engine/Core/Math/Vector4.h b/Source/Engine/Core/Math/Vector4.h index 5f5e436ad..167937c81 100644 --- a/Source/Engine/Core/Math/Vector4.h +++ b/Source/Engine/Core/Math/Vector4.h @@ -153,6 +153,12 @@ public: return Math::IsOne(X) && Math::IsOne(Y) && Math::IsOne(Z) && Math::IsOne(W); } + // Calculates the length of the vector. + T Length() const + { + return Math::Sqrt(X * X + Y * Y + Z * Z + W * W); + } + /// /// Returns the average arithmetic of all the components. /// From 1318dde3ca00159abe82d262db385e16501661a2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Mar 2026 18:58:32 +0100 Subject: [PATCH 17/24] Add new HSV wheel material, maintain Editor icons atlas for now #3987 --- Content/Editor/HSWheel.flax | 3 +++ Source/Editor/EditorIcons.cs | 1 + 2 files changed, 4 insertions(+) create mode 100644 Content/Editor/HSWheel.flax diff --git a/Content/Editor/HSWheel.flax b/Content/Editor/HSWheel.flax new file mode 100644 index 000000000..7fb2f0600 --- /dev/null +++ b/Content/Editor/HSWheel.flax @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:946a92d15b50e01a67d9e0a9d71493b222dce2b7cc464383b07450ef3afa6888 +size 15068 diff --git a/Source/Editor/EditorIcons.cs b/Source/Editor/EditorIcons.cs index 7da1d6fb3..8688c502a 100644 --- a/Source/Editor/EditorIcons.cs +++ b/Source/Editor/EditorIcons.cs @@ -134,6 +134,7 @@ namespace FlaxEditor public SpriteHandle Document128; public SpriteHandle XBoxOne128; public SpriteHandle UWPStore128; + public SpriteHandle ColorWheel128; public SpriteHandle LinuxSettings128; public SpriteHandle NavigationSettings128; public SpriteHandle AudioSettings128; From d2ef0671e37769c3635abd56fc318378bda71e64 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Mar 2026 19:32:17 +0100 Subject: [PATCH 18/24] Fix incorrect terrain debug buffers disposing --- Source/Engine/Terrain/TerrainPatch.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Terrain/TerrainPatch.cpp b/Source/Engine/Terrain/TerrainPatch.cpp index 95e9ffd05..61bbf7e3c 100644 --- a/Source/Engine/Terrain/TerrainPatch.cpp +++ b/Source/Engine/Terrain/TerrainPatch.cpp @@ -2241,11 +2241,11 @@ void TerrainPatch::DestroyCollision() _physicsHeightField = nullptr; #if TERRAIN_USE_PHYSICS_DEBUG _debugLinesDirty = true; - SAFE_DELETE(_debugLines); + SAFE_DELETE_GPU_RESOURCE(_debugLines); #endif #if USE_EDITOR _collisionTriangles.Resize(0); - SAFE_DELETE(_collisionTrianglesBuffer); + SAFE_DELETE_GPU_RESOURCE(_collisionTrianglesBuffer); _collisionTrianglesBufferDirty = true; #endif _collisionVertices.Resize(0); From f0f1c57ff19d3464e2dea5c2584558081cc79fc6 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Mar 2026 19:43:37 +0100 Subject: [PATCH 19/24] Add scrolling scene tree hierarchy to the newly spawned actor after drag and drop --- Source/Editor/Viewport/ViewportDraggingHelper.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Source/Editor/Viewport/ViewportDraggingHelper.cs b/Source/Editor/Viewport/ViewportDraggingHelper.cs index 21287fc94..3e6fce6ad 100644 --- a/Source/Editor/Viewport/ViewportDraggingHelper.cs +++ b/Source/Editor/Viewport/ViewportDraggingHelper.cs @@ -198,6 +198,11 @@ namespace FlaxEditor.Viewport actor.Position = PostProcessSpawnedActorLocation(actor, ref hitLocation); _owner.Spawn(actor); _viewport.Focus(); + + // Scroll to the new actor in the hierarchy + var actorNode = Editor.Instance.Scene.GetActorNode(actor); + if (actorNode?.TreeNode.ParentTree.Parent is Panel treePanel) + FlaxEngine.Scripting.InvokeOnUpdate(() => treePanel.ScrollViewTo(actorNode.TreeNode)); } private void Spawn(ScriptItem item, SceneGraphNode hit, ref Float2 location, ref Vector3 hitLocation, ref Vector3 hitNormal) From a63e05d444248164da7c88d947860602a9d57306 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Mar 2026 20:10:20 +0100 Subject: [PATCH 20/24] Fix Cloth to snap to the parent actor on spawn in Editor --- Source/Editor/SceneGraph/Actors/ClothNode.cs | 29 +++++++++++++++++++ Source/Editor/SceneGraph/SceneGraphFactory.cs | 1 + 2 files changed, 30 insertions(+) create mode 100644 Source/Editor/SceneGraph/Actors/ClothNode.cs diff --git a/Source/Editor/SceneGraph/Actors/ClothNode.cs b/Source/Editor/SceneGraph/Actors/ClothNode.cs new file mode 100644 index 000000000..b5caa8258 --- /dev/null +++ b/Source/Editor/SceneGraph/Actors/ClothNode.cs @@ -0,0 +1,29 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using FlaxEngine; + +namespace FlaxEditor.SceneGraph.Actors +{ + /// + /// Scene tree node for actor type. + /// + [HideInEditor] + public sealed class ClothNode : ActorNode + { + /// + public ClothNode(Actor actor) + : base(actor) + { + } + + /// + public override void PostSpawn() + { + base.PostSpawn(); + + // Snap to the parent + if (!(ParentNode is SceneNode)) + Actor.LocalTransform = Transform.Identity; + } + } +} diff --git a/Source/Editor/SceneGraph/SceneGraphFactory.cs b/Source/Editor/SceneGraph/SceneGraphFactory.cs index c4aecef7e..bd465aad2 100644 --- a/Source/Editor/SceneGraph/SceneGraphFactory.cs +++ b/Source/Editor/SceneGraph/SceneGraphFactory.cs @@ -51,6 +51,7 @@ namespace FlaxEditor.SceneGraph CustomNodesTypes.Add(typeof(AudioSource), typeof(AudioSourceNode)); CustomNodesTypes.Add(typeof(BoneSocket), typeof(BoneSocketNode)); CustomNodesTypes.Add(typeof(Decal), typeof(DecalNode)); + CustomNodesTypes.Add(typeof(Cloth), typeof(ClothNode)); CustomNodesTypes.Add(typeof(BoxCollider), typeof(BoxColliderNode)); CustomNodesTypes.Add(typeof(SphereCollider), typeof(ColliderNode)); CustomNodesTypes.Add(typeof(CapsuleCollider), typeof(ColliderNode)); From 29abfbcdc99f6b2dd36272ab627b8e532412e318 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Mar 2026 22:59:21 +0100 Subject: [PATCH 21/24] Fix Cloth with models that use compressed vertex buffer #4017 --- Source/Engine/Graphics/Models/MeshAccessor.h | 21 ++ Source/Engine/Level/Actors/AnimatedModel.cpp | 10 + Source/Engine/Level/Actors/AnimatedModel.h | 1 + .../Level/Actors/ModelInstanceActor.cpp | 6 + .../Engine/Level/Actors/ModelInstanceActor.h | 13 + Source/Engine/Level/Actors/StaticModel.cpp | 10 + Source/Engine/Level/Actors/StaticModel.h | 1 + Source/Engine/Physics/Actors/Cloth.cpp | 238 +++++++++--------- Source/Engine/Physics/Actors/Cloth.h | 2 +- 9 files changed, 186 insertions(+), 116 deletions(-) diff --git a/Source/Engine/Graphics/Models/MeshAccessor.h b/Source/Engine/Graphics/Models/MeshAccessor.h index 25fc01a1a..96ead669c 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.h +++ b/Source/Engine/Graphics/Models/MeshAccessor.h @@ -39,51 +39,61 @@ public: FORCE_INLINE int32 GetInt(int32 index) const { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); return (int32)_sampler.Read(_data.Get() + index * _stride).X; } FORCE_INLINE float GetFloat(int32 index) const { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); return _sampler.Read(_data.Get() + index * _stride).X; } FORCE_INLINE Float2 GetFloat2(int32 index) const { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); return Float2(_sampler.Read(_data.Get() + index * _stride)); } FORCE_INLINE Float3 GetFloat3(int32 index) const { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); return Float3(_sampler.Read(_data.Get() + index * _stride)); } FORCE_INLINE Float4 GetFloat4(int32 index) const { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); return _sampler.Read(_data.Get() + index * _stride); } FORCE_INLINE void SetInt(int32 index, const int32 value) { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); _sampler.Write(_data.Get() + index * _stride, Float4((float)value)); } FORCE_INLINE void SetFloat(int32 index, const float value) { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); _sampler.Write(_data.Get() + index * _stride, Float4(value)); } FORCE_INLINE void SetFloat2(int32 index, const Float2& value) { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); _sampler.Write(_data.Get() + index * _stride, Float4(value)); } FORCE_INLINE void SetFloat3(int32 index, const Float3& value) { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); _sampler.Write(_data.Get() + index * _stride, Float4(value)); } FORCE_INLINE void SetFloat4(int32 index, const Float4& value) { + ASSERT_LOW_LAYER(index * _stride < _data.Length()); _sampler.Write(_data.Get() + index * _stride, value); } @@ -94,11 +104,22 @@ public: void Set(Span src); void Set(Span src); void Set(Span src); + template + void Set(const Array& dst) const + { + Set(Span(dst.Get(), dst.Count())); + } // Copies the contents of this stream into a destination data span. void CopyTo(Span dst) const; void CopyTo(Span dst) const; void CopyTo(Span dst) const; + template + void CopyTo(Array& dst) const + { + dst.Resize(GetCount()); + CopyTo(Span(dst.Get(), dst.Count())); + } }; private: diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index 11497e558..ddafd215d 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -1397,6 +1397,16 @@ bool AnimatedModel::GetMeshData(const MeshReference& ref, MeshBufferType type, B return mesh.DownloadDataCPU(type, result, count, layout); } +MeshBase* AnimatedModel::GetMesh(const MeshReference& ref) const +{ + const auto model = SkinnedModel.Get(); + if (!model || model->WaitForLoaded()) + return nullptr; + auto& lod = model->LODs[Math::Min(ref.LODIndex, model->LODs.Count() - 1)]; + auto& mesh = lod.Meshes[Math::Min(ref.MeshIndex, lod.Meshes.Count() - 1)]; + return &mesh; +} + MeshDeformation* AnimatedModel::GetMeshDeformation() const { if (!_deformation) diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index a520d6723..f9182d115 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -479,6 +479,7 @@ public: bool IntersectsEntry(int32 entryIndex, const Ray& ray, Real& distance, Vector3& normal) override; bool IntersectsEntry(const Ray& ray, Real& distance, Vector3& normal, int32& entryIndex) override; bool GetMeshData(const MeshReference& ref, MeshBufferType type, BytesContainer& result, int32& count, GPUVertexLayout** layout) const override; + MeshBase* GetMesh(const MeshReference& ref) const override; void UpdateBounds() override; MeshDeformation* GetMeshDeformation() const override; void OnDeleteObject() override; diff --git a/Source/Engine/Level/Actors/ModelInstanceActor.cpp b/Source/Engine/Level/Actors/ModelInstanceActor.cpp index 527775bfc..b8dc3af1a 100644 --- a/Source/Engine/Level/Actors/ModelInstanceActor.cpp +++ b/Source/Engine/Level/Actors/ModelInstanceActor.cpp @@ -14,6 +14,12 @@ String ModelInstanceActor::MeshReference::ToString() const return String::Format(TEXT("Actor={},LOD={},Mesh={}"), Actor ? Actor->GetNamePath() : String::Empty, LODIndex, MeshIndex); } +MeshBase* ModelInstanceActor::MeshReference::Get() const +{ + auto actor = Actor.Get(); + return actor ? actor->GetMesh(*this) : nullptr; +} + void ModelInstanceActor::SetEntries(const Array& value) { WaitForModelLoad(); diff --git a/Source/Engine/Level/Actors/ModelInstanceActor.h b/Source/Engine/Level/Actors/ModelInstanceActor.h index d5e2498fb..d553d132a 100644 --- a/Source/Engine/Level/Actors/ModelInstanceActor.h +++ b/Source/Engine/Level/Actors/ModelInstanceActor.h @@ -29,6 +29,7 @@ API_CLASS(Abstract) class FLAXENGINE_API ModelInstanceActor : public Actor API_FIELD() int32 MeshIndex = 0; String ToString() const; + MeshBase* Get() const; }; protected: @@ -113,6 +114,7 @@ public: /// /// Extracts mesh buffer data from CPU. Might be cached internally (eg. by Model/SkinnedModel). + /// [Deprecated in 1.12] /// /// Mesh reference. /// Buffer type @@ -120,11 +122,22 @@ public: /// The amount of items inside the result buffer. /// The result layout of the result buffer (for vertex buffers). Optional, pass null to ignore it. /// True if failed, otherwise false. + DEPRECATED("Use GetMesh to resolve mesh reference and access mesh data with MeshAccessor.") virtual bool GetMeshData(const MeshReference& ref, MeshBufferType type, BytesContainer& result, int32& count, GPUVertexLayout** layout = nullptr) const { return true; } + /// + /// Resolves a given mesh reference. + /// + /// Mesh reference. + /// Mesh or null if invalid ref. + virtual MeshBase* GetMesh(const MeshReference& ref) const + { + return nullptr; + } + /// /// Gets the mesh deformation utility for this model instance (optional). /// diff --git a/Source/Engine/Level/Actors/StaticModel.cpp b/Source/Engine/Level/Actors/StaticModel.cpp index 19e4e3f34..fad068bf0 100644 --- a/Source/Engine/Level/Actors/StaticModel.cpp +++ b/Source/Engine/Level/Actors/StaticModel.cpp @@ -665,6 +665,16 @@ bool StaticModel::GetMeshData(const MeshReference& ref, MeshBufferType type, Byt return mesh.DownloadDataCPU(type, result, count, layout); } +MeshBase* StaticModel::GetMesh(const MeshReference& ref) const +{ + const auto model = Model.Get(); + if (!model || model->WaitForLoaded()) + return nullptr; + auto& lod = model->LODs[Math::Min(ref.LODIndex, model->LODs.Count() - 1)]; + auto& mesh = lod.Meshes[Math::Min(ref.MeshIndex, lod.Meshes.Count() - 1)]; + return &mesh; +} + MeshDeformation* StaticModel::GetMeshDeformation() const { if (!_deformation) diff --git a/Source/Engine/Level/Actors/StaticModel.h b/Source/Engine/Level/Actors/StaticModel.h index 4b575a0ed..063638e14 100644 --- a/Source/Engine/Level/Actors/StaticModel.h +++ b/Source/Engine/Level/Actors/StaticModel.h @@ -181,6 +181,7 @@ public: bool IntersectsEntry(int32 entryIndex, const Ray& ray, Real& distance, Vector3& normal) override; bool IntersectsEntry(const Ray& ray, Real& distance, Vector3& normal, int32& entryIndex) override; bool GetMeshData(const MeshReference& ref, MeshBufferType type, BytesContainer& result, int32& count, GPUVertexLayout** layout) const override; + MeshBase* GetMesh(const MeshReference& ref) const override; MeshDeformation* GetMeshDeformation() const override; void UpdateBounds() override; diff --git a/Source/Engine/Physics/Actors/Cloth.cpp b/Source/Engine/Physics/Actors/Cloth.cpp index 1f833fa0e..8c9b996ce 100644 --- a/Source/Engine/Physics/Actors/Cloth.cpp +++ b/Source/Engine/Physics/Actors/Cloth.cpp @@ -208,8 +208,12 @@ void Cloth::SetPaint(Span value) if (_cloth) { // Update cloth particles + MeshAccessor accessor; + MeshBufferType bufferTypes[2] = { MeshBufferType::Index, MeshBufferType::Vertex0 }; + if (accessor.LoadMesh(GetMesh().Get(), false, ToSpan(bufferTypes, 2))) + return; Array invMasses; - CalculateInvMasses(invMasses); + CalculateInvMasses(accessor, invMasses); PhysicsBackend::LockClothParticles(_cloth); PhysicsBackend::SetClothParticles(_cloth, Span(), Span(), ToSpan(invMasses)); PhysicsBackend::SetClothPaint(_cloth, value); @@ -227,18 +231,20 @@ bool Cloth::IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) if (_cloth) { // Precise per-triangle intersection - const ModelInstanceActor::MeshReference mesh = GetMesh(); - if (mesh.Actor == nullptr) + const ModelInstanceActor::MeshReference meshRef = GetMesh(); + if (meshRef.Actor == nullptr) return false; - BytesContainer indicesData; - int32 indicesCount; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) + MeshAccessor accessor; + MeshBufferType bufferTypes[1] = { MeshBufferType::Index }; + if (accessor.LoadMesh(meshRef.Get(), false, ToSpan(bufferTypes, 1))) return false; + auto indices = accessor.Index(); + auto indicesData = indices.GetData(); PhysicsBackend::LockClothParticles(_cloth); const Span particles = PhysicsBackend::GetClothParticles(_cloth); const Transform transform = GetTransform(); - const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); - const int32 trianglesCount = indicesCount / 3; + const bool indices16bit = indices.GetFormat() == PixelFormat::R16_UInt; + const int32 trianglesCount = indices.GetCount() / 3; bool result = false; distance = MAX_Real; for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) @@ -341,18 +347,20 @@ void Cloth::DrawPhysicsDebug(RenderView& view) if (_cloth) { PROFILE_CPU(); - const ModelInstanceActor::MeshReference mesh = GetMesh(); - if (mesh.Actor == nullptr) + const ModelInstanceActor::MeshReference meshRef = GetMesh(); + if (meshRef.Actor == nullptr) return; - BytesContainer indicesData; - int32 indicesCount; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) + MeshAccessor accessor; + MeshBufferType bufferTypes[1] = { MeshBufferType::Index }; + if (accessor.LoadMesh(meshRef.Get(), false, ToSpan(bufferTypes, 1))) return; + auto indices = accessor.Index(); + auto indicesData = indices.GetData(); PhysicsBackend::LockClothParticles(_cloth); const Span particles = PhysicsBackend::GetClothParticles(_cloth); const Transform transform = GetTransform(); - const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); - const int32 trianglesCount = indicesCount / 3; + const bool indices16bit = indices.GetFormat() == PixelFormat::R16_UInt; + const int32 trianglesCount = indices.GetCount() / 3; for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) { const int32 index = triangleIndex * 3; @@ -390,18 +398,20 @@ void Cloth::OnDebugDrawSelected() if (_cloth) { DEBUG_DRAW_WIRE_BOX(_box, Color::Violet.RGBMultiplied(0.8f), 0, true); - const ModelInstanceActor::MeshReference mesh = GetMesh(); - if (mesh.Actor == nullptr) + const ModelInstanceActor::MeshReference meshRef = GetMesh(); + if (meshRef.Actor == nullptr) return; - BytesContainer indicesData; - int32 indicesCount; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) + MeshAccessor accessor; + MeshBufferType bufferTypes[1] = { MeshBufferType::Index }; + if (accessor.LoadMesh(meshRef.Get(), false, ToSpan(bufferTypes, 1))) return; + auto indices = accessor.Index(); + auto indicesData = indices.GetData(); PhysicsBackend::LockClothParticles(_cloth); const Span particles = PhysicsBackend::GetClothParticles(_cloth); const Transform transform = GetTransform(); - const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); - const int32 trianglesCount = indicesCount / 3; + const bool indices16bit = indices.GetFormat() == PixelFormat::R16_UInt; + const int32 trianglesCount = indices.GetCount() / 3; for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) { const int32 index = triangleIndex * 3; @@ -556,26 +566,36 @@ bool Cloth::CreateCloth() // Get mesh data // TODO: consider making it via async task so physics can wait on the cloth setup from mesh data just before next fixed update which gives more time when loading scene - const ModelInstanceActor::MeshReference mesh = GetMesh(); - if (mesh.Actor == nullptr) + const ModelInstanceActor::MeshReference meshRef = GetMesh(); + if (meshRef.Actor == nullptr) return false; PhysicsClothDesc desc; desc.Actor = this; - BytesContainer data; - int32 count; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Vertex0, data, count)) + MeshAccessor accessor; + MeshBufferType bufferTypes[2] = { MeshBufferType::Index, MeshBufferType::Vertex0 }; + if (accessor.LoadMesh(meshRef.Get(), false, ToSpan(bufferTypes, 2))) return true; - // TODO: use MeshAccessor vertex data layout descriptor instead hardcoded position data at the beginning of VB0 - desc.VerticesData = data.Get(); - desc.VerticesCount = count; - desc.VerticesStride = data.Length() / count; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, data, count)) - return true; - desc.IndicesData = data.Get(); - desc.IndicesCount = count; - desc.IndicesStride = data.Length() / count; + auto position = accessor.Position(); + Array tempPositions; + if (position.GetFormat() == PixelFormat::R32G32B32_Float) + { + desc.VerticesData = position.GetData().Get(); + desc.VerticesCount = position.GetCount(); + desc.VerticesStride = position.GetStride(); + } + else + { + position.CopyTo(tempPositions); + desc.VerticesData = tempPositions.Get(); + desc.VerticesCount = tempPositions.Count(); + desc.VerticesStride = sizeof(Float3); + } + auto indices = accessor.Index(); + desc.IndicesData = indices.GetData().Get(); + desc.IndicesCount = indices.GetCount(); + desc.IndicesStride = indices.GetStride(); Array invMasses; - CalculateInvMasses(invMasses); + CalculateInvMasses(accessor, invMasses); desc.InvMassesData = invMasses.Count() == desc.VerticesCount ? invMasses.Get() : nullptr; desc.InvMassesStride = sizeof(float); desc.MaxDistancesData = _paint.Count() == desc.VerticesCount ? _paint.Get() : nullptr; @@ -595,13 +615,13 @@ bool Cloth::CreateCloth() PhysicsBackend::ClearClothInertia(_cloth); // Add cloth mesh deformer - if (auto* deformation = mesh.Actor->GetMeshDeformation()) + if (auto* deformation = meshRef.Actor->GetMeshDeformation()) { Function deformer; deformer.Bind(this); - deformation->AddDeformer(mesh.LODIndex, mesh.MeshIndex, MeshBufferType::Vertex0, deformer); + deformation->AddDeformer(meshRef.LODIndex, meshRef.MeshIndex, MeshBufferType::Vertex0, deformer); if (_simulationSettings.ComputeNormals) - deformation->AddDeformer(mesh.LODIndex, mesh.MeshIndex, MeshBufferType::Vertex1, deformer); + deformation->AddDeformer(meshRef.LODIndex, meshRef.MeshIndex, MeshBufferType::Vertex1, deformer); _meshDeformation = deformation; } @@ -631,7 +651,7 @@ void Cloth::DestroyCloth() #endif } -void Cloth::CalculateInvMasses(Array& invMasses) +void Cloth::CalculateInvMasses(MeshAccessor& accessor, Array& invMasses) { // Use per-particle max distance to evaluate which particles are immovable #if WITH_CLOTH @@ -641,29 +661,22 @@ void Cloth::CalculateInvMasses(Array& invMasses) PROFILE_MEM(Physics); // Get mesh data - const ModelInstanceActor::MeshReference mesh = GetMesh(); - if (mesh.Actor == nullptr) - return; - BytesContainer verticesData; - int32 verticesCount; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Vertex0, verticesData, verticesCount)) - return; - BytesContainer indicesData; - int32 indicesCount; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Index, indicesData, indicesCount)) - return; + auto positions = accessor.Position(); + auto indices = accessor.Index(); + CHECK(positions.IsValid() && indices.IsValid()); + int32 verticesCount = positions.GetCount(); if (_paint.Count() != verticesCount) { // Fix incorrect paint data - LOG(Warning, "Incorrect cloth '{}' paint size {} for mesh '{}' that has {} vertices", GetNamePath(), _paint.Count(), mesh.ToString(), verticesCount); + LOG(Warning, "Incorrect cloth '{}' paint size {} for mesh '{}' that has {} vertices", GetNamePath(), _paint.Count(), GetMesh().ToString(), verticesCount); int32 countBefore = _paint.Count(); _paint.Resize(verticesCount); for (int32 i = countBefore; i < verticesCount; i++) _paint.Get()[i] = 0.0f; } - const int32 verticesStride = verticesData.Length() / verticesCount; - const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); - const int32 trianglesCount = indicesCount / 3; + const bool indices16bit = indices.GetFormat() == PixelFormat::R16_UInt; + const int32 trianglesCount = indices.GetCount() / 3; + auto indicesData = indices.GetData(); // Sum triangle area for each influenced particle invMasses.Resize(verticesCount); @@ -676,12 +689,9 @@ void Cloth::CalculateInvMasses(Array& invMasses) const int32 i0 = indicesData.Get()[index]; const int32 i1 = indicesData.Get()[index + 1]; const int32 i2 = indicesData.Get()[index + 2]; - // TODO: use MeshAccessor vertex data layout descriptor instead hardcoded position data at the beginning of VB0 -#define GET_POS(i) *(Float3*)((byte*)verticesData.Get() + i * verticesStride) - const Float3 v0(GET_POS(i0)); - const Float3 v1(GET_POS(i1)); - const Float3 v2(GET_POS(i2)); -#undef GET_POS + const Float3 v0(positions.GetFloat3(i0)); + const Float3 v1(positions.GetFloat3(i1)); + const Float3 v2(positions.GetFloat3(i2)); const float area = Float3::TriangleArea(v0, v1, v2); invMasses.Get()[i0] += area; invMasses.Get()[i1] += area; @@ -696,12 +706,9 @@ void Cloth::CalculateInvMasses(Array& invMasses) const int32 i0 = indicesData.Get()[index]; const int32 i1 = indicesData.Get()[index + 1]; const int32 i2 = indicesData.Get()[index + 2]; - // TODO: use MeshAccessor vertex data layout descriptor instead hardcoded position data at the beginning of VB0 -#define GET_POS(i) *(Float3*)((byte*)verticesData.Get() + i * verticesStride) - const Float3 v0(GET_POS(i0)); - const Float3 v1(GET_POS(i1)); - const Float3 v2(GET_POS(i2)); -#undef GET_POS + const Float3 v0(positions.GetFloat3(i0)); + const Float3 v1(positions.GetFloat3(i1)); + const Float3 v2(positions.GetFloat3(i2)); const float area = Float3::TriangleArea(v0, v1, v2); invMasses.Get()[i0] += area; invMasses.Get()[i1] += area; @@ -787,25 +794,22 @@ bool Cloth::OnPreUpdate() { if (animatedModel->GraphInstance.NodesPose.IsEmpty() || _paint.IsEmpty()) return false; - const ModelInstanceActor::MeshReference mesh = GetMesh(); - if (mesh.Actor == nullptr) - return false; - BytesContainer verticesData; - int32 verticesCount; - GPUVertexLayout* layout; - if (mesh.Actor->GetMeshData(mesh, MeshBufferType::Vertex0, verticesData, verticesCount, &layout)) + const ModelInstanceActor::MeshReference meshRef = GetMesh(); + if (meshRef.Actor == nullptr) return false; MeshAccessor accessor; - if (accessor.LoadBuffer(MeshBufferType::Vertex0, verticesData, layout)) + MeshBufferType bufferTypes[1] = { MeshBufferType::Vertex0 }; + if (accessor.LoadMesh(meshRef.Get(), false, ToSpan(bufferTypes, 1))) return false; auto positionStream = accessor.Position(); auto blendIndicesStream = accessor.BlendIndices(); auto blendWeightsStream = accessor.BlendWeights(); if (!positionStream.IsValid() || !blendIndicesStream.IsValid() || !blendWeightsStream.IsValid()) return false; + const int32 verticesCount = positionStream.GetCount(); if (verticesCount != _paint.Count()) { - LOG(Warning, "Incorrect cloth '{}' paint size {} for mesh '{}' that has {} vertices", GetNamePath(), _paint.Count(), mesh.ToString(), verticesCount); + LOG(Warning, "Incorrect cloth '{}' paint size {} for mesh '{}' that has {} vertices", GetNamePath(), _paint.Count(), meshRef.ToString(), verticesCount); return false; } PROFILE_CPU_NAMED("Skinned Pose"); @@ -934,49 +938,53 @@ void Cloth::RunClothDeformer(const MeshBase* mesh, MeshDeformationData& deformat // Calculate normals Array normals; const ModelInstanceActor::MeshReference meshRef = GetMesh(); - BytesContainer indicesData; - int32 indicesCount; - if ((_simulationSettings.ComputeNormals || deformation.Type == MeshBufferType::Vertex1) && - meshRef.Actor && !meshRef.Actor->GetMeshData(meshRef, MeshBufferType::Index, indicesData, indicesCount)) + if ((_simulationSettings.ComputeNormals || deformation.Type == MeshBufferType::Vertex1) && meshRef.Actor) { - PROFILE_CPU_NAMED("Normals"); - // TODO: optimize memory allocs (eg. use shared allocator) - normals.Resize(vbCount); - Platform::MemoryClear(normals.Get(), vbCount * sizeof(Float3)); - const bool indices16bit = indicesData.Length() / indicesCount == sizeof(uint16); - const int32 trianglesCount = indicesCount / 3; - if (indices16bit) + MeshAccessor accessor; + MeshBufferType bufferTypes[1] = { MeshBufferType::Index }; + if (!accessor.LoadMesh(meshRef.Get(), false, ToSpan(bufferTypes, 1))) { - for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) + PROFILE_CPU_NAMED("Normals"); + auto indices = accessor.Index(); + auto indicesData = indices.GetData(); + // TODO: optimize memory allocs (eg. use shared allocator) + normals.Resize(vbCount); + Platform::MemoryClear(normals.Get(), vbCount * sizeof(Float3)); + const bool indices16bit = indices.GetFormat() == PixelFormat::R16_UInt; + const int32 trianglesCount = indices.GetCount() / 3; + if (indices16bit) { - const int32 index = triangleIndex * 3; - const int32 i0 = indicesData.Get()[index]; - const int32 i1 = indicesData.Get()[index + 1]; - const int32 i2 = indicesData.Get()[index + 2]; - const Float3 v0(particles.Get()[i0]); - const Float3 v1(particles.Get()[i1]); - const Float3 v2(particles.Get()[i2]); - const Float3 normal = Float3::Cross(v1 - v0, v2 - v0); - normals.Get()[i0] += normal; - normals.Get()[i1] += normal; - normals.Get()[i2] += normal; + for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) + { + const int32 index = triangleIndex * 3; + const int32 i0 = indicesData.Get()[index]; + const int32 i1 = indicesData.Get()[index + 1]; + const int32 i2 = indicesData.Get()[index + 2]; + const Float3 v0(particles.Get()[i0]); + const Float3 v1(particles.Get()[i1]); + const Float3 v2(particles.Get()[i2]); + const Float3 normal = Float3::Cross(v1 - v0, v2 - v0); + normals.Get()[i0] += normal; + normals.Get()[i1] += normal; + normals.Get()[i2] += normal; + } } - } - else - { - for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) + else { - const int32 index = triangleIndex * 3; - const int32 i0 = indicesData.Get()[index]; - const int32 i1 = indicesData.Get()[index + 1]; - const int32 i2 = indicesData.Get()[index + 2]; - const Float3 v0(particles.Get()[i0]); - const Float3 v1(particles.Get()[i1]); - const Float3 v2(particles.Get()[i2]); - const Float3 normal = Float3::Cross(v1 - v0, v2 - v0); - normals.Get()[i0] += normal; - normals.Get()[i1] += normal; - normals.Get()[i2] += normal; + for (int32 triangleIndex = 0; triangleIndex < trianglesCount; triangleIndex++) + { + const int32 index = triangleIndex * 3; + const int32 i0 = indicesData.Get()[index]; + const int32 i1 = indicesData.Get()[index + 1]; + const int32 i2 = indicesData.Get()[index + 2]; + const Float3 v0(particles.Get()[i0]); + const Float3 v1(particles.Get()[i1]); + const Float3 v2(particles.Get()[i2]); + const Float3 normal = Float3::Cross(v1 - v0, v2 - v0); + normals.Get()[i0] += normal; + normals.Get()[i1] += normal; + normals.Get()[i2] += normal; + } } } } diff --git a/Source/Engine/Physics/Actors/Cloth.h b/Source/Engine/Physics/Actors/Cloth.h index f83f1c499..8d6f13370 100644 --- a/Source/Engine/Physics/Actors/Cloth.h +++ b/Source/Engine/Physics/Actors/Cloth.h @@ -371,6 +371,6 @@ private: ImplementPhysicsDebug; bool CreateCloth(); void DestroyCloth(); - void CalculateInvMasses(Array& invMasses); + void CalculateInvMasses(class MeshAccessor& accessor, Array& invMasses); void RunClothDeformer(const MeshBase* mesh, struct MeshDeformationData& deformation); }; From 13f5222ec75ccc90a4a5a86db72293c1bbac5f06 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Mar 2026 23:00:51 +0100 Subject: [PATCH 22/24] Fix `MeshAccessor` to properly reference model asset data during the usage --- Source/Engine/Graphics/Models/MeshAccessor.h | 3 +++ Source/Engine/Graphics/Models/MeshBase.cpp | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/Source/Engine/Graphics/Models/MeshAccessor.h b/Source/Engine/Graphics/Models/MeshAccessor.h index 96ead669c..820a6db43 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.h +++ b/Source/Engine/Graphics/Models/MeshAccessor.h @@ -126,8 +126,11 @@ private: BytesContainer _data[(int32)MeshBufferType::MAX]; PixelFormat _formats[(int32)MeshBufferType::MAX] = {}; GPUVertexLayout* _layouts[(int32)MeshBufferType::MAX] = {}; + Array> _usedModels; public: + ~MeshAccessor(); + /// /// Loads the data from the mesh. /// diff --git a/Source/Engine/Graphics/Models/MeshBase.cpp b/Source/Engine/Graphics/Models/MeshBase.cpp index bbec523a2..275a072d1 100644 --- a/Source/Engine/Graphics/Models/MeshBase.cpp +++ b/Source/Engine/Graphics/Models/MeshBase.cpp @@ -188,6 +188,16 @@ void MeshAccessor::Stream::CopyTo(Span<::Color> dst) const } } +MeshAccessor::~MeshAccessor() +{ + for (ModelBase* model : _usedModels) + { + if (model->Storage) + model->Storage->UnlockChunks(); + model->RemoveReference(); + } +} + bool MeshAccessor::LoadMesh(const MeshBase* mesh, bool forceGpu, Span buffers) { CHECK_RETURN(mesh, true); @@ -196,6 +206,14 @@ bool MeshAccessor::LoadMesh(const MeshBase* mesh, bool forceGpu, Span(allBuffers, ARRAY_COUNT(allBuffers)); Array> meshBuffers; Array> meshLayouts; + if (ModelBase* model = mesh->GetModelBase()) + { + // Maintain reference to mesh data (it's buffers might be referenced, not copied) + model->AddReference(); + if (model->Storage) + model->Storage->LockChunks(); + _usedModels.Add(model); + } if (mesh->DownloadData(buffers, meshBuffers, meshLayouts, forceGpu)) return true; for (int32 i = 0; i < buffers.Length(); i++) From 7b3cfd989f257a86433a52413bd16b00946cea74 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Mar 2026 23:01:09 +0100 Subject: [PATCH 23/24] Add memory profiler category for Cloth --- Source/Engine/Physics/Actors/Cloth.cpp | 14 +++++++------- .../Engine/Physics/PhysX/PhysicsBackendPhysX.cpp | 4 +++- Source/Engine/Profiler/ProfilerMemory.cpp | 1 + Source/Engine/Profiler/ProfilerMemory.h | 2 ++ 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Source/Engine/Physics/Actors/Cloth.cpp b/Source/Engine/Physics/Actors/Cloth.cpp index 8c9b996ce..3e0eb42d4 100644 --- a/Source/Engine/Physics/Actors/Cloth.cpp +++ b/Source/Engine/Physics/Actors/Cloth.cpp @@ -133,7 +133,7 @@ Array Cloth::GetParticles() const if (_cloth) { PROFILE_CPU(); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); PhysicsBackend::LockClothParticles(_cloth); const Span particles = PhysicsBackend::GetClothParticles(_cloth); result.Resize(particles.Length()); @@ -150,7 +150,7 @@ Array Cloth::GetParticles() const void Cloth::SetParticles(Span value) { PROFILE_CPU(); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); #if USE_CLOTH_SANITY_CHECKS { // Sanity check @@ -180,7 +180,7 @@ Span Cloth::GetPaint() const void Cloth::SetPaint(Span value) { PROFILE_CPU(); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); #if USE_CLOTH_SANITY_CHECKS { // Sanity check @@ -312,7 +312,7 @@ void Cloth::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) { Actor::Deserialize(stream, modifier); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); DESERIALIZE_MEMBER(Mesh, _mesh); _mesh.Actor = nullptr; // Don't store this reference DESERIALIZE_MEMBER(Force, _forceSettings); @@ -552,7 +552,7 @@ bool Cloth::CreateCloth() { #if WITH_CLOTH PROFILE_CPU(); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); // Skip if all vertices are fixed so cloth sim doesn't make sense if (_paint.HasItems()) @@ -658,7 +658,7 @@ void Cloth::CalculateInvMasses(MeshAccessor& accessor, Array& invMasses) if (_paint.IsEmpty()) return; PROFILE_CPU(); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); // Get mesh data auto positions = accessor.Position(); @@ -929,7 +929,7 @@ void Cloth::RunClothDeformer(const MeshBase* mesh, MeshDeformationData& deformat return; #if WITH_CLOTH PROFILE_CPU_NAMED("Cloth"); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); PhysicsBackend::LockClothParticles(_cloth); const Span particles = PhysicsBackend::GetClothParticles(_cloth); auto vbCount = (uint32)mesh->GetVertexCount(); diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp index b5efa9d73..64fe6fd65 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -1177,6 +1177,7 @@ void ScenePhysX::UpdateVehicles(float dt) void ScenePhysX::PreSimulateCloth(int32 i) { PROFILE_CPU(); + PROFILE_MEM(PhysicsCloth); auto clothPhysX = ClothsList[i]; auto& clothSettings = Cloths[clothPhysX]; @@ -1379,6 +1380,7 @@ void ScenePhysX::UpdateCloths(float dt) if (!clothSolver || ClothsList.IsEmpty()) return; PROFILE_CPU_NAMED("Physics.Cloth"); + PROFILE_MEM(PhysicsCloth); { PROFILE_CPU_NAMED("Pre"); @@ -3994,7 +3996,7 @@ void PhysicsBackend::RemoveVehicle(void* scene, WheeledVehicle* actor) void* PhysicsBackend::CreateCloth(const PhysicsClothDesc& desc) { PROFILE_CPU(); - PROFILE_MEM(Physics); + PROFILE_MEM(PhysicsCloth); #if USE_CLOTH_SANITY_CHECKS { // Sanity check diff --git a/Source/Engine/Profiler/ProfilerMemory.cpp b/Source/Engine/Profiler/ProfilerMemory.cpp index 6b8f18ce3..eaa6cbfcd 100644 --- a/Source/Engine/Profiler/ProfilerMemory.cpp +++ b/Source/Engine/Profiler/ProfilerMemory.cpp @@ -263,6 +263,7 @@ void InitProfilerMemory(const Char* cmdLine, int32 stage) INIT_PARENT(Level, LevelTerrain); INIT_PARENT(Navigation, NavigationMesh); INIT_PARENT(Navigation, NavigationBuilding); + INIT_PARENT(Physics, PhysicsCloth); INIT_PARENT(Scripting, ScriptingVisual); INIT_PARENT(Scripting, ScriptingCSharp); INIT_PARENT(ScriptingCSharp, ScriptingCSharpGCCommitted); diff --git a/Source/Engine/Profiler/ProfilerMemory.h b/Source/Engine/Profiler/ProfilerMemory.h index 9177ae6e7..ab33a42f3 100644 --- a/Source/Engine/Profiler/ProfilerMemory.h +++ b/Source/Engine/Profiler/ProfilerMemory.h @@ -120,6 +120,8 @@ public: // Total physics memory. Physics, + // Cloth simulation and particles data. + PhysicsCloth, // Total scripting memory allocated by game. Scripting, From e8134803c40605d7b90001b043a3644925e08101 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 24 Mar 2026 23:13:50 +0100 Subject: [PATCH 24/24] Fix invalid index buffer format returned by `MeshAccessor` when model is not yet loaded #4017 --- Source/Engine/Graphics/Models/MeshAccessor.cs | 12 +++++++++--- Source/Engine/Graphics/Models/MeshBase.cpp | 17 ++++++++++++++--- Source/Engine/Graphics/Models/MeshBase.h | 2 +- .../ModelTool/MeshAccelerationStructure.cpp | 2 +- 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs index 29aa86c18..ffe509476 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.cs +++ b/Source/Engine/Graphics/Models/MeshAccessor.cs @@ -413,10 +413,16 @@ namespace FlaxEngine return true; for (int i = 0; i < buffersLocal.Length; i++) { - _data[(int)buffersLocal[i]] = meshBuffers[i]; - _layouts[(int)buffersLocal[i]] = meshLayouts[i]; + int buffer = (int)buffersLocal[i]; + _data[buffer] = meshBuffers[i]; + _layouts[buffer] = meshLayouts[i]; + + // Get format if using a single item (eg. index buffer type) + var format = PixelFormat.Unknown; + if (meshLayouts[i] && meshLayouts[i].Elements.Length == 1) + format = meshLayouts[i].Elements[0].Format; + _formats[buffer] = format; } - _formats[(int)MeshBufferType.Index] = mesh.Use16BitIndexBuffer ? PixelFormat.R16_UInt : PixelFormat.R32_UInt; return false; } diff --git a/Source/Engine/Graphics/Models/MeshBase.cpp b/Source/Engine/Graphics/Models/MeshBase.cpp index 275a072d1..81fffb3e7 100644 --- a/Source/Engine/Graphics/Models/MeshBase.cpp +++ b/Source/Engine/Graphics/Models/MeshBase.cpp @@ -218,10 +218,16 @@ bool MeshAccessor::LoadMesh(const MeshBase* mesh, bool forceGpu, SpanGetElements().Count() == 1) + format = meshLayouts[i]->GetElements()[0].Format; + _formats[buffer] = format; } - _formats[(int32)MeshBufferType::Index] = mesh->Use16BitIndexBuffer() ? PixelFormat::R16_UInt : PixelFormat::R32_UInt; return false; } @@ -732,6 +738,9 @@ bool MeshBase::DownloadDataCPU(MeshBufferType type, BytesContainer& result, int3 _cachedVertexBufferCount = meshData.Vertices; _cachedIndexBufferCount = (int32)meshData.Triangles * 3; _cachedIndexBuffer.Copy((const byte*)meshData.IBData, _cachedIndexBufferCount * (int32)meshData.IBStride); + GPUVertexLayout::Elements ibLayout; + ibLayout.Add({ VertexElement::Types::Attribute, 0, 0, 0, meshData.IBStride == sizeof(uint16) ? PixelFormat::R16_UInt : PixelFormat::R32_UInt }); + _cachedVertexLayouts[3] = GPUVertexLayout::Get(ibLayout); for (int32 vb = 0; vb < meshData.VBData.Count(); vb++) { _cachedVertexBuffers[vb].Copy((const byte*)meshData.VBData[vb], (int32)(meshData.VBLayout[vb]->GetStride() * meshData.Vertices)); @@ -746,6 +755,8 @@ bool MeshBase::DownloadDataCPU(MeshBufferType type, BytesContainer& result, int3 case MeshBufferType::Index: result.Link(_cachedIndexBuffer); count = _cachedIndexBufferCount; + if (layout) + *layout = _cachedVertexLayouts[3]; break; case MeshBufferType::Vertex0: result.Link(_cachedVertexBuffers[0]); diff --git a/Source/Engine/Graphics/Models/MeshBase.h b/Source/Engine/Graphics/Models/MeshBase.h index 1c76df166..cb51f466b 100644 --- a/Source/Engine/Graphics/Models/MeshBase.h +++ b/Source/Engine/Graphics/Models/MeshBase.h @@ -50,7 +50,7 @@ protected: GPUBuffer* _indexBuffer = nullptr; mutable BytesContainer _cachedVertexBuffers[MODEL_MAX_VB]; - mutable GPUVertexLayout* _cachedVertexLayouts[MODEL_MAX_VB] = {}; + mutable GPUVertexLayout* _cachedVertexLayouts[MODEL_MAX_VB + 1] = {}; mutable BytesContainer _cachedIndexBuffer; mutable int32 _cachedIndexBufferCount = 0, _cachedVertexBufferCount = 0; diff --git a/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp b/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp index 297a6f868..7dd33c12a 100644 --- a/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp +++ b/Source/Engine/Tools/ModelTool/MeshAccelerationStructure.cpp @@ -349,7 +349,7 @@ void MeshAccelerationStructure::Add(Model* model, int32 lodIndex) 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.Use16BitIndexBuffer = indexStream.GetFormat() == PixelFormat::R16_UInt; meshData.Bounds = mesh.GetBox(); } }