From 65b35a4b8adebe264618976d7ec5dc9df99b12d5 Mon Sep 17 00:00:00 2001 From: Saas Date: Wed, 18 Mar 2026 21:40:22 +0100 Subject: [PATCH 01/15] auto scroll to new track when adding track to timeline --- Source/Editor/GUI/Timeline/Timeline.cs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs index 15e7eb953..0cd0cdc04 100644 --- a/Source/Editor/GUI/Timeline/Timeline.cs +++ b/Source/Editor/GUI/Timeline/Timeline.cs @@ -229,6 +229,7 @@ namespace FlaxEditor.GUI.Timeline private List _mediaMoveStartTracks; private byte[][] _mediaMoveStartData; private float _zoom = 1.0f; + private float _tracksVScrollTarget = 0f; private bool _isMovingPositionHandle; private bool _canPlayPause = true, _canStop = true; private List _batchedUndoActions; @@ -898,6 +899,8 @@ namespace FlaxEditor.GUI.Timeline }; UpdatePositionHandle(); PlaybackState = PlaybackStates.Disabled; + + _tracksVScrollTarget = _tracksPanelArea.VScrollBar.TargetValue; } private void UpdatePositionHandle() @@ -1305,6 +1308,10 @@ namespace FlaxEditor.GUI.Timeline MarkAsEdited(); if (withUndo) Undo?.AddAction(new AddRemoveTrackAction(this, track, true)); + + // Scroll to track + _tracksPanelArea.ScrollViewTo(track); + _tracksVScrollTarget = _tracksPanelArea.VScrollBar.TargetValue; } /// @@ -2033,12 +2040,18 @@ namespace FlaxEditor.GUI.Timeline base.Update(deltaTime); // Synchronize scroll vertical bars for tracks and media panels to keep the view in sync - var scroll1 = _tracksPanelArea.VScrollBar; - var scroll2 = _backgroundArea.VScrollBar; - if (scroll1.IsThumbClicked || _tracksPanelArea.IsMouseOver) - scroll2.TargetValue = scroll1.Value; + var tracksVScroll = _tracksPanelArea.VScrollBar; + var backgroundVScroll = _backgroundArea.VScrollBar; + bool forceBackgroundToTracksScroll = !Mathf.WithinEpsilon(_tracksVScrollTarget - tracksVScroll.Value, 0f, 5f); + if (tracksVScroll.IsThumbClicked || _tracksPanelArea.IsMouseOver || forceBackgroundToTracksScroll) + { + backgroundVScroll.TargetValue = tracksVScroll.Value; + } else - scroll1.TargetValue = scroll2.Value; + { + _tracksVScrollTarget = 0f; + tracksVScroll.TargetValue = backgroundVScroll.Value; + } // Batch undo actions if (_batchedUndoActions != null && _batchedUndoActions.Count != 0) From 503a0e6763a735ccc00cdafdbeced545c0d5ece7 Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 19 Mar 2026 17:33:45 +0100 Subject: [PATCH 02/15] add slider to adjust skeleton name font size --- .../Viewport/Previews/AnimatedModelPreview.cs | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs index a678e868a..5bb33ef63 100644 --- a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs +++ b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs @@ -2,6 +2,7 @@ using System; using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.GUI.Input; using FlaxEngine; using Object = FlaxEngine.Object; @@ -14,7 +15,7 @@ namespace FlaxEditor.Viewport.Previews public class AnimatedModelPreview : AssetPreview { private AnimatedModel _previewModel; - private ContextMenuButton _showNodesButton, _showBoundsButton, _showFloorButton, _showNodesNamesButton; + private ContextMenuButton _showNodesButton, _showBoundsButton, _showFloorButton, _showNodesNamesButton, _nodeNameSizeButton; private bool _showNodes, _showBounds, _showFloor, _showNodesNames; private StaticModel _floorModel; private bool _playAnimation, _playAnimationOnce; @@ -110,9 +111,16 @@ namespace FlaxEditor.Viewport.Previews ShowDebugDraw = true; if (_showNodesNamesButton != null) _showNodesNamesButton.Checked = value; + if (_nodeNameSizeButton != null) + _nodeNameSizeButton.Enabled = value; } } + /// + /// The font size used in the node name debug draw. + /// + public int NodeNamesSize = 10; + /// /// Gets or sets a value indicating whether show animated model bounding box debug view. /// @@ -210,6 +218,15 @@ namespace FlaxEditor.Viewport.Previews _showFloorButton.CloseMenuOnClick = false; } + _nodeNameSizeButton = ViewWidgetButtonMenu.AddButton("Skeleton Names Size"); + _nodeNameSizeButton.CloseMenuOnClick = false; + var nodeNameSizeValue = new IntValueBox(NodeNamesSize, 118, 2, 70.0f, 1, 32) + { + Parent = _nodeNameSizeButton + }; + _nodeNameSizeButton.Enabled = ShowNodesNames; + nodeNameSizeValue.ValueChanged += () => NodeNamesSize = nodeNameSizeValue.Value; + // Enable shadows PreviewLight.ShadowsMode = ShadowsCastingMode.All; PreviewLight.CascadeCount = 3; @@ -371,7 +388,7 @@ namespace FlaxEditor.Viewport.Previews if (nodesMask != null && !nodesMask[nodeIndex]) continue; //var t = new Transform(pose[nodeIndex].TranslationVector, Quaternion.Identity, new Float3(0.1f)); - DebugDraw.DrawText(nodes[nodeIndex].Name, pose[nodeIndex].TranslationVector, Color.White, 20, 0.0f, 0.1f); + DebugDraw.DrawText(nodes[nodeIndex].Name, pose[nodeIndex].TranslationVector, Color.White, NodeNamesSize, 0.0f, 0.25f); } } } From 9505be310fbb01193ee2e9b077f910b5d304545c Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 19 Mar 2026 17:34:01 +0100 Subject: [PATCH 03/15] fix spelling in ShowBitDepth --- Source/Editor/Content/Import/AudioImportSettings.cs | 2 +- Source/Engine/Tools/AudioTool/AudioTool.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Content/Import/AudioImportSettings.cs b/Source/Editor/Content/Import/AudioImportSettings.cs index b645c1509..a0af8c154 100644 --- a/Source/Editor/Content/Import/AudioImportSettings.cs +++ b/Source/Editor/Content/Import/AudioImportSettings.cs @@ -12,7 +12,7 @@ namespace FlaxEngine.Tools { partial struct Options { - private bool ShowBtiDepth => Format != AudioFormat.Vorbis; + private bool ShowBitDepth => Format != AudioFormat.Vorbis; } } } diff --git a/Source/Engine/Tools/AudioTool/AudioTool.h b/Source/Engine/Tools/AudioTool/AudioTool.h index b0c9c4c76..289b61c72 100644 --- a/Source/Engine/Tools/AudioTool/AudioTool.h +++ b/Source/Engine/Tools/AudioTool/AudioTool.h @@ -70,7 +70,7 @@ public: /// /// The size of a single sample in bits. The clip will be converted to this bit depth on import. /// - API_FIELD(Attributes="EditorOrder(50), VisibleIf(nameof(ShowBtiDepth))") + API_FIELD(Attributes="EditorOrder(50), VisibleIf(nameof(ShowBitDepth))") BitDepth BitDepth = BitDepth::_16; String ToString() const; From 699fb1260426ee1068b0ad7dae2d4b4d176e8d3f Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 19 Mar 2026 17:35:57 +0100 Subject: [PATCH 04/15] improve Properties Panel layout for SkinnedModel --- Source/Engine/Level/Actors/AnimatedModel.h | 42 +++++++++++++--------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index a520d6723..b62b165c6 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -98,99 +98,99 @@ public: /// /// The skinned model asset used for rendering. /// - API_FIELD(Attributes="EditorOrder(10), DefaultValue(null), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(10), DefaultValue(null), EditorDisplay(\"Skeleton\")") AssetReference SkinnedModel; /// /// The animation graph asset used for the skinned mesh skeleton bones evaluation (controls the animation). /// - API_FIELD(Attributes="EditorOrder(15), DefaultValue(null), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(15), DefaultValue(null), EditorDisplay(\"Skeleton\")") AssetReference AnimationGraph; /// /// If true, use per-bone motion blur on this skeletal model. It requires additional rendering, can be disabled to save performance. /// - API_FIELD(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Rendering\")") bool PerBoneMotionBlur = true; /// /// If true, animation speed will be affected by the global timescale parameter. /// - API_FIELD(Attributes="EditorOrder(30), DefaultValue(true), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(30), DefaultValue(true), EditorDisplay(\"Update\")") bool UseTimeScale = true; /// /// If true, the animation will be updated even when an actor cannot be seen by any camera. Otherwise, the animations themselves will also stop running when the actor is off-screen. /// - API_FIELD(Attributes="EditorOrder(40), DefaultValue(false), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(40), DefaultValue(false), EditorDisplay(\"Update\")") bool UpdateWhenOffscreen = false; /// /// The animation update delta timescale. Can be used to speed up animation playback or create slow motion effect. /// - API_FIELD(Attributes="EditorOrder(45), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(45), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Update\")") float UpdateSpeed = 1.0f; /// /// The animation update mode. Can be used to optimize the performance. /// - API_FIELD(Attributes="EditorOrder(50), DefaultValue(AnimationUpdateMode.Auto), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(50), DefaultValue(AnimationUpdateMode.Auto), EditorDisplay(\"Update\")") AnimationUpdateMode UpdateMode = AnimationUpdateMode::Auto; /// /// The master scale parameter for the actor bounding box. Helps to reduce mesh flickering effect on screen edges. /// - API_FIELD(Attributes="EditorOrder(60), DefaultValue(1.5f), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(60), DefaultValue(1.5f), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Rendering\")") float BoundsScale = 1.5f; /// /// The custom bounds(in actor local space). If set to empty bounds then source skinned model bind pose bounds will be used. /// - API_FIELD(Attributes="EditorOrder(70), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(70), EditorDisplay(\"Rendering\")") BoundingBox CustomBounds = BoundingBox::Zero; /// /// The model Level Of Detail bias value. Allows to increase or decrease rendered model quality. /// - API_FIELD(Attributes="EditorOrder(80), DefaultValue(0), Limit(-100, 100, 0.1f), EditorDisplay(\"Skinned Model\", \"LOD Bias\")") + API_FIELD(Attributes="EditorOrder(80), DefaultValue(0), Limit(-100, 100, 0.1f), EditorDisplay(\"Rendering\", \"LOD Bias\")") int32 LODBias = 0; /// /// Gets the model forced Level Of Detail index. Allows to bind the given model LOD to show. Value -1 disables this feature. /// - API_FIELD(Attributes="EditorOrder(90), DefaultValue(-1), Limit(-1, 100, 0.1f), EditorDisplay(\"Skinned Model\", \"Forced LOD\")") + API_FIELD(Attributes="EditorOrder(90), DefaultValue(-1), Limit(-1, 100, 0.1f), EditorDisplay(\"Rendering\", \"Forced LOD\")") int32 ForcedLOD = -1; /// /// The draw passes to use for rendering this object. /// - API_FIELD(Attributes="EditorOrder(100), DefaultValue(DrawPass.Default), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(100), DefaultValue(DrawPass.Default), EditorDisplay(\"Rendering\")") DrawPass DrawModes = DrawPass::Default; /// /// The object sort order key used when sorting drawable objects during rendering. Use lower values to draw object before others, higher values are rendered later (on top). Can be used to control transparency drawing. /// - API_FIELD(Attributes="EditorDisplay(\"Skinned Model\"), EditorOrder(110), DefaultValue(0)") + API_FIELD(Attributes="EditorOrder(110), DefaultValue(0), EditorDisplay(\"Rendering\")") int8 SortOrder = 0; /// /// The shadows casting mode. /// [Deprecated on 26.10.2022, expires on 26.10.2024] /// - API_FIELD(Attributes="EditorOrder(110), DefaultValue(ShadowsCastingMode.All), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(110), DefaultValue(ShadowsCastingMode.All), EditorDisplay(\"Rendering\")") DEPRECATED() ShadowsCastingMode ShadowsMode = ShadowsCastingMode::All; /// /// The animation root motion apply target. If not specified the animated model will apply it itself. /// - API_FIELD(Attributes="EditorOrder(120), DefaultValue(null), EditorDisplay(\"Skinned Model\")") + API_FIELD(Attributes="EditorOrder(120), DefaultValue(null), EditorDisplay(\"Skeleton\")") ScriptingObjectReference RootMotionTarget; #if USE_EDITOR /// - /// If checked, the skeleton pose will be shawn during debug shapes drawing. + /// If checked, the skeleton pose will be shown during debug shapes drawing. /// - API_FIELD(Attributes="EditorOrder(200), EditorDisplay(\"Skinned Model\")") bool ShowDebugDrawSkeleton = false; + API_FIELD(Attributes="EditorOrder(200), EditorDisplay(\"Debug\"), VisibleIf(nameof(ShowDebugDrawOptions))") bool ShowDebugDrawSkeleton = false; #endif public: @@ -440,6 +440,14 @@ public: API_FUNCTION() bool IsPlayingSlotAnimation(const StringView& slotName, Animation* anim = nullptr); private: + /// + /// Used to hide options if when the skinned model or animation graph is null. + /// + API_PROPERTY(Attributes = "HideInEditor, NoSerialize") bool GetShowDebugDrawOptions() const + { + return SkinnedModel != nullptr && AnimationGraph != nullptr; + } + void ApplyRootMotion(const Transform& rootMotionDelta); void SyncParameters(); void RunBlendShapeDeformer(const MeshBase* mesh, struct MeshDeformationData& deformation); From d77e1e9a532575b5a1a1e4ab84adfc334d8764c5 Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 19 Mar 2026 18:25:00 +0100 Subject: [PATCH 05/15] wrap GetShowDebugDrawOptions() with USE_EDITOR preprocessor --- Source/Engine/Level/Actors/AnimatedModel.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index b62b165c6..46ed275c0 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -440,6 +440,7 @@ public: API_FUNCTION() bool IsPlayingSlotAnimation(const StringView& slotName, Animation* anim = nullptr); private: +#if USE_EDITOR /// /// Used to hide options if when the skinned model or animation graph is null. /// @@ -447,6 +448,7 @@ private: { return SkinnedModel != nullptr && AnimationGraph != nullptr; } +#endif void ApplyRootMotion(const Transform& rootMotionDelta); void SyncParameters(); From a831e15bf7ba6310e124a00592bd7192013631a8 Mon Sep 17 00:00:00 2001 From: Saas Date: Thu, 19 Mar 2026 20:47:34 +0100 Subject: [PATCH 06/15] fix rare bug where tracks pannel wouldn't scroll all the way through --- Source/Editor/GUI/Timeline/Timeline.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs index 0cd0cdc04..516c30544 100644 --- a/Source/Editor/GUI/Timeline/Timeline.cs +++ b/Source/Editor/GUI/Timeline/Timeline.cs @@ -229,7 +229,7 @@ namespace FlaxEditor.GUI.Timeline private List _mediaMoveStartTracks; private byte[][] _mediaMoveStartData; private float _zoom = 1.0f; - private float _tracksVScrollTarget = 0f; + private float _tracksVScrollTarget; private bool _isMovingPositionHandle; private bool _canPlayPause = true, _canStop = true; private List _batchedUndoActions; @@ -899,8 +899,6 @@ namespace FlaxEditor.GUI.Timeline }; UpdatePositionHandle(); PlaybackState = PlaybackStates.Disabled; - - _tracksVScrollTarget = _tracksPanelArea.VScrollBar.TargetValue; } private void UpdatePositionHandle() @@ -2042,14 +2040,20 @@ namespace FlaxEditor.GUI.Timeline // Synchronize scroll vertical bars for tracks and media panels to keep the view in sync var tracksVScroll = _tracksPanelArea.VScrollBar; var backgroundVScroll = _backgroundArea.VScrollBar; - bool forceBackgroundToTracksScroll = !Mathf.WithinEpsilon(_tracksVScrollTarget - tracksVScroll.Value, 0f, 5f); - if (tracksVScroll.IsThumbClicked || _tracksPanelArea.IsMouseOver || forceBackgroundToTracksScroll) + bool forceBackgroundToTracksScroll = _tracksVScrollTarget > 0; + if (forceBackgroundToTracksScroll) + { + backgroundVScroll.TargetValue = tracksVScroll.Value; + + if (Mathf.Abs(tracksVScroll.Value - _tracksVScrollTarget) < 0.5f) + _tracksVScrollTarget = 0f; + } + else if (tracksVScroll.IsThumbClicked || _tracksPanelArea.IsMouseOver) { backgroundVScroll.TargetValue = tracksVScroll.Value; } else { - _tracksVScrollTarget = 0f; tracksVScroll.TargetValue = backgroundVScroll.Value; } From 811bf0d630058302dedf3c9fe4981ce1611f5956 Mon Sep 17 00:00:00 2001 From: Jake Young Date: Thu, 7 May 2026 01:56:09 -0400 Subject: [PATCH 07/15] Change the default for showing debug draw to be on by default. --- Source/Editor/Windows/GameWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Windows/GameWindow.cs b/Source/Editor/Windows/GameWindow.cs index e95fb61b2..7b7370c26 100644 --- a/Source/Editor/Windows/GameWindow.cs +++ b/Source/Editor/Windows/GameWindow.cs @@ -123,7 +123,7 @@ namespace FlaxEditor.Windows private readonly ScaledRenderOutputControl _viewport; private readonly GameRoot _guiRoot; private bool _showGUI = true, _editGUI = true; - private bool _showDebugDraw = false; + private bool _showDebugDraw = true; private bool _audioMuted = false; private float _audioVolume = 1; private bool _isMaximized = false, _isUnlockingMouse = false; From 320d37d9a2569a8699f47c80fb7e1c92338a25c5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 09:54:18 +0200 Subject: [PATCH 08/15] Fix `MeshAccessor` triangle count calculation bug #4121 --- Source/Engine/Graphics/Models/MeshAccessor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs index cd216cdf5..17c1e71d7 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.cs +++ b/Source/Engine/Graphics/Models/MeshAccessor.cs @@ -621,7 +621,7 @@ namespace FlaxEngine { ibData = dataPtr[IB]; use16BitIndexBuffer = _formats[IB] == PixelFormat.R16_UInt; - triangles = (uint)(_data[IB].Length / PixelFormatExtensions.SizeInBytes(_formats[IB])); + triangles = (uint)(_data[IB].Length / (PixelFormatExtensions.SizeInBytes(_formats[IB]) * 3)); } if (mesh.Init(vertices, triangles, vbData, ibData, use16BitIndexBuffer, vbLayout)) From e7c5f257e9e8ac819c21f1a18556531077d866e5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 09:54:35 +0200 Subject: [PATCH 09/15] Minor fixes and improvements --- Source/Editor/GUI/ItemsListContextMenu.cs | 2 ++ Source/Engine/Graphics/Async/GPUTasksContext.cpp | 3 ++- Source/Engine/Graphics/Models/MeshAccessor.cs | 13 +++++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Source/Editor/GUI/ItemsListContextMenu.cs b/Source/Editor/GUI/ItemsListContextMenu.cs index 50c08e9ba..5f09342c7 100644 --- a/Source/Editor/GUI/ItemsListContextMenu.cs +++ b/Source/Editor/GUI/ItemsListContextMenu.cs @@ -589,6 +589,8 @@ namespace FlaxEditor.GUI // Get the next item bool controlDown = Root.GetKey(KeyboardKeys.Control); var items = GetVisibleItems(!controlDown); + if (items.Count == 0) + return true; var focusedIndex = items.IndexOf(focusedItem); int delta = key == KeyboardKeys.ArrowDown ? -1 : 1; diff --git a/Source/Engine/Graphics/Async/GPUTasksContext.cpp b/Source/Engine/Graphics/Async/GPUTasksContext.cpp index fe2198865..4fbe88510 100644 --- a/Source/Engine/Graphics/Async/GPUTasksContext.cpp +++ b/Source/Engine/Graphics/Async/GPUTasksContext.cpp @@ -76,9 +76,10 @@ void GPUTasksContext::OnFrameBegin() { auto task = _tasksSyncing[i]; auto state = task->GetState(); + if (EnumHasAllFlags(task->Flags, ObjectFlags::WasMarkedToDelete)) + state = TaskState::Finished; if (task->GetSyncPoint() <= _currentSyncPoint && state != TaskState::Finished) { - // TODO: add stats counter and count performed jobs, print to log on exit. task->Sync(); } if (state == TaskState::Failed || state == TaskState::Canceled) diff --git a/Source/Engine/Graphics/Models/MeshAccessor.cs b/Source/Engine/Graphics/Models/MeshAccessor.cs index 17c1e71d7..fccb4e518 100644 --- a/Source/Engine/Graphics/Models/MeshAccessor.cs +++ b/Source/Engine/Graphics/Models/MeshAccessor.cs @@ -643,11 +643,16 @@ namespace FlaxEngine else { Float3 min = Float3.Maximum, max = Float3.Minimum; - for (int i = 0; i < vertices; i++) + PixelFormatSampler.Get(positionStream.Format, out var positionSampler); + int positionStride = positionStream.Stride; + fixed (byte* data = positionStream.Data) { - Float3 pos = positionStream.GetFloat3(i); - Float3.Min(ref min, ref pos, out min); - Float3.Max(ref max, ref pos, out max); + for (int i = 0; i < vertices; i++) + { + Float3 pos = new Float3(positionSampler.Read(data + i * positionStride)); + Float3.Min(ref min, ref pos, out min); + Float3.Max(ref max, ref pos, out max); + } } bounds = new BoundingBox(min, max); } From 3e670f5e80d9a98b095b48ab2ecd25ee74dfd0e9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 10:07:16 +0200 Subject: [PATCH 10/15] Remove not needed line #4013 --- Source/Editor/GUI/Timeline/Timeline.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Source/Editor/GUI/Timeline/Timeline.cs b/Source/Editor/GUI/Timeline/Timeline.cs index 3ca83f903..861bd7e44 100644 --- a/Source/Editor/GUI/Timeline/Timeline.cs +++ b/Source/Editor/GUI/Timeline/Timeline.cs @@ -1302,7 +1302,6 @@ namespace FlaxEditor.GUI.Timeline if (track.ParentTrack != null) OnTracksOrderChanged(); track.OnSpawned(); - _tracksPanelArea.ScrollViewTo(track); MarkAsEdited(); if (withUndo) Undo?.AddAction(new AddRemoveTrackAction(this, track, true)); From b004e90606c124e001054c59f1286d818770c210 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 10:32:16 +0200 Subject: [PATCH 11/15] Adjust category names #4014 --- Source/Engine/Level/Actors/AnimatedModel.h | 30 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index 8d0959147..b6d922744 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -110,74 +110,74 @@ public: /// /// If true, use per-bone motion blur on this skeletal model. It requires additional rendering, can be disabled to save performance. /// - API_FIELD(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Rendering\")") + API_FIELD(Attributes="EditorOrder(20), DefaultValue(true), EditorDisplay(\"Drawing\")") bool PerBoneMotionBlur = true; /// /// If true, animation speed will be affected by the global timescale parameter. /// - API_FIELD(Attributes="EditorOrder(30), DefaultValue(true), EditorDisplay(\"Update\")") + API_FIELD(Attributes="EditorOrder(30), DefaultValue(true), EditorDisplay(\"Updating\")") bool UseTimeScale = true; /// /// If true, the animation will be updated even when an actor cannot be seen by any camera. Otherwise, the animations themselves will also stop running when the actor is off-screen. /// - API_FIELD(Attributes="EditorOrder(40), DefaultValue(false), EditorDisplay(\"Update\")") + API_FIELD(Attributes="EditorOrder(40), DefaultValue(false), EditorDisplay(\"Updating\")") bool UpdateWhenOffscreen = false; /// /// The animation update delta timescale. Can be used to speed up animation playback or create slow motion effect. /// - API_FIELD(Attributes="EditorOrder(45), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Update\")") + API_FIELD(Attributes="EditorOrder(45), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Updating\")") float UpdateSpeed = 1.0f; /// /// The animation update mode. Can be used to optimize the performance. /// - API_FIELD(Attributes="EditorOrder(50), DefaultValue(AnimationUpdateMode.Auto), EditorDisplay(\"Update\")") + API_FIELD(Attributes="EditorOrder(50), DefaultValue(AnimationUpdateMode.Auto), EditorDisplay(\"Updating\")") AnimationUpdateMode UpdateMode = AnimationUpdateMode::Auto; /// /// The master scale parameter for the actor bounding box. Helps to reduce mesh flickering effect on screen edges. /// - API_FIELD(Attributes="EditorOrder(60), DefaultValue(1.5f), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Rendering\")") + API_FIELD(Attributes="EditorOrder(60), DefaultValue(1.5f), Limit(0, float.MaxValue, 0.025f), EditorDisplay(\"Drawing\")") float BoundsScale = 1.5f; /// /// The custom bounds(in actor local space). If set to empty bounds then source skinned model bind pose bounds will be used. /// - API_FIELD(Attributes="EditorOrder(70), EditorDisplay(\"Rendering\")") + API_FIELD(Attributes="EditorOrder(70), EditorDisplay(\"Drawing\")") BoundingBox CustomBounds = BoundingBox::Zero; /// /// The model Level Of Detail bias value. Allows to increase or decrease rendered model quality. /// - API_FIELD(Attributes="EditorOrder(80), DefaultValue(0), Limit(-100, 100, 0.1f), EditorDisplay(\"Rendering\", \"LOD Bias\")") + API_FIELD(Attributes="EditorOrder(80), DefaultValue(0), Limit(-100, 100, 0.1f), EditorDisplay(\"Drawing\", \"LOD Bias\")") int32 LODBias = 0; /// /// Gets the model forced Level Of Detail index. Allows to bind the given model LOD to show. Value -1 disables this feature. /// - API_FIELD(Attributes="EditorOrder(90), DefaultValue(-1), Limit(-1, 100, 0.1f), EditorDisplay(\"Rendering\", \"Forced LOD\")") + API_FIELD(Attributes="EditorOrder(90), DefaultValue(-1), Limit(-1, 100, 0.1f), EditorDisplay(\"Drawing\", \"Forced LOD\")") int32 ForcedLOD = -1; /// /// The draw passes to use for rendering this object. /// - API_FIELD(Attributes="EditorOrder(100), DefaultValue(DrawPass.Default), EditorDisplay(\"Rendering\")") + API_FIELD(Attributes="EditorOrder(100), DefaultValue(DrawPass.Default), EditorDisplay(\"Drawing\")") DrawPass DrawModes = DrawPass::Default; /// /// The object sort order key used when sorting drawable objects during rendering. Use lower values to draw object before others, higher values are rendered later (on top). Can be used to control transparency drawing. /// - API_FIELD(Attributes="EditorOrder(110), DefaultValue(0), EditorDisplay(\"Rendering\")") + API_FIELD(Attributes="EditorOrder(110), DefaultValue(0), EditorDisplay(\"Drawing\")") int8 SortOrder = 0; /// /// The shadows casting mode. /// [Deprecated on 26.10.2022, expires on 26.10.2024] /// - API_FIELD(Attributes="EditorOrder(110), DefaultValue(ShadowsCastingMode.All), EditorDisplay(\"Rendering\")") + API_FIELD(Attributes="EditorOrder(110), DefaultValue(ShadowsCastingMode.All), EditorDisplay(\"Drawing\")") DEPRECATED() ShadowsCastingMode ShadowsMode = ShadowsCastingMode::All; /// @@ -190,7 +190,7 @@ public: /// /// If checked, the skeleton pose will be shown during debug shapes drawing. /// - API_FIELD(Attributes="EditorOrder(200), EditorDisplay(\"Debug\"), VisibleIf(nameof(ShowDebugDrawOptions))") bool ShowDebugDrawSkeleton = false; + API_FIELD(Attributes="EditorOrder(200), EditorDisplay(\"Skeleton\"), VisibleIf(nameof(ShowDebugDrawOptions))") bool ShowDebugDrawSkeleton = false; #endif public: @@ -442,9 +442,9 @@ public: private: #if USE_EDITOR /// - /// Used to hide options if when the skinned model or animation graph is null. + /// Used to hide options if when the skinned model or animation graph is null. /// - API_PROPERTY(Attributes = "HideInEditor, NoSerialize") bool GetShowDebugDrawOptions() const + API_PROPERTY(Attributes="HideInEditor, NoSerialize") bool GetShowDebugDrawOptions() const { return SkinnedModel != nullptr && AnimationGraph != nullptr; } From 6fa38b75d593c8e8805427a7b55e5673c728a4cf Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 10:40:51 +0200 Subject: [PATCH 12/15] Add material instance creation out of other instance #4063 --- .../Editor/Content/Proxy/MaterialBaseProxy.cs | 109 ++++++++++++++++++ .../Content/Proxy/MaterialInstanceProxy.cs | 52 +-------- Source/Editor/Content/Proxy/MaterialProxy.cs | 96 +-------------- 3 files changed, 111 insertions(+), 146 deletions(-) create mode 100644 Source/Editor/Content/Proxy/MaterialBaseProxy.cs diff --git a/Source/Editor/Content/Proxy/MaterialBaseProxy.cs b/Source/Editor/Content/Proxy/MaterialBaseProxy.cs new file mode 100644 index 000000000..83c98ee6e --- /dev/null +++ b/Source/Editor/Content/Proxy/MaterialBaseProxy.cs @@ -0,0 +1,109 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +using FlaxEditor.Content.Thumbnails; +using FlaxEditor.GUI.ContextMenu; +using FlaxEditor.Viewport.Previews; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.Content +{ + /// + /// A base class for asset proxy object. + /// + /// + public abstract class MaterialBaseProxy : BinaryAssetProxy + { + /// + /// The material preview drawer. + /// + protected MaterialPreview _preview; + + /// + public override bool CanCreate(ContentFolder targetLocation) + { + return targetLocation.CanHaveAssets; + } + + /// + public override void OnContentWindowContextMenu(ContextMenu menu, ContentItem item) + { + base.OnContentWindowContextMenu(menu, item); + + if (item is BinaryAssetItem binaryAssetItem) + { + var button = menu.AddButton("Create Material Instance", CreateMaterialInstanceClicked); + button.Tag = binaryAssetItem; + } + } + + private void CreateMaterialInstanceClicked(ContextMenuButton button) + { + var binaryAssetItem = (BinaryAssetItem)button.Tag; + CreateMaterialInstance(binaryAssetItem); + } + + /// + /// Creates the material instance from the given material. + /// + /// The material item to use as a base material. + public static void CreateMaterialInstance(BinaryAssetItem materialItem) + { + var materialInstanceName = materialItem.ShortName + " Instance"; + var materialInstanceProxy = Editor.Instance.ContentDatabase.GetProxy(); + Editor.Instance.Windows.ContentWin.NewItem(materialInstanceProxy, null, item => OnMaterialInstanceCreated(item, materialItem), materialInstanceName); + } + + private static void OnMaterialInstanceCreated(ContentItem item, BinaryAssetItem materialItem) + { + var assetItem = (AssetItem)item; + var materialInstance = FlaxEngine.Content.LoadAsync(assetItem.ID); + if (materialInstance == null || materialInstance.WaitForLoaded()) + { + Editor.LogError("Failed to load created material instance."); + return; + } + materialInstance.BaseMaterial = FlaxEngine.Content.LoadAsync(materialItem.ID); + materialInstance.Save(); + } + + /// + public override void OnThumbnailDrawPrepare(ThumbnailRequest request) + { + if (_preview == null) + { + _preview = new MaterialPreview(false); + InitAssetPreview(_preview); + } + } + + /// + public override void OnThumbnailDrawBegin(ThumbnailRequest request, ContainerControl guiRoot, GPUContext context) + { + _preview.Material = (MaterialBase)request.Asset; + _preview.Parent = guiRoot; + _preview.SyncBackbufferSize(); + + _preview.Task.OnDraw(); + } + + /// + public override void OnThumbnailDrawEnd(ThumbnailRequest request, ContainerControl guiRoot) + { + _preview.Material = null; + _preview.Parent = null; + } + + /// + public override void Dispose() + { + if (_preview != null) + { + _preview.Dispose(); + _preview = null; + } + + base.Dispose(); + } + } +} diff --git a/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs b/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs index fc4fcdbc1..212417e9f 100644 --- a/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs +++ b/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs @@ -2,23 +2,18 @@ using System; using FlaxEditor.Content.Thumbnails; -using FlaxEditor.Viewport.Previews; using FlaxEditor.Windows; using FlaxEditor.Windows.Assets; using FlaxEngine; -using FlaxEngine.GUI; namespace FlaxEditor.Content { /// /// A asset proxy object. /// - /// [ContentContextMenu("New/Material/Material Instance")] - public class MaterialInstanceProxy : BinaryAssetProxy + public class MaterialInstanceProxy : MaterialBaseProxy { - private MaterialPreview _preview; - /// public override string Name => "Material Instance"; @@ -34,12 +29,6 @@ namespace FlaxEditor.Content /// public override Type AssetType => typeof(MaterialInstance); - /// - public override bool CanCreate(ContentFolder targetLocation) - { - return targetLocation.CanHaveAssets; - } - /// public override void Create(string outputPath, object arg) { @@ -47,49 +36,10 @@ namespace FlaxEditor.Content throw new Exception("Failed to create new asset."); } - /// - public override void OnThumbnailDrawPrepare(ThumbnailRequest request) - { - if (_preview == null) - { - _preview = new MaterialPreview(false); - InitAssetPreview(_preview); - } - } - /// public override bool CanDrawThumbnail(ThumbnailRequest request) { return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((MaterialInstance)request.Asset); } - - /// - public override void OnThumbnailDrawBegin(ThumbnailRequest request, ContainerControl guiRoot, GPUContext context) - { - _preview.Material = (MaterialInstance)request.Asset; - _preview.Parent = guiRoot; - _preview.SyncBackbufferSize(); - - _preview.Task.OnDraw(); - } - - /// - public override void OnThumbnailDrawEnd(ThumbnailRequest request, ContainerControl guiRoot) - { - _preview.Material = null; - _preview.Parent = null; - } - - /// - public override void Dispose() - { - if (_preview != null) - { - _preview.Dispose(); - _preview = null; - } - - base.Dispose(); - } } } diff --git a/Source/Editor/Content/Proxy/MaterialProxy.cs b/Source/Editor/Content/Proxy/MaterialProxy.cs index 4769ca548..58d34299c 100644 --- a/Source/Editor/Content/Proxy/MaterialProxy.cs +++ b/Source/Editor/Content/Proxy/MaterialProxy.cs @@ -2,24 +2,18 @@ using System; using FlaxEditor.Content.Thumbnails; -using FlaxEditor.GUI.ContextMenu; -using FlaxEditor.Viewport.Previews; using FlaxEditor.Windows; using FlaxEditor.Windows.Assets; using FlaxEngine; -using FlaxEngine.GUI; namespace FlaxEditor.Content { /// /// A asset proxy object. /// - /// [ContentContextMenu("New/Material/Material")] - public class MaterialProxy : BinaryAssetProxy + public class MaterialProxy : MaterialBaseProxy { - private MaterialPreview _preview; - /// public override string Name => "Material"; @@ -35,12 +29,6 @@ namespace FlaxEditor.Content /// public override Type AssetType => typeof(Material); - /// - public override bool CanCreate(ContentFolder targetLocation) - { - return targetLocation.CanHaveAssets; - } - /// public override void Create(string outputPath, object arg) { @@ -48,92 +36,10 @@ namespace FlaxEditor.Content throw new Exception("Failed to create new asset."); } - /// - public override void OnContentWindowContextMenu(ContextMenu menu, ContentItem item) - { - base.OnContentWindowContextMenu(menu, item); - - if (item is BinaryAssetItem binaryAssetItem) - { - var button = menu.AddButton("Create Material Instance", CreateMaterialInstanceClicked); - button.Tag = binaryAssetItem; - } - } - - private void CreateMaterialInstanceClicked(ContextMenuButton obj) - { - var binaryAssetItem = (BinaryAssetItem)obj.Tag; - CreateMaterialInstance(binaryAssetItem); - } - - /// - /// Creates the material instance from the given material. - /// - /// The material item to use as a base material. - public static void CreateMaterialInstance(BinaryAssetItem materialItem) - { - var materialInstanceName = materialItem.ShortName + " Instance"; - var materialInstanceProxy = Editor.Instance.ContentDatabase.GetProxy(); - Editor.Instance.Windows.ContentWin.NewItem(materialInstanceProxy, null, item => OnMaterialInstanceCreated(item, materialItem), materialInstanceName); - } - - private static void OnMaterialInstanceCreated(ContentItem item, BinaryAssetItem materialItem) - { - var assetItem = (AssetItem)item; - var materialInstance = FlaxEngine.Content.LoadAsync(assetItem.ID); - if (materialInstance == null || materialInstance.WaitForLoaded()) - { - Editor.LogError("Failed to load created material instance."); - return; - } - - materialInstance.BaseMaterial = FlaxEngine.Content.LoadAsync(materialItem.ID); - materialInstance.Save(); - } - - /// - public override void OnThumbnailDrawPrepare(ThumbnailRequest request) - { - if (_preview == null) - { - _preview = new MaterialPreview(false); - InitAssetPreview(_preview); - } - } - /// public override bool CanDrawThumbnail(ThumbnailRequest request) { return _preview.HasLoadedAssets && ThumbnailsModule.HasMinimumQuality((Material)request.Asset); } - - /// - public override void OnThumbnailDrawBegin(ThumbnailRequest request, ContainerControl guiRoot, GPUContext context) - { - _preview.Material = (Material)request.Asset; - _preview.Parent = guiRoot; - _preview.SyncBackbufferSize(); - - _preview.Task.OnDraw(); - } - - /// - public override void OnThumbnailDrawEnd(ThumbnailRequest request, ContainerControl guiRoot) - { - _preview.Material = null; - _preview.Parent = null; - } - - /// - public override void Dispose() - { - if (_preview != null) - { - _preview.Dispose(); - _preview = null; - } - - base.Dispose(); - } } } From b7a59447a324eb0320fa61a1e27f073d48339bc3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 11:32:24 +0200 Subject: [PATCH 13/15] Fix Web build files location to properly exist in final package https://forum.flaxengine.com/t/web-build-issue-with-c-script/2590 --- .../Web/Binaries/{Data => }/check_browser_version.js | 0 Source/Platforms/Web/Binaries/{Data => }/check_jspi.js | 0 Source/Platforms/Web/Binaries/{Data => }/shell.html | 0 Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs | 6 +++--- 4 files changed, 3 insertions(+), 3 deletions(-) rename Source/Platforms/Web/Binaries/{Data => }/check_browser_version.js (100%) rename Source/Platforms/Web/Binaries/{Data => }/check_jspi.js (100%) rename Source/Platforms/Web/Binaries/{Data => }/shell.html (100%) diff --git a/Source/Platforms/Web/Binaries/Data/check_browser_version.js b/Source/Platforms/Web/Binaries/check_browser_version.js similarity index 100% rename from Source/Platforms/Web/Binaries/Data/check_browser_version.js rename to Source/Platforms/Web/Binaries/check_browser_version.js diff --git a/Source/Platforms/Web/Binaries/Data/check_jspi.js b/Source/Platforms/Web/Binaries/check_jspi.js similarity index 100% rename from Source/Platforms/Web/Binaries/Data/check_jspi.js rename to Source/Platforms/Web/Binaries/check_jspi.js diff --git a/Source/Platforms/Web/Binaries/Data/shell.html b/Source/Platforms/Web/Binaries/shell.html similarity index 100% rename from Source/Platforms/Web/Binaries/Data/shell.html rename to Source/Platforms/Web/Binaries/shell.html diff --git a/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs b/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs index a465ccac0..6551e3625 100644 --- a/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs +++ b/Source/Tools/Flax.Build/Platforms/Web/WebToolchain.cs @@ -367,7 +367,7 @@ namespace Flax.Build.Platforms if (args.All(arg => !arg.Contains("-sASSERTIONS"))) { // minimum_runtime_check.js from Emscripten checks min browser versions only with ASSERTIONS enabled so use custom check - var checkBrowserVersion = File.ReadAllText(Path.Combine(Globals.EngineRoot, "Source/Platforms/Web/Binaries/Data/check_browser_version.js")); + var checkBrowserVersion = File.ReadAllText(Path.Combine(Globals.EngineRoot, "Source/Platforms/Web/Binaries/check_browser_version.js")); checkBrowserVersion = checkBrowserVersion.Replace("TARGET_NOT_SUPPORTED", "0x7fffffff"); checkBrowserVersion = checkBrowserVersion.Replace("MIN_CHROME_VERSION", minChrome.ToString()); checkBrowserVersion = checkBrowserVersion.Replace("MIN_FIREFOX_VERSION", minFirefox.ToString()); @@ -377,11 +377,11 @@ namespace Flax.Build.Platforms args.Add($"--pre-js \"{path}\""); } if (addJSPI) - args.Add($"--pre-js \"{Globals.EngineRoot}/Source/Platforms/Web/Binaries/Data/check_jspi.js\""); + args.Add($"--pre-js \"{Globals.EngineRoot}/Source/Platforms/Web/Binaries/check_jspi.js\""); // Customize output HTML shell if (options.LinkEnv.Output == LinkerOutput.Executable) - args.Add($"--shell-file \"{Globals.EngineRoot}/Source/Platforms/Web/Binaries/Data/shell.html\""); + args.Add($"--shell-file \"{Globals.EngineRoot}/Source/Platforms/Web/Binaries/shell.html\""); } args.Add("-Wl,--start-group"); From c36c39df376c6ec2dce5e47f70124c70b0b4e1ec Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 11:38:34 +0200 Subject: [PATCH 14/15] Add auto focus on model when changing Base Model in Anim Graph --- Source/Editor/Viewport/Previews/AnimationPreview.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Viewport/Previews/AnimationPreview.cs b/Source/Editor/Viewport/Previews/AnimationPreview.cs index 8431821d6..b8942f2bf 100644 --- a/Source/Editor/Viewport/Previews/AnimationPreview.cs +++ b/Source/Editor/Viewport/Previews/AnimationPreview.cs @@ -1,6 +1,5 @@ // Copyright (c) Wojciech Figat. All rights reserved. -using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Input; using FlaxEngine; using FlaxEditor.Viewport.Widgets; @@ -14,6 +13,7 @@ namespace FlaxEditor.Viewport.Previews /// public class AnimationPreview : AnimatedModelPreview { + private bool _baseModelMissing; private ViewportWidgetButton _playPauseButton; /// @@ -94,14 +94,23 @@ namespace FlaxEditor.Viewport.Previews var style = Style.Current; var skinnedModel = SkinnedModel; + var baseModelMissing = false; if (skinnedModel == null) { Render2D.DrawText(style.FontLarge, "Missing Base Model", new Rectangle(Float2.Zero, Size), Color.Red, TextAlignment.Center, TextAlignment.Center, TextWrapping.WrapWords); + baseModelMissing = true; } else if (!skinnedModel.IsLoaded) { Render2D.DrawText(style.FontLarge, skinnedModel.LastLoadFailed ? "Failed to load" : "Loading...", new Rectangle(Float2.Zero, Size), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center); + baseModelMissing = true; } + if (_baseModelMissing && !baseModelMissing) + { + // Focus model when base model appears + ResetCamera(); + } + _baseModelMissing = baseModelMissing; } /// From 6daec81db1e19dc08c6c9fce9d72e9ec1c8c6ad4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 28 May 2026 13:30:40 +0200 Subject: [PATCH 15/15] Fix crash when using curves inside Anim Graph https://forum.flaxengine.com/t/bug-report-v1-12-using-curves-in-the-animation-graph-causes-a-crash/2594 --- .../Animations/Graph/AnimGraph.Base.cpp | 4 +- Source/Engine/Content/Assets/VisualScript.cpp | 10 ++-- Source/Engine/Content/Assets/VisualScript.h | 40 +++++++++++++- Source/Engine/Visject/ShaderGraph.h | 9 ++++ Source/Engine/Visject/VisjectGraph.cpp | 2 +- Source/Engine/Visject/VisjectGraph.h | 53 ++++--------------- 6 files changed, 66 insertions(+), 52 deletions(-) diff --git a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp index 1cc9c66a4..8f1d494ec 100644 --- a/Source/Engine/Animations/Graph/AnimGraph.Base.cpp +++ b/Source/Engine/Animations/Graph/AnimGraph.Base.cpp @@ -61,14 +61,14 @@ void AnimGraphBase::Clear() StateTransitions.Resize(0); // Base - GraphType::Clear(); + VisjectGraph::Clear(); } #if USE_EDITOR void AnimGraphBase::GetReferences(Array& output) const { - GraphType::GetReferences(output); + VisjectGraph::GetReferences(output); // Collect references from nested graph (assets used in state machines) for (const auto* subGraph : SubGraphs) diff --git a/Source/Engine/Content/Assets/VisualScript.cpp b/Source/Engine/Content/Assets/VisualScript.cpp index a7e132bdc..62d901b6f 100644 --- a/Source/Engine/Content/Assets/VisualScript.cpp +++ b/Source/Engine/Content/Assets/VisualScript.cpp @@ -163,7 +163,7 @@ VisjectExecutor::Value VisualScriptExecutor::eatBox(Node* caller, Box* box) // Add to the calling stack VisualScripting::StackFrame frame = *stack.Stack; - frame.Node = parentNode; + frame.Node = (VisualScriptGraphNode*)parentNode; frame.Box = box; frame.PreviousFrame = stack.Stack; stack.Stack = &frame; @@ -189,7 +189,7 @@ VisjectExecutor::Value VisualScriptExecutor::eatBox(Node* caller, Box* box) VisjectExecutor::Graph* VisualScriptExecutor::GetCurrentGraph() const { auto& stack = ThreadStacks.Get(); - return stack.Stack && stack.Stack->Script ? &stack.Stack->Script->Graph : nullptr; + return stack.Stack && stack.Stack->Script ? (Graph*)&stack.Stack->Script->Graph : nullptr; } void VisualScriptExecutor::ProcessGroupParameters(Box* box, Node* node, Value& value) @@ -432,7 +432,7 @@ void VisualScriptExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value& // Call Impulse or Pure Method if (boxBase->ID == 0 || (bool)node->Values[3]) { - auto& cache = node->Data.InvokeMethod; + auto& cache = ((VisualScriptGraphNode*)node)->Data.InvokeMethod; if (!cache.Method) { // Load method signature @@ -667,7 +667,7 @@ void VisualScriptExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value& // Get Field case 7: { - auto& cache = node->Data.GetSetField; + auto& cache = ((VisualScriptGraphNode*)node)->Data.GetSetField; if (!cache.Field) { const auto typeName = (StringView)node->Values[0]; @@ -753,7 +753,7 @@ void VisualScriptExecutor::ProcessGroupFunction(Box* boxBase, Node* node, Value& // Get Field case 8: { - auto& cache = node->Data.GetSetField; + auto& cache = ((VisualScriptGraphNode*)node)->Data.GetSetField; if (!cache.Field) { const auto typeName = (StringView)node->Values[0]; diff --git a/Source/Engine/Content/Assets/VisualScript.h b/Source/Engine/Content/Assets/VisualScript.h index 9e7af97cd..91b9506fa 100644 --- a/Source/Engine/Content/Assets/VisualScript.h +++ b/Source/Engine/Content/Assets/VisualScript.h @@ -12,11 +12,47 @@ #define VISUAL_SCRIPT_GRAPH_MAX_CALL_STACK 250 #define VISUAL_SCRIPT_DEBUGGING USE_EDITOR -#define VisualScriptGraphNode VisjectGraphNode<> - class VisualScripting; class VisualScriptingBinaryModule; +/// +/// Visual Script graph node. +/// +class VisualScriptGraphNode : public VisjectGraphNode<> +{ +public: + struct InvokeMethodData + { + void* Method; + BinaryModule* Module; + int32 ParamsCount; + uint32 OutParamsMask; + bool IsStatic; + }; + + struct GetSetFieldData + { + void* Field; + BinaryModule* Module; + bool IsStatic; + }; + + /// + /// Custom cached data per node type. Compact to use as small amount of memory as possible. + /// + struct AdditionalData + { + union + { + InvokeMethodData InvokeMethod; + GetSetFieldData GetSetField; + }; + }; + + // The custom per-node data. Used to cache data for faster usage at runtime. + AdditionalData Data; +}; + /// /// The Visual Script graph data. /// diff --git a/Source/Engine/Visject/ShaderGraph.h b/Source/Engine/Visject/ShaderGraph.h index 5b17604d0..d22260a87 100644 --- a/Source/Engine/Visject/ShaderGraph.h +++ b/Source/Engine/Visject/ShaderGraph.h @@ -167,6 +167,15 @@ public: // Base return Base::onNodeLoaded(n); } + void Clear() override + { + FloatCurves.Clear(); + Float2Curves.Clear(); + Float3Curves.Clear(); + Float4Curves.Clear(); + + Base::Clear(); + } }; /// diff --git a/Source/Engine/Visject/VisjectGraph.cpp b/Source/Engine/Visject/VisjectGraph.cpp index ec4f51364..8b8c27010 100644 --- a/Source/Engine/Visject/VisjectGraph.cpp +++ b/Source/Engine/Visject/VisjectGraph.cpp @@ -952,7 +952,7 @@ void VisjectExecutor::ProcessGroupTools(Box* box, Node* node, Value& value) #define SAMPLE_CURVE(id, curves, type, graphType) \ case id: \ { \ - const auto& curve = GetCurrentGraph()->curves[node->Data.Curve.CurveIndex]; \ + const auto& curve = GetCurrentGraph()->curves[node->CurveIndex]; \ const float time = (float)tryGetValue(node->GetBox(0), Value::Zero); \ value.Type = VariantType(VariantType::graphType); \ curve.Evaluate(*(type*)value.AsData, time, false); \ diff --git a/Source/Engine/Visject/VisjectGraph.h b/Source/Engine/Visject/VisjectGraph.h index 1f0de1ce0..0eceaf8cb 100644 --- a/Source/Engine/Visject/VisjectGraph.h +++ b/Source/Engine/Visject/VisjectGraph.h @@ -42,42 +42,6 @@ public: template class VisjectGraphNode : public GraphNode { -public: - struct CurveData - { - /// - /// The curve index. - /// - int32 CurveIndex; - }; - - /// - /// Custom cached data per node type. Compact to use as small amount of memory as possible. - /// - struct AdditionalData - { - union - { - CurveData Curve; - - struct - { - void* Method; - BinaryModule* Module; - int32 ParamsCount; - uint32 OutParamsMask; - bool IsStatic; - } InvokeMethod; - - struct - { - void* Field; - BinaryModule* Module; - bool IsStatic; - } GetSetField; - }; - }; - public: VisjectGraphNode() : GraphNode() @@ -85,10 +49,7 @@ public: } public: - /// - /// The custom data (depends on node type). Used to cache data for faster usage at runtime. - /// - AdditionalData Data; + int32 CurveIndex = MAX_uint16; /// /// The asset references. Linked resources such as Animation assets are referenced in graph data as ID. We need to keep valid refs to them at runtime to keep data in memory. @@ -148,7 +109,7 @@ public: #define SETUP_CURVE(id, curves, access) \ case id: \ { \ - n->Data.Curve.CurveIndex = curves.Count(); \ + n->CurveIndex = curves.Count(); \ auto& curve = curves.AddOne(); \ const int32 keyframesCount = n->Values[0].AsInt; \ auto& keyframes = curve.GetKeyframes(); \ @@ -177,9 +138,17 @@ public: } } - // Base return Base::onNodeLoaded(n); } + void Clear() override + { + FloatCurves.Clear(); + Float2Curves.Clear(); + Float3Curves.Clear(); + Float4Curves.Clear(); + + Base::Clear(); + } }; ///