From f87515305b3e64f8d04f5f06f2a80c34dcd65f08 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 16 Apr 2026 15:24:50 +0200 Subject: [PATCH 01/51] Add `Instance Transform` node to Anim Graph --- Source/Editor/Surface/Archetypes/Animation.cs | 15 +++++++++++++ .../Animations/Graph/AnimGroup.Animation.cpp | 22 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/Source/Editor/Surface/Archetypes/Animation.cs b/Source/Editor/Surface/Archetypes/Animation.cs index e376c3e43..e3d1d9d36 100644 --- a/Source/Editor/Surface/Archetypes/Animation.cs +++ b/Source/Editor/Surface/Archetypes/Animation.cs @@ -1138,6 +1138,21 @@ namespace FlaxEditor.Surface.Archetypes NodeElementArchetype.Factory.Output(0, "", typeof(float), 0), } }, + new NodeArchetype + { + TypeID = 37, + Title = "Instance Transform", + Description = "Animated model transformation (in world space).", + Flags = NodeFlags.AnimGraph, + Size = new Float2(200, 80), + Elements = new[] + { + NodeElementArchetype.Factory.Output(0, "", typeof(Transform), 0), + NodeElementArchetype.Factory.Output(1, "Position", typeof(Vector3), 1), + NodeElementArchetype.Factory.Output(2, "Rotation", typeof(Quaternion), 2), + NodeElementArchetype.Factory.Output(3, "Scale", typeof(Float3), 3), + } + }, }; } } diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp index e06f0deaa..b07b5a132 100644 --- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp +++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp @@ -2660,6 +2660,28 @@ void AnimGraphExecutor::ProcessGroupAnimation(Box* boxBase, Node* nodeBase, Valu value = actor ? actor->GetPerInstanceRandom() : 0.0f; break; } + // Instance Transform + case 37: + { + auto* actor = ScriptingObject::Cast(context.Data->Object); + const auto& transform = actor ? actor->GetTransform() : Transform::Identity; + switch (box->ID) + { + case 0: + value = Value(transform); + break; + case 1: + value = transform.Translation; + break; + case 2: + value = transform.Orientation; + break; + case 3: + value = transform.Scale; + break; + } + break; + } default: break; } From 7b5b688c687faefb06795074ef0783f60ba38a52 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 16 Apr 2026 15:27:24 +0200 Subject: [PATCH 02/51] Update engine assets --- Content/Editor/Particles/Constant Burst.flax | 4 ++-- Content/Editor/Particles/Particle Material Preview.flax | 4 ++-- Content/Editor/Particles/Periodic Burst.flax | 4 ++-- Content/Editor/Particles/Ribbon Spiral.flax | 4 ++-- Content/Editor/Particles/Smoke.flax | 4 ++-- Content/Editor/Particles/Sparks.flax | 4 ++-- Content/Shaders/Fog.flax | 4 ++-- Content/Shaders/PostProcessing.flax | 4 ++-- Content/Shaders/Quad.flax | 4 ++-- Content/Shaders/SSAO.flax | 4 ++-- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/Content/Editor/Particles/Constant Burst.flax b/Content/Editor/Particles/Constant Burst.flax index bb90199fe..59c68065c 100644 --- a/Content/Editor/Particles/Constant Burst.flax +++ b/Content/Editor/Particles/Constant Burst.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fc1e46708152005dc92532e940b3ce19ec527b595fb18365b41ebdcd338ad2c4 -size 2705 +oid sha256:5876bad52753b665a608a15d060087ce2f963bed8cb792bb2b0fdbf256f461be +size 2825 diff --git a/Content/Editor/Particles/Particle Material Preview.flax b/Content/Editor/Particles/Particle Material Preview.flax index 1f18aaeaa..409664367 100644 --- a/Content/Editor/Particles/Particle Material Preview.flax +++ b/Content/Editor/Particles/Particle Material Preview.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88de448e023b35296531fff817d4b95200bac97fbd5c927ee6a5e5a815323978 -size 2110 +oid sha256:4fe07c97b27512fc284a18952fe52031be6077f52cea0f584d44b5fd1cfaef5b +size 1970 diff --git a/Content/Editor/Particles/Periodic Burst.flax b/Content/Editor/Particles/Periodic Burst.flax index 784d456c5..ce0444d3e 100644 --- a/Content/Editor/Particles/Periodic Burst.flax +++ b/Content/Editor/Particles/Periodic Burst.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:94bf88983e3cf9fe555141a867af3c20e5bd58d13a527a9b4e02d4d1be157be9 -size 3664 +oid sha256:9d83eac50915cf1995559ea152c47136a08c174a551074a73628e2dbe1994214 +size 3797 diff --git a/Content/Editor/Particles/Ribbon Spiral.flax b/Content/Editor/Particles/Ribbon Spiral.flax index 4e06db822..c48c7c768 100644 --- a/Content/Editor/Particles/Ribbon Spiral.flax +++ b/Content/Editor/Particles/Ribbon Spiral.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9016e074d23649b91b9eb2438c4f48d0dbd5bed0c7d29b58fdaea5a8530e905 -size 2365 +oid sha256:c673d1a044b7e0ad70b9bfbad171b2ceb0da2451100d3e3672eb48232f6f230a +size 2485 diff --git a/Content/Editor/Particles/Smoke.flax b/Content/Editor/Particles/Smoke.flax index 400f89257..1549cd14d 100644 --- a/Content/Editor/Particles/Smoke.flax +++ b/Content/Editor/Particles/Smoke.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:661f4dfadadedb9b5830db059949938d63e3b4db727496c86a04aa008bba12e6 -size 14660 +oid sha256:d0e663c8424630e82c1d42a62ff5daf0807237a50a9f376e678b2b9db08b999a +size 4616 diff --git a/Content/Editor/Particles/Sparks.flax b/Content/Editor/Particles/Sparks.flax index f32b22c84..4c5da4e81 100644 --- a/Content/Editor/Particles/Sparks.flax +++ b/Content/Editor/Particles/Sparks.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93a9342516ef1a13cfadaa5b9e8ff9b9328f13569b3fff7ec51bb9e103ed623c -size 13698 +oid sha256:f0c29ed8547987e54cc7cbce9e36f6386db9e8443f24527deb5e68219685c5d4 +size 13816 diff --git a/Content/Shaders/Fog.flax b/Content/Shaders/Fog.flax index 10e6f56c5..2cf1eb131 100644 --- a/Content/Shaders/Fog.flax +++ b/Content/Shaders/Fog.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b1571139183579891b51fde139e0ac9767251a8265a1c5b98f50475c229f3378 -size 2196 +oid sha256:5471e1f7c6352b3e514a6c1de72751b8eee88ed8c8ec6f99f35391bd570261e8 +size 2193 diff --git a/Content/Shaders/PostProcessing.flax b/Content/Shaders/PostProcessing.flax index bec24f8c0..37d834717 100644 --- a/Content/Shaders/PostProcessing.flax +++ b/Content/Shaders/PostProcessing.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:17fc5977b0e82aea17310dad1a93745fd923ebc920b74ee29a55afb909275e56 -size 23340 +oid sha256:5c9a661a83ce1120c203347ac149fc7626bcef66386c797f4c89bc46cf42bb39 +size 23321 diff --git a/Content/Shaders/Quad.flax b/Content/Shaders/Quad.flax index d4906d999..f73292191 100644 --- a/Content/Shaders/Quad.flax +++ b/Content/Shaders/Quad.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8f4cfb3320b1f07f864aa8a7759883b4b0a2f31f97b40de3856ba73561d1868c -size 4060 +oid sha256:e7be75cc2c4dc8b815ca3dca0828b7181d3fb7874cddac97e1ca7a563832b018 +size 4059 diff --git a/Content/Shaders/SSAO.flax b/Content/Shaders/SSAO.flax index 6da2efd41..cfe92b71d 100644 --- a/Content/Shaders/SSAO.flax +++ b/Content/Shaders/SSAO.flax @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bdeb8672975efe6858402b7a19a90851328406abf073059f9000e9a9eb2a8c38 -size 36980 +oid sha256:73d13eacaf02ba27354b817cc639bde59e2afdc3a8d848de6d17981b5bb48cbe +size 36979 From 4e72be1604dccf7d2bf451e329d60ce32ac77272 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 16 Apr 2026 15:44:28 +0200 Subject: [PATCH 03/51] Add selected code editor name to UI button for scripts project opening --- Source/Editor/Modules/UIModule.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Source/Editor/Modules/UIModule.cs b/Source/Editor/Modules/UIModule.cs index 1ecab7089..1a9abe23c 100644 --- a/Source/Editor/Modules/UIModule.cs +++ b/Source/Editor/Modules/UIModule.cs @@ -499,6 +499,7 @@ namespace FlaxEditor.Modules InitWindowDecorations(mainWindow); Editor.Options.OptionsChanged += OnOptionsChanged; + Editor.CodeEditing.SelectedEditorChanged += OnSelectedCodeEditorChanged; mainWindow.PerformLayout(true); } @@ -870,6 +871,11 @@ namespace FlaxEditor.Modules UpdateToolstrip(); } + private void OnSelectedCodeEditorChanged(SourceCodeEditing.ISourceCodeEditor codeEditor) + { + _menuFileOpenScriptsProject.Text = $"Open scripts project ({(codeEditor?.Name ?? "Default")})"; + } + private void InitToolstrip(RootControl mainWindow) { var inputOptions = Editor.Options.Options.Input; From 1f5fcdc7c7939bb403b13f6e0720de55237ac847 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 16 Apr 2026 15:46:24 +0200 Subject: [PATCH 04/51] Fix crash when running with `D3D10` --- Source/Engine/Renderer/AmbientOcclusionPass.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Renderer/AmbientOcclusionPass.cpp b/Source/Engine/Renderer/AmbientOcclusionPass.cpp index 07a22e8f0..341270a61 100644 --- a/Source/Engine/Renderer/AmbientOcclusionPass.cpp +++ b/Source/Engine/Renderer/AmbientOcclusionPass.cpp @@ -98,7 +98,7 @@ bool AmbientOcclusionPass::Init() _psNonSmartBlur = GPUDevice::Instance->CreatePipelineState(); _psApply = GPUDevice::Instance->CreatePipelineState(); _psApplyHalf = GPUDevice::Instance->CreatePipelineState(); - _depthBounds = GPUDevice::Instance->Limits.HasDepthBounds; + _depthBounds = GPUDevice::Instance->Limits.HasDepthBounds && GPUDevice::Instance->Limits.HasReadOnlyDepth; // Load shader _shader = Content::LoadAsyncInternal(TEXT("Shaders/SSAO")); From 6db9116047e82eda8ab0ed711173f27a3947dd1f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 16 Apr 2026 21:30:47 +0200 Subject: [PATCH 05/51] Fix crash when using `SkeletonMask::GetNodesMask` from 2 threads at once *(g. in Anim Graph) --- Source/Engine/Content/Assets/SkeletonMask.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Content/Assets/SkeletonMask.cpp b/Source/Engine/Content/Assets/SkeletonMask.cpp index 2776ba02c..37b543f8f 100644 --- a/Source/Engine/Content/Assets/SkeletonMask.cpp +++ b/Source/Engine/Content/Assets/SkeletonMask.cpp @@ -51,13 +51,14 @@ AssetChunksFlag SkeletonMask::getChunksToPreload() const const BitArray<>& SkeletonMask::GetNodesMask() { - if (_mask.IsEmpty() && Skeleton && !Skeleton->WaitForLoaded()) + if (!Skeleton || Skeleton->WaitForLoaded()) + return _mask; + ScopeLock lock(Locker); + if (_mask.IsEmpty()) { _mask.Resize(Skeleton->Skeleton.Nodes.Count()); for (int32 i = 0; i < _mask.Count(); i++) - { _mask.Set(i, _maskedNodes.Contains(Skeleton->Skeleton.Nodes[i].Name)); - } } return _mask; From b6c73589535aa3c54762f670b9aaee93eb08eaaa Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 16 Apr 2026 22:33:36 +0200 Subject: [PATCH 06/51] Fix crash when caching `DebugCommands` and classes cache gets modified #4054 --- Source/Engine/Engine/NativeInterop.cs | 2 ++ Source/Engine/Scripting/ManagedCLR/MAssembly.h | 6 ++++++ Source/Engine/Scripting/ManagedCLR/MCore.cpp | 3 ++- Source/Engine/Scripting/Runtime/DotNet.cpp | 13 ++++++++++--- Source/Engine/Scripting/Runtime/Mono.cpp | 5 +++++ Source/Engine/Scripting/Runtime/None.cpp | 5 +++++ 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index 564c53801..304270b34 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -1626,7 +1626,9 @@ namespace FlaxEngine.Interop // We need private types of this assembly too, DefinedTypes contains a lot of types from other assemblies... var types = referencedTypes.Any() ? assembly.DefinedTypes.Where(x => !referencedTypes.Contains(x.FullName)).ToArray() : assembly.DefinedTypes.ToArray(); +#if FLAX_EDITOR Assert.IsTrue(Utils.GetAssemblies().Count(x => x.GetName().Name == "FlaxEngine.CSharp") == 1); +#endif return types; } diff --git a/Source/Engine/Scripting/ManagedCLR/MAssembly.h b/Source/Engine/Scripting/ManagedCLR/MAssembly.h index 0a785c06a..59d6d5360 100644 --- a/Source/Engine/Scripting/ManagedCLR/MAssembly.h +++ b/Source/Engine/Scripting/ManagedCLR/MAssembly.h @@ -38,6 +38,7 @@ private: mutable int32 _hasCachedClasses : 1; mutable ClassesDictionary _classes; + mutable ClassesDictionary _typeClasses; int32 _reloadCount; StringAnsi _name; @@ -234,6 +235,11 @@ public: /// const ClassesDictionary& GetClasses() const; + /// + /// Gets the classes lookup cache that includes runtime-cached types. Non-stable to iterate over due to dynamic types caching. + /// + ClassesDictionary& GetTypeClasses() const; + private: bool LoadCorlib(); bool LoadImage(const String& assemblyPath, const StringView& nativePath); diff --git a/Source/Engine/Scripting/ManagedCLR/MCore.cpp b/Source/Engine/Scripting/ManagedCLR/MCore.cpp index 6fa499002..5f20b3881 100644 --- a/Source/Engine/Scripting/ManagedCLR/MCore.cpp +++ b/Source/Engine/Scripting/ManagedCLR/MCore.cpp @@ -142,7 +142,8 @@ void MAssembly::Unload(bool isReloading) _isLoaded = false; _hasCachedClasses = false; #if USE_NETCORE - ArenaAllocator::ClearDelete(_classes); + ArenaAllocator::ClearDelete(_typeClasses); + _classes.Clear(); #else _classes.ClearDelete(); #endif diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 65dd09b44..0c1901e4b 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -355,7 +355,7 @@ void MCore::UnloadScriptingAssemblyLoadContext() MAssembly* a = e.Value; if (!a->IsLoaded() || !a->_hasCachedClasses) continue; - for (const auto& q : a->GetClasses()) + for (const auto& q : a->GetTypeClasses()) { MClass* c = q.Value; c->_hasCachedAttributes = false; @@ -781,6 +781,7 @@ const MAssembly::ClassesDictionary& MAssembly::GetClasses() const MCore::GC::FreeMemory((void*)managedClasses[i].fullname); MCore::GC::FreeMemory((void*)managedClasses[i].namespace_); } + _typeClasses = _classes; static void* RegisterManagedClassNativePointersPtr = GetStaticMethodPointer(TEXT("RegisterManagedClassNativePointers")); CallStaticMethod(RegisterManagedClassNativePointersPtr, &managedClasses, classCount); @@ -799,6 +800,12 @@ const MAssembly::ClassesDictionary& MAssembly::GetClasses() const return _classes; } +MAssembly::ClassesDictionary& MAssembly::GetTypeClasses() const +{ + GetClasses(); + return _typeClasses; +} + void GetAssemblyName(void* assemblyHandle, StringAnsi& name, StringAnsi& fullname) { static void* GetAssemblyNamePtr = GetStaticMethodPointer(TEXT("GetAssemblyName")); @@ -828,7 +835,7 @@ DEFINE_INTERNAL_CALL(void) NativeInterop_CreateClass(NativeClassDefinitions* man MClass* klass = assembly->Memory.New(assembly, managedClass->typeHandle, managedClass->name, managedClass->fullname, managedClass->namespace_, managedClass->typeAttributes); if (assembly != nullptr) { - auto& classes = const_cast(assembly->GetClasses()); + auto& classes = assembly->GetTypeClasses(); MClass* oldKlass; if (classes.TryGet(klass->GetFullName(), oldKlass)) { @@ -1746,7 +1753,7 @@ MClass* GetOrCreateClass(MType* typeHandle) klass = assembly->Memory.New(assembly, classInfo.typeHandle, classInfo.name, classInfo.fullname, classInfo.namespace_, classInfo.typeAttributes); if (assembly != nullptr) { - auto& classes = const_cast(assembly->GetClasses()); + auto& classes = assembly->GetTypeClasses(); if (classes.ContainsKey(klass->GetFullName())) { LOG(Warning, "Class '{0}' was already added to assembly '{1}'", String(klass->GetFullName()), String(assembly->GetName())); diff --git a/Source/Engine/Scripting/Runtime/Mono.cpp b/Source/Engine/Scripting/Runtime/Mono.cpp index 1449d405f..f32d12c57 100644 --- a/Source/Engine/Scripting/Runtime/Mono.cpp +++ b/Source/Engine/Scripting/Runtime/Mono.cpp @@ -1091,6 +1091,11 @@ const MAssembly::ClassesDictionary& MAssembly::GetClasses() const return _classes; } +const MAssembly::ClassesDictionary& MAssembly::GetTypeClasses() const +{ + return GetClasses(); +} + bool MAssembly::Load(MonoImage* monoImage) { if (IsLoaded()) diff --git a/Source/Engine/Scripting/Runtime/None.cpp b/Source/Engine/Scripting/Runtime/None.cpp index 178e98444..87ec35ac0 100644 --- a/Source/Engine/Scripting/Runtime/None.cpp +++ b/Source/Engine/Scripting/Runtime/None.cpp @@ -303,6 +303,11 @@ const MAssembly::ClassesDictionary& MAssembly::GetClasses() const return _classes; } +const MAssembly::ClassesDictionary& MAssembly::GetTypeClasses() const +{ + return GetClasses(); +} + bool MAssembly::LoadCorlib() { return false; From 9350a4a70643f1c129fd2f789ed1f1c2a05f38df Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 16 Apr 2026 23:23:36 +0200 Subject: [PATCH 07/51] Refactor model LOD transition to run only when any material uses `Dithered LOD Transition` #4055 --- Source/Engine/Content/Assets/Model.cpp | 100 +------------- Source/Engine/Content/Assets/SkinnedModel.cpp | 104 +------------- Source/Engine/Graphics/Models/ModelDraw.h | 130 ++++++++++++++++++ Source/Engine/Graphics/RenderTools.cpp | 7 +- Source/Engine/Graphics/RenderTools.h | 13 +- 5 files changed, 152 insertions(+), 202 deletions(-) create mode 100644 Source/Engine/Graphics/Models/ModelDraw.h diff --git a/Source/Engine/Content/Assets/Model.cpp b/Source/Engine/Content/Assets/Model.cpp index d7172e091..340726644 100644 --- a/Source/Engine/Content/Assets/Model.cpp +++ b/Source/Engine/Content/Assets/Model.cpp @@ -16,6 +16,7 @@ #include "Engine/Graphics/Async/GPUTask.h" #include "Engine/Graphics/Async/Tasks/GPUUploadTextureMipTask.h" #include "Engine/Graphics/Models/MeshDeformation.h" +#include "Engine/Graphics/Models/ModelDraw.h" #include "Engine/Graphics/Textures/GPUTexture.h" #include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Profiler/ProfilerMemory.h" @@ -136,105 +137,6 @@ void Model::Draw(const RenderContext& renderContext, MaterialBase* material, con LODs[lodIndex].Draw(renderContext, material, world, flags, receiveDecals, DrawPass::Default, 0, sortOrder); } -template -FORCE_INLINE void ModelDraw(Model* model, const RenderContext& renderContext, const ContextType& context, const Mesh::DrawInfo& info) -{ - ASSERT(info.Buffer); - if (!model->CanBeRendered()) - return; - if (!info.Buffer->IsValidFor(model)) - info.Buffer->Setup(model); - const auto frame = Engine::FrameCount; - const auto modelFrame = info.DrawState->PrevFrame + 1; - - // Select a proper LOD index (model may be culled) - int32 lodIndex; - if (info.ForcedLOD != -1) - { - lodIndex = info.ForcedLOD; - } - else - { - lodIndex = RenderTools::ComputeModelLOD(model, info.Bounds.Center, (float)info.Bounds.Radius, renderContext); - if (lodIndex == -1) - { - // Handling model fade-out transition - if (modelFrame == frame && info.DrawState->PrevLOD != -1 && !renderContext.View.IsSingleFrame) - { - // Check if start transition - if (info.DrawState->LODTransition == 255) - { - info.DrawState->LODTransition = 0; - } - - RenderTools::UpdateModelLODTransition(info.DrawState->LODTransition); - - // Check if end transition - if (info.DrawState->LODTransition == 255) - { - info.DrawState->PrevLOD = lodIndex; - } - else - { - const auto prevLOD = model->ClampLODIndex(info.DrawState->PrevLOD); - const float normalizedProgress = static_cast(info.DrawState->LODTransition) * (1.0f / 255.0f); - model->LODs.Get()[prevLOD].Draw(renderContext, info, normalizedProgress); - } - } - - return; - } - } - lodIndex += info.LODBias + renderContext.View.ModelLODBias; - lodIndex = model->ClampLODIndex(lodIndex); - - if (renderContext.View.IsSingleFrame) - { - } - // Check if it's the new frame and could update the drawing state (note: model instance could be rendered many times per frame to different viewports) - else if (modelFrame == frame) - { - // Check if start transition - if (info.DrawState->PrevLOD != lodIndex && info.DrawState->LODTransition == 255) - { - info.DrawState->LODTransition = 0; - } - - RenderTools::UpdateModelLODTransition(info.DrawState->LODTransition); - - // Check if end transition - if (info.DrawState->LODTransition == 255) - { - info.DrawState->PrevLOD = lodIndex; - } - } - // Check if there was a gap between frames in drawing this model instance - else if (modelFrame < frame || info.DrawState->PrevLOD == -1) - { - // Reset state - info.DrawState->PrevLOD = lodIndex; - info.DrawState->LODTransition = 255; - } - - // Draw - if (info.DrawState->PrevLOD == lodIndex || renderContext.View.IsSingleFrame) - { - model->LODs.Get()[lodIndex].Draw(context, info, 0.0f); - } - else if (info.DrawState->PrevLOD == -1) - { - const float normalizedProgress = static_cast(info.DrawState->LODTransition) * (1.0f / 255.0f); - model->LODs.Get()[lodIndex].Draw(context, info, 1.0f - normalizedProgress); - } - else - { - const auto prevLOD = model->ClampLODIndex(info.DrawState->PrevLOD); - const float normalizedProgress = static_cast(info.DrawState->LODTransition) * (1.0f / 255.0f); - model->LODs.Get()[prevLOD].Draw(context, info, normalizedProgress); - model->LODs.Get()[lodIndex].Draw(context, info, normalizedProgress - 1.0f); - } -} - void Model::Draw(const RenderContext& renderContext, const Mesh::DrawInfo& info) { ModelDraw(this, renderContext, renderContext, info); diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index c0355ea0e..965df12f0 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -12,6 +12,7 @@ #include "Engine/Graphics/Models/Config.h" #include "Engine/Graphics/Models/MeshDeformation.h" #include "Engine/Graphics/Models/ModelInstanceEntry.h" +#include "Engine/Graphics/Models/ModelDraw.h" #include "Engine/Graphics/Shaders/GPUVertexLayout.h" #include "Engine/Content/Content.h" #include "Engine/Content/Factories/BinaryAssetFactory.h" @@ -234,113 +235,14 @@ BoundingBox SkinnedModel::GetBox(int32 lodIndex) const return LODs[lodIndex].GetBox(); } -template -FORCE_INLINE void SkinnedModelDraw(SkinnedModel* model, const RenderContext& renderContext, const ContextType& context, const SkinnedMesh::DrawInfo& info) -{ - ASSERT(info.Buffer); - if (!model->CanBeRendered()) - return; - if (!info.Buffer->IsValidFor(model)) - info.Buffer->Setup(model); - const auto frame = Engine::FrameCount; - const auto modelFrame = info.DrawState->PrevFrame + 1; - - // Select a proper LOD index (model may be culled) - int32 lodIndex; - if (info.ForcedLOD != -1) - { - lodIndex = info.ForcedLOD; - } - else - { - lodIndex = RenderTools::ComputeSkinnedModelLOD(model, info.Bounds.Center, (float)info.Bounds.Radius, renderContext); - if (lodIndex == -1) - { - // Handling model fade-out transition - if (modelFrame == frame && info.DrawState->PrevLOD != -1 && !renderContext.View.IsSingleFrame) - { - // Check if start transition - if (info.DrawState->LODTransition == 255) - { - info.DrawState->LODTransition = 0; - } - - RenderTools::UpdateModelLODTransition(info.DrawState->LODTransition); - - // Check if end transition - if (info.DrawState->LODTransition == 255) - { - info.DrawState->PrevLOD = lodIndex; - } - else - { - const auto prevLOD = model->ClampLODIndex(info.DrawState->PrevLOD); - const float normalizedProgress = static_cast(info.DrawState->LODTransition) * (1.0f / 255.0f); - model->LODs.Get()[prevLOD].Draw(renderContext, info, normalizedProgress); - } - } - - return; - } - } - lodIndex += info.LODBias + renderContext.View.ModelLODBias; - lodIndex = model->ClampLODIndex(lodIndex); - - if (renderContext.View.IsSingleFrame) - { - } - // Check if it's the new frame and could update the drawing state (note: model instance could be rendered many times per frame to different viewports) - else if (modelFrame == frame) - { - // Check if start transition - if (info.DrawState->PrevLOD != lodIndex && info.DrawState->LODTransition == 255) - { - info.DrawState->LODTransition = 0; - } - - RenderTools::UpdateModelLODTransition(info.DrawState->LODTransition); - - // Check if end transition - if (info.DrawState->LODTransition == 255) - { - info.DrawState->PrevLOD = lodIndex; - } - } - // Check if there was a gap between frames in drawing this model instance - else if (modelFrame < frame || info.DrawState->PrevLOD == -1) - { - // Reset state - info.DrawState->PrevLOD = lodIndex; - info.DrawState->LODTransition = 255; - } - - // Draw - if (info.DrawState->PrevLOD == lodIndex || renderContext.View.IsSingleFrame) - { - model->LODs.Get()[lodIndex].Draw(context, info, 0.0f); - } - else if (info.DrawState->PrevLOD == -1) - { - const float normalizedProgress = static_cast(info.DrawState->LODTransition) * (1.0f / 255.0f); - model->LODs.Get()[lodIndex].Draw(context, info, 1.0f - normalizedProgress); - } - else - { - const auto prevLOD = model->ClampLODIndex(info.DrawState->PrevLOD); - const float normalizedProgress = static_cast(info.DrawState->LODTransition) * (1.0f / 255.0f); - model->LODs.Get()[prevLOD].Draw(context, info, normalizedProgress); - model->LODs.Get()[lodIndex].Draw(context, info, normalizedProgress - 1.0f); - } -} - void SkinnedModel::Draw(const RenderContext& renderContext, const SkinnedMesh::DrawInfo& info) { - SkinnedModelDraw(this, renderContext, renderContext, info); + ModelDraw(this, renderContext, renderContext, info); } void SkinnedModel::Draw(const RenderContextBatch& renderContextBatch, const SkinnedMesh::DrawInfo& info) { - SkinnedModelDraw(this, renderContextBatch.GetMainContext(), renderContextBatch, info); + ModelDraw(this, renderContextBatch.GetMainContext(), renderContextBatch, info); } bool SkinnedModel::SetupLODs(const Span& meshesCountPerLod) diff --git a/Source/Engine/Graphics/Models/ModelDraw.h b/Source/Engine/Graphics/Models/ModelDraw.h new file mode 100644 index 000000000..3e2a61242 --- /dev/null +++ b/Source/Engine/Graphics/Models/ModelDraw.h @@ -0,0 +1,130 @@ +// Copyright (c) Wojciech Figat. All rights reserved. + +#pragma once + +#include "Engine/Engine/Engine.h" +#include "Engine/Graphics/RenderTask.h" +#include "Engine/Graphics/RenderTools.h" +#include "Engine/Graphics/Materials/MaterialInfo.h" + +template +FORCE_INLINE bool ModelDrawTransition(ModelType* model, const DrawInfoType& info) +{ + for (auto& e : *info.Buffer) + { + if (e.Material && EnumHasAllFlags(e.Material->GetInfo().FeaturesFlags, MaterialFeaturesFlags::DitheredLODTransition)) + return true; + } + for (auto& e : model->MaterialSlots) + { + if (e.Material && EnumHasAllFlags(e.Material->GetInfo().FeaturesFlags, MaterialFeaturesFlags::DitheredLODTransition)) + return true; + } + return false; +} + +template +FORCE_INLINE void ModelDraw(ModelType* model, const RenderContext& renderContext, const ContextType& context, const DrawInfoType& info) +{ + ASSERT(info.Buffer); + if (!model->CanBeRendered()) + return; + if (!info.Buffer->IsValidFor(model)) + info.Buffer->Setup(model); + const auto frame = Engine::FrameCount; + const auto modelFrame = info.DrawState->PrevFrame + 1; + + // Select a proper LOD index (model may be culled) + int32 lodIndex; + if (info.ForcedLOD != -1) + { + lodIndex = info.ForcedLOD; + } + else + { + lodIndex = RenderTools::ComputeModelLOD(model, info.Bounds.Center, (float)info.Bounds.Radius, renderContext); + if (lodIndex == -1) + { + // Handling model fade-out transition + if (modelFrame == frame && info.DrawState->PrevLOD != -1 && !renderContext.View.IsSingleFrame && ModelDrawTransition(model, info)) + { + // Check if start transition + if (info.DrawState->LODTransition == 255) + { + info.DrawState->LODTransition = 0; + } + + RenderTools::UpdateModelLODTransition(info.DrawState->LODTransition); + + // Check if end transition + if (info.DrawState->LODTransition == 255) + { + info.DrawState->PrevLOD = lodIndex; + } + else + { + const auto prevLOD = model->ClampLODIndex(info.DrawState->PrevLOD); + const float normalizedProgress = static_cast(info.DrawState->LODTransition) * (1.0f / 255.0f); + model->LODs.Get()[prevLOD].Draw(renderContext, info, normalizedProgress); + } + } + + return; + } + } + lodIndex += info.LODBias + renderContext.View.ModelLODBias; + lodIndex = model->ClampLODIndex(lodIndex); + + if (renderContext.View.IsSingleFrame) + { + } + // Check if it's the new frame and could update the drawing state (note: model instance could be rendered many times per frame to different viewports) + else if (modelFrame == frame) + { + // Check if materials use transition + if (!ModelDrawTransition(model, info)) + { + info.DrawState->PrevLOD = lodIndex; + info.DrawState->LODTransition = 255; + } + + // Check if start transition + if (info.DrawState->PrevLOD != lodIndex && info.DrawState->LODTransition == 255) + { + info.DrawState->LODTransition = 0; + } + + RenderTools::UpdateModelLODTransition(info.DrawState->LODTransition); + + // Check if end transition + if (info.DrawState->LODTransition == 255) + { + info.DrawState->PrevLOD = lodIndex; + } + } + // Check if there was a gap between frames in drawing this model instance + else if (modelFrame < frame || info.DrawState->PrevLOD == -1) + { + // Reset state + info.DrawState->PrevLOD = lodIndex; + info.DrawState->LODTransition = 255; + } + + // Draw + if (info.DrawState->PrevLOD == lodIndex || renderContext.View.IsSingleFrame) + { + model->LODs.Get()[lodIndex].Draw(context, info, 0.0f); + } + else if (info.DrawState->PrevLOD == -1) + { + const float normalizedProgress = static_cast(info.DrawState->LODTransition) * (1.0f / 255.0f); + model->LODs.Get()[lodIndex].Draw(context, info, 1.0f - normalizedProgress); + } + else + { + const auto prevLOD = model->ClampLODIndex(info.DrawState->PrevLOD); + const float normalizedProgress = static_cast(info.DrawState->LODTransition) * (1.0f / 255.0f); + model->LODs.Get()[prevLOD].Draw(context, info, normalizedProgress); + model->LODs.Get()[lodIndex].Draw(context, info, normalizedProgress - 1.0f); + } +} diff --git a/Source/Engine/Graphics/RenderTools.cpp b/Source/Engine/Graphics/RenderTools.cpp index 0a98f1401..5a7ca9079 100644 --- a/Source/Engine/Graphics/RenderTools.cpp +++ b/Source/Engine/Graphics/RenderTools.cpp @@ -461,7 +461,7 @@ int32 RenderTools::ComputeModelLOD(const Model* model, const Float3& origin, flo return 0; } -int32 RenderTools::ComputeSkinnedModelLOD(const SkinnedModel* model, const Float3& origin, float radius, const RenderContext& renderContext) +int32 RenderTools::ComputeModelLOD(const SkinnedModel* model, const Float3& origin, float radius, const RenderContext& renderContext) { const auto lodView = (renderContext.LodProxyView ? renderContext.LodProxyView : &renderContext.View); const float screenRadiusSquared = ComputeBoundsScreenRadiusSquared(origin, radius, *lodView) * renderContext.View.ModelLODDistanceFactorSqrt; @@ -486,6 +486,11 @@ int32 RenderTools::ComputeSkinnedModelLOD(const SkinnedModel* model, const Float return 0; } +int32 RenderTools::ComputeSkinnedModelLOD(const SkinnedModel* model, const Float3& origin, float radius, const RenderContext& renderContext) +{ + return ComputeModelLOD(model, origin, radius, renderContext); +} + void RenderTools::ComputeCascadeUpdateFrequency(int32 cascadeIndex, int32 cascadeCount, int32& updateFrequency, int32& updatePhrase, int32 updateMaxCountPerFrame) { switch (updateMaxCountPerFrame) diff --git a/Source/Engine/Graphics/RenderTools.h b/Source/Engine/Graphics/RenderTools.h index 7be73c912..8c565eed5 100644 --- a/Source/Engine/Graphics/RenderTools.h +++ b/Source/Engine/Graphics/RenderTools.h @@ -109,15 +109,26 @@ public: /// The zero-based LOD index. Returns -1 if model should not be rendered. API_FUNCTION() static int32 ComputeModelLOD(const Model* model, API_PARAM(Ref) const Float3& origin, float radius, API_PARAM(Ref) const RenderContext& renderContext); + /// + /// Computes the model LOD index to use during rendering. + /// + /// The model. + /// The bounds origin. + /// The bounds radius. + /// The rendering context. + /// The zero-based LOD index. Returns -1 if model should not be rendered. + API_FUNCTION() static int32 ComputeModelLOD(const SkinnedModel* model, API_PARAM(Ref) const Float3& origin, float radius, API_PARAM(Ref) const RenderContext& renderContext); + /// /// Computes the skinned model LOD index to use during rendering. + /// [Deprecated in v1.12] /// /// The skinned model. /// The bounds origin. /// The bounds radius. /// The rendering context. /// The zero-based LOD index. Returns -1 if model should not be rendered. - API_FUNCTION() static int32 ComputeSkinnedModelLOD(const SkinnedModel* model, API_PARAM(Ref) const Float3& origin, float radius, API_PARAM(Ref) const RenderContext& renderContext); + API_FUNCTION() DEPRECATED("Use ComputeModelLOD instead.") static int32 ComputeSkinnedModelLOD(const SkinnedModel* model, API_PARAM(Ref) const Float3& origin, float radius, API_PARAM(Ref) const RenderContext& renderContext); /// /// Computes the sorting key for depth value (quantized) From cb2f2bf6443eb806e441e4a93c68e692ac725476 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 16 Apr 2026 23:24:53 +0200 Subject: [PATCH 08/51] Fix drawing incorrect LOD transition when using 2 cameras in a scene at once #4055 --- Source/Engine/Graphics/Models/ModelDraw.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Graphics/Models/ModelDraw.h b/Source/Engine/Graphics/Models/ModelDraw.h index 3e2a61242..2867499f3 100644 --- a/Source/Engine/Graphics/Models/ModelDraw.h +++ b/Source/Engine/Graphics/Models/ModelDraw.h @@ -111,7 +111,7 @@ FORCE_INLINE void ModelDraw(ModelType* model, const RenderContext& renderContext } // Draw - if (info.DrawState->PrevLOD == lodIndex || renderContext.View.IsSingleFrame) + if (info.DrawState->PrevLOD == lodIndex || info.DrawState->LODTransition == 255 || renderContext.View.IsSingleFrame) { model->LODs.Get()[lodIndex].Draw(context, info, 0.0f); } From e9d61ebf9f471f7f76308672e4eb18def94dd87d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 17 Apr 2026 08:39:26 +0200 Subject: [PATCH 09/51] Fix Editor packaging on macOS without SDL --- Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs index b3ff862bb..66067eaa6 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs @@ -348,7 +348,8 @@ namespace Flax.Deploy DeployFile(src, dst, "MoltenVK_icd.json"); DeployFiles(src, dst, "*.dll"); DeployFiles(src, dst, "*.dylib"); - DeployFile(src, dst, "Logo.png"); + if (EngineConfiguration.UseSDL && MacConfiguration.UseSDL) + DeployFile(src, dst, "Logo.png"); // Optimize package size Utilities.Run("strip", "FlaxEditor", null, dst, Utilities.RunOptions.None); From 0754ea85a956e2973632d47ca3610b7f6e53cf86 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 17 Apr 2026 08:55:41 +0200 Subject: [PATCH 10/51] Fix material slot comboboxes update when editing model material slots --- .../Editor/Windows/Assets/ModelBaseWindow.cs | 33 +++---------------- 1 file changed, 4 insertions(+), 29 deletions(-) diff --git a/Source/Editor/Windows/Assets/ModelBaseWindow.cs b/Source/Editor/Windows/Assets/ModelBaseWindow.cs index 09b4606c4..3046ea43b 100644 --- a/Source/Editor/Windows/Assets/ModelBaseWindow.cs +++ b/Source/Editor/Windows/Assets/ModelBaseWindow.cs @@ -112,12 +112,14 @@ namespace FlaxEditor.Windows.Assets Window._isolateIndex = -1; Window._highlightIndex = -1; + Window._meshProxy = this; } public override void OnClean() { Window._isolateIndex = -1; Window._highlightIndex = -1; + Window._meshProxy = null; base.OnClean(); } @@ -402,40 +404,12 @@ namespace FlaxEditor.Windows.Assets slots[i].ShadowsMode = shadowsModes[i]; } - UpdateMaterialSlotsUI(); + Window?._meshProxy?.UpdateMaterialSlotsUI(); } } } } - private readonly List _materialSlotComboBoxes = new List(); - - /// - /// Updates the material slots UI parts. Should be called after material slot rename. - /// - public void UpdateMaterialSlotsUI() - { - Window._skipEffectsGuiEvents = true; - - // Generate material slots labels (with index prefix) - var slots = Asset.MaterialSlots; - var slotsLabels = new string[slots.Length]; - for (int i = 0; i < slots.Length; i++) - { - slotsLabels[i] = string.Format("[{0}] {1}", i, slots[i].Name); - } - - // Update comboboxes - for (int i = 0; i < _materialSlotComboBoxes.Count; i++) - { - var comboBox = _materialSlotComboBoxes[i]; - comboBox.SetItems(slotsLabels); - comboBox.SelectedIndex = ((Mesh)comboBox.Tag).MaterialSlotIndex; - } - - Window._skipEffectsGuiEvents = false; - } - protected class ProxyEditor : ProxyEditorBase { public override void Initialize(LayoutElementsContainer layout) @@ -776,6 +750,7 @@ namespace FlaxEditor.Windows.Assets protected readonly Tabs _tabs; protected readonly ToolStripButton _saveButton; + private MeshesPropertiesProxyBase _meshProxy; protected ModelImportSettings _importSettings = new ModelImportSettings(); protected bool _refreshOnLODsLoaded; protected bool _skipEffectsGuiEvents; From 915f655e2cef096226401596e617be391a4893db Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 17 Apr 2026 09:51:28 +0200 Subject: [PATCH 11/51] Fix missing shader file in packaged build and startup on broken gizmo material --- Source/Editor/Gizmo/TransformGizmoBase.Draw.cs | 3 ++- Source/ThirdParty/FidelityFX/FidelityFX.Build.cs | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Gizmo/TransformGizmoBase.Draw.cs b/Source/Editor/Gizmo/TransformGizmoBase.Draw.cs index 03c18268d..8ab597952 100644 --- a/Source/Editor/Gizmo/TransformGizmoBase.Draw.cs +++ b/Source/Editor/Gizmo/TransformGizmoBase.Draw.cs @@ -81,7 +81,8 @@ namespace FlaxEditor.Gizmo _isDisabled = ShouldGizmoBeLocked(); float brightness = _isDisabled ? options.Visual.TransformGizmoBrightnessDisabled : options.Visual.TransformGizmoBrightness; - if (Mathf.NearEqual(brightness, (float)_materialAxisX.GetParameterValue(_brightnessParamName))) + var currentValue = _materialAxisX.GetParameterValue(_brightnessParamName); + if (currentValue is not float currentValueFloat || Mathf.NearEqual(brightness, currentValueFloat)) return; _materialAxisX.SetParameterValue(_brightnessParamName, brightness); _materialAxisY.SetParameterValue(_brightnessParamName, brightness); diff --git a/Source/ThirdParty/FidelityFX/FidelityFX.Build.cs b/Source/ThirdParty/FidelityFX/FidelityFX.Build.cs index 2057aad9b..a180a27d9 100644 --- a/Source/ThirdParty/FidelityFX/FidelityFX.Build.cs +++ b/Source/ThirdParty/FidelityFX/FidelityFX.Build.cs @@ -1,7 +1,8 @@ // Copyright (c) Wojciech Figat. All rights reserved. using Flax.Build; -using Flax.Build.NativeCpp; +using System.Collections.Generic; +using System.IO; /// /// https://github.com/GPUOpen-LibrariesAndSDKs/FidelityFX-SDK @@ -19,4 +20,12 @@ public class FidelityFX : HeaderOnlyModule // Merge third-party modules into engine binary BinaryModuleName = "FlaxEngine"; } + + /// + public override void GetFilesToDeploy(List files) + { + base.GetFilesToDeploy(files); + + files.AddRange(Directory.GetFiles(FolderPath, "*.h", SearchOption.AllDirectories)); + } } From c1de97b77b02df4be3ead9bf9d8497dcef811e01 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 17 Apr 2026 12:00:18 +0200 Subject: [PATCH 12/51] Add drawing particle debug in system editor window --- Source/Editor/Windows/Assets/ParticleSystemWindow.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Editor/Windows/Assets/ParticleSystemWindow.cs b/Source/Editor/Windows/Assets/ParticleSystemWindow.cs index 358618f80..83be4b43c 100644 --- a/Source/Editor/Windows/Assets/ParticleSystemWindow.cs +++ b/Source/Editor/Windows/Assets/ParticleSystemWindow.cs @@ -338,6 +338,8 @@ namespace FlaxEditor.Windows.Assets PlaySimulation = true, Parent = _split2.Panel1 }; + _preview.PreviewActor.ShowDebugDraw = true; + _preview.ShowDebugDraw = true; // Timeline _timeline = new ParticleSystemTimeline(_preview, _undo) From f5b5228ddff249b5d3924bd8449928e91ffec4f3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 17 Apr 2026 13:22:15 +0200 Subject: [PATCH 13/51] Add debug drawing particle collision modules --- ...rticleEmitterGraph.CPU.ParticleModules.cpp | 62 ++++++++++++++++--- .../Graph/CPU/ParticleEmitterGraph.CPU.cpp | 2 + 2 files changed, 54 insertions(+), 10 deletions(-) diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.ParticleModules.cpp b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.ParticleModules.cpp index bb1e977e4..f5e53503c 100644 --- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.ParticleModules.cpp +++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.ParticleModules.cpp @@ -12,6 +12,7 @@ #include "Engine/Core/Math/OrientedBoundingBox.h" #include "Engine/Utilities/Noise.h" #include "Engine/Debug/DebugDraw.h" +#include "Engine/Engine/Units.h" // ReSharper disable CppCStyleCast // ReSharper disable CppClangTidyClangDiagnosticCastAlign @@ -1485,7 +1486,9 @@ void ParticleEmitterGraphCPUExecutor::DebugDrawModule(ParticleEmitterGraphCPUNod if (node->UsePerParticleDataResolve()) return; - const Color color = Color::White; + const Color colorPosition = Color::White; + const Color colorCollision = Color::GreenYellow; + const Color colorCollisionFill = colorCollision.AlphaMultiplied(0.1f); switch (node->TypeID) { case 202: // Position (sphere surface) @@ -1493,7 +1496,7 @@ void ParticleEmitterGraphCPUExecutor::DebugDrawModule(ParticleEmitterGraphCPUNod { const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2)); const float radius = (float)GetValue(node->GetBox(1), 3); - DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(center, radius), color, 0.0f, true); + DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(center, radius), colorPosition, 0.0f, true); break; } case 203: // Position (plane) @@ -1503,7 +1506,7 @@ void ParticleEmitterGraphCPUExecutor::DebugDrawModule(ParticleEmitterGraphCPUNod const Float3 halfExtent = Float3(size.X * 0.5f, 0.0f, size.Y * 0.5f); OrientedBoundingBox box(halfExtent, Transform(center)); box.Transform(transform); - DEBUG_DRAW_WIRE_BOX(box, color, 0.0f, true); + DEBUG_DRAW_WIRE_BOX(box, colorPosition, 0.0f, true); break; } case 204: // Position (circle) @@ -1511,7 +1514,7 @@ void ParticleEmitterGraphCPUExecutor::DebugDrawModule(ParticleEmitterGraphCPUNod { const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2)); const float radius = (float)GetValue(node->GetBox(1), 3); - DEBUG_DRAW_WIRE_CYLINDER(center, transform.Orientation * Quaternion::Euler(90, 0, 0), radius, 0.0f, color, 0.0f, true); + DEBUG_DRAW_WIRE_CYLINDER(center, transform.Orientation * Quaternion::Euler(90, 0, 0), radius, 0.0f, colorPosition, 0.0f, true); break; } case 206: // Position (box surface) @@ -1521,7 +1524,7 @@ void ParticleEmitterGraphCPUExecutor::DebugDrawModule(ParticleEmitterGraphCPUNod const Float3 size = (Float3)GetValue(node->GetBox(1), 3); OrientedBoundingBox box(size * 0.5f, Transform(center)); box.Transform(transform); - DEBUG_DRAW_WIRE_BOX(box, color, 0.0f, true); + DEBUG_DRAW_WIRE_BOX(box, colorPosition, 0.0f, true); break; } // Position (cylinder) @@ -1530,7 +1533,7 @@ void ParticleEmitterGraphCPUExecutor::DebugDrawModule(ParticleEmitterGraphCPUNod const float height = (float)GetValue(node->GetBox(2), 4); const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2) + Float3(0, 0, height * 0.5f)); const float radius = (float)GetValue(node->GetBox(1), 3); - DEBUG_DRAW_WIRE_CYLINDER(center, transform.Orientation * Quaternion::Euler(90, 0, 0), radius, height, color, 0.0f, true); + DEBUG_DRAW_WIRE_CYLINDER(center, transform.Orientation * Quaternion::Euler(90, 0, 0), radius, height, colorPosition, 0.0f, true); break; } // Position (line) @@ -1538,7 +1541,7 @@ void ParticleEmitterGraphCPUExecutor::DebugDrawModule(ParticleEmitterGraphCPUNod { const Float3 start = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2)); const Float3 end = transform.LocalToWorld((Float3)GetValue(node->GetBox(1), 3)); - DEBUG_DRAW_LINE(start, end, color, 0.0f, true); + DEBUG_DRAW_LINE(start, end, colorPosition, 0.0f, true); break; } // Position (torus) @@ -1547,15 +1550,54 @@ void ParticleEmitterGraphCPUExecutor::DebugDrawModule(ParticleEmitterGraphCPUNod const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2)); const float radius = Math::Max((float)GetValue(node->GetBox(1), 3), ZeroTolerance); const float thickness = (float)GetValue(node->GetBox(2), 4); - DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(center, radius + thickness), color, 0.0f, true); + DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(center, radius + thickness), colorPosition, 0.0f, true); break; } - // Position (spiral) case 214: { const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(0), 2)); - DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(center, 5.0f), color, 0.0f, true); + DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(center, 5.0f), colorPosition, 0.0f, true); + break; + } + // Collision (plane) + case 330: + { + const Float3 position = transform.LocalToWorld((Float3)GetValue(node->GetBox(5), 8)); + //const Float3 normal = transform.Orientation * (Float3)GetValue(node->GetBox(6), 9) * ((bool)node->Values[2] ? -1.0f : 1.0f); + const Float3 extent(METERS_TO_UNITS(10), 0.001f, METERS_TO_UNITS(10)); + DEBUG_DRAW_BOX(BoundingBox(position - extent, position + extent), colorCollisionFill, 0.0f, true); + DEBUG_DRAW_WIRE_BOX(BoundingBox(position - extent, position + extent), colorCollision, 0.0f, true); + break; + } + // Collision (sphere) + case 331: + { + const Float3 position = transform.LocalToWorld((Float3)GetValue(node->GetBox(5), 8)); + const float radius = (float)GetValue(node->GetBox(6), 9); + DEBUG_DRAW_SPHERE(BoundingSphere(position, radius), colorCollisionFill, 0.0f, true); + DEBUG_DRAW_WIRE_SPHERE(BoundingSphere(position, radius), colorCollision, 0.0f, true); + break; + } + // Collision (box) + case 332: + { + const Float3 center = (Float3)GetValue(node->GetBox(5), 8); + const Float3 size = (Float3)GetValue(node->GetBox(6), 9); + OrientedBoundingBox box(size * 0.5f, Transform(center)); + box.Transform(transform); + DEBUG_DRAW_BOX(box, colorCollisionFill, 0.0f, true); + DEBUG_DRAW_WIRE_BOX(box, colorCollision, 0.0f, true); + break; + } + // Collision (cylinder) + case 333: + { + const float height = (float)GetValue(node->GetBox(6), 9); + const Float3 center = transform.LocalToWorld((Float3)GetValue(node->GetBox(5), 8) + Float3(0, 0, height * 0.5f)); + const float radius = (float)GetValue(node->GetBox(7), 10); + DEBUG_DRAW_CYLINDER(center, transform.Orientation * Quaternion::Euler(90, 0, 0), radius, height, colorCollisionFill, 0.0f, true); + DEBUG_DRAW_WIRE_CYLINDER(center, transform.Orientation * Quaternion::Euler(90, 0, 0), radius, height, colorCollision, 0.0f, true); break; } } diff --git a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp index bdfdf1956..54b2ea0cc 100644 --- a/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp +++ b/Source/Engine/Particles/Graph/CPU/ParticleEmitterGraph.CPU.cpp @@ -437,6 +437,8 @@ void ParticleEmitterGraphCPUExecutor::DrawDebug(ParticleEmitter* emitter, Partic DebugDrawModule(module, transform); for (auto module : emitter->Graph.InitModules) DebugDrawModule(module, transform); + for (auto module : emitter->Graph.UpdateModules) + DebugDrawModule(module, transform); } #endif From fa01d2d8a3db6c487886077c62eb1a66153a39af Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 17 Apr 2026 14:25:58 +0200 Subject: [PATCH 14/51] Add additional way of loading unsupported texture formats via Texture Tool conversion This fixes loading `BC7` textures on `D3D10` backend. --- .../Engine/Tools/TextureTool/TextureTool.cpp | 56 +++++++++++++------ 1 file changed, 39 insertions(+), 17 deletions(-) diff --git a/Source/Engine/Tools/TextureTool/TextureTool.cpp b/Source/Engine/Tools/TextureTool/TextureTool.cpp index d42a8bb59..4cb807c54 100644 --- a/Source/Engine/Tools/TextureTool/TextureTool.cpp +++ b/Source/Engine/Tools/TextureTool/TextureTool.cpp @@ -415,31 +415,53 @@ bool TextureTool::UpdateTexture(GPUContext* context, GPUTexture* texture, int32 if (textureFormat != dataFormat) { PROFILE_CPU_NAMED("ConvertTexture"); - auto dataSampler = PixelFormatSampler::Get(dataFormat); - auto textureSampler = PixelFormatSampler::Get(textureFormat); - if (!dataSampler || !textureSampler) - return true; int32 mipWidth, mipHeight, mipDepth; texture->GetMipSize(mipIndex, mipWidth, mipHeight, mipDepth); - auto tempRowPitch = mipWidth * textureSampler->PixelSize; - auto tempSlicePitch = tempRowPitch * mipHeight; - tempData.Resize(tempSlicePitch * mipDepth); - - ASSERT(data.Length() / rowPitch >= (uint32)mipHeight); - for (int32 y = 0; y < mipHeight; y++) + auto dataSampler = PixelFormatSampler::Get(dataFormat); + auto textureSampler = PixelFormatSampler::Get(textureFormat); + if (dataSampler && textureSampler) { - for (int32 x = 0; x < mipWidth; x++) + // Conversion with an in-built samplers + auto tempRowPitch = mipWidth * textureSampler->PixelSize; + auto tempSlicePitch = tempRowPitch * mipHeight; + tempData.Resize(tempSlicePitch * mipDepth); + ASSERT(data.Length() / rowPitch >= (uint32)mipHeight); + for (int32 y = 0; y < mipHeight; y++) { - Color color = dataSampler->SamplePoint(data.Get(), x, y, rowPitch); - textureSampler->Store(tempData.Get(), x, y, tempRowPitch, color); + for (int32 x = 0; x < mipWidth; x++) + { + Color color = dataSampler->SamplePoint(data.Get(), x, y, rowPitch); + textureSampler->Store(tempData.Get(), x, y, tempRowPitch, color); + } } + data = ToSpan(tempData); + rowPitch = tempRowPitch; + slicePitch = tempSlicePitch; + } + else + { + // Conversion with external library + TextureData src, dst; + src.Width = mipWidth; + src.Height = mipHeight; + src.Depth = mipDepth; + src.Format = dataFormat; + auto& srcItem = src.Items.AddOne(); + auto& srcMip = srcItem.Mips.AddOne(); + srcMip.RowPitch = rowPitch; + srcMip.DepthPitch = slicePitch; + srcMip.Lines = slicePitch / rowPitch; + srcMip.Data.Link(data); + if (Convert(dst, src, textureFormat)) + return true; + auto& dstMip = dst.Items[0].Mips[0]; + tempData.Set(dstMip.Data.Get(), dstMip.Data.Length()); + data = ToSpan(tempData); + rowPitch = dstMip.RowPitch; + slicePitch = dstMip.DepthPitch; } - - data = ToSpan(tempData); - rowPitch = tempRowPitch; - slicePitch = tempSlicePitch; } // Update texture From bfc2b276e15086d5cef1ab9c371cb7ebc5415fe9 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 17 Apr 2026 14:53:14 +0200 Subject: [PATCH 15/51] Fix crash when loading Visject graph with a broken connection --- Source/Engine/Visject/Graph.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Visject/Graph.h b/Source/Engine/Visject/Graph.h index 19aa3e892..7a09e63d4 100644 --- a/Source/Engine/Visject/Graph.h +++ b/Source/Engine/Visject/Graph.h @@ -287,7 +287,11 @@ public: { int32 hintIndex = (int32)(intptr)box->Connections[k]; TmpConnectionHint hint = tmpHints[hintIndex]; - box->Connections[k] = hint.Node->GetBox(hint.BoxID); + Box* hintBox = hint.Node->TryGetBox(hint.BoxID); + if (hintBox == nullptr) + box->Connections.RemoveAtKeepOrder(k--); + else + box->Connections[k] = hintBox; } } } From f1f17c82891bbff6983c23b1b4c813f6f68177d0 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 17 Apr 2026 15:59:51 +0200 Subject: [PATCH 16/51] Fix compilation for Web --- Source/Engine/Scripting/Runtime/Mono.cpp | 4 ++-- Source/Engine/Scripting/Runtime/None.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Engine/Scripting/Runtime/Mono.cpp b/Source/Engine/Scripting/Runtime/Mono.cpp index f32d12c57..2c924a874 100644 --- a/Source/Engine/Scripting/Runtime/Mono.cpp +++ b/Source/Engine/Scripting/Runtime/Mono.cpp @@ -1091,9 +1091,9 @@ const MAssembly::ClassesDictionary& MAssembly::GetClasses() const return _classes; } -const MAssembly::ClassesDictionary& MAssembly::GetTypeClasses() const +MAssembly::ClassesDictionary& MAssembly::GetTypeClasses() const { - return GetClasses(); + return const_cast(GetClasses()); } bool MAssembly::Load(MonoImage* monoImage) diff --git a/Source/Engine/Scripting/Runtime/None.cpp b/Source/Engine/Scripting/Runtime/None.cpp index 87ec35ac0..1782134d9 100644 --- a/Source/Engine/Scripting/Runtime/None.cpp +++ b/Source/Engine/Scripting/Runtime/None.cpp @@ -303,9 +303,9 @@ const MAssembly::ClassesDictionary& MAssembly::GetClasses() const return _classes; } -const MAssembly::ClassesDictionary& MAssembly::GetTypeClasses() const +MAssembly::ClassesDictionary& MAssembly::GetTypeClasses() const { - return GetClasses(); + return const_cast(GetClasses()); } bool MAssembly::LoadCorlib() From d0ac31c02b87d4d4b296ebab2994c2d732339fff Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 17 Apr 2026 17:43:03 +0200 Subject: [PATCH 17/51] Fix incorrect shader sample support for WebGPU texture formats from Tier1 that are only `unfilterable-float` for shader load-only --- .../GraphicsDevice/WebGPU/GPUDeviceWebGPU.cpp | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Source/Engine/GraphicsDevice/WebGPU/GPUDeviceWebGPU.cpp b/Source/Engine/GraphicsDevice/WebGPU/GPUDeviceWebGPU.cpp index 4eb807f18..2b5a2d6ac 100644 --- a/Source/Engine/GraphicsDevice/WebGPU/GPUDeviceWebGPU.cpp +++ b/Source/Engine/GraphicsDevice/WebGPU/GPUDeviceWebGPU.cpp @@ -321,17 +321,19 @@ bool GPUDeviceWebGPU::Init() FormatSupport::Buffer | FormatSupport::InputAssemblyIndexBuffer | FormatSupport::InputAssemblyVertexBuffer; - auto supportsTexture = + auto supportsTextureUnfilterable = FormatSupport::Texture1D | FormatSupport::Texture2D | FormatSupport::Texture3D | FormatSupport::TextureCube | FormatSupport::ShaderLoad | + FormatSupport::Mip; + auto supportsTexture = + supportsTextureUnfilterable | FormatSupport::ShaderSample | FormatSupport::ShaderSampleComparison | FormatSupport::ShaderGather | - FormatSupport::ShaderGatherComparison | - FormatSupport::Mip; + FormatSupport::ShaderGatherComparison; auto supportsRender = FormatSupport::RenderTarget | FormatSupport::Blendable; @@ -414,18 +416,18 @@ bool GPUDeviceWebGPU::Init() FeaturesPerFormat[(int32)PixelFormat::R8_SNorm].Support |= supportsBuffer | supportsTexture | supportsBasicStorage; FeaturesPerFormat[(int32)PixelFormat::R8_UInt].Support |= supportsBasicStorage; FeaturesPerFormat[(int32)PixelFormat::R8_SInt].Support |= supportsBasicStorage; - FeaturesPerFormat[(int32)PixelFormat::R16_UNorm].Support |= supportsBuffer | supportsTexture | supportsRender | supportsMSAA | supportsBasicStorage; - FeaturesPerFormat[(int32)PixelFormat::R16_SNorm].Support |= supportsBuffer | supportsTexture | supportsBasicStorage; + FeaturesPerFormat[(int32)PixelFormat::R16_UNorm].Support |= supportsBuffer | supportsTextureUnfilterable | supportsRender | supportsMSAA | supportsBasicStorage; + FeaturesPerFormat[(int32)PixelFormat::R16_SNorm].Support |= supportsBuffer | supportsTextureUnfilterable | supportsBasicStorage; //FeaturesPerFormat[(int32)PixelFormat::R16_UInt].Support |= supportsBasicStorage; // TODO: fix issues with particle indices buffer that could use it FeaturesPerFormat[(int32)PixelFormat::R16_SInt].Support |= supportsBasicStorage; FeaturesPerFormat[(int32)PixelFormat::R16_Float].Support |= supportsBasicStorage; - FeaturesPerFormat[(int32)PixelFormat::R16G16_UNorm].Support |= supportsBuffer | supportsTexture | supportsRender | supportsMultisampling | supportsBasicStorage; - FeaturesPerFormat[(int32)PixelFormat::R16G16_SNorm].Support |= supportsBuffer | supportsTexture | supportsRender | supportsMultisampling | supportsBasicStorage; + FeaturesPerFormat[(int32)PixelFormat::R16G16_UNorm].Support |= supportsBuffer | supportsTextureUnfilterable | supportsRender | supportsMultisampling | supportsBasicStorage; + FeaturesPerFormat[(int32)PixelFormat::R16G16_SNorm].Support |= supportsBuffer | supportsTextureUnfilterable | supportsRender | supportsMultisampling | supportsBasicStorage; FeaturesPerFormat[(int32)PixelFormat::R16G16_UInt].Support |= supportsBasicStorage; FeaturesPerFormat[(int32)PixelFormat::R16G16_SInt].Support |= supportsBasicStorage; FeaturesPerFormat[(int32)PixelFormat::R16G16_Float].Support |= supportsBasicStorage; - FeaturesPerFormat[(int32)PixelFormat::R16G16B16A16_UNorm].Support |= supportsBuffer | supportsTexture | supportsRender | supportsMultisampling | supportsBasicStorage; - FeaturesPerFormat[(int32)PixelFormat::R16G16B16A16_SNorm].Support |= supportsBuffer | supportsTexture | supportsRender | supportsMultisampling | supportsBasicStorage; + FeaturesPerFormat[(int32)PixelFormat::R16G16B16A16_UNorm].Support |= supportsBuffer | supportsTextureUnfilterable | supportsRender | supportsMultisampling | supportsBasicStorage; + FeaturesPerFormat[(int32)PixelFormat::R16G16B16A16_SNorm].Support |= supportsBuffer | supportsTextureUnfilterable | supportsRender | supportsMultisampling | supportsBasicStorage; FeaturesPerFormat[(int32)PixelFormat::R10G10B10A2_UNorm].Support |= supportsBasicStorage; FeaturesPerFormat[(int32)PixelFormat::R11G11B10_Float].Support |= supportsBasicStorage; } From cf65c442de6fbc733e7732b16bc10a8d708d8ea3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 17 Apr 2026 18:51:50 +0200 Subject: [PATCH 18/51] Add `AutoAttachDebugPreviewActor` for Behavior Tree editor --- .../Editor/Windows/Assets/BehaviorTreeWindow.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs index 03ba26fc8..1317300c3 100644 --- a/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs +++ b/Source/Editor/Windows/Assets/BehaviorTreeWindow.cs @@ -509,10 +509,25 @@ namespace FlaxEditor.Windows.Assets // Check if don't have valid behavior picked if (!_behaviorPicker.Value) { - // Try to reassign the debug behavior var id = _cachedBehaviorId; + if (id == Guid.Empty && Editor.IsPlayMode && Editor.Options.Options.General.AutoAttachDebugPreviewActor) + { + // Auto-attach preview + var behaviorTree = Asset; + var behaviors = Level.GetScripts(); + foreach (var behavior in behaviors) + { + if (behavior.Tree == behaviorTree && + behavior.IsEnabledInHierarchy) + { + id = behavior.ID; + break; + } + } + } if (id != Guid.Empty) { + // Try to reassign the debug behavior var obj = FlaxEngine.Object.TryFind(ref id); if (obj && obj.Tree == Asset) _behaviorPicker.Value = obj; From 22964255e4e011ae2937ef8cdfba533c07176ccc Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 17 Apr 2026 16:13:36 -0500 Subject: [PATCH 19/51] Add tree view only shortcuts. --- .../Content/Tree/ContentItemTreeNode.cs | 22 --- Source/Editor/Content/Tree/TreeViewPanel.cs | 183 ++++++++++++++++++ Source/Editor/Windows/ContentWindow.cs | 10 +- 3 files changed, 187 insertions(+), 28 deletions(-) create mode 100644 Source/Editor/Content/Tree/TreeViewPanel.cs diff --git a/Source/Editor/Content/Tree/ContentItemTreeNode.cs b/Source/Editor/Content/Tree/ContentItemTreeNode.cs index 09d1ec01b..6fd2f1ebc 100644 --- a/Source/Editor/Content/Tree/ContentItemTreeNode.cs +++ b/Source/Editor/Content/Tree/ContentItemTreeNode.cs @@ -128,28 +128,6 @@ public sealed class ContentItemTreeNode : TreeNode, IContentItemOwner return base.OnMouseDoubleClickHeader(ref location, button); } - /// - public override bool OnKeyDown(KeyboardKeys key) - { - if (IsFocused) - { - switch (key) - { - case KeyboardKeys.Return: - Editor.Instance.Windows.ContentWin.Open(Item); - return true; - case KeyboardKeys.F2: - Editor.Instance.Windows.ContentWin.Rename(Item); - return true; - case KeyboardKeys.Delete: - Editor.Instance.Windows.ContentWin.Delete(Item); - return true; - } - } - - return base.OnKeyDown(key); - } - /// protected override void DoDragDrop() { diff --git a/Source/Editor/Content/Tree/TreeViewPanel.cs b/Source/Editor/Content/Tree/TreeViewPanel.cs new file mode 100644 index 000000000..90666b3b6 --- /dev/null +++ b/Source/Editor/Content/Tree/TreeViewPanel.cs @@ -0,0 +1,183 @@ +using System.Collections.Generic; +using FlaxEditor.GUI.Tree; +using FlaxEditor.Options; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEditor.Content; + +/// +/// The content tree view panel. +/// +public class TreeViewPanel : Panel +{ + /// + /// The content tree assigned to this panel. + /// + public Tree ContentTree; + + private InputActionsContainer _inputActions; + private bool _isCutting; + private List _cutItems = new List(); + + /// + /// Initializes a new instance of the class. + /// + public TreeViewPanel() + : base(ScrollBars.None) + { + // Setup input actions + _inputActions = new InputActionsContainer(new[] + { + new InputActionsContainer.Binding(options => options.Rename, Rename), + new InputActionsContainer.Binding(options => options.Delete, Delete), + new InputActionsContainer.Binding(options => options.Duplicate, Duplicate), + new InputActionsContainer.Binding(options => options.Copy, Copy), + new InputActionsContainer.Binding(options => options.Paste, Paste), + new InputActionsContainer.Binding(options => options.Cut, Cut), + }); + } + + private void Rename() + { + if (ContentTree == null || !Visible) + return; + + var selection = ContentTree.Selection; + if (selection.Count > 0) + { + var node = selection[0]; + if (node is ContentItemTreeNode contentNode) + { + Editor.Instance.Windows.ContentWin.Rename(contentNode.Item); + } + } + } + + private void Delete() + { + if (ContentTree == null || !Visible) + return; + + var selection = ContentTree.Selection; + if (selection.Count > 0) + { + foreach (var node in selection) + { + if (node is ContentItemTreeNode contentNode) + { + Editor.Instance.Windows.ContentWin.Delete(contentNode.Item); + } + } + } + } + + private void Duplicate() + { + if (ContentTree == null || !Visible) + return; + + var selection = ContentTree.Selection; + if (selection.Count > 0) + { + foreach (var node in selection) + { + if (node is ContentItemTreeNode contentNode) + { + Editor.Instance.Windows.ContentWin.Duplicate(contentNode.Item); + } + } + } + } + + private void Copy() + { + if (ContentTree == null || !Visible) + return; + + var selection = ContentTree.Selection; + if (selection.Count == 0) + return; + var filePaths = new List(); + foreach (var node in selection) + if (node is ContentItemTreeNode contentNode) + filePaths.Add(contentNode.Item.Path); + + Clipboard.Files = filePaths.ToArray(); + UpdateContentItemCut(false); + } + + private void Paste() + { + if (ContentTree == null || !Visible) + return; + + var files = Clipboard.Files; + if (files == null || files.Length == 0) + return; + + Editor.Instance.Windows.ContentWin.Paste(files, _isCutting); + UpdateContentItemCut(false); + } + + private void Cut() + { + if (ContentTree == null || !Visible) + return; + + Copy(); + UpdateContentItemCut(true); + } + + private void UpdateContentItemCut(bool cut) + { + _isCutting = cut; + + // Add selection to cut list + if (cut) + { + var selection = ContentTree.Selection; + foreach (var node in selection) + { + if (node is ContentItemTreeNode contentNode) + _cutItems.Add(contentNode.Item); + } + } + + // Update item with if it is being cut. + foreach (var item in _cutItems) + { + item.IsBeingCut = cut; + } + + // Clean up cut items + if (!cut) + _cutItems.Clear(); + } + + /// + public override bool OnKeyDown(KeyboardKeys key) + { + if (!Visible) + return false; + if (ContentTree == null) + return base.OnKeyDown(key); + + if (_inputActions.Process(Editor.Instance, this, key)) + return true; + + var selection = ContentTree.Selection; + if (selection.Count > 0) + { + if (key == KeyboardKeys.Return) + { + foreach (var node in selection) + { + if (node is ContentItemTreeNode contentNode) + Editor.Instance.Windows.ContentWin.Open(contentNode.Item); + } + } + } + return base.OnKeyDown(key); + } +} diff --git a/Source/Editor/Windows/ContentWindow.cs b/Source/Editor/Windows/ContentWindow.cs index 2141b283e..5a73530bc 100644 --- a/Source/Editor/Windows/ContentWindow.cs +++ b/Source/Editor/Windows/ContentWindow.cs @@ -32,7 +32,7 @@ namespace FlaxEditor.Windows private string _workspaceRebuildLocation; private string _lastViewedFolderBeforeReload; private SplitPanel _split; - private Panel _treeOnlyPanel; + private TreeViewPanel _treeOnlyPanel; private ContainerControl _treePanelRoot; private ContainerControl _treeHeaderPanel; private Panel _contentItemsSearchPanel; @@ -186,7 +186,7 @@ namespace FlaxEditor.Windows }; // Tree-only panel (used when showing all content in the tree) - _treeOnlyPanel = new Panel(ScrollBars.None) + _treeOnlyPanel = new TreeViewPanel { AnchorPreset = AnchorPresets.StretchAll, Offsets = new Margin(0, 0, _toolStrip.Bottom, 0), @@ -237,6 +237,7 @@ namespace FlaxEditor.Windows Parent = _contentTreePanel, }; _tree.SelectedChanged += OnTreeSelectionChanged; + _treeOnlyPanel.ContentTree = _tree; // Content items searching query input box and filters selector _contentItemsSearchPanel = new Panel @@ -632,11 +633,7 @@ namespace FlaxEditor.Windows var area = node.TextRect; const float minRenameWidth = 220.0f; if (area.Width < minRenameWidth) - { - float expand = minRenameWidth - area.Width; - area.X -= expand * 0.5f; area.Width = minRenameWidth; - } area.Y -= 2; area.Height += 4.0f; popup = RenamePopup.Show(node, area, item.ShortName, true); @@ -889,6 +886,7 @@ namespace FlaxEditor.Windows // Refresh this folder now and try to find duplicated item Editor.ContentDatabase.RefreshFolder(item.ParentFolder, true); RefreshView(); + RefreshTreeItems(); var targetItem = item.ParentFolder.FindChild(targetPath); // Start renaming it From fe90f4954e2631dc072bf716fcbccbe4e9b0d589 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 17 Apr 2026 16:17:55 -0500 Subject: [PATCH 20/51] Use list overloads --- Source/Editor/Content/Tree/TreeViewPanel.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Content/Tree/TreeViewPanel.cs b/Source/Editor/Content/Tree/TreeViewPanel.cs index 90666b3b6..854bafc7f 100644 --- a/Source/Editor/Content/Tree/TreeViewPanel.cs +++ b/Source/Editor/Content/Tree/TreeViewPanel.cs @@ -62,13 +62,16 @@ public class TreeViewPanel : Panel var selection = ContentTree.Selection; if (selection.Count > 0) { + var items = new List(); foreach (var node in selection) { if (node is ContentItemTreeNode contentNode) { - Editor.Instance.Windows.ContentWin.Delete(contentNode.Item); + items.Add(contentNode.Item); } } + + Editor.Instance.Windows.ContentWin.Delete(items); } } @@ -80,13 +83,16 @@ public class TreeViewPanel : Panel var selection = ContentTree.Selection; if (selection.Count > 0) { + var items = new List(); foreach (var node in selection) { if (node is ContentItemTreeNode contentNode) { - Editor.Instance.Windows.ContentWin.Duplicate(contentNode.Item); + items.Add(contentNode.Item); } } + + Editor.Instance.Windows.ContentWin.Duplicate(items); } } From 6c8c1aed457b3807c62f9b037f9c6c7b9565a1d6 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 17 Apr 2026 16:31:49 -0500 Subject: [PATCH 21/51] Fix context menu. --- Source/Editor/Content/Tree/TreeViewPanel.cs | 30 ++++++++++--- .../Windows/ContentWindow.ContextMenu.cs | 43 +++++++++++++++---- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/Source/Editor/Content/Tree/TreeViewPanel.cs b/Source/Editor/Content/Tree/TreeViewPanel.cs index 854bafc7f..44bbfa878 100644 --- a/Source/Editor/Content/Tree/TreeViewPanel.cs +++ b/Source/Editor/Content/Tree/TreeViewPanel.cs @@ -38,7 +38,10 @@ public class TreeViewPanel : Panel }); } - private void Rename() + /// + /// Renames the selected item. + /// + public void Rename() { if (ContentTree == null || !Visible) return; @@ -54,7 +57,10 @@ public class TreeViewPanel : Panel } } - private void Delete() + /// + /// Deletes the selected items. + /// + public void Delete() { if (ContentTree == null || !Visible) return; @@ -75,7 +81,10 @@ public class TreeViewPanel : Panel } } - private void Duplicate() + /// + /// Duplicates the selected items. + /// + public void Duplicate() { if (ContentTree == null || !Visible) return; @@ -96,7 +105,10 @@ public class TreeViewPanel : Panel } } - private void Copy() + /// + /// Copies the items. + /// + public void Copy() { if (ContentTree == null || !Visible) return; @@ -113,7 +125,10 @@ public class TreeViewPanel : Panel UpdateContentItemCut(false); } - private void Paste() + /// + /// Pastes the items. + /// + public void Paste() { if (ContentTree == null || !Visible) return; @@ -126,7 +141,10 @@ public class TreeViewPanel : Panel UpdateContentItemCut(false); } - private void Cut() + /// + /// Cuts the items. + /// + public void Cut() { if (ContentTree == null || !Visible) return; diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index ad5a8caf2..f4985eb7a 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -67,12 +67,28 @@ namespace FlaxEditor.Windows b = cm.AddButton("Open", () => Open(item)); b.Enabled = proxy != null || isFolder; - if (_view.SelectedCount > 1) - b = cm.AddButton("Open (all selected)", () => + if (_showAllContentInTree) + { + var selection = _tree.Selection; + if (selection.Count > 0) { - foreach (var e in _view.Selection) - Open(e); - }); + b = cm.AddButton("Open (all selected)", () => + { + foreach (var e in _tree.Selection) + if (e is ContentItemTreeNode contentNode) + Open(contentNode.Item); + }); + } + } + else + { + if (_view.SelectedCount > 1) + b = cm.AddButton("Open (all selected)", () => + { + foreach (var e in _view.Selection) + Open(e); + }); + } cm.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(System.IO.Path.GetDirectoryName(item.Path))); @@ -137,12 +153,21 @@ namespace FlaxEditor.Windows { cm.AddButton("Delete", () => Delete(item)); cm.AddSeparator(); - cm.AddButton("Duplicate", _view.Duplicate); - cm.AddButton("Cut", _view.Cut); - cm.AddButton("Copy", _view.Copy); + if (_showAllContentInTree) + { + cm.AddButton("Duplicate", _treeOnlyPanel.Duplicate); + cm.AddButton("Cut", _treeOnlyPanel.Cut); + cm.AddButton("Copy", _treeOnlyPanel.Copy); + } + else + { + cm.AddButton("Duplicate", _view.Duplicate); + cm.AddButton("Cut", _view.Cut); + cm.AddButton("Copy", _view.Copy); + } } - b = cm.AddButton("Paste", _view.Paste); + b = _showAllContentInTree ? cm.AddButton("Paste", _treeOnlyPanel.Paste) : cm.AddButton("Paste", _view.Paste); b.Enabled = _view.CanPaste(); if (isFolder && folder.Node is MainContentFolderTreeNode) From 421e8d5b43c2e192570583af844bcdcfce9d5cab Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 18 Apr 2026 19:55:12 +0200 Subject: [PATCH 22/51] FIx codesigning editor package with Web platform tool --- Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs index 66067eaa6..8c1fb83d5 100644 --- a/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs +++ b/Source/Tools/Flax.Build/Deploy/Deployment.Editor.cs @@ -185,6 +185,15 @@ namespace Flax.Deploy plist = plist.Replace("{Arch}", arch == TargetArchitecture.ARM64 ? "arm64" : "x86_64"); File.WriteAllText(Path.Combine(appContentsPath, "Info.plist"), plist, Encoding.ASCII); + // Codesign tint compiler executable and remove ones for Windows/Linux + var webPlatformFolder = Path.Combine(OutputPath, "Source/Platforms/Web"); + if (Directory.Exists(webPlatformFolder)) + { + Utilities.DirectoryDelete(Path.Combine(webPlatformFolder, "Binaries/Tools/Linux")); + Utilities.DirectoryDelete(Path.Combine(webPlatformFolder, "Binaries/Tools/Windows")); + CodeSign(Path.Combine(webPlatformFolder, "Binaries/Tools/Mac/ARM64/tint")); + } + // Copy output editor files Utilities.DirectoryCopy(OutputPath, appContentsPath); From cf319876e6d558edbbb8c3600f180468cfb49f62 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 18 Apr 2026 19:55:37 +0200 Subject: [PATCH 23/51] Fix build error on missing Emscripten SDK to be more usable --- .../GraphicsDevice/WebGPU/GraphicsDeviceWebGPU.Build.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Engine/GraphicsDevice/WebGPU/GraphicsDeviceWebGPU.Build.cs b/Source/Engine/GraphicsDevice/WebGPU/GraphicsDeviceWebGPU.Build.cs index 0a56770a4..59c241664 100644 --- a/Source/Engine/GraphicsDevice/WebGPU/GraphicsDeviceWebGPU.Build.cs +++ b/Source/Engine/GraphicsDevice/WebGPU/GraphicsDeviceWebGPU.Build.cs @@ -1,5 +1,6 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System; using System.IO; using Flax.Build.NativeCpp; using Flax.Build.Platforms; @@ -23,6 +24,9 @@ public class GraphicsDeviceWebGPU : GraphicsDeviceBaseModule { base.Setup(options); + if (!EmscriptenSdk.Instance.IsValid) + throw new Exception("Cannot build WebGPU for Web without Emscripten SDK. Check environment variable 'EMSDK'."); + var port = "--use-port=emdawnwebgpu:cpp_bindings=false"; options.OutputFiles.Add(port); options.CompileEnv.CustomArgs.Add(port); From e65cd3799ff84e67256e4d0265ab83f6713aaf19 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 18 Apr 2026 20:44:03 +0200 Subject: [PATCH 24/51] Fix cooking game for Web on macOS when using old XCode 16.4 to use a newer python from Emscripten SDK --- .../Editor/Cooker/Platform/Web/WebPlatformTools.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp b/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp index e8956cec1..575a11d37 100644 --- a/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp @@ -188,11 +188,21 @@ bool WebPlatformTools::OnPostProcess(CookingData& data) #endif procSettings.Arguments = String::Format(TEXT("files.data --preload \"{}@/\" --lz4 --js-output=files.js"), data.DataOutputPath); procSettings.WorkingDirectory = data.OriginalOutputPath; +#if PLATFORM_MAC + // Use python bundled with SDK (python from min-spec XCode 16.4 is 3.9.6 which is < 3.10 needed by SDK 5.0) + Array pythons; + FileSystem::GetChildDirectories(pythons, emscriptenSdk / TEXT("/python")); + if (pythons.HasItems()) + { + procSettings.Arguments = procSettings.FileName + TEXT(".py ") + procSettings.Arguments; + procSettings.FileName = pythons[0] / TEXT("/bin/python3"); + } +#endif const int32 result = Platform::CreateProcess(procSettings); if (result != 0) { if (!FileSystem::FileExists(procSettings.FileName)) - data.Error(TEXT("Missing file_packager.bat. Ensure Emscripten SDK installation is valid and 'EMSDK' environment variable points to it.")); + data.Error(TEXT("Missing file_packager tool. Ensure Emscripten SDK installation is valid and 'EMSDK' environment variable points to it.")); data.Error(String::Format(TEXT("Failed to package project files (result code: {0}). See log for more info."), result)); return true; } From 925b83e095db49069dd7d4e68dbf1b3700e0ea5f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 18 Apr 2026 21:01:37 +0200 Subject: [PATCH 25/51] Fix text box selection height on macOS with custom system scale (hack) --- Source/Engine/UI/GUI/Common/RichTextBoxBase.cs | 3 +++ Source/Engine/UI/GUI/Common/TextBox.cs | 7 +++++-- Source/Engine/UI/GUI/Common/TextBoxBase.cs | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs index 7722fc94b..ff94a73f0 100644 --- a/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/RichTextBoxBase.cs @@ -379,6 +379,9 @@ namespace FlaxEngine.GUI var leftEdge = selection.StartIndex <= textBlock.Range.StartIndex ? textBlock.Bounds.UpperLeft : GetCharPosition(selection.StartIndex, out _); var rightEdge = selection.EndIndex >= textBlock.Range.EndIndex ? textBlock.Bounds.UpperRight : GetCharPosition(selection.EndIndex, out _); float height = font.Height; +#if PLATFORM_MAC && !PLATFORM_SDL + height /= (float)Platform.Dpi / 96.0f; // TODO: refactor DPI support on macOS to skip such hacks +#endif float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f); alpha *= alpha; Color selectionColor = Color.White * alpha; diff --git a/Source/Engine/UI/GUI/Common/TextBox.cs b/Source/Engine/UI/GUI/Common/TextBox.cs index 3d7099db6..1346a8c2b 100644 --- a/Source/Engine/UI/GUI/Common/TextBox.cs +++ b/Source/Engine/UI/GUI/Common/TextBox.cs @@ -253,8 +253,11 @@ namespace FlaxEngine.GUI { var leftEdge = font.GetCharPosition(text, SelectionLeft, ref _layout); var rightEdge = font.GetCharPosition(text, SelectionRight, ref _layout); - var fontHeight = font.Height; - var textHeight = fontHeight / DpiScale; + float fontHeight = font.Height; +#if PLATFORM_MAC && !PLATFORM_SDL + fontHeight /= (float)Platform.Dpi / 96.0f; // TODO: refactor DPI support on macOS to skip such hacks +#endif + float textHeight = fontHeight / DpiScale; // Draw selection background float alpha = Mathf.Min(1.0f, Mathf.Cos(_animateTime * BackgroundSelectedFlashSpeed) * 0.5f + 1.3f); diff --git a/Source/Engine/UI/GUI/Common/TextBoxBase.cs b/Source/Engine/UI/GUI/Common/TextBoxBase.cs index 491fbcd94..415a3c9c8 100644 --- a/Source/Engine/UI/GUI/Common/TextBoxBase.cs +++ b/Source/Engine/UI/GUI/Common/TextBoxBase.cs @@ -556,6 +556,9 @@ namespace FlaxEngine.GUI { const float caretWidth = 1.2f; Float2 caretPos = GetCharPosition(CaretPosition, out var height); +#if PLATFORM_MAC && !PLATFORM_SDL + height /= (float)Platform.Dpi / 96.0f; // TODO: refactor DPI support on macOS to skip such hacks +#endif return new Rectangle( caretPos.X - (caretWidth * 0.5f), caretPos.Y, From b6dfd1351dd60cebd7cbc1d03659244687a13d94 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 18 Apr 2026 22:31:57 +0200 Subject: [PATCH 26/51] Use bundled python from Emscripten SDK on Windows too --- .../Cooker/Platform/Web/WebPlatformTools.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp b/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp index 575a11d37..a2c35f38f 100644 --- a/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp +++ b/Source/Editor/Cooker/Platform/Web/WebPlatformTools.cpp @@ -183,20 +183,27 @@ bool WebPlatformTools::OnPostProcess(CookingData& data) String emscriptenSdk = TEXT("EMSDK"); Platform::GetEnvironmentVariable(emscriptenSdk, emscriptenSdk); procSettings.FileName = emscriptenSdk / TEXT("upstream/emscripten/tools/file_packager"); -#if PLATFORM_WIN32 - procSettings.FileName += TEXT(".bat"); -#endif procSettings.Arguments = String::Format(TEXT("files.data --preload \"{}@/\" --lz4 --js-output=files.js"), data.DataOutputPath); procSettings.WorkingDirectory = data.OriginalOutputPath; -#if PLATFORM_MAC +#if PLATFORM_MAC || PLATFORM_WINDOWS // Use python bundled with SDK (python from min-spec XCode 16.4 is 3.9.6 which is < 3.10 needed by SDK 5.0) Array pythons; FileSystem::GetChildDirectories(pythons, emscriptenSdk / TEXT("/python")); if (pythons.HasItems()) { procSettings.Arguments = procSettings.FileName + TEXT(".py ") + procSettings.Arguments; +#if PLATFORM_WINDOWS + procSettings.FileName = pythons[0] / TEXT("/python.exe"); +#else procSettings.FileName = pythons[0] / TEXT("/bin/python3"); +#endif } +#if PLATFORM_WINDOWS + else + { + procSettings.FileName += TEXT(".bat"); + } +#endif #endif const int32 result = Platform::CreateProcess(procSettings); if (result != 0) From 36157fb01e35f810b472ea88cf5f18dd6c78ce59 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 18 Apr 2026 23:22:12 +0200 Subject: [PATCH 27/51] Fix `Behavior` time to not reset on root loop --- Source/Engine/AI/Behavior.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/Source/Engine/AI/Behavior.cpp b/Source/Engine/AI/Behavior.cpp index 5eee07dbf..55d96e415 100644 --- a/Source/Engine/AI/Behavior.cpp +++ b/Source/Engine/AI/Behavior.cpp @@ -113,8 +113,6 @@ void Behavior::UpdateAsync() { // Reset State _result = BehaviorUpdateResult::Running; - _accumulatedTime = 0.0f; - _totalTime = 0; } else if (_result != BehaviorUpdateResult::Running) { From 641f70d4acb8986c794b7310958ca72a07ebc04b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 18 Apr 2026 23:44:21 +0200 Subject: [PATCH 28/51] Fix Blend Mode on Terrain materials to be unenviable due to not implemented #4056 --- Source/Editor/Windows/Assets/MaterialWindow.cs | 11 ++++++++++- .../Graphics/Materials/MaterialShaderFeatures.h | 1 - .../Graphics/Materials/TerrainMaterialShader.cpp | 16 ---------------- 3 files changed, 10 insertions(+), 18 deletions(-) diff --git a/Source/Editor/Windows/Assets/MaterialWindow.cs b/Source/Editor/Windows/Assets/MaterialWindow.cs index 934cd6594..b051542a1 100644 --- a/Source/Editor/Windows/Assets/MaterialWindow.cs +++ b/Source/Editor/Windows/Assets/MaterialWindow.cs @@ -57,7 +57,7 @@ namespace FlaxEditor.Windows.Assets [EditorOrder(20), VisibleIf(nameof(IsStandard)), EditorDisplay("General"), Tooltip("Defines how material inputs and properties are combined to result the final surface color.")] public MaterialShadingModel ShadingModel; - [EditorOrder(30), VisibleIf(nameof(IsStandard)), EditorDisplay("General"), Tooltip("Determinates how materials' color should be blended with the background colors.")] + [EditorOrder(30), VisibleIf(nameof(ShowBlendMode)), EditorDisplay("General"), Tooltip("Determinates how materials' color should be blended with the background colors.")] public MaterialBlendMode BlendMode; // Rendering @@ -145,6 +145,7 @@ namespace FlaxEditor.Windows.Assets // Visibility conditionals + private bool ShowBlendMode => Domain != MaterialDomain.Terrain; private bool IsPostProcess => Domain == MaterialDomain.PostProcess; private bool IsDecal => Domain == MaterialDomain.Decal; private bool IsGUI => Domain == MaterialDomain.GUI; @@ -187,6 +188,14 @@ namespace FlaxEditor.Windows.Assets // Link Window = window; + + // [Deprecated in 1.12] + // Fix old terrain materials to go back into opaque + if (Domain == MaterialDomain.Terrain && BlendMode != MaterialBlendMode.Opaque) + { + BlendMode = MaterialBlendMode.Opaque; + FlaxEngine.Scripting.InvokeOnUpdate(Window.MarkAsEdited); + } } /// diff --git a/Source/Engine/Graphics/Materials/MaterialShaderFeatures.h b/Source/Engine/Graphics/Materials/MaterialShaderFeatures.h index f03266f19..6cfb28e88 100644 --- a/Source/Engine/Graphics/Materials/MaterialShaderFeatures.h +++ b/Source/Engine/Graphics/Materials/MaterialShaderFeatures.h @@ -3,7 +3,6 @@ #pragma once #include "MaterialShader.h" -#include "Engine/Core/Math/Rectangle.h" #include "Engine/Core/Types/Span.h" #include "Engine/Renderer/GI/DynamicDiffuseGlobalIllumination.h" #include "Engine/Renderer/GlobalSignDistanceFieldPass.h" diff --git a/Source/Engine/Graphics/Materials/TerrainMaterialShader.cpp b/Source/Engine/Graphics/Materials/TerrainMaterialShader.cpp index bb0796234..4db4acda1 100644 --- a/Source/Engine/Graphics/Materials/TerrainMaterialShader.cpp +++ b/Source/Engine/Graphics/Materials/TerrainMaterialShader.cpp @@ -150,21 +150,6 @@ bool TerrainMaterialShader::Load() } #endif - // Support blending but then use only emissive channel - switch (_info.BlendMode) - { - case MaterialBlendMode::Transparent: - psDesc.BlendMode = BlendingMode::AlphaBlend; - break; - case MaterialBlendMode::Additive: - psDesc.BlendMode = BlendingMode::Additive; - break; - case MaterialBlendMode::Multiply: - psDesc.BlendMode = BlendingMode::Multiply; - break; - default: ; - } - // GBuffer Pass psDesc.VS = _shader->GetVS("VS"); psDesc.PS = _shader->GetPS("PS_GBuffer"); @@ -185,7 +170,6 @@ bool TerrainMaterialShader::Load() // Depth Pass psDesc.CullMode = CullMode::TwoSided; - psDesc.BlendMode = BlendingMode::Opaque; psDesc.DepthClipEnable = false; psDesc.DepthWriteEnable = true; psDesc.DepthEnable = true; From 0ef35ce2571fd477efb13130d349262ca2c05219 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 18 Apr 2026 23:44:43 +0200 Subject: [PATCH 29/51] Fix dock window title to properly update on decoration UI --- Source/Editor/GUI/Docking/DockWindow.cs | 2 +- Source/Editor/GUI/Docking/FloatWindowDockPanel.cs | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Source/Editor/GUI/Docking/DockWindow.cs b/Source/Editor/GUI/Docking/DockWindow.cs index 96e01f8b1..8cde79ce8 100644 --- a/Source/Editor/GUI/Docking/DockWindow.cs +++ b/Source/Editor/GUI/Docking/DockWindow.cs @@ -89,7 +89,7 @@ namespace FlaxEditor.GUI.Docking // Check if is docked to the floating window and is selected so update window title if (IsSelected && _dockedTo is FloatWindowDockPanel floatPanel) { - floatPanel.Window.Title = Title; + floatPanel.UpdateTitle(_title); } } } diff --git a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs index f0504db3d..b47d76bdd 100644 --- a/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs +++ b/Source/Editor/GUI/Docking/FloatWindowDockPanel.cs @@ -200,6 +200,14 @@ namespace FlaxEditor.GUI.Docking Dispose(); } + internal void UpdateTitle(string title) + { + _window.Title = title; + var decorations = Parent.GetChild(); + if (decorations != null) + decorations.PerformLayout(); + } + /// public override bool IsFloating => true; @@ -227,10 +235,7 @@ namespace FlaxEditor.GUI.Docking if (_window != null && SelectedTab != null) { - _window.Title = SelectedTab.Title; - var decorations = Parent.GetChild(); - if (decorations != null) - decorations.PerformLayout(); + UpdateTitle(SelectedTab.Title); } } From 28ea7f134d6780ce4cd8cade69898de39b61fef2 Mon Sep 17 00:00:00 2001 From: fibref Date: Sat, 18 Apr 2026 22:37:19 +0800 Subject: [PATCH 30/51] fix fallback font broken when using MSDF --- Source/Engine/Render2D/Font.cpp | 4 ++++ Source/Engine/Render2D/FontAsset.cpp | 16 ++++++++++++++++ Source/Engine/Render2D/FontAsset.h | 7 +++++++ 3 files changed, 27 insertions(+) diff --git a/Source/Engine/Render2D/Font.cpp b/Source/Engine/Render2D/Font.cpp index 63c59ff70..b2e6e8b75 100644 --- a/Source/Engine/Render2D/Font.cpp +++ b/Source/Engine/Render2D/Font.cpp @@ -52,6 +52,10 @@ void Font::GetCharacter(Char c, FontCharacterEntry& result, bool enableFallback) for (int32 fallbackIndex = 0; fallbackIndex < FallbackFonts.Count(); fallbackIndex++) { FontAsset* fallbackFont = FallbackFonts.Get()[fallbackIndex].Get(); + if (fallbackFont && _asset->GetOptions().RasterMode == FontRasterMode::MSDF) + { + fallbackFont = fallbackFont->GetMSDF(); + } if (fallbackFont && fallbackFont->ContainsChar(c)) { fallbackFont->CreateFont(GetSize())->GetCharacter(c, result, enableFallback); diff --git a/Source/Engine/Render2D/FontAsset.cpp b/Source/Engine/Render2D/FontAsset.cpp index 813eb0c66..b8edf0448 100644 --- a/Source/Engine/Render2D/FontAsset.cpp +++ b/Source/Engine/Render2D/FontAsset.cpp @@ -155,6 +155,22 @@ FontAsset* FontAsset::GetItalic() return _virtualItalic; } +FontAsset* FontAsset::GetMSDF() +{ + ScopeLock lock(Locker); + if (_options.RasterMode == FontRasterMode::MSDF) + return this; + if (!_virtualMSDF) + { + _virtualMSDF = Content::CreateVirtualAsset(); + _virtualMSDF->Init(_fontFile); + auto options = _options; + options.RasterMode = FontRasterMode::MSDF; + _virtualMSDF->SetOptions(options); + } + return _virtualMSDF; +} + bool FontAsset::Init(const BytesContainer& fontFile) { ScopeLock lock(Locker); diff --git a/Source/Engine/Render2D/FontAsset.h b/Source/Engine/Render2D/FontAsset.h index 56cc3d655..44ffbbdd1 100644 --- a/Source/Engine/Render2D/FontAsset.h +++ b/Source/Engine/Render2D/FontAsset.h @@ -122,6 +122,7 @@ private: Array> _fonts; AssetReference _virtualBold; AssetReference _virtualItalic; + AssetReference _virtualMSDF; public: /// @@ -180,6 +181,12 @@ public: /// The virtual font or this. API_FUNCTION() FontAsset* GetItalic(); + /// + /// Gets the MSDF version of the font. Returns itself or creates a new virtual font asset using this font but rasterized with MSDF. + /// + /// The virtual font or this. + API_FUNCTION() FontAsset* GetMSDF(); + /// /// Initializes the font with a custom font file data. /// From d2cdb85678f7d99f76a3a3b988958ffae7a05e79 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sat, 18 Apr 2026 23:51:56 +0200 Subject: [PATCH 31/51] Fix reference on font asset unload 28ea7f134d6780ce4cd8cade69898de39b61fef2 #4059 --- Source/Engine/Render2D/FontAsset.cpp | 1 + Source/Engine/Render2D/FontAsset.h | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Render2D/FontAsset.cpp b/Source/Engine/Render2D/FontAsset.cpp index b8edf0448..34ffe7330 100644 --- a/Source/Engine/Render2D/FontAsset.cpp +++ b/Source/Engine/Render2D/FontAsset.cpp @@ -69,6 +69,7 @@ void FontAsset::unload(bool isReloading) _fontFile.Release(); _virtualBold = nullptr; _virtualItalic = nullptr; + _virtualMSDF = nullptr; } AssetChunksFlag FontAsset::getChunksToPreload() const diff --git a/Source/Engine/Render2D/FontAsset.h b/Source/Engine/Render2D/FontAsset.h index 44ffbbdd1..c3aa141a8 100644 --- a/Source/Engine/Render2D/FontAsset.h +++ b/Source/Engine/Render2D/FontAsset.h @@ -182,7 +182,7 @@ public: API_FUNCTION() FontAsset* GetItalic(); /// - /// Gets the MSDF version of the font. Returns itself or creates a new virtual font asset using this font but rasterized with MSDF. + /// Gets the MSDF version of the font. Returns itself or creates a new virtual font asset using this font but rasterized with Multi-channel Signed Distance Field (MSDF). /// /// The virtual font or this. API_FUNCTION() FontAsset* GetMSDF(); From 54e5e6895df80a44aeee9b281dabd1ac7faab042 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 19 Apr 2026 10:22:35 +0200 Subject: [PATCH 32/51] Fix material instance parameters order in editor to match from base material --- Source/Editor/Surface/SurfaceParameter.cs | 6 +++++ Source/Editor/Surface/SurfaceUtils.cs | 25 +++++++++++++------ .../Engine/Graphics/Materials/MaterialInfo.cs | 9 +++++++ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/Source/Editor/Surface/SurfaceParameter.cs b/Source/Editor/Surface/SurfaceParameter.cs index c76cb058e..be69f69b2 100644 --- a/Source/Editor/Surface/SurfaceParameter.cs +++ b/Source/Editor/Surface/SurfaceParameter.cs @@ -48,5 +48,11 @@ namespace FlaxEditor.Surface /// [NoSerialize, HideInEditor] public readonly SurfaceMeta Meta = new SurfaceMeta(); + + /// + public override string ToString() + { + return $"{Type} {Name} = {Value?.ToString() ?? "null"}"; + } } } diff --git a/Source/Editor/Surface/SurfaceUtils.cs b/Source/Editor/Surface/SurfaceUtils.cs index 60cd714c4..638d8338c 100644 --- a/Source/Editor/Surface/SurfaceUtils.cs +++ b/Source/Editor/Surface/SurfaceUtils.cs @@ -21,6 +21,7 @@ namespace FlaxEditor.Surface { public GraphParameter Parameter; public bool IsPublic; + public int Index; public Type Type; public object Tag; public Attribute[] Attributes; @@ -31,15 +32,16 @@ namespace FlaxEditor.Surface public HeaderAttribute Header; public string DisplayName; - public GraphParameterData(GraphParameter parameter, object tag = null) - : this(parameter, null, parameter.IsPublic, parameter.Type, SurfaceMeta.GetAttributes(parameter), tag) + public GraphParameterData(GraphParameter parameter, int index, object tag = null) + : this(parameter, index, null, parameter.IsPublic, parameter.Type, SurfaceMeta.GetAttributes(parameter), tag) { } - public GraphParameterData(GraphParameter parameter, string name, bool isPublic, Type type, Attribute[] attributes, object tag) + public GraphParameterData(GraphParameter parameter, int index, string name, bool isPublic, Type type, Attribute[] attributes, object tag) { Parameter = parameter; IsPublic = isPublic; + Index = index; Type = type; Tag = tag; Attributes = attributes; @@ -74,8 +76,14 @@ namespace FlaxEditor.Surface if (Editor.Instance.Options.Options.General.ScriptMembersOrder == GeneralOptions.MembersOrder.Alphabetical) return string.Compare(x.DisplayName, y.DisplayName, StringComparison.InvariantCulture); - // Keep same order - return 0; + // Keep same order (from input indices) + return x.Index - y.Index; + } + + /// + public override string ToString() + { + return $"{Type} {DisplayName}"; } } @@ -206,6 +214,7 @@ namespace FlaxEditor.Surface Profiler.EndEvent(); } + int index = 0; foreach (var parameter in parameters) { var parameterId = parameter.ParameterID; @@ -229,7 +238,7 @@ namespace FlaxEditor.Surface surfaceParameters.Add(surfaceParameter); } var attributes = surfaceParameter?.Meta.GetAttributes() ?? FlaxEngine.Utils.GetEmptyArray(); - data[i] = new GraphParameterData(null, parameter.Name, parameter.IsPublic, ToType(parameter.ParameterType), attributes, parameter); + data[i] = new GraphParameterData(null, index++, parameter.Name, parameter.IsPublic, ToType(parameter.ParameterType), attributes, parameter); i++; } Array.Sort(data, GraphParameterData.Compare); @@ -243,7 +252,7 @@ namespace FlaxEditor.Surface int i = 0; foreach (var parameter in parameters) { - data[i] = new GraphParameterData(parameter.EmitterParameter, parameter); + data[i] = new GraphParameterData(parameter.EmitterParameter, i, parameter); i++; } Array.Sort(data, GraphParameterData.Compare); @@ -257,7 +266,7 @@ namespace FlaxEditor.Surface int i = 0; foreach (var parameter in parameters) { - data[i] = new GraphParameterData(parameter); + data[i] = new GraphParameterData(parameter, i); i++; } Array.Sort(data, GraphParameterData.Compare); diff --git a/Source/Engine/Graphics/Materials/MaterialInfo.cs b/Source/Engine/Graphics/Materials/MaterialInfo.cs index a8ff82665..4ff56ea63 100644 --- a/Source/Engine/Graphics/Materials/MaterialInfo.cs +++ b/Source/Engine/Graphics/Materials/MaterialInfo.cs @@ -97,4 +97,13 @@ namespace FlaxEngine } } } + + partial class MaterialParameter + { + /// + public override string ToString() + { + return $"{ParameterType} {Name} = {Value?.ToString() ?? "null"}"; + } + } } From b79335c92024ddbcaada67d10114b344701d157b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 19 Apr 2026 21:30:16 +0200 Subject: [PATCH 33/51] Fix compiling C# scripts that use `nuget` package to properly resolve it on 1st build --- Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs | 8 ++++++++ Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs b/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs index 164cd412f..4afb57bd1 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs @@ -284,9 +284,17 @@ namespace Flax.Build if (buildData.TargetOptions.NugetPackageReferences.Any()) { var nugetPath = Utilities.GetNugetPackagesPath(); + var restoreOnce = true; foreach (var reference in buildOptions.NugetPackageReferences) { var path = reference.GetLibPath(nugetPath); + if (!File.Exists(path) && restoreOnce) + { + // Package binaries folder is missing so restore packages (incl. dependency packages) + RestoreNugetPackages(graph, buildOptions.Target, buildOptions); + restoreOnce = false; + path = reference.GetLibPath(nugetPath); + } args.Add(string.Format("/reference:\"{0}\"", path)); } } diff --git a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs index 8251ff186..9cca0dfa6 100644 --- a/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs +++ b/Source/Tools/Flax.Build/Build/NativeCpp/BuildOptions.cs @@ -139,7 +139,9 @@ namespace Flax.Build.NativeCpp { if (libFolder == null) libFolder = GetLibFolder(nugetPath); - var dlls = Directory.GetFiles(libFolder, "*.dll", SearchOption.TopDirectoryOnly); + if (libFolder == string.Empty) + return string.Empty; + var dlls = Directory.Exists(libFolder) ? Directory.GetFiles(libFolder, "*.dll", SearchOption.TopDirectoryOnly) : []; if (dlls.Length == 0) { Log.Error($"Missing NuGet package \"{Name}, {Version}, {Framework}\" binaries (folder: {libFolder})"); From 85537853970fdb0501baabbef77521e2942e020b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 19 Apr 2026 21:48:23 +0200 Subject: [PATCH 34/51] Fix crash when drawing minor foliage node with both children and instances --- Source/Engine/Foliage/Foliage.cpp | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Source/Engine/Foliage/Foliage.cpp b/Source/Engine/Foliage/Foliage.cpp index 1fe9e39a9..7428a5a8b 100644 --- a/Source/Engine/Foliage/Foliage.cpp +++ b/Source/Engine/Foliage/Foliage.cpp @@ -168,9 +168,6 @@ void Foliage::DrawCluster(DrawContext& context, FoliageCluster* cluster, DrawCal // Draw visible children if (cluster->Children[0]) { - // Don't store instances in non-leaf nodes - ASSERT_LOW_LAYER(cluster->Instances.IsEmpty()); - BoundingBox box; #define DRAW_CLUSTER(idx) \ box = cluster->Children[idx]->TotalBounds; \ @@ -184,7 +181,7 @@ void Foliage::DrawCluster(DrawContext& context, FoliageCluster* cluster, DrawCal DRAW_CLUSTER(3); #undef DRAW_CLUSTER } - else + //else // Minor clusters can be subdivided and contain instances { // Draw visible instances const auto frame = Engine::FrameCount; @@ -311,9 +308,6 @@ void Foliage::DrawCluster(DrawContext& context, FoliageCluster* cluster, Mesh::D // Draw visible children if (cluster->Children[0]) { - // Don't store instances in non-leaf nodes - ASSERT_LOW_LAYER(cluster->Instances.IsEmpty()); - BoundingBox box; #define DRAW_CLUSTER(idx) \ box = cluster->Children[idx]->TotalBounds; \ @@ -327,7 +321,7 @@ void Foliage::DrawCluster(DrawContext& context, FoliageCluster* cluster, Mesh::D DRAW_CLUSTER(3); #undef DRAW_CLUSTER } - else + //else // Minor clusters can be subdivided and contain instances { // Draw visible instances const auto frame = Engine::FrameCount; @@ -390,7 +384,7 @@ void Foliage::DrawClusterGlobalSDF(class GlobalSignDistanceFieldPass* globalSDF, DRAW_CLUSTER(3); #undef DRAW_CLUSTER } - else + //else // Minor clusters can be subdivided and contain instances { // Draw visible instances for (int32 i = 0; i < cluster->Instances.Count(); i++) @@ -421,7 +415,7 @@ void Foliage::DrawClusterGlobalSA(GlobalSurfaceAtlasPass* globalSA, const Vector DRAW_CLUSTER(3); #undef DRAW_CLUSTER } - else + //else // Minor clusters can be subdivided and contain instances { // Draw visible instances for (int32 i = 0; i < cluster->Instances.Count(); i++) From 635ef0ad663f56ea42dd2095725a48633ac68ae0 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 19 Apr 2026 22:37:58 +0200 Subject: [PATCH 35/51] Fix drawing editor thumbnails when texture used by asset is using dynamic streaming --- .../Editor/Content/Proxy/CubeTextureProxy.cs | 2 - .../Editor/Content/Proxy/IESProfileProxy.cs | 6 +-- .../Content/Proxy/MaterialInstanceProxy.cs | 2 - Source/Editor/Content/Proxy/MaterialProxy.cs | 2 - Source/Editor/Content/Proxy/ModelProxy.cs | 2 - Source/Editor/Content/Proxy/PrefabProxy.cs | 3 +- .../Editor/Content/Proxy/SkinnedModelProxy.cs | 2 - .../Editor/Content/Proxy/SpriteAtlasProxy.cs | 6 +-- Source/Editor/Content/Proxy/TextureProxy.cs | 2 - .../Content/Thumbnails/ThumbnailsModule.cs | 40 ++++++++++--------- .../Engine/Graphics/Textures/TextureBase.cpp | 6 +++ Source/Engine/Graphics/Textures/TextureBase.h | 5 +++ 12 files changed, 36 insertions(+), 42 deletions(-) diff --git a/Source/Editor/Content/Proxy/CubeTextureProxy.cs b/Source/Editor/Content/Proxy/CubeTextureProxy.cs index 137376673..efccfdbe5 100644 --- a/Source/Editor/Content/Proxy/CubeTextureProxy.cs +++ b/Source/Editor/Content/Proxy/CubeTextureProxy.cs @@ -47,8 +47,6 @@ namespace FlaxEditor.Content _preview = new CubeTexturePreview(false); InitAssetPreview(_preview); } - - // TODO: disable streaming for asset during thumbnail rendering (and restore it after) } /// diff --git a/Source/Editor/Content/Proxy/IESProfileProxy.cs b/Source/Editor/Content/Proxy/IESProfileProxy.cs index 4a9320a9f..6b6918567 100644 --- a/Source/Editor/Content/Proxy/IESProfileProxy.cs +++ b/Source/Editor/Content/Proxy/IESProfileProxy.cs @@ -50,16 +50,12 @@ namespace FlaxEditor.Content Offsets = Margin.Zero, }; } - - // TODO: disable streaming for asset during thumbnail rendering (and restore it after) } /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - // Check if asset is streamed enough - var asset = (IESProfile)request.Asset; - return asset.ResidentMipLevels >= Mathf.Max(1, (int)(asset.MipLevels * ThumbnailsModule.MinimumRequiredResourcesQuality)); + return ThumbnailsModule.HasMinimumQuality((IESProfile)request.Asset); } /// diff --git a/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs b/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs index 672b02701..fc4fcdbc1 100644 --- a/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs +++ b/Source/Editor/Content/Proxy/MaterialInstanceProxy.cs @@ -55,8 +55,6 @@ namespace FlaxEditor.Content _preview = new MaterialPreview(false); InitAssetPreview(_preview); } - - // TODO: disable streaming for dependant assets during thumbnail rendering (and restore it after) } /// diff --git a/Source/Editor/Content/Proxy/MaterialProxy.cs b/Source/Editor/Content/Proxy/MaterialProxy.cs index db0f9c810..4769ca548 100644 --- a/Source/Editor/Content/Proxy/MaterialProxy.cs +++ b/Source/Editor/Content/Proxy/MaterialProxy.cs @@ -99,8 +99,6 @@ namespace FlaxEditor.Content _preview = new MaterialPreview(false); InitAssetPreview(_preview); } - - // TODO: disable streaming for dependant assets during thumbnail rendering (and restore it after) } /// diff --git a/Source/Editor/Content/Proxy/ModelProxy.cs b/Source/Editor/Content/Proxy/ModelProxy.cs index 6dd9ea193..8b20073d8 100644 --- a/Source/Editor/Content/Proxy/ModelProxy.cs +++ b/Source/Editor/Content/Proxy/ModelProxy.cs @@ -78,8 +78,6 @@ namespace FlaxEditor.Content }; InitAssetPreview(_preview); } - - // TODO: disable streaming for asset during thumbnail rendering (and restore it after) } /// diff --git a/Source/Editor/Content/Proxy/PrefabProxy.cs b/Source/Editor/Content/Proxy/PrefabProxy.cs index 191193c66..1c2e6f79f 100644 --- a/Source/Editor/Content/Proxy/PrefabProxy.cs +++ b/Source/Editor/Content/Proxy/PrefabProxy.cs @@ -113,8 +113,6 @@ namespace FlaxEditor.Content _preview = new PrefabPreview(false); InitAssetPreview(_preview); } - - // TODO: disable streaming for asset during thumbnail rendering (and restore it after) } /// @@ -125,6 +123,7 @@ namespace FlaxEditor.Content // Check if asset is streamed enough var asset = (Prefab)request.Asset; + // TODO: check all prefab actors to have loaded resources (models/textures/etc.) return asset.IsLoaded; } diff --git a/Source/Editor/Content/Proxy/SkinnedModelProxy.cs b/Source/Editor/Content/Proxy/SkinnedModelProxy.cs index f9d043a21..95f2f7510 100644 --- a/Source/Editor/Content/Proxy/SkinnedModelProxy.cs +++ b/Source/Editor/Content/Proxy/SkinnedModelProxy.cs @@ -101,8 +101,6 @@ namespace FlaxEditor.Content _preview = new AnimatedModelPreview(false); InitAssetPreview(_preview); } - - // TODO: disable streaming for asset during thumbnail rendering (and restore it after) } /// diff --git a/Source/Editor/Content/Proxy/SpriteAtlasProxy.cs b/Source/Editor/Content/Proxy/SpriteAtlasProxy.cs index 2217be358..03a3ef147 100644 --- a/Source/Editor/Content/Proxy/SpriteAtlasProxy.cs +++ b/Source/Editor/Content/Proxy/SpriteAtlasProxy.cs @@ -50,16 +50,12 @@ namespace FlaxEditor.Content Offsets = Margin.Zero, }; } - - // TODO: disable streaming for asset during thumbnail rendering (and restore it after) } /// public override bool CanDrawThumbnail(ThumbnailRequest request) { - // Check if asset is streamed enough - var asset = (SpriteAtlas)request.Asset; - return asset.ResidentMipLevels >= Mathf.Max(1, (int)(asset.MipLevels * ThumbnailsModule.MinimumRequiredResourcesQuality)); + return ThumbnailsModule.HasMinimumQuality((SpriteAtlas)request.Asset); } /// diff --git a/Source/Editor/Content/Proxy/TextureProxy.cs b/Source/Editor/Content/Proxy/TextureProxy.cs index 361f4c3f6..45deb0b39 100644 --- a/Source/Editor/Content/Proxy/TextureProxy.cs +++ b/Source/Editor/Content/Proxy/TextureProxy.cs @@ -50,8 +50,6 @@ namespace FlaxEditor.Content Offsets = Margin.Zero, }; } - - // TODO: disable streaming for asset during thumbnail rendering (and restore it after) } /// diff --git a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs index 2fca96ba2..75ab79c4d 100644 --- a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs +++ b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs @@ -122,39 +122,43 @@ namespace FlaxEditor.Content.Thumbnails internal static bool HasMinimumQuality(TextureBase asset) { + // Don't block thumbnails queue when texture fails to stream in (eg. unsupported format) if (asset.HasStreamingError) - return true; // Don't block thumbnails queue when texture fails to stream in (eg. unsupported format) + return true; + + // Check if enough mip levels are loaded var mipLevels = asset.MipLevels; var minMipLevels = Mathf.Min(mipLevels, 7); - return asset.IsLoaded && asset.ResidentMipLevels >= Mathf.Max(minMipLevels, (int)(mipLevels * MinimumRequiredResourcesQuality)); + if (asset.IsLoaded && asset.ResidentMipLevels >= Mathf.Max(minMipLevels, (int)(mipLevels * MinimumRequiredResourcesQuality))) + return true; + + // Inform streaming about resource usage to stream it in for the thumbnail + asset.SetStreamingVisible(); + + return false; } - internal static bool HasMinimumQuality(Model asset) + internal static bool HasMinimumQuality(ModelBase asset) { if (!asset.IsLoaded) return false; - var lods = asset.LODs.Length; + var lods = asset.LODsCount; var slots = asset.MaterialSlots; - foreach (var slot in slots) - { - if (slot.Material && !HasMinimumQuality(slot.Material)) - return false; - } - return asset.LoadedLODs >= Mathf.Max(1, (int)(lods * MinimumRequiredResourcesQuality)); - } - internal static bool HasMinimumQuality(SkinnedModel asset) - { - var lods = asset.LODs.Length; - if (asset.IsLoaded && lods == 0) - return true; // Skeleton-only model - var slots = asset.MaterialSlots; + // Check if all materials are loaded (incl. dependent resources) foreach (var slot in slots) { if (slot.Material && !HasMinimumQuality(slot.Material)) return false; } - return asset.LoadedLODs >= Mathf.Max(1, (int)(lods * MinimumRequiredResourcesQuality)); + + // Check if enough LODs are loaded + if (asset.LoadedLODs >= Mathf.Max(1, (int)(lods * MinimumRequiredResourcesQuality))) + return true; + + // TODO: impl SetStreamingVisible for models similar to textures (ModelsStreamingHandler needs to use it) + + return false; } internal static bool HasMinimumQuality(MaterialBase asset) diff --git a/Source/Engine/Graphics/Textures/TextureBase.cpp b/Source/Engine/Graphics/Textures/TextureBase.cpp index b5f020611..ddd0498bc 100644 --- a/Source/Engine/Graphics/Textures/TextureBase.cpp +++ b/Source/Engine/Graphics/Textures/TextureBase.cpp @@ -302,6 +302,12 @@ bool TextureBase::HasStreamingError() const return _texture.Streaming.Error; } +void TextureBase::SetStreamingVisible() const +{ + if (_texture.GetTexture()) + _texture.GetTexture()->LastRenderTime = Platform::GetTimeSeconds(); +} + BytesContainer TextureBase::GetMipData(int32 mipIndex, int32& rowPitch, int32& slicePitch) { BytesContainer result; diff --git a/Source/Engine/Graphics/Textures/TextureBase.h b/Source/Engine/Graphics/Textures/TextureBase.h index b8ae267c8..a461a1018 100644 --- a/Source/Engine/Graphics/Textures/TextureBase.h +++ b/Source/Engine/Graphics/Textures/TextureBase.h @@ -153,6 +153,11 @@ public: /// API_PROPERTY() bool HasStreamingError() const; + /// + /// Sets the texture as visible this frame to inform streaming about usage which will stream boost its priority for the streaming. + /// + API_FUNCTION() void SetStreamingVisible() const; + public: /// /// Gets the mip data. From c474f2e5228f0cb91eddf006aa457eb04645d31b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 20 Apr 2026 09:46:43 +0200 Subject: [PATCH 36/51] Add `GPUDevice.DumpResources` command and fix output to be sorted by size and cleaner to read --- Source/Engine/Graphics/GPUDevice.cpp | 94 +++++++++++++++++++++++----- Source/Engine/Graphics/GPUDevice.h | 5 ++ 2 files changed, 84 insertions(+), 15 deletions(-) diff --git a/Source/Engine/Graphics/GPUDevice.cpp b/Source/Engine/Graphics/GPUDevice.cpp index 6aaee8d01..741d0474f 100644 --- a/Source/Engine/Graphics/GPUDevice.cpp +++ b/Source/Engine/Graphics/GPUDevice.cpp @@ -19,6 +19,7 @@ #include "Engine/Content/Assets/Material.h" #include "Engine/Content/Content.h" #include "Engine/Content/SoftAssetReference.h" +#include "Engine/Core/Collections/Sorting.h" #include "Engine/Render2D/Render2D.h" #include "Engine/Engine/CommandLine.h" #include "Engine/Engine/Engine.h" @@ -511,7 +512,7 @@ void GPUDevice::DumpResourcesToLog() const true, // CubeTexture true, // VolumeTexture true, // Buffer - true, // Shader + false, // Shader false, // PipelineState false, // Descriptor false, // Query @@ -522,29 +523,86 @@ void GPUDevice::DumpResourcesToLog() const const auto type = static_cast(typeIndex); const auto printType = printTypes[typeIndex]; - output.AppendFormat(TEXT("Group: {0}s"), ScriptingEnum::ToString(type)); - output.AppendLine(); - - int32 count = 0; + // Get resource sof a given type uint64 memUsage = 0; + struct Resource + { + const GPUResource* Object; + uint64 MemoryUsage; + + bool operator<(const Resource& other) const + { + if (MemoryUsage != other.MemoryUsage) + return MemoryUsage > other.MemoryUsage; + return Object->GetName().Compare(other.Object->GetName()) > 0; + } + }; + Array resources; for (int32 i = 0; i < _resources.Count(); i++) { const GPUResource* resource = _resources[i]; if (resource->GetResourceType() == type && resource->GetMemoryUsage() != 0) { - count++; - memUsage += resource->GetMemoryUsage(); - auto str = resource->ToString(); - if (str.HasChars() && printType) - { - output.Append(TEXT('\t')); - output.Append(str); - output.AppendLine(); - } + resources.Add({ resource, resource->GetMemoryUsage() }); } } + if (resources.IsEmpty()) + continue; + output.AppendFormat(TEXT("> {0}:"), ScriptingEnum::ToString(type)); + output.AppendLine(); - output.AppendFormat(TEXT("Total count: {0}, memory usage: {1}"), count, Utilities::BytesToText(memUsage)); + // Sort them by size + Sorting::QuickSort(resources); + + // Print resources + for (auto e : resources) + { + memUsage += e.MemoryUsage; + if (!printType) + continue; + output.Append(TEXT(" ")); + output.Append(Utilities::BytesToText(e.MemoryUsage)); + output.Append(TEXT(", ")); + if (e.Object->Is()) + { + auto texture = (GPUTexture*)e.Object; + auto& desc = texture->GetDescription(); + output.AppendFormat(TEXT("Size: {}x{}x{}[{}], "), desc.Width, desc.Height, desc.Depth, desc.ArraySize); + if (texture->ResidentMipLevels() == desc.MipLevels) + output.AppendFormat(TEXT("Mips: {}, "), desc.MipLevels); + else + output.AppendFormat(TEXT("Mips: {}/{}, "), texture->ResidentMipLevels(), desc.MipLevels); +#if GPU_ENABLE_RESOURCE_NAMING + auto name = texture->GetName(); +#else + StringView name; +#endif + output.AppendFormat(TEXT("Format: {}, Flags: {}, {}"), ScriptingEnum::ToString(desc.Format), ScriptingEnum::ToStringFlags(desc.Flags), name); + } + else if (e.Object->Is()) + { + auto buffer = (GPUBuffer*)e.Object; + auto& desc = buffer->GetDescription(); + output.AppendFormat(TEXT("Stride: {} bytes, "), desc.Stride); + if (desc.Format != PixelFormat::Unknown) + output.AppendFormat(TEXT("Format: {}, "), ScriptingEnum::ToString(desc.Format)); + if (desc.Usage != GPUResourceUsage::Default) + output.AppendFormat(TEXT("Usage: {}, "), ScriptingEnum::ToString(desc.Usage)); +#if GPU_ENABLE_RESOURCE_NAMING + auto name = buffer->GetName(); +#else + StringView name; +#endif + output.AppendFormat(TEXT("Flags: {}, {}"), ScriptingEnum::ToStringFlags(desc.Flags), name); + } + else + { + output.Append(e.Object->ToString()); + } + output.AppendLine(); + } + + output.AppendFormat(TEXT("Total count: {0}, memory usage: {1}"), resources.Count(), Utilities::BytesToText(memUsage)); output.AppendLine(); output.AppendLine(); } @@ -553,6 +611,12 @@ void GPUDevice::DumpResourcesToLog() const LOG_STR(Info, output.ToStringView()); } +void GPUDevice::DumpResources() +{ + if (GPUDevice::Instance) + GPUDevice::Instance->DumpResourcesToLog(); +} + extern void ClearVertexLayoutCache(); void GPUDevice::preDispose() diff --git a/Source/Engine/Graphics/GPUDevice.h b/Source/Engine/Graphics/GPUDevice.h index b9f681b5c..e6df74886 100644 --- a/Source/Engine/Graphics/GPUDevice.h +++ b/Source/Engine/Graphics/GPUDevice.h @@ -389,6 +389,11 @@ public: /// void DumpResourcesToLog() const; + /// + /// Dumps all GPU resources information to the log. + /// + API_FUNCTION(Attributes="DebugCommand") static void DumpResources(); + protected: virtual void preDispose(); From f85836d0907ce6a326e038cc1aa245417ad4ff2b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 20 Apr 2026 09:47:13 +0200 Subject: [PATCH 37/51] Add logging timeout size on Vulkan fence wait fail --- Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp index 760662a93..112a64bf3 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.cpp @@ -2257,6 +2257,7 @@ bool FenceManagerVulkan::WaitForFence(FenceVulkan* fence, float timeoutSeconds) fence->IsSignaled = true; return false; } + LOG(Warning, "vkWaitForFences failed with timeout: {}s", timeoutSeconds); return true; } From 0ba611e57f3550c31eda8fabe5f55d0aacf8b53a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 20 Apr 2026 10:42:20 +0200 Subject: [PATCH 38/51] Fix compilation --- Source/Engine/Graphics/GPUDevice.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Source/Engine/Graphics/GPUDevice.cpp b/Source/Engine/Graphics/GPUDevice.cpp index 741d0474f..9a1935228 100644 --- a/Source/Engine/Graphics/GPUDevice.cpp +++ b/Source/Engine/Graphics/GPUDevice.cpp @@ -534,7 +534,11 @@ void GPUDevice::DumpResourcesToLog() const { if (MemoryUsage != other.MemoryUsage) return MemoryUsage > other.MemoryUsage; +#if GPU_ENABLE_RESOURCE_NAMING return Object->GetName().Compare(other.Object->GetName()) > 0; +#else + return (uintptr)Object < (uintptr)other.Object; +#endif } }; Array resources; From 7014f409650dac376b14987d0ac3c55cc006959b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 20 Apr 2026 12:28:47 +0200 Subject: [PATCH 39/51] Fix crash when asset loading task remains leftover ref for some unknown reason --- Source/Engine/Content/Asset.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Content/Asset.cpp b/Source/Engine/Content/Asset.cpp index 4d7c21b91..da6b7329a 100644 --- a/Source/Engine/Content/Asset.cpp +++ b/Source/Engine/Content/Asset.cpp @@ -585,7 +585,8 @@ void Asset::startLoading() { PROFILE_MEM(ContentAssets); ASSERT(!IsLoaded()); - ASSERT(Platform::AtomicRead(&_loadingTask) == 0); + auto task = (Task*)Platform::AtomicRead(&_loadingTask); + ASSERT(task == nullptr || task->IsFinished() || task->IsCanceled()); auto loadingTask = createLoadingTask(); ASSERT(loadingTask != nullptr); Platform::AtomicStore(&_loadingTask, (intptr)loadingTask); From e90dde491df6917a6cbd089111612b71f658c2d3 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 20 Apr 2026 12:48:52 +0200 Subject: [PATCH 40/51] Fix prefab preview to better match model bounds inside it --- Source/Editor/Content/Proxy/PrefabProxy.cs | 44 ++++++++++++++++++---- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/Source/Editor/Content/Proxy/PrefabProxy.cs b/Source/Editor/Content/Proxy/PrefabProxy.cs index 1c2e6f79f..c01fc9ebf 100644 --- a/Source/Editor/Content/Proxy/PrefabProxy.cs +++ b/Source/Editor/Content/Proxy/PrefabProxy.cs @@ -127,16 +127,34 @@ namespace FlaxEditor.Content return asset.IsLoaded; } - private void Prepare(Actor actor) + private void Prepare(Actor actor, ref BoundingBox bounds) { + if (!actor.IsActive) + return; if (actor is TextRender textRender) { textRender.UpdateLayout(); } + // Use bounds from visual-only elements + var actorBounds = BoundingBox.Empty; + if (actor is ModelInstanceActor || + actor is SpriteRender || + actor is TextRender) + { + actorBounds = actor.EditorBox; + } + if (actorBounds != BoundingBox.Empty) + { + if (bounds == BoundingBox.Empty) + bounds = actorBounds; + else + BoundingBox.Merge(ref actorBounds, ref bounds, out bounds); + } + for (int i = 0; i < actor.ChildrenCount; i++) { - Prepare(actor.GetChild(i)); + Prepare(actor.GetChild(i), ref bounds); } } @@ -164,14 +182,24 @@ namespace FlaxEditor.Content else { // Update some actors data (some actor types update bounds/data later but its required to be done before rendering) - Prepare(_preview.Instance); + var bounds = BoundingBox.Empty; + Prepare(_preview.Instance, ref bounds); + //bounds = _preview.Instance.EditorBoxChildren; // Auto fit actor to camera - float targetSize = 30.0f; - var bounds = _preview.Instance.EditorBoxChildren; - var maxSize = Math.Max(0.001f, (float)bounds.Size.MaxValue); - _preview.Instance.Scale = new Float3(targetSize / maxSize); - _preview.Instance.Position = Vector3.Zero; + if (bounds != BoundingBox.Empty) + { + float targetSize = 38.0f; + var maxSize = Math.Max(0.001f, (float)bounds.Size.MaxValue); + float scale = targetSize / maxSize; + _preview.Instance.Scale = new Float3(scale); + _preview.Instance.Position = -bounds.Center * scale; + } + else + { + _preview.Instance.Scale = Float3.One; + _preview.Instance.Position = Vector3.Zero; + } } _preview.Task.OnDraw(); From 633f2fa901aeca1c67c6f102937194a17568f7ee Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 20 Apr 2026 12:49:43 +0200 Subject: [PATCH 41/51] Fix prefab preview to wait for the assets streaming --- Source/Editor/Content/Proxy/PrefabProxy.cs | 4 +- .../Content/Thumbnails/ThumbnailsModule.cs | 45 +++++++++++++++++++ Source/Engine/Level/Actors/AnimatedModel.cpp | 5 +++ Source/Engine/Level/Actors/AnimatedModel.h | 1 + .../Engine/Level/Actors/ModelInstanceActor.h | 5 +++ Source/Engine/Level/Actors/SplineModel.cpp | 5 +++ Source/Engine/Level/Actors/SplineModel.h | 1 + Source/Engine/Level/Actors/StaticModel.cpp | 5 +++ Source/Engine/Level/Actors/StaticModel.h | 1 + 9 files changed, 69 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Content/Proxy/PrefabProxy.cs b/Source/Editor/Content/Proxy/PrefabProxy.cs index c01fc9ebf..0fb247605 100644 --- a/Source/Editor/Content/Proxy/PrefabProxy.cs +++ b/Source/Editor/Content/Proxy/PrefabProxy.cs @@ -122,9 +122,7 @@ namespace FlaxEditor.Content return false; // Check if asset is streamed enough - var asset = (Prefab)request.Asset; - // TODO: check all prefab actors to have loaded resources (models/textures/etc.) - return asset.IsLoaded; + return ThumbnailsModule.HasMinimumQuality((Prefab)request.Asset); } private void Prepare(Actor actor, ref BoundingBox bounds) diff --git a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs index 75ab79c4d..dbc970b05 100644 --- a/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs +++ b/Source/Editor/Content/Thumbnails/ThumbnailsModule.cs @@ -181,6 +181,14 @@ namespace FlaxEditor.Content.Thumbnails return baseMaterial == null || HasMinimumQualityInternal(baseMaterial); } + internal static bool HasMinimumQuality(Prefab asset) + { + if (!asset.IsLoaded) + return false; + var defaultInstance = asset.GetDefaultInstance(); + return defaultInstance == null || HasMinimumQualityInternal(defaultInstance); + } + private static bool HasMinimumQualityInternal(MaterialBase asset) { if (!asset.IsLoaded) @@ -194,6 +202,43 @@ namespace FlaxEditor.Content.Thumbnails return true; } + private static bool HasMinimumQualityInternal(Actor actor) + { + if (!actor.IsActive) + return true; + + if (actor is ModelInstanceActor modelInstance) + { + var model = modelInstance.GetModel(); + if (model && !HasMinimumQuality(model)) + return false; + var slots = modelInstance.MaterialSlots; + foreach (var slot in slots) + { + if (slot.Material && !HasMinimumQuality(slot.Material)) + return false; + } + } + if (actor is SpriteRender spriteRender) + { + if (spriteRender.Material && !HasMinimumQuality(spriteRender.Material)) + return false; + } + if (actor is TextRender textRender) + { + if (textRender.Material && !HasMinimumQuality(textRender.Material)) + return false; + } + + for (int i = 0; i < actor.ChildrenCount; i++) + { + if (!HasMinimumQualityInternal(actor.GetChild(i))) + return false; + } + + return true; + } + #region IContentItemOwner /// diff --git a/Source/Engine/Level/Actors/AnimatedModel.cpp b/Source/Engine/Level/Actors/AnimatedModel.cpp index 1f441b6b0..ce106dc83 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.cpp +++ b/Source/Engine/Level/Actors/AnimatedModel.cpp @@ -1332,6 +1332,11 @@ MaterialBase* AnimatedModel::GetMaterial(int32 entryIndex) return material; } +ModelBase* AnimatedModel::GetModel() +{ + return SkinnedModel.Get(); +} + bool AnimatedModel::IntersectsEntry(int32 entryIndex, const Ray& ray, Real& distance, Vector3& normal) { auto model = SkinnedModel.Get(); diff --git a/Source/Engine/Level/Actors/AnimatedModel.h b/Source/Engine/Level/Actors/AnimatedModel.h index f9182d115..5e549e4a8 100644 --- a/Source/Engine/Level/Actors/AnimatedModel.h +++ b/Source/Engine/Level/Actors/AnimatedModel.h @@ -476,6 +476,7 @@ public: void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; const Span GetMaterialSlots() const override; MaterialBase* GetMaterial(int32 entryIndex) override; + ModelBase* GetModel() override; 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; diff --git a/Source/Engine/Level/Actors/ModelInstanceActor.h b/Source/Engine/Level/Actors/ModelInstanceActor.h index d553d132a..05b6c7f91 100644 --- a/Source/Engine/Level/Actors/ModelInstanceActor.h +++ b/Source/Engine/Level/Actors/ModelInstanceActor.h @@ -66,6 +66,11 @@ public: /// The material slot entry index. API_FUNCTION(Sealed) virtual MaterialBase* GetMaterial(int32 entryIndex) = 0; + /// + /// Gets the model (base class). + /// + API_FUNCTION(Sealed) virtual ModelBase* GetModel() = 0; + /// /// Sets the material to the entry slot. Can be used to override the material of the meshes using this slot. /// diff --git a/Source/Engine/Level/Actors/SplineModel.cpp b/Source/Engine/Level/Actors/SplineModel.cpp index 42b2dc620..0da4a3d70 100644 --- a/Source/Engine/Level/Actors/SplineModel.cpp +++ b/Source/Engine/Level/Actors/SplineModel.cpp @@ -367,6 +367,11 @@ MaterialBase* SplineModel::GetMaterial(int32 entryIndex) return material; } +ModelBase* SplineModel::GetModel() +{ + return Model.Get(); +} + void SplineModel::UpdateBounds() { OnSplineUpdated(); diff --git a/Source/Engine/Level/Actors/SplineModel.h b/Source/Engine/Level/Actors/SplineModel.h index a63d4f721..a9b105a8e 100644 --- a/Source/Engine/Level/Actors/SplineModel.h +++ b/Source/Engine/Level/Actors/SplineModel.h @@ -117,6 +117,7 @@ public: void OnParentChanged() override; const Span GetMaterialSlots() const override; MaterialBase* GetMaterial(int32 entryIndex) override; + ModelBase* GetModel() override; void UpdateBounds() override; protected: diff --git a/Source/Engine/Level/Actors/StaticModel.cpp b/Source/Engine/Level/Actors/StaticModel.cpp index fad068bf0..1fba1ba2e 100644 --- a/Source/Engine/Level/Actors/StaticModel.cpp +++ b/Source/Engine/Level/Actors/StaticModel.cpp @@ -599,6 +599,11 @@ MaterialBase* StaticModel::GetMaterial(int32 entryIndex) return material; } +ModelBase* StaticModel::GetModel() +{ + return Model.Get(); +} + bool StaticModel::IntersectsEntry(int32 entryIndex, const Ray& ray, Real& distance, Vector3& normal) { auto model = Model.Get(); diff --git a/Source/Engine/Level/Actors/StaticModel.h b/Source/Engine/Level/Actors/StaticModel.h index 063638e14..3a8c391f6 100644 --- a/Source/Engine/Level/Actors/StaticModel.h +++ b/Source/Engine/Level/Actors/StaticModel.h @@ -178,6 +178,7 @@ public: void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; const Span GetMaterialSlots() const override; MaterialBase* GetMaterial(int32 entryIndex) override; + ModelBase* GetModel() override; 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; From 21cac2ad6e96cb73eed729a3380dcf652bc959eb Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 20 Apr 2026 13:17:04 +0200 Subject: [PATCH 42/51] Various improvements to model previews in editor --- Source/Editor/Content/Proxy/ModelProxy.cs | 1 + Source/Editor/Content/Proxy/PrefabProxy.cs | 2 +- .../Viewport/Previews/AnimatedModelPreview.cs | 9 +++++- .../Editor/Viewport/Previews/ModelPreview.cs | 28 +++++++++++++++---- .../Viewport/Previews/SkinnedModelPreview.cs | 8 +++--- 5 files changed, 37 insertions(+), 11 deletions(-) diff --git a/Source/Editor/Content/Proxy/ModelProxy.cs b/Source/Editor/Content/Proxy/ModelProxy.cs index 8b20073d8..a57ddbf61 100644 --- a/Source/Editor/Content/Proxy/ModelProxy.cs +++ b/Source/Editor/Content/Proxy/ModelProxy.cs @@ -95,6 +95,7 @@ namespace FlaxEditor.Content var bounds = _preview.Model.GetBox(); var maxSize = Math.Max(0.001f, (float)bounds.Size.MaxValue); _preview.ViewportCamera.SetArcBallView(bounds); + _preview.NearPlane = Mathf.Min(10.0f, maxSize * 0.5f); _preview.FarPlane = Mathf.Max(1000.0f, maxSize * 2 + 100.0f); _preview.Task.OnDraw(); diff --git a/Source/Editor/Content/Proxy/PrefabProxy.cs b/Source/Editor/Content/Proxy/PrefabProxy.cs index 0fb247605..410df23b0 100644 --- a/Source/Editor/Content/Proxy/PrefabProxy.cs +++ b/Source/Editor/Content/Proxy/PrefabProxy.cs @@ -188,7 +188,7 @@ namespace FlaxEditor.Content if (bounds != BoundingBox.Empty) { float targetSize = 38.0f; - var maxSize = Math.Max(0.001f, (float)bounds.Size.MaxValue); + float maxSize = Math.Max(0.001f, (float)bounds.Size.MaxValue); float scale = targetSize / maxSize; _preview.Instance.Scale = new Float3(scale); _preview.Instance.Position = -bounds.Center * scale; diff --git a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs index a678e868a..10dcdf671 100644 --- a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs +++ b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs @@ -17,7 +17,7 @@ namespace FlaxEditor.Viewport.Previews private ContextMenuButton _showNodesButton, _showBoundsButton, _showFloorButton, _showNodesNamesButton; private bool _showNodes, _showBounds, _showFloor, _showNodesNames; private StaticModel _floorModel; - private bool _playAnimation, _playAnimationOnce; + private bool _playAnimation, _playAnimationOnce, _autoAdjustCamera = true; private float _playSpeed = 1.0f; /// @@ -291,6 +291,13 @@ namespace FlaxEditor.Viewport.Previews private void OnBegin(RenderTask task, GPUContext context) { + if (_autoAdjustCamera && SkinnedModel && SkinnedModel.IsLoaded) + { + // Control camera's near/far planes to properly cover object + _autoAdjustCamera = false; + ModelPreview.AdjustCamera(this, SkinnedModel.GetBox()); + } + if (!ScaleToFit) { if (_snapToOrigin) diff --git a/Source/Editor/Viewport/Previews/ModelPreview.cs b/Source/Editor/Viewport/Previews/ModelPreview.cs index ae0424da3..55f5a4592 100644 --- a/Source/Editor/Viewport/Previews/ModelPreview.cs +++ b/Source/Editor/Viewport/Previews/ModelPreview.cs @@ -18,7 +18,7 @@ namespace FlaxEditor.Viewport.Previews private ContextMenuButton _showBoundsButton, _showCurrentLODButton, _showNormalsButton, _showTangentsButton, _showBitangentsButton, _showFloorButton; private ContextMenu _previewLODsWidgetButtonMenu; private StaticModel _previewModel, _floorModel; - private bool _showBounds, _showCurrentLOD, _showNormals, _showTangents, _showBitangents, _showFloor; + private bool _showBounds, _showCurrentLOD, _showNormals, _showTangents, _showBitangents, _showFloor, _autoAdjustCamera = true; private MeshDataCache _meshDatas; /// @@ -221,8 +221,8 @@ namespace FlaxEditor.Viewport.Previews _showCurrentLODButton.IndexInParent = 2; _showCurrentLODButton.CloseMenuOnClick = false; - // Preview LODs mode widget - var PreviewLODsMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); + // Preview LOD mode widget + var previewLODsMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); _previewLODsWidgetButtonMenu = new ContextMenu(); _previewLODsWidgetButtonMenu.VisibleChanged += control => { @@ -248,14 +248,32 @@ namespace FlaxEditor.Viewport.Previews new ViewportWidgetButton("Preview LOD", SpriteHandle.Invalid, _previewLODsWidgetButtonMenu) { TooltipText = "Preview LOD properties", - Parent = PreviewLODsMode, + Parent = previewLODsMode, }; - PreviewLODsMode.Parent = this; + previewLODsMode.Parent = this; + } + } + + internal static void AdjustCamera(AssetPreview preview, BoundingBox box) + { + if (box.Size.MaxValue < preview.NearPlane) + { + // Very small object + preview.NearPlane = Mathf.Min(preview.NearPlane, box.Size.MaxValue * 0.5f + 0.01f); + preview.FarPlane *= 0.5f; + preview.MovementSpeed = 0.1f; } } private void OnBegin(RenderTask task, GPUContext context) { + if (_autoAdjustCamera && Model && Model.IsLoaded) + { + // Control camera's near/far planes to properly cover object + _autoAdjustCamera = false; + AdjustCamera(this, Model.GetBox()); + } + if (!ScaleToFit) { _previewModel.Scale = Float3.One; diff --git a/Source/Editor/Viewport/Previews/SkinnedModelPreview.cs b/Source/Editor/Viewport/Previews/SkinnedModelPreview.cs index 3048d0c78..d394190ce 100644 --- a/Source/Editor/Viewport/Previews/SkinnedModelPreview.cs +++ b/Source/Editor/Viewport/Previews/SkinnedModelPreview.cs @@ -51,8 +51,8 @@ namespace FlaxEditor.Viewport.Previews _showCurrentLODButton.IndexInParent = 2; _showCurrentLODButton.CloseMenuOnClick = false; - // PreviewLODS mode widget - var PreviewLODSMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); + // Preview LOD mode widget + var previewLODMode = new ViewportWidgetsContainer(ViewportWidgetLocation.UpperRight); _previewLODsWidgetButtonMenu = new ContextMenu(); _previewLODsWidgetButtonMenu.VisibleChanged += control => { @@ -78,9 +78,9 @@ namespace FlaxEditor.Viewport.Previews new ViewportWidgetButton("Preview LOD", SpriteHandle.Invalid, _previewLODsWidgetButtonMenu) { TooltipText = "Preview LOD properties", - Parent = PreviewLODSMode, + Parent = previewLODMode, }; - PreviewLODSMode.Parent = this; + previewLODMode.Parent = this; } } From 3cea8621a74043e26d4a0637c4994d4d4ca8ef11 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 20 Apr 2026 14:06:51 +0200 Subject: [PATCH 43/51] Fix `Time::Synchronize` to not reset time on unpause --- Source/Editor/States/PlayingState.cs | 4 ++-- Source/Engine/Engine/Engine.cpp | 2 +- Source/Engine/Engine/Time.cpp | 13 +++++++------ Source/Engine/Engine/Time.h | 5 +++-- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/Source/Editor/States/PlayingState.cs b/Source/Editor/States/PlayingState.cs index 50cde2212..51dfd3677 100644 --- a/Source/Editor/States/PlayingState.cs +++ b/Source/Editor/States/PlayingState.cs @@ -163,7 +163,7 @@ namespace FlaxEditor.States IsPlayModeStarting = false; Profiler.EndEvent(); - Time.Synchronize(); + Time.Synchronize(true); } private void SetupEditorEnvOptions() @@ -213,7 +213,7 @@ namespace FlaxEditor.States IsPlayModeEnding = false; Profiler.EndEvent(); - Time.Synchronize(); + Time.Synchronize(true); } } } diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp index 3d709499b..23768f6b0 100644 --- a/Source/Engine/Engine/Engine.cpp +++ b/Source/Engine/Engine/Engine.cpp @@ -157,7 +157,7 @@ int32 Engine::OnInit(const Char* cmdLine) Application::BeforeRun(); LOG_FLOOR(); LOG_FLUSH(); - Time::Synchronize(); + Time::Synchronize(true); EngineImpl::IsReady = true; PROFILE_MEM_END(); diff --git a/Source/Engine/Engine/Time.cpp b/Source/Engine/Engine/Time.cpp index 7d268cf73..4ca9f7ecd 100644 --- a/Source/Engine/Engine/Time.cpp +++ b/Source/Engine/Engine/Time.cpp @@ -75,10 +75,11 @@ void TimeSettings::Apply() #endif } -void Time::TickData::Synchronize(float targetFps, double currentTime) +void Time::TickData::Synchronize(float targetFps, double currentTime, bool resetTotalTime) { OnReset(targetFps, currentTime); - Time = UnscaledTime = TimeSpan::Zero(); + if (resetTotalTime) + Time = UnscaledTime = TimeSpan::Zero(); NextBegin = targetFps > ZeroTolerance ? LastBegin + (1.0f / targetFps) : 0.0; } @@ -258,13 +259,13 @@ void Time::SetFixedDeltaTime(bool enable, float value) FixedDeltaTimeValue = value; } -void Time::Synchronize() +void Time::Synchronize(bool resetTotalTime) { // Initialize tick data (based on a time settings) const double time = Platform::GetTimeSeconds(); - Update.Synchronize(UpdateFPS, time); - Physics.Synchronize(PhysicsFPS, time); - Draw.Synchronize(DrawFPS, time); + Update.Synchronize(UpdateFPS, time, resetTotalTime); + Physics.Synchronize(PhysicsFPS, time, resetTotalTime); + Draw.Synchronize(DrawFPS, time, resetTotalTime); } bool Time::OnBeginUpdate(double time) diff --git a/Source/Engine/Engine/Time.h b/Source/Engine/Engine/Time.h index bda7a7537..ee6802f67 100644 --- a/Source/Engine/Engine/Time.h +++ b/Source/Engine/Engine/Time.h @@ -75,7 +75,7 @@ public: TimeSpan UnscaledTime; public: - virtual void Synchronize(float targetFps, double currentTime); + virtual void Synchronize(float targetFps, double currentTime, bool resetTotalTime); virtual void OnReset(float targetFps, double currentTime); virtual bool OnTickBegin(double time, float targetFps, float maxDeltaTime); virtual void OnTickEnd(); @@ -220,7 +220,8 @@ public: /// /// Synchronizes update, fixed update and draw. Resets any pending deltas for fresh ticking in sync. /// - API_FUNCTION() static void Synchronize(); + /// True if reset total time, otherwise only delta time and ticking is reset. + API_FUNCTION() static void Synchronize(bool resetTotalTime = false); private: // Methods used by the Engine class From fc864cb504e1a6cd6e206bf02bbcdb3f357d1636 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 20 Apr 2026 14:57:33 +0200 Subject: [PATCH 44/51] Fix internal function name collision with base class function in bindings --- Source/Tools/Flax.Build/Bindings/ClassStructInfo.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/Source/Tools/Flax.Build/Bindings/ClassStructInfo.cs b/Source/Tools/Flax.Build/Bindings/ClassStructInfo.cs index 727483988..891c79b43 100644 --- a/Source/Tools/Flax.Build/Bindings/ClassStructInfo.cs +++ b/Source/Tools/Flax.Build/Bindings/ClassStructInfo.cs @@ -97,11 +97,22 @@ namespace Flax.Build.Bindings UniqueFunctionNames = new HashSet(); int idx = 1; functionInfo.UniqueName = functionInfo.Name; - while (UniqueFunctionNames.Contains(functionInfo.UniqueName)) + while (!IsUniqueFunctionName(this, functionInfo.UniqueName)) functionInfo.UniqueName = functionInfo.Name + idx++; UniqueFunctionNames.Add(functionInfo.UniqueName); } + private static bool IsUniqueFunctionName(VirtualClassInfo type, string name) + { + while (type != null) + { + while (type.UniqueFunctionNames.Contains(name)) + return false; + type = type.BaseType as VirtualClassInfo; + } + return true; + } + public abstract int GetScriptVTableSize(out int offset); public abstract int GetScriptVTableOffset(VirtualClassInfo classInfo); From 0592816ef1db64fce1087075f88612a059e5661b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 20 Apr 2026 15:31:02 +0200 Subject: [PATCH 45/51] Fix compiler error with Large Worlds enabled --- Source/Editor/Viewport/Previews/ModelPreview.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Viewport/Previews/ModelPreview.cs b/Source/Editor/Viewport/Previews/ModelPreview.cs index 55f5a4592..aff9ebb4c 100644 --- a/Source/Editor/Viewport/Previews/ModelPreview.cs +++ b/Source/Editor/Viewport/Previews/ModelPreview.cs @@ -259,7 +259,7 @@ namespace FlaxEditor.Viewport.Previews if (box.Size.MaxValue < preview.NearPlane) { // Very small object - preview.NearPlane = Mathf.Min(preview.NearPlane, box.Size.MaxValue * 0.5f + 0.01f); + preview.NearPlane = Mathf.Min(preview.NearPlane, (float)box.Size.MaxValue * 0.5f + 0.01f); preview.FarPlane *= 0.5f; preview.MovementSpeed = 0.1f; } From c23ac8782b97526c204e7e24f1e7d87526e7dac8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 20 Apr 2026 17:48:27 +0200 Subject: [PATCH 46/51] Add missing `ManagedEditor.h` header to editor package --- Source/Editor/Editor.Build.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Editor.Build.cs b/Source/Editor/Editor.Build.cs index 40085fd91..fadcab3be 100644 --- a/Source/Editor/Editor.Build.cs +++ b/Source/Editor/Editor.Build.cs @@ -73,21 +73,19 @@ public class Editor : EditorModule AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "PS4", "PLATFORM_TOOLS_PS4"); AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "PS5", "PLATFORM_TOOLS_PS5"); AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "XboxScarlett", "PLATFORM_TOOLS_XBOX_SCARLETT", "PLATFORM_TOOLS_GDK"); - AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "Android", "PLATFORM_TOOLS_ANDROID"); AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "Switch", "PLATFORM_TOOLS_SWITCH"); AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "Linux", "PLATFORM_TOOLS_LINUX"); } else if (options.Platform.Target == TargetPlatform.Linux) { AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "Linux", "PLATFORM_TOOLS_LINUX"); - AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "Android", "PLATFORM_TOOLS_ANDROID"); } else if (options.Platform.Target == TargetPlatform.Mac) { AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "Mac", "PLATFORM_TOOLS_MAC"); - AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "Android", "PLATFORM_TOOLS_ANDROID"); AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "iOS", "PLATFORM_TOOLS_IOS"); } + AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "Android", "PLATFORM_TOOLS_ANDROID"); AddPlatformTools(options, platformToolsRoot, platformToolsRootExternal, "Web", "PLATFORM_TOOLS_WEB"); // Visual Studio integration @@ -118,11 +116,11 @@ public class Editor : EditorModule { files.Add(Path.Combine(FolderPath, "Editor.h")); files.Add(Path.Combine(FolderPath, "ProjectInfo.h")); - files.Add(Path.Combine(FolderPath, "Cooker/CookingData.h")); files.Add(Path.Combine(FolderPath, "Cooker/GameCooker.h")); files.Add(Path.Combine(FolderPath, "Cooker/PlatformTools.h")); files.Add(Path.Combine(FolderPath, "Cooker/Steps/CookAssetsStep.h")); files.Add(Path.Combine(FolderPath, "Utilities/ViewportIconsRenderer.h")); + files.Add(Path.Combine(FolderPath, "Managed/ManagedEditor.h")); } } From 6a5742352c60e55a8e3dd034db1cab9e082be89b Mon Sep 17 00:00:00 2001 From: envision3d Date: Mon, 20 Apr 2026 12:01:48 -0500 Subject: [PATCH 47/51] Include selected folders in items for deletion --- Source/Editor/Content/Tree/TreeViewPanel.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Content/Tree/TreeViewPanel.cs b/Source/Editor/Content/Tree/TreeViewPanel.cs index 44bbfa878..07af1e2d1 100644 --- a/Source/Editor/Content/Tree/TreeViewPanel.cs +++ b/Source/Editor/Content/Tree/TreeViewPanel.cs @@ -71,9 +71,12 @@ public class TreeViewPanel : Panel var items = new List(); foreach (var node in selection) { - if (node is ContentItemTreeNode contentNode) + if (node is ContentItemTreeNode fileNode) { - items.Add(contentNode.Item); + items.Add(fileNode.Item); + } else if (node is ContentFolderTreeNode folderNode) + { + items.Add(folderNode.Folder); } } From c130928e7a2c729b50b9898dec2e338649573c42 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 22 Apr 2026 10:19:28 +0200 Subject: [PATCH 48/51] Fix skinned model retarget source asset filter to work after `AssetPicker` refactor with new validator --- Source/Editor/GUI/AssetPicker.cs | 5 ----- Source/Editor/Windows/Assets/SkinnedModelWindow.cs | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/Source/Editor/GUI/AssetPicker.cs b/Source/Editor/GUI/AssetPicker.cs index bf04aa068..56c358c2f 100644 --- a/Source/Editor/GUI/AssetPicker.cs +++ b/Source/Editor/GUI/AssetPicker.cs @@ -38,11 +38,6 @@ namespace FlaxEditor.GUI /// public event Action SelectedItemChanged; - /// - /// The custom callback for assets validation. Cane be used to implement a rule for assets to pick. - /// - public Func CheckValid; - /// /// False if changing selected item is disabled. /// diff --git a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs index 75fa87dfe..839905f40 100644 --- a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs +++ b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs @@ -272,7 +272,7 @@ namespace FlaxEditor.Windows.Assets infoLabel.AutoHeight = true; var sourceAssetPicker = setupGroup.AddPropertyItem("Source Asset").Custom().CustomControl; sourceAssetPicker.Height = 48; - sourceAssetPicker.CheckValid = CheckSourceAssetValid; + sourceAssetPicker.Validator.CheckValid = CheckSourceAssetValid; sourceAssetPicker.SelectedItemChanged += () => { proxy.Setups.Add(sourceAssetPicker.Validator.SelectedAsset, new SetupProxy()); From 9264a6e0117d399438272c349f9ae0b05c814f1e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 22 Apr 2026 11:41:35 +0200 Subject: [PATCH 49/51] Fix tooltip for content items in Tree View mode --- Source/Editor/Content/Tree/ContentItemTreeNode.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Source/Editor/Content/Tree/ContentItemTreeNode.cs b/Source/Editor/Content/Tree/ContentItemTreeNode.cs index 6fd2f1ebc..780bff376 100644 --- a/Source/Editor/Content/Tree/ContentItemTreeNode.cs +++ b/Source/Editor/Content/Tree/ContentItemTreeNode.cs @@ -134,6 +134,9 @@ public sealed class ContentItemTreeNode : TreeNode, IContentItemOwner DoDragDrop(DragItems.GetDragData(Item)); } + /// + protected override bool ShowTooltip => true; + /// public override bool OnShowTooltip(out string text, out Float2 location, out Rectangle area) { From 11aeaba92a44345be2ac54ea9f1ddec65598d1af Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 22 Apr 2026 14:48:54 +0200 Subject: [PATCH 50/51] Fix missing include files in packaged build --- Source/Editor/Managed/ManagedEditor.h | 1 - Source/Engine/Tools/AudioTool/AudioTool.Build.cs | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Managed/ManagedEditor.h b/Source/Editor/Managed/ManagedEditor.h index e320e00cf..4df4658b6 100644 --- a/Source/Editor/Managed/ManagedEditor.h +++ b/Source/Editor/Managed/ManagedEditor.h @@ -4,7 +4,6 @@ #include "Engine/Scripting/ScriptingObject.h" #include "Engine/Platform/Window.h" -#include "Engine/ShadowsOfMordor/Types.h" #include "Engine/Tools/TextureTool/TextureTool.h" #include "Engine/Tools/ModelTool/ModelTool.h" #include "Engine/Tools/AudioTool/AudioTool.h" diff --git a/Source/Engine/Tools/AudioTool/AudioTool.Build.cs b/Source/Engine/Tools/AudioTool/AudioTool.Build.cs index 88999826d..e2b1abc48 100644 --- a/Source/Engine/Tools/AudioTool/AudioTool.Build.cs +++ b/Source/Engine/Tools/AudioTool/AudioTool.Build.cs @@ -1,5 +1,6 @@ // Copyright (c) Wojciech Figat. All rights reserved. +using System.IO; using System.Collections.Generic; using Flax.Build; using Flax.Build.NativeCpp; @@ -26,5 +27,6 @@ public class AudioTool : EngineModule /// public override void GetFilesToDeploy(List files) { + files.Add(Path.Combine(FolderPath, "AudioTool.h")); } } From df059f79c7c51071c06fd65aeaa8b8e706125294 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 22 Apr 2026 16:53:08 +0200 Subject: [PATCH 51/51] Optimize out loading texture `DefaultLensDirt` if it's not needed --- Source/Engine/Renderer/PostProcessingPass.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Renderer/PostProcessingPass.cpp b/Source/Engine/Renderer/PostProcessingPass.cpp index 7c7ea48d2..206087303 100644 --- a/Source/Engine/Renderer/PostProcessingPass.cpp +++ b/Source/Engine/Renderer/PostProcessingPass.cpp @@ -546,7 +546,10 @@ void PostProcessingPass::Render(RenderContext& renderContext, GPUTexture* input, // - 5 - LensStar - lens star texture // - 7 - ColorGradingLUT context->BindSR(0, input->View()); - context->BindSR(4, GetCustomOrDefault(settings.LensFlares.LensDirt, _defaultLensDirt, TEXT("Engine/Textures/DefaultLensDirt"))); + if ((useLensFlares || useBloom) && settings.LensFlares.LensDirtIntensity > 0) + context->BindSR(4, GetCustomOrDefault(settings.LensFlares.LensDirt, _defaultLensDirt, TEXT("Engine/Textures/DefaultLensDirt"))); + else + context->BindSR(4, device->GetDefaultBlackTexture()); context->BindSR(7, colorGradingLutView); // Composite final frame during single pass (done in full resolution)