From 317886e8935c91ca2c2b05ff8f7b42b01de6a057 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Tue, 21 Nov 2023 12:03:01 -0600 Subject: [PATCH 01/79] Remove ability to delete content and source folders. Limit CM options on those folders only to ones that make sense. --- Source/Editor/Content/Tree/ContentTreeNode.cs | 7 ++++++- .../Content/Tree/MainContentTreeNode.cs | 3 +++ .../Windows/ContentWindow.ContextMenu.cs | 20 ++++++++++++++----- 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Content/Tree/ContentTreeNode.cs b/Source/Editor/Content/Tree/ContentTreeNode.cs index 0c0fc6a51..f13aec2d4 100644 --- a/Source/Editor/Content/Tree/ContentTreeNode.cs +++ b/Source/Editor/Content/Tree/ContentTreeNode.cs @@ -24,6 +24,11 @@ namespace FlaxEditor.Content /// protected ContentFolder _folder; + /// + /// Whether this node can be deleted. + /// + protected virtual bool _canDelete => true; + /// /// Gets the content folder item. /// @@ -301,7 +306,7 @@ namespace FlaxEditor.Content StartRenaming(); return true; case KeyboardKeys.Delete: - if (Folder.Exists) + if (Folder.Exists && _canDelete) Editor.Instance.Windows.ContentWin.Delete(Folder); return true; } diff --git a/Source/Editor/Content/Tree/MainContentTreeNode.cs b/Source/Editor/Content/Tree/MainContentTreeNode.cs index def873622..23895a669 100644 --- a/Source/Editor/Content/Tree/MainContentTreeNode.cs +++ b/Source/Editor/Content/Tree/MainContentTreeNode.cs @@ -12,6 +12,9 @@ namespace FlaxEditor.Content { private FileSystemWatcher _watcher; + /// + protected override bool _canDelete => false; + /// /// Initializes a new instance of the class. /// diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index 168067977..a36c862bd 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -114,18 +114,28 @@ namespace FlaxEditor.Windows } } - cm.AddButton("Delete", () => Delete(item)); + if (isFolder && folder.Node is not MainContentTreeNode) + { + cm.AddButton("Delete", () => Delete(item)); - cm.AddSeparator(); + cm.AddSeparator(); - cm.AddButton("Duplicate", _view.Duplicate); + cm.AddButton("Duplicate", _view.Duplicate); - cm.AddButton("Copy", _view.Copy); + cm.AddButton("Copy", _view.Copy); + } + else + { + cm.AddSeparator(); + } b = cm.AddButton("Paste", _view.Paste); b.Enabled = _view.CanPaste(); - cm.AddButton("Rename", () => Rename(item)); + if (isFolder && folder.Node is not MainContentTreeNode) + { + cm.AddButton("Rename", () => Rename(item)); + } // Custom options ContextMenuShow?.Invoke(cm, item); From fe53317ec72b1a91b50c81486fd979483f038296 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Tue, 21 Nov 2023 12:10:44 -0600 Subject: [PATCH 02/79] Fix issue of options not showing up for regular content items. --- .../Editor/Windows/ContentWindow.ContextMenu.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Windows/ContentWindow.ContextMenu.cs b/Source/Editor/Windows/ContentWindow.ContextMenu.cs index a36c862bd..dda812f84 100644 --- a/Source/Editor/Windows/ContentWindow.ContextMenu.cs +++ b/Source/Editor/Windows/ContentWindow.ContextMenu.cs @@ -114,7 +114,11 @@ namespace FlaxEditor.Windows } } - if (isFolder && folder.Node is not MainContentTreeNode) + if (isFolder && folder.Node is MainContentTreeNode) + { + cm.AddSeparator(); + } + else { cm.AddButton("Delete", () => Delete(item)); @@ -124,15 +128,15 @@ namespace FlaxEditor.Windows cm.AddButton("Copy", _view.Copy); } - else - { - cm.AddSeparator(); - } b = cm.AddButton("Paste", _view.Paste); b.Enabled = _view.CanPaste(); - if (isFolder && folder.Node is not MainContentTreeNode) + if (isFolder && folder.Node is MainContentTreeNode) + { + // Do nothing + } + else { cm.AddButton("Rename", () => Rename(item)); } From 94f1dff497e91a564233eede25f54a824c4852b5 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Tue, 21 Nov 2023 15:40:34 -0600 Subject: [PATCH 03/79] Add main content nodes to not be duplicated. --- Source/Editor/Content/Tree/ContentTreeNode.cs | 7 ++++++- Source/Editor/Content/Tree/MainContentTreeNode.cs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Content/Tree/ContentTreeNode.cs b/Source/Editor/Content/Tree/ContentTreeNode.cs index f13aec2d4..4560d35c9 100644 --- a/Source/Editor/Content/Tree/ContentTreeNode.cs +++ b/Source/Editor/Content/Tree/ContentTreeNode.cs @@ -28,6 +28,11 @@ namespace FlaxEditor.Content /// Whether this node can be deleted. /// protected virtual bool _canDelete => true; + + /// + /// Whether this node can be duplicated. + /// + protected virtual bool _canDuplicate => true; /// /// Gets the content folder item. @@ -315,7 +320,7 @@ namespace FlaxEditor.Content switch (key) { case KeyboardKeys.D: - if (Folder.Exists) + if (Folder.Exists && _canDuplicate) Editor.Instance.Windows.ContentWin.Duplicate(Folder); return true; } diff --git a/Source/Editor/Content/Tree/MainContentTreeNode.cs b/Source/Editor/Content/Tree/MainContentTreeNode.cs index 23895a669..16115e1cf 100644 --- a/Source/Editor/Content/Tree/MainContentTreeNode.cs +++ b/Source/Editor/Content/Tree/MainContentTreeNode.cs @@ -15,6 +15,9 @@ namespace FlaxEditor.Content /// protected override bool _canDelete => false; + /// + protected override bool _canDuplicate => false; + /// /// Initializes a new instance of the class. /// From 53aae90d45f50abf3952fa7d007c3cc114810762 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 24 Nov 2023 07:50:00 -0600 Subject: [PATCH 04/79] Code style fix --- Source/Editor/Content/Tree/ContentTreeNode.cs | 8 ++++---- Source/Editor/Content/Tree/MainContentTreeNode.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/Editor/Content/Tree/ContentTreeNode.cs b/Source/Editor/Content/Tree/ContentTreeNode.cs index 4560d35c9..f83349893 100644 --- a/Source/Editor/Content/Tree/ContentTreeNode.cs +++ b/Source/Editor/Content/Tree/ContentTreeNode.cs @@ -27,12 +27,12 @@ namespace FlaxEditor.Content /// /// Whether this node can be deleted. /// - protected virtual bool _canDelete => true; + protected virtual bool CanDelete => true; /// /// Whether this node can be duplicated. /// - protected virtual bool _canDuplicate => true; + protected virtual bool CanDuplicate => true; /// /// Gets the content folder item. @@ -311,7 +311,7 @@ namespace FlaxEditor.Content StartRenaming(); return true; case KeyboardKeys.Delete: - if (Folder.Exists && _canDelete) + if (Folder.Exists && CanDelete) Editor.Instance.Windows.ContentWin.Delete(Folder); return true; } @@ -320,7 +320,7 @@ namespace FlaxEditor.Content switch (key) { case KeyboardKeys.D: - if (Folder.Exists && _canDuplicate) + if (Folder.Exists && CanDuplicate) Editor.Instance.Windows.ContentWin.Duplicate(Folder); return true; } diff --git a/Source/Editor/Content/Tree/MainContentTreeNode.cs b/Source/Editor/Content/Tree/MainContentTreeNode.cs index 16115e1cf..316006de0 100644 --- a/Source/Editor/Content/Tree/MainContentTreeNode.cs +++ b/Source/Editor/Content/Tree/MainContentTreeNode.cs @@ -13,10 +13,10 @@ namespace FlaxEditor.Content private FileSystemWatcher _watcher; /// - protected override bool _canDelete => false; + protected override bool CanDelete => false; /// - protected override bool _canDuplicate => false; + protected override bool CanDuplicate => false; /// /// Initializes a new instance of the class. From 2ddef2c6beaa8e10987369dc52db30ec4b4bb835 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Fri, 24 Nov 2023 10:53:43 -0600 Subject: [PATCH 05/79] make vars public --- Source/Editor/Content/Tree/ContentTreeNode.cs | 4 ++-- Source/Editor/Content/Tree/MainContentTreeNode.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Source/Editor/Content/Tree/ContentTreeNode.cs b/Source/Editor/Content/Tree/ContentTreeNode.cs index f83349893..e2cd1e771 100644 --- a/Source/Editor/Content/Tree/ContentTreeNode.cs +++ b/Source/Editor/Content/Tree/ContentTreeNode.cs @@ -27,12 +27,12 @@ namespace FlaxEditor.Content /// /// Whether this node can be deleted. /// - protected virtual bool CanDelete => true; + public virtual bool CanDelete => true; /// /// Whether this node can be duplicated. /// - protected virtual bool CanDuplicate => true; + public virtual bool CanDuplicate => true; /// /// Gets the content folder item. diff --git a/Source/Editor/Content/Tree/MainContentTreeNode.cs b/Source/Editor/Content/Tree/MainContentTreeNode.cs index 316006de0..a4f24f4ab 100644 --- a/Source/Editor/Content/Tree/MainContentTreeNode.cs +++ b/Source/Editor/Content/Tree/MainContentTreeNode.cs @@ -13,10 +13,10 @@ namespace FlaxEditor.Content private FileSystemWatcher _watcher; /// - protected override bool CanDelete => false; + public override bool CanDelete => false; /// - protected override bool CanDuplicate => false; + public override bool CanDuplicate => false; /// /// Initializes a new instance of the class. From e5b1a10d34d031b99136dcd15e9dacfa83da4b19 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 26 Nov 2023 20:02:08 +0200 Subject: [PATCH 06/79] Fix VSCode intellisense not finding generated C# bindings definitions --- .../Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs index 77b7abf46..fd47e3261 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs @@ -73,8 +73,8 @@ namespace Flax.Build.Projects.VisualStudio csProjectFileContent.AppendLine(string.Format(" {0}", string.Join(";", allPlatforms))); // Provide default platform and configuration - csProjectFileContent.AppendLine(string.Format(" {0}", defaultConfiguration.Text)); - csProjectFileContent.AppendLine(string.Format(" {0}", defaultConfiguration.ArchitectureName)); + csProjectFileContent.AppendLine(string.Format(" {0}", defaultConfiguration.Text)); + csProjectFileContent.AppendLine(string.Format(" {0}", defaultConfiguration.ArchitectureName)); switch (project.OutputType ?? defaultTarget.OutputType) { From 0bcd0154e1cea52d7ef251fa7ebe8e5bb925a225 Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 26 Nov 2023 20:04:30 +0200 Subject: [PATCH 07/79] Remove wrong .NET SDK preprocessor definitions and support `NET8_0` Only `X_OR_GREATER` symbols should be defined for all versions, and only the latest detected SDK symbol should be generated. --- Source/Engine/Scripting/Scripting.Build.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Source/Engine/Scripting/Scripting.Build.cs b/Source/Engine/Scripting/Scripting.Build.cs index 10fc2aff9..0071a60f1 100644 --- a/Source/Engine/Scripting/Scripting.Build.cs +++ b/Source/Engine/Scripting/Scripting.Build.cs @@ -20,10 +20,7 @@ public class Scripting : EngineModule void AddFrameworkDefines(string template, int major, int latestMinor) { for (int minor = latestMinor; minor >= 0; minor--) - { options.ScriptingAPI.Defines.Add(string.Format(template, major, minor)); - options.ScriptingAPI.Defines.Add(string.Format($"{template}_OR_GREATER", major, minor)); - } } // .NET @@ -31,14 +28,15 @@ public class Scripting : EngineModule options.ScriptingAPI.Defines.Add("USE_NETCORE"); // .NET SDK - AddFrameworkDefines("NET{0}_{1}", 7, 0); // "NET7_0" and "NET7_0_OR_GREATER" - AddFrameworkDefines("NET{0}_{1}", 6, 0); - AddFrameworkDefines("NET{0}_{1}", 5, 0); + var dotnetSdk = DotNetSdk.Instance; options.ScriptingAPI.Defines.Add("NET"); - AddFrameworkDefines("NETCOREAPP{0}_{1}", 3, 1); // "NETCOREAPP3_1" and "NETCOREAPP3_1_OR_GREATER" - AddFrameworkDefines("NETCOREAPP{0}_{1}", 2, 2); - AddFrameworkDefines("NETCOREAPP{0}_{1}", 1, 1); + AddFrameworkDefines("NET{0}_{1}", dotnetSdk.Version.Major, 0); // "NET7_0" + for (int i = 5; i <= dotnetSdk.Version.Major; i++) + AddFrameworkDefines("NET{0}_{1}_OR_GREATER", dotnetSdk.Version.Major, 0); // "NET7_0_OR_GREATER" options.ScriptingAPI.Defines.Add("NETCOREAPP"); + AddFrameworkDefines("NETCOREAPP{0}_{1}_OR_GREATER", 3, 1); // "NETCOREAPP3_1_OR_GREATER" + AddFrameworkDefines("NETCOREAPP{0}_{1}_OR_GREATER", 2, 2); + AddFrameworkDefines("NETCOREAPP{0}_{1}_OR_GREATER", 1, 1); if (options.Target is EngineTarget engineTarget && engineTarget.UseSeparateMainExecutable(options)) { From aab88a746df197a14f2999301170862d2595a63a Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 26 Nov 2023 20:26:53 +0200 Subject: [PATCH 08/79] Use detected .NET and C# language version in generated project files --- Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs | 12 ++++++++++++ .../Projects/VisualStudio/CSSDKProjectGenerator.cs | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs index 240c95c16..fe49887ed 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/DotNetSdk.cs @@ -161,6 +161,18 @@ namespace Flax.Build /// public readonly string RuntimeVersionName; + /// + /// Maximum supported C#-language version for the SDK. + /// + public string CSharpLanguageVersion => Version.Major switch + { + 8 => "12.0", + 7 => "11.0", + 6 => "10.0", + 5 => "9.0", + _ => "7.3", + }; + /// /// Initializes a new instance of the class. /// diff --git a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs index fd47e3261..c678b2521 100644 --- a/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs +++ b/Source/Tools/Flax.Build/Projects/VisualStudio/CSSDKProjectGenerator.cs @@ -28,6 +28,7 @@ namespace Flax.Build.Projects.VisualStudio /// public override void GenerateProject(Project project, string solutionPath) { + var dotnetSdk = DotNetSdk.Instance; var csProjectFileContent = new StringBuilder(); var vsProject = (VisualStudioProject)project; @@ -96,7 +97,7 @@ namespace Flax.Build.Projects.VisualStudio var cacheProjectsPath = Utilities.MakePathRelativeTo(Path.Combine(Globals.Root, "Cache", "Projects"), projectDirectory); var flaxBuildTargetsPath = !string.IsNullOrEmpty(cacheProjectsPath) ? Path.Combine(cacheProjectsPath, flaxBuildTargetsFilename) : flaxBuildTargetsFilename; - csProjectFileContent.AppendLine(" net7.0"); + csProjectFileContent.AppendLine($" net{dotnetSdk.Version.Major}.{dotnetSdk.Version.Minor}"); csProjectFileContent.AppendLine(" disable"); csProjectFileContent.AppendLine(string.Format(" {0}", baseConfiguration.TargetBuildOptions.ScriptingAPI.CSharpNullableReferences.ToString().ToLowerInvariant())); csProjectFileContent.AppendLine(" false"); @@ -108,7 +109,7 @@ namespace Flax.Build.Projects.VisualStudio csProjectFileContent.AppendLine(" false"); csProjectFileContent.AppendLine(string.Format(" {0}", project.BaseName)); csProjectFileContent.AppendLine(string.Format(" {0}.CSharp", project.BaseName)); - csProjectFileContent.AppendLine(" 11.0"); + csProjectFileContent.AppendLine($" {dotnetSdk.CSharpLanguageVersion}"); csProjectFileContent.AppendLine(" 512"); //csProjectFileContent.AppendLine(" false"); // TODO: use it to reduce burden of framework libs From ef8bb33105d57725e6f3137db21e1cc134816f6c Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 26 Nov 2023 20:27:12 +0200 Subject: [PATCH 09/79] Compile C# scripts with latest detected C# language version --- Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs | 2 +- 1 file changed, 1 insertion(+), 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 c8e709d86..8866f7a88 100644 --- a/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs +++ b/Source/Tools/Flax.Build/Build/DotNet/Builder.DotNet.cs @@ -247,7 +247,7 @@ namespace Flax.Build args.Add("/fullpaths"); args.Add("/filealign:512"); #if USE_NETCORE - args.Add("/langversion:11.0"); + args.Add($"/langversion:{dotnetSdk.CSharpLanguageVersion}"); args.Add(string.Format("/nullable:{0}", buildOptions.ScriptingAPI.CSharpNullableReferences.ToString().ToLowerInvariant())); if (buildOptions.ScriptingAPI.CSharpNullableReferences == CSharpNullableReferences.Disable) args.Add("-nowarn:8632"); // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. From c577c78f3f04ea1a9f4996bb63956db483ceb88c Mon Sep 17 00:00:00 2001 From: Ari Vuollet Date: Sun, 26 Nov 2023 20:47:42 +0200 Subject: [PATCH 10/79] Fix running Flax.Build with .NET 8 runtime --- Source/Tools/Flax.Build/Flax.Build.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Tools/Flax.Build/Flax.Build.csproj b/Source/Tools/Flax.Build/Flax.Build.csproj index 9c837ee89..1837bf0ec 100644 --- a/Source/Tools/Flax.Build/Flax.Build.csproj +++ b/Source/Tools/Flax.Build/Flax.Build.csproj @@ -21,6 +21,7 @@ none true 1701;1702;8981 + LatestMajor From 2f4b956d78cce858b80e1e43a332a934a24d432f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 27 Nov 2023 11:53:21 +0100 Subject: [PATCH 11/79] Fix crash when unloading texture that has active streaming tasks --- Source/Engine/Graphics/Textures/StreamingTexture.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Source/Engine/Graphics/Textures/StreamingTexture.cpp b/Source/Engine/Graphics/Textures/StreamingTexture.cpp index 173b0ef85..015386fff 100644 --- a/Source/Engine/Graphics/Textures/StreamingTexture.cpp +++ b/Source/Engine/Graphics/Textures/StreamingTexture.cpp @@ -63,7 +63,6 @@ StreamingTexture::~StreamingTexture() { UnloadTexture(); SAFE_DELETE(_texture); - ASSERT(_streamingTasks.Count() == 0); } Float2 StreamingTexture::Size() const @@ -134,11 +133,9 @@ bool StreamingTexture::Create(const TextureHeader& header) void StreamingTexture::UnloadTexture() { ScopeLock lock(_owner->GetOwnerLocker()); - - // Release + CancelStreamingTasks(); _texture->ReleaseGPU(); _header.MipLevels = 0; - ASSERT(_streamingTasks.Count() == 0); } From 39dc5939e3461ab1dca6c1de5ff564dbda69ed83 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 27 Nov 2023 17:08:07 +0100 Subject: [PATCH 12/79] Fix crash when boxing native non-POD structure into managed format #1992 --- Source/Engine/Engine/NativeInterop.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index 3f19f5951..30ce4d39f 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -1135,7 +1135,7 @@ namespace FlaxEngine.Interop marshallers[i](fields[i], offsets[i], ref managedValue, fieldPtr, out int fieldSize); fieldPtr += fieldSize; } - Assert.IsTrue((fieldPtr - nativePtr) <= Unsafe.SizeOf()); + Assert.IsTrue((fieldPtr - nativePtr) <= GetTypeSize(typeof(T))); } internal static void ToManaged(ref T managedValue, IntPtr nativePtr, bool byRef) @@ -1182,7 +1182,7 @@ namespace FlaxEngine.Interop marshallers[i](fields[i], offsets[i], ref managedValue, nativePtr, out int fieldSize); nativePtr += fieldSize; } - Assert.IsTrue((nativePtr - fieldPtr) <= Unsafe.SizeOf()); + Assert.IsTrue((nativePtr - fieldPtr) <= GetTypeSize(typeof(T))); } internal static void ToNative(ref T managedValue, IntPtr nativePtr) @@ -1580,7 +1580,7 @@ namespace FlaxEngine.Interop private static IntPtr PinValue(T value) where T : struct { // Store the converted value in unmanaged memory so it will not be relocated by the garbage collector. - int size = Unsafe.SizeOf(); + int size = GetTypeSize(typeof(T)); uint index = Interlocked.Increment(ref pinnedAllocationsPointer) % (uint)pinnedAllocations.Length; ref (IntPtr ptr, int size) alloc = ref pinnedAllocations[index]; if (alloc.size < size) From 8ffc86ef881d0c82ebea99111e19f71bbc243656 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Nov 2023 11:20:07 +0100 Subject: [PATCH 13/79] Fix missing output parameters conversion when calling interface implementation in scripting #1992 --- .../Bindings/BindingsGenerator.Cpp.cs | 22 +++++++++++++------ Source/Tools/Flax.Build/Bindings/TypeInfo.cs | 11 +++++++--- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 63cf9d2f8..2c5d967f8 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -1532,9 +1532,7 @@ namespace Flax.Build.Bindings if (paramIsRef && !parameterInfo.Type.IsConst) { // Unbox from MObject* - parameterInfo.Type.IsRef = false; - contents.Append($" {parameterInfo.Name} = MUtils::Unbox<{parameterInfo.Type}>(*(MObject**)params[{i}]);").AppendLine(); - parameterInfo.Type.IsRef = true; + contents.Append($" {parameterInfo.Name} = MUtils::Unbox<{parameterInfo.Type.ToString(false)}>(*(MObject**)params[{i}]);").AppendLine(); } } } @@ -1559,8 +1557,7 @@ namespace Flax.Build.Bindings for (var i = 0; i < functionInfo.Parameters.Count; i++) { var parameterInfo = functionInfo.Parameters[i]; - var paramIsRef = parameterInfo.IsRef || parameterInfo.IsOut; - if (paramIsRef && !parameterInfo.Type.IsConst) + if (parameterInfo.IsRef || parameterInfo.IsOut && !parameterInfo.Type.IsConst) { // Direct value convert var managedToNative = GenerateCppWrapperManagedToNative(buildData, parameterInfo.Type, classInfo, out var managedType, out var apiType, null, out _); @@ -2007,8 +2004,7 @@ namespace Flax.Build.Bindings for (var i = 0; i < paramsCount; i++) { var paramType = eventInfo.Type.GenericArgs[i]; - var paramIsRef = paramType.IsRef && !paramType.IsConst; - if (paramIsRef) + if (paramType.IsRef && !paramType.IsConst) { // Convert value back from managed to native (could be modified there) paramType.IsRef = false; @@ -2569,6 +2565,18 @@ namespace Flax.Build.Bindings contents.AppendLine(" {"); contents.AppendLine(" Variant __result;"); contents.AppendLine($" typeHandle.Module->InvokeMethod(method, Object, Span(parameters, {functionInfo.Parameters.Count}), __result);"); + + // Convert parameter values back from scripting to native (could be modified there) + for (var i = 0; i < functionInfo.Parameters.Count; i++) + { + var parameterInfo = functionInfo.Parameters[i]; + var paramIsRef = parameterInfo.IsRef || parameterInfo.IsOut; + if (paramIsRef && !parameterInfo.Type.IsConst) + { + contents.AppendLine($" {parameterInfo.Name} = {GenerateCppWrapperVariantToNative(buildData, parameterInfo.Type, interfaceInfo, $"parameters[{i}]")};"); + } + } + if (functionInfo.ReturnType.IsVoid) contents.AppendLine(" return;"); else diff --git a/Source/Tools/Flax.Build/Bindings/TypeInfo.cs b/Source/Tools/Flax.Build/Bindings/TypeInfo.cs index e3bbfb6a1..cdf293085 100644 --- a/Source/Tools/Flax.Build/Bindings/TypeInfo.cs +++ b/Source/Tools/Flax.Build/Bindings/TypeInfo.cs @@ -180,7 +180,7 @@ namespace Flax.Build.Bindings return sb.ToString(); } - public override string ToString() + public string ToString(bool canRef = true) { var sb = new StringBuilder(64); if (IsConst) @@ -199,13 +199,18 @@ namespace Flax.Build.Bindings } if (IsPtr) sb.Append('*'); - if (IsRef) + if (IsRef && canRef) sb.Append('&'); - if (IsMoveRef) + if (IsMoveRef && canRef) sb.Append('&'); return sb.ToString(); } + public override string ToString() + { + return ToString(true); + } + public static bool Equals(List a, List b) { if (a == null && b == null) From 17dca8c5c77ae52085ede252f304d5d482181b44 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Nov 2023 11:21:29 +0100 Subject: [PATCH 14/79] Fix invalid codegen for array reference passed as output parameter in scripting interface method --- Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 2c5d967f8..4476c380a 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -2775,11 +2775,12 @@ namespace Flax.Build.Bindings // Variant converting helper methods foreach (var typeInfo in CppVariantToTypes) { + var name = typeInfo.ToString(false); header.AppendLine(); header.AppendLine("namespace {"); - header.Append($"{typeInfo} VariantTo{GenerateCppWrapperNativeToVariantMethodName(typeInfo)}(const Variant& v)").AppendLine(); + header.Append($"{name} VariantTo{GenerateCppWrapperNativeToVariantMethodName(typeInfo)}(const Variant& v)").AppendLine(); header.Append('{').AppendLine(); - header.Append($" {typeInfo} result;").AppendLine(); + header.Append($" {name} result;").AppendLine(); if (typeInfo.Type == "Array" && typeInfo.GenericArgs != null) { header.Append(" const auto* array = reinterpret_cast*>(v.AsData);").AppendLine(); From fd938e8284177f1c6f28006bc1aee69f5adaa4fe Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Nov 2023 11:22:14 +0100 Subject: [PATCH 15/79] Fix incorrect pointer marshalling from `Variant` to managed runtime #1992 --- Source/Engine/Engine/NativeInterop.Unmanaged.cs | 3 +++ Source/Engine/Engine/NativeInterop.cs | 8 +++----- Source/Engine/Scripting/ManagedCLR/MUtils.cpp | 4 ++++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Engine/NativeInterop.Unmanaged.cs b/Source/Engine/Engine/NativeInterop.Unmanaged.cs index 574b74580..e99276a4e 100644 --- a/Source/Engine/Engine/NativeInterop.Unmanaged.cs +++ b/Source/Engine/Engine/NativeInterop.Unmanaged.cs @@ -1271,6 +1271,9 @@ namespace FlaxEngine.Interop case Type _ when type == typeof(IntPtr): monoType = MTypes.Ptr; break; + case Type _ when type.IsPointer: + monoType = MTypes.Ptr; + break; case Type _ when type.IsEnum: monoType = MTypes.Enum; break; diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index 30ce4d39f..9548c3f59 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -1112,11 +1112,9 @@ namespace FlaxEngine.Interop internal static void ToManagedPointer(ref IntPtr managedValue, IntPtr nativePtr, bool byRef) { - Type type = typeof(T); - byRef |= type.IsByRef; // Is this needed? - if (type.IsByRef) - Assert.IsTrue(type.GetElementType().IsValueType); - managedValue = byRef ? nativePtr : Unsafe.Read(nativePtr.ToPointer()); + if (byRef) + nativePtr = Unsafe.Read(nativePtr.ToPointer()); + managedValue = nativePtr; } internal static void ToManagedHandle(ref ManagedHandle managedValue, IntPtr nativePtr, bool byRef) diff --git a/Source/Engine/Scripting/ManagedCLR/MUtils.cpp b/Source/Engine/Scripting/ManagedCLR/MUtils.cpp index be8e10fab..52d049403 100644 --- a/Source/Engine/Scripting/ManagedCLR/MUtils.cpp +++ b/Source/Engine/Scripting/ManagedCLR/MUtils.cpp @@ -1208,6 +1208,10 @@ void* MUtils::VariantToManagedArgPtr(Variant& value, MType* type, bool& failed) object = nullptr; return object; } + case MTypes::Ptr: + if (value.Type.Type == VariantType::Null) + return nullptr; + return (void*)value; default: break; } From 35ebdb0ffe5beabe4606db7b37b153c2e2af86ab Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Nov 2023 11:24:17 +0100 Subject: [PATCH 16/79] Refactor `INetworkDriver::PopEvent` to use network event as output parameter rather than raw pointer #1992 --- .../Engine/Networking/Drivers/ENetDriver.cpp | 18 ++++---- Source/Engine/Networking/Drivers/ENetDriver.h | 2 +- .../Networking/Drivers/NetworkLagDriver.cpp | 6 +-- .../Networking/Drivers/NetworkLagDriver.h | 2 +- Source/Engine/Networking/INetworkDriver.h | 2 +- Source/Engine/Networking/NetworkConnection.h | 10 ++++- Source/Engine/Networking/NetworkEvent.h | 16 ++++--- Source/Engine/Networking/NetworkPeer.cpp | 2 +- Source/Engine/Networking/NetworkPeer.h | 42 +++++++------------ Source/Engine/Scripting/ManagedCLR/MClass.h | 2 +- Source/Engine/Scripting/Runtime/DotNet.cpp | 4 +- 11 files changed, 51 insertions(+), 55 deletions(-) diff --git a/Source/Engine/Networking/Drivers/ENetDriver.cpp b/Source/Engine/Networking/Drivers/ENetDriver.cpp index d7a3617a4..c44c83fb8 100644 --- a/Source/Engine/Networking/Drivers/ENetDriver.cpp +++ b/Source/Engine/Networking/Drivers/ENetDriver.cpp @@ -162,7 +162,7 @@ void ENetDriver::Disconnect(const NetworkConnection& connection) } } -bool ENetDriver::PopEvent(NetworkEvent* eventPtr) +bool ENetDriver::PopEvent(NetworkEvent& eventPtr) { ASSERT(_host); ENetEvent event; @@ -173,30 +173,30 @@ bool ENetDriver::PopEvent(NetworkEvent* eventPtr) { // Copy sender data const uint32 connectionId = enet_peer_get_id(event.peer); - eventPtr->Sender.ConnectionId = connectionId; + eventPtr.Sender.ConnectionId = connectionId; switch (event.type) { case ENET_EVENT_TYPE_CONNECT: - eventPtr->EventType = NetworkEventType::Connected; + eventPtr.EventType = NetworkEventType::Connected; if (IsServer()) _peerMap.Add(connectionId, event.peer); break; case ENET_EVENT_TYPE_DISCONNECT: - eventPtr->EventType = NetworkEventType::Disconnected; + eventPtr.EventType = NetworkEventType::Disconnected; if (IsServer()) _peerMap.Remove(connectionId); break; case ENET_EVENT_TYPE_DISCONNECT_TIMEOUT: - eventPtr->EventType = NetworkEventType::Timeout; + eventPtr.EventType = NetworkEventType::Timeout; if (IsServer()) _peerMap.Remove(connectionId); break; case ENET_EVENT_TYPE_RECEIVE: - eventPtr->EventType = NetworkEventType::Message; - eventPtr->Message = _networkHost->CreateMessage(); - eventPtr->Message.Length = event.packet->dataLength; - Platform::MemoryCopy(eventPtr->Message.Buffer, event.packet->data, event.packet->dataLength); + eventPtr.EventType = NetworkEventType::Message; + eventPtr.Message = _networkHost->CreateMessage(); + eventPtr.Message.Length = event.packet->dataLength; + Platform::MemoryCopy(eventPtr.Message.Buffer, event.packet->data, event.packet->dataLength); break; default: break; diff --git a/Source/Engine/Networking/Drivers/ENetDriver.h b/Source/Engine/Networking/Drivers/ENetDriver.h index a91263caa..fdabdd5ce 100644 --- a/Source/Engine/Networking/Drivers/ENetDriver.h +++ b/Source/Engine/Networking/Drivers/ENetDriver.h @@ -29,7 +29,7 @@ public: bool Connect() override; void Disconnect() override; void Disconnect(const NetworkConnection& connection) override; - bool PopEvent(NetworkEvent* eventPtr) override; + bool PopEvent(NetworkEvent& eventPtr) override; void SendMessage(NetworkChannelType channelType, const NetworkMessage& message) override; void SendMessage(NetworkChannelType channelType, const NetworkMessage& message, NetworkConnection target) override; void SendMessage(NetworkChannelType channelType, const NetworkMessage& message, const Array& targets) override; diff --git a/Source/Engine/Networking/Drivers/NetworkLagDriver.cpp b/Source/Engine/Networking/Drivers/NetworkLagDriver.cpp index 615aab7d6..be07f367c 100644 --- a/Source/Engine/Networking/Drivers/NetworkLagDriver.cpp +++ b/Source/Engine/Networking/Drivers/NetworkLagDriver.cpp @@ -92,7 +92,7 @@ void NetworkLagDriver::Disconnect(const NetworkConnection& connection) _driver->Disconnect(connection); } -bool NetworkLagDriver::PopEvent(NetworkEvent* eventPtr) +bool NetworkLagDriver::PopEvent(NetworkEvent& eventPtr) { if (!_driver) return false; @@ -104,7 +104,7 @@ bool NetworkLagDriver::PopEvent(NetworkEvent* eventPtr) if (e.Lag > 0.0) continue; - *eventPtr = e.Event; + eventPtr = e.Event; _events.RemoveAtKeepOrder(i); return true; } @@ -117,7 +117,7 @@ bool NetworkLagDriver::PopEvent(NetworkEvent* eventPtr) auto& e = _events.AddOne(); e.Lag = (double)Lag; - e.Event = *eventPtr; + e.Event = eventPtr; } return false; } diff --git a/Source/Engine/Networking/Drivers/NetworkLagDriver.h b/Source/Engine/Networking/Drivers/NetworkLagDriver.h index e07545b55..4021523ca 100644 --- a/Source/Engine/Networking/Drivers/NetworkLagDriver.h +++ b/Source/Engine/Networking/Drivers/NetworkLagDriver.h @@ -68,7 +68,7 @@ public: bool Connect() override; void Disconnect() override; void Disconnect(const NetworkConnection& connection) override; - bool PopEvent(NetworkEvent* eventPtr) override; + bool PopEvent(NetworkEvent& eventPtr) override; void SendMessage(NetworkChannelType channelType, const NetworkMessage& message) override; void SendMessage(NetworkChannelType channelType, const NetworkMessage& message, NetworkConnection target) override; void SendMessage(NetworkChannelType channelType, const NetworkMessage& message, const Array& targets) override; diff --git a/Source/Engine/Networking/INetworkDriver.h b/Source/Engine/Networking/INetworkDriver.h index e3f23a0a3..1abd4a177 100644 --- a/Source/Engine/Networking/INetworkDriver.h +++ b/Source/Engine/Networking/INetworkDriver.h @@ -71,7 +71,7 @@ public: /// /// The pointer to event structure. /// True when succeeded and the event can be processed. - API_FUNCTION() virtual bool PopEvent(NetworkEvent* eventPtr) = 0; + API_FUNCTION() virtual bool PopEvent(API_PARAM(Out) NetworkEvent& eventPtr) = 0; /// /// Sends given message over specified channel to the server. diff --git a/Source/Engine/Networking/NetworkConnection.h b/Source/Engine/Networking/NetworkConnection.h index c7ae20138..f6a39ea72 100644 --- a/Source/Engine/Networking/NetworkConnection.h +++ b/Source/Engine/Networking/NetworkConnection.h @@ -10,13 +10,19 @@ API_STRUCT(Namespace="FlaxEngine.Networking") struct FLAXENGINE_API NetworkConnection { DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkConnection); + public: /// /// The identifier of the connection. /// /// Used by network driver implementations. - API_FIELD() - uint32 ConnectionId; + API_FIELD() uint32 ConnectionId; +}; + +template<> +struct TIsPODType +{ + enum { Value = true }; }; inline bool operator==(const NetworkConnection& a, const NetworkConnection& b) diff --git a/Source/Engine/Networking/NetworkEvent.h b/Source/Engine/Networking/NetworkEvent.h index 691d33f0c..eb00aac03 100644 --- a/Source/Engine/Networking/NetworkEvent.h +++ b/Source/Engine/Networking/NetworkEvent.h @@ -43,24 +43,28 @@ API_ENUM(Namespace="FlaxEngine.Networking") enum class NetworkEventType API_STRUCT(Namespace="FlaxEngine.Networking") struct FLAXENGINE_API NetworkEvent { DECLARE_SCRIPTING_TYPE_MINIMAL(NetworkEvent); + public: /// /// The type of the received event. /// - API_FIELD(); - NetworkEventType EventType; + API_FIELD() NetworkEventType EventType; /// /// The message when this event is an "message" event - not valid in any other cases. /// If this is an message-event, make sure to return the message using RecycleMessage function of the peer after processing it! /// - API_FIELD(); - NetworkMessage Message; + API_FIELD() NetworkMessage Message; /// /// The connected of the client that has sent message, connected, disconnected or got a timeout. /// /// Only valid when event has been received on server-peer. - API_FIELD(); - NetworkConnection Sender; + API_FIELD() NetworkConnection Sender; +}; + +template<> +struct TIsPODType +{ + enum { Value = true }; }; diff --git a/Source/Engine/Networking/NetworkPeer.cpp b/Source/Engine/Networking/NetworkPeer.cpp index b39b617e0..c4c0ea712 100644 --- a/Source/Engine/Networking/NetworkPeer.cpp +++ b/Source/Engine/Networking/NetworkPeer.cpp @@ -134,7 +134,7 @@ void NetworkPeer::Disconnect(const NetworkConnection& connection) bool NetworkPeer::PopEvent(NetworkEvent& eventRef) { PROFILE_CPU(); - return NetworkDriver->PopEvent(&eventRef); + return NetworkDriver->PopEvent(eventRef); } NetworkMessage NetworkPeer::CreateMessage() diff --git a/Source/Engine/Networking/NetworkPeer.h b/Source/Engine/Networking/NetworkPeer.h index 6b36e3278..d68584754 100644 --- a/Source/Engine/Networking/NetworkPeer.h +++ b/Source/Engine/Networking/NetworkPeer.h @@ -37,30 +37,26 @@ public: /// Once this is called, this peer becomes a server. /// /// True when succeeded. - API_FUNCTION() - bool Listen(); + API_FUNCTION() bool Listen(); /// /// Starts connection handshake with the end point specified in the structure. /// Once this is called, this peer becomes a client. /// /// True when succeeded. - API_FUNCTION() - bool Connect(); + API_FUNCTION() bool Connect(); /// /// Disconnects from the server. /// /// Can be used only by the client! - API_FUNCTION() - void Disconnect(); + API_FUNCTION() void Disconnect(); /// /// Disconnects given connection from the server. /// /// Can be used only by the server! - API_FUNCTION() - void Disconnect(const NetworkConnection& connection); + API_FUNCTION() void Disconnect(const NetworkConnection& connection); /// /// Tries to pop an network event from the queue. @@ -68,8 +64,7 @@ public: /// The reference to event structure. /// True when succeeded and the event can be processed. /// If this returns message event, make sure to recycle the message using function after processing it! - API_FUNCTION() - bool PopEvent(API_PARAM(out) NetworkEvent& eventRef); + API_FUNCTION() bool PopEvent(API_PARAM(Out) NetworkEvent& eventRef); /// /// Acquires new message from the pool. @@ -77,29 +72,25 @@ public: /// /// The acquired message. /// Make sure to recycle the message to this peer once it is no longer needed! - API_FUNCTION() - NetworkMessage CreateMessage(); + API_FUNCTION() NetworkMessage CreateMessage(); /// /// Returns given message to the pool. /// /// Make sure that this message belongs to the peer and has not been recycled already (debug build checks for this)! - API_FUNCTION() - void RecycleMessage(const NetworkMessage& message); + API_FUNCTION() void RecycleMessage(const NetworkMessage& message); /// /// Acquires new message from the pool and setups it for sending. /// /// The acquired message. - API_FUNCTION() - NetworkMessage BeginSendMessage(); + API_FUNCTION() NetworkMessage BeginSendMessage(); /// /// Aborts given message send. This effectively deinitializes the message and returns it to the pool. /// /// The message. - API_FUNCTION() - void AbortSendMessage(const NetworkMessage& message); + API_FUNCTION() void AbortSendMessage(const NetworkMessage& message); /// /// Sends given message over specified channel to the server. @@ -111,8 +102,7 @@ public: /// Do not recycle the message after calling this. /// This function automatically recycles the message. /// - API_FUNCTION() - bool EndSendMessage(NetworkChannelType channelType, const NetworkMessage& message); + API_FUNCTION() bool EndSendMessage(NetworkChannelType channelType, const NetworkMessage& message); /// /// Sends given message over specified channel to the given client connection (target). @@ -125,8 +115,7 @@ public: /// Do not recycle the message after calling this. /// This function automatically recycles the message. /// - API_FUNCTION() - bool EndSendMessage(NetworkChannelType channelType, const NetworkMessage& message, const NetworkConnection& target); + API_FUNCTION() bool EndSendMessage(NetworkChannelType channelType, const NetworkMessage& message, const NetworkConnection& target); /// /// Sends given message over specified channel to the given client connection (target). @@ -139,8 +128,7 @@ public: /// Do not recycle the message after calling this. /// This function automatically recycles the message. /// - API_FUNCTION() - bool EndSendMessage(NetworkChannelType channelType, const NetworkMessage& message, const Array& targets); + API_FUNCTION() bool EndSendMessage(NetworkChannelType channelType, const NetworkMessage& message, const Array& targets); /// /// Creates new peer using given configuration. @@ -148,15 +136,13 @@ public: /// The configuration to create and setup new peer. /// The peer. /// Peer should be destroyed using once it is no longer in use. Returns null if failed to create a peer (eg. config is invalid). - API_FUNCTION() - static NetworkPeer* CreatePeer(const NetworkConfig& config); + API_FUNCTION() static NetworkPeer* CreatePeer(const NetworkConfig& config); /// /// Shutdowns and destroys given peer. /// /// The peer to destroy. - API_FUNCTION() - static void ShutdownPeer(NetworkPeer* peer); + API_FUNCTION() static void ShutdownPeer(NetworkPeer* peer); public: bool IsValid() const diff --git a/Source/Engine/Scripting/ManagedCLR/MClass.h b/Source/Engine/Scripting/ManagedCLR/MClass.h index 49b153731..a8fbf6db0 100644 --- a/Source/Engine/Scripting/ManagedCLR/MClass.h +++ b/Source/Engine/Scripting/ManagedCLR/MClass.h @@ -18,7 +18,7 @@ private: #elif USE_NETCORE void* _handle; StringAnsi _name; - StringAnsi _namespace_; + StringAnsi _namespace; uint32 _types = 0; mutable uint32 _size = 0; #endif diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 3c90eb4d1..464e60ba6 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -836,7 +836,7 @@ bool MAssembly::UnloadImage(bool isReloading) MClass::MClass(const MAssembly* parentAssembly, void* handle, const char* name, const char* fullname, const char* namespace_, MTypeAttributes attributes) : _handle(handle) , _name(name) - , _namespace_(namespace_) + , _namespace(namespace_) , _assembly(parentAssembly) , _fullname(fullname) , _hasCachedProperties(false) @@ -915,7 +915,7 @@ StringAnsiView MClass::GetName() const StringAnsiView MClass::GetNamespace() const { - return _namespace_; + return _namespace; } MType* MClass::GetType() const From a909b57e82cbf8eab155780e015da274ce9f5d91 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Nov 2023 11:24:46 +0100 Subject: [PATCH 17/79] Fix deadlock in `NetworkManager` when network peer returns invalid event type #1992 --- Source/Engine/Networking/NetworkManager.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Networking/NetworkManager.cpp b/Source/Engine/Networking/NetworkManager.cpp index ee2395f2a..06ae9b4d3 100644 --- a/Source/Engine/Networking/NetworkManager.cpp +++ b/Source/Engine/Networking/NetworkManager.cpp @@ -382,7 +382,8 @@ void NetworkManagerService::Update() // Process network messages NetworkEvent event; - while (peer->PopEvent(event)) + bool eventIsValid = true; + while (peer->PopEvent(event) && eventIsValid) { switch (event.EventType) { @@ -472,6 +473,9 @@ void NetworkManagerService::Update() } peer->RecycleMessage(event.Message); break; + default: + eventIsValid = false; + break; } } From 47b8c9978fa16fa99ed8faa2f5cdc00b68d42f35 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Nov 2023 11:30:04 +0100 Subject: [PATCH 18/79] Fix missing channel masking in material Scene Texture node #2000 --- .../MaterialGenerator.Textures.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp index 7edd9a43f..ad601589e 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp @@ -441,6 +441,23 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) break; } } + + // Channel masking + switch (box->ID) + { + case 2: + value = Value(VariantType::Float, value.Value + _subs[0]); + break; + case 3: + value = Value(VariantType::Float, value.Value + _subs[1]); + break; + case 4: + value = Value(VariantType::Float, value.Value + _subs[2]); + break; + case 5: + value = Value(VariantType::Float, value.Value + _subs[3]); + break; + } break; } // Scene Color From cf8b7a20c24c797428fb74e654702e171a1e85cd Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Nov 2023 14:55:26 +0100 Subject: [PATCH 19/79] Improve 47b8c9978fa16fa99ed8faa2f5cdc00b68d42f35 to handle non-vec4 cases #2000 --- .../MaterialGenerator.Textures.cpp | 8 ++-- Source/Engine/Visject/ShaderGraphValue.cpp | 42 +++++++++++++++++++ Source/Engine/Visject/ShaderGraphValue.h | 15 ++----- 3 files changed, 49 insertions(+), 16 deletions(-) diff --git a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp index ad601589e..ed79b5edc 100644 --- a/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp +++ b/Source/Engine/Tools/MaterialGenerator/MaterialGenerator.Textures.cpp @@ -446,16 +446,16 @@ void MaterialGenerator::ProcessGroupTextures(Box* box, Node* node, Value& value) switch (box->ID) { case 2: - value = Value(VariantType::Float, value.Value + _subs[0]); + value = value.GetX(); break; case 3: - value = Value(VariantType::Float, value.Value + _subs[1]); + value = value.GetY(); break; case 4: - value = Value(VariantType::Float, value.Value + _subs[2]); + value = value.GetZ(); break; case 5: - value = Value(VariantType::Float, value.Value + _subs[3]); + value = value.GetW(); break; } break; diff --git a/Source/Engine/Visject/ShaderGraphValue.cpp b/Source/Engine/Visject/ShaderGraphValue.cpp index 6564cde2b..bb330e718 100644 --- a/Source/Engine/Visject/ShaderGraphValue.cpp +++ b/Source/Engine/Visject/ShaderGraphValue.cpp @@ -271,6 +271,48 @@ ShaderGraphValue ShaderGraphValue::InitForOne(VariantType::Types type) return ShaderGraphValue(type, String(v)); } +ShaderGraphValue ShaderGraphValue::GetY() const +{ + switch (Type) + { + case VariantType::Float2: + case VariantType::Float3: + case VariantType::Float4: + case VariantType::Double2: + case VariantType::Double3: + case VariantType::Double4: + return ShaderGraphValue(VariantType::Types::Float, Value + _subs[1]); + default: + return Zero; + } +} + +ShaderGraphValue ShaderGraphValue::GetZ() const +{ + switch (Type) + { + case VariantType::Float3: + case VariantType::Float4: + case VariantType::Double3: + case VariantType::Double4: + return ShaderGraphValue(VariantType::Types::Float, Value + _subs[2]); + default: + return Zero; + } +} + +ShaderGraphValue ShaderGraphValue::GetW() const +{ + switch (Type) + { + case VariantType::Float4: + case VariantType::Double4: + return ShaderGraphValue(VariantType::Types::Float, Value + _subs[3]); + default: + return One; + } +} + ShaderGraphValue ShaderGraphValue::Cast(const ShaderGraphValue& v, VariantType::Types to) { // If they are the same types or input value is empty, then just return value diff --git a/Source/Engine/Visject/ShaderGraphValue.h b/Source/Engine/Visject/ShaderGraphValue.h index 439b59077..214012bd8 100644 --- a/Source/Engine/Visject/ShaderGraphValue.h +++ b/Source/Engine/Visject/ShaderGraphValue.h @@ -318,28 +318,19 @@ public: /// Gets the Y component of the value. Valid only for vector types. /// /// The Y component. - ShaderGraphValue GetY() const - { - return ShaderGraphValue(VariantType::Types::Float, Value + _subs[1]); - } + ShaderGraphValue GetY() const; /// /// Gets the Z component of the value. Valid only for vector types. /// /// The Z component. - ShaderGraphValue GetZ() const - { - return ShaderGraphValue(VariantType::Types::Float, Value + _subs[2]); - } + ShaderGraphValue GetZ() const; /// /// Gets the W component of the value. Valid only for vector types. /// /// The W component. - ShaderGraphValue GetW() const - { - return ShaderGraphValue(VariantType::Types::Float, Value + _subs[3]); - } + ShaderGraphValue GetW() const; public: /// From 0aeac36f0912deeb12303d928356216ab3bb423b Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Nov 2023 15:55:34 +0100 Subject: [PATCH 20/79] Fix `__cplusplus` macro on MSVC and add logging C++ version used during compilation --- Source/Engine/Engine/Engine.cpp | 8 +++++++- .../Flax.Build/Platforms/Windows/WindowsToolchainBase.cs | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Source/Engine/Engine/Engine.cpp b/Source/Engine/Engine/Engine.cpp index c0cfad34c..259649062 100644 --- a/Source/Engine/Engine/Engine.cpp +++ b/Source/Engine/Engine/Engine.cpp @@ -522,7 +522,13 @@ void EngineImpl::InitLog() LOG(Info, "Compiled for Dev Environment"); #endif LOG(Info, "Version " FLAXENGINE_VERSION_TEXT); - LOG(Info, "Compiled: {0} {1}", TEXT(__DATE__), TEXT(__TIME__)); + const Char* cpp = TEXT("?"); + if (__cplusplus == 202101L) cpp = TEXT("C++23"); + else if (__cplusplus == 202002L) cpp = TEXT("C++20"); + else if (__cplusplus == 201703L) cpp = TEXT("C++17"); + else if (__cplusplus == 201402L) cpp = TEXT("C++14"); + else if (__cplusplus == 201103L) cpp = TEXT("C++11"); + LOG(Info, "Compiled: {0} {1} {2}", TEXT(__DATE__), TEXT(__TIME__), cpp); #ifdef _MSC_VER const String mcsVer = StringUtils::ToString(_MSC_FULL_VER); LOG(Info, "Compiled with Visual C++ {0}.{1}.{2}.{3:0^2d}", mcsVer.Substring(0, 2), mcsVer.Substring(2, 2), mcsVer.Substring(4, 5), _MSC_BUILD); diff --git a/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs b/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs index e7dbaac19..994cdcc72 100644 --- a/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs +++ b/Source/Tools/Flax.Build/Platforms/Windows/WindowsToolchainBase.cs @@ -453,6 +453,7 @@ namespace Flax.Build.Platforms commonArgs.Add("/std:c++latest"); break; } + commonArgs.Add("/Zc:__cplusplus"); // Generate Intrinsic Functions if (compileEnvironment.IntrinsicFunctions) From 4f8aff4352745bc86bb9e9c0a7cc6604cf7e6b7a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Nov 2023 16:02:36 +0100 Subject: [PATCH 21/79] Refactor memory allocators to use dedicated path when moving collection data that is not blittable #2001 #1920 --- Source/Engine/Core/Collections/Array.h | 34 +++++++++++--- Source/Engine/Core/Collections/Dictionary.h | 52 +++++++++++++++++---- Source/Engine/Core/Collections/HashSet.h | 50 ++++++++++++++++---- Source/Engine/Core/Compiler.h | 7 +++ Source/Engine/Core/Memory/Allocation.h | 22 ++++----- Source/Engine/Renderer/RendererAllocation.h | 2 + 6 files changed, 132 insertions(+), 35 deletions(-) diff --git a/Source/Engine/Core/Collections/Array.h b/Source/Engine/Core/Collections/Array.h index 58117cf0a..cf45ed060 100644 --- a/Source/Engine/Core/Collections/Array.h +++ b/Source/Engine/Core/Collections/Array.h @@ -25,6 +25,19 @@ private: int32 _capacity; AllocationData _allocation; + FORCE_INLINE static void MoveToEmpty(AllocationData& to, AllocationData& from, int32 fromCount, int32 fromCapacity) + { + if IF_CONSTEXPR (AllocationType::HasSwap) + to.Swap(from); + else + { + to.Allocate(fromCapacity); + Memory::MoveItems(to.Get(), from.Get(), fromCount); + Memory::DestructItems(from.Get(), fromCount); + from.Free(); + } + } + public: /// /// Initializes a new instance of the class. @@ -134,7 +147,7 @@ public: _capacity = other._capacity; other._count = 0; other._capacity = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _count, _capacity); } /// @@ -191,7 +204,7 @@ public: _capacity = other._capacity; other._count = 0; other._capacity = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _count, _capacity); } return *this; } @@ -713,9 +726,18 @@ public: /// The other collection. void Swap(Array& other) { - ::Swap(_count, other._count); - ::Swap(_capacity, other._capacity); - _allocation.Swap(other._allocation); + if IF_CONSTEXPR (AllocationType::HasSwap) + { + _allocation.Swap(other._allocation); + ::Swap(_count, other._count); + ::Swap(_capacity, other._capacity); + } + else + { + Array tmp = MoveTemp(other); + other = *this; + *this = MoveTemp(tmp); + } } /// @@ -726,9 +748,7 @@ public: T* data = _allocation.Get(); const int32 count = _count / 2; for (int32 i = 0; i < count; i++) - { ::Swap(data[i], data[_count - i - 1]); - } } public: diff --git a/Source/Engine/Core/Collections/Dictionary.h b/Source/Engine/Core/Collections/Dictionary.h index 4d65a4123..dd73be390 100644 --- a/Source/Engine/Core/Collections/Dictionary.h +++ b/Source/Engine/Core/Collections/Dictionary.h @@ -110,6 +110,33 @@ private: int32 _size = 0; AllocationData _allocation; + FORCE_INLINE static void MoveToEmpty(AllocationData& to, AllocationData& from, int32 fromSize) + { + if IF_CONSTEXPR (AllocationType::HasSwap) + to.Swap(from); + else + { + to.Allocate(fromSize); + Bucket* toData = to.Get(); + Bucket* fromData = from.Get(); + for (int32 i = 0; i < fromSize; i++) + { + Bucket& fromBucket = fromData[i]; + if (fromBucket.IsOccupied()) + { + Bucket& toBucket = toData[i]; + Memory::MoveItems(&toBucket.Key, &fromBucket.Key, 1); + Memory::MoveItems(&toBucket.Value, &fromBucket.Value, 1); + toBucket._state = Bucket::Occupied; + Memory::DestructItem(&fromBucket.Key); + Memory::DestructItem(&fromBucket.Value); + fromBucket._state = Bucket::Empty; + } + } + from.Free(); + } + } + public: /// /// Initializes a new instance of the class. @@ -139,7 +166,7 @@ public: other._elementsCount = 0; other._deletedCount = 0; other._size = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _size); } /// @@ -180,7 +207,7 @@ public: other._elementsCount = 0; other._deletedCount = 0; other._size = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _size); } return *this; } @@ -510,7 +537,7 @@ public: return; ASSERT(capacity >= 0); AllocationData oldAllocation; - oldAllocation.Swap(_allocation); + MoveToEmpty(oldAllocation, _allocation, _size); const int32 oldSize = _size; const int32 oldElementsCount = _elementsCount; _deletedCount = _elementsCount = 0; @@ -580,10 +607,19 @@ public: /// The other collection. void Swap(Dictionary& other) { - ::Swap(_elementsCount, other._elementsCount); - ::Swap(_deletedCount, other._deletedCount); - ::Swap(_size, other._size); - _allocation.Swap(other._allocation); + if IF_CONSTEXPR (AllocationType::HasSwap) + { + ::Swap(_elementsCount, other._elementsCount); + ::Swap(_deletedCount, other._deletedCount); + ::Swap(_size, other._size); + _allocation.Swap(other._allocation); + } + else + { + Dictionary tmp = MoveTemp(other); + other = *this; + *this = MoveTemp(tmp); + } } public: @@ -930,7 +966,7 @@ private: { // Rebuild entire table completely AllocationData oldAllocation; - oldAllocation.Swap(_allocation); + MoveToEmpty(oldAllocation, _allocation, _size); _allocation.Allocate(_size); Bucket* data = _allocation.Get(); for (int32 i = 0; i < _size; i++) diff --git a/Source/Engine/Core/Collections/HashSet.h b/Source/Engine/Core/Collections/HashSet.h index 4a4f3e924..a683edf15 100644 --- a/Source/Engine/Core/Collections/HashSet.h +++ b/Source/Engine/Core/Collections/HashSet.h @@ -93,6 +93,31 @@ private: int32 _size = 0; AllocationData _allocation; + FORCE_INLINE static void MoveToEmpty(AllocationData& to, AllocationData& from, int32 fromSize) + { + if IF_CONSTEXPR (AllocationType::HasSwap) + to.Swap(from); + else + { + to.Allocate(fromSize); + Bucket* toData = to.Get(); + Bucket* fromData = from.Get(); + for (int32 i = 0; i < fromSize; i++) + { + Bucket& fromBucket = fromData[i]; + if (fromBucket.IsOccupied()) + { + Bucket& toBucket = toData[i]; + Memory::MoveItems(&toBucket.Item, &fromBucket.Item, 1); + toBucket._state = Bucket::Occupied; + Memory::DestructItem(&fromBucket.Item); + fromBucket._state = Bucket::Empty; + } + } + from.Free(); + } + } + public: /// /// Initializes a new instance of the class. @@ -122,7 +147,7 @@ public: other._elementsCount = 0; other._deletedCount = 0; other._size = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _size); } /// @@ -163,7 +188,7 @@ public: other._elementsCount = 0; other._deletedCount = 0; other._size = 0; - _allocation.Swap(other._allocation); + MoveToEmpty(_allocation, other._allocation, _size); } return *this; } @@ -389,7 +414,7 @@ public: return; ASSERT(capacity >= 0); AllocationData oldAllocation; - oldAllocation.Swap(_allocation); + MoveToEmpty(oldAllocation, _allocation, _size); const int32 oldSize = _size; const int32 oldElementsCount = _elementsCount; _deletedCount = _elementsCount = 0; @@ -458,10 +483,19 @@ public: /// The other collection. void Swap(HashSet& other) { - ::Swap(_elementsCount, other._elementsCount); - ::Swap(_deletedCount, other._deletedCount); - ::Swap(_size, other._size); - _allocation.Swap(other._allocation); + if IF_CONSTEXPR (AllocationType::HasSwap) + { + ::Swap(_elementsCount, other._elementsCount); + ::Swap(_deletedCount, other._deletedCount); + ::Swap(_size, other._size); + _allocation.Swap(other._allocation); + } + else + { + HashSet tmp = MoveTemp(other); + other = *this; + *this = MoveTemp(tmp); + } } public: @@ -726,7 +760,7 @@ private: { // Rebuild entire table completely AllocationData oldAllocation; - oldAllocation.Swap(_allocation); + MoveToEmpty(oldAllocation, _allocation, _size); _allocation.Allocate(_size); Bucket* data = _allocation.Get(); for (int32 i = 0; i < _size; i++) diff --git a/Source/Engine/Core/Compiler.h b/Source/Engine/Core/Compiler.h index 9a33b8758..4ea246077 100644 --- a/Source/Engine/Core/Compiler.h +++ b/Source/Engine/Core/Compiler.h @@ -93,3 +93,10 @@ #endif #define PACK_STRUCT(__Declaration__) PACK_BEGIN() __Declaration__ PACK_END() + +// C++ 17 +#if __cplusplus >= 201703L +#define IF_CONSTEXPR constexpr +#else +#define IF_CONSTEXPR +#endif diff --git a/Source/Engine/Core/Memory/Allocation.h b/Source/Engine/Core/Memory/Allocation.h index 8d7188d91..fb6b4555b 100644 --- a/Source/Engine/Core/Memory/Allocation.h +++ b/Source/Engine/Core/Memory/Allocation.h @@ -12,6 +12,8 @@ template class FixedAllocation { public: + enum { HasSwap = false }; + template class Data { @@ -61,12 +63,9 @@ public: { } - FORCE_INLINE void Swap(Data& other) + void Swap(Data& other) { - byte tmp[Capacity * sizeof(T)]; - Platform::MemoryCopy(tmp, _data, Capacity * sizeof(T)); - Platform::MemoryCopy(_data, other._data, Capacity * sizeof(T)); - Platform::MemoryCopy(other._data, tmp, Capacity * sizeof(T)); + // Not supported } }; }; @@ -77,6 +76,8 @@ public: class HeapAllocation { public: + enum { HasSwap = true }; + template class Data { @@ -179,6 +180,8 @@ template class InlinedAllocation { public: + enum { HasSwap = false }; + template class Data { @@ -267,14 +270,9 @@ public: } } - FORCE_INLINE void Swap(Data& other) + void Swap(Data& other) { - byte tmp[Capacity * sizeof(T)]; - Platform::MemoryCopy(tmp, _data, Capacity * sizeof(T)); - Platform::MemoryCopy(_data, other._data, Capacity * sizeof(T)); - Platform::MemoryCopy(other._data, tmp, Capacity * sizeof(T)); - ::Swap(_useOther, other._useOther); - _other.Swap(other._other); + // Not supported } }; }; diff --git a/Source/Engine/Renderer/RendererAllocation.h b/Source/Engine/Renderer/RendererAllocation.h index 22e7518c6..2a4df4b5f 100644 --- a/Source/Engine/Renderer/RendererAllocation.h +++ b/Source/Engine/Renderer/RendererAllocation.h @@ -11,6 +11,8 @@ public: static FLAXENGINE_API void* Allocate(uintptr size); static FLAXENGINE_API void Free(void* ptr, uintptr size); + enum { HasSwap = true }; + template class Data { From 8ff4f95cefb7879aeba5e20f209cb5e09ac16ba5 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Nov 2023 16:02:51 +0100 Subject: [PATCH 22/79] Optimize some code and cleanup code style in natvis file --- Source/Engine/Scripting/ManagedCLR/MUtils.h | 14 +++++++------- Source/flax.natvis | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Source/Engine/Scripting/ManagedCLR/MUtils.h b/Source/Engine/Scripting/ManagedCLR/MUtils.h index 9a79a83c9..ed1b690c2 100644 --- a/Source/Engine/Scripting/ManagedCLR/MUtils.h +++ b/Source/Engine/Scripting/ManagedCLR/MUtils.h @@ -118,7 +118,7 @@ struct MConverter { MString** dataPtr = MCore::Array::GetAddress(data); for (int32 i = 0; i < result.Length(); i++) - MUtils::ToString(dataPtr[i], result[i]); + MUtils::ToString(dataPtr[i], result.Get()[i]); } }; @@ -151,7 +151,7 @@ struct MConverter { MString** dataPtr = MCore::Array::GetAddress(data); for (int32 i = 0; i < result.Length(); i++) - MUtils::ToString(dataPtr[i], result[i]); + MUtils::ToString(dataPtr[i], result.Get()[i]); } }; @@ -184,7 +184,7 @@ struct MConverter { MString** dataPtr = MCore::Array::GetAddress(data); for (int32 i = 0; i < result.Length(); i++) - MUtils::ToString(dataPtr[i], result[i]); + MUtils::ToString(dataPtr[i], result.Get()[i]); } }; @@ -217,7 +217,7 @@ struct MConverter { MObject** dataPtr = MCore::Array::GetAddress(data); for (int32 i = 0; i < result.Length(); i++) - result[i] = MUtils::UnboxVariant(dataPtr[i]); + result.Get()[i] = MUtils::UnboxVariant(dataPtr[i]); } }; @@ -250,7 +250,7 @@ struct MConverter::Va { MObject** dataPtr = MCore::Array::GetAddress(data); for (int32 i = 0; i < result.Length(); i++) - result[i] = (T*)ScriptingObject::ToNative(dataPtr[i]); + result.Get()[i] = (T*)ScriptingObject::ToNative(dataPtr[i]); } }; @@ -307,7 +307,7 @@ struct MConverter> { MObject** dataPtr = MCore::Array::GetAddress(data); for (int32 i = 0; i < result.Length(); i++) - result[i] = (T*)ScriptingObject::ToNative(dataPtr[i]); + result.Get()[i] = (T*)ScriptingObject::ToNative(dataPtr[i]); } }; @@ -343,7 +343,7 @@ struct MConverter> { MObject** dataPtr = MCore::Array::GetAddress(data); for (int32 i = 0; i < result.Length(); i++) - result[i] = (T*)ScriptingObject::ToNative(dataPtr[i]); + result.Get()[i] = (T*)ScriptingObject::ToNative(dataPtr[i]); } }; diff --git a/Source/flax.natvis b/Source/flax.natvis index fc545474f..43de321ea 100644 --- a/Source/flax.natvis +++ b/Source/flax.natvis @@ -34,7 +34,7 @@ _count _capacity - + _count _allocation._data @@ -153,22 +153,22 @@ - + {{ X={X}, Y={Y} }} - + {{ X={X}, Y={Y}, Z={Z} }} - + {{ X={X}, Y={Y}, Z={Z}, W={W} }} - + {{ X={X}, Y={Y}, Z={Z}, W={W} }} @@ -231,7 +231,7 @@ {{ Length={_length} }} _length - + _length _data From d3a77c7a55a1ab14266a649b7c749ce74177e05e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Nov 2023 17:38:06 +0100 Subject: [PATCH 23/79] Fix regressions --- Source/Engine/Engine/NativeInterop.cs | 4 ++-- Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index 9548c3f59..b07facf4d 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -1133,7 +1133,7 @@ namespace FlaxEngine.Interop marshallers[i](fields[i], offsets[i], ref managedValue, fieldPtr, out int fieldSize); fieldPtr += fieldSize; } - Assert.IsTrue((fieldPtr - nativePtr) <= GetTypeSize(typeof(T))); + //Assert.IsTrue((fieldPtr - nativePtr) <= GetTypeSize(typeof(T))); } internal static void ToManaged(ref T managedValue, IntPtr nativePtr, bool byRef) @@ -1180,7 +1180,7 @@ namespace FlaxEngine.Interop marshallers[i](fields[i], offsets[i], ref managedValue, nativePtr, out int fieldSize); nativePtr += fieldSize; } - Assert.IsTrue((nativePtr - fieldPtr) <= GetTypeSize(typeof(T))); + //Assert.IsTrue((nativePtr - fieldPtr) <= GetTypeSize(typeof(T))); } internal static void ToNative(ref T managedValue, IntPtr nativePtr) diff --git a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs index 4476c380a..5e63f489d 100644 --- a/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs +++ b/Source/Tools/Flax.Build/Bindings/BindingsGenerator.Cpp.cs @@ -1557,7 +1557,7 @@ namespace Flax.Build.Bindings for (var i = 0; i < functionInfo.Parameters.Count; i++) { var parameterInfo = functionInfo.Parameters[i]; - if (parameterInfo.IsRef || parameterInfo.IsOut && !parameterInfo.Type.IsConst) + if ((parameterInfo.IsRef || parameterInfo.IsOut) && !parameterInfo.Type.IsConst) { // Direct value convert var managedToNative = GenerateCppWrapperManagedToNative(buildData, parameterInfo.Type, classInfo, out var managedType, out var apiType, null, out _); From c6017a21f30fbad2ad30756c22e5c35163ce468f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 28 Nov 2023 23:19:47 +0100 Subject: [PATCH 24/79] Fix constant value sliders in material graphs to not be used due to shader compilations --- Source/Editor/Surface/Elements/FloatValue.cs | 4 ++++ Source/Editor/Surface/MaterialSurface.cs | 3 +++ Source/Editor/Surface/VisjectSurface.cs | 5 +++++ 3 files changed, 12 insertions(+) diff --git a/Source/Editor/Surface/Elements/FloatValue.cs b/Source/Editor/Surface/Elements/FloatValue.cs index 7674c68b9..56539e3e3 100644 --- a/Source/Editor/Surface/Elements/FloatValue.cs +++ b/Source/Editor/Surface/Elements/FloatValue.cs @@ -33,6 +33,10 @@ namespace FlaxEditor.Surface.Elements Archetype = archetype; ParentNode.ValuesChanged += OnNodeValuesChanged; + + // Disable slider if surface doesn't allow it + if (!ParentNode.Surface.CanLivePreviewValueChanges) + _slideSpeed = 0.0f; } private void OnNodeValuesChanged() diff --git a/Source/Editor/Surface/MaterialSurface.cs b/Source/Editor/Surface/MaterialSurface.cs index 08e1161d9..ba7f2e01e 100644 --- a/Source/Editor/Surface/MaterialSurface.cs +++ b/Source/Editor/Surface/MaterialSurface.cs @@ -22,6 +22,9 @@ namespace FlaxEditor.Surface { } + /// + public override bool CanLivePreviewValueChanges => false; + /// public override string GetTypeName(ScriptType type) { diff --git a/Source/Editor/Surface/VisjectSurface.cs b/Source/Editor/Surface/VisjectSurface.cs index 0185e364a..3ac702549 100644 --- a/Source/Editor/Surface/VisjectSurface.cs +++ b/Source/Editor/Surface/VisjectSurface.cs @@ -534,6 +534,11 @@ namespace FlaxEditor.Surface /// public virtual bool CanSetParameters => false; + /// + /// Gets a value indicating whether surface supports/allows live previewing graph modifications due to value sliders and color pickers. True by default but disabled for shader surfaces that generate and compile shader source at flight. + /// + public virtual bool CanLivePreviewValueChanges => true; + /// /// Determines whether the specified node archetype can be used in the surface. /// From b7e4fe3e857d68100b21e9561906cf916281e8bc Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 29 Nov 2023 12:28:19 +0100 Subject: [PATCH 25/79] Add automatic code modules references adding when cloning plugin project #1335 --- Source/Editor/Windows/PluginsWindow.cs | 53 +++++++++++++++++++++----- 1 file changed, 44 insertions(+), 9 deletions(-) diff --git a/Source/Editor/Windows/PluginsWindow.cs b/Source/Editor/Windows/PluginsWindow.cs index 372505795..f43fb0342 100644 --- a/Source/Editor/Windows/PluginsWindow.cs +++ b/Source/Editor/Windows/PluginsWindow.cs @@ -391,7 +391,7 @@ namespace FlaxEditor.Windows } Editor.Log("Plugin project has been cloned."); - + try { // Start git submodule clone @@ -412,24 +412,28 @@ namespace FlaxEditor.Windows } // Find project config file. Could be different then what the user named the folder. - var files = Directory.GetFiles(clonePath); string pluginProjectName = ""; - foreach (var file in files) + foreach (var file in Directory.GetFiles(clonePath)) { if (file.Contains(".flaxproj", StringComparison.OrdinalIgnoreCase)) { pluginProjectName = Path.GetFileNameWithoutExtension(file); - Debug.Log(pluginProjectName); + break; } } - if (string.IsNullOrEmpty(pluginProjectName)) - Editor.LogError("Failed to find plugin project file to add to Project config. Please add manually."); - else { - await AddReferenceToProject(pluginName, pluginProjectName); - MessageBox.Show($"{pluginName} has been successfully cloned. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK); + Editor.LogError("Failed to find plugin project file to add to Project config. Please add manually."); + return; } + + await AddModuleReferencesInGameModule(clonePath); + await AddReferenceToProject(pluginName, pluginProjectName); + + if (Editor.Options.Options.SourceCode.AutoGenerateScriptsProjectFiles) + Editor.ProgressReporting.GenerateScriptsProjectFiles.RunAsync(); + + MessageBox.Show($"{pluginName} has been successfully cloned. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK); } private void OnAddButtonClicked() @@ -749,6 +753,37 @@ namespace FlaxEditor.Windows MessageBox.Show($"{pluginName} has been successfully created. Restart editor for changes to take effect.", "Plugin Project Created", MessageBoxButtons.OK); } + private async Task AddModuleReferencesInGameModule(string pluginFolderPath) + { + // Common game build script location + var gameScript = Path.Combine(Globals.ProjectFolder, "Source/Game/Game.Build.cs"); + if (File.Exists(gameScript)) + { + var gameScriptContents = await File.ReadAllTextAsync(gameScript); + var insertLocation = gameScriptContents.IndexOf("base.Setup(options);", StringComparison.Ordinal); + if (insertLocation != -1) + { + insertLocation += 20; + var modifiedAny = false; + + // Find all code modules in a plugin to auto-reference them in game build script + foreach (var subDir in Directory.GetDirectories(Path.Combine(pluginFolderPath, "Source"))) + { + var pluginModuleName = Path.GetFileName(subDir); + var pluginModuleScriptPath = Path.Combine(subDir, pluginModuleName + ".Build.cs"); + if (File.Exists(pluginModuleScriptPath)) + { + gameScriptContents = gameScriptContents.Insert(insertLocation, $"\n options.PublicDependencies.Add(\"{pluginModuleName}\");"); + modifiedAny = true; + } + } + + if (modifiedAny) + await File.WriteAllTextAsync(gameScript, gameScriptContents, Encoding.UTF8); + } + } + } + private async Task AddReferenceToProject(string pluginFolderName, string pluginName) { // Project flax config file From eb508fdc73fed17db247e37af43b8a1db24ddbde Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 29 Nov 2023 12:28:30 +0100 Subject: [PATCH 26/79] Fix Json serialzier regression after hot-reload from 0f14672e3bd22d82699c434d2e0bf2f24815c8fc --- Source/Engine/Serialization/JsonSerializer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Serialization/JsonSerializer.cs b/Source/Engine/Serialization/JsonSerializer.cs index a5d6e0771..076e8abe7 100644 --- a/Source/Engine/Serialization/JsonSerializer.cs +++ b/Source/Engine/Serialization/JsonSerializer.cs @@ -19,7 +19,7 @@ namespace FlaxEngine.Json { internal class SerializerCache { - public readonly JsonSerializerSettings JsonSettings; + public bool IsManagedOnly; public Newtonsoft.Json.JsonSerializer JsonSerializer; public StringBuilder StringBuilder; public StringWriter StringWriter; @@ -33,9 +33,9 @@ namespace FlaxEngine.Json public uint CacheVersion; #endif - public unsafe SerializerCache(JsonSerializerSettings settings) + public unsafe SerializerCache(bool isManagedOnly) { - JsonSettings = settings; + IsManagedOnly = isManagedOnly; StringBuilder = new StringBuilder(256); StringWriter = new StringWriter(StringBuilder, CultureInfo.InvariantCulture); MemoryStream = new UnmanagedMemoryStream((byte*)0, 0); @@ -114,7 +114,7 @@ namespace FlaxEngine.Json /// Builds the serializer private void BuildSerializer() { - JsonSerializer = Newtonsoft.Json.JsonSerializer.CreateDefault(Settings); + JsonSerializer = Newtonsoft.Json.JsonSerializer.CreateDefault(IsManagedOnly ? SettingsManagedOnly : Settings); JsonSerializer.Formatting = Formatting.Indented; JsonSerializer.ReferenceLoopHandling = ReferenceLoopHandling.Serialize; } @@ -149,8 +149,8 @@ namespace FlaxEngine.Json internal static ExtendedSerializationBinder SerializationBinder; internal static FlaxObjectConverter ObjectConverter; internal static ThreadLocal Current = new ThreadLocal(); - internal static ThreadLocal Cache = new ThreadLocal(() => new SerializerCache(Settings)); - internal static ThreadLocal CacheManagedOnly = new ThreadLocal(() => new SerializerCache(SettingsManagedOnly)); + internal static ThreadLocal Cache = new ThreadLocal(() => new SerializerCache(false)); + internal static ThreadLocal CacheManagedOnly = new ThreadLocal(() => new SerializerCache(true)); internal static ThreadLocal CachedGuidBuffer = new ThreadLocal(() => Marshal.AllocHGlobal(32 * sizeof(char)), true); internal static string CachedGuidDigits = "0123456789abcdef"; #if FLAX_EDITOR From cebd28c3a70eaf5bb8dea7574e253e59f895a85e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 29 Nov 2023 18:46:18 +0100 Subject: [PATCH 27/79] Revert fd938e8284177f1c6f28006bc1aee69f5adaa4fe --- Source/Engine/Engine/NativeInterop.cs | 8 +++++--- Source/Engine/Scripting/ManagedCLR/MUtils.cpp | 15 +++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Engine/NativeInterop.cs b/Source/Engine/Engine/NativeInterop.cs index b07facf4d..fc04d9668 100644 --- a/Source/Engine/Engine/NativeInterop.cs +++ b/Source/Engine/Engine/NativeInterop.cs @@ -1112,9 +1112,11 @@ namespace FlaxEngine.Interop internal static void ToManagedPointer(ref IntPtr managedValue, IntPtr nativePtr, bool byRef) { - if (byRef) - nativePtr = Unsafe.Read(nativePtr.ToPointer()); - managedValue = nativePtr; + Type type = typeof(T); + byRef |= type.IsByRef; // Is this needed? + if (type.IsByRef) + Assert.IsTrue(type.GetElementType().IsValueType); + managedValue = byRef ? nativePtr : Unsafe.Read(nativePtr.ToPointer()); } internal static void ToManagedHandle(ref ManagedHandle managedValue, IntPtr nativePtr, bool byRef) diff --git a/Source/Engine/Scripting/ManagedCLR/MUtils.cpp b/Source/Engine/Scripting/ManagedCLR/MUtils.cpp index 52d049403..1bbfa2a6d 100644 --- a/Source/Engine/Scripting/ManagedCLR/MUtils.cpp +++ b/Source/Engine/Scripting/ManagedCLR/MUtils.cpp @@ -1209,9 +1209,20 @@ void* MUtils::VariantToManagedArgPtr(Variant& value, MType* type, bool& failed) return object; } case MTypes::Ptr: - if (value.Type.Type == VariantType::Null) + switch (value.Type.Type) + { + case VariantType::Pointer: + return &value.AsPointer; + case VariantType::Object: + return &value.AsObject; + case VariantType::Asset: + return &value.AsAsset; + case VariantType::Structure: + case VariantType::Blob: + return &value.AsBlob.Data; + default: return nullptr; - return (void*)value; + } default: break; } From 7f87e9794b331a87481a323527df0e966259d6b8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 29 Nov 2023 19:12:58 +0100 Subject: [PATCH 28/79] Fix job system buffer allocation data --- Source/Engine/Level/SceneObjectsFactory.cpp | 1 - Source/Engine/Threading/JobSystem.cpp | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp index 985da6c29..2ae947c85 100644 --- a/Source/Engine/Level/SceneObjectsFactory.cpp +++ b/Source/Engine/Level/SceneObjectsFactory.cpp @@ -166,7 +166,6 @@ SceneObject* SceneObjectsFactory::Spawn(Context& context, const ISerializable::D return nullptr; } const StringAnsiView typeName(typeNameMember->value.GetStringAnsiView()); - const ScriptingTypeHandle type = Scripting::FindScriptingType(typeName); if (type) { diff --git a/Source/Engine/Threading/JobSystem.cpp b/Source/Engine/Threading/JobSystem.cpp index 0dd16b3d3..c89e6aca5 100644 --- a/Source/Engine/Threading/JobSystem.cpp +++ b/Source/Engine/Threading/JobSystem.cpp @@ -105,7 +105,7 @@ namespace CriticalSection WaitMutex; CriticalSection JobsLocker; #if JOB_SYSTEM_USE_MUTEX - RingBuffer> Jobs; + RingBuffer Jobs; #else ConcurrentQueue Jobs; #endif From 712c400e432c6f3649567cc17b95cc6d06497279 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 29 Nov 2023 21:51:07 +0100 Subject: [PATCH 29/79] Add new mac icon --- Source/Platforms/Mac/Default.icns | Bin 176267 -> 252565 bytes Source/Platforms/Mac/Icon.png | 3 +++ Source/Platforms/Mac/IconAlt.png | 3 +++ 3 files changed, 6 insertions(+) create mode 100644 Source/Platforms/Mac/Icon.png create mode 100644 Source/Platforms/Mac/IconAlt.png diff --git a/Source/Platforms/Mac/Default.icns b/Source/Platforms/Mac/Default.icns index 911276d8ee57429688ff8d8979e4b479a509c53a..92394b74bf612b01f71472c6eadbee483c524018 100644 GIT binary patch literal 252565 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


52)AEVy{E}9+leIW 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 z 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ⅅ`q+h;G?@4Xm#RyMIC2~z(Q9| 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 z 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ⅆbo2SYSxSs!b_e3iF!khIKxj3jD+DIUkMIPHl+e}&;k z&_7u7j8TQCSkfce2Z3JuVX?=BSg#S+2Fj7x?~GW3-+{yKG)>{k*T2sCn6BTeNMg@a zWp9IIhT!npgespy=1b*+ptHZeDx@ zRIpq@ovP(XkEh08M#>L_2c|rg?qy<(Yj31x3;Pz=+42_1aFVfI(!3BxPkGgSjM75h zlQ&NS&C@nhH0ajGE?Bn#51V-+L!TJqz5ZR!DcP}b?lnmBJLXgI;Pw|-olHG<+gXYNzaNK@5xLG{KHYK zBd8QJ9z6Bu7^bON3)W}Wdi{h(=D>D6(#`MDH?sTtv;{t+q6y(HSEQU;Bp$MbT)t z5z45qRcy^Q0goZoJNPxd2MWp=4?d6)5!yu8oxc^KkVQ{Ld=4$9QTS55XozAYxZa|% zAT|b?TJGq%c|A#W&yKH+~xUjI)1L?48FFNdBS7r<5}8&!iDxbdTDw=6ix&Z&1*5J!~d`n*e6KOzG)W{p3*G3gxXi9cm-{WpZ_g6my2> zg6`d07TW^|@fN+q5bst0wDju^?2p5SU(*XnX&>R;{UCF2-*yBy0KH0FRZM4rg#eQy z$R{2$3W-C@}5qa^-o6xWLG_^pq==#{D`j`mTOxpr|fm$Hkp&W2_Hbk_0(Y!dEqP7V* zv6%kNKBVF+1TEe-g2GzyAteSLv=1i>K9Pw_PSs#{_+znu6I?6~gk}6H$_MhxyrmI76KfpHWjsPAnM~vBPl>*^ zGa^<(E;0b!0Pnfl_o8^{f~k-oqASEjvPVexqiE2sESC?-X*}{%bJIVR9zO-d;dHLn zTjNd9!y`D#mF(VD`$_%FLfMBqc@rKKXpT=a95k|;+(;m%w|N(W3eSz~LY%AYZ*{qh z{=+)@afwe}keVK0>it>df#?V1x-f{Q99G)dr`|A!>bSyK6rO#ld}keiCihz|T-pFC z>!p7!QSpwB(%KzpAIfkQCm@EPC|jUpZ{t@YU>AS>5s3tss$F{zoY6?($e%qfOs<6Ib#bP33Z3e2RU0IW?AF`;_X#Jdu zg(_ls2x6Dye5D$={xbeo2*Tis>bHpcEuwylsNW*$w}|>JqJE30-y-U_i25y}ev7EzBI>t@`YobTis>bHpc zEuwylsNW*$w}|>JqJE30-y-U_i25y}ev7EzBI>t@`YobTis>bHpcEuwylsNW*$ zw}|>JqJE30-y-U_i25y}ev7EzBI>t@`YobTis>bHpcEuwylsNW*$w}|>JqJE30 z-y-U_i25y}ev7F8|A(kQ0RV8uvscm*5P;aIIN%zi*oYV)849@u50MyzUV{uuLa%|6 zOY{ewC_(=MHn=&9F+}oje#lLJ06=E~{H`JAYis90*O=Q;3cNfoBDVdnv3^UFQP6Vh zh&})9dyTU##iZz6wc5)|BeU&ujT7$h=j{4=L4=W&`V->%>6x@DI=T0Xo|Tze0e!U2 zOa1Bk!jhbpQAkf@?Iz8{F&4(E9@@GS!i`R!DKIk1mCYfpA-UA@XNYU)Cu&yu0(7#C zo$?d9_-z?FX5P8$DFI5EZC|t`GSrk7t8yeVzy1G_kY7zYYCGsg=B0B#wD>nF{GTG5 zn>UK=ChN(|Y9;QX6`9uN=HZjJdi`b^t;nvXkbYR>#*o}oon3TI34XKZ*VoG`LSnor zH;QN*qmu{5{9+=!dg!A~dES)k3(v=VLN9GzZQP`Vr6%LWOyjzDW286His%Um>eer! zCI7~&ccVxOub4muI@$Y-Ck0*nt`5J5{NgoA>50xRT9IiTULN=Lx12iDXhqNxDw=2~ z@7^e~vHJh3$mP+|C0db_zJj#W+};zkB3pUUpJP+XvvRl4ik#%c7qm?uE~mxhouF&# zY23cNzTVG_PmJkDD{`4vj82{w$0a7j6rhie(qj6rFYBUX<1;EU4{y@($(ucirQP!< zT`5P`*OvwH@%exD(2|d#^P?5%k4cOhM<*A)eC|gVf07rQkhXWdlm0pH1g*$cUd-pZ z!~9QqTWCel5`Hbo>pMX!a(Q%c`Cp5e8FHgtH0nvnOC@PMYzl026XD6qNWDV4E&31) zz+vdGLziD?=r52x+K++aL8>)$LdsZE_PwGaz=l4C1E3%?0D6-J{h>gA005E(0wCyb z(9LgY;Q!8ofi%ef9p7ZcIjsx;0C7N8>e*`-(55Fw5>?j^PuVUy>h+iX{YJqF)OPzA zVl1}&*NZqpw(o?#8Gba@e(V?axZ8pp1IEq6b1)q;=kV}yW58_b=bFB=wUcx9N~KqZ zcKq6!*R+tX?oobzLa9HMk7|xm$GYODqvlzDk$Llr@E_y+T5E(w(t$KsZkZcD6p*oQ zOzS>79;s{c1^C~^Lilpzv#*}(=6KNlIt?9qbtta;PznJj@Tnu<|4K{q12U8{iIJ+d z`mL#x^t4bDnc;?V%%VA+aoz@bZq>tv()`>$ulm^Uy1Ir#PB?yM5PN^&H4i+{ln4f3 zAkY&0%vKfB;4Zms)F)fCdAEy9{8pNwbG% z1kh5{0`JxQrL72^j=1J7bwG|D>DsKJ8&&JqR`A<$bY2Vu4EXt+M(F*+bK_@QG?GWF_Gg|ESBgLV0^kH7265<=A5Z~vKSI!zN+6lt3darWu`vk9g!P2k zfuy&)?(4Yx*M~nOuO$FPc*_4S>6ZlV2X48E%X7&iYuyS}eSmC?p?k_$K;5eIIANm< zvfywvN7_V06Rj;BSgN@m-fU6&8Y%16|Jp!5StE_>Ein9?*6E!6@HCdj?+`KlU%|oR zmQZmvox-jlH;>|0KmpcC!iWLr9=0@hrF&=4yxeQ6jQF1n!H6a}f`v;LIer*l*a}QB zhO@mu3$R-~{^PHnh*vJ==J`L(d!u6F$7Zb}{fGGXpjEt;k8^aa{Q(=dx$79;GgXo1 z$^Y3A#Guay#?YE`cN5O@1^mGpq#8JY&Z(}*$%G5Kj9`Q|A)tY}CqkqCOut@RXig4< zstCWnez1C-gXw%>mF#K3_m8sE@XfJhv8*n8Hu$E!wuTNfu-b~`Kbak0NOfzrPW?}O z@k~O^BD@-9B%c~VZbB;a$kvD5t71aHVUIRVFbY#Rx9ct&oL^8jDg$l43(l7W z(yKmA|IZ#%zRebvtB=^As1nA|8v~z4-9NF;xnm0auf-7X8a$9Gusyn9dk-4fFs#yH z3r28U!KcqlX~qSxA3P2GPcR#}!f@YJrUdYbKp>l64jAsZhCN?a5s8dJ{--J80etgU z#Dz)#;v{*5UK);Wq%JnP>UG;tnwQt*|M(zUZZ13@qODV_`s*KqOHufZ|F*g)Os$ykb5PSr+T@H;t!Ybqt=CY&DC(IqLZc^b1-$z(~_1$UFKgK(@5$29RGD2g>F`t?^Jz2J2>)3 zr_a_<$LyfWJKcZlLK#B?Kdht*G;~e|`W1l@RoD+apIsRK`+&P~>`h64+}^spM3C^0 z0>GBwAxU{$MRRymZSSv1dee?K&+!jX0@hWb57?)}_~I5fH_}de_+JTRmD7`tj{kWX z9oUhfR8p+rRR99d(TlP zp;@5e>1*+ZITJS{sJr1?>HlRwz@L!&WN5W@$k@Pl2V;9rn=6AZG=K_jk>gu&(t%y|~p< zHB8+3nz~~CjoTuI9VF$yyAA>O$M9ij=;Im%TO6!d`IrKz3()?d&diT1qb^OWat>9-25SD>T3xl3(IxWaG>9)pPviQi zp(}4JVRQv$DYEogT~9Y(57ltgd!~H}+c~P$_w*TOZ9Exx9W0{|9J|=ADNi7y6c(j$ zk6_|`}xC>X)6qca2#|)K4k*#k_FajcdtDAd{&J>!4ar%|!tLectMYetZamKB%P&pPH)=G?-5lDKc^2$rbQAy8vfaJgbO)RX)4gh zf+oVkImcMH)E8%x?djJkG_!>Va$Yh};b4Fy*HQ@C-B}c5)RQmqrmk-I)dnfaJ#8tW zhsR?PSBJhk&w?#>9#=q2@H@or3(9Fdqw6+*;OS=6gK88Q$&#$Z@&Ws7?UqY8GwyzS zdi~|BKKJKQ4m~KX{B4cQop_LDFv5h8b-2TGEBmrcf9>s??`7guJE9+cZ`0%Io6wHx zkRPl+*4RPzly?VYFJPaW`Jgh7NJG7{iExBigY?rrzylA5Vyu!MGjR$DJi*g1l4=%# zQ~k>J#jW{Te1H7Be7`|YY0;LFc>d#zEPn|9h$l?|GM(dm?(Hz{~Iz;PEW1x;kGnjBq2{X{0+k! zunL5n8}-DHI!1yT%9yn)F&;`a>qGI)XCrA*R6OFz%KW3i#VbyiF@5m~UL?cI3O!O! z-OX>SoLb+;wGItbUrcB}yl}jt6aE+zGTo^u$piQRMv8EV-}z#93~6-^m;OAWTv-(A zRRnGpFn3EH61oR;uBq{z7BA{Cpx#3xs5#?m<(>L+epdCq(>-)vSE9uMIsJXTRqtx_ zW#*V|6#I1w2~2B+sV}Xu)^6IEIz}em1cU9v8MP*a&;|$TzY99cW?yZxT;i{bMImR=4ak9A2f)W`Sr&)sUDNiRC^p`WPIVy z{%$BeP>a`A3VRY(0XW{Z!W@5-a2gDPL`XS`$gq~P(FzgvrzbFxr#*ecN2m4f%mB}?=NwpDHM;36dvCh_26*!EyKS#S) z3%a9Rg;&x8$$bRi4&Pl~CEP(Nm738=0nnlZ9H*Wdtc{|kpomzx!ZQc^>YbJzR2qlj zTXj|qx-8=)9sbPl3)B}8-hKVz4k6o@Jimz<0I1xNxF*V2l>Y+`FgwmUv+LQ@Bt^T*Q zTf21NSq%{CkzSMe(4bEv=V=^ViU{=ZVqrAEdh@)%I6VJ?@GGXu)ckxp!|#VPU*Nw& z0O+fJ#du5-jz5f{yhy30w{P}~%Ug!tU?1TX}V^8G-fat@% zg0?BTnFx5&Gt}_htAr<>q2I5rDBhIYHr%-5K#b4t8b#|S!zngtgYHsbP%0gQ)lcm71?S&mM_uMe0UjGNVF5uqA?@M>z!=0Y z5#0Rz%u8!v=#UqO5RL`zU}0Na7~)g=bDFn)2dlI2R|5PL>hpG5t4DEm?U7!6I!^7Q zK`J&?ZO3oefVy}Z`g&w#HRqQ}!{a`3I9+`2V!|JzLvwlK-|Gx!*^3V*Pm{TyT0D0l zM4Z#nH1AT>tO9>VuE~!-IQp*n~ ziyD895{|YyTF(T*`SC#TMmih@k)si{fY?8%p{7}lz{5UAE!E#NT?PQ!_4-cx1ZJ}Y)Z!T``0=?Ob+z2qg?VP$YRx^S zEP)i4n*sH&5QJ!GmV%$Iu`yG(;_MdV?4{P7I{dK^VlfLa_~$6p2CMi43$OttOF=s2#_#Q~Ow`qq%UX}!(ib+F2;m?woO4Ry zs9ZyL>efLNMNC5U`&^gYN0mx;O4g`DWP!3<8ku`~=6?qdO@>zF)W z)LBZ7*}&zb+p#J`9Ffcg*bZGKm-zJH;aqw=?kU(;yqZN1R;xF`;?3|} znFpl#09O|a;2|&O!ioCe;*&$jI_WKu^2+hLeqmf*_6=|S>`z4LffOAs{IkYu*>@gc z9w!^qkGy~b9Dk6*ft9TvHb8qi1(08je+S{4Aw`p`)LyI?d$*1G#n#alE8H}VKMm+& ztXJ*s?ua%!#$>hF^}FC0TdqUrWoldrI!wcH9rXl>F@sGQ^)1ilu8|}8 zXLqhyz(aWVjiz>OXkjU!I(9=;vgTCi zz0m{#gUwSgL4TCtLPxcF_mFi;Zci2Vkf_g2aC02oXf8_`!X>Y#x4Y!*vE&GCe&V&1 ztVS_l{f$bE)_a2T4SK+9g9$#pWKeJ3@p>%db=Dx--!KvGON=LEQ4k%R6OprNTy+b^;Yi zUn*rTdgnfZU*w&n6Q%kl8#KFrhC}S{PZxH3y~6FY+SB?ErxThCHZahIQ^x2+E|qed znbUKmaoJVXo8V?MXyi2uIR$VEVZgO<7ts{{C~o#9OIXOMmPS6SqVQvt7jh8d^hW{e z0d3KnozV08+vY{`X1G74Ov8(zRB=e?4p>~s69l5uxI=P>p;SZlXy&q;Dk0O#i;l>4 ze5XaWnS&AuD1eO{7KSi$a3a}OVvw&=s`>+p}5ntAFDKCV~4HYf0qbk74$#OO_Gsc zTxA|x@>hA1IdWXVkPZXIEi=f8Im4N^U#q-0u6V!AnKPi6p>(Hh4|iKB`S%G-T&KfNrIyZdFGP2vUwj}t0O(a1Zc%nPA=E= za>U8N91DnQN$rpT(ECOEZBCj&8__{uv0Yxulb;yD<-X`KYdJx%h8=1$9P^NzTXwfl ze-8>yAOK{pcsvUAMkY|+b@pXcf;zpCje$tsdTFTdb6~D}*RVBuw+Q84B4)(D|{A@uzpcMao_y3dAF2LtO6-On&y8jiYesd6m zfmM&fuD}=@KpGHYf--pN<=nFL8R_ONiw1wee7p9)(oh@|ZW@y-2Id(RYkCgf7T0tG zE+q?D5sp44Aqcy${x_&Z3=2G@)u!h9&B)*-NwZd89=nV_*6u+Se7fF{z~vLL8dS}S z|M4T?8|;~AX$z@kl~0Nrr6jj;GTV(u;J~`4Mz9K7) z=-Wy{e#?sFBWY)z!!K|fHx=aB1o2Ls-=FS8Rs^c_n|m5E*q`Yy;Ir68l0ZiZo*a93 z+>J9$Lsw=cQ3dTKEC{`3KFw8$!9qN$@pWh9P~UM~Cl%$VQN1JnYo4!k_hDzgA<2OI znenZgBXf)Hu=UBOq?31Xb9jy)VuaT}TZl6+iw<@a-j2Vyl zQQ8Q>ctL*Fnim6eqf8}1HPL<~dD zN0S&HTq~|F8#9=tLeL%mIA<`vFVLIk@@8hJC;fKux z8lE=!6e|7>Fay_gn#K3yy2GBPK;7Erocuy+oFv+l?Q`GTsw4_NDm+^r4Eq z1^M;rj}k70dH7X~Dvtd;g;>8YsTs^=Md zd$-9sB6I&v?o z{=oqlT~1jfa!bKy6~P~$O!OT8Vb7(-OIQHHkY1Tr=|O}BS9|sJ8+e|EV(Mtwkw7KI zBt_8}DCIh64f!nIbQw@CNU1JtHN+;Tlg&o+T8`nVl%Ay(XgVeQG_LlmB7^?b#B7d~ zm9C~&xD-`F;nrVt^0!#*Zo>#fM0$&TIFL#bLrP1pw`5BL9`}ltyXJplzB;h%us)ek z3HNatrsnRmG&ZsZ#og=d_xu8ia0=*4UPF zw{SR0mQ!=1ad0&H1YwjD1bn(y+hYnej9)krYfwsfl+b6@kQ#Rx|M~MB-bVsj?nl@| z%-h<}y^!z=di+pXgowrj2OWDTQ2jxFag~GP(nYN0+Q<8R-jfcRtY;iRUId2}^koI1PJ^>NP7pMn?KC z-LzTs%_+AL6w_FdGko;%7)^ZasNL|5YQ zC0l>KPF<$W;(I10ebuf84^C`B`k7^08kuxP*#`e%oO{V6L)&(z7-2(+wWM2QkEsJi zysM~eu-B|*)F+&)eWGfO#y6)HQ-m-(pVDzh*r>UXY~)3{v$ z#Ou^jdYpauy;x_0fnm{@)&TM#M>p^*_!|OngeGE}?3UHDX`CzEv?iD%%n9evh!IBi zwCkdxPLUq_l7(VO)g*)bng?9mmw%<_=JUDf)g~qI`&xV(7JH+mbo@@7>35WCiEcA> z+J9W*Td)%dHe09Qc<*-Fxv}e`y0DJp(m~Nkz_{Td?YU63`)K@c4A!5{$a47vQzUy| z4wb#Z-!iFn`{V6<_wk_7PU{rqowBw&?2vk`E&o!mQWLJ(v2G|yKf%xQ&5@TD-&5)o z!G=5|*ezPW8ws;}CN}9m`?JnrK&YEu8bVJoCF?gD9*-4q9Lk@myhCkUuM2xYfCx0^ z8*xyVja)dH%&!WWrxD~{{n{CtEACL6r5_1XX?PIvPocHC$KW+q{P}Y8EB4~gUQeS# zov3&84?Oz@Sin1KwR;l>I-XYpDocp0u1WOJgO3Cdwvo!)4Snwob3!Zajt~`W``#KAeyj3LFtq0%Lc>E6_|nYKD3XsuZWzR+9Zq+ zul}~kNcZK@JDn=I7kDbUb0_!5jNnTi$Equnli)tbD3Oxv+4rzXFw;+6KEB*vYK!vr zDQG|WLsj}P{-yA@D#G})U@Y*m{|MoGjG{zg0+{#lI7GLlS+;iiNC8jr?X)7 zNkQ$uvAv*#m`qTC$P>E*lIN03k$xjqxiUS&U#MQ zgki>C{vo@*CS*ladE2Wxm7Ia$r;|q)xYHFnj&QK~j7d0Ileepk}DAu$v2yj_sUx3Y~OZQ3n8J>Pw->6 zX`2<$hCgN4S_;T8szD*rSDEZ_vL>G0&NtH>#FhsGD9?uEYzf4DjqmRXdq`Cwm^op8 zXDW#IuSH5{#uK7%GR*eH|;yyv$0|x23?b zTMIJ0h4=Hm%Y&hUH&v*bJMGFlZU2IxP1@ANf4EWV#nqn_J4k@^KqKO+j;k6GG9DJA zw=i}&`VW{NvZf6E<&#M3Xk9BhSjKk>upeUmTU!5Z>$P!*q&*Tn%uY=o9lqW!7BlD5 z?q|KO#W?grV^}LZx{MQh^vx5a`eee^aw=z|HrUyvVB~LIFmSa^AZLRa*}<`YousyB zhtF{(j@ulR^&oAzhff{*++Gu~DG`GuyAq~0n52mF>kT*GYPwdH$&7s&oKZdArxtT) zaNCLHl)AT4)!(zSd}KgO4@zZzlCbNZkl=MW_pPX-q~mR5+#CIoJ2Mz)T;S}{J*29U zAz^TuI|Ck!V+WIwZsT=6^+NVy&zhEmG{368SYI_6M{je+Zj{8-(Y(d+J>K!2&`dzb z)rulf@*>QALf`JC)ZJ-zvy(IUGo%7zm>rMrl4ZHF(8oyKtXzJkHxR!;@{w`G;R8ui z^<(r6*yNcawvI2?Q5xv#N}YE%YDq83Mf3W&aS@-9Wue_rK8TO{0=S068x|q9F0|?D zQaxa8CKqxN)-Bupa&PT$%Obejg6=kloGka4GHBN)F@#7F*T}mLjm?B{As(o;6M^8( z%V6xz!`f)x)76wXf@KT-hblM=fUL1SO&jm%G+$R?*(Mh<9S@yhZzgaFx}G?SCd$kLjuGJ+O*)*#uwkv+nxFfEkSlz)gNywdZ58y-ZJTW|9Yq7Wa7J0$oWx{<;}Y1wDRYj66IdP z!`~~gYB`5a>(b8~Pm7+vYwo^U_n&(afDdmbl|sr$NmZ_~b~A!6SZCwD8SkxsHbq^Z z%V6nP$mXEDcz*+?j)(6>f2gW|;=)M+|3^IDt&5$NA9}}svQr9n)be&M!f~J`_sG|y z&Of!t)tSiC7>@TU*HFYCC(L&IZkyI;<;7V|P;H=?6ud~z|D;0y%h@Nj26X(-9&bX^ zsK;ef4cV^=j;mciCd~HKuH#RK`Kaw}TQ=oVP0FC$8AqKU_vb7P?rHd0E(8kN+ZAt1 zf8GCJGA7+9H>sh6*aG{5Q>7;-nrObif-zU$Xkxod1|DaMmY8}>N9TMGcg>xl zn|eN|SU8zy|26+bP)@%$Cj9_RU_B>BVmoTA18yJK0Sxu?XCUu#Us*%Lw{twDo0T|aq=H0uC z$Ikn&wT(jbtaS_mrOVMfKD#fyq!gvsu52w1Qqnufwo(VGy~{6Ygc+_RcwQ?ivAKo5 zSvAY}gRA6j95Qe3AV0e|!>ZD|r-iW=KqIC9aI%w4o}5>`$?{)vzo?*`uuip3C;K$Q zhd^3EA$5-SvsEX~za)-|#8GvBR~M^O{NWopc%^`f}zEU2H zvwS!xDfCEP-pHreDU-Z@(5#TEhgEJCrX-~mEBpTgbd+V*N>WmWFmfDn1;^l!en;Z* znNS6;>TebkBqrmz;5{*3d%KL7=~k7(-juS(y#}ieQmQY6l2=c5vu8^TTh9;TESV=M zzeRxAhD$l4)Vn;}Zn2zl;%BShupNpC@L(OutkO9(s--5L+s2$YFch42CNzK=-Tk(m z=EtqEQeYge4!ZuA&FmsERAz7O(0R=(anwIMtJpg(t_6nvSbpI_XtS2HqlWGJ3gRNF zOx#)+7B0GH^GoOiaOtZ`EKhEf%&INXbt}bLs4Bwo#YCRc;WcS^squ? z%B;tKNutj-&Q8&xH`r>iZ`rlX=NYL)hNU}iT^hdLDC|;>&1#F8Ah8~Z&7Qh9QP-JY`g_+ONHE>oRg)cC=aj$n6BT?_2+O*gsWQyB55X zhcW5*wqOdQ@$zg0ehK1F69%dLBEq95tdFo-_5(eVx5m#OLrP&JK%g9%*u~7FLw@!#XF5!h$4|i#=2$)SRE7Amm1~PTxM0iJgylAu5Vw<5}|@WKM=tE z-M@AjD$P4uY>$K4U0S-k@9i`m5fh_cYZQ_s;h0)_j&U(OvmrYC%n>cPxJfk|WdNyR za-YM9>^0ZuCTF4e7&K0o>1ano|M#c z-Q}|>I^*8`9=fiq(u)LQbE>kCRpJ0w0$?54NXWNw9LI|lwVk^jg-_sSKN|9Iy3kg( z*(4N4K=H5G6%Cu9hGP-`XM|(bFATh0&lA1a3>{$Zi z(eb7K^t?>TqOT$)eX@r8JQyks&~q8-($o2MzV(aj$WkY1^I^Z%nJ`8FxGJjh*}VHG z=lg3T_f*4HGc@ixFI##{ok(PO*oi8GIKh&#B>DKp6OG29tInB8tSfsL2@Z4+YME^7No4ZY-)#AlRd~E+pQ0Y;GM(3P`6$#)%B2KDm z*388@gN1PA&niPgYMPs6v(;QK>zFEQYJJmZmF|4xAP7}ushsx*4u8>_q<%(MVf!c1 zzCi?Vw>gdPegB`nA(Uh4@sKGjJ}QWcxbC9kd2|m{5V&e28xvH+EydJ>i$r(~Pl~Y0M_#FWgeGgc7w*02-x)>&rOasp1?oV%(mP>1hheN2$Wis-hFLQ-(ZZ|^0pm3`rh^sd5-$=(5j>`u4aAq z@b%h_kk-&0@hsnA$XzJ!CkepuV+A?}6|&#zhPDB~{d-ucS#7~l)M^MkcCosXzhI&U0sKvlzRe1q?uJ6{mR3|Jt=##58u`oH`MU(StFDJPazT$e z9`}=`9yQpCiebq{yFsg}2|M&74mpQuGnF}F`Jd6_=-TiCh?z-G$M1@MhH!*Fi;jCV z5b}R#%Uur)E%ScdCA)bP`bVfXJEsN|qmbHNA-ky>9zt_2jX|;G@fwu%6ssyLrlNDq zZ#J67?EmN_$kC{sGW`b&rQUf)r&n|vc(|TiQ43_x5kN=R2#+6MUSR$!m$PXVm>u{E zmA02Ve*kF=ENDz{&^-niSJ%3G$HCLT`4(k5TFO_Aws(JqNAiA188w@xO?g1Y!1@7i zWS_x@)B&|r0*a?oWAKmAiSw2Our*cJ(_%d;*`rH!&oiU#jdpC=F8PXuGYw)mV7krR zt634#FQu`327(tU1gYGXb>g@8jBG@f#r~c~JqdmfL=YuxZigfTm`DR){7wAcvpD zIn|_7!y)UByqV>>WYAyG@)aj(usg(J+*PVn;n?wz43&;LsVWv`t?Fo6P!#;OSAT%E z`yk8w66|$Djb!1GFs`UZ zzFVsAG6hzga|OQehcTO26g?Tp+BrQ!(rv@0g!Hx4)H;-UYU?0$W0)u6l^@%TD(cfv z#Px8-CL!-5jSy#{5N7x|9t~?!jO3!tXYGeWIyB%*sS~V5IlYm2qYat zC_{{}fMI2I)>=}%$fv{02+*njCXIq5(G?kF;-Os2=DmCnsCCmcedaLkLlh9p1ivQt zl;T}<>1JG)+Hg8UkD`Y^lPQ}$NMZKf`nrt<$Cq5DaGC82*oQNK( z@+I79d;!_WSDwPLpbr+#TZ4EXJB3GncF3dGr^hCA;)Av&o**49@9Bg;{7qd=%{Z}G zJa`R^KjKg0tXcBrTBE^9_r6+ew)zn=A*Thh*8hI9+|4|g-U-_i4agyckki8{kjT$A zAqobIMOy+34~EF43jV|v0&48ttWv;A00g9+u-{e&}K)zGP zezG`~9k~7ymECMWy6W+z;gX|8tMA?S22f}B!s(bgxA9vU%j{B$E_t3z9`PIvj`s6% zDET0?m+FQqc9^v2EgOqX`#G1@C+rPg5HM!^iXdVB(4D3EAxeG#CKnngbVUsL)Et|F zW~5`po=pO<*s}k7If~qy3ME;vw8VKTp^BBp^DClDY4*Z9>Gb1cN!;VhM19N)lmG{FA4}4C_*->)m9i(ZC4Mc1c?&7!qK7aL6Y998k_|oL9dQ)_gE_71d>6 zQ)08|(>U9B!G}H*98yISh=Ao}+M#K@4ST-6%J8bk3Pjl3wFw#~?D{pJz!XwRn@rRQ zyp)L1e9dK);u=&11f;g?7zvzFjeLnCkpdR?uYI4*!3!AyK7^SHPSX**X#~h70>Fk} ztOVYZ2kN0<`O3iAfr?}>exbQgjx$3UULnlx?IK}nkIT6#*pvk2bC{^?eDL%T@+x-j zZ&HsQJsMDi^{&>o#U;!y2wh9y{sW!wXG!phF^Ht_si9^3E(=meIfc6X!`JxK|JvUV z5yC0gBGMLR{pp@BBlw`e2Qj4&t*)YXU7f>_KvG`vPC*AvzoKJTMgkYqQcC1x;)(I! zg;n(-nZLLO;eZPY0>^1B))RbgLSV*WiL1@9=dtQ&HmF#0sG{>NigqpvzbBr=(8Om5*hHhJMY*Cvf7KrTYB1}lq%?YwO#p0hEVF^dCnfRs zr|P@^6w5u4A7%Rtpv4zqmGSIwct@g*gap(kZp6dS@OUu_j(*!-!gj*9HtjMpnb zr&d3my3sv6A}Sf5#XfPo$PxSZlu3ip>)hO89fuar(tRoEfv(x@z?573raf<4^kZ2b zBVeu!zviMfLM9g@0c#`eC3Cqr|L4x6;Qsv18m_q4YjSVDEIkZ;!j;r~#zPhM^$VsK z8l;?*x{3Bwjzw!7HoqrC6AM)-u`~NRu^Sl>n#%KwO|_8w@~qpMY$&)SgIO|~=dl-4 z%yARkL0a@H{pnF#XFw!5K*qjmcaVr%2V~x)A=uUqVrF>E*C^$#zm;HuV&zNkhlgn> zA2BxJF%?vYZ4Iy4B<;ou_>KnPFn&V{%mVm@WU*R`V-`5v0IC1}v4LU2~J1uo}R?p~6W-y)N8Gs1~Yql1r#mDvEG7UhsroJB`TnPUJ8 zSoEN&*+;V6B8B)V8IJcS4Gz95@R^&u5P8h!Vxm>-eYz^ONRLs{Ub?!T_@6*D*1p z2?M;~ySm0u)pGr+j$9-ukX#;!?7AX@tt$6#$PXZvcd>?LQEeHWK%2(B3MJC#L&w_z zn2N=9I%Z}R;04eRrt3wL0MX>yLyd;7CFFr}2^l*p9C1tT@B)vnn@&?yu(=0j98Uv6 zmCNH5{-d6X=^&pNn~I;TH4hAM*HEwm@MhB+q>l~?eE6$c<&oV0V%*As&-t)cRfhg} zicQJwUHS8MDa?JZ@GAnKs8r2-n7%kOy#Ecvq>Tu`FH4{N$pR>Ar9#Rh161t>q;b!C z{@SDdM}0>6+fz~^SF+Lf{Q%JF)2(M_D<{o_2EWL_`|&mg6|nup**kzuw&!Y|BH;0| zONb(%<;Qgwa@gBN@l}RTEVl3CN=rs|t%_+zlc)GFvI`DoVa|mE^}rs^1uFMD02uywwX&Y=hAeZO zxZ_2aJl-()c~I7QcHQEQ`SkbkFIXE!By8YX$kA>7yAwTpdwWb`a3eTmM6iP5Ik3+v zTp8f>?v3lN$as1gxMP%;b&b3dC{f0t18YJBmYOc(-%akx<4+uqh3o9{>3nN0}E zfe;sCt}|3R4{su`mmUc5P;ajBX!h$UH#OGgHP59EGK9dDfv_JetRU1@pps0BLa|1F;p`&M;Em{`M>2vwrbFgYw zS}kJu`f4A?Wbi;1_+jxZWwCWV)=*QlkaisvuA+}02h1VPzfLc&?aQ}q;<`UE=OGUG z@)L`F0Qr{parCLl<;#6c1WvVghPMbLk{D>*7iM3<1@>2u7)49dtZ)oi!Z60o$iNJ& z+-)!HW1v&ld(pA~P(*$jhz+E=F;y#)2mocvH-9?KR6~JF>p%s0ft3GLonL%s1+oc- z)u{nMPtr>Hp?}yN0k!#*>S83sjLRSb5QoHCUId#KYJATo!r)(6{=L4r3t3z6CTk2C z?3W+5o^1N@I>!-bQNsKCF9x-|_LMfdG!AV38I-rMyxuecA7V%*i?s^#<>!EshHsX5}0eI;Je*%QA06~J2}dBHdk5(QSSX^5+GB| z%LgmwHyU@?pYZq7C!(Xd!k_mi=qtCFF!G{T9=fLDM{usMg z&jUpeY8umVu|T-zR;iZ^q;q}M%u(pGp%JeCy~RzfNEq*9mYBk)G;d4z5_7`!tStmE)_RLdB?~~dqG{Ng zpilv~?2Jx61r$JiDm-3Mk|IO+4m@?JPId!|1k7`{60Dukp<0p1yY6c)hV@m-S3O!@ zIN%xqU3Hsb3K;?g6ifigM-l{RvQOjXBs>V^GGANF;Wjss!}xnXn&)x9C?Po@#q=Oz zsyv78$OsqMLddxGK&Y7j6YD;(k@BAE&gHz!yaPOW|Nkt2pZw~%-jvn*uUO9o0-SxX zdh0AZ(eF(ok-yeQ#Rvih2;fa=to;-afG3^u$!O=ta0D+{IF+%QK={xXFzgTlQQp=C z?Lwvv_ftd`8Go(2o-Rh;(d2B4d%Ane2qT_M<^)b7}7v>BB@qH zw3TwWf{+rjGF5^pO^Sb@6JEIe1)Hx~>Cek^C?@#?WNACBmakU`4Qi9Rs5(tIOAKCp z`QD(uUHXyFHaSxq3?p;h+qg8Lz(3H2R+N}(z=|16N>0sTC|3A$ymM9C|9UhVdQ{+~ zhA0h!q!DA#FE}&NSOmqEr4dQTt_e7$fp5<|o#%K(pYvN<0i=BY*4DF|r^S3jdA`bu zqxA7WHtC}I350#C)9~@FUjhXkllT1aM3Z$g&Taxn(A}0I=ws9RRhzRa{`+)1lz`y$ z8xi8q_tqH-^+*EX$ImkMVKvf%5S)m*R!9}xnOJCudAlj8jB|Ibe1LL>s=0RMGZII) z?F5cM?FB`NKzPI29w%Mt!*#RGTcz+65HYfVb6&r?+kSg=m=>Q&;e(D>j6}K(THqRK zQ3wXI&!qAiI{q5?=Y;vx`ZIMpxdJ9&Nvf${Y0EIJRTm6E*035Dzn7v>6iW3aCOb1U zW0&=z9Ip&+4S_DHmTOm0iWp$aC#kSzpbo9=Y9n7~XG&7@KYEej!Nt<#=+j$OrQ*rX zm?bU{Z*sVBloxvO=cN~!EFdioC;M%T#q_7OYx#;t7I>lgmOu|2P_ds)YdjPBt8OkS znzl{(@evu~cY|g;9|ACPcRrlxl|I^Z^qZ{Ad=%W}O|H$z0p(4ic zkw9~=Y0U1=>^)4AI3>IowtG{Q(|n~+{nLf zX4$hd$RkpCoHpvtHaA~)QXS(9r7idUW`U)?Y(H4`;9>z47 zu9YvZz2IYae>4G|X-7%StQ>(t3_UBbS*E&*+wb3dq;JKn>C<&tuu~z*FTig{f%C>< zN#u*aVqj9Ig=OA6pAeDRqs)C(Q)dMpjpUMVYeQx>7qYQ)jpiB5bAqnRWHeW z<^vj40N@U1-}_w z1-)Du3;2^3Z_*_RV%eEe%?;-}D=)=`1dcgPcvc?~%DNq@^3sFL6(x$U)= z+LgZGTnFHe2rv1V9$u^;8l7m9*ot<#k1{KFjzO7Cy9gsMRy=w?fUIv3YrbB@=d!CO zgb-YnPW9;(i1{^dTjsO8!Eac{S4);8yFc75O(Dspk&4~YWL<~#fi(M;??rU@+TSBP zzLC*nVB-$a4~rrIavulb+~m^NR*a0!qek^gE7V;^{$8)fkC*3S#dIT)F+Dgki~`<#7h-ZOrU?`h2BEX9NhI03m3r@+rIh=M3ed;)7a z+t!*ReLVuiUA9RT&l!R`YHGk1nFsG9bx`; zdkSSCuB*ya$)2d85xD@nvY|QNM3DU#wBG)Cq@8?he7_SY_-8)$U3WcnQ`UZ#phQNQ z_8((azf6Pjm?%K#1m8I3fx!EhhPmQ;@eJxtT9FBC|N`&np+qx~yH(ahi&$r-N4{nCDN za>gOO;eIIYNRps82MwJabYbZ(C$*?6Da+vAK7-~$TMm-z|LIxHl(o(mZjm@w%%~dBrRec7`DuDRItmT^*wj5*HF&8VA=XcUnb1Xo_nt#2QuH4eQ5J zR*gWJyGxWYxR)C*$J{|4g5Y`e=P(3;M%)(jI$PgnbN8L)fV7=-S89U_t&ek@!ds^G z)nz{M(~7|{T2%h~u~<|L!8;Ho<8YVc{dX}j`zCL1=5#*tKYg`Ro~6)J*-Ie~D(&TT z*=HC#;W`?Eays=hVsmCS=O|oKXzDHZqhklGwV%R+D551#@Jwu*9&RYOA>28Wy_u!V&sJZ5$;W-S<8$P6AmI$p69P$>$<=)ANS zk26%niv~tWlPH7FGGZc*CY@a=l%6Y<9i0?N=IEw2DvVAgu@4#*FpL@WQ#g1(o5gXVW1q(0K8p6=nyB^siDrdy$^1s_IPIY~ zm_g`cOV(~Pl;8ye8Xl=pd2UFJGmF#mgG!xy>b(f+E+Oy`MF&`bXJu+kaO%PCWf=)7LWz1g@NCr`K9fT(We>5jBuKfxbK!!EM9ry zSBQpz;t?uY=O6nAwzoPaCu-otDu(=#2Rv*^kDFwl?9xkOqHoO4HP&~p0`;U+Z%aRS znYWQlKMMm5Rf!KIj<4!D{L@EN+fB_<|owm9&9AuOz3#i~?v zB|A*^9H>g5cKkKO4bu?s6p0Gub>sD!7e`EDBL>G;TyM0lfZxDwWw~1@60( z&nmCNZo-3H@BCPcq^Cr(L^~cxni2}{VAn0Sw>&G;qm%Irv6~T3Z6k0H;**H$+gL#C z+)0qF!$L@Cg#YkP!`!X8FV&D4MBnBxA;A9oh@|k}3>dl5IffgOfhLVj#X2gGv(1yX zDz~h@RSK8sNThC{p^9&CU6H_ymOtH7s8_j;tsgD|#m7Is_VmoP^x?#h)pe0^q>`bn zXHCuazBGOUt7V;m`!T{mg#4(M4IS za8-bc&2LOg9+;##H93;SAsnOg8dDjioAXug{^j1hBIu(}2!7xmJ^?%p=5p{1GmH^b%7bU!{>0D(Uh-^mJ@da6IzetBale%r%nDYgCf zomRa0!c1aF`|%c*_j+%G=+(#^EQdW_GJY?MfMH?3KUa)CmdJ1}P@dY1vPV@uPR55zFW089K$hr;aNv-Rnj96IFa z@+E8NPwlY_(T>lWD3G6P7Vn6Zk<0$L*`(ltBl^wlKRjGBAzJjsa`33i;~tW({QNoJ zFr#lvM^&7I6a_Nuq{@wc7F#V<$mt}?HEGZ6fe!(Gt8(R3NN%>NGbc1QQwlVLgBJ4F zEe3Vytqrj!N1buKL5thjU`*2#^MCg_o>T)CcDoVOJlm=Am8aCOD3ait=?a(@C6?uW zQGqXF&rqatEC2DKU;s?i3mT``bqtU+4|hH9uwTxEf*ooL7Kagv=BA;+YtQEq^S_Dv zwgyZ?WBO@jya=p>RG9V|CRVuFufhKO46C6Y3rKQX&5eJ+I7(eS#8_XY;dvbAZ_gud z9msDLUN<|aq+|l}3kIAYsw7(Rd}G3_rzufVYz7c#Pv?4WGu*!As~=89T7uCj=KZs- zZoDp0pKPl{4KUr{Fe|8wzX-YfshI>4TULr}?&D{QsmpJo<-;Gf2-aa=eo`VEc=~Ah zUi?Z?Fg(KY zEG6&0e!b)ZR^o!)zS!V{$u`6&N2J|yqnkQ5D=ha0#=ynH(-l&fT`=2E$6hKSgm~!C zXHp~i1%#;8BMAWl?+8!?fXmf6c?TdQ{x#u^Q(%&vy&1TU8QSPUA&-#oxCuj`Ux^3L z$n$1(O7M?VmR?Na)DA14T2MQRnm@klKBQZ>DlI|FP|^4*0|k8DmJjr-e&nAv(W#9} zBDY*xY`mx=GB=Oh|A<$CnCdbGnX7OO_LkAyruO1VSvlG>-N{XlPv75PeJGI93IFbw zRFRVu*T>z~4X6HV2bywCzVvZ&l<^0i7?T_5nAh@-%zvU{0_s0V8TFw^E1F? z{X%GZ{xDhYs&=Vmw;*lpLbvm^$oAPJY1VThk!n%cWWM`k-BVQ)|9n71P*g~_LvHFn zWQJ~e)6*v7Z*vRbOYtQ=$AFN#&Wpz#_2yL&$E@f^h^tKPM-wQjunu4XPNOz?`y(JE zDBn9?*Nh73bA&G(XQ|E%BSl}Bn>d3C{n^md0J^%2S1F?QRPcgzjlH|2GjI}3zUq}0 znSNK!;jxd~a6Kxds3Gg1ggStC`DFF(`ssgDLjJ8{1#4m!vsilYD*$kE=~}!o$mo#O zm&{V1%2N#tMtJ6$Dp-i;`bP9^2D3AuzPF&xV-m$x!?AiLBb zF<{Y=;Or&I!WO8)igU}Y(`&6SXQ0V#NDX%a1;K|^Ppwc7<98rsXtVZ`7L=$3Tf%@;_Kk^7(MVPgIJB}yf{5GShX7A&U2 zCmX?u7@o0vTze!nW2!k#8nnNcrMBWq*sa>S(X^6jko7*a`}rc2!&*dHrYL9mj-Kx0 zb}6x1n7kKj$&mJU6lHutzb;JfDq=z_=$it;$sW*+S+esCms- z55$~gAN`N`#~-wgsckZ5CyJc~6agFaCxAj!%(iII^2S|);E+IyM|>5IJx%gsN}_Ef z@_n7fO*-Ru-H`zJ6Oje5hNPiud$8D?-Pp8AHJm%qd=(^52Nn8KAg$((Z23Ci!XBwqpC|?@+VAtlucyS_aqskWD4Jrr>UAV< zWGRtUuY1b+o7VK_&(5!cxZ5Z{KGCX|D89gW>0A}TGGm%*STz>A>o2uWH~>3%AHtnU zL1&JZSNf2Zu}Xc>*NcM9sc?s%ZIaJTKCWniW6^>XB@>fZ)2>~b=@RPDl?q_M8n*poBL4CQi zh1Hgu4sh|Z;rUml5hSEnXCPkxW$jXlKHdh zpLD;(eyZMTV8WR)PgT>t!iQIAvtVjv||foD>K&Uy^?eg(`;vbRFeo8g7tX6;Q3rHt+;^BR8S&U7%vo7g1kx>IUyLPTI}|T}fU494%G5!SQA#QWtq>P?Mv|3SY;lTRV`G zZ1XAP_iRp|E!HI|95Z>7M-}>+2?VtA{_H|Cy%}cO5r72e*I3Cw&o>YPp{6qud%i%$l_Gw-~VlyL2-x9L)0G zK!5)xq~vQ9rDl*=puv(pxVR#yMk$0rz?#sUM;`l{W&O8X=HCF|&{L0>4mzW{Q_Uee9C?o1PCtiYpV7I`~;E5b1*x4c(1>~_kpOR5fzM}a(N zI0oA~Q({(Wk6O)VRT7-P7P`xDRxDY-`KoY>h7z-;2LGQj#DgPeeu0XWph<+b___Jo z$W7Y@*e)38O2?74#5$cnDVvHmF0l$nWzN~MdZq+U%$ff5a7bmP9i+}|gU<6m&S#5b zg^B6h6Lpe`61g2sBnvcY%T|{B=8GZ$xFa8?RX_J^jBZE2km7+tEUNSkUk@Pu8$s4Z zo6hk1!(Gq|kY`Uf^^p1X^G(F;zBeVAb5$4TeFw-KMt%mj1TNHD+JpDgyuTcy%dB#~sN=Y5XV=6>{~NDk{rZ}L5R(u3T2p$!5zPVi z;b9h6(jPfzbN->WvZP-JlADkY)J}W0+R!mVOF2{fg;8@?r2M}vVG-ZY8EWgWZxxhb zSG<9ByUi5MU}(_A!)kBWdyy-rgWBX0a;tVJIkL!qCWuXeXqnvhZ;FQ#6vzmzecaoo zzo}!v)eK(fh{x3xH7I>9pB8`!qUM>nBBRxYz&DZ!9^WzQ34AyKQJx_#hirN~AvzqJ z?W!LP0Z}0AnUed*jcd&i}cl~H0Ur5gqh$80|v{_MTG$!vNS?aUW4_f&chW7b?Fq6P|} zf6B!Is+Vh1W2F6pyKKe$K@>gr*Ejic=4qzCv|cEnoLFn%n=1U{UgYlu?U-S$zJ&Fb z-WEr?JarDPju`qbU(beFJpf#Ad=kdMPl?AcIc2Y$7MeRGup+NidAm!Lo{(h2arx@j zw0pePm>(N~;`kK0OoR;mMR2eq%ntGz&{07_MuHUR{MpxEkOm1SKDUK9M#<(EMs^FcQu z;x`odBovg`(cyGsMDxn^K$zwKnEDF1rn~R|&&Gh!1C)*d5+dD_qfw+&LO`S?6zSLo z5{iUDh?FAT(o#bNR6vmKR$4$h_y6!bkKg}~*LaPw`?+!MxpnS&zmHxY>LhXs-_ydz zBW*Py*{2<#NG(E%6=uW}-a920`N%6tFE)thnZQG$wu?ANCmGc0{3}x80T?EE#@dAt zD%MiF&OzcP2#iC7atVAi8}Bop$#?(BWozj9c}gWSlr15#0_oYAf%u z+tbDRk>ocsxAzm^5*Xm_Y4~=$t(3qy7p5BeT0^G?FsCJ1aMGxuyq8S5auZ&p+izI8 z6YS0d{oEz}SyWVzd>=+Ov)?dibD)s#7n@9}d4J!W_UW<5Bd zdc0(AXFW&zeVzg~`zy-2uV}O6k4Ds(HfI+6!q>88N?g5WU7khX_*R%%EFkq%@a2L; zwo?W#67-`2l11tWKGnW#mxE)dwmyNkodY$h(<>4|tG8kN5+8p~#eaVedI5qIsSoMQ z=g>YS%j2R7UzjF+Uh~3$&;ClFCGj##hjm-quoNMdPm*wbgaL8kSeq~#6#>!&;>bLO z=n?n?_Hb?)m@S@N3TZaK@N7Rpi69Pqes}-V+maPZf5*|L9cV5_*wOuE?OrR$+jL8P zHv9(j;tzVd=i4)nA(0Ub9PPqP!?`5Yb!+Zebe_2=sfO6)NEh5b%_r{g9lrku}- z$_R9LvOXSE<&wDV$~lXI(YZ-=;}x&>iamI-&3+1S9O{8cpOrCg@xz-|zBx`X%(E)k zHGX6OAoS*~lmxx90!Y9zz}kP8=E&+Dq^shA$Abkhu4Tiu9MX z_7)s z#q{@+GQKTd;nM7~8Yh#QD1r;zF+hTzz;B+RnRzjKXG(qct7z2nbx4+pY!4uH?rU}X z@(aR}>2kdcH0XO|eI3K*UKPJ>qt?+{+R}m`&>4`zV=Tt^!a*yy*%fhou~T3ipJ5)u z2cPYq?SY#a&a0-)V#Si)1c#1TO?;TL0?78HO*?AY3vsZW8_9HD`H%CiosnJ$Yi>i)++EoD7E;lv#YVO_*GdikbNjnaY=;_ za7`$ZaMbDjNX7`?J_qx%)IG!>r$OGAT z&*`s0U}^|DPp;W6hQlpyvae*@`^M4En|8^~n-%p;|5#lp)dJdY)Z2PBSM_)@ew&bK z_Ru2(7}7lp8Ig)osZ=4IdP9BEB2qjSii6LvFLT`~&U*ZbIrWL5Aa$DI^!dH?-=_+A z=TZXJAjPl*!k%x^WxrkZB7{oYcPh!}*n@m}uRyxnUyhdM7G1{8KbHSwBWLRNGkIFCkt10hXy1pJAkX0ddp+{vly#m*c^bpC+K2Meifdq(W-ACrH7!94Z zSvlJ5-=@rxksNgh7bIca@DuLGU`qeHw|=Z-dL zA^r5hUxFUBf$jB3yfWe};EYfO>6He{xE&Jz^erDfv|=3lhhP9nzSqK@cJ6`w-U2R0*6`0i=YC?bEaq#jP`SObb=5fccD^+(l6a!Xt9LdQA z`=?7;T?+EL$Am~87;8oa@}wCLp>KBigUp3fDk0ZK2K7MLgZ|!X>Qx)YaSQ%$MONbe z_nn=o0d}Bx{U_+NVUeX^al(*pS%xl;e1;c^Rr1BtQ@RLR%jQO`#C>m}AjB})@0P4J z6fl8fKoYsVt8LSPqXq||u@3Z~16S^9DZbU|*R_evXXpkI_k6a@wHd0GF(broOb!!> zb(eJfL>O+_RSpR-R2D)}7&o5vCyQ>yQjPdC!}q79V5XjFnv6Q z$K1Is_C8a3;Cz^v60E=%)X$TRwU4Ab1j9gtFd-vBCTrI-Y#OPKCh#`b^9=MQHS9oF z@w~fBq5#eEu^T}z)CL8~{zhAC83Fkb-yGsttpx09G_=Co!NSBXTnJ;fJ4tISpuM-zO*aT84lOJ#GE@ zpYQ!IEikeP+xwcclnO_GK<_bka5DjiUEly)b3hko#9(Tu5ENhKna*iew5ilpe}Q(P zr^V9EqCKVBWRPHeLcvR>KL|!V7V9`LmdIprL_D+3 z0YdVnhMY|$F9EBJdTK=Lqe6){!d6>5i`cpBXEifjI&cM+jiithRsag#(tLh&)u@+K z?3@7j9%=M^HxImvs-I&bIxnEoB?b17FUQ4noAI$3hF?#?Pm4YAc0{<+Fh+u=>*v^A zCeUh806N}*&Z=nSmoaalj47p44dLJC%$S!@6v1rpLD7_8JfQUgRFx=PGH8T9R-Cut z^rE|%fC@mnkK5NnC9)KerV3xYyV1``w}!}-xZjPS))94-s6Q+|)e6z1R!xn752_(M zzA@CLG7`9jeHw8>Fl;YciJkXmQJ>Qf|F#LRsydT?ra*{^+@2}hU10XrbmguNQ?e-=i`qc%uehnIMbV20j>}x z$hUACGbeI^7a-nvu64Sy%ZEe6`&_q68)ilQKGBgx7`k;rye=J58~@(*mr_zBbr#Sk zqBvY@)AX)#cSNTD=jHB8%fwb0vV_4HQGVcJvD1qUvQD2W|Cx1q23i~q4MCuVH4rgZ zNg8`Bon=;Yxr>{;*85BlWX2GEeB+7eAAG2U{#LoB=~eK6s0uo_Y(cKsBMH^mh3=d* zfEQ-x4`w7{w`~M|{Fpq_yn9;YhwoX;n_0^?z`x`C0UMcm3B_cp`mP+g|7=t~GQ6W3 z4=8c$bh~sjEWdtRTA8hNIixqc(qUy)rTWBsu7LKvNU*u-9d2-7G=9?=U?vO0AOJiRKhO$o5T1gD1ASZCckQ!(g2b_3oy2RHrj0gz%8H9` z{ti@Q#21l%IgH=hvh+ZI3_JUstNbGAQx1tAB;e0aZXUk@qY#1xboazH!49R8^XII`{5pGYUT0*b zjw+ij&wQ!rX!6Sg`W{!TODmC;?bk37x zE{XN|#E3sH3gd21M&UpfnpfLO5zo+35T@r)dC_`|O6>>J- zu+v_%1|u?Cf+QYO>YU)`^wZ`-T_Cc_q|MUw&Q(7Jc}+(Av+d1S*zi?+kj)wgJ(VX& zj934W^%8KONf7~Ucy!B15wIs1g%yYnA@#fGvK>q_{BvLV_LeK2PI4YC#^`{LCvb%K zqCu|oq=`(`)Wu^*eW2V0Zs~C4+r`;e^cjUM?K?*kT6$*gO8Si?KgByvrk&&)IyxOK zZ>=Ah)FM0y-;cMPK8${!*S|DV+L{g6(!a2vnui9T9D9HdbLH}w3|SGtaDrYt`>H%X zU3Jaf7B~jHF0=9B>B87kCxMs{a3v|H(k{wXIuDM1w1P6i^prChuySg02Csu8nmISW z-V*q(YTNlc`{4wEIOyfm*M}TdGRC7%k8Fvnzdg$Z9}i#=G{SjCiry;ksY^AqZq!IP zx%*3sv8={{&WXco%d6n@==;|X;NiC8(COpyZA0Bbs_r9 zk&df6Vuh?}D;!L^`9;Tj-MMbI(OW*=)jw`Jj@Rr&%fDwNxSfW@83_v~J^*prt?}ZK zZLWl2MHGEVw-!933H~h;9Vx(}QggASEch*voU6pPU*H}PMRy?VMU1wf?+A$NT7x6E zj)~nC$11vD8BAJ>pJ@VMTiv&AK1~GaoUrd}p2Y-*leUaE#Ha$%MHmciG@}6*UI@mX77(mK(H#_ZvZv)&T;5C z$#X*u1I45}4;zfc&iob}xifBHYk!Xe3UZcq;6TF9F0eJFN1`@SB-r#I(9t<6{uODM zWQ7a?M9sGG_{ILi0YAnyr9mzg2=X&rwd62Y=Am-QrH*_2vr`!awtt*W?G5w0w78o3$R!;M$d5 zgzI-UwI1X5;)&oHi2`%Vm| zVAd)H8kaAP({gw(jxly73eFHa*Ob3*#)t>qW}K0fg{IvHZ0TZ)Ht3I-*Yx2kB8M@2 z04&~G-|UwoZrUP)dP14*?FONdW z1RPxYl~GR(K?4q_4n*=vYH;YV^WDLGQPC^kz+)Xk`I6f@H`5OWHvt40AD_B9p>isc z&*!%6g!y5HlW*@wfrhTBsi4j42h&cWmINtB&42h-d~hYu3xZ5Xlj+vS@lL&EVNy!AHO_8SxJoMBgh#~oKW3p%XE zXEexNg}Ice-!eD6&gE&dN8W<998G^44T;7Kp1uVQjrcf;bAo=B_Mt4hDh`72@u8Lt z%U~VeAG}Bi^0+%j`&QA*d(!xTET7Fum^fsdQJ-9cw}w5{Rl?v}Jk|s+s=t75%~MM5 zKo3!CEb4;C9$BZ+5;WsIA6H)W_v8*t7iW;Mx7;R$S_tPKRi#s*8^|ta@w7mvJq^CJ_;2uojH*0#X7W;P<20uti87|bMT9=6gZLD_gN@q6|h%oHx8Ej znhP3dYv)bMxlpyO3!NEo&wJHrwB&Zw5SlE;m61H*TE@&`>V%sJ^9GMRYxL5suJPyY zg9Z9N-Nf`rH<_5brr}&D z9{X0Rqjaij8=vt;;W>D`9OTR_?b{ElSM-oMmW{9ldl~V}*Q>g2xj1`czB1@zI%J%U zAuDpbg8zm&=^+yvflHI_+q=-9f^Bqf=XVfIrU!O5MlRj^?XISH%lg8lFjW*3W#AQ^*RCfn z!WI@J#}C;bsK_~=W+KH~6l+ui)U^S=_-FAWYq{JL?tL#d2!GNcC|L%VKpa5qm-8~3qrCuWfen51hV6F z>K$bEyQGF6V&XRt@88u+Zr!LOzw_a)c0%D#H|u7bmVtAr6|SzykDbc~1{@5qx{;Qk zWby|35Oa+M+1C75t`889^LA(5ZKObJU0<$4NW)U@+;FK$NOsx%h|fH@98AfCciL-P z{zmS=(I(!RbI;RTmd&n3!)FA$_O>=dZ+$-M)CYi`$VPAhEXXY=MqsI6(jDhK)5l|_ z^f~pe22#l;J@g&^N?m*)&pNbm>-fcEjSBXOdmeB8h&J+uZmx4*k$46u$e-}-oA{mk zMEf{TAW@P=Cf|?=1=lOZ@?jf>GZ}*S=P6W6?81$+y=lQ(p-uoQ`MvsywT%2LY-dZ! z`}61|bDBq77kwT=dy@Y|n!EzEUr}k~7{`#3XaV^MJ+Ee#(b`0vcZdWb4x~)Uyq=Eh z-I6!pSilYkagN7iy}XjD^=n^?RF(9c)+=1;IQIs_De=Dh20FqxJONzP`!e;l@=x_7 z4o8P+pr@XZs6iy7He|rD_=EI}4R!RfS`kfVNOh}6eNfWQ8;)4Kvq4#1+KX`fto?*> zn)wXT6P(>8poXwOqfomYMjC1^ z^|9iNHplF5=BV4W<`eU%tOS2pK1GZF?OX+hC^iBz$@3-U1-uB@g1i6%w`h$S%4R=o4ciL78-$?~Ff z{e3}q%iM{kTFP4NnZyQrf@6WHBp=DMuN7H z?CHaoY#B~tTPzG~tuCiCH)7Fahg2$-{S|t(y0JexpGh$B7)6soJxFXdt>mAweR@6J zh~Iy?zU-m%%tPifO$?ww)Gt20VAS$-g;=<1TS@hzChGg#>qcxWejmhj*b-a|n1*;~ zZ$Y$4%8!u*FA35;L4SHaxueJX9y~~D!FIx2(cpY|GL%$-s3SKfUL_VkR=$l7mpBhO zJbNYZB1Dst2tP+KG1lh4lWd>);?ey4WDjt!y*+!G<0*+m&WrClZR@)R=~L_?ta*G_ zh_LJmMBn|~mMZoK;}!In@!C1to8D~|y30k6w$bzEn)r71dbQ0|xAH zVV`ZyTM-qJ5(q(W?`fQ@rHgMfPkE-b>u*H?C((=VnS7@bJIAV%50Z-<(5W02IgT#O zwpIEk=HJzwSlwTW#%h|SkMk_stEyEj-;Kw{hcfxcW8Lf)@R3j0uQTH%xA)6tsUZ6l z>s)ogUldh2ev`=902Kv-mk!)%r-@la^V26j4P3Sic=)hN71{VG{I%snL%|@YU$y(c zg06+#RvzSMZfR%c4Y?Q7{PpvjyzbNEA${Y0kT|eykD809`k;u3U-}~~jU%gZIjNxk zl(VlWNZjd~?H5dqA?0on^*eK+WXS&I_1drT-LYAbkYnYAAbqZ1njZ%&!q9z5J!|7<QksNngCvh@gQaLq1@oSQjU+&!f~?=|r4?P)7fANUw*qW)7O6VU1SWdwXt zex*ITERFHxei((@GovVHE@fMm2>z*xtIdtR+4^;h;qaSWm-}$ztUq{FvG14iI%R47 z#5-4o<0I_PY+s&ky2XBNQG6})52|uKeJwudE=!U&1fvaD5NYMt2nUbu^e%f)#VUQ3 zt@T_SzDtWxR*@G=T)o~C)Jd?#P?!Fvxdj^+Q`%skYowLffqpa*wi9wH_{d|XPmnkx zN)g4uK~nc%<_%=4#Z2pq)E55j$${b|TZ>y&x>#{GEwoM`3xS3?gM^n3Z#Fi2L+94o zN+#;MI^r_z-Z(5Mt#062Hsqa;c#$p^qooJ|>C1?;zyh8h zJYN2lAALt=BF9s-Pp*^QKN=+~2J_8#P}-|*uK|L7pTP$YTUJVM*XT3h4LW;UR<5jX z;uqRGM;Do+T$_|u1rc>na0^q%vt6N5aJ72Aio67J$oCg_zJ1^Ve*%BLwqW@dEK_+ z=?>?wqE7{tLVB-CT|!Tga_XyLWPy4~O1X8B3}<>wD1AH3LaJ=37<=|pECt#=5#~#B zDf>wbtp~*-UE|N9T8*CpX&w0No$jn>e6jd^q#tQgwH;|1PsmDlo}?piNi7S8wPrZ+ z&L_YjZe8Pu@EO1>B4*#r=Pd1gaN7FSS7a$(d!=0O4q=G%lP&tMIT&!cdUMz}8GRDv z#e}E5XtAk)oaT9K|9j6~51n+fnV8;{t7I__**ABKJh~4+H1!i*)m|GZhy_tla6jLP zQ%PM23^R}LvN1>;di__&K9PPHWq2+)auk6*m+@;2w*ZwCO||%14bgzqg6Rkb{$gF7Bxm) z&eO4{u|lMV@PAyHYkJ-?W8pX;Y94hB-1Z0LZI^90&fT?;Wk2t6SSV>Bm_9zL{nB#d z+?_uf6%9)hMR=0fszys+eGQj^1`qgCP7n&quJLI{dSHA>KmVc%w>TmsJjp`H^nR!@ zL5u6Z?Z5|*9mdc0V$p6lB;Cd%T4cUu3|XRKH#&WWf9GV1*Apct5O}3TEGzD$hQS}- zwC(70Tc74tsuremud+Z8xegEZ@)`Ro-{_QF)b8DIu&;10F0>n~y}e(g)a>%wGH2J! zB=Yi<8DP|W#Bh4D>kJzB-erv>C5w(`t@Ygz%RsG=~m&oJ@`SrS-GxeCK~0 z4n%j_PT#uu>x8adP z(i@5tMeoFo&a8K>$ZiZ*3TvWl5X%^>hF!P%cu$4hHh&wxWI0n?J8eUfqf~MdRrzF; zC(S_4#Ve>`-YjJO3l~$Ac+k(p6j&(Eo}CzCTfz$rwh?{@9%pe6@1phB@7>8^g2g{FSOX*>^O~r_Y8< zyd$jx?AYZ%q^T7z`_t5Bs^_>7txiUab$gT8F5oyjKZ_987o(QBf)%u^+-R3h8Z{CY zs?xFiAc`8qJT%|d`<-xbO?j83FErS~g2oY|TghfEMtK{{`=f@I?GUy?`SSIQYQO9w zsO!OigXd1J&&K3H!`PA2Zu#!<_yF4Z;Y|Y)C_Co{Vz>R|X^=e}z<+M9y!QObi>D|p zm}zKj4;%ViJ7}Th+I{K9z8~G?io+E;_aw{O?Wzp{X- zaG}<)3mud-ygc!~Zm)wxo4$UJOV3c*g7-<`){kZ;21k-#S=M`U44%!fiVMmGl@Z|| zUX9p~sEzR5swlDZ4XT=YF!hA#R8=+4*o3S6TSLiv|Ca8C{A{>d7z#X5Ow#50IR?Aw z+0_EI6oy1vVSeB_liK#p=@~oO`saml^OIrX<|qBn`xyAnN@l9G;~Wcr+^?FiW!Dzk ztyS){Y$K|EDPlsA@lo;~w4D-lQwWlVnQ)3si=cUml7#Dzt;hN$t0K(r*$7Xqr?vO# zl&E;Yz1GH--?G}-gfd9{Ae-Q!--n(DPY~~C`k84hv^>@MNfdeu?X&i3Zum%#`N!9O zk-ah`)BuU|E2XV~ZEV zXU^4Aj}GpSxM)48w7N$eGS{>{;DNubv_WdOs^y04IS-dvQ89)Ks9Xif7s&eVLNMy~ zyf_IK-TC@3_@Oc;4A&#J{rmZRwTOMmr{R^XdNoP zmV4YA{$|=J(&yF6G-<&~7avUu95TyP*4Pil-9Efqc#snZGA=@73+4uQD$4|X8;I&O z(|<)cT<+OSGC1NU>BG?Mao0tBKyT9db}S1?-9uTHY$rc{zXJy5V}_!(8Eh06KraS9E~$i8tk4O<`;tCqe=`Q#?Aq3(IZIz@_@g zRrrm+=S5U$JY6{UO3l>fB=mYa`fKah_s`pBbz zS#>^Wb6lIt9Vn|hG9M;&e|jT{_)cc?WV6+WuVeapVXy68=TNxDwEH%Zl8W@i@0}UT zD6CHBJUI)#$IBcTY$?}xdulXPWJu8f-uR>yKlp;mG6@i&Bk4%K_1>ErE(T#LevNMr z4DuNldvmsOPy=aRaU0CXazkbFuP^hmEovG}K{WtjHUEj3&FTH?eN?M6vKQ}5Vv_bpKc zWeVi%)q`}+TXv?p=d}KW;dsWJB7+RTi-bneO145V231Tu3U6;E6cLbM6(`6PoCNG_ z^Js}&A36eo1cr>!dvv9B=lZkKl*eftcP(q@v(U!gax0Jbhlx9L8GZP_qh_9>?$gx4 zpt$UuqB|KtA0x>U=QmM(m%GO-(xCMM8!L6zHdJFfr@>3E-}Octb_srOPe?I!{OTV& z+!9WYot9!WqUg-kd&J54Nl7H*Dkt(*;e>=02H_klC%Zyn7oqpN`V?fUwNm zyWSPN-w2LW;=MD-zI*0PhOJmK1e2n?#FW|6v+{f64`zzlBzYpV< zVx-0kt0x3$#fj1^ayw+)DYy8}u*+4fBijcLZn7sG?%U%ummC>m*UTr9_ND?ul}6_` z1JxoWqY&V{OQH)1nQSne#uoF%sH#JOWXK)$S0=a*UsfzsXlAoom_VGTiDgB$ZMfiL z!cUTGzN5bc$G+cW*&`y22>WEOqC&c(>fNdF0ERAIkZzm(u5i@UAYG}bka|0BpQ6Ni z?GZ2e<9dGwUTDQFjQt@PQO%inHcP)0z`a^t4}H=WLp+s3#)*W+0fb?jBb!y5S>omX zF1aC*jA4=@8dW6Xu!))_8R?Eoq?B#skYtL(nx|;ep9n9bBZjRb$Ch|;0n89mp*>YX zw{7~AXt>hSXl=Zp!pC_4M*{r3m)!+sox(@(*ydetK)pP}WncB>Nv8RZv1seAF4TxR z3PyRQJV-C2Btjph{we3$hx1)fDu!T zfC8I1_T@d9Nz-3bANio12pVkKWRos@^i_SH7m3}TsZ+X_(Jznex16Lz4V61n>lkTm z@OG!0ILoiIQ6R zUDAB}rNq_sh}|MXPU`#woN^6bJC9NwaE^bIPd-|_+XJ33E_65O!@+n`Nzk(}P^8W& z(c;8wUftc_YOJ>$%H90f;sQFV(wOp$2uMlDf8*cvwl4`O&-p0{NngqlmVagG(i}@q zL^%Oh&kUHdo~u}_PyV>|&T=(B-LPwQNLySEfI)Y#!d0bAlP3JA$dh)fyCr zC0BFz+(wcdL#b6*)sSg0vFEovcWGzLUafF^4F6=wQ9pyNbi4LVYvu{azLX5qvU*HB zcbj1J`z5PvOA8Y1)i}t#wIhQ)-jLg|N==&#uB}QS?u`yOBK&UC0v&x zYQMFVm3c?FJ*2bok{Sy3?&d59`T68G8#_1{I3~*K7t)n3OC{~t5O&IeYgMG*C#xI8 zeZeY9Q)!Z)N<_A8GV&qyf0Py{JeEAPhz@?Ock`XxPbQD7JO{PkZ}UVFo^s7>z1CHR zsUZ_!&PSdA48t8&Xsxy5`Ndh%5t5P z4qHwUs;LlL6*#crl?8gYMqlXHzpqx#a>^k_IK$gnRX%5ipG4+hLwXM!+%pnTtL~3i553 z^DZYLH3IU_3+qfXE0t-<)bzhnlZ`KUV*%E7BF#K_A;{B2@lO8@ThtoK&mz$Ss;TXynnQ ze_IO{!+>@9aZPBSf;Us+{+Gm#J0WVal(ciU*CIk4u+_*%Btts3|4&ucVI7xatL?tZ2!K`5DZiBP zcxQ>aclZB@P2U}gSD{=|0pJt2g3Jj4MYhKna+Ybe_0IoOl_5?PZ*ZRNR&SxHP>u)X z1Pvxxm_Oyo&I~egF@Cf`PxF5g4u=f+72&ao~I+@Ua-87^4j}LRcp6HI;&s z3VCLli+dh4g`2}gh0Fq3|k|Er=35F?x)(q zE&n?yA#Olf4l`AP83^#MEcRlbtAJVRC?xJD@SP~_$>?N5>0e!c{2U{_r4)wEj0?Ui z`#A!btp;FNUm2xZKHt64wZAjm5VD-%;GA~5-aK?c`0VrnH{ef2tc+w`YD4s@vOF7w zgO=BShlUC_PBP^89GY%>%G_!}X)wGa2hjiXmV^9J>k@UelIkhRyGMk8^1sPvkgBsb|`tVdk%!YUKZnZ>_?-P`v8KeD@XujMTng@^Zmy3g^zHg{g_%bypJ--TKli zpBG^NkA7&WK?(hrSw1(uFJ{?;sp{>~Z;tK4D^HTF-U;st%ui0+pH0&99-t_CZfCsj} zBy;C5&4dqxIf=byybTt z5)~KUnR+t!NZ@R($K~h1imjiguV3y`b!fI_%F0UUl$^EoUSZ+u^2aP#kj}AvRp-SX zZbn97-t9E@$B2x@3U!HQBpXTK`?Vjp;*$cetYSBVGgq{WC>xcYNatzio~v8u1+xzx z#`GHQqO31?IuY292${F+Y(EHkGsX!sO_nQKI_M9J&EvBJXMJ+m4yXNoLqawhhnE(ttv2(h!+H^nT)##93x=mvkQ{v^JfnfkX9)3E!K!U8~T+Q`#_dY zZ$ClrRDx+icWYokolVYBVAcG~#L5qoc-_Fo6|DL+2s3`*1cukXsQ(rR5Zh{}kY$3a zFp4h;ygz9FhAn89Pk#;wgK4u~c&k%971|%IuljX^t`g53SUh6iKuU;UkhhV)2Bh8D3R)$X!SDW1#e>XEu^In8(ya>cL~aD4 zv_-Vz>jR68us|+h?`1s_;N!C22z9eoe9`W5o~Dxmh^+tvKC9sx;JEwc#_=}-yN&H^ z)6KF(?f_u;Y~Pb>yN5|^;k%frzZplP++iqwtK!b6z~ItPz%8N<5Yr1-@3!JP-=VYF z^taop7R$2&x%IpP=kSSP$3_ac@&Duq;k1>85?ag!1NKg8Zeu^tiWcfY};TdH+K^{-?21@b%$eN;#e0 z{P1;eOEW8OVEI$L%)!jaLS`V&GxKlt!H7HNB@}gf@zN%U{MVV>{Tr@Z=cUxZqiyyq zg`;>j8@F=J(-;4CQ8elvf}yOo$tVJ&{=7cK*3YLA$Rb0NZS>ph9lm6{iuH+iqP=J+ zC~x>R=q#~(ekUzITt@cB^K4Ljr^5OO&1G!M|Ku?lhP&^+EXWU=E#r#05b)u1o&(Q3 zDrexo9>ZKj8gT}G4=p^n3`=P0GiZMR#R#3lr%uY*My``ym5TgZF)#S7zOM84D!>cG z^<0^oKwmqaNd1?ALPBEv-^zk5```>zt zP+(~``UnCiv%^ph@8!UVCMbIREy6iQg=SV45r6c*sgO`R)GTXLr>8L-_33SNBDg2~ zcz7_Jp_JFT70rQT{+|?-px(PlEv6i{CWoHZA)V2nY8=|+>!1XFQa)dm-_*Vk6%wk3 z41?bv4#I0iVCtVctL|t`8q?!9S3{Kg!(-4_h%g6Npcg|7!i(&?!7O^do64}i|Ja%! zi}eh<|A$b4=F_z0bi#k4!0aG^>sw2OB>r4!I%GveVD&TImg)|pydMV$|66-6S?DOD zCChD5uWhWa+y$tzZ|RFuC?K{v|DW8!K=Rc2eYGtnI=CGtG)tx@`!)f%8*|!8A<5g?!MOa@!x+ zuADMT$&Io8TNC(ID3a)0Edqd1klrA94P?bAg`GSb;ZnFF=#=ws87Nq{2Jjr(QmTT| zG9-=qOopOIs~rZJPzjwo>gn#s{o8DnC}6nHOs}krLgK?RTh(ShCQZJ+sWaM$|ZvSuA%k*-Ub*PA-VBg0Fem>SZn zA0B-DqqTve&bFaks26UvN>(vgBJ&$E{u1X+RYf&jfn zPyW#h3>=09hQ~V}ZN!qzk5>x0d;qel&u+vQ^%f?2{;N6{Me1{4BoC5uO{cHWVs0Xr zTnKnpetamPnA`g+!f4D4oMlA3%+LsD)l!!4bGPqe9Nn-OSqenKZU2A!KE(iZ=qX%km*u4y11ik7}B83cLpkvcIGGUjmSon-QCB z(bir_Xin#>191bJuUXX+v^zV`&NnWf`Bz7q;Fxf~&6VowS*ZkcK$Wi=7w4K1G9^Il zuMiVByh?^or%-NGUtn1@M80R@d&|_zvHptxC4g`u#9+fNJ#9mmygtv!!3rUViKgA{ zGd7O`*#73Rh3vET+-X30{H!HH`Qcfs=5Gfs5cyjAMw`*wMdgT2i zK4zHf=KQX3^TpJUdk(+kT7)AU9X4Nk$1FFud(bk{Vs&~3i^-z~1J>mi%U;!tOZxiU z-r*rg#?PW)Dnv|~_yq(qEY3csS!+-QBUhGZgcGuBUZ)FnYc2agck<^Oe?g%9%xUH!#5RF}c+~-2}A~Z1{Zzb_)zz!q+;R379&o9ln{g`h3jX;WPvM z4u{~oTJQ>FHGim2{|5{Q?JzQJ|G}+SbgX_Uzj~Qn$v~)6&-e>SDpJv=`haBB!OKIxlMr@$IYQXC$CVl%U0v8H3p?gNWlJ?Vf&JmQ^WCsb<>>pHVpRlAl5gr9i+qAWqbLeeEF|xo012SMJzP9OQaT1cS9* z5th3yL19~>hEssE4CaQB*S#&}89C|hk78CKRWuk;KYa$0A)uZd_m|BCqv(WYuI(hR zKL_L9@~gH4Mp0c3_4aYg=AXGO4mgItTwRTB@sjIyPkgU}`bZ8&D{LcLyaD2XOvwqH zs#8lipmxf$e*A#E$w>O@C2$@kCiHv9W;W zvmpnfd_7?K9lbzbcq=-LINeN^1v5m_u<>B?#bO`Hy_iCQU6g#50@tmXvNUNGop0kEZ1ZTT?n zV&n0FMEPu{bpLCx9&9oUU|a7R`TdgQrF54-^_r*QOz>!U{c?i?tZha&Qv~=<{EqgH zd}9encuBlc<_L9;26Zfkdi0%p6r(M=M=?#j$ONb}*GqXv9F8AU12K14Anebr*G<-n zAWdfR#3R(ga`M`{GO4qH8y;3aNTkHx!Sf$#(ef2IqT;XgIDiC8*XsBC;~stuaL=)J z3sl#9cOJp!TgYJ<6;PEKHc`Y!iQlafn#cS?)VJ{?%!~eDv-t@>T`g&F)ZMf8SXCN3lKktlXAK0Ya2~ zBJ%95BUmqK4U0)sEwNw)qbEqu!Z*|38_E{gX+S7P5#5RnmVkMKIgf}ai$n1TKZ`o1SY~37sSoFV&ue}X@{eq~x}w*sa%sI8BO1!% zXT38|s?Dx)8#jI>s^{*%b^paHj|xIaTP_1!A6s3^n=)a3^pg(BlKf*4xcMH~t5}Y& zvDxM>-kUi2!Y}e#(VGH&&ccM*p|1n;qKot#L;~Vk-rbliuXFG*WnZ_2n<3hZ?yM?S zgK48;TkrH+RSL^o6D2J}e}M$s!)YcADMkVV97!TJ>BB*-w~3Q;1Ff?Aw?V!9#?as_90c(_r-EDj=Y zAlE5joPJ}?UdJ;r9h;G^Nbhc*K4#U;{=0Wn-X_C+?~D9bbio@mK0p+_emEB{aBj?4 ztGsz(pPliq?&j?r@ig!2gnBpvEfble8f<83dAmn#c9VVfQlG4WbR>dG_B9+n^cUew zPUTG5L?ri%ksm%ITW4Xo!_MV=Gca# z^>SD40b|7|p9$(wD2Bd-E)DATgUh|Tgv6n}N1iI$8w3bo&&s0S($uMGt*!N?vi<1I z*M*JS6i65x$vdTbSfyh)_3&zEsBN(1#T(>S`+ez9jN>M+D}3XC0J=QD>4Z$0lwZGe{zqe5aR2;1No`Qdh^cNc?afE+?_zlr)hd%0ZCBhsh)7->=3$q zUx9@f`BCTPQ!~FPzvK03?f__?CkpIV zfLpVhIGoL6V+RY*$gp?dhwMA6-?;a(Wc|JtywUl2@3Nb`<iob(@S;yr}qU(B<#_te?O0Y#Mxn=jZeJ=S(C(=>k9%Go%+>* zg{8N(iR&_t>P{rBEXK&$oJHW|zuucKTzeJ`IHl$H)({TJ1a608l8_ejIqEokD#pe; zOJ06U9tez303f~7_c>ME(qcx9F z-HP5Tww;t#^n>*R7z97Cv-sM;n2P-%-R z-<%DZ$L8W#%N6Wp$T-nr(q@l58MHUE1tZ1r-a0GxwKCFfi5yPAO5 zejLw9RfHd+&3JaqIN-D+fLibOue3^?0Jd_q0ZB5roTNVt#-c+;O~zKP^Wt#kvgg*P zY{vi=N|({?wx<{#2H^P&xBUEK?(C;zuLGD^-K(r1u-Lr|_pP2yB!CFe$%vk;DwFxj z=Dn&`RSjRPRpyV`ta$YF5du!WapS)%%WHG<{nlsX-Y4bW)G%l4f0?j)xOTNlZ0`oae(o|CnS)4%$(tOXN>gP2SB< z(9&I8Wgj_+)%eqT@Lb`=y7!f~&BTt)o6i_!)&%fe30KV9v(0fJSWY^)cG?+!E@2(d z4S+@0vF35}jE?h_diU-o9^3|1J;p@YMxxOxPk?K+#ur2aHe;;f4bR0Vs8mhwHe`O+@1#(D6Ur zDK^wR!HqnXa*LNJ#YKJ<+MZaeGpOza>9$>?=??1WAO>U;czLj#Rc9B z3^l>&KJs>{pL&CJ_mRg%-{m-)`(J7(g+!aiW=MiKqhwCs@$2NAUf_{T?mdi?uSqDz zKK%O=1SRVP|IKn!^H+7)kSQ6n20woLl+Jr?-D+ga9RM%B3$j7=FCTvVi2sVZEm2s1 z)3VGr$(>Gy&p7eHsb^5vg4c@>HD+<=sZNrhzRqb>>yt_W8*Y);5_ylHjDYTp=hGGE z`4{uE#yyq7Z$E(W_&O;)Vn@Gisa(r`xk`~k6rlf8Y;};|@R~ZbkGG;C>OZYa^Xt>T zSMObtnk6O02O^}qF3 zq%A1CM_!MWNuokM%gOXc6cEpNPZIL7KhZzC@t=9N%K6LH@#+KvzT3uU?oJ%_dcD@W zuK_~!&vMhsRjAdiU2!Ys;nOo5pNALI^bf+0`B6fG!2?=iX6!zG6t_DuT=$=8M;JG==LPlCiiWv7l~yJ_0e88y!X`nEJS|PRO~o%> z|Ne_rc%{py#=_1^vSB%$su6mvQ-Tw7N1 zoy7=!NTj?&cDe3a?=2<_WBpv3S?WoNi`IB_>nqq#!;re#_Ec~gNf`-TRyu`dCPP7s zC#?o_6SC?P%hhp-pQ%S<1T6bW)e84NW5FQbP4r-iJGl9wxIQ96=P!Qp!~ z=^S7&+17|3go*Nze7Px`Ac>WeTc}TygFG*kE!WKFW8XP)uHGXUREzKhPP#ID>J9Es z;(xiHUyhq@W0z(*f50p+*h$ek@6?gBr)4U(q|UP$>cMy>pK2P@&nLF}sM9M>UuKag8NksX`zyyL(aY4efo7OS5Z*`faPq_Ii}i3p3i7S z!)oj6+4u3PxpL7jx&fapU3SaIv$|U+mWCGF(sH&xdIurpw_@o!Pfm$**~Znrq5*~P zKqnsO91nJ>wjmyZxvr4*WaHrm-t%QXCaoIDMHX168f zj+->lAs_1jR4mQX$fPCOE@Yl<>^}QLrp9ySNEBA3rnvzAadu9#9zA;HN}wc*kX%0P7k-pSLMN5jWp5>7$-?JUno@Ba2P-7~LU7 zPQE3gIKAIc;A@1Mo=(rRyWDCxvCfs8jbXlPA-KH#;r#&5Hy*e6Ubf!>p=*97)Oqxo zE3>+#JV6nAL)(WWe@k?o(4hH@Brn(aA_Gj>{Cl{_r7WbWl%-xeR~n^|=hIoy=of5# z5#%--Er*>B<~@GUtA)C-2^M@_I_W2O6)-?`kGp8+WfUqUci@vkBFkS9Sw)`K z^65|8Cv2`r4AET3%#2ipv}-b+-^$XD18k1K6~)qkN`wL&?;TCZEm*oQ6Vq75pBBEx zPXZe8tHfHb0#ce1KjeqV@O}hE@}*krg{pS(gRg6|xv(%f0XuqirL#BuSH&SwA<3zl0WFS5U zID1bB%(x)~cTxL>FDx!T_E}s^N}c`XhYKv#y9+ZEL%+*WfLE%T<9=n=xhd{D-M;vXNAy?dp$OyUf|7!>!qQR z4}F-12h+?moq(L+C8p$$|45=W^UGf=17CyBK}I*qnvW~QsrfmLo}&I|Y54NXuwj5= z!p$lsw~}Vgfz=$M%ea90hJP#ijeMb>t|%`@hyQMGJzmL|G#AwQe($6bf6E(}buM@N z=shsm$&{pjZStUgPr*i`yQNjhD8M?W>v)A+!3G)C%)N1vps{XG#(gYBQWyN?QqFRn zj3Mrh1CYKJNTL$mD3Ys9lB9(6d+W}$+{%g`_P^~OQQz5MkaVneUyb9Dy|7Y$wQgm3 z#ftgcEt~pU+P-a2TEO+ducV5OS)hh(z3)fHNOzENj*DkokNS%gX@k9t5wI}=-bR%B zK$Y(|Q3uJ}wNs*6@6_)u%$f|~HM$Vh$RKsK&WQbXR(%Gw;_*ndqp4K9DKZFHn!Huo zTFg#Y)4P9$mf-htkqRDhPM7BT8flspD~@yokyXoF0U*~)s@n3am{ZW?8NOltp;#9{ zQ(KeAJ&fu<=Vjn}+b$k{Ncm!+Q(fFmh5*iRun&fLnB_wq=Bi{*RHMnEg0yx0%6EmQ z_9R7-e2u~9OFhn(>+C5^3q$y#H!&yJ^X3;z@dKj#aBF9Bk<zDN$<(0 zErc%1(3}vq!fx-=Ky!-$r)uu2=kb9Da=0?bQd}4O0M3B)Z5B4%!pQvf^a~kR(?Z!L z=Cbef>=P};^NP6bI!SzdI0`vM0zRWIPd@5oJR@U${%s)e{$b&33X<`iNM+PcHm)oQ z4*mU)$i88vjk(p52w)QK8J@WheAvfd(c?d=5-4D*_N4?cMLRn4_b#hj)}l3mBlzuW zRb|`>Ez^QaAND74--r*8!ND?qh^GF>fjn|s>8KJ)wMppg6e2rtO>?>n*FKWs*cKkW z2Rm-!VH7AflzDm9lZG!Tv!e13uNBzKrq9He2^%OEAHtX5Qbo2U072wFbV6$P1;S`Dn57aY2MY)Z;nb$gL4D6MK< zr$>jCy&CeqC!z6cXVSENYIYL+z+>9-i^VWwlAuIkYX+_llQ8rUzOrrkg#B#wTZy&^ z7piRD$BG524p6jAIN3M>291MRDxnA436`e|UVl~JtQM$tP;CAA|2A@O7H{Rjh1JY=#I$gU9`$Lzy6{WscUsDYazlW&@ ze4N;g7qTT4SUy~x`JulvpO%!fs*$;70gS0kF0haLTz+cPXtL9M0wd(5b2w60gJ!G# zXSEBXicOi&G)houz$|q8>NM;V!+?CUA=e4{PPvei zUybz@%i0Y3p@%r<#?iajjWJ7u3q~Q%L*X1?hksW1`{D3JLRPf-{I{O@tS@+c<4q%qUYDr= z*8V}U#BVfIa&3~QdFX4XJTK;P_H)3w9y;#PmfwHy{Y1woRNq?HAV{tlulbF~)O|V$ z7M;?D{9qOR%`_`bV0u=3#3;skEY1H|S>=Iy*ppealz9>r5982r2S>$GV11z0v966j z7sx34>T-8AjV3X(e2McqxSv!~jbET&V3xlX=}RuBq?A0y^v0@%_+J8d$;U?>0QE}+ zZFCG4608-1-jU{^lVE9PasNshz8%wWjCO+oNfi^()U#Y?{U2UhY&$NA;!;F&29}mC-gcfjTWl`m4ugChgEe9FutqpH{K%kVp~i~MTai;lVgXh*S~j6 z{LrEqqB0qc1NVTS7vPYV=2xf-XKN?k(QOWJzT7Ox^-P{#%%)u}k~z85Dv|w$DqYG= zO8Ato8t@O&Red;Dkd)MepYE6;(vOCHce*h!5~jpk{?%fT%48q|d?x;5AJ<=IdS!*E zCxu+^AA_vpCjDCqnPY;rw9x{?hJ(%bmK;NLUn4=Z;abcl`z+J0O)|as#_id!M2`97 zq^Rb^dvfU&>V+m=E5-J@c3g-7&*Q9#@yD65O3-@}UA$Lsm(ubi5ga}`VdGlI zQi%E0S)x@}w;aO&LjQNXKEtM0N>bwL?0nkz7Or~x<5PI#Xu0E3LN7j?)UsuX z#9UU4oSa&RI`AayiEhQ}CYEr25c?4IIXoAeH0pUBB>JupujXy}fL4n`{hDPq|F2SP z_-B`8;IX!t+o~G(r7?Dp%DO8qZHNegT+3sEl}Wv6zI{hP?zcFEy8{yjN3+&w0k8f2 zKRLt?^l5}@0zAQuwW7BY)qeAB2-kRe4^*D3ua>Pi^jN(en)x5Nk-#6vCjIR5QcqyK zkI*>W{%lHAmr45yq2~)KX?v6D#Kb3WjCMG{`fBN9o6}hrW&Q{}OrQQmVR}xz9;zKT zop_T}rKAH!tksW<7V!E$1y1Te)n{8Lyc2toEbGfCrM?e}56J!FrEbM{)B8t8#0*+* z6V`74#>Dc#!BuD>iV6x+pq3o$HrBu0!_|fVERJQQ;nzx9*jr|qNsamAt;BmcLcg|r z?6%i4R$X7{3;c5A9y`&d@CHbay{qg)fG@QWQBV|)2P`En`8g2Z_J8bu7$b>`W(hoh z>3`)C_mt^2`pvlBmd{%3EU?UM@X)LYIOa0k+0V|~I$GVjA=P43PZ@>HGuAWPBy1kE zK6+7X&1+WX$M2S6;P&*fJS77B{2++r&-b~bFgby~dFw6dnQ+0)0{F>S27l&$;jOu`a&=FCK`*DYDM3Z)BekM!zCiG;0AD^NT;&j_@uR09uL8x zuBA-?--WUcK6*bebVPYf(&f@MH+cMt`}Q()yo5+HU|AM!ad94-uxcGdell@hwAAoZ z@+pAb=6a2eTs?=7&@(Q_fz}FxqfMRv(hJMmTPyqX8UUBT^aK;g*XTU3p7p=_hG@E}=VhvxRG$a}bJEJpf8`OR zBz#;$-7lg+Qi#+?i${9)4vR}J{)n7l7_R#5@fMC|9!pHr>Q$lNB@47u?W2wS&a^7B zYoex69WWYWD?b4c7ces!^+(dYx^88CCq zra+Hx0{p;@x=WvjI5f6R(|)Krdfr!tx0z8DPp_MQj$6vQ6VG576v<@kG3LJ}$1d%E zGt=A{&uoTbVF(%H|M3yB3tyb z4`4E8HC{#?AZ)j+3Uc39ER1eFo;wxQ?l}iuoLiyiVFItEAt&1sJbZfWcdJvTYKY{& ztx~O$w$d@|Qf5caUISh-2EGQ0n$n)Cap)T_O;*Qv)P@!d8I6aQNQb`!IBE-cJ$Iyi zQu9ZC?(!+a(t8?e{iz#23R0;8SMuGbvXD8mtw#0W)X5sfFQD@BNsL znCVM^1S=frc4QW>oKG9A#ossyZ%DecDRf_(X!?5H*Rt}VR*mYncf}LeZ9l3YoNG;A zyZUAUA|-j-w%ynKbpO_}UPA}X*}Kp%&g{eK%huh@Do?ELzoN~PwX{5oq+x0oZJv#* zJg^A1^71%`hu8yD5mSMfe(A?cF=19cVA~~ZOMHt&1Vci#PJVStEgpuV9+p=0yA6EV zkOs~Dv+?tI_v6;fCknw=x&V1(>RCagtQ?+bv=%(89>2=+%Q1cH(@1H$WcC}p_sor? zfj2mA?hTyh{S4&}yB+iX%0M*WnH}FZsApQhc8&V-j{ z90hQXIYj;|R# zzsyCCDk6=s5s-5XtS?D_ZLs>@bVL+N!%1~34E|9#^PEvZcBY!Z?{&LiojKVf(~PMj z#tCqz-w_i31u`5EZgX@1e$jW?7t{Mdc#%Y&%x7681(=8rC@u2?yo}o8{VyqDDnxva z2?i+fYRLGj<|X)Y+zOln6FdO>Ubz1La*=)P04O9@gVOIwH(Ka_+Q&QW)0PaV{BSlk zZ>LMd)4~B)Qv&s3_=7R^ggwI^Yg>WT;tXo|Z+P*HvmEFSaT>Q4s+2f2UnaukqIb*k z#Q;pYc0yU?>-x7%+_kIh5Z!Ak;NQ}8Ti?s#xN9Xdpn4y&&|-7a)T=gYD%@W9?uvz3 z0fjLOdHG9rx!ZQXLf-y7PK!BZrq?A;HJfdtRO9;APT2cuVZ`q#LMjX$yW1MD|B&Y@ zjp1^~la^k)Gcmbj9In@*z#{ZuI_a=+Il*;j(ZlkJJ%y@L`E4QkUzozpC%nJp5l!#p z)Vfeo)0a&WB<&+k4_=t~4kvqyG@V16VLJ?u#}BHy_ImFTfdwmp8)} zd-)?D`9By@N8Ad-ob;wFQ3zZyit^-&A|nP!8Sf>;%1qk4(Yf4W#)aM%I-@F7Ki6<9 zHex?#QPMCtyw|jeFN`2GPKm*=Esal!+a-22i_uY=PP$8tw_?b{wRj*l9L;n6XmCP& zH{JWS2^Vn{GmJAKyvtkm0Y5#B_i;=Du1KVh2!%5%T=MwVi!Q7xfY9~NekRH^i+>aX zElOwGfnF{+8VmdZiaKs z@p+#`^CZHK9jt3xq!hM`FmfACqf5<3Zi|?s)Gg!*^gaUrRUblgBX3KQKVs9{F-dLo zu5H@e`td|0_?Q!3!;bc*0eo&26O-WMc6mf98j`BW9hheIC5n~I6sNF%WTDd%?$LPG z^}+*A^$%e)BelmT$9*_8wp8gj6o|}fpS%I{B}czItiiX^b>YoeHT$3H#X^qjD&E@3 zOF5oxFs7%>k7BjvE^IMdb(q+KB}6Q94(7At91-=#F_Y!hHTy&m-uQ7B>tuPsEcWuh z(P5q=l`#OHGM&T{X8k0gf?u0F8jeTk66lM481z?siER`fj{era38^C)+JYVb>DEx$)dsUJgXYi*}aLIhYxK0XZllHRbmS6B+^9Q z!9@dFQcvP$SO@(nZ(Q%qyr&O1ZJG{vtzHu?3IKsI+dB*u$IG@aS}&s&yHE;YL88Yu zV6W@ql5mZ5jkwaNAWl23fA{;a>%(DG6PA|5x1`kxflY8^tIFu1Pr{zlSRbf7u;_fTzj#*=ZzLqNj4=p8=NeepE>&cZC~qjoDGVQ>xArfdaj;O?+F>h zzozFs6ll7+$WN2<0nB%%F#rPfQc~q*gPX*8YfCRI=%xToi-@C^f8LH88@Ve2xNi@A z+%^7hVpg-~;eR9s5s)hmhS+&7&I^2R3TVV}inr0Q{ioVLgdiJ9jS5j?FJJ=tvzJO6W$9~e5%jhL#GbF@neJ=wAU@A9rIJKcooT#Nd63c0<5_S8n~THft`WGrF5b4lQ#}KLE)m z-HohUnrqjw4d7-j-OeupRbK^U@uZ)IW4+UTIxK5@(q9Q8*aO9Kt4PDT*JV}o)L%!u zA3?g6uRb%>ctk`-Cz5eGd>qJgBKk9IQf>4&BO_nevEHk2LsqV5Qy)-Nz1Z zM;;*D#)`-)4jUpLX5hyYE`GYtQ7k3&rX?Zddv;3&@B7Dd3V_Ib+4Gu2M(Z{|efYb# zNN-$N@lA$Wrac8t?FH2Mx+r5DT&=*~?A_p6RA5*#|L+ItxqL_aZPwI1Aq6QMGBNyD z9Y{&1CGZAmv&?+|9KE~`MbSXiT(kC@z{&wiJ^h4vFt-^y!f!rDr*QJM011kf_kAue z|H1`t02UlQT=&5oX`RWZQ!6M77b0f+iWMG($a1OUw3YiUh`B_oQ#N#x$&>j}c~Q5T zUhRu6>H=p{ zua{15p~o*;n*&}Eg)ft+e1)qw$5cx{Z_zlbT!&K>JBo(5fm4fS2fU00jOsp}>P(#p z9o;{!UvsG&-+wGJTZx@(Yii!}ib>@F;S_%tN-S`t`BpSE5|dLz$T;1VE7GqEgKAQ4 zp2^wg5@qQqrzY?_ivrM!CB|WGg`Jn{Bgk#8kz8rTUBrlTPcB2P+jrDIGraBtmpxSV zwD~4w-i|#xO$jtj2dy*wu#@H6W;lKlfLrG8y|Z19e66*e47j>VUD*2HJ2%)& zSkXCInlF0FroYYVDn#9c5{rYdt;f`;S=H~0id~rLHNxI&c7QV&)3{Kh!}`8#e%ggn zvpD&TOpS)TA(rmX`cYI+42@1tjp1WyMW|T%p}iHclqFw8j%Vv>izxy$_dvvPD=M5TY_L{9WpJ1-8tBbFK_8x?;Gd;!n>f)e#-)WR?+jn6GtQ-Yhwkp*hvg`~$ zP;q}&3{XSJ4ey9!awxA*!@QRzKQ-d}6PQUOB}7*AdFUr6q^g|^D+a7y`z|?>gZBR% z5Z9{Dr~n5#I_yju-iAK}Mt$1!&FmE9*m2Q^DWpH1#v&lQ*E}6vMAYkU=1Iby_gh72 z1M7dBv|@XGT$LZC_{P!L@AOpKXDUIkKRBqQHGq8PSe|6W)JjLF#R#?C* z;&i;k?kJ_Kh(b;#s&!UUD~2Y?myJuL`wMa`0{RB66CWb@o*z1 z^Rp*zYvKc+i@+PBI<05q9Z!cd0PmU;y#`|sWSLyheAso`5v#J#RD|q%A1=YDHbr^a zGF;$tnWTf69V}3&r)FJ+XPtj@v8l0+fO!T(81I~j?D3g^kJp6+$K{`CUXvYf7=>6g zqzdiapDfWA8hYpQ^R_&cr1SC*LZe%?nzrQe&qY`tt**XZnnY0ykMB{|HJdCeL_$)qILpHj)7%$XX_WXv6Vrbdmeyc%6F$^6q21pVd##6R ze>_fiBA%4?$@)(}Kg1Ck@?& zod`em(3CwL1k|e%g2+Y+@2lvWdjPJBxJ5>NJ$0uD8CgCS&cze?#CbCdfdgH@N^er)wy(7FN$fS9FRR|FIHF?q}-u!(RX40LO<%h%* zJLWHV@Ibwn9e{H>=-JWVrIB-{;NGsfYTy{Z-`!4gF;|%eQtpjuVB06@?MoQ@2bV8= zKH~Y`U5UO7u{RroETzjF@8zHf|EqvwDuF9@n9TO-Q$)F5JLgcgSri`h{_{~$3LY5Y zc1D;9na|m8mr`d(Te^tAfd(vLSN8ET?=)!tRfWAP5k$hcxRHX!wsGs<_~94|En{Y0 zP8i?VEW?z6T!x>9ISxx2Jkss@E-qd|u^TUe& z1VUks?Y|T%Gc1cD7dg~uJO5L?4rYjOtr+YYQ^l2DCwxK*4MBoChzw{Bv->vIs)Cf1 z`JEa=YVyD3z5f73^rj%52uTmQL9=sx>@M%^#!$#u`zeB69)c~$Fg~b-!-d_`Qd$I+ za3Ia$h!4s#v?*feVArAFT>}tg^Ni(qYZpAYb`dQiB)~(l8OCcqH<`|7ZlZwy-$7rd=f$M7OqVSCrMRi;7@iXwT6h`r z(5)TDzz&&McY;RBbNX}FgCg^0u=D;uFMyvyni)QHL8Yd_ulP78zhKhqKPwb({r1VG|cbk78E!Y1(TF~ZS4);Ue0rp;Ez3;nX- z{SUlDj?9~(v5$J1q85yR+=d07o&E)}?TTQ{k8VKFd(Zj_R3{QD#U&c(dP^wiU^Bx7 zNV%J2o4OIXD{qMe>s03NAHb0`gRrTU@JhiBQCzr9@}&A-=FuDA>Z9id-5rnRu zTjyaeygaG1xh5C_iID}G=j74(%G2$wPajfgeep>1u~_$R3lbx3T9F{Gkz@fwr{7)w zoRPPz-!OcpQ9?qNH?=fN?O6Y6SA{^ZIl}76tU_GcT-nZ#i7u>lL`9uAJ2TzOJzz?z z{=`j!HWpg;O~|e2s={qNUd+~A9hT9$Rxh$%x>}lSzkaH&l0VcEJH-p+O^%07ilTSt z?|W0rLvm7R>OaPW?DGw+)5rYs&|R&kocqHt1)mTCdQ={tD6*kl#P<+Qj zAvV62%U%7#ONU1vZ;BJZ@((ln%K*%MU~RFJws*jm_3~v&tl<^8)_QHf{ruc;sXxaI#f4U%E+;8%HVkWQ9Ehtut%LK||ue-hsdL_Q1%f z=whF4axwD{QSmgXu$~cvzuPm&cU}m`vMF z;JC*fB+A-8gBoS8Dmlx_YL`2cv}XCN$4Qh7Q@snG9WC(_iz)H90m?xMEf$uU*OBw- zVKk0B>QTpbdEQR)l!#gAEiw0Y-oJVC&gH&5k1(3+MP>J?{S?U5l;6ESI=z>uj5682 zMK>t>xKe;p7^OHwvmWICeZ(n81FLlZe(v|M!ImI5D|ztD&}w+as)W$|rw=BrGC-D{ zDqB~5usU;JN>q4<$Ao|O3ZtsmY?}W4jYxUq#s^d6$?%!(j$%S-M8qE;r2+l0>PpY! z4*cb(jU)GJcz=KAxas=Sf5>8!^Du^heYNap$27Vigdn4-@<^xD5BNGj=ai(0J1pq@ zS79+9Z9X>O?rq?VDjeX`r7=ySX!4b}pLb!u)l1f$Op@{1f1`kr9~TaH>gP!M*R5D) zb3P%fULey*l%c-ZS}RPV%3zd@Th_V

ev$zU+4x6EXMSw!Pr5zC>v89P=F&O%7#T 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 U3lzRE=yi#Hi7fy4{qOky0cXE98vp Date: Thu, 30 Nov 2023 11:31:58 +0100 Subject: [PATCH 30/79] Add various improvements to models importing code --- Source/Engine/Animations/AnimationData.h | 2 -- Source/Engine/ContentImporters/ImportModel.h | 5 ----- Source/Engine/ContentImporters/ImportModelFile.cpp | 4 ---- Source/Engine/Graphics/Models/ModelData.h | 2 -- Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp | 10 +++++++--- Source/Engine/Tools/ModelTool/ModelTool.cpp | 4 ++-- Source/Engine/Tools/ModelTool/ModelTool.h | 4 ++-- 7 files changed, 11 insertions(+), 20 deletions(-) diff --git a/Source/Engine/Animations/AnimationData.h b/Source/Engine/Animations/AnimationData.h index e382ef6e3..623d9ea6c 100644 --- a/Source/Engine/Animations/AnimationData.h +++ b/Source/Engine/Animations/AnimationData.h @@ -151,9 +151,7 @@ public: { int32 result = 0; for (int32 i = 0; i < Channels.Count(); i++) - { result += Channels[i].GetKeyframesCount(); - } return result; } diff --git a/Source/Engine/ContentImporters/ImportModel.h b/Source/Engine/ContentImporters/ImportModel.h index 02e6bfc8d..b7af2789e 100644 --- a/Source/Engine/ContentImporters/ImportModel.h +++ b/Source/Engine/ContentImporters/ImportModel.h @@ -8,11 +8,6 @@ #include "Engine/Tools/ModelTool/ModelTool.h" -///

-/// Enable/disable caching model import options -/// -#define IMPORT_MODEL_CACHE_OPTIONS 1 - /// /// Importing models utility /// diff --git a/Source/Engine/ContentImporters/ImportModelFile.cpp b/Source/Engine/ContentImporters/ImportModelFile.cpp index b34bbfaaf..64a24217e 100644 --- a/Source/Engine/ContentImporters/ImportModelFile.cpp +++ b/Source/Engine/ContentImporters/ImportModelFile.cpp @@ -18,7 +18,6 @@ bool ImportModelFile::TryGetImportOptions(const StringView& path, Options& options) { -#if IMPORT_MODEL_CACHE_OPTIONS if (FileSystem::FileExists(path)) { // Try to load asset file and asset info @@ -44,7 +43,6 @@ bool ImportModelFile::TryGetImportOptions(const StringView& path, Options& optio } } } -#endif return false; } @@ -158,7 +156,6 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context) if (result != CreateAssetResult::Ok) return result; -#if IMPORT_MODEL_CACHE_OPTIONS // Create json with import context rapidjson_flax::StringBuffer importOptionsMetaBuffer; importOptionsMetaBuffer.Reserve(256); @@ -171,7 +168,6 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context) } importOptionsMeta.EndObject(); context.Data.Metadata.Copy((const byte*)importOptionsMetaBuffer.GetString(), (uint32)importOptionsMetaBuffer.GetSize()); -#endif return CreateAssetResult::Ok; } diff --git a/Source/Engine/Graphics/Models/ModelData.h b/Source/Engine/Graphics/Models/ModelData.h index d7359d905..8874fecb3 100644 --- a/Source/Engine/Graphics/Models/ModelData.h +++ b/Source/Engine/Graphics/Models/ModelData.h @@ -452,7 +452,6 @@ public: /// /// Gets the valid level of details count. /// - /// The LOD count. FORCE_INLINE int32 GetLODsCount() const { return LODs.Count(); @@ -461,7 +460,6 @@ public: /// /// Determines whether this instance has valid skeleton structure. /// - /// True if has skeleton, otherwise false. FORCE_INLINE bool HasSkeleton() const { return Skeleton.Bones.HasItems(); diff --git a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp index c7ee11b42..490318ba7 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp @@ -1117,11 +1117,15 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt } ofbx::u64 loadFlags = 0; if (EnumHasAnyFlags(data.Types, ImportDataTypes::Geometry)) + { loadFlags |= (ofbx::u64)ofbx::LoadFlags::TRIANGULATE; + if (!options.ImportBlendShapes) + loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES; + } else - loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_GEOMETRY; - if (!options.ImportBlendShapes) - loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES; + { + loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_GEOMETRY | (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES; + } ofbx::IScene* scene = ofbx::load(fileData.Get(), fileData.Count(), loadFlags); if (!scene) { diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index a07cd8f23..6d0a418ad 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -787,7 +787,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op switch (options.Type) { case ModelType::Model: - importDataTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Textures; + importDataTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes; if (options.ImportMaterials) importDataTypes |= ImportDataTypes::Materials; if (options.ImportTextures) @@ -1036,7 +1036,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op // When splitting imported meshes allow only the first mesh to import assets (mesh[0] is imported after all following ones so import assets during mesh[1]) if (!options.SplitObjects && options.ObjectIndex != 1 && options.ObjectIndex != -1) { - // Find that asset create previously + // Find that asset created previously AssetInfo info; if (Content::GetAssetInfo(assetPath, info)) material.AssetID = info.ID; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index ac8cb231f..5a8e90d3c 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -24,7 +24,7 @@ enum class ImportDataTypes : int32 None = 0, /// - /// Imports materials and meshes. + /// Imports meshes (and LODs). /// Geometry = 1 << 0, @@ -104,7 +104,7 @@ public: Array Materials; /// - /// The level of details data. + /// The level of details data with meshes. /// Array LODs; From 6e92d3103c67b09b662248f2bc99a8240d3fe76a Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 30 Nov 2023 11:46:07 +0100 Subject: [PATCH 31/79] Replace `ImportedModelData` with `ModelData` for model importing --- Source/Engine/Graphics/Models/ModelData.h | 39 +++++-- .../Tools/ModelTool/ModelTool.Assimp.cpp | 32 +++--- .../Tools/ModelTool/ModelTool.OpenFBX.cpp | 36 +++---- Source/Engine/Tools/ModelTool/ModelTool.cpp | 53 +++------ Source/Engine/Tools/ModelTool/ModelTool.h | 101 ++---------------- 5 files changed, 87 insertions(+), 174 deletions(-) diff --git a/Source/Engine/Graphics/Models/ModelData.h b/Source/Engine/Graphics/Models/ModelData.h index 8874fecb3..1516554fe 100644 --- a/Source/Engine/Graphics/Models/ModelData.h +++ b/Source/Engine/Graphics/Models/ModelData.h @@ -366,12 +366,32 @@ struct FLAXENGINE_API MaterialSlotEntry bool UsesProperties() const; }; +/// +/// Data container for model hierarchy node. +/// +struct FLAXENGINE_API ModelDataNode +{ + /// + /// The parent node index. The root node uses value -1. + /// + int32 ParentIndex; + + /// + /// The local transformation of the node, relative to the parent node. + /// + Transform LocalTransform; + + /// + /// The name of this node. + /// + String Name; +}; + /// /// Data container for LOD metadata and sub meshes. /// -class FLAXENGINE_API ModelLodData +struct FLAXENGINE_API ModelLodData { -public: /// /// The screen size to switch LODs. Bottom limit of the model screen size to render this LOD. /// @@ -382,14 +402,6 @@ public: ///
Array Meshes; -public: - /// - /// Initializes a new instance of the class. - /// - ModelLodData() - { - } - /// /// Finalizes an instance of the class. /// @@ -426,7 +438,7 @@ public: Array Materials; /// - /// Array with all LODs. The first element is the top most LOD0 followed by the LOD1, LOD2, etc. + /// Array with all Level Of Details that contain meshes. The first element is the top most LOD0 followed by the LOD1, LOD2, etc. /// Array LODs; @@ -435,6 +447,11 @@ public: ///
SkeletonData Skeleton; + /// + /// The scene nodes (in hierarchy). + /// + Array Nodes; + /// /// The node animations. /// diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp index 3d9be61d2..ffcce99fc 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp @@ -238,7 +238,7 @@ void ProcessNodes(AssimpImporterData& data, aiNode* aNode, int32 parentIndex) } } -bool ProcessMesh(ImportedModelData& result, AssimpImporterData& data, const aiMesh* aMesh, MeshData& mesh, String& errorMsg) +bool ProcessMesh(ModelData& result, AssimpImporterData& data, const aiMesh* aMesh, MeshData& mesh, String& errorMsg) { // Properties mesh.Name = aMesh->mName.C_Str(); @@ -363,7 +363,7 @@ bool ProcessMesh(ImportedModelData& result, AssimpImporterData& data, const aiMe } // Blend Indices and Blend Weights - if (aMesh->mNumBones > 0 && aMesh->mBones && EnumHasAnyFlags(result.Types, ImportDataTypes::Skeleton)) + if (aMesh->mNumBones > 0 && aMesh->mBones && EnumHasAnyFlags(data.Options.ImportTypes, ImportDataTypes::Skeleton)) { const int32 vertexCount = mesh.Positions.Count(); mesh.BlendIndices.Resize(vertexCount); @@ -444,7 +444,7 @@ bool ProcessMesh(ImportedModelData& result, AssimpImporterData& data, const aiMe } // Blend Shapes - if (aMesh->mNumAnimMeshes > 0 && EnumHasAnyFlags(result.Types, ImportDataTypes::Skeleton) && data.Options.ImportBlendShapes) + if (aMesh->mNumAnimMeshes > 0 && EnumHasAnyFlags(data.Options.ImportTypes, ImportDataTypes::Skeleton) && data.Options.ImportBlendShapes) { mesh.BlendShapes.EnsureCapacity(aMesh->mNumAnimMeshes); for (unsigned int animMeshIndex = 0; animMeshIndex < aMesh->mNumAnimMeshes; animMeshIndex++) @@ -489,7 +489,7 @@ bool ProcessMesh(ImportedModelData& result, AssimpImporterData& data, const aiMe return false; } -bool ImportTexture(ImportedModelData& result, AssimpImporterData& data, aiString& aFilename, int32& textureIndex, TextureEntry::TypeHint type) +bool ImportTexture(ModelData& result, AssimpImporterData& data, aiString& aFilename, int32& textureIndex, TextureEntry::TypeHint type) { // Find texture file path const String filename = String(aFilename.C_Str()).TrimTrailing(); @@ -514,7 +514,7 @@ bool ImportTexture(ImportedModelData& result, AssimpImporterData& data, aiString return true; } -bool ImportMaterialTexture(ImportedModelData& result, AssimpImporterData& data, const aiMaterial* aMaterial, aiTextureType aTextureType, int32& textureIndex, TextureEntry::TypeHint type) +bool ImportMaterialTexture(ModelData& result, AssimpImporterData& data, const aiMaterial* aMaterial, aiTextureType aTextureType, int32& textureIndex, TextureEntry::TypeHint type) { aiString aFilename; if (aMaterial->GetTexture(aTextureType, 0, &aFilename, nullptr, nullptr, nullptr, nullptr) == AI_SUCCESS) @@ -560,7 +560,7 @@ bool ImportMaterialTexture(ImportedModelData& result, AssimpImporterData& data, return false; } -bool ImportMaterials(ImportedModelData& result, AssimpImporterData& data, String& errorMsg) +bool ImportMaterials(ModelData& result, AssimpImporterData& data, String& errorMsg) { const uint32 materialsCount = data.Scene->mNumMaterials; result.Materials.Resize(materialsCount, false); @@ -574,7 +574,7 @@ bool ImportMaterials(ImportedModelData& result, AssimpImporterData& data, String materialSlot.Name = String(aName.C_Str()).TrimTrailing(); materialSlot.AssetID = Guid::Empty; - if (EnumHasAnyFlags(result.Types, ImportDataTypes::Materials)) + if (EnumHasAnyFlags(data.Options.ImportTypes, ImportDataTypes::Materials)) { aiColor3D aColor; if (aMaterial->Get(AI_MATKEY_COLOR_DIFFUSE, aColor) == AI_SUCCESS) @@ -586,7 +586,7 @@ bool ImportMaterials(ImportedModelData& result, AssimpImporterData& data, String if (aMaterial->Get(AI_MATKEY_OPACITY, aFloat) == AI_SUCCESS) materialSlot.Opacity.Value = aFloat; - if (EnumHasAnyFlags(result.Types, ImportDataTypes::Textures)) + if (EnumHasAnyFlags(data.Options.ImportTypes, ImportDataTypes::Textures)) { ImportMaterialTexture(result, data, aMaterial, aiTextureType_DIFFUSE, materialSlot.Diffuse.TextureIndex, TextureEntry::TypeHint::ColorRGB); ImportMaterialTexture(result, data, aMaterial, aiTextureType_EMISSIVE, materialSlot.Emissive.TextureIndex, TextureEntry::TypeHint::ColorRGB); @@ -612,7 +612,7 @@ bool IsMeshInvalid(const aiMesh* aMesh) return aMesh->mPrimitiveTypes != aiPrimitiveType_TRIANGLE || aMesh->mNumVertices == 0 || aMesh->mNumFaces == 0 || aMesh->mFaces[0].mNumIndices != 3; } -bool ImportMesh(int32 i, ImportedModelData& result, AssimpImporterData& data, String& errorMsg) +bool ImportMesh(int32 i, ModelData& result, AssimpImporterData& data, String& errorMsg) { const auto aMesh = data.Scene->mMeshes[i]; @@ -739,7 +739,7 @@ void ImportCurve(aiQuatKey* keys, uint32 keysCount, LinearCurve& cur } } -bool ModelTool::ImportDataAssimp(const char* path, ImportedModelData& data, Options& options, String& errorMsg) +bool ModelTool::ImportDataAssimp(const char* path, ModelData& data, Options& options, String& errorMsg) { auto context = (AssimpImporterData*)options.SplitContext; if (!context) @@ -750,8 +750,8 @@ bool ModelTool::ImportDataAssimp(const char* path, ImportedModelData& data, Opti AssimpInited = true; LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision()); } - bool importMeshes = EnumHasAnyFlags(data.Types, ImportDataTypes::Geometry); - bool importAnimations = EnumHasAnyFlags(data.Types, ImportDataTypes::Animations); + bool importMeshes = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry); + bool importAnimations = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations); context = New(path, options); // Setup import flags @@ -820,7 +820,7 @@ bool ModelTool::ImportDataAssimp(const char* path, ImportedModelData& data, Opti } // Import geometry - if (EnumHasAnyFlags(data.Types, ImportDataTypes::Geometry) && context->Scene->HasMeshes()) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context->Scene->HasMeshes()) { const int meshCount = context->Scene->mNumMeshes; if (options.SplitObjects && options.ObjectIndex == -1 && meshCount > 1) @@ -863,7 +863,7 @@ bool ModelTool::ImportDataAssimp(const char* path, ImportedModelData& data, Opti } // Import skeleton - if (EnumHasAnyFlags(data.Types, ImportDataTypes::Skeleton)) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton)) { data.Skeleton.Nodes.Resize(context->Nodes.Count(), false); for (int32 i = 0; i < context->Nodes.Count(); i++) @@ -893,7 +893,7 @@ bool ModelTool::ImportDataAssimp(const char* path, ImportedModelData& data, Opti } // Import animations - if (EnumHasAnyFlags(data.Types, ImportDataTypes::Animations) && context->Scene->HasAnimations()) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations) && context->Scene->HasAnimations()) { const int32 animCount = (int32)context->Scene->mNumAnimations; if (options.SplitObjects && options.ObjectIndex == -1 && animCount > 1) @@ -948,7 +948,7 @@ bool ModelTool::ImportDataAssimp(const char* path, ImportedModelData& data, Opti } // Import nodes - if (EnumHasAnyFlags(data.Types, ImportDataTypes::Nodes)) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Nodes)) { data.Nodes.Resize(context->Nodes.Count()); for (int32 i = 0; i < context->Nodes.Count(); i++) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp index 490318ba7..b3e5ecdc8 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp @@ -182,7 +182,7 @@ struct OpenFbxImporterData #endif } - bool ImportMaterialTexture(ImportedModelData& result, const ofbx::Material* mat, ofbx::Texture::TextureType textureType, int32& textureIndex, TextureEntry::TypeHint type) const + bool ImportMaterialTexture(ModelData& result, const ofbx::Material* mat, ofbx::Texture::TextureType textureType, int32& textureIndex, TextureEntry::TypeHint type) const { const ofbx::Texture* tex = mat->getTexture(textureType); if (tex) @@ -217,7 +217,7 @@ struct OpenFbxImporterData return false; } - int32 AddMaterial(ImportedModelData& result, const ofbx::Material* mat) + int32 AddMaterial(ModelData& result, const ofbx::Material* mat) { int32 index = Materials.Find(mat); if (index == -1) @@ -229,11 +229,11 @@ struct OpenFbxImporterData if (mat) material.Name = String(mat->name).TrimTrailing(); - if (mat && EnumHasAnyFlags(result.Types, ImportDataTypes::Materials)) + if (mat && EnumHasAnyFlags(Options.ImportTypes, ImportDataTypes::Materials)) { material.Diffuse.Color = ToColor(mat->getDiffuseColor()); - if (EnumHasAnyFlags(result.Types, ImportDataTypes::Textures)) + if (EnumHasAnyFlags(Options.ImportTypes, ImportDataTypes::Textures)) { ImportMaterialTexture(result, mat, ofbx::Texture::DIFFUSE, material.Diffuse.TextureIndex, TextureEntry::TypeHint::ColorRGB); ImportMaterialTexture(result, mat, ofbx::Texture::EMISSIVE, material.Emissive.TextureIndex, TextureEntry::TypeHint::ColorRGB); @@ -522,7 +522,7 @@ bool ImportBones(OpenFbxImporterData& data, String& errorMsg) return false; } -bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofbx::Mesh* aMesh, MeshData& mesh, String& errorMsg, int32 triangleStart, int32 triangleEnd) +bool ProcessMesh(ModelData& result, OpenFbxImporterData& data, const ofbx::Mesh* aMesh, MeshData& mesh, String& errorMsg, int32 triangleStart, int32 triangleEnd) { // Prepare const int32 firstVertexOffset = triangleStart * 3; @@ -682,7 +682,7 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb } // Blend Indices and Blend Weights - if (skin && skin->getClusterCount() > 0 && EnumHasAnyFlags(result.Types, ImportDataTypes::Skeleton)) + if (skin && skin->getClusterCount() > 0 && EnumHasAnyFlags(data.Options.ImportTypes, ImportDataTypes::Skeleton)) { mesh.BlendIndices.Resize(vertexCount); mesh.BlendWeights.Resize(vertexCount); @@ -746,7 +746,7 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb } // Blend Shapes - if (blendShape && blendShape->getBlendShapeChannelCount() > 0 && EnumHasAnyFlags(result.Types, ImportDataTypes::Skeleton) && data.Options.ImportBlendShapes) + if (blendShape && blendShape->getBlendShapeChannelCount() > 0 && EnumHasAnyFlags(data.Options.ImportTypes, ImportDataTypes::Skeleton) && data.Options.ImportBlendShapes) { mesh.BlendShapes.EnsureCapacity(blendShape->getBlendShapeChannelCount()); for (int32 channelIndex = 0; channelIndex < blendShape->getBlendShapeChannelCount(); channelIndex++) @@ -853,7 +853,7 @@ bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofb return false; } -bool ImportMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofbx::Mesh* aMesh, String& errorMsg, int32 triangleStart, int32 triangleEnd) +bool ImportMesh(ModelData& result, OpenFbxImporterData& data, const ofbx::Mesh* aMesh, String& errorMsg, int32 triangleStart, int32 triangleEnd) { // Find the parent node int32 nodeIndex = data.FindNode(aMesh); @@ -906,7 +906,7 @@ bool ImportMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofbx return false; } -bool ImportMesh(int32 index, ImportedModelData& result, OpenFbxImporterData& data, String& errorMsg) +bool ImportMesh(int32 index, ModelData& result, OpenFbxImporterData& data, String& errorMsg) { const auto aMesh = data.Scene->getMesh(index); const auto aGeometry = aMesh->getGeometry(); @@ -1006,7 +1006,7 @@ void ImportCurve(const ofbx::AnimationCurveNode* curveNode, LinearCurve& curv } } -bool ImportAnimation(int32 index, ImportedModelData& data, OpenFbxImporterData& importerData) +bool ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importerData) { const ofbx::AnimationStack* stack = importerData.Scene->getAnimationStack(index); const ofbx::AnimationLayer* layer = stack->getLayer(0); @@ -1103,7 +1103,7 @@ static Float3 FbxVectorFromAxisAndSign(int axis, int sign) return { 0.f, 0.f, 0.f }; } -bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Options& options, String& errorMsg) +bool ModelTool::ImportDataOpenFBX(const char* path, ModelData& data, Options& options, String& errorMsg) { auto context = (OpenFbxImporterData*)options.SplitContext; if (!context) @@ -1116,7 +1116,7 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt return true; } ofbx::u64 loadFlags = 0; - if (EnumHasAnyFlags(data.Types, ImportDataTypes::Geometry)) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry)) { loadFlags |= (ofbx::u64)ofbx::LoadFlags::TRIANGULATE; if (!options.ImportBlendShapes) @@ -1161,7 +1161,7 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt #endif // Extract embedded textures - if (EnumHasAnyFlags(data.Types, ImportDataTypes::Textures)) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Textures)) { String outputPath; for (int i = 0, c = scene->getEmbeddedDataCount(); i < c; i++) @@ -1232,7 +1232,7 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt DeleteMe contextCleanup(options.SplitContext ? nullptr : context); // Build final skeleton bones hierarchy before importing meshes - if (EnumHasAnyFlags(data.Types, ImportDataTypes::Skeleton)) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton)) { if (ImportBones(*context, errorMsg)) { @@ -1244,7 +1244,7 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt } // Import geometry (meshes and materials) - if (EnumHasAnyFlags(data.Types, ImportDataTypes::Geometry) && context->Scene->getMeshCount() > 0) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context->Scene->getMeshCount() > 0) { const int meshCount = context->Scene->getMeshCount(); if (options.SplitObjects && options.ObjectIndex == -1 && meshCount > 1) @@ -1303,7 +1303,7 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt } // Import skeleton - if (EnumHasAnyFlags(data.Types, ImportDataTypes::Skeleton)) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton)) { data.Skeleton.Nodes.Resize(context->Nodes.Count(), false); for (int32 i = 0; i < context->Nodes.Count(); i++) @@ -1343,7 +1343,7 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt } // Import animations - if (EnumHasAnyFlags(data.Types, ImportDataTypes::Animations)) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations)) { const int animCount = context->Scene->getAnimationStackCount(); if (options.SplitObjects && options.ObjectIndex == -1 && animCount > 1) @@ -1386,7 +1386,7 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Opt } // Import nodes - if (EnumHasAnyFlags(data.Types, ImportDataTypes::Nodes)) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Nodes)) { data.Nodes.Resize(context->Nodes.Count()); for (int32 i = 0; i < context->Nodes.Count(); i++) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 6d0a418ad..38f010e28 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -332,26 +332,6 @@ bool ModelTool::GenerateModelSDF(Model* inputModel, ModelData* modelData, float #if USE_EDITOR -BoundingBox ImportedModelData::LOD::GetBox() const -{ - if (Meshes.IsEmpty()) - return BoundingBox::Empty; - - BoundingBox box; - Meshes[0]->CalculateBox(box); - for (int32 i = 1; i < Meshes.Count(); i++) - { - if (Meshes[i]->Positions.HasItems()) - { - BoundingBox t; - Meshes[i]->CalculateBox(t); - BoundingBox::Merge(box, t, box); - } - } - - return box; -} - void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj) { SERIALIZE_GET_OTHER_OBJ(ModelTool::Options); @@ -463,7 +443,7 @@ void RemoveNamespace(String& name) name = name.Substring(namespaceStart + 1); } -bool ModelTool::ImportData(const String& path, ImportedModelData& data, Options& options, String& errorMsg) +bool ModelTool::ImportData(const String& path, ModelData& data, Options& options, String& errorMsg) { // Validate options options.Scale = Math::Clamp(options.Scale, 0.0001f, 100000.0f); @@ -610,7 +590,7 @@ bool ModelTool::ImportData(const String& path, ImportedModelData& data, Options& } // Flip normals of the imported geometry - if (options.FlipNormals && EnumHasAnyFlags(data.Types, ImportDataTypes::Geometry)) + if (options.FlipNormals && EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry)) { for (auto& lod : data.LODs) { @@ -783,30 +763,29 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op const auto startTime = DateTime::NowUTC(); // Import data - ImportDataTypes importDataTypes; switch (options.Type) { case ModelType::Model: - importDataTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes; + options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes; if (options.ImportMaterials) - importDataTypes |= ImportDataTypes::Materials; + options.ImportTypes |= ImportDataTypes::Materials; if (options.ImportTextures) - importDataTypes |= ImportDataTypes::Textures; + options.ImportTypes |= ImportDataTypes::Textures; break; case ModelType::SkinnedModel: - importDataTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Skeleton; + options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Skeleton; if (options.ImportMaterials) - importDataTypes |= ImportDataTypes::Materials; + options.ImportTypes |= ImportDataTypes::Materials; if (options.ImportTextures) - importDataTypes |= ImportDataTypes::Textures; + options.ImportTypes |= ImportDataTypes::Textures; break; case ModelType::Animation: - importDataTypes = ImportDataTypes::Animations; + options.ImportTypes = ImportDataTypes::Animations; break; default: return true; } - ImportedModelData data(importDataTypes); + ModelData data; if (ImportData(path, data, options, errorMsg)) return true; @@ -965,7 +944,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op auto& texture = data.Textures[i]; // Auto-import textures - if (autoImportOutput.IsEmpty() || (data.Types & ImportDataTypes::Textures) == ImportDataTypes::None || texture.FilePath.IsEmpty()) + if (autoImportOutput.IsEmpty() || (options.ImportTypes & ImportDataTypes::Textures) == ImportDataTypes::None || texture.FilePath.IsEmpty()) continue; String filename = StringUtils::GetFileNameWithoutExtension(texture.FilePath); for (int32 j = filename.Length() - 1; j >= 0; j--) @@ -1012,7 +991,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op material.Name = TEXT("Material ") + StringUtils::ToString(i); // Auto-import materials - if (autoImportOutput.IsEmpty() || (data.Types & ImportDataTypes::Materials) == ImportDataTypes::None || !material.UsesProperties()) + if (autoImportOutput.IsEmpty() || (options.ImportTypes & ImportDataTypes::Materials) == ImportDataTypes::None || !material.UsesProperties()) continue; auto filename = material.Name; for (int32 j = filename.Length() - 1; j >= 0; j--) @@ -1124,10 +1103,10 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op } // Perform simple nodes mapping to single node (will transform meshes to model local space) - SkeletonMapping skeletonMapping(data.Nodes, nullptr); + SkeletonMapping skeletonMapping(data.Nodes, nullptr); // Refresh skeleton updater with model skeleton - SkeletonUpdater hierarchyUpdater(data.Nodes); + SkeletonUpdater hierarchyUpdater(data.Nodes); hierarchyUpdater.UpdateMatrices(); // Move meshes in the new nodes @@ -1421,10 +1400,10 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op } // Perform simple nodes mapping to single node (will transform meshes to model local space) - SkeletonMapping skeletonMapping(data.Nodes, nullptr); + SkeletonMapping skeletonMapping(data.Nodes, nullptr); // Refresh skeleton updater with model skeleton - SkeletonUpdater hierarchyUpdater(data.Nodes); + SkeletonUpdater hierarchyUpdater(data.Nodes); hierarchyUpdater.UpdateMatrices(); if (options.CalculateBoneOffsetMatrices) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index 5a8e90d3c..e3da7e2c3 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -56,94 +56,6 @@ enum class ImportDataTypes : int32 DECLARE_ENUM_OPERATORS(ImportDataTypes); -/// -/// Imported model data container. Represents unified model source file data (meshes, animations, skeleton, materials). -/// -class ImportedModelData -{ -public: - struct LOD - { - Array Meshes; - - BoundingBox GetBox() const; - }; - - struct Node - { - /// - /// The parent node index. The root node uses value -1. - /// - int32 ParentIndex; - - /// - /// The local transformation of the node, relative to the parent node. - /// - Transform LocalTransform; - - /// - /// The name of this node. - /// - String Name; - }; - -public: - /// - /// The import data types types. - /// - ImportDataTypes Types; - - /// - /// The textures slots. - /// - Array Textures; - - /// - /// The material slots. - /// - Array Materials; - - /// - /// The level of details data with meshes. - /// - Array LODs; - - /// - /// The skeleton data. - /// - SkeletonData Skeleton; - - /// - /// The scene nodes. - /// - Array Nodes; - - /// - /// The node animations. - /// - AnimationData Animation; - -public: - /// - /// Initializes a new instance of the class. - /// - /// The types. - ImportedModelData(ImportDataTypes types) - { - Types = types; - } - - /// - /// Finalizes an instance of the class. - /// - ~ImportedModelData() - { - // Ensure to cleanup data - for (int32 i = 0; i < LODs.Count(); i++) - LODs[i].Meshes.ClearDelete(); - } -}; - #endif struct ModelSDFHeader @@ -382,10 +294,15 @@ public: API_FIELD(Attributes="EditorOrder(3030), EditorDisplay(\"Other\")") String SubAssetFolder = TEXT(""); + public: // Internals + // Runtime data for objects splitting during import (used internally) void* SplitContext = nullptr; Function OnSplitImport; + // Internal flags for objects to import. + ImportDataTypes ImportTypes = ImportDataTypes::None; + public: // [ISerializable] void Serialize(SerializeStream& stream, const void* otherObj) override; @@ -401,7 +318,7 @@ public: /// The import options. /// The error message container. /// True if fails, otherwise false. - static bool ImportData(const String& path, ImportedModelData& data, Options& options, String& errorMsg); + static bool ImportData(const String& path, ModelData& data, Options& options, String& errorMsg); /// /// Imports the model. @@ -444,13 +361,13 @@ public: private: static void CalculateBoneOffsetMatrix(const Array& nodes, Matrix& offsetMatrix, int32 nodeIndex); #if USE_ASSIMP - static bool ImportDataAssimp(const char* path, ImportedModelData& data, Options& options, String& errorMsg); + static bool ImportDataAssimp(const char* path, ModelData& data, Options& options, String& errorMsg); #endif #if USE_AUTODESK_FBX_SDK - static bool ImportDataAutodeskFbxSdk(const char* path, ImportedModelData& data, Options& options, String& errorMsg); + static bool ImportDataAutodeskFbxSdk(const char* path, ModelData& data, Options& options, String& errorMsg); #endif #if USE_OPEN_FBX - static bool ImportDataOpenFBX(const char* path, ImportedModelData& data, Options& options, String& errorMsg); + static bool ImportDataOpenFBX(const char* path, ModelData& data, Options& options, String& errorMsg); #endif #endif }; From 640e01262faa66f4d4ff40de42e9bc71d905c88d Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Thu, 30 Nov 2023 20:40:02 -0600 Subject: [PATCH 32/79] Make `SloppyOptimization` false by default. `Lower LODTargetErro`r default. --- Source/Engine/Tools/ModelTool/ModelTool.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index ac8cb231f..3c066cabb 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -335,10 +335,10 @@ public: float TriangleReduction = 0.5f; // Whether to do a sloppy mesh optimization. This is faster but does not follow the topology of the original mesh. API_FIELD(Attributes="EditorOrder(1140), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(ShowGeometry))") - bool SloppyOptimization = true; + bool SloppyOptimization = false; // Only used if Sloppy is false. Target error is an approximate measure of the deviation from the original mesh using distance normalized to [0..1] range (e.g. 1e-2f means that simplifier will try to maintain the error to be below 1% of the mesh extents). API_FIELD(Attributes="EditorOrder(1150), EditorDisplay(\"Level Of Detail\"), VisibleIf(nameof(SloppyOptimization), true), Limit(0.01f, 1, 0.001f)") - float LODTargetError = 0.1f; + float LODTargetError = 0.05f; public: // Materials From a808bcdbf6c0af5021b2c1e2ca8ddd3f8133f2e8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 1 Dec 2023 13:57:08 +0100 Subject: [PATCH 33/79] Refactor objects splitting in models importing to be handled by `ModelTool` not the importer code itself --- .../Editor/Managed/ManagedEditor.Internal.cpp | 2 +- Source/Engine/Animations/AnimationData.h | 12 +- .../AssetsImportingManager.cpp | 56 +-- .../{ImportModelFile.cpp => ImportModel.cpp} | 213 +++++++-- Source/Engine/ContentImporters/ImportModel.h | 8 +- .../Engine/ContentImporters/ImportModelFile.h | 54 --- Source/Engine/Graphics/Models/ModelData.cpp | 43 +- Source/Engine/Graphics/Models/ModelData.h | 20 +- .../Tools/ModelTool/ModelTool.Assimp.cpp | 297 ++++++------- .../Tools/ModelTool/ModelTool.OpenFBX.cpp | 410 +++++++----------- Source/Engine/Tools/ModelTool/ModelTool.cpp | 162 ++++--- Source/Engine/Tools/ModelTool/ModelTool.h | 16 +- 12 files changed, 630 insertions(+), 663 deletions(-) rename Source/Engine/ContentImporters/{ImportModelFile.cpp => ImportModel.cpp} (51%) delete mode 100644 Source/Engine/ContentImporters/ImportModelFile.h diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp index 969608676..8855ba58a 100644 --- a/Source/Editor/Managed/ManagedEditor.Internal.cpp +++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp @@ -769,7 +769,7 @@ bool ManagedEditor::TryRestoreImportOptions(ModelTool::Options& options, String // Get options from model FileSystem::NormalizePath(assetPath); - return ImportModelFile::TryGetImportOptions(assetPath, options); + return ImportModel::TryGetImportOptions(assetPath, options); } bool ManagedEditor::Import(const String& inputPath, const String& outputPath, const AudioTool::Options& options) diff --git a/Source/Engine/Animations/AnimationData.h b/Source/Engine/Animations/AnimationData.h index 623d9ea6c..c69de8afa 100644 --- a/Source/Engine/Animations/AnimationData.h +++ b/Source/Engine/Animations/AnimationData.h @@ -98,7 +98,6 @@ public: /// struct AnimationData { -public: /// /// The duration of the animation (in frames). /// @@ -114,6 +113,11 @@ public: ///
bool EnableRootMotion = false; + /// + /// The animation name. + /// + String Name; + /// /// The custom node name to be used as a root motion source. If not specified the actual root node will be used. /// @@ -131,14 +135,14 @@ public: FORCE_INLINE float GetLength() const { #if BUILD_DEBUG - ASSERT(FramesPerSecond != 0); + ASSERT(FramesPerSecond > ZeroTolerance); #endif return static_cast(Duration / FramesPerSecond); } uint64 GetMemoryUsage() const { - uint64 result = RootNodeName.Length() * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData); + uint64 result = (Name.Length() + RootNodeName.Length()) * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData); for (const auto& e : Channels) result += e.GetMemoryUsage(); return result; @@ -164,6 +168,7 @@ public: ::Swap(Duration, other.Duration); ::Swap(FramesPerSecond, other.FramesPerSecond); ::Swap(EnableRootMotion, other.EnableRootMotion); + ::Swap(Name, other.Name); ::Swap(RootNodeName, other.RootNodeName); Channels.Swap(other.Channels); } @@ -173,6 +178,7 @@ public: ///
void Dispose() { + Name.Clear(); Duration = 0.0; FramesPerSecond = 0.0; RootNodeName.Clear(); diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.cpp b/Source/Engine/ContentImporters/AssetsImportingManager.cpp index cb366ffb3..91c391711 100644 --- a/Source/Engine/ContentImporters/AssetsImportingManager.cpp +++ b/Source/Engine/ContentImporters/AssetsImportingManager.cpp @@ -15,7 +15,7 @@ #include "Engine/Platform/Platform.h" #include "Engine/Engine/Globals.h" #include "ImportTexture.h" -#include "ImportModelFile.h" +#include "ImportModel.h" #include "ImportAudio.h" #include "ImportShader.h" #include "ImportFont.h" @@ -425,37 +425,37 @@ bool AssetsImportingManagerService::Init() { TEXT("otf"), ASSET_FILES_EXTENSION, ImportFont::Import }, // Models - { TEXT("obj"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("fbx"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("x"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("dae"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("gltf"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("glb"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, + { TEXT("obj"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("fbx"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("x"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("dae"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("gltf"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("glb"), ASSET_FILES_EXTENSION, ImportModel::Import }, // gettext PO files { TEXT("po"), TEXT("json"), CreateJson::ImportPo }, // Models (untested formats - may fail :/) - { TEXT("blend"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("bvh"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("ase"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("ply"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("dxf"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("ifc"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("nff"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("smd"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("vta"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("mdl"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("md2"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("md3"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("md5mesh"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("q3o"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("q3s"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("ac"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("stl"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("lwo"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("lws"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, - { TEXT("lxo"), ASSET_FILES_EXTENSION, ImportModelFile::Import }, + { TEXT("blend"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("bvh"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("ase"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("ply"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("dxf"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("ifc"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("nff"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("smd"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("vta"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("mdl"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("md2"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("md3"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("md5mesh"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("q3o"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("q3s"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("ac"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("stl"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("lwo"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("lws"), ASSET_FILES_EXTENSION, ImportModel::Import }, + { TEXT("lxo"), ASSET_FILES_EXTENSION, ImportModel::Import }, }; AssetsImportingManager::Importers.Add(InBuildImporters, ARRAY_COUNT(InBuildImporters)); @@ -473,7 +473,7 @@ bool AssetsImportingManagerService::Init() { AssetsImportingManager::CreateMaterialInstanceTag, CreateMaterialInstance::Create }, // Models - { AssetsImportingManager::CreateModelTag, ImportModelFile::Create }, + { AssetsImportingManager::CreateModelTag, ImportModel::Create }, // Other { AssetsImportingManager::CreateRawDataTag, CreateRawData::Create }, diff --git a/Source/Engine/ContentImporters/ImportModelFile.cpp b/Source/Engine/ContentImporters/ImportModel.cpp similarity index 51% rename from Source/Engine/ContentImporters/ImportModelFile.cpp rename to Source/Engine/ContentImporters/ImportModel.cpp index 64a24217e..5aa44ecc4 100644 --- a/Source/Engine/ContentImporters/ImportModelFile.cpp +++ b/Source/Engine/ContentImporters/ImportModel.cpp @@ -5,6 +5,8 @@ #if COMPILE_WITH_ASSETS_IMPORTER #include "Engine/Core/Log.h" +#include "Engine/Core/Collections/Sorting.h" +#include "Engine/Core/Collections/ArrayExtensions.h" #include "Engine/Serialization/MemoryWriteStream.h" #include "Engine/Serialization/JsonWriters.h" #include "Engine/Graphics/Models/ModelData.h" @@ -16,7 +18,7 @@ #include "Engine/Platform/FileSystem.h" #include "AssetsImportingManager.h" -bool ImportModelFile::TryGetImportOptions(const StringView& path, Options& options) +bool ImportModel::TryGetImportOptions(const StringView& path, Options& options) { if (FileSystem::FileExists(path)) { @@ -88,7 +90,32 @@ void TryRestoreMaterials(CreateAssetContext& context, ModelData& modelData) } } -CreateAssetResult ImportModelFile::Import(CreateAssetContext& context) +void SetupMaterialSlots(ModelData& data, const Array& materials) +{ + Array materialSlotsTable; + materialSlotsTable.Resize(materials.Count()); + materialSlotsTable.SetAll(-1); + for (auto& lod : data.LODs) + { + for (MeshData* mesh : lod.Meshes) + { + int32 newSlotIndex = materialSlotsTable[mesh->MaterialSlotIndex]; + if (newSlotIndex == -1) + { + newSlotIndex = data.Materials.Count(); + data.Materials.AddOne() = materials[mesh->MaterialSlotIndex]; + } + mesh->MaterialSlotIndex = newSlotIndex; + } + } +} + +bool SortMeshGroups(IGrouping const& i1, IGrouping const& i2) +{ + return i1.GetKey().Compare(i2.GetKey()) < 0; +} + +CreateAssetResult ImportModel::Import(CreateAssetContext& context) { // Get import options Options options; @@ -105,9 +132,50 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context) LOG(Warning, "Missing model import options. Using default values."); } } + + // Import model file + ModelData* data = options.Cached ? options.Cached->Data : nullptr; + ModelData dataThis; + Array>* meshesByNamePtr = options.Cached ? (Array>*)options.Cached->MeshesByName : nullptr; + Array> meshesByNameThis; + if (!data) + { + String errorMsg; + String autoImportOutput(StringUtils::GetDirectoryName(context.TargetAssetPath)); + autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath)); + if (ModelTool::ImportModel(context.InputPath, dataThis, options, errorMsg, autoImportOutput)) + { + LOG(Error, "Cannot import model file. {0}", errorMsg); + return CreateAssetResult::Error; + } + data = &dataThis; + + // Group meshes by the name (the same mesh name can be used by multiple meshes that use different materials) + if (data->LODs.Count() != 0) + { + const Function f = [](MeshData* const& x) -> StringView + { + return x->Name; + }; + ArrayExtensions::GroupBy(data->LODs[0].Meshes, f, meshesByNameThis); + Sorting::QuickSort(meshesByNameThis.Get(), meshesByNameThis.Count(), &SortMeshGroups); + } + meshesByNamePtr = &meshesByNameThis; + } + Array>& meshesByName = *meshesByNamePtr; + + // Import objects from file separately if (options.SplitObjects) { - options.OnSplitImport.Bind([&context](Options& splitOptions, const String& objectName) + // Import the first object within this call + options.SplitObjects = false; + options.ObjectIndex = 0; + + // Import rest of the objects recursive but use current model data to skip loading file again + ModelTool::Options::CachedData cached = { data, (void*)meshesByNamePtr }; + options.Cached = &cached; + Function splitImport; + splitImport.Bind([&context](Options& splitOptions, const StringView& objectName) { // Recursive importing of the split object String postFix = objectName; @@ -117,42 +185,132 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context) const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax"); return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions); }); + auto splitOptions = options; + switch (options.Type) + { + case ModelTool::ModelType::Model: + case ModelTool::ModelType::SkinnedModel: + LOG(Info, "Splitting imported {0} meshes", meshesByName.Count()); + for (int32 groupIndex = 1; groupIndex < meshesByName.Count(); groupIndex++) + { + auto& group = meshesByName[groupIndex]; + splitOptions.ObjectIndex = groupIndex; + splitImport(splitOptions, group.GetKey()); + } + break; + case ModelTool::ModelType::Animation: + LOG(Info, "Splitting imported {0} animations", data->Animations.Count()); + for (int32 i = 1; i < data->Animations.Count(); i++) + { + auto& animation = data->Animations[i]; + splitOptions.ObjectIndex = i; + splitImport(splitOptions, animation.Name); + } + break; + } } - // Import model file - ModelData modelData; - String errorMsg; - String autoImportOutput(StringUtils::GetDirectoryName(context.TargetAssetPath)); - autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath)); - if (ModelTool::ImportModel(context.InputPath, modelData, options, errorMsg, autoImportOutput)) + // When importing a single object as model asset then select a specific mesh group + Array meshesToDelete; + if (options.ObjectIndex >= 0 && + options.ObjectIndex < meshesByName.Count() && + (options.Type == ModelTool::ModelType::Model || options.Type == ModelTool::ModelType::SkinnedModel)) { - LOG(Error, "Cannot import model file. {0}", errorMsg); - return CreateAssetResult::Error; + auto& group = meshesByName[options.ObjectIndex]; + if (&dataThis == data) + { + // Use meshes only from the the grouping (others will be removed manually) + { + auto& lod = dataThis.LODs[0]; + meshesToDelete.Add(lod.Meshes); + lod.Meshes.Clear(); + for (MeshData* mesh : group) + { + lod.Meshes.Add(mesh); + meshesToDelete.Remove(mesh); + } + } + for (int32 lodIndex = 1; lodIndex < dataThis.LODs.Count(); lodIndex++) + { + auto& lod = dataThis.LODs[lodIndex]; + Array lodMeshes = lod.Meshes; + lod.Meshes.Clear(); + for (MeshData* lodMesh : lodMeshes) + { + if (lodMesh->Name == group.GetKey()) + lod.Meshes.Add(lodMesh); + else + meshesToDelete.Add(lodMesh); + } + } + + // Use only materials references by meshes from the first grouping + { + auto materials = dataThis.Materials; + dataThis.Materials.Clear(); + SetupMaterialSlots(dataThis, materials); + } + } + else + { + // Copy data from others data + dataThis.Skeleton = data->Skeleton; + dataThis.Nodes = data->Nodes; + + // Move meshes from this group (including any LODs of them) + { + auto& lod = dataThis.LODs.AddOne(); + lod.ScreenSize = data->LODs[0].ScreenSize; + lod.Meshes.Add(group); + for (MeshData* mesh : group) + data->LODs[0].Meshes.Remove(mesh); + } + for (int32 lodIndex = 1; lodIndex < data->LODs.Count(); lodIndex++) + { + Array lodMeshes = data->LODs[lodIndex].Meshes; + for (int32 i = lodMeshes.Count() - 1; i >= 0; i--) + { + MeshData* lodMesh = lodMeshes[i]; + if (lodMesh->Name == group.GetKey()) + data->LODs[lodIndex].Meshes.Remove(lodMesh); + else + lodMeshes.RemoveAtKeepOrder(i); + } + if (lodMeshes.Count() == 0) + break; // No meshes of that name in this LOD so skip further ones + auto& lod = dataThis.LODs.AddOne(); + lod.ScreenSize = data->LODs[lodIndex].ScreenSize; + lod.Meshes.Add(lodMeshes); + } + + // Copy materials used by the meshes + SetupMaterialSlots(dataThis, data->Materials); + } + data = &dataThis; } // Check if restore materials on model reimport - if (options.RestoreMaterialsOnReimport && modelData.Materials.HasItems()) + if (options.RestoreMaterialsOnReimport && data->Materials.HasItems()) { - TryRestoreMaterials(context, modelData); + TryRestoreMaterials(context, *data); } - // Auto calculate LODs transition settings - modelData.CalculateLODsScreenSizes(); - // Create destination asset type CreateAssetResult result = CreateAssetResult::InvalidTypeID; switch (options.Type) { case ModelTool::ModelType::Model: - result = ImportModel(context, modelData, &options); + result = CreateModel(context, *data, &options); break; case ModelTool::ModelType::SkinnedModel: - result = ImportSkinnedModel(context, modelData, &options); + result = CreateSkinnedModel(context, *data, &options); break; case ModelTool::ModelType::Animation: - result = ImportAnimation(context, modelData, &options); + result = CreateAnimation(context, *data, &options); break; } + for (auto mesh : meshesToDelete) + Delete(mesh); if (result != CreateAssetResult::Ok) return result; @@ -172,7 +330,7 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context) return CreateAssetResult::Ok; } -CreateAssetResult ImportModelFile::Create(CreateAssetContext& context) +CreateAssetResult ImportModel::Create(CreateAssetContext& context) { ASSERT(context.CustomArg != nullptr); auto& modelData = *(ModelData*)context.CustomArg; @@ -187,13 +345,11 @@ CreateAssetResult ImportModelFile::Create(CreateAssetContext& context) // Auto calculate LODs transition settings modelData.CalculateLODsScreenSizes(); - // Import - return ImportModel(context, modelData); + return CreateModel(context, modelData); } -CreateAssetResult ImportModelFile::ImportModel(CreateAssetContext& context, ModelData& modelData, const Options* options) +CreateAssetResult ImportModel::CreateModel(CreateAssetContext& context, ModelData& modelData, const Options* options) { - // Base IMPORT_SETUP(Model, Model::SerializedVersion); // Save model header @@ -242,9 +398,8 @@ CreateAssetResult ImportModelFile::ImportModel(CreateAssetContext& context, Mode return CreateAssetResult::Ok; } -CreateAssetResult ImportModelFile::ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options) +CreateAssetResult ImportModel::CreateSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options) { - // Base IMPORT_SETUP(SkinnedModel, SkinnedModel::SerializedVersion); // Save skinned model header @@ -284,14 +439,14 @@ CreateAssetResult ImportModelFile::ImportSkinnedModel(CreateAssetContext& contex return CreateAssetResult::Ok; } -CreateAssetResult ImportModelFile::ImportAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options) +CreateAssetResult ImportModel::CreateAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options) { - // Base IMPORT_SETUP(Animation, Animation::SerializedVersion); // Save animation data MemoryWriteStream stream(8182); - if (modelData.Pack2AnimationHeader(&stream)) + const int32 animIndex = options && options->ObjectIndex != -1 ? options->ObjectIndex : 0; // Single animation per asset + if (modelData.Pack2AnimationHeader(&stream, animIndex)) return CreateAssetResult::Error; if (context.AllocateChunk(0)) return CreateAssetResult::CannotAllocateChunk; diff --git a/Source/Engine/ContentImporters/ImportModel.h b/Source/Engine/ContentImporters/ImportModel.h index b7af2789e..31a525852 100644 --- a/Source/Engine/ContentImporters/ImportModel.h +++ b/Source/Engine/ContentImporters/ImportModel.h @@ -11,7 +11,7 @@ /// /// Importing models utility /// -class ImportModelFile +class ImportModel { public: typedef ModelTool::Options Options; @@ -40,9 +40,9 @@ public: static CreateAssetResult Create(CreateAssetContext& context); private: - static CreateAssetResult ImportModel(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr); - static CreateAssetResult ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr); - static CreateAssetResult ImportAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr); + static CreateAssetResult CreateModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr); + static CreateAssetResult CreateSkinnedModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr); + static CreateAssetResult CreateAnimation(CreateAssetContext& context, ModelData& data, const Options* options = nullptr); }; #endif diff --git a/Source/Engine/ContentImporters/ImportModelFile.h b/Source/Engine/ContentImporters/ImportModelFile.h deleted file mode 100644 index a40109601..000000000 --- a/Source/Engine/ContentImporters/ImportModelFile.h +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. - -#pragma once - -#include "Types.h" - -#if COMPILE_WITH_ASSETS_IMPORTER - -#include "Engine/Content/Assets/Model.h" -#include "Engine/Tools/ModelTool/ModelTool.h" - -/// -/// Enable/disable caching model import options -/// -#define IMPORT_MODEL_CACHE_OPTIONS 1 - -/// -/// Importing models utility -/// -class ImportModelFile -{ -public: - typedef ModelTool::Options Options; - -public: - /// - /// Tries the get model import options from the target location asset. - /// - /// The asset path. - /// The options. - /// True if success, otherwise false. - static bool TryGetImportOptions(String path, Options& options); - - /// - /// Imports the model file. - /// - /// The importing context. - /// Result. - static CreateAssetResult Import(CreateAssetContext& context); - - /// - /// Creates the model asset from the ModelData storage (input argument should be pointer to ModelData). - /// - /// The importing context. - /// Result. - static CreateAssetResult Create(CreateAssetContext& context); - -private: - static CreateAssetResult ImportModel(CreateAssetContext& context, ModelData& modelData); - static CreateAssetResult ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData); - static CreateAssetResult ImportAnimation(CreateAssetContext& context, ModelData& modelData); -}; - -#endif diff --git a/Source/Engine/Graphics/Models/ModelData.cpp b/Source/Engine/Graphics/Models/ModelData.cpp index 1d67f4737..3a31c5771 100644 --- a/Source/Engine/Graphics/Models/ModelData.cpp +++ b/Source/Engine/Graphics/Models/ModelData.cpp @@ -625,6 +625,11 @@ bool MaterialSlotEntry::UsesProperties() const Normals.TextureIndex != -1; } +ModelLodData::~ModelLodData() +{ + Meshes.ClearDelete(); +} + BoundingBox ModelLodData::GetBox() const { if (Meshes.IsEmpty()) @@ -644,11 +649,9 @@ void ModelData::CalculateLODsScreenSizes() { const float autoComputeLodPowerBase = 0.5f; const int32 lodCount = LODs.Count(); - for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++) { auto& lod = LODs[lodIndex]; - if (lodIndex == 0) { lod.ScreenSize = 1.0f; @@ -675,6 +678,8 @@ void ModelData::TransformBuffer(const Matrix& matrix) } } +#if USE_EDITOR + bool ModelData::Pack2ModelHeader(WriteStream* stream) const { // Validate input @@ -880,20 +885,21 @@ bool ModelData::Pack2SkinnedModelHeader(WriteStream* stream) const return false; } -bool ModelData::Pack2AnimationHeader(WriteStream* stream) const +bool ModelData::Pack2AnimationHeader(WriteStream* stream, int32 animIndex) const { // Validate input - if (stream == nullptr) + if (stream == nullptr || animIndex < 0 || animIndex >= Animations.Count()) { Log::ArgumentNullException(); return true; } - if (Animation.Duration <= ZeroTolerance || Animation.FramesPerSecond <= ZeroTolerance) + auto& anim = Animations.Get()[animIndex]; + if (anim.Duration <= ZeroTolerance || anim.FramesPerSecond <= ZeroTolerance) { Log::InvalidOperationException(TEXT("Invalid animation duration.")); return true; } - if (Animation.Channels.IsEmpty()) + if (anim.Channels.IsEmpty()) { Log::ArgumentOutOfRangeException(TEXT("Channels"), TEXT("Animation channels collection cannot be empty.")); return true; @@ -901,22 +907,23 @@ bool ModelData::Pack2AnimationHeader(WriteStream* stream) const // Info stream->WriteInt32(100); // Header version (for fast version upgrades without serialization format change) - stream->WriteDouble(Animation.Duration); - stream->WriteDouble(Animation.FramesPerSecond); - stream->WriteBool(Animation.EnableRootMotion); - stream->WriteString(Animation.RootNodeName, 13); + stream->WriteDouble(anim.Duration); + stream->WriteDouble(anim.FramesPerSecond); + stream->WriteBool(anim.EnableRootMotion); + stream->WriteString(anim.RootNodeName, 13); // Animation channels - stream->WriteInt32(Animation.Channels.Count()); - for (int32 i = 0; i < Animation.Channels.Count(); i++) + stream->WriteInt32(anim.Channels.Count()); + for (int32 i = 0; i < anim.Channels.Count(); i++) { - auto& anim = Animation.Channels[i]; - - stream->WriteString(anim.NodeName, 172); - Serialization::Serialize(*stream, anim.Position); - Serialization::Serialize(*stream, anim.Rotation); - Serialization::Serialize(*stream, anim.Scale); + auto& channel = anim.Channels[i]; + stream->WriteString(channel.NodeName, 172); + Serialization::Serialize(*stream, channel.Position); + Serialization::Serialize(*stream, channel.Rotation); + Serialization::Serialize(*stream, channel.Scale); } return false; } + +#endif diff --git a/Source/Engine/Graphics/Models/ModelData.h b/Source/Engine/Graphics/Models/ModelData.h index 1516554fe..65bedf876 100644 --- a/Source/Engine/Graphics/Models/ModelData.h +++ b/Source/Engine/Graphics/Models/ModelData.h @@ -405,10 +405,7 @@ struct FLAXENGINE_API ModelLodData /// /// Finalizes an instance of the class. /// - ~ModelLodData() - { - Meshes.ClearDelete(); - } + ~ModelLodData(); /// /// Gets the bounding box combined for all meshes in this model LOD. @@ -455,15 +452,7 @@ public: /// /// The node animations. /// - AnimationData Animation; - -public: - /// - /// Initializes a new instance of the class. - /// - ModelData() - { - } + Array Animations; public: /// @@ -494,6 +483,7 @@ public: /// The matrix to use for the transformation. void TransformBuffer(const Matrix& matrix); +#if USE_EDITOR public: /// /// Pack mesh data to the header stream @@ -513,6 +503,8 @@ public: /// Pack animation data to the header stream /// /// Output stream + /// Index of animation. /// True if cannot save data, otherwise false - bool Pack2AnimationHeader(WriteStream* stream) const; + bool Pack2AnimationHeader(WriteStream* stream, int32 animIndex = 0) const; +#endif }; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp index ffcce99fc..e62d03b1c 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp @@ -4,7 +4,6 @@ #include "ModelTool.h" #include "Engine/Core/Log.h" -#include "Engine/Core/DeleteMe.h" #include "Engine/Core/Math/Matrix.h" #include "Engine/Core/Collections/Dictionary.h" #include "Engine/Platform/FileSystem.h" @@ -28,7 +27,6 @@ using namespace Assimp; class AssimpLogStream : public LogStream { public: - AssimpLogStream() { DefaultLogger::create(""); @@ -612,32 +610,35 @@ bool IsMeshInvalid(const aiMesh* aMesh) return aMesh->mPrimitiveTypes != aiPrimitiveType_TRIANGLE || aMesh->mNumVertices == 0 || aMesh->mNumFaces == 0 || aMesh->mFaces[0].mNumIndices != 3; } -bool ImportMesh(int32 i, ModelData& result, AssimpImporterData& data, String& errorMsg) +bool ImportMesh(int32 index, ModelData& result, AssimpImporterData& data, String& errorMsg) { - const auto aMesh = data.Scene->mMeshes[i]; + const auto aMesh = data.Scene->mMeshes[index]; // Skip invalid meshes if (IsMeshInvalid(aMesh)) return false; // Skip unused meshes - if (!data.MeshIndexToNodeIndex.ContainsKey(i)) + if (!data.MeshIndexToNodeIndex.ContainsKey(index)) return false; // Import mesh data MeshData* meshData = New(); if (ProcessMesh(result, data, aMesh, *meshData, errorMsg)) - return true; - - auto& nodesWithMesh = data.MeshIndexToNodeIndex[i]; - for (int32 j = 0; j < nodesWithMesh.Count(); j++) { - const auto nodeIndex = nodesWithMesh[j]; + Delete(meshData); + return true; + } + + auto& nodesWithMesh = data.MeshIndexToNodeIndex[index]; + for (int32 i = 0; i < nodesWithMesh.Count(); i++) + { + const auto nodeIndex = nodesWithMesh[i]; auto& node = data.Nodes[nodeIndex]; const int32 lodIndex = node.LodIndex; // The first mesh instance uses meshData directly while others have to clone it - if (j != 0) + if (i != 0) { meshData = New(*meshData); } @@ -739,222 +740,166 @@ void ImportCurve(aiQuatKey* keys, uint32 keysCount, LinearCurve& cur } } +void ImportAnimation(int32 index, ModelData& data, AssimpImporterData& importerData) +{ + const auto animations = importerData.Scene->mAnimations[index]; + auto& anim = data.Animations.AddOne(); + anim.Channels.Resize(animations->mNumChannels, false); + anim.Duration = animations->mDuration; + anim.FramesPerSecond = animations->mTicksPerSecond; + if (anim.FramesPerSecond <= 0) + { + anim.FramesPerSecond = importerData.Options.DefaultFrameRate; + if (anim.FramesPerSecond <= 0) + anim.FramesPerSecond = 30.0f; + } + anim.Name = animations->mName.C_Str(); + + for (unsigned i = 0; i < animations->mNumChannels; i++) + { + const auto aAnim = animations->mChannels[i]; + auto& channel = anim.Channels[i]; + + channel.NodeName = aAnim->mNodeName.C_Str(); + + ImportCurve(aAnim->mPositionKeys, aAnim->mNumPositionKeys, channel.Position); + ImportCurve(aAnim->mRotationKeys, aAnim->mNumRotationKeys, channel.Rotation); + if (importerData.Options.ImportScaleTracks) + ImportCurve(aAnim->mScalingKeys, aAnim->mNumScalingKeys, channel.Scale); + } +} + bool ModelTool::ImportDataAssimp(const char* path, ModelData& data, Options& options, String& errorMsg) { - auto context = (AssimpImporterData*)options.SplitContext; - if (!context) + static bool AssimpInited = false; + if (!AssimpInited) { - static bool AssimpInited = false; - if (!AssimpInited) - { - AssimpInited = true; - LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision()); - } - bool importMeshes = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry); - bool importAnimations = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations); - context = New(path, options); - - // Setup import flags - unsigned int flags = - aiProcess_JoinIdenticalVertices | - aiProcess_LimitBoneWeights | - aiProcess_Triangulate | - aiProcess_SortByPType | - aiProcess_GenUVCoords | - aiProcess_FindDegenerates | - aiProcess_FindInvalidData | - //aiProcess_ValidateDataStructure | - aiProcess_ConvertToLeftHanded; - if (importMeshes) - { - if (options.CalculateNormals) - flags |= aiProcess_FixInfacingNormals | aiProcess_GenSmoothNormals; - if (options.CalculateTangents) - flags |= aiProcess_CalcTangentSpace; - if (options.OptimizeMeshes) - flags |= aiProcess_OptimizeMeshes | aiProcess_SplitLargeMeshes | aiProcess_ImproveCacheLocality; - if (options.MergeMeshes) - flags |= aiProcess_RemoveRedundantMaterials; - } - - // Setup import options - context->AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_GSN_MAX_SMOOTHING_ANGLE, options.SmoothingNormalsAngle); - context->AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_CT_MAX_SMOOTHING_ANGLE, options.SmoothingTangentsAngle); - //context->AssimpImporter.SetPropertyInteger(AI_CONFIG_PP_SLM_TRIANGLE_LIMIT, MAX_uint16); - context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false); - context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false); - context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, false); - context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, importAnimations); - //context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); // TODO: optimize pivots when https://github.com/assimp/assimp/issues/1068 gets fixed - context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true); - - // Import file - context->Scene = context->AssimpImporter.ReadFile(path, flags); - if (context->Scene == nullptr) - { - LOG_STR(Warning, String(context->AssimpImporter.GetErrorString())); - LOG_STR(Warning, String(path)); - LOG_STR(Warning, StringUtils::ToString(flags)); - errorMsg = context->AssimpImporter.GetErrorString(); - Delete(context); - return true; - } - - // Create root node - AssimpNode& rootNode = context->Nodes.AddOne(); - rootNode.ParentIndex = -1; - rootNode.LodIndex = 0; - rootNode.Name = TEXT("Root"); - rootNode.LocalTransform = Transform::Identity; - - // Process imported scene nodes - ProcessNodes(*context, context->Scene->mRootNode, 0); + AssimpInited = true; + LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision()); } - DeleteMe contextCleanup(options.SplitContext ? nullptr : context); + bool importMeshes = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry); + bool importAnimations = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations); + AssimpImporterData context(path, options); + + // Setup import flags + unsigned int flags = + aiProcess_JoinIdenticalVertices | + aiProcess_LimitBoneWeights | + aiProcess_Triangulate | + aiProcess_SortByPType | + aiProcess_GenUVCoords | + aiProcess_FindDegenerates | + aiProcess_FindInvalidData | + //aiProcess_ValidateDataStructure | + aiProcess_ConvertToLeftHanded; + if (importMeshes) + { + if (options.CalculateNormals) + flags |= aiProcess_FixInfacingNormals | aiProcess_GenSmoothNormals; + if (options.CalculateTangents) + flags |= aiProcess_CalcTangentSpace; + if (options.OptimizeMeshes) + flags |= aiProcess_OptimizeMeshes | aiProcess_SplitLargeMeshes | aiProcess_ImproveCacheLocality; + if (options.MergeMeshes) + flags |= aiProcess_RemoveRedundantMaterials; + } + + // Setup import options + context.AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_GSN_MAX_SMOOTHING_ANGLE, options.SmoothingNormalsAngle); + context.AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_CT_MAX_SMOOTHING_ANGLE, options.SmoothingTangentsAngle); + //context.AssimpImporter.SetPropertyInteger(AI_CONFIG_PP_SLM_TRIANGLE_LIMIT, MAX_uint16); + context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false); + context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false); + context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, false); + context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, importAnimations); + //context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); // TODO: optimize pivots when https://github.com/assimp/assimp/issues/1068 gets fixed + context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true); + + // Import file + context.Scene = context.AssimpImporter.ReadFile(path, flags); + if (context.Scene == nullptr) + { + LOG_STR(Warning, String(context.AssimpImporter.GetErrorString())); + LOG_STR(Warning, String(path)); + LOG_STR(Warning, StringUtils::ToString(flags)); + errorMsg = context.AssimpImporter.GetErrorString(); + return true; + } + + // Create root node + AssimpNode& rootNode = context.Nodes.AddOne(); + rootNode.ParentIndex = -1; + rootNode.LodIndex = 0; + rootNode.Name = TEXT("Root"); + rootNode.LocalTransform = Transform::Identity; + + // Process imported scene nodes + ProcessNodes(context, context.Scene->mRootNode, 0); // Import materials - if (ImportMaterials(data, *context, errorMsg)) + if (ImportMaterials(data, context, errorMsg)) { LOG(Warning, "Failed to import materials."); return true; } // Import geometry - if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context->Scene->HasMeshes()) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context.Scene->HasMeshes()) { - const int meshCount = context->Scene->mNumMeshes; - if (options.SplitObjects && options.ObjectIndex == -1 && meshCount > 1) + for (unsigned meshIndex = 0; meshIndex < context.Scene->mNumMeshes; meshIndex++) { - // Import the first object within this call - options.SplitObjects = false; - options.ObjectIndex = 0; - - if (options.OnSplitImport.IsBinded()) - { - // Split all animations into separate assets - LOG(Info, "Splitting imported {0} meshes", meshCount); - for (int32 i = 1; i < meshCount; i++) - { - auto splitOptions = options; - splitOptions.ObjectIndex = i; - splitOptions.SplitContext = context; - const auto aMesh = context->Scene->mMeshes[i]; - const String objectName(aMesh->mName.C_Str()); - options.OnSplitImport(splitOptions, objectName); - } - } - } - if (options.ObjectIndex != -1) - { - // Import the selected mesh - const auto meshIndex = Math::Clamp(options.ObjectIndex, 0, meshCount - 1); - if (ImportMesh(meshIndex, data, *context, errorMsg)) + if (ImportMesh((int32)meshIndex, data, context, errorMsg)) return true; } - else - { - // Import all meshes - for (int32 meshIndex = 0; meshIndex < meshCount; meshIndex++) - { - if (ImportMesh(meshIndex, data, *context, errorMsg)) - return true; - } - } } // Import skeleton if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton)) { - data.Skeleton.Nodes.Resize(context->Nodes.Count(), false); - for (int32 i = 0; i < context->Nodes.Count(); i++) + data.Skeleton.Nodes.Resize(context.Nodes.Count(), false); + for (int32 i = 0; i < context.Nodes.Count(); i++) { auto& node = data.Skeleton.Nodes[i]; - auto& aNode = context->Nodes[i]; + auto& aNode = context.Nodes[i]; node.Name = aNode.Name; node.ParentIndex = aNode.ParentIndex; node.LocalTransform = aNode.LocalTransform; } - data.Skeleton.Bones.Resize(context->Bones.Count(), false); - for (int32 i = 0; i < context->Bones.Count(); i++) + data.Skeleton.Bones.Resize(context.Bones.Count(), false); + for (int32 i = 0; i < context.Bones.Count(); i++) { auto& bone = data.Skeleton.Bones[i]; - auto& aBone = context->Bones[i]; + auto& aBone = context.Bones[i]; const auto boneNodeIndex = aBone.NodeIndex; - const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : context->Bones[aBone.ParentBoneIndex].NodeIndex; + const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : context.Bones[aBone.ParentBoneIndex].NodeIndex; bone.ParentIndex = aBone.ParentBoneIndex; bone.NodeIndex = aBone.NodeIndex; - bone.LocalTransform = CombineTransformsFromNodeIndices(context->Nodes, parentBoneNodeIndex, boneNodeIndex); + bone.LocalTransform = CombineTransformsFromNodeIndices(context.Nodes, parentBoneNodeIndex, boneNodeIndex); bone.OffsetMatrix = aBone.OffsetMatrix; } } // Import animations - if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations) && context->Scene->HasAnimations()) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations)) { - const int32 animCount = (int32)context->Scene->mNumAnimations; - if (options.SplitObjects && options.ObjectIndex == -1 && animCount > 1) + for (unsigned animIndex = 0; animIndex < context.Scene->mNumAnimations; animIndex++) { - // Import the first object within this call - options.SplitObjects = false; - options.ObjectIndex = 0; - - if (options.OnSplitImport.IsBinded()) - { - // Split all animations into separate assets - LOG(Info, "Splitting imported {0} animations", animCount); - for (int32 i = 1; i < animCount; i++) - { - auto splitOptions = options; - splitOptions.ObjectIndex = i; - splitOptions.SplitContext = context; - const auto animations = context->Scene->mAnimations[i]; - const String objectName(animations->mName.C_Str()); - options.OnSplitImport(splitOptions, objectName); - } - } - } - - // Import the animation - { - const auto animIndex = Math::Clamp(options.ObjectIndex, 0, context->Scene->mNumAnimations - 1); - const auto animations = context->Scene->mAnimations[animIndex]; - data.Animation.Channels.Resize(animations->mNumChannels, false); - data.Animation.Duration = animations->mDuration; - data.Animation.FramesPerSecond = animations->mTicksPerSecond; - if (data.Animation.FramesPerSecond <= 0) - { - data.Animation.FramesPerSecond = context->Options.DefaultFrameRate; - if (data.Animation.FramesPerSecond <= 0) - data.Animation.FramesPerSecond = 30.0f; - } - - for (unsigned i = 0; i < animations->mNumChannels; i++) - { - const auto aAnim = animations->mChannels[i]; - auto& anim = data.Animation.Channels[i]; - - anim.NodeName = aAnim->mNodeName.C_Str(); - - ImportCurve(aAnim->mPositionKeys, aAnim->mNumPositionKeys, anim.Position); - ImportCurve(aAnim->mRotationKeys, aAnim->mNumRotationKeys, anim.Rotation); - if (options.ImportScaleTracks) - ImportCurve(aAnim->mScalingKeys, aAnim->mNumScalingKeys, anim.Scale); - } + ImportAnimation((int32)animIndex, data, context); } } // Import nodes if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Nodes)) { - data.Nodes.Resize(context->Nodes.Count()); - for (int32 i = 0; i < context->Nodes.Count(); i++) + data.Nodes.Resize(context.Nodes.Count()); + for (int32 i = 0; i < context.Nodes.Count(); i++) { auto& node = data.Nodes[i]; - auto& aNode = context->Nodes[i]; + auto& aNode = context.Nodes[i]; node.Name = aNode.Name; node.ParentIndex = aNode.ParentIndex; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp index b3e5ecdc8..a6e6225b1 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp @@ -4,7 +4,6 @@ #include "ModelTool.h" #include "Engine/Core/Log.h" -#include "Engine/Core/DeleteMe.h" #include "Engine/Core/Math/Mathd.h" #include "Engine/Core/Math/Matrix.h" #include "Engine/Core/Collections/Sorting.h" @@ -1006,27 +1005,19 @@ void ImportCurve(const ofbx::AnimationCurveNode* curveNode, LinearCurve& curv } } -bool ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importerData) +void ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importerData) { const ofbx::AnimationStack* stack = importerData.Scene->getAnimationStack(index); const ofbx::AnimationLayer* layer = stack->getLayer(0); const ofbx::TakeInfo* takeInfo = importerData.Scene->getTakeInfo(stack->name); if (takeInfo == nullptr) - return true; + return; // Initialize animation animation keyframes sampling const float frameRate = importerData.FrameRate; - data.Animation.FramesPerSecond = frameRate; const double localDuration = takeInfo->local_time_to - takeInfo->local_time_from; if (localDuration <= ZeroTolerance) - return true; - data.Animation.Duration = (double)(int32)(localDuration * frameRate + 0.5f); - AnimInfo info; - info.TimeStart = takeInfo->local_time_from; - info.TimeEnd = takeInfo->local_time_to; - info.Duration = localDuration; - info.FramesCount = (int32)data.Animation.Duration; - info.SamplingPeriod = 1.0f / frameRate; + return; // Count valid animation channels Array animatedNodes(importerData.Nodes.Count()); @@ -1042,15 +1033,32 @@ bool ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importer animatedNodes.Add(nodeIndex); } if (animatedNodes.IsEmpty()) - return true; - data.Animation.Channels.Resize(animatedNodes.Count(), false); + return; + + // Setup animation descriptor + auto& animation = data.Animations.AddOne(); + animation.Duration = (double)(int32)(localDuration * frameRate + 0.5f); + animation.FramesPerSecond = frameRate; + char nameData[256]; + takeInfo->name.toString(nameData); + animation.Name = nameData; + animation.Name = animation.Name.TrimTrailing(); + if (animation.Name.IsEmpty()) + animation.Name = String(layer->name); + animation.Channels.Resize(animatedNodes.Count(), false); + AnimInfo info; + info.TimeStart = takeInfo->local_time_from; + info.TimeEnd = takeInfo->local_time_to; + info.Duration = localDuration; + info.FramesCount = (int32)animation.Duration; + info.SamplingPeriod = 1.0f / frameRate; // Import curves for (int32 i = 0; i < animatedNodes.Count(); i++) { const int32 nodeIndex = animatedNodes[i]; auto& aNode = importerData.Nodes[nodeIndex]; - auto& anim = data.Animation.Channels[i]; + auto& anim = animation.Channels[i]; const ofbx::AnimationCurveNode* translationNode = layer->getCurveNode(*aNode.FbxObj, "Lcl Translation"); const ofbx::AnimationCurveNode* rotationNode = layer->getCurveNode(*aNode.FbxObj, "Lcl Rotation"); @@ -1066,9 +1074,8 @@ bool ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importer if (importerData.ConvertRH) { - for (int32 i = 0; i < data.Animation.Channels.Count(); i++) + for (auto& anim : animation.Channels) { - auto& anim = data.Animation.Channels[i]; auto& posKeys = anim.Position.GetKeyframes(); auto& rotKeys = anim.Rotation.GetKeyframes(); @@ -1084,8 +1091,6 @@ bool ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importer } } } - - return false; } static Float3 FbxVectorFromAxisAndSign(int axis, int sign) @@ -1105,239 +1110,185 @@ static Float3 FbxVectorFromAxisAndSign(int axis, int sign) bool ModelTool::ImportDataOpenFBX(const char* path, ModelData& data, Options& options, String& errorMsg) { - auto context = (OpenFbxImporterData*)options.SplitContext; - if (!context) + // Import file + Array fileData; + if (File::ReadAllBytes(String(path), fileData)) { - // Import file - Array fileData; - if (File::ReadAllBytes(String(path), fileData)) - { - errorMsg = TEXT("Cannot load file."); - return true; - } - ofbx::u64 loadFlags = 0; - if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry)) - { - loadFlags |= (ofbx::u64)ofbx::LoadFlags::TRIANGULATE; - if (!options.ImportBlendShapes) - loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES; - } - else - { - loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_GEOMETRY | (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES; - } - ofbx::IScene* scene = ofbx::load(fileData.Get(), fileData.Count(), loadFlags); - if (!scene) - { - errorMsg = ofbx::getError(); - return true; - } - fileData.Resize(0); - - // Tweak scene if exported by Blender - auto& globalInfo = *scene->getGlobalInfo(); - if (StringAnsiView(globalInfo.AppName).StartsWith(StringAnsiView("Blender"), StringSearchCase::IgnoreCase)) - { - auto ptr = const_cast(scene->getGlobalSettings()); - ptr->UpAxis = (ofbx::UpVector)((int32)ptr->UpAxis + 1); - } - - // Process imported scene - context = New(path, options, scene); - auto& globalSettings = context->GlobalSettings; - ProcessNodes(*context, scene->getRoot(), -1); - - // Apply model scene global scale factor - context->Nodes[0].LocalTransform = Transform(Vector3::Zero, Quaternion::Identity, globalSettings.UnitScaleFactor) * context->Nodes[0].LocalTransform; - - // Log scene info - LOG(Info, "Loaded FBX model, Frame Rate: {0}, Unit Scale Factor: {1}", context->FrameRate, globalSettings.UnitScaleFactor); - LOG(Info, "{0}, {1}, {2}", String(globalInfo.AppName), String(globalInfo.AppVersion), String(globalInfo.AppVendor)); - LOG(Info, "Up: {1}{0}", globalSettings.UpAxis == ofbx::UpVector_AxisX ? TEXT("X") : globalSettings.UpAxis == ofbx::UpVector_AxisY ? TEXT("Y") : TEXT("Z"), globalSettings.UpAxisSign == 1 ? TEXT("+") : TEXT("-")); - LOG(Info, "Front: {1}{0}", globalSettings.FrontAxis == ofbx::FrontVector_ParityEven ? TEXT("ParityEven") : TEXT("ParityOdd"), globalSettings.FrontAxisSign == 1 ? TEXT("+") : TEXT("-")); - LOG(Info, "{0} Handed{1}", globalSettings.CoordAxis == ofbx::CoordSystem_RightHanded ? TEXT("Right") : TEXT("Left"), globalSettings.CoordAxisSign == 1 ? TEXT("") : TEXT(" (negative)")); -#if OPEN_FBX_CONVERT_SPACE - LOG(Info, "Imported scene: Up={0}, Front={1}, Right={2}", context->Up, context->Front, context->Right); -#endif - - // Extract embedded textures - if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Textures)) - { - String outputPath; - for (int i = 0, c = scene->getEmbeddedDataCount(); i < c; i++) - { - const ofbx::DataView aEmbedded = scene->getEmbeddedData(i); - ofbx::DataView aFilename = scene->getEmbeddedFilename(i); - char filenameData[256]; - aFilename.toString(filenameData); - if (outputPath.IsEmpty()) - { - String pathStr(path); - outputPath = String(StringUtils::GetDirectoryName(pathStr)) / TEXT("textures"); - FileSystem::CreateDirectory(outputPath); - } - const String filenameStr(filenameData); - String embeddedPath = outputPath / StringUtils::GetFileName(filenameStr); - if (FileSystem::FileExists(embeddedPath)) - continue; - LOG(Info, "Extracing embedded resource to {0}", embeddedPath); - if (File::WriteAllBytes(embeddedPath, aEmbedded.begin + 4, (int32)(aEmbedded.end - aEmbedded.begin - 4))) - { - LOG(Error, "Failed to write data to file"); - } - } - } - -#if OPEN_FBX_CONVERT_SPACE - // Transform nodes to match the engine coordinates system - DirectX (UpVector = +Y, FrontVector = +Z, CoordSystem = -X (LeftHanded)) - if (context->Up == Float3(1, 0, 0) && context->Front == Float3(0, 0, 1) && context->Right == Float3(0, 1, 0)) - { - context->RootConvertRotation = Quaternion::Euler(0, 180, 0); - } - else if (context->Up == Float3(0, 1, 0) && context->Front == Float3(-1, 0, 0) && context->Right == Float3(0, 0, 1)) - { - context->RootConvertRotation = Quaternion::Euler(90, -90, 0); - } - /*Float3 engineUp(0, 1, 0); - Float3 engineFront(0, 0, 1); - Float3 engineRight(-1, 0, 0);*/ - /*Float3 engineUp(1, 0, 0); - Float3 engineFront(0, 0, 1); - Float3 engineRight(0, 1, 0); - if (context->Up != engineUp || context->Front != engineFront || context->Right != engineRight) - { - LOG(Info, "Converting imported scene nodes to match engine coordinates system"); - context->RootConvertRotation = Quaternion::GetRotationFromTo(context->Up, engineUp, engineUp); - //context->RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * context->Right, engineRight, engineRight); - //context->RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * context->Front, engineFront, engineFront); - }*/ - /*Float3 hackUp = FbxVectorFromAxisAndSign(globalSettings.UpAxis, globalSettings.UpAxisSign); - if (hackUp == Float3::UnitX) - context->RootConvertRotation = Quaternion::Euler(-90, 0, 0); - else if (hackUp == Float3::UnitZ) - context->RootConvertRotation = Quaternion::Euler(90, 0, 0);*/ - if (!context->RootConvertRotation.IsIdentity()) - { - for (auto& node : context->Nodes) - { - if (node.ParentIndex == -1) - { - node.LocalTransform.Orientation = context->RootConvertRotation * node.LocalTransform.Orientation; - break; - } - } - } -#endif + errorMsg = TEXT("Cannot load file."); + return true; } - DeleteMe contextCleanup(options.SplitContext ? nullptr : context); + ofbx::u64 loadFlags = 0; + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry)) + { + loadFlags |= (ofbx::u64)ofbx::LoadFlags::TRIANGULATE; + if (!options.ImportBlendShapes) + loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES; + } + else + { + loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_GEOMETRY | (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES; + } + ofbx::IScene* scene = ofbx::load(fileData.Get(), fileData.Count(), loadFlags); + if (!scene) + { + errorMsg = ofbx::getError(); + return true; + } + fileData.Resize(0); + + // Tweak scene if exported by Blender + auto& globalInfo = *scene->getGlobalInfo(); + if (StringAnsiView(globalInfo.AppName).StartsWith(StringAnsiView("Blender"), StringSearchCase::IgnoreCase)) + { + auto ptr = const_cast(scene->getGlobalSettings()); + ptr->UpAxis = (ofbx::UpVector)((int32)ptr->UpAxis + 1); + } + + // Process imported scene + OpenFbxImporterData context(path, options, scene); + auto& globalSettings = context.GlobalSettings; + ProcessNodes(context, scene->getRoot(), -1); + + // Apply model scene global scale factor + context.Nodes[0].LocalTransform = Transform(Vector3::Zero, Quaternion::Identity, globalSettings.UnitScaleFactor) * context.Nodes[0].LocalTransform; + + // Log scene info + LOG(Info, "Loaded FBX model, Frame Rate: {0}, Unit Scale Factor: {1}", context.FrameRate, globalSettings.UnitScaleFactor); + LOG(Info, "{0}, {1}, {2}", String(globalInfo.AppName), String(globalInfo.AppVersion), String(globalInfo.AppVendor)); + LOG(Info, "Up: {1}{0}", globalSettings.UpAxis == ofbx::UpVector_AxisX ? TEXT("X") : globalSettings.UpAxis == ofbx::UpVector_AxisY ? TEXT("Y") : TEXT("Z"), globalSettings.UpAxisSign == 1 ? TEXT("+") : TEXT("-")); + LOG(Info, "Front: {1}{0}", globalSettings.FrontAxis == ofbx::FrontVector_ParityEven ? TEXT("ParityEven") : TEXT("ParityOdd"), globalSettings.FrontAxisSign == 1 ? TEXT("+") : TEXT("-")); + LOG(Info, "{0} Handed{1}", globalSettings.CoordAxis == ofbx::CoordSystem_RightHanded ? TEXT("Right") : TEXT("Left"), globalSettings.CoordAxisSign == 1 ? TEXT("") : TEXT(" (negative)")); +#if OPEN_FBX_CONVERT_SPACE + LOG(Info, "Imported scene: Up={0}, Front={1}, Right={2}", context.Up, context.Front, context.Right); +#endif + + // Extract embedded textures + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Textures)) + { + String outputPath; + for (int i = 0, c = scene->getEmbeddedDataCount(); i < c; i++) + { + const ofbx::DataView aEmbedded = scene->getEmbeddedData(i); + ofbx::DataView aFilename = scene->getEmbeddedFilename(i); + char filenameData[256]; + aFilename.toString(filenameData); + if (outputPath.IsEmpty()) + { + String pathStr(path); + outputPath = String(StringUtils::GetDirectoryName(pathStr)) / TEXT("textures"); + FileSystem::CreateDirectory(outputPath); + } + const String filenameStr(filenameData); + String embeddedPath = outputPath / StringUtils::GetFileName(filenameStr); + if (FileSystem::FileExists(embeddedPath)) + continue; + LOG(Info, "Extracing embedded resource to {0}", embeddedPath); + if (File::WriteAllBytes(embeddedPath, aEmbedded.begin + 4, (int32)(aEmbedded.end - aEmbedded.begin - 4))) + { + LOG(Error, "Failed to write data to file"); + } + } + } + +#if OPEN_FBX_CONVERT_SPACE + // Transform nodes to match the engine coordinates system - DirectX (UpVector = +Y, FrontVector = +Z, CoordSystem = -X (LeftHanded)) + if (context.Up == Float3(1, 0, 0) && context.Front == Float3(0, 0, 1) && context.Right == Float3(0, 1, 0)) + { + context.RootConvertRotation = Quaternion::Euler(0, 180, 0); + } + else if (context.Up == Float3(0, 1, 0) && context.Front == Float3(-1, 0, 0) && context.Right == Float3(0, 0, 1)) + { + context.RootConvertRotation = Quaternion::Euler(90, -90, 0); + } + /*Float3 engineUp(0, 1, 0); + Float3 engineFront(0, 0, 1); + Float3 engineRight(-1, 0, 0);*/ + /*Float3 engineUp(1, 0, 0); + Float3 engineFront(0, 0, 1); + Float3 engineRight(0, 1, 0); + if (context.Up != engineUp || context.Front != engineFront || context.Right != engineRight) + { + LOG(Info, "Converting imported scene nodes to match engine coordinates system"); + context.RootConvertRotation = Quaternion::GetRotationFromTo(context.Up, engineUp, engineUp); + //context.RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * context.Right, engineRight, engineRight); + //context.RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * context.Front, engineFront, engineFront); + }*/ + /*Float3 hackUp = FbxVectorFromAxisAndSign(globalSettings.UpAxis, globalSettings.UpAxisSign); + if (hackUp == Float3::UnitX) + context.RootConvertRotation = Quaternion::Euler(-90, 0, 0); + else if (hackUp == Float3::UnitZ) + context.RootConvertRotation = Quaternion::Euler(90, 0, 0);*/ + if (!context.RootConvertRotation.IsIdentity()) + { + for (auto& node : context.Nodes) + { + if (node.ParentIndex == -1) + { + node.LocalTransform.Orientation = context.RootConvertRotation * node.LocalTransform.Orientation; + break; + } + } + } +#endif // Build final skeleton bones hierarchy before importing meshes if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton)) { - if (ImportBones(*context, errorMsg)) + if (ImportBones(context, errorMsg)) { LOG(Warning, "Failed to import skeleton bones."); return true; } - - Sorting::QuickSort(context->Bones.Get(), context->Bones.Count()); + Sorting::QuickSort(context.Bones.Get(), context.Bones.Count()); } // Import geometry (meshes and materials) - if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context->Scene->getMeshCount() > 0) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context.Scene->getMeshCount() > 0) { - const int meshCount = context->Scene->getMeshCount(); - if (options.SplitObjects && options.ObjectIndex == -1 && meshCount > 1) + const int meshCount = context.Scene->getMeshCount(); + for (int32 meshIndex = 0; meshIndex < meshCount; meshIndex++) { - // Import the first object within this call - options.SplitObjects = false; - options.ObjectIndex = 0; - - if (options.OnSplitImport.IsBinded()) - { - // Split all animations into separate assets - LOG(Info, "Splitting imported {0} meshes", meshCount); - for (int32 i = 1; i < meshCount; i++) - { - auto splitOptions = options; - splitOptions.ObjectIndex = i; - splitOptions.SplitContext = context; - const auto aMesh = context->Scene->getMesh(i); - const String objectName(aMesh->name); - options.OnSplitImport(splitOptions, objectName); - } - } - } - if (options.ObjectIndex != -1) - { - // Import the selected mesh - const auto meshIndex = Math::Clamp(options.ObjectIndex, 0, meshCount - 1); - if (ImportMesh(meshIndex, data, *context, errorMsg)) + if (ImportMesh(meshIndex, data, context, errorMsg)) return true; - - // Let the firstly imported mesh import all materials from all meshes (index 0 is importing all following ones before itself during splitting - see code above) - if (options.ObjectIndex == 1) - { - for (int32 i = 0; i < meshCount; i++) - { - const auto aMesh = context->Scene->getMesh(i); - if (i == 1 || IsMeshInvalid(aMesh)) - continue; - for (int32 j = 0; j < aMesh->getMaterialCount(); j++) - { - const ofbx::Material* aMaterial = aMesh->getMaterial(j); - context->AddMaterial(data, aMaterial); - } - } - } - } - else - { - // Import all meshes - for (int32 meshIndex = 0; meshIndex < meshCount; meshIndex++) - { - if (ImportMesh(meshIndex, data, *context, errorMsg)) - return true; - } } } // Import skeleton if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton)) { - data.Skeleton.Nodes.Resize(context->Nodes.Count(), false); - for (int32 i = 0; i < context->Nodes.Count(); i++) + data.Skeleton.Nodes.Resize(context.Nodes.Count(), false); + for (int32 i = 0; i < context.Nodes.Count(); i++) { auto& node = data.Skeleton.Nodes[i]; - auto& aNode = context->Nodes[i]; + auto& aNode = context.Nodes[i]; node.Name = aNode.Name; node.ParentIndex = aNode.ParentIndex; node.LocalTransform = aNode.LocalTransform; } - data.Skeleton.Bones.Resize(context->Bones.Count(), false); - for (int32 i = 0; i < context->Bones.Count(); i++) + data.Skeleton.Bones.Resize(context.Bones.Count(), false); + for (int32 i = 0; i < context.Bones.Count(); i++) { auto& bone = data.Skeleton.Bones[i]; - auto& aBone = context->Bones[i]; + auto& aBone = context.Bones[i]; const auto boneNodeIndex = aBone.NodeIndex; // Find the parent bone int32 parentBoneIndex = -1; - for (int32 j = context->Nodes[boneNodeIndex].ParentIndex; j != -1; j = context->Nodes[j].ParentIndex) + for (int32 j = context.Nodes[boneNodeIndex].ParentIndex; j != -1; j = context.Nodes[j].ParentIndex) { - parentBoneIndex = context->FindBone(j); + parentBoneIndex = context.FindBone(j); if (parentBoneIndex != -1) break; } aBone.ParentBoneIndex = parentBoneIndex; - const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : context->Bones[aBone.ParentBoneIndex].NodeIndex; + const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : context.Bones[aBone.ParentBoneIndex].NodeIndex; bone.ParentIndex = aBone.ParentBoneIndex; bone.NodeIndex = aBone.NodeIndex; - bone.LocalTransform = CombineTransformsFromNodeIndices(context->Nodes, parentBoneNodeIndex, boneNodeIndex); + bone.LocalTransform = CombineTransformsFromNodeIndices(context.Nodes, parentBoneNodeIndex, boneNodeIndex); bone.OffsetMatrix = aBone.OffsetMatrix; } } @@ -1345,54 +1296,21 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ModelData& data, Options& op // Import animations if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations)) { - const int animCount = context->Scene->getAnimationStackCount(); - if (options.SplitObjects && options.ObjectIndex == -1 && animCount > 1) + const int animCount = context.Scene->getAnimationStackCount(); + for (int32 animIndex = 0; animIndex < animCount; animIndex++) { - // Import the first object within this call - options.SplitObjects = false; - options.ObjectIndex = 0; - - if (options.OnSplitImport.IsBinded()) - { - // Split all animations into separate assets - LOG(Info, "Splitting imported {0} animations", animCount); - for (int32 i = 1; i < animCount; i++) - { - auto splitOptions = options; - splitOptions.ObjectIndex = i; - splitOptions.SplitContext = context; - const ofbx::AnimationStack* stack = context->Scene->getAnimationStack(i); - const ofbx::AnimationLayer* layer = stack->getLayer(0); - const String objectName(layer->name); - options.OnSplitImport(splitOptions, objectName); - } - } - } - if (options.ObjectIndex != -1) - { - // Import selected animation - const auto animIndex = Math::Clamp(options.ObjectIndex, 0, animCount - 1); - ImportAnimation(animIndex, data, *context); - } - else - { - // Import first valid animation - for (int32 animIndex = 0; animIndex < animCount; animIndex++) - { - if (!ImportAnimation(animIndex, data, *context)) - break; - } + ImportAnimation(animIndex, data, context); } } // Import nodes if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Nodes)) { - data.Nodes.Resize(context->Nodes.Count()); - for (int32 i = 0; i < context->Nodes.Count(); i++) + data.Nodes.Resize(context.Nodes.Count()); + for (int32 i = 0; i < context.Nodes.Count(); i++) { auto& node = data.Nodes[i]; - auto& aNode = context->Nodes[i]; + auto& aNode = context.Nodes[i]; node.Name = aNode.Name; node.ParentIndex = aNode.ParentIndex; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 38f010e28..4d6438522 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -452,6 +452,9 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options options.FramesRange.Y = Math::Max(options.FramesRange.Y, options.FramesRange.X); options.DefaultFrameRate = Math::Max(0.0f, options.DefaultFrameRate); options.SamplingRate = Math::Max(0.0f, options.SamplingRate); + if (options.SplitObjects) + options.MergeMeshes = false; // Meshes merging doesn't make sense when we want to import each mesh individually + // TODO: maybe we could update meshes merger to collapse meshes within the same name if splitting is enabled? // Validate path // Note: Assimp/Autodesk supports only ANSI characters in imported file path @@ -511,8 +514,6 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options FileSystem::DeleteFile(tmpPath); } - // TODO: check model LODs sequence (eg. {LOD0, LOD2, LOD5} is invalid) - // Remove namespace prefixes from the nodes names { for (auto& node : data.Nodes) @@ -523,9 +524,10 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options { RemoveNamespace(node.Name); } - for (auto& channel : data.Animation.Channels) + for (auto& animation : data.Animations) { - RemoveNamespace(channel.NodeName); + for (auto& channel : animation.Channels) + RemoveNamespace(channel.NodeName); } for (auto& lod : data.LODs) { @@ -533,18 +535,19 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options { RemoveNamespace(mesh->Name); for (auto& blendShape : mesh->BlendShapes) - { RemoveNamespace(blendShape.Name); - } } } } // Validate the animation channels - if (data.Animation.Channels.HasItems()) + for (auto& animation : data.Animations) { + auto& channels = animation.Channels; + if (channels.IsEmpty()) + continue; + // Validate bone animations uniqueness - auto& channels = data.Animation.Channels; for (int32 i = 0; i < channels.Count(); i++) { for (int32 j = i + 1; j < channels.Count(); j++) @@ -742,7 +745,7 @@ void TrySetupMaterialParameter(MaterialInstance* instance, Span par { if (type == MaterialParameterType::Color) { - if (paramType != MaterialParameterType::Vector3 || + if (paramType != MaterialParameterType::Vector3 || paramType != MaterialParameterType::Vector4) continue; } @@ -757,7 +760,7 @@ void TrySetupMaterialParameter(MaterialInstance* instance, Span par } } -bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& options, String& errorMsg, const String& autoImportOutput) +bool ModelTool::ImportModel(const String& path, ModelData& data, Options& options, String& errorMsg, const String& autoImportOutput) { LOG(Info, "Importing model from \'{0}\'", path); const auto startTime = DateTime::NowUTC(); @@ -785,7 +788,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op default: return true; } - ModelData data; if (ImportData(path, data, options, errorMsg)) return true; @@ -926,13 +928,20 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op case ModelType::Animation: { // Validate - if (data.Animation.Channels.IsEmpty()) + if (data.Animations.IsEmpty()) { errorMsg = TEXT("Imported file has no valid animations."); return true; } - - LOG(Info, "Imported animation has {0} channels, duration: {1} frames, frames per second: {2}", data.Animation.Channels.Count(), data.Animation.Duration, data.Animation.FramesPerSecond); + for (auto& animation : data.Animations) + { + LOG(Info, "Imported animation '{}' has {} channels, duration: {} frames, frames per second: {}", animation.Name, animation.Channels.Count(), animation.Duration, animation.FramesPerSecond); + if (animation.Duration <= ZeroTolerance || animation.FramesPerSecond <= ZeroTolerance) + { + errorMsg = TEXT("Invalid animation duration."); + return true; + } + } break; } } @@ -976,7 +985,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op case TextureEntry::TypeHint::Normals: textureOptions.Type = TextureFormatType::NormalMap; break; - default: ; } AssetsImportingManager::ImportIfEdited(texture.FilePath, assetPath, texture.AssetID, &textureOptions); #endif @@ -1461,65 +1469,67 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op } else if (options.Type == ModelType::Animation) { - // Trim the animation keyframes range if need to - if (options.Duration == AnimationDuration::Custom) + for (auto& animation : data.Animations) { - // Custom animation import, frame index start and end - const float start = options.FramesRange.X; - const float end = options.FramesRange.Y; - for (int32 i = 0; i < data.Animation.Channels.Count(); i++) + // Trim the animation keyframes range if need to + if (options.Duration == AnimationDuration::Custom) { - auto& anim = data.Animation.Channels[i]; - anim.Position.Trim(start, end); - anim.Rotation.Trim(start, end); - anim.Scale.Trim(start, end); - } - data.Animation.Duration = end - start; - } - - // Change the sampling rate if need to - if (!Math::IsZero(options.SamplingRate)) - { - const float timeScale = (float)(data.Animation.FramesPerSecond / options.SamplingRate); - if (!Math::IsOne(timeScale)) - { - data.Animation.FramesPerSecond = options.SamplingRate; - for (int32 i = 0; i < data.Animation.Channels.Count(); i++) + // Custom animation import, frame index start and end + const float start = options.FramesRange.X; + const float end = options.FramesRange.Y; + for (int32 i = 0; i < animation.Channels.Count(); i++) { - auto& anim = data.Animation.Channels[i]; + auto& anim = animation.Channels[i]; + anim.Position.Trim(start, end); + anim.Rotation.Trim(start, end); + anim.Scale.Trim(start, end); + } + animation.Duration = end - start; + } - anim.Position.TransformTime(timeScale, 0.0f); - anim.Rotation.TransformTime(timeScale, 0.0f); - anim.Scale.TransformTime(timeScale, 0.0f); + // Change the sampling rate if need to + if (!Math::IsZero(options.SamplingRate)) + { + const float timeScale = (float)(animation.FramesPerSecond / options.SamplingRate); + if (!Math::IsOne(timeScale)) + { + animation.FramesPerSecond = options.SamplingRate; + for (int32 i = 0; i < animation.Channels.Count(); i++) + { + auto& anim = animation.Channels[i]; + anim.Position.TransformTime(timeScale, 0.0f); + anim.Rotation.TransformTime(timeScale, 0.0f); + anim.Scale.TransformTime(timeScale, 0.0f); + } } } - } - // Optimize the keyframes - if (options.OptimizeKeyframes) - { - const int32 before = data.Animation.GetKeyframesCount(); - for (int32 i = 0; i < data.Animation.Channels.Count(); i++) + // Optimize the keyframes + if (options.OptimizeKeyframes) { - auto& anim = data.Animation.Channels[i]; - - // Optimize keyframes - OptimizeCurve(anim.Position); - OptimizeCurve(anim.Rotation); - OptimizeCurve(anim.Scale); - - // Remove empty channels - if (anim.GetKeyframesCount() == 0) + const int32 before = animation.GetKeyframesCount(); + for (int32 i = 0; i < animation.Channels.Count(); i++) { - data.Animation.Channels.RemoveAt(i--); - } - } - const int32 after = data.Animation.GetKeyframesCount(); - LOG(Info, "Optimized {0} animation keyframe(s). Before: {1}, after: {2}, Ratio: {3}%", before - after, before, after, Utilities::RoundTo2DecimalPlaces((float)after / before)); - } + auto& anim = animation.Channels[i]; - data.Animation.EnableRootMotion = options.EnableRootMotion; - data.Animation.RootNodeName = options.RootNodeName; + // Optimize keyframes + OptimizeCurve(anim.Position); + OptimizeCurve(anim.Rotation); + OptimizeCurve(anim.Scale); + + // Remove empty channels + if (anim.GetKeyframesCount() == 0) + { + animation.Channels.RemoveAt(i--); + } + } + const int32 after = animation.GetKeyframesCount(); + LOG(Info, "Optimized {0} animation keyframe(s). Before: {1}, after: {2}, Ratio: {3}%", before - after, before, after, Utilities::RoundTo2DecimalPlaces((float)after / before)); + } + + animation.EnableRootMotion = options.EnableRootMotion; + animation.RootNodeName = options.RootNodeName; + } } // Merge meshes with the same parent nodes, material and skinning @@ -1696,27 +1706,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op } } - // Export imported data to the output container (we reduce vertex data copy operations to minimum) - { - meshData.Textures.Swap(data.Textures); - meshData.Materials.Swap(data.Materials); - meshData.LODs.Resize(data.LODs.Count(), false); - for (int32 i = 0; i < data.LODs.Count(); i++) - { - auto& dst = meshData.LODs[i]; - auto& src = data.LODs[i]; - - dst.Meshes = src.Meshes; - } - meshData.Skeleton.Swap(data.Skeleton); - meshData.Animation.Swap(data.Animation); - - // Clear meshes from imported data (we link them to result model data). This reduces amount of allocations. - data.LODs.Resize(0); - } - // Calculate blend shapes vertices ranges - for (auto& lod : meshData.LODs) + for (auto& lod : data.LODs) { for (auto& mesh : lod.Meshes) { @@ -1737,6 +1728,9 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op } } + // Auto calculate LODs transition settings + data.CalculateLODsScreenSizes(); + const auto endTime = DateTime::NowUTC(); LOG(Info, "Model file imported in {0} ms", static_cast((endTime - startTime).GetTotalMilliseconds())); diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index e3da7e2c3..cec9bdcf2 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -296,13 +296,17 @@ public: public: // Internals - // Runtime data for objects splitting during import (used internally) - void* SplitContext = nullptr; - Function OnSplitImport; - // Internal flags for objects to import. ImportDataTypes ImportTypes = ImportDataTypes::None; + struct CachedData + { + ModelData* Data = nullptr; + void* MeshesByName = nullptr; + }; + // Cached model data - used when performing nested importing (eg. via objects splitting). Allows to read and process source file only once and use those results for creation of multiple assets (permutation via ObjectIndex). + CachedData* Cached = nullptr; + public: // [ISerializable] void Serialize(SerializeStream& stream, const void* otherObj) override; @@ -324,12 +328,12 @@ public: /// Imports the model. /// /// The file path. - /// The output data. + /// The output data. /// The import options. /// The error message container. /// The output folder for the additional imported data - optional. Used to auto-import textures and material assets. /// True if fails, otherwise false. - static bool ImportModel(const String& path, ModelData& meshData, Options& options, String& errorMsg, const String& autoImportOutput = String::Empty); + static bool ImportModel(const String& path, ModelData& data, Options& options, String& errorMsg, const String& autoImportOutput = String::Empty); public: static int32 DetectLodIndex(const String& nodeName); From c8dd2c045c85a990be66216b0bf0effc3f9e3d6e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 1 Dec 2023 13:57:34 +0100 Subject: [PATCH 34/79] Simplify sorting arrays code --- Source/Editor/Cooker/Steps/CookAssetsStep.cpp | 2 +- Source/Editor/Cooker/Steps/DeployDataStep.cpp | 2 +- Source/Engine/Core/Collections/Sorting.h | 10 ++++++++++ Source/Engine/Core/Log.cpp | 2 +- .../GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp | 8 ++++---- Source/Engine/Renderer/RenderList.cpp | 4 ++-- Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp | 2 +- Source/Engine/Visject/VisjectGraph.cpp | 2 +- 8 files changed, 21 insertions(+), 11 deletions(-) diff --git a/Source/Editor/Cooker/Steps/CookAssetsStep.cpp b/Source/Editor/Cooker/Steps/CookAssetsStep.cpp index 4d4ea5e07..8fd87bcfd 100644 --- a/Source/Editor/Cooker/Steps/CookAssetsStep.cpp +++ b/Source/Editor/Cooker/Steps/CookAssetsStep.cpp @@ -1270,7 +1270,7 @@ bool CookAssetsStep::Perform(CookingData& data) { Array assetTypes; data.Stats.AssetStats.GetValues(assetTypes); - Sorting::QuickSort(assetTypes.Get(), assetTypes.Count()); + Sorting::QuickSort(assetTypes); LOG(Info, ""); LOG(Info, "Top assets types stats:"); diff --git a/Source/Editor/Cooker/Steps/DeployDataStep.cpp b/Source/Editor/Cooker/Steps/DeployDataStep.cpp index c1179c77a..80cb73c2b 100644 --- a/Source/Editor/Cooker/Steps/DeployDataStep.cpp +++ b/Source/Editor/Cooker/Steps/DeployDataStep.cpp @@ -119,7 +119,7 @@ bool DeployDataStep::Perform(CookingData& data) if (!version.StartsWith(TEXT("7."))) version.Clear(); } - Sorting::QuickSort(versions.Get(), versions.Count()); + Sorting::QuickSort(versions); const String version = versions.Last(); FileSystem::NormalizePath(srcDotnet); LOG(Info, "Using .Net Runtime {} at {}", version, srcDotnet); diff --git a/Source/Engine/Core/Collections/Sorting.h b/Source/Engine/Core/Collections/Sorting.h index 2210f6f1c..67b639c80 100644 --- a/Source/Engine/Core/Collections/Sorting.h +++ b/Source/Engine/Core/Collections/Sorting.h @@ -45,6 +45,16 @@ public: }; public: + /// + /// Sorts the linear data array using Quick Sort algorithm (non recursive version, uses temporary stack collection). + /// + /// The data container. + template + FORCE_INLINE static void QuickSort(Array& data) + { + QuickSort(data.Get(), data.Count()); + } + /// /// Sorts the linear data array using Quick Sort algorithm (non recursive version, uses temporary stack collection). /// diff --git a/Source/Engine/Core/Log.cpp b/Source/Engine/Core/Log.cpp index bb8d42aba..b6b4fa3e2 100644 --- a/Source/Engine/Core/Log.cpp +++ b/Source/Engine/Core/Log.cpp @@ -58,7 +58,7 @@ bool Log::Logger::Init() int32 remaining = oldLogs.Count() - maxLogFiles + 1; if (remaining > 0) { - Sorting::QuickSort(oldLogs.Get(), oldLogs.Count()); + Sorting::QuickSort(oldLogs); // Delete the oldest logs int32 i = 0; diff --git a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp index 0b857d394..a69187f6f 100644 --- a/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp +++ b/Source/Engine/GraphicsDevice/Vulkan/GPUDeviceVulkan.Layers.cpp @@ -247,7 +247,7 @@ void GPUDeviceVulkan::GetInstanceLayersAndExtensions(Array& outInst if (foundUniqueLayers.HasItems()) { LOG(Info, "Found instance layers:"); - Sorting::QuickSort(foundUniqueLayers.Get(), foundUniqueLayers.Count()); + Sorting::QuickSort(foundUniqueLayers); for (const StringAnsi& name : foundUniqueLayers) { LOG(Info, "- {0}", String(name)); @@ -257,7 +257,7 @@ void GPUDeviceVulkan::GetInstanceLayersAndExtensions(Array& outInst if (foundUniqueExtensions.HasItems()) { LOG(Info, "Found instance extensions:"); - Sorting::QuickSort(foundUniqueExtensions.Get(), foundUniqueExtensions.Count()); + Sorting::QuickSort(foundUniqueExtensions); for (const StringAnsi& name : foundUniqueExtensions) { LOG(Info, "- {0}", String(name)); @@ -455,7 +455,7 @@ void GPUDeviceVulkan::GetDeviceExtensionsAndLayers(VkPhysicalDevice gpu, Array Date: Sat, 2 Dec 2023 15:29:49 +0300 Subject: [PATCH 35/79] Add Half to Vector2/Vector4 --- Source/Engine/Core/Math/Vector2.cpp | 2 ++ Source/Engine/Core/Math/Vector2.h | 3 +++ Source/Engine/Core/Math/Vector4.cpp | 2 ++ Source/Engine/Core/Math/Vector4.h | 3 +++ 4 files changed, 10 insertions(+) diff --git a/Source/Engine/Core/Math/Vector2.cpp b/Source/Engine/Core/Math/Vector2.cpp index ec168de33..63e6fc4c5 100644 --- a/Source/Engine/Core/Math/Vector2.cpp +++ b/Source/Engine/Core/Math/Vector2.cpp @@ -15,6 +15,8 @@ const Float2 Float2::Zero(0.0f); template<> const Float2 Float2::One(1.0f); template<> +const Float2 Float2::Half(0.5f); +template<> const Float2 Float2::UnitX(1.0f, 0.0f); template<> const Float2 Float2::UnitY(0.0f, 1.0f); diff --git a/Source/Engine/Core/Math/Vector2.h b/Source/Engine/Core/Math/Vector2.h index cea03ec10..cce77788a 100644 --- a/Source/Engine/Core/Math/Vector2.h +++ b/Source/Engine/Core/Math/Vector2.h @@ -44,6 +44,9 @@ public: // Vector with all components equal 1 static FLAXENGINE_API const Vector2Base One; + // Vector with all components equal 0.5 + static FLAXENGINE_API const Vector2Base Half; + // Vector X=1, Y=0 static FLAXENGINE_API const Vector2Base UnitX; diff --git a/Source/Engine/Core/Math/Vector4.cpp b/Source/Engine/Core/Math/Vector4.cpp index b5fa7a81d..372fde6db 100644 --- a/Source/Engine/Core/Math/Vector4.cpp +++ b/Source/Engine/Core/Math/Vector4.cpp @@ -17,6 +17,8 @@ const Float4 Float4::Zero(0.0f); template<> const Float4 Float4::One(1.0f); template<> +const Float4 Float4::Half(0.5f); +template<> const Float4 Float4::UnitX(1.0f, 0.0f, 0.0f, 0.0f); template<> const Float4 Float4::UnitY(0.0f, 1.0f, 0.0f, 0.0f); diff --git a/Source/Engine/Core/Math/Vector4.h b/Source/Engine/Core/Math/Vector4.h index 1cc6d4db8..7edb97ce5 100644 --- a/Source/Engine/Core/Math/Vector4.h +++ b/Source/Engine/Core/Math/Vector4.h @@ -54,6 +54,9 @@ public: // Vector with all components equal 1 static FLAXENGINE_API const Vector4Base One; + // Vector with all components equal 0.5 + static FLAXENGINE_API const Vector4Base Half; + // Vector X=1, Y=0, Z=0, W=0 static FLAXENGINE_API const Vector4Base UnitX; From 9a712ba3cf3dcecde6d2e39e7b33cdc17d850612 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 2 Dec 2023 09:12:38 -0600 Subject: [PATCH 36/79] Fix selecting objects/gizmos with high far plane. --- Source/Editor/Viewport/EditorViewport.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Viewport/EditorViewport.cs b/Source/Editor/Viewport/EditorViewport.cs index c49392d01..9ac9b5571 100644 --- a/Source/Editor/Viewport/EditorViewport.cs +++ b/Source/Editor/Viewport/EditorViewport.cs @@ -1374,8 +1374,8 @@ namespace FlaxEditor.Viewport ivp.Invert(); // Create near and far points - var nearPoint = new Vector3(mousePosition, 0.0f); - var farPoint = new Vector3(mousePosition, 1.0f); + var nearPoint = new Vector3(mousePosition, _nearPlane); + var farPoint = new Vector3(mousePosition, _farPlane); viewport.Unproject(ref nearPoint, ref ivp, out nearPoint); viewport.Unproject(ref farPoint, ref ivp, out farPoint); From f67c0d2ac0fff1d62c842f1cd13bed1ad0957613 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 2 Dec 2023 09:23:43 -0600 Subject: [PATCH 37/79] Change `ScaleWithResolution` defaults --- Source/Engine/UI/GUI/CanvasScaler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Source/Engine/UI/GUI/CanvasScaler.cs b/Source/Engine/UI/GUI/CanvasScaler.cs index 94936c0e2..684583c32 100644 --- a/Source/Engine/UI/GUI/CanvasScaler.cs +++ b/Source/Engine/UI/GUI/CanvasScaler.cs @@ -98,8 +98,8 @@ namespace FlaxEngine.GUI private float _scale = 1.0f; private float _scaleFactor = 1.0f; private float _physicalUnitSize = 1.0f; - private Float2 _resolutionMin = new Float2(1, 1); - private Float2 _resolutionMax = new Float2(10000, 10000); + private Float2 _resolutionMin = new Float2(640, 480); + private Float2 _resolutionMax = new Float2(7680, 4320); /// /// Gets the current UI scale. Computed based on the setup when performing layout. From 58bfd1954ef7c7cf47274d93dce9e76eed168979 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 2 Dec 2023 09:24:04 -0600 Subject: [PATCH 38/79] Fix UICanvas to only spawn CanvasScalar if it doesnt already have one. --- .../Editor/SceneGraph/Actors/UICanvasNode.cs | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs index ed6fc08d6..01a02b089 100644 --- a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs +++ b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs @@ -31,13 +31,25 @@ namespace FlaxEditor.SceneGraph.Actors // Rotate to match the space (GUI uses upper left corner as a root) Actor.LocalOrientation = Quaternion.Euler(0, -180, -180); - var uiControl = new UIControl + bool canSpawn = true; + foreach (var uiControl in Actor.GetChildren()) { - Name = "Canvas Scalar", - Transform = Actor.Transform, - Control = new CanvasScaler() - }; - Root.Spawn(uiControl, Actor); + if (uiControl.Get() == null) + continue; + canSpawn = false; + } + + if (canSpawn) + { + var uiControl = new UIControl + { + Name = "Canvas Scalar", + Transform = Actor.Transform, + Control = new CanvasScaler() + }; + Root.Spawn(uiControl, Actor); + } + _treeNode.Expand(); } /// From 7d15944381765d2b95ab02c9e769a33ea059c4b0 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 2 Dec 2023 09:27:13 -0600 Subject: [PATCH 39/79] Add break --- Source/Editor/SceneGraph/Actors/UICanvasNode.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs index 01a02b089..97cc505b4 100644 --- a/Source/Editor/SceneGraph/Actors/UICanvasNode.cs +++ b/Source/Editor/SceneGraph/Actors/UICanvasNode.cs @@ -37,6 +37,7 @@ namespace FlaxEditor.SceneGraph.Actors if (uiControl.Get() == null) continue; canSpawn = false; + break; } if (canSpawn) From 9bde0f9f9b7f4b02560fa891f0a6a8c2d241b270 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 2 Dec 2023 09:40:33 -0600 Subject: [PATCH 40/79] Fix layout of editor updating when adding a script to multiple actors in a scene. --- Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs index d7bfbbad7..c0567b533 100644 --- a/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs +++ b/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs @@ -245,6 +245,7 @@ namespace FlaxEditor.CustomEditors.Dedicated var multiAction = new MultiUndoAction(actions); multiAction.Do(); + ScriptsEditor.ParentEditor?.RebuildLayout(); var presenter = ScriptsEditor.Presenter; if (presenter != null) { From c5c20c8e28c5ef313cb5eaaf398179dcced1629c Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 2 Dec 2023 10:50:59 -0600 Subject: [PATCH 41/79] Remove zero clamp on hinge velocity. --- Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp index 768bff1fa..a51ce073a 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -2800,7 +2800,7 @@ void PhysicsBackend::SetHingeJointLimit(void* joint, const LimitAngularRange& va void PhysicsBackend::SetHingeJointDrive(void* joint, const HingeJointDrive& value) { auto jointPhysX = (PxRevoluteJoint*)joint; - jointPhysX->setDriveVelocity(Math::Max(value.Velocity, 0.0f)); + jointPhysX->setDriveVelocity(value.Velocity); jointPhysX->setDriveForceLimit(Math::Max(value.ForceLimit, 0.0f)); jointPhysX->setDriveGearRatio(Math::Max(value.GearRatio, 0.0f)); jointPhysX->setRevoluteJointFlag(PxRevoluteJointFlag::eDRIVE_FREESPIN, value.FreeSpin); From 73d33e4af017aca6b0a9f7557cd377296ba205e9 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 2 Dec 2023 12:01:32 -0600 Subject: [PATCH 42/79] Refactor Physics Colliders to use auto serialization. --- .../Engine/Physics/Colliders/BoxCollider.cpp | 18 ----------- Source/Engine/Physics/Colliders/BoxCollider.h | 3 +- .../Physics/Colliders/CapsuleCollider.cpp | 20 ------------- .../Physics/Colliders/CapsuleCollider.h | 3 +- .../Physics/Colliders/CharacterController.cpp | 30 ------------------- .../Physics/Colliders/CharacterController.h | 3 +- Source/Engine/Physics/Colliders/Collider.cpp | 24 --------------- Source/Engine/Physics/Colliders/Collider.h | 3 +- .../Engine/Physics/Colliders/MeshCollider.cpp | 18 ----------- .../Engine/Physics/Colliders/MeshCollider.h | 3 +- .../Physics/Colliders/SphereCollider.cpp | 18 ----------- .../Engine/Physics/Colliders/SphereCollider.h | 3 +- .../Physics/Colliders/SplineCollider.cpp | 20 ------------- .../Engine/Physics/Colliders/SplineCollider.h | 3 +- 14 files changed, 7 insertions(+), 162 deletions(-) diff --git a/Source/Engine/Physics/Colliders/BoxCollider.cpp b/Source/Engine/Physics/Colliders/BoxCollider.cpp index fde3b4632..a0a6b96ba 100644 --- a/Source/Engine/Physics/Colliders/BoxCollider.cpp +++ b/Source/Engine/Physics/Colliders/BoxCollider.cpp @@ -116,24 +116,6 @@ bool BoxCollider::IntersectsItself(const Ray& ray, Real& distance, Vector3& norm return _bounds.Intersects(ray, distance, normal); } -void BoxCollider::Serialize(SerializeStream& stream, const void* otherObj) -{ - // Base - Collider::Serialize(stream, otherObj); - - SERIALIZE_GET_OTHER_OBJ(BoxCollider); - - SERIALIZE_MEMBER(Size, _size); -} - -void BoxCollider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) -{ - // Base - Collider::Deserialize(stream, modifier); - - DESERIALIZE_MEMBER(Size, _size); -} - void BoxCollider::UpdateBounds() { // Cache bounds diff --git a/Source/Engine/Physics/Colliders/BoxCollider.h b/Source/Engine/Physics/Colliders/BoxCollider.h index 0cc0e7be9..5bcc21b45 100644 --- a/Source/Engine/Physics/Colliders/BoxCollider.h +++ b/Source/Engine/Physics/Colliders/BoxCollider.h @@ -12,6 +12,7 @@ API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Colliders/Box Collider\"), ActorToolbox(\"Physics\")") class FLAXENGINE_API BoxCollider : public Collider { + API_AUTO_SERIALIZATION(); DECLARE_SCENE_OBJECT(BoxCollider); private: Float3 _size; @@ -49,8 +50,6 @@ public: void OnDebugDrawSelected() override; #endif bool IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) override; - void Serialize(SerializeStream& stream, const void* otherObj) override; - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; protected: // [Collider] diff --git a/Source/Engine/Physics/Colliders/CapsuleCollider.cpp b/Source/Engine/Physics/Colliders/CapsuleCollider.cpp index 98346b0a9..a64a1fb0c 100644 --- a/Source/Engine/Physics/Colliders/CapsuleCollider.cpp +++ b/Source/Engine/Physics/Colliders/CapsuleCollider.cpp @@ -81,26 +81,6 @@ bool CapsuleCollider::IntersectsItself(const Ray& ray, Real& distance, Vector3& return _orientedBox.Intersects(ray, distance, normal); } -void CapsuleCollider::Serialize(SerializeStream& stream, const void* otherObj) -{ - // Base - Collider::Serialize(stream, otherObj); - - SERIALIZE_GET_OTHER_OBJ(CapsuleCollider); - - SERIALIZE_MEMBER(Radius, _radius); - SERIALIZE_MEMBER(Height, _height); -} - -void CapsuleCollider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) -{ - // Base - Collider::Deserialize(stream, modifier); - - DESERIALIZE_MEMBER(Radius, _radius); - DESERIALIZE_MEMBER(Height, _height); -} - void CapsuleCollider::UpdateBounds() { // Cache bounds diff --git a/Source/Engine/Physics/Colliders/CapsuleCollider.h b/Source/Engine/Physics/Colliders/CapsuleCollider.h index 5ae53cf53..8eff1c164 100644 --- a/Source/Engine/Physics/Colliders/CapsuleCollider.h +++ b/Source/Engine/Physics/Colliders/CapsuleCollider.h @@ -13,6 +13,7 @@ API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Colliders/Capsule Collider\"), ActorToolbox(\"Physics\")") class FLAXENGINE_API CapsuleCollider : public Collider { + API_AUTO_SERIALIZATION(); DECLARE_SCENE_OBJECT(CapsuleCollider); private: float _radius; @@ -58,8 +59,6 @@ public: void OnDebugDrawSelected() override; #endif bool IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) override; - void Serialize(SerializeStream& stream, const void* otherObj) override; - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; protected: // [Collider] diff --git a/Source/Engine/Physics/Colliders/CharacterController.cpp b/Source/Engine/Physics/Colliders/CharacterController.cpp index 41ee95d04..6b20203a0 100644 --- a/Source/Engine/Physics/Colliders/CharacterController.cpp +++ b/Source/Engine/Physics/Colliders/CharacterController.cpp @@ -387,33 +387,3 @@ void CharacterController::OnPhysicsSceneChanged(PhysicsScene* previous) DeleteController(); CreateController(); } - -void CharacterController::Serialize(SerializeStream& stream, const void* otherObj) -{ - // Base - Collider::Serialize(stream, otherObj); - - SERIALIZE_GET_OTHER_OBJ(CharacterController); - - SERIALIZE_MEMBER(StepOffset, _stepOffset); - SERIALIZE_MEMBER(SlopeLimit, _slopeLimit); - SERIALIZE_MEMBER(NonWalkableMode, _nonWalkableMode); - SERIALIZE_MEMBER(Radius, _radius); - SERIALIZE_MEMBER(Height, _height); - SERIALIZE_MEMBER(MinMoveDistance, _minMoveDistance); - SERIALIZE_MEMBER(UpDirection, _upDirection); -} - -void CharacterController::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) -{ - // Base - Collider::Deserialize(stream, modifier); - - DESERIALIZE_MEMBER(StepOffset, _stepOffset); - DESERIALIZE_MEMBER(SlopeLimit, _slopeLimit); - DESERIALIZE_MEMBER(NonWalkableMode, _nonWalkableMode); - DESERIALIZE_MEMBER(Radius, _radius); - DESERIALIZE_MEMBER(Height, _height); - DESERIALIZE_MEMBER(MinMoveDistance, _minMoveDistance); - DESERIALIZE_MEMBER(UpDirection, _upDirection); -} diff --git a/Source/Engine/Physics/Colliders/CharacterController.h b/Source/Engine/Physics/Colliders/CharacterController.h index 919deaafe..737d98728 100644 --- a/Source/Engine/Physics/Colliders/CharacterController.h +++ b/Source/Engine/Physics/Colliders/CharacterController.h @@ -12,6 +12,7 @@ API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Character Controller\"), ActorToolbox(\"Physics\")") class FLAXENGINE_API CharacterController : public Collider, public IPhysicsActor { + API_AUTO_SERIALIZATION(); DECLARE_SCENE_OBJECT(CharacterController); public: /// @@ -198,8 +199,6 @@ public: #if USE_EDITOR void OnDebugDrawSelected() override; #endif - void Serialize(SerializeStream& stream, const void* otherObj) override; - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; void CreateShape() override; void UpdateBounds() override; void AddMovement(const Vector3& translation, const Quaternion& rotation) override; diff --git a/Source/Engine/Physics/Colliders/Collider.cpp b/Source/Engine/Physics/Colliders/Collider.cpp index 6c292bf20..4d8fecd7e 100644 --- a/Source/Engine/Physics/Colliders/Collider.cpp +++ b/Source/Engine/Physics/Colliders/Collider.cpp @@ -292,30 +292,6 @@ void Collider::OnMaterialChanged() PhysicsBackend::SetShapeMaterial(_shape, Material.Get()); } -void Collider::Serialize(SerializeStream& stream, const void* otherObj) -{ - // Base - PhysicsColliderActor::Serialize(stream, otherObj); - - SERIALIZE_GET_OTHER_OBJ(Collider); - - SERIALIZE_MEMBER(IsTrigger, _isTrigger); - SERIALIZE_MEMBER(Center, _center); - SERIALIZE_MEMBER(ContactOffset, _contactOffset); - SERIALIZE(Material); -} - -void Collider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) -{ - // Base - PhysicsColliderActor::Deserialize(stream, modifier); - - DESERIALIZE_MEMBER(IsTrigger, _isTrigger); - DESERIALIZE_MEMBER(Center, _center); - DESERIALIZE_MEMBER(ContactOffset, _contactOffset); - DESERIALIZE(Material); -} - void Collider::BeginPlay(SceneBeginData* data) { // Check if has no shape created (it means no rigidbody requested it but also collider may be spawned at runtime) diff --git a/Source/Engine/Physics/Colliders/Collider.h b/Source/Engine/Physics/Colliders/Collider.h index d3bae9407..bd17aa27a 100644 --- a/Source/Engine/Physics/Colliders/Collider.h +++ b/Source/Engine/Physics/Colliders/Collider.h @@ -17,6 +17,7 @@ class RigidBody; /// API_CLASS(Abstract) class FLAXENGINE_API Collider : public PhysicsColliderActor { + API_AUTO_SERIALIZATION(); DECLARE_SCENE_OBJECT_ABSTRACT(Collider); protected: Vector3 _center; @@ -196,8 +197,6 @@ private: public: // [PhysicsColliderActor] - void Serialize(SerializeStream& stream, const void* otherObj) override; - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; RigidBody* GetAttachedRigidBody() const override; protected: diff --git a/Source/Engine/Physics/Colliders/MeshCollider.cpp b/Source/Engine/Physics/Colliders/MeshCollider.cpp index 94429ab7d..28ce0ff16 100644 --- a/Source/Engine/Physics/Colliders/MeshCollider.cpp +++ b/Source/Engine/Physics/Colliders/MeshCollider.cpp @@ -117,24 +117,6 @@ bool MeshCollider::IntersectsItself(const Ray& ray, Real& distance, Vector3& nor return _box.Intersects(ray, distance, normal); } -void MeshCollider::Serialize(SerializeStream& stream, const void* otherObj) -{ - // Base - Collider::Serialize(stream, otherObj); - - SERIALIZE_GET_OTHER_OBJ(MeshCollider); - - SERIALIZE(CollisionData); -} - -void MeshCollider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) -{ - // Base - Collider::Deserialize(stream, modifier); - - DESERIALIZE(CollisionData); -} - void MeshCollider::UpdateBounds() { // Cache bounds diff --git a/Source/Engine/Physics/Colliders/MeshCollider.h b/Source/Engine/Physics/Colliders/MeshCollider.h index a25f4d9c8..ab3b7047a 100644 --- a/Source/Engine/Physics/Colliders/MeshCollider.h +++ b/Source/Engine/Physics/Colliders/MeshCollider.h @@ -13,6 +13,7 @@ API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Colliders/Mesh Collider\"), ActorToolbox(\"Physics\")") class FLAXENGINE_API MeshCollider : public Collider { + API_AUTO_SERIALIZATION(); DECLARE_SCENE_OBJECT(MeshCollider); public: /// @@ -33,8 +34,6 @@ public: void OnDebugDrawSelected() override; #endif bool IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) override; - void Serialize(SerializeStream& stream, const void* otherObj) override; - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; protected: // [Collider] diff --git a/Source/Engine/Physics/Colliders/SphereCollider.cpp b/Source/Engine/Physics/Colliders/SphereCollider.cpp index fa2fb1cca..1c44811b9 100644 --- a/Source/Engine/Physics/Colliders/SphereCollider.cpp +++ b/Source/Engine/Physics/Colliders/SphereCollider.cpp @@ -58,24 +58,6 @@ bool SphereCollider::IntersectsItself(const Ray& ray, Real& distance, Vector3& n return _sphere.Intersects(ray, distance, normal); } -void SphereCollider::Serialize(SerializeStream& stream, const void* otherObj) -{ - // Base - Collider::Serialize(stream, otherObj); - - SERIALIZE_GET_OTHER_OBJ(SphereCollider); - - SERIALIZE_MEMBER(Radius, _radius); -} - -void SphereCollider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) -{ - // Base - Collider::Deserialize(stream, modifier); - - DESERIALIZE_MEMBER(Radius, _radius); -} - void SphereCollider::UpdateBounds() { // Cache bounds diff --git a/Source/Engine/Physics/Colliders/SphereCollider.h b/Source/Engine/Physics/Colliders/SphereCollider.h index 39f6fd63c..061372af4 100644 --- a/Source/Engine/Physics/Colliders/SphereCollider.h +++ b/Source/Engine/Physics/Colliders/SphereCollider.h @@ -11,6 +11,7 @@ API_CLASS(Attributes="ActorContextMenu(\"New/Physics/Colliders/Sphere Collider\"), ActorToolbox(\"Physics\")") class FLAXENGINE_API SphereCollider : public Collider { + API_AUTO_SERIALIZATION(); DECLARE_SCENE_OBJECT(SphereCollider); private: float _radius; @@ -38,8 +39,6 @@ public: void OnDebugDrawSelected() override; #endif bool IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) override; - void Serialize(SerializeStream& stream, const void* otherObj) override; - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; protected: // [Collider] diff --git a/Source/Engine/Physics/Colliders/SplineCollider.cpp b/Source/Engine/Physics/Colliders/SplineCollider.cpp index b85617da4..4dc811e19 100644 --- a/Source/Engine/Physics/Colliders/SplineCollider.cpp +++ b/Source/Engine/Physics/Colliders/SplineCollider.cpp @@ -124,26 +124,6 @@ bool SplineCollider::IntersectsItself(const Ray& ray, Real& distance, Vector3& n return _box.Intersects(ray, distance, normal); } -void SplineCollider::Serialize(SerializeStream& stream, const void* otherObj) -{ - // Base - Collider::Serialize(stream, otherObj); - - SERIALIZE_GET_OTHER_OBJ(SplineCollider); - - SERIALIZE(CollisionData); - SERIALIZE_MEMBER(PreTransform, _preTransform) -} - -void SplineCollider::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) -{ - // Base - Collider::Deserialize(stream, modifier); - - DESERIALIZE(CollisionData); - DESERIALIZE_MEMBER(PreTransform, _preTransform); -} - void SplineCollider::OnParentChanged() { if (_spline) diff --git a/Source/Engine/Physics/Colliders/SplineCollider.h b/Source/Engine/Physics/Colliders/SplineCollider.h index bea1059d8..794e90e4e 100644 --- a/Source/Engine/Physics/Colliders/SplineCollider.h +++ b/Source/Engine/Physics/Colliders/SplineCollider.h @@ -15,6 +15,7 @@ class Spline; /// API_CLASS() class FLAXENGINE_API SplineCollider : public Collider { + API_AUTO_SERIALIZATION(); DECLARE_SCENE_OBJECT(SplineCollider); private: Spline* _spline = nullptr; @@ -61,8 +62,6 @@ public: void OnDebugDrawSelected() override; #endif bool IntersectsItself(const Ray& ray, Real& distance, Vector3& normal) override; - void Serialize(SerializeStream& stream, const void* otherObj) override; - void Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) override; void OnParentChanged() override; void EndPlay() override; From a6caa9dbfaedd267c3d927d2dc65a25058b2a2aa Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Sat, 2 Dec 2023 12:03:30 -0600 Subject: [PATCH 43/79] Remove unused includes --- Source/Engine/Physics/Colliders/BoxCollider.cpp | 1 - Source/Engine/Physics/Colliders/CapsuleCollider.cpp | 1 - Source/Engine/Physics/Colliders/CharacterController.cpp | 1 - Source/Engine/Physics/Colliders/Collider.cpp | 1 - Source/Engine/Physics/Colliders/MeshCollider.cpp | 1 - Source/Engine/Physics/Colliders/SphereCollider.cpp | 1 - Source/Engine/Physics/Colliders/SplineCollider.cpp | 1 - 7 files changed, 7 deletions(-) diff --git a/Source/Engine/Physics/Colliders/BoxCollider.cpp b/Source/Engine/Physics/Colliders/BoxCollider.cpp index a0a6b96ba..b80b68de0 100644 --- a/Source/Engine/Physics/Colliders/BoxCollider.cpp +++ b/Source/Engine/Physics/Colliders/BoxCollider.cpp @@ -1,7 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. #include "BoxCollider.h" -#include "Engine/Serialization/Serialization.h" #include "Engine/Physics/PhysicsBackend.h" BoxCollider::BoxCollider(const SpawnParams& params) diff --git a/Source/Engine/Physics/Colliders/CapsuleCollider.cpp b/Source/Engine/Physics/Colliders/CapsuleCollider.cpp index a64a1fb0c..20f28e883 100644 --- a/Source/Engine/Physics/Colliders/CapsuleCollider.cpp +++ b/Source/Engine/Physics/Colliders/CapsuleCollider.cpp @@ -1,7 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. #include "CapsuleCollider.h" -#include "Engine/Serialization/Serialization.h" CapsuleCollider::CapsuleCollider(const SpawnParams& params) : Collider(params) diff --git a/Source/Engine/Physics/Colliders/CharacterController.cpp b/Source/Engine/Physics/Colliders/CharacterController.cpp index 6b20203a0..9b36a92ff 100644 --- a/Source/Engine/Physics/Colliders/CharacterController.cpp +++ b/Source/Engine/Physics/Colliders/CharacterController.cpp @@ -5,7 +5,6 @@ #include "Engine/Physics/Physics.h" #include "Engine/Physics/PhysicsBackend.h" #include "Engine/Physics/PhysicsScene.h" -#include "Engine/Serialization/Serialization.h" #include "Engine/Engine/Time.h" #define CC_MIN_SIZE 0.001f diff --git a/Source/Engine/Physics/Colliders/Collider.cpp b/Source/Engine/Physics/Colliders/Collider.cpp index 4d8fecd7e..4326a033d 100644 --- a/Source/Engine/Physics/Colliders/Collider.cpp +++ b/Source/Engine/Physics/Colliders/Collider.cpp @@ -5,7 +5,6 @@ #if USE_EDITOR #include "Engine/Level/Scene/SceneRendering.h" #endif -#include "Engine/Serialization/Serialization.h" #include "Engine/Physics/PhysicsSettings.h" #include "Engine/Physics/Physics.h" #include "Engine/Physics/PhysicsBackend.h" diff --git a/Source/Engine/Physics/Colliders/MeshCollider.cpp b/Source/Engine/Physics/Colliders/MeshCollider.cpp index 28ce0ff16..c29485a64 100644 --- a/Source/Engine/Physics/Colliders/MeshCollider.cpp +++ b/Source/Engine/Physics/Colliders/MeshCollider.cpp @@ -3,7 +3,6 @@ #include "MeshCollider.h" #include "Engine/Core/Math/Matrix.h" #include "Engine/Core/Math/Ray.h" -#include "Engine/Serialization/Serialization.h" #include "Engine/Physics/Physics.h" #include "Engine/Physics/PhysicsScene.h" #if USE_EDITOR || !BUILD_RELEASE diff --git a/Source/Engine/Physics/Colliders/SphereCollider.cpp b/Source/Engine/Physics/Colliders/SphereCollider.cpp index 1c44811b9..92196eeae 100644 --- a/Source/Engine/Physics/Colliders/SphereCollider.cpp +++ b/Source/Engine/Physics/Colliders/SphereCollider.cpp @@ -1,7 +1,6 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. #include "SphereCollider.h" -#include "Engine/Serialization/Serialization.h" SphereCollider::SphereCollider(const SpawnParams& params) : Collider(params) diff --git a/Source/Engine/Physics/Colliders/SplineCollider.cpp b/Source/Engine/Physics/Colliders/SplineCollider.cpp index 4dc811e19..5c3c87335 100644 --- a/Source/Engine/Physics/Colliders/SplineCollider.cpp +++ b/Source/Engine/Physics/Colliders/SplineCollider.cpp @@ -5,7 +5,6 @@ #include "Engine/Core/Math/Matrix.h" #include "Engine/Core/Math/Ray.h" #include "Engine/Level/Actors/Spline.h" -#include "Engine/Serialization/Serialization.h" #include "Engine/Physics/Physics.h" #include "Engine/Physics/PhysicsBackend.h" #include "Engine/Physics/PhysicsScene.h" From 022a69aaf2c559e19e320e531cdbc570ec9f9d7f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 3 Dec 2023 10:55:40 +0100 Subject: [PATCH 44/79] Continue refactoring model tool to support importing multiple objects properly --- .../Engine/ContentImporters/ImportModel.cpp | 87 ++ .../Engine/Graphics/Models/SkeletonMapping.h | 2 +- Source/Engine/Tools/ModelTool/ModelTool.cpp | 856 ++++++++---------- 3 files changed, 452 insertions(+), 493 deletions(-) diff --git a/Source/Engine/ContentImporters/ImportModel.cpp b/Source/Engine/ContentImporters/ImportModel.cpp index 5aa44ecc4..6cda60948 100644 --- a/Source/Engine/ContentImporters/ImportModel.cpp +++ b/Source/Engine/ContentImporters/ImportModel.cpp @@ -16,6 +16,7 @@ #include "Engine/Content/Assets/Animation.h" #include "Engine/Content/Content.h" #include "Engine/Platform/FileSystem.h" +#include "Engine/Utilities/RectPack.h" #include "AssetsImportingManager.h" bool ImportModel::TryGetImportOptions(const StringView& path, Options& options) @@ -48,6 +49,85 @@ bool ImportModel::TryGetImportOptions(const StringView& path, Options& options) return false; } +void RepackMeshLightmapUVs(ModelData& data) +{ + // Use weight-based coordinates space placement and rect-pack to allocate more space for bigger meshes in the model lightmap chart + int32 lodIndex = 0; + auto& lod = data.LODs[lodIndex]; + + // Build list of meshes with their area + struct LightmapUVsPack : RectPack + { + LightmapUVsPack(float x, float y, float width, float height) + : RectPack(x, y, width, height) + { + } + + void OnInsert() + { + } + }; + struct MeshEntry + { + MeshData* Mesh; + float Area; + float Size; + LightmapUVsPack* Slot; + }; + Array entries; + entries.Resize(lod.Meshes.Count()); + float areaSum = 0; + for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++) + { + auto& entry = entries[meshIndex]; + entry.Mesh = lod.Meshes[meshIndex]; + entry.Area = entry.Mesh->CalculateTrianglesArea(); + entry.Size = Math::Sqrt(entry.Area); + areaSum += entry.Area; + } + + if (areaSum > ZeroTolerance) + { + // Pack all surfaces into atlas + float atlasSize = Math::Sqrt(areaSum) * 1.02f; + int32 triesLeft = 10; + while (triesLeft--) + { + bool failed = false; + const float chartsPadding = (4.0f / 256.0f) * atlasSize; + LightmapUVsPack root(chartsPadding, chartsPadding, atlasSize - chartsPadding, atlasSize - chartsPadding); + for (auto& entry : entries) + { + entry.Slot = root.Insert(entry.Size, entry.Size, chartsPadding); + if (entry.Slot == nullptr) + { + // Failed to insert surface, increase atlas size and try again + atlasSize *= 1.5f; + failed = true; + break; + } + } + + if (!failed) + { + // Transform meshes lightmap UVs into the slots in the whole atlas + const float atlasSizeInv = 1.0f / atlasSize; + for (const auto& entry : entries) + { + Float2 uvOffset(entry.Slot->X * atlasSizeInv, entry.Slot->Y * atlasSizeInv); + Float2 uvScale((entry.Slot->Width - chartsPadding) * atlasSizeInv, (entry.Slot->Height - chartsPadding) * atlasSizeInv); + // TODO: SIMD + for (auto& uv : entry.Mesh->LightmapUVs) + { + uv = uv * uvScale + uvOffset; + } + } + break; + } + } + } +} + void TryRestoreMaterials(CreateAssetContext& context, ModelData& modelData) { // Skip if file is missing @@ -295,6 +375,13 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context) TryRestoreMaterials(context, *data); } + // When using generated lightmap UVs those coordinates needs to be moved so all meshes are in unique locations in [0-1]x[0-1] coordinates space + // Model importer generates UVs in [0-1] space for each mesh so now we need to pack them inside the whole model (only when using multiple meshes) + if (options.Type == ModelTool::ModelType::Model && options.LightmapUVsSource == ModelLightmapUVsSource::Generate && data->LODs.HasItems() && data->LODs[0].Meshes.Count() > 1) + { + RepackMeshLightmapUVs(*data); + } + // Create destination asset type CreateAssetResult result = CreateAssetResult::InvalidTypeID; switch (options.Type) diff --git a/Source/Engine/Graphics/Models/SkeletonMapping.h b/Source/Engine/Graphics/Models/SkeletonMapping.h index e3ec0c793..29fd307fc 100644 --- a/Source/Engine/Graphics/Models/SkeletonMapping.h +++ b/Source/Engine/Graphics/Models/SkeletonMapping.h @@ -36,7 +36,7 @@ public: /// /// The source model skeleton. /// The target skeleton. May be null to disable nodes mapping. - SkeletonMapping(Items& sourceSkeleton, Items* targetSkeleton) + SkeletonMapping(const Items& sourceSkeleton, const Items* targetSkeleton) { Size = sourceSkeleton.Count(); SourceToTarget.Resize(Size); // model => skeleton mapping diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 4d6438522..10e722a83 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -28,7 +28,6 @@ #include "Engine/Core/Utilities.h" #include "Engine/Core/Types/StringView.h" #include "Engine/Platform/FileSystem.h" -#include "Engine/Utilities/RectPack.h" #include "Engine/Tools/TextureTool/TextureTool.h" #include "Engine/ContentImporters/AssetsImportingManager.h" #include "Engine/ContentImporters/CreateMaterial.h" @@ -760,6 +759,27 @@ void TrySetupMaterialParameter(MaterialInstance* instance, Span par } } +String GetAdditionalImportPath(const String& autoImportOutput, Array& importedFileNames, const String& name) +{ + String filename = name; + for (int32 j = filename.Length() - 1; j >= 0; j--) + { + if (EditorUtilities::IsInvalidPathChar(filename[j])) + filename[j] = ' '; + } + if (importedFileNames.Contains(filename)) + { + int32 counter = 1; + do + { + filename = name + TEXT(" ") + StringUtils::ToString(counter); + counter++; + } while (importedFileNames.Contains(filename)); + } + importedFileNames.Add(filename); + return autoImportOutput / filename + ASSET_FILES_EXTENSION_WITH_DOT; +} + bool ModelTool::ImportModel(const String& path, ModelData& data, Options& options, String& errorMsg, const String& autoImportOutput) { LOG(Info, "Importing model from \'{0}\'", path); @@ -792,22 +812,43 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option return true; // Validate result data - switch (options.Type) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry)) { - case ModelType::Model: - { - // Validate - if (data.LODs.IsEmpty() || data.LODs[0].Meshes.IsEmpty()) - { - errorMsg = TEXT("Imported model has no valid geometry."); - return true; - } + LOG(Info, "Imported model has {0} LODs, {1} meshes (in LOD0) and {2} materials", data.LODs.Count(), data.LODs.Count() != 0 ? data.LODs[0].Meshes.Count() : 0, data.Materials.Count()); - LOG(Info, "Imported model has {0} LODs, {1} meshes (in LOD0) and {2} materials", data.LODs.Count(), data.LODs[0].Meshes.Count(), data.Materials.Count()); - break; + // Process blend shapes + for (auto& lod : data.LODs) + { + for (auto& mesh : lod.Meshes) + { + for (int32 blendShapeIndex = mesh->BlendShapes.Count() - 1; blendShapeIndex >= 0; blendShapeIndex--) + { + auto& blendShape = mesh->BlendShapes[blendShapeIndex]; + + // Remove blend shape vertices with empty deltas + for (int32 i = blendShape.Vertices.Count() - 1; i >= 0; i--) + { + auto& v = blendShape.Vertices.Get()[i]; + if (v.PositionDelta.IsZero() && v.NormalDelta.IsZero()) + { + blendShape.Vertices.RemoveAt(i); + } + } + + // Remove empty blend shapes + if (blendShape.Vertices.IsEmpty() || blendShape.Name.IsEmpty()) + { + LOG(Info, "Removing empty blend shape '{0}' from mesh '{1}'", blendShape.Name, mesh->Name); + mesh->BlendShapes.RemoveAt(blendShapeIndex); + } + } + } + } } - case ModelType::SkinnedModel: + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton)) { + LOG(Info, "Imported skeleton has {0} bones and {1} nodes", data.Skeleton.Bones.Count(), data.Nodes.Count()); + // Add single node if imported skeleton is empty if (data.Skeleton.Nodes.IsEmpty()) { @@ -843,448 +884,12 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option } } - // Validate + // Check bones limit currently supported by the engine if (data.Skeleton.Bones.Count() > MAX_BONES_PER_MODEL) { errorMsg = String::Format(TEXT("Imported model skeleton has too many bones. Imported: {0}, maximum supported: {1}. Please optimize your asset."), data.Skeleton.Bones.Count(), MAX_BONES_PER_MODEL); return true; } - if (data.LODs.Count() > 1) - { - LOG(Warning, "Imported skinned model has more than one LOD. Removing the lower LODs. Only single one is supported."); - data.LODs.Resize(1); - } - const int32 meshesCount = data.LODs.Count() != 0 ? data.LODs[0].Meshes.Count() : 0; - for (int32 i = 0; i < meshesCount; i++) - { - const auto mesh = data.LODs[0].Meshes[i]; - if (mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty()) - { - auto indices = Int4::Zero; - auto weights = Float4::UnitX; - - // Check if use a single bone for skinning - auto nodeIndex = data.Skeleton.FindNode(mesh->Name); - auto boneIndex = data.Skeleton.FindBone(nodeIndex); - if (boneIndex == -1 && nodeIndex != -1 && data.Skeleton.Bones.Count() < MAX_BONES_PER_MODEL) - { - // Add missing bone to be used by skinned model from animated nodes pose - boneIndex = data.Skeleton.Bones.Count(); - auto& bone = data.Skeleton.Bones.AddOne(); - bone.ParentIndex = -1; - bone.NodeIndex = nodeIndex; - bone.LocalTransform = CombineTransformsFromNodeIndices(data.Nodes, -1, nodeIndex); - CalculateBoneOffsetMatrix(data.Skeleton.Nodes, bone.OffsetMatrix, bone.NodeIndex); - LOG(Warning, "Using auto-created bone {0} (index {1}) for mesh \'{2}\'", data.Skeleton.Nodes[nodeIndex].Name, boneIndex, mesh->Name); - indices.X = boneIndex; - } - else if (boneIndex != -1) - { - // Fallback to already added bone - LOG(Warning, "Using auto-detected bone {0} (index {1}) for mesh \'{2}\'", data.Skeleton.Nodes[nodeIndex].Name, boneIndex, mesh->Name); - indices.X = boneIndex; - } - else - { - // No bone - LOG(Warning, "Imported mesh \'{0}\' has missing skinning data. It may result in invalid rendering.", mesh->Name); - } - - mesh->BlendIndices.Resize(mesh->Positions.Count()); - mesh->BlendWeights.Resize(mesh->Positions.Count()); - mesh->BlendIndices.SetAll(indices); - mesh->BlendWeights.SetAll(weights); - } -#if BUILD_DEBUG - else - { - auto& indices = mesh->BlendIndices; - for (int32 j = 0; j < indices.Count(); j++) - { - const int32 min = indices[j].MinValue(); - const int32 max = indices[j].MaxValue(); - if (min < 0 || max >= data.Skeleton.Bones.Count()) - { - LOG(Warning, "Imported mesh \'{0}\' has invalid blend indices. It may result in invalid rendering.", mesh->Name); - } - } - - auto& weights = mesh->BlendWeights; - for (int32 j = 0; j < weights.Count(); j++) - { - const float sum = weights[j].SumValues(); - if (Math::Abs(sum - 1.0f) > ZeroTolerance) - { - LOG(Warning, "Imported mesh \'{0}\' has invalid blend weights. It may result in invalid rendering.", mesh->Name); - } - } - } -#endif - } - - LOG(Info, "Imported skeleton has {0} bones, {3} nodes, {1} meshes and {2} material", data.Skeleton.Bones.Count(), meshesCount, data.Materials.Count(), data.Nodes.Count()); - break; - } - case ModelType::Animation: - { - // Validate - if (data.Animations.IsEmpty()) - { - errorMsg = TEXT("Imported file has no valid animations."); - return true; - } - for (auto& animation : data.Animations) - { - LOG(Info, "Imported animation '{}' has {} channels, duration: {} frames, frames per second: {}", animation.Name, animation.Channels.Count(), animation.Duration, animation.FramesPerSecond); - if (animation.Duration <= ZeroTolerance || animation.FramesPerSecond <= ZeroTolerance) - { - errorMsg = TEXT("Invalid animation duration."); - return true; - } - } - break; - } - } - - // Prepare textures - Array importedFileNames; - for (int32 i = 0; i < data.Textures.Count(); i++) - { - auto& texture = data.Textures[i]; - - // Auto-import textures - if (autoImportOutput.IsEmpty() || (options.ImportTypes & ImportDataTypes::Textures) == ImportDataTypes::None || texture.FilePath.IsEmpty()) - continue; - String filename = StringUtils::GetFileNameWithoutExtension(texture.FilePath); - for (int32 j = filename.Length() - 1; j >= 0; j--) - { - if (EditorUtilities::IsInvalidPathChar(filename[j])) - filename[j] = ' '; - } - if (importedFileNames.Contains(filename)) - { - int32 counter = 1; - do - { - filename = String(StringUtils::GetFileNameWithoutExtension(texture.FilePath)) + TEXT(" ") + StringUtils::ToString(counter); - counter++; - } while (importedFileNames.Contains(filename)); - } - importedFileNames.Add(filename); -#if COMPILE_WITH_ASSETS_IMPORTER - auto assetPath = autoImportOutput / filename + ASSET_FILES_EXTENSION_WITH_DOT; - TextureTool::Options textureOptions; - switch (texture.Type) - { - case TextureEntry::TypeHint::ColorRGB: - textureOptions.Type = TextureFormatType::ColorRGB; - break; - case TextureEntry::TypeHint::ColorRGBA: - textureOptions.Type = TextureFormatType::ColorRGBA; - break; - case TextureEntry::TypeHint::Normals: - textureOptions.Type = TextureFormatType::NormalMap; - break; - } - AssetsImportingManager::ImportIfEdited(texture.FilePath, assetPath, texture.AssetID, &textureOptions); -#endif - } - - // Prepare material - for (int32 i = 0; i < data.Materials.Count(); i++) - { - auto& material = data.Materials[i]; - - if (material.Name.IsEmpty()) - material.Name = TEXT("Material ") + StringUtils::ToString(i); - - // Auto-import materials - if (autoImportOutput.IsEmpty() || (options.ImportTypes & ImportDataTypes::Materials) == ImportDataTypes::None || !material.UsesProperties()) - continue; - auto filename = material.Name; - for (int32 j = filename.Length() - 1; j >= 0; j--) - { - if (EditorUtilities::IsInvalidPathChar(filename[j])) - filename[j] = ' '; - } - if (importedFileNames.Contains(filename)) - { - int32 counter = 1; - do - { - filename = material.Name + TEXT(" ") + StringUtils::ToString(counter); - counter++; - } while (importedFileNames.Contains(filename)); - } - importedFileNames.Add(filename); -#if COMPILE_WITH_ASSETS_IMPORTER - auto assetPath = autoImportOutput / filename + ASSET_FILES_EXTENSION_WITH_DOT; - - // When splitting imported meshes allow only the first mesh to import assets (mesh[0] is imported after all following ones so import assets during mesh[1]) - if (!options.SplitObjects && options.ObjectIndex != 1 && options.ObjectIndex != -1) - { - // Find that asset created previously - AssetInfo info; - if (Content::GetAssetInfo(assetPath, info)) - material.AssetID = info.ID; - continue; - } - - if (options.ImportMaterialsAsInstances) - { - // Create material instance - AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialInstanceTag, assetPath, material.AssetID); - if (auto* materialInstance = Content::Load(assetPath)) - { - materialInstance->SetBaseMaterial(options.InstanceToImportAs); - - // Customize base material based on imported material (blind guess based on the common names used in materials) - const Char* diffuseColorNames[] = { TEXT("color"), TEXT("col"), TEXT("diffuse"), TEXT("basecolor"), TEXT("base color") }; - TrySetupMaterialParameter(materialInstance, ToSpan(diffuseColorNames, ARRAY_COUNT(diffuseColorNames)), material.Diffuse.Color, MaterialParameterType::Color); - const Char* emissiveColorNames[] = { TEXT("emissive"), TEXT("emission"), TEXT("light") }; - TrySetupMaterialParameter(materialInstance, ToSpan(emissiveColorNames, ARRAY_COUNT(emissiveColorNames)), material.Emissive.Color, MaterialParameterType::Color); - const Char* opacityValueNames[] = { TEXT("opacity"), TEXT("alpha") }; - TrySetupMaterialParameter(materialInstance, ToSpan(opacityValueNames, ARRAY_COUNT(opacityValueNames)), material.Opacity.Value, MaterialParameterType::Float); - - materialInstance->Save(); - } - else - { - LOG(Error, "Failed to load material instance after creation. ({0})", assetPath); - } - } - else - { - // Create material - CreateMaterial::Options materialOptions; - materialOptions.Diffuse.Color = material.Diffuse.Color; - if (material.Diffuse.TextureIndex != -1) - materialOptions.Diffuse.Texture = data.Textures[material.Diffuse.TextureIndex].AssetID; - materialOptions.Diffuse.HasAlphaMask = material.Diffuse.HasAlphaMask; - materialOptions.Emissive.Color = material.Emissive.Color; - if (material.Emissive.TextureIndex != -1) - materialOptions.Emissive.Texture = data.Textures[material.Emissive.TextureIndex].AssetID; - materialOptions.Opacity.Value = material.Opacity.Value; - if (material.Opacity.TextureIndex != -1) - materialOptions.Opacity.Texture = data.Textures[material.Opacity.TextureIndex].AssetID; - if (material.Normals.TextureIndex != -1) - materialOptions.Normals.Texture = data.Textures[material.Normals.TextureIndex].AssetID; - if (material.TwoSided || material.Diffuse.HasAlphaMask) - materialOptions.Info.CullMode = CullMode::TwoSided; - if (!Math::IsOne(material.Opacity.Value) || material.Opacity.TextureIndex != -1) - materialOptions.Info.BlendMode = MaterialBlendMode::Transparent; - AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialTag, assetPath, material.AssetID, &materialOptions); - } -#endif - } - - // Prepare import transformation - Transform importTransform(options.Translation, options.Rotation, Float3(options.Scale)); - if (options.UseLocalOrigin && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems()) - { - importTransform.Translation -= importTransform.Orientation * data.LODs[0].Meshes[0]->OriginTranslation * importTransform.Scale; - } - if (options.CenterGeometry && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems()) - { - // Calculate the bounding box (use LOD0 as a reference) - BoundingBox box = data.LODs[0].GetBox(); - auto center = data.LODs[0].Meshes[0]->OriginOrientation * importTransform.Orientation * box.GetCenter() * importTransform.Scale * data.LODs[0].Meshes[0]->Scaling; - importTransform.Translation -= center; - } - const bool applyImportTransform = !importTransform.IsIdentity(); - - // Post-process imported data based on a target asset type - if (options.Type == ModelType::Model) - { - if (data.Nodes.IsEmpty()) - { - errorMsg = TEXT("Missing model nodes."); - return true; - } - - // Apply the import transformation - if (applyImportTransform) - { - // Transform the root node using the import transformation - auto& root = data.Nodes[0]; - root.LocalTransform = importTransform.LocalToWorld(root.LocalTransform); - } - - // Perform simple nodes mapping to single node (will transform meshes to model local space) - SkeletonMapping skeletonMapping(data.Nodes, nullptr); - - // Refresh skeleton updater with model skeleton - SkeletonUpdater hierarchyUpdater(data.Nodes); - hierarchyUpdater.UpdateMatrices(); - - // Move meshes in the new nodes - for (int32 lodIndex = 0; lodIndex < data.LODs.Count(); lodIndex++) - { - for (int32 meshIndex = 0; meshIndex < data.LODs[lodIndex].Meshes.Count(); meshIndex++) - { - auto& mesh = *data.LODs[lodIndex].Meshes[meshIndex]; - - // Check if there was a remap using model skeleton - if (skeletonMapping.SourceToSource[mesh.NodeIndex] != mesh.NodeIndex) - { - // Transform vertices - const auto transformationMatrix = hierarchyUpdater.CombineMatricesFromNodeIndices(skeletonMapping.SourceToSource[mesh.NodeIndex], mesh.NodeIndex); - if (!transformationMatrix.IsIdentity()) - mesh.TransformBuffer(transformationMatrix); - } - - // Update new node index using real asset skeleton - mesh.NodeIndex = skeletonMapping.SourceToTarget[mesh.NodeIndex]; - } - } - - // Collision mesh output - if (options.CollisionMeshesPrefix.HasChars()) - { - // Extract collision meshes - ModelData collisionModel; - for (auto& lod : data.LODs) - { - for (int32 i = lod.Meshes.Count() - 1; i >= 0; i--) - { - auto mesh = lod.Meshes[i]; - if (mesh->Name.StartsWith(options.CollisionMeshesPrefix, StringSearchCase::IgnoreCase)) - { - if (collisionModel.LODs.Count() == 0) - collisionModel.LODs.AddOne(); - collisionModel.LODs[0].Meshes.Add(mesh); - lod.Meshes.RemoveAtKeepOrder(i); - if (lod.Meshes.IsEmpty()) - break; - } - } - } - if (collisionModel.LODs.HasItems()) - { -#if COMPILE_WITH_PHYSICS_COOKING - // Create collision - CollisionCooking::Argument arg; - arg.Type = options.CollisionType; - arg.OverrideModelData = &collisionModel; - auto assetPath = autoImportOutput / StringUtils::GetFileNameWithoutExtension(path) + TEXT("Collision") ASSET_FILES_EXTENSION_WITH_DOT; - if (CreateCollisionData::CookMeshCollision(assetPath, arg)) - { - LOG(Error, "Failed to create collision mesh."); - } -#endif - } - } - - // For generated lightmap UVs coordinates needs to be moved so all meshes are in unique locations in [0-1]x[0-1] coordinates space - if (options.LightmapUVsSource == ModelLightmapUVsSource::Generate && data.LODs.HasItems() && data.LODs[0].Meshes.Count() > 1) - { - // Use weight-based coordinates space placement and rect-pack to allocate more space for bigger meshes in the model lightmap chart - int32 lodIndex = 0; - auto& lod = data.LODs[lodIndex]; - - // Build list of meshes with their area - struct LightmapUVsPack : RectPack - { - LightmapUVsPack(float x, float y, float width, float height) - : RectPack(x, y, width, height) - { - } - - void OnInsert() - { - } - }; - struct MeshEntry - { - MeshData* Mesh; - float Area; - float Size; - LightmapUVsPack* Slot; - }; - Array entries; - entries.Resize(lod.Meshes.Count()); - float areaSum = 0; - for (int32 meshIndex = 0; meshIndex < lod.Meshes.Count(); meshIndex++) - { - auto& entry = entries[meshIndex]; - entry.Mesh = lod.Meshes[meshIndex]; - entry.Area = entry.Mesh->CalculateTrianglesArea(); - entry.Size = Math::Sqrt(entry.Area); - areaSum += entry.Area; - } - - if (areaSum > ZeroTolerance) - { - // Pack all surfaces into atlas - float atlasSize = Math::Sqrt(areaSum) * 1.02f; - int32 triesLeft = 10; - while (triesLeft--) - { - bool failed = false; - const float chartsPadding = (4.0f / 256.0f) * atlasSize; - LightmapUVsPack root(chartsPadding, chartsPadding, atlasSize - chartsPadding, atlasSize - chartsPadding); - for (auto& entry : entries) - { - entry.Slot = root.Insert(entry.Size, entry.Size, chartsPadding); - if (entry.Slot == nullptr) - { - // Failed to insert surface, increase atlas size and try again - atlasSize *= 1.5f; - failed = true; - break; - } - } - - if (!failed) - { - // Transform meshes lightmap UVs into the slots in the whole atlas - const float atlasSizeInv = 1.0f / atlasSize; - for (const auto& entry : entries) - { - Float2 uvOffset(entry.Slot->X * atlasSizeInv, entry.Slot->Y * atlasSizeInv); - Float2 uvScale((entry.Slot->Width - chartsPadding) * atlasSizeInv, (entry.Slot->Height - chartsPadding) * atlasSizeInv); - // TODO: SIMD - for (auto& uv : entry.Mesh->LightmapUVs) - { - uv = uv * uvScale + uvOffset; - } - } - break; - } - } - } - } - } - else if (options.Type == ModelType::SkinnedModel) - { - // Process blend shapes - for (auto& lod : data.LODs) - { - for (auto& mesh : lod.Meshes) - { - for (int32 blendShapeIndex = mesh->BlendShapes.Count() - 1; blendShapeIndex >= 0; blendShapeIndex--) - { - auto& blendShape = mesh->BlendShapes[blendShapeIndex]; - - // Remove blend shape vertices with empty deltas - for (int32 i = blendShape.Vertices.Count() - 1; i >= 0; i--) - { - auto& v = blendShape.Vertices.Get()[i]; - if (v.PositionDelta.IsZero() && v.NormalDelta.IsZero()) - { - blendShape.Vertices.RemoveAt(i); - } - } - - // Remove empty blend shapes - if (blendShape.Vertices.IsEmpty() || blendShape.Name.IsEmpty()) - { - LOG(Info, "Removing empty blend shape '{0}' from mesh '{1}'", blendShape.Name, mesh->Name); - mesh->BlendShapes.RemoveAt(blendShapeIndex); - } - } - } - } // Ensure that root node is at index 0 int32 rootIndex = -1; @@ -1372,15 +977,245 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option } } #endif - - // Apply the import transformation - if (applyImportTransform) + } + if (EnumHasAllFlags(options.ImportTypes, ImportDataTypes::Geometry | ImportDataTypes::Skeleton)) + { + // Validate skeleton bones used by the meshes + const int32 meshesCount = data.LODs.Count() != 0 ? data.LODs[0].Meshes.Count() : 0; + for (int32 i = 0; i < meshesCount; i++) { - // Transform the root node using the import transformation - auto& root = data.Skeleton.RootNode(); - Transform meshTransform = root.LocalTransform.WorldToLocal(importTransform).LocalToWorld(root.LocalTransform); - root.LocalTransform = importTransform.LocalToWorld(root.LocalTransform); + const auto mesh = data.LODs[0].Meshes[i]; + if (mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty()) + { + auto indices = Int4::Zero; + auto weights = Float4::UnitX; + // Check if use a single bone for skinning + auto nodeIndex = data.Skeleton.FindNode(mesh->Name); + auto boneIndex = data.Skeleton.FindBone(nodeIndex); + if (boneIndex == -1 && nodeIndex != -1 && data.Skeleton.Bones.Count() < MAX_BONES_PER_MODEL) + { + // Add missing bone to be used by skinned model from animated nodes pose + boneIndex = data.Skeleton.Bones.Count(); + auto& bone = data.Skeleton.Bones.AddOne(); + bone.ParentIndex = -1; + bone.NodeIndex = nodeIndex; + bone.LocalTransform = CombineTransformsFromNodeIndices(data.Nodes, -1, nodeIndex); + CalculateBoneOffsetMatrix(data.Skeleton.Nodes, bone.OffsetMatrix, bone.NodeIndex); + LOG(Warning, "Using auto-created bone {0} (index {1}) for mesh \'{2}\'", data.Skeleton.Nodes[nodeIndex].Name, boneIndex, mesh->Name); + indices.X = boneIndex; + } + else if (boneIndex != -1) + { + // Fallback to already added bone + LOG(Warning, "Using auto-detected bone {0} (index {1}) for mesh \'{2}\'", data.Skeleton.Nodes[nodeIndex].Name, boneIndex, mesh->Name); + indices.X = boneIndex; + } + else + { + // No bone + LOG(Warning, "Imported mesh \'{0}\' has missing skinning data. It may result in invalid rendering.", mesh->Name); + } + + mesh->BlendIndices.Resize(mesh->Positions.Count()); + mesh->BlendWeights.Resize(mesh->Positions.Count()); + mesh->BlendIndices.SetAll(indices); + mesh->BlendWeights.SetAll(weights); + } +#if BUILD_DEBUG + else + { + auto& indices = mesh->BlendIndices; + for (int32 j = 0; j < indices.Count(); j++) + { + const int32 min = indices[j].MinValue(); + const int32 max = indices[j].MaxValue(); + if (min < 0 || max >= data.Skeleton.Bones.Count()) + { + LOG(Warning, "Imported mesh \'{0}\' has invalid blend indices. It may result in invalid rendering.", mesh->Name); + } + } + + auto& weights = mesh->BlendWeights; + for (int32 j = 0; j < weights.Count(); j++) + { + const float sum = weights[j].SumValues(); + if (Math::Abs(sum - 1.0f) > ZeroTolerance) + { + LOG(Warning, "Imported mesh \'{0}\' has invalid blend weights. It may result in invalid rendering.", mesh->Name); + } + } + } +#endif + } + } + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations)) + { + for (auto& animation : data.Animations) + { + LOG(Info, "Imported animation '{}' has {} channels, duration: {} frames, frames per second: {}", animation.Name, animation.Channels.Count(), animation.Duration, animation.FramesPerSecond); + if (animation.Duration <= ZeroTolerance || animation.FramesPerSecond <= ZeroTolerance) + { + errorMsg = TEXT("Invalid animation duration."); + return true; + } + } + } + switch (options.Type) + { + case ModelType::Model: + if (data.LODs.IsEmpty() || data.LODs[0].Meshes.IsEmpty()) + { + errorMsg = TEXT("Imported model has no valid geometry."); + return true; + } + if (data.Nodes.IsEmpty()) + { + errorMsg = TEXT("Missing model nodes."); + return true; + } + break; + case ModelType::SkinnedModel: + if (data.LODs.Count() > 1) + { + LOG(Warning, "Imported skinned model has more than one LOD. Removing the lower LODs. Only single one is supported."); + data.LODs.Resize(1); + } + break; + case ModelType::Animation: + if (data.Animations.IsEmpty()) + { + errorMsg = TEXT("Imported file has no valid animations."); + return true; + } + break; + } + + // Keep additionally imported files well organized + Array importedFileNames; + + // Prepare textures + for (int32 i = 0; i < data.Textures.Count(); i++) + { + auto& texture = data.Textures[i]; + + // Auto-import textures + if (autoImportOutput.IsEmpty() || EnumHasNoneFlags(options.ImportTypes, ImportDataTypes::Textures) || texture.FilePath.IsEmpty()) + continue; + String assetPath = GetAdditionalImportPath(autoImportOutput, importedFileNames, StringUtils::GetFileNameWithoutExtension(texture.FilePath)); +#if COMPILE_WITH_ASSETS_IMPORTER + TextureTool::Options textureOptions; + switch (texture.Type) + { + case TextureEntry::TypeHint::ColorRGB: + textureOptions.Type = TextureFormatType::ColorRGB; + break; + case TextureEntry::TypeHint::ColorRGBA: + textureOptions.Type = TextureFormatType::ColorRGBA; + break; + case TextureEntry::TypeHint::Normals: + textureOptions.Type = TextureFormatType::NormalMap; + break; + } + AssetsImportingManager::ImportIfEdited(texture.FilePath, assetPath, texture.AssetID, &textureOptions); +#endif + } + + // Prepare materials + for (int32 i = 0; i < data.Materials.Count(); i++) + { + auto& material = data.Materials[i]; + + if (material.Name.IsEmpty()) + material.Name = TEXT("Material ") + StringUtils::ToString(i); + + // Auto-import materials + if (autoImportOutput.IsEmpty() || EnumHasNoneFlags(options.ImportTypes, ImportDataTypes::Materials) || !material.UsesProperties()) + continue; + String assetPath = GetAdditionalImportPath(autoImportOutput, importedFileNames, material.Name); +#if COMPILE_WITH_ASSETS_IMPORTER + // When splitting imported meshes allow only the first mesh to import assets (mesh[0] is imported after all following ones so import assets during mesh[1]) + if (!options.SplitObjects && options.ObjectIndex != 1 && options.ObjectIndex != -1) + { + // Find that asset created previously + AssetInfo info; + if (Content::GetAssetInfo(assetPath, info)) + material.AssetID = info.ID; + continue; + } + + if (options.ImportMaterialsAsInstances) + { + // Create material instance + AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialInstanceTag, assetPath, material.AssetID); + if (auto* materialInstance = Content::Load(assetPath)) + { + materialInstance->SetBaseMaterial(options.InstanceToImportAs); + + // Customize base material based on imported material (blind guess based on the common names used in materials) + const Char* diffuseColorNames[] = { TEXT("color"), TEXT("col"), TEXT("diffuse"), TEXT("basecolor"), TEXT("base color") }; + TrySetupMaterialParameter(materialInstance, ToSpan(diffuseColorNames, ARRAY_COUNT(diffuseColorNames)), material.Diffuse.Color, MaterialParameterType::Color); + const Char* emissiveColorNames[] = { TEXT("emissive"), TEXT("emission"), TEXT("light") }; + TrySetupMaterialParameter(materialInstance, ToSpan(emissiveColorNames, ARRAY_COUNT(emissiveColorNames)), material.Emissive.Color, MaterialParameterType::Color); + const Char* opacityValueNames[] = { TEXT("opacity"), TEXT("alpha") }; + TrySetupMaterialParameter(materialInstance, ToSpan(opacityValueNames, ARRAY_COUNT(opacityValueNames)), material.Opacity.Value, MaterialParameterType::Float); + + materialInstance->Save(); + } + else + { + LOG(Error, "Failed to load material instance after creation. ({0})", assetPath); + } + } + else + { + // Create material + CreateMaterial::Options materialOptions; + materialOptions.Diffuse.Color = material.Diffuse.Color; + if (material.Diffuse.TextureIndex != -1) + materialOptions.Diffuse.Texture = data.Textures[material.Diffuse.TextureIndex].AssetID; + materialOptions.Diffuse.HasAlphaMask = material.Diffuse.HasAlphaMask; + materialOptions.Emissive.Color = material.Emissive.Color; + if (material.Emissive.TextureIndex != -1) + materialOptions.Emissive.Texture = data.Textures[material.Emissive.TextureIndex].AssetID; + materialOptions.Opacity.Value = material.Opacity.Value; + if (material.Opacity.TextureIndex != -1) + materialOptions.Opacity.Texture = data.Textures[material.Opacity.TextureIndex].AssetID; + if (material.Normals.TextureIndex != -1) + materialOptions.Normals.Texture = data.Textures[material.Normals.TextureIndex].AssetID; + if (material.TwoSided || material.Diffuse.HasAlphaMask) + materialOptions.Info.CullMode = CullMode::TwoSided; + if (!Math::IsOne(material.Opacity.Value) || material.Opacity.TextureIndex != -1) + materialOptions.Info.BlendMode = MaterialBlendMode::Transparent; + AssetsImportingManager::Create(AssetsImportingManager::CreateMaterialTag, assetPath, material.AssetID, &materialOptions); + } +#endif + } + + // Prepare import transformation + Transform importTransform(options.Translation, options.Rotation, Float3(options.Scale)); + if (options.UseLocalOrigin && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems()) + { + importTransform.Translation -= importTransform.Orientation * data.LODs[0].Meshes[0]->OriginTranslation * importTransform.Scale; + } + if (options.CenterGeometry && data.LODs.HasItems() && data.LODs[0].Meshes.HasItems()) + { + // Calculate the bounding box (use LOD0 as a reference) + BoundingBox box = data.LODs[0].GetBox(); + auto center = data.LODs[0].Meshes[0]->OriginOrientation * importTransform.Orientation * box.GetCenter() * importTransform.Scale * data.LODs[0].Meshes[0]->Scaling; + importTransform.Translation -= center; + } + + // Apply the import transformation + if (!importTransform.IsIdentity()) + { + // Transform the root node using the import transformation + auto& root = data.Skeleton.RootNode(); + Transform meshTransform = root.LocalTransform.WorldToLocal(importTransform).LocalToWorld(root.LocalTransform); + root.LocalTransform = importTransform.LocalToWorld(root.LocalTransform); + + if (options.Type == ModelType::SkinnedModel) + { // Apply import transform on meshes Matrix meshTransformMatrix; meshTransform.GetWorld(meshTransformMatrix); @@ -1400,20 +1235,15 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option for (SkeletonBone& bone : data.Skeleton.Bones) { if (bone.ParentIndex == -1) - { bone.LocalTransform = importTransform.LocalToWorld(bone.LocalTransform); - } bone.OffsetMatrix = importMatrixInv * bone.OffsetMatrix; } } + } - // Perform simple nodes mapping to single node (will transform meshes to model local space) - SkeletonMapping skeletonMapping(data.Nodes, nullptr); - - // Refresh skeleton updater with model skeleton - SkeletonUpdater hierarchyUpdater(data.Nodes); - hierarchyUpdater.UpdateMatrices(); - + // Post-process imported data + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton)) + { if (options.CalculateBoneOffsetMatrices) { // Calculate offset matrix (inverse bind pose transform) for every bone manually @@ -1423,27 +1253,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option } } - // Move meshes in the new nodes - for (int32 lodIndex = 0; lodIndex < data.LODs.Count(); lodIndex++) - { - for (int32 meshIndex = 0; meshIndex < data.LODs[lodIndex].Meshes.Count(); meshIndex++) - { - auto& mesh = *data.LODs[lodIndex].Meshes[meshIndex]; - - // Check if there was a remap using model skeleton - if (skeletonMapping.SourceToSource[mesh.NodeIndex] != mesh.NodeIndex) - { - // Transform vertices - const auto transformationMatrix = hierarchyUpdater.CombineMatricesFromNodeIndices(skeletonMapping.SourceToSource[mesh.NodeIndex], mesh.NodeIndex); - if (!transformationMatrix.IsIdentity()) - mesh.TransformBuffer(transformationMatrix); - } - - // Update new node index using real asset skeleton - mesh.NodeIndex = skeletonMapping.SourceToTarget[mesh.NodeIndex]; - } - } - #if USE_SKELETON_NODES_SORTING // Sort skeleton nodes and bones hierarchy (parents first) // Then it can be used with a simple linear loop update @@ -1467,7 +1276,37 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option ! #endif } - else if (options.Type == ModelType::Animation) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry)) + { + // Perform simple nodes mapping to single node (will transform meshes to model local space) + SkeletonMapping skeletonMapping(data.Nodes, nullptr); + + // Refresh skeleton updater with model skeleton + SkeletonUpdater hierarchyUpdater(data.Nodes); + hierarchyUpdater.UpdateMatrices(); + + // Move meshes in the new nodes + for (int32 lodIndex = 0; lodIndex < data.LODs.Count(); lodIndex++) + { + for (int32 meshIndex = 0; meshIndex < data.LODs[lodIndex].Meshes.Count(); meshIndex++) + { + auto& mesh = *data.LODs[lodIndex].Meshes[meshIndex]; + + // Check if there was a remap using model skeleton + if (skeletonMapping.SourceToSource[mesh.NodeIndex] != mesh.NodeIndex) + { + // Transform vertices + const auto transformationMatrix = hierarchyUpdater.CombineMatricesFromNodeIndices(skeletonMapping.SourceToSource[mesh.NodeIndex], mesh.NodeIndex); + if (!transformationMatrix.IsIdentity()) + mesh.TransformBuffer(transformationMatrix); + } + + // Update new node index using real asset skeleton + mesh.NodeIndex = skeletonMapping.SourceToTarget[mesh.NodeIndex]; + } + } + } + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations)) { for (auto& animation : data.Animations) { @@ -1532,11 +1371,47 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option } } + // Collision mesh output + if (options.CollisionMeshesPrefix.HasChars()) + { + // Extract collision meshes from the model + ModelData collisionModel; + for (auto& lod : data.LODs) + { + for (int32 i = lod.Meshes.Count() - 1; i >= 0; i--) + { + auto mesh = lod.Meshes[i]; + if (mesh->Name.StartsWith(options.CollisionMeshesPrefix, StringSearchCase::IgnoreCase)) + { + if (collisionModel.LODs.Count() == 0) + collisionModel.LODs.AddOne(); + collisionModel.LODs[0].Meshes.Add(mesh); + lod.Meshes.RemoveAtKeepOrder(i); + if (lod.Meshes.IsEmpty()) + break; + } + } + } +#if COMPILE_WITH_PHYSICS_COOKING + if (collisionModel.LODs.HasItems() && options.CollisionType != CollisionDataType::None) + { + // Cook collision + String assetPath = GetAdditionalImportPath(autoImportOutput, importedFileNames, TEXT("Collision")); + CollisionCooking::Argument arg; + arg.Type = options.CollisionType; + arg.OverrideModelData = &collisionModel; + if (CreateCollisionData::CookMeshCollision(assetPath, arg)) + { + LOG(Error, "Failed to create collision mesh."); + } + } +#endif + } + // Merge meshes with the same parent nodes, material and skinning if (options.MergeMeshes) { int32 meshesMerged = 0; - for (int32 lodIndex = 0; lodIndex < data.LODs.Count(); lodIndex++) { auto& meshes = data.LODs[lodIndex].Meshes; @@ -1568,11 +1443,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option } } } - if (meshesMerged) - { LOG(Info, "Merged {0} meshes", meshesMerged); - } } // Automatic LOD generation From 1843689a885affadc75d5b2762211eb06632409f Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 3 Dec 2023 11:23:45 +0100 Subject: [PATCH 45/79] Add various profiler events to analyze models importing workflow --- Source/Engine/Content/Storage/FlaxStorage.cpp | 3 +++ .../AssetsImportingManager.cpp | 4 +++- Source/Engine/ContentImporters/ImportModel.cpp | 4 ++++ .../Engine/Graphics/Models/ModelData.Tool.cpp | 7 +++++++ Source/Engine/Physics/CollisionCooking.cpp | 2 ++ .../Physics/PhysX/PhysicsBackendPhysX.cpp | 3 +++ .../Tools/ModelTool/ModelTool.OpenFBX.cpp | 18 +++++++++++++----- Source/Engine/Tools/ModelTool/ModelTool.cpp | 3 +++ 8 files changed, 38 insertions(+), 6 deletions(-) diff --git a/Source/Engine/Content/Storage/FlaxStorage.cpp b/Source/Engine/Content/Storage/FlaxStorage.cpp index a47e0bd0e..77e91f0c4 100644 --- a/Source/Engine/Content/Storage/FlaxStorage.cpp +++ b/Source/Engine/Content/Storage/FlaxStorage.cpp @@ -562,6 +562,7 @@ bool FlaxStorage::Reload() { if (!IsLoaded()) return false; + PROFILE_CPU(); OnReloading(this); @@ -776,6 +777,8 @@ FlaxChunk* FlaxStorage::AllocateChunk() bool FlaxStorage::Create(const StringView& path, const AssetInitData* data, int32 dataCount, bool silentMode, const CustomData* customData) { + PROFILE_CPU(); + ZoneText(*path, path.Length()); LOG(Info, "Creating package at \'{0}\'. Silent Mode: {1}", path, silentMode); // Prepare to have access to the file diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.cpp b/Source/Engine/ContentImporters/AssetsImportingManager.cpp index 91c391711..76b9c211d 100644 --- a/Source/Engine/ContentImporters/AssetsImportingManager.cpp +++ b/Source/Engine/ContentImporters/AssetsImportingManager.cpp @@ -306,6 +306,8 @@ bool AssetsImportingManager::ImportIfEdited(const StringView& inputPath, const S bool AssetsImportingManager::Create(const Function& callback, const StringView& inputPath, const StringView& outputPath, Guid& assetId, void* arg) { + PROFILE_CPU(); + ZoneText(*outputPath, outputPath.Length()); const auto startTime = Platform::GetTimeSeconds(); // Pick ID if not specified @@ -369,7 +371,7 @@ bool AssetsImportingManager::Create(const FunctionRegisterAsset(context.Data.Header, outputPath); + Content::GetRegistry()->RegisterAsset(context.Data.Header, context.TargetAssetPath); // Done const auto endTime = Platform::GetTimeSeconds(); diff --git a/Source/Engine/ContentImporters/ImportModel.cpp b/Source/Engine/ContentImporters/ImportModel.cpp index 6cda60948..4a2298740 100644 --- a/Source/Engine/ContentImporters/ImportModel.cpp +++ b/Source/Engine/ContentImporters/ImportModel.cpp @@ -17,6 +17,7 @@ #include "Engine/Content/Content.h" #include "Engine/Platform/FileSystem.h" #include "Engine/Utilities/RectPack.h" +#include "Engine/Profiler/ProfilerCPU.h" #include "AssetsImportingManager.h" bool ImportModel::TryGetImportOptions(const StringView& path, Options& options) @@ -437,6 +438,7 @@ CreateAssetResult ImportModel::Create(CreateAssetContext& context) CreateAssetResult ImportModel::CreateModel(CreateAssetContext& context, ModelData& modelData, const Options* options) { + PROFILE_CPU(); IMPORT_SETUP(Model, Model::SerializedVersion); // Save model header @@ -487,6 +489,7 @@ CreateAssetResult ImportModel::CreateModel(CreateAssetContext& context, ModelDat CreateAssetResult ImportModel::CreateSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options) { + PROFILE_CPU(); IMPORT_SETUP(SkinnedModel, SkinnedModel::SerializedVersion); // Save skinned model header @@ -528,6 +531,7 @@ CreateAssetResult ImportModel::CreateSkinnedModel(CreateAssetContext& context, M CreateAssetResult ImportModel::CreateAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options) { + PROFILE_CPU(); IMPORT_SETUP(Animation, Animation::SerializedVersion); // Save animation data diff --git a/Source/Engine/Graphics/Models/ModelData.Tool.cpp b/Source/Engine/Graphics/Models/ModelData.Tool.cpp index 727fe7754..91ee2bcd7 100644 --- a/Source/Engine/Graphics/Models/ModelData.Tool.cpp +++ b/Source/Engine/Graphics/Models/ModelData.Tool.cpp @@ -10,6 +10,7 @@ #include "Engine/Core/Collections/BitArray.h" #include "Engine/Tools/ModelTool/ModelTool.h" #include "Engine/Tools/ModelTool/VertexTriangleAdjacency.h" +#include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Platform/Platform.h" #define USE_MIKKTSPACE 1 #include "ThirdParty/MikkTSpace/mikktspace.h" @@ -78,6 +79,7 @@ void RemapArrayHelper(Array& target, const std::vector& remap) bool MeshData::GenerateLightmapUVs() { + PROFILE_CPU(); #if PLATFORM_WINDOWS // Prepare HRESULT hr; @@ -235,6 +237,7 @@ void RemapBuffer(Array& src, Array& dst, const Array& mapping, int3 void MeshData::BuildIndexBuffer() { + PROFILE_CPU(); const auto startTime = Platform::GetTimeSeconds(); const int32 vertexCount = Positions.Count(); @@ -341,6 +344,7 @@ bool MeshData::GenerateNormals(float smoothingAngle) LOG(Warning, "Missing vertex or index data to generate normals."); return true; } + PROFILE_CPU(); const auto startTime = Platform::GetTimeSeconds(); @@ -520,6 +524,7 @@ bool MeshData::GenerateTangents(float smoothingAngle) LOG(Warning, "Missing normals or texcoors data to generate tangents."); return true; } + PROFILE_CPU(); const auto startTime = Platform::GetTimeSeconds(); const int32 vertexCount = Positions.Count(); @@ -706,6 +711,7 @@ void MeshData::ImproveCacheLocality() if (Positions.IsEmpty() || Indices.IsEmpty() || Positions.Count() <= VertexCacheSize) return; + PROFILE_CPU(); const auto startTime = Platform::GetTimeSeconds(); @@ -886,6 +892,7 @@ void MeshData::ImproveCacheLocality() float MeshData::CalculateTrianglesArea() const { + PROFILE_CPU(); float sum = 0; // TODO: use SIMD for (int32 i = 0; i + 2 < Indices.Count(); i += 3) diff --git a/Source/Engine/Physics/CollisionCooking.cpp b/Source/Engine/Physics/CollisionCooking.cpp index 4c8cffdcc..805364e00 100644 --- a/Source/Engine/Physics/CollisionCooking.cpp +++ b/Source/Engine/Physics/CollisionCooking.cpp @@ -7,10 +7,12 @@ #include "Engine/Graphics/Async/GPUTask.h" #include "Engine/Graphics/Models/MeshBase.h" #include "Engine/Threading/Threading.h" +#include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Core/Log.h" bool CollisionCooking::CookCollision(const Argument& arg, CollisionData::SerializedOptions& outputOptions, BytesContainer& outputData) { + PROFILE_CPU(); int32 convexVertexLimit = Math::Clamp(arg.ConvexVertexLimit, CONVEX_VERTEX_MIN, CONVEX_VERTEX_MAX); if (arg.ConvexVertexLimit == 0) convexVertexLimit = CONVEX_VERTEX_MAX; diff --git a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp index 768bff1fa..3d278f97c 100644 --- a/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp +++ b/Source/Engine/Physics/PhysX/PhysicsBackendPhysX.cpp @@ -961,6 +961,7 @@ void PhysicalMaterial::UpdatePhysicsMaterial() bool CollisionCooking::CookConvexMesh(CookingInput& input, BytesContainer& output) { + PROFILE_CPU(); ENSURE_CAN_COOK; if (input.VertexCount == 0) LOG(Warning, "Empty mesh data for collision cooking."); @@ -1004,6 +1005,7 @@ bool CollisionCooking::CookConvexMesh(CookingInput& input, BytesContainer& outpu bool CollisionCooking::CookTriangleMesh(CookingInput& input, BytesContainer& output) { + PROFILE_CPU(); ENSURE_CAN_COOK; if (input.VertexCount == 0 || input.IndexCount == 0) LOG(Warning, "Empty mesh data for collision cooking."); @@ -1038,6 +1040,7 @@ bool CollisionCooking::CookTriangleMesh(CookingInput& input, BytesContainer& out bool CollisionCooking::CookHeightField(int32 cols, int32 rows, const PhysicsBackend::HeightFieldSample* data, WriteStream& stream) { + PROFILE_CPU(); ENSURE_CAN_COOK; PxHeightFieldDesc heightFieldDesc; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp index ce6d1a0e6..5c72d963f 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp @@ -9,6 +9,7 @@ #include "Engine/Core/Collections/Sorting.h" #include "Engine/Platform/FileSystem.h" #include "Engine/Tools/TextureTool/TextureTool.h" +#include "Engine/Profiler/ProfilerCPU.h" #include "Engine/Platform/File.h" #define OPEN_FBX_CONVERT_SPACE 1 @@ -425,7 +426,7 @@ Matrix GetOffsetMatrix(OpenFbxImporterData& data, const ofbx::Mesh* mesh, const } } } - //return Matrix::Identity; + //return Matrix::Identity; return ToMatrix(node->getGlobalTransform()); #else Matrix t = Matrix::Identity; @@ -523,7 +524,9 @@ bool ImportBones(OpenFbxImporterData& data, String& errorMsg) bool ProcessMesh(ModelData& result, OpenFbxImporterData& data, const ofbx::Mesh* aMesh, MeshData& mesh, String& errorMsg, int32 triangleStart, int32 triangleEnd) { - // Prepare + PROFILE_CPU(); + mesh.Name = aMesh->name; + ZoneText(*mesh.Name, mesh.Name.Length()); const int32 firstVertexOffset = triangleStart * 3; const int32 lastVertexOffset = triangleEnd * 3; const ofbx::Geometry* aGeometry = aMesh->getGeometry(); @@ -538,7 +541,6 @@ bool ProcessMesh(ModelData& result, OpenFbxImporterData& data, const ofbx::Mesh* const ofbx::BlendShape* blendShape = aGeometry->getBlendShape(); // Properties - mesh.Name = aMesh->name; const ofbx::Material* aMaterial = nullptr; if (aMesh->getMaterialCount() > 0) { @@ -842,7 +844,7 @@ bool ProcessMesh(ModelData& result, OpenFbxImporterData& data, const ofbx::Mesh* mesh.OriginTranslation = scale * Vector3(translation.X, translation.Y, -translation.Z); else mesh.OriginTranslation = scale * Vector3(translation.X, translation.Y, translation.Z); - + auto rot = aMesh->getLocalRotation(); auto quat = Quaternion::Euler(-(float)rot.x, -(float)rot.y, -(float)rot.z); mesh.OriginOrientation = quat; @@ -854,6 +856,8 @@ bool ProcessMesh(ModelData& result, OpenFbxImporterData& data, const ofbx::Mesh* bool ImportMesh(ModelData& result, OpenFbxImporterData& data, const ofbx::Mesh* aMesh, String& errorMsg, int32 triangleStart, int32 triangleEnd) { + PROFILE_CPU(); + // Find the parent node int32 nodeIndex = data.FindNode(aMesh); @@ -1128,7 +1132,11 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ModelData& data, Options& op { loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_GEOMETRY | (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES; } - ofbx::IScene* scene = ofbx::load(fileData.Get(), fileData.Count(), loadFlags); + ofbx::IScene* scene; + { + PROFILE_CPU_NAMED("ofbx::load"); + scene = ofbx::load(fileData.Get(), fileData.Count(), loadFlags); + } if (!scene) { errorMsg = ofbx::getError(); diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 10e722a83..b71584150 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -444,6 +444,8 @@ void RemoveNamespace(String& name) bool ModelTool::ImportData(const String& path, ModelData& data, Options& options, String& errorMsg) { + PROFILE_CPU(); + // Validate options options.Scale = Math::Clamp(options.Scale, 0.0001f, 100000.0f); options.SmoothingNormalsAngle = Math::Clamp(options.SmoothingNormalsAngle, 0.0f, 175.0f); @@ -782,6 +784,7 @@ String GetAdditionalImportPath(const String& autoImportOutput, Array& im bool ModelTool::ImportModel(const String& path, ModelData& data, Options& options, String& errorMsg, const String& autoImportOutput) { + PROFILE_CPU(); LOG(Info, "Importing model from \'{0}\'", path); const auto startTime = DateTime::NowUTC(); From d6dc1f99985081d46130d0600736964c5624f988 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 3 Dec 2023 14:09:23 +0100 Subject: [PATCH 46/79] Various minor tweaks --- Source/Engine/ContentImporters/CreateJson.cpp | 2 +- Source/Engine/Level/SceneInfo.cpp | 23 ------------------- Source/Engine/Threading/MainThreadTask.h | 4 ++-- Source/Engine/Threading/Task.cpp | 10 ++++---- Source/Engine/Threading/Task.h | 8 +++---- Source/Engine/Threading/ThreadPoolTask.h | 4 ++-- 6 files changed, 14 insertions(+), 37 deletions(-) diff --git a/Source/Engine/ContentImporters/CreateJson.cpp b/Source/Engine/ContentImporters/CreateJson.cpp index 3ca21601c..96a10ab27 100644 --- a/Source/Engine/ContentImporters/CreateJson.cpp +++ b/Source/Engine/ContentImporters/CreateJson.cpp @@ -53,7 +53,7 @@ bool CreateJson::Create(const StringView& path, const StringAnsiView& data, cons { if (FileSystem::CreateDirectory(directory)) { - LOG(Warning, "Failed to create directory"); + LOG(Warning, "Failed to create directory '{}'", directory); return true; } } diff --git a/Source/Engine/Level/SceneInfo.cpp b/Source/Engine/Level/SceneInfo.cpp index 65e4aa462..9c504bad7 100644 --- a/Source/Engine/Level/SceneInfo.cpp +++ b/Source/Engine/Level/SceneInfo.cpp @@ -11,29 +11,6 @@ String SceneInfo::ToString() const return TEXT("SceneInfo"); } -const int32 lightmapAtlasSizes[] = -{ - 32, - 64, - 128, - 256, - 512, - 1024, - 2048, - 4096 -}; -DECLARE_ENUM_8(LightmapAtlasSize, _32, _64, _128, _256, _512, _1024, _2048, _4096); - -LightmapAtlasSize getLightmapAtlasSize(int32 size) -{ - for (int32 i = 0; i < LightmapAtlasSize_Count; i++) - { - if (lightmapAtlasSizes[i] == size) - return (LightmapAtlasSize)i; - } - return LightmapAtlasSize::_1024; -} - void SceneInfo::Serialize(SerializeStream& stream, const void* otherObj) { SERIALIZE_GET_OTHER_OBJ(SceneInfo); diff --git a/Source/Engine/Threading/MainThreadTask.h b/Source/Engine/Threading/MainThreadTask.h index 36e66bfb0..fc5b9fe2d 100644 --- a/Source/Engine/Threading/MainThreadTask.h +++ b/Source/Engine/Threading/MainThreadTask.h @@ -66,7 +66,7 @@ public: /// /// The action. /// The target object. - MainThreadActionTask(Function& action, Object* target = nullptr) + MainThreadActionTask(const Function& action, Object* target = nullptr) : MainThreadTask() , _action1(action) , _target(target) @@ -90,7 +90,7 @@ public: /// /// The action. /// The target object. - MainThreadActionTask(Function& action, Object* target = nullptr) + MainThreadActionTask(const Function& action, Object* target = nullptr) : MainThreadTask() , _action2(action) , _target(target) diff --git a/Source/Engine/Threading/Task.cpp b/Source/Engine/Threading/Task.cpp index a516c31c7..84737a71b 100644 --- a/Source/Engine/Threading/Task.cpp +++ b/Source/Engine/Threading/Task.cpp @@ -96,13 +96,13 @@ Task* Task::ContinueWith(const Action& action, Object* target) return result; } -Task* Task::ContinueWith(Function action, Object* target) +Task* Task::ContinueWith(const Function& action, Object* target) { ASSERT(action.IsBinded()); return ContinueWith(New(action, target)); } -Task* Task::ContinueWith(Function action, Object* target) +Task* Task::ContinueWith(const Function& action, Object* target) { ASSERT(action.IsBinded()); return ContinueWith(New(action, target)); @@ -116,17 +116,17 @@ Task* Task::StartNew(Task* task) return task; } -Task* Task::StartNew(Function& action, Object* target) +Task* Task::StartNew(const Function& action, Object* target) { return StartNew(New(action, target)); } -Task* Task::StartNew(Function::Signature action, Object* target) +Task* Task::StartNew(const Function::Signature action, Object* target) { return StartNew(New(action, target)); } -Task* Task::StartNew(Function& action, Object* target) +Task* Task::StartNew(const Function& action, Object* target) { return StartNew(New(action, target)); } diff --git a/Source/Engine/Threading/Task.h b/Source/Engine/Threading/Task.h index 4a17de727..12b131d1b 100644 --- a/Source/Engine/Threading/Task.h +++ b/Source/Engine/Threading/Task.h @@ -221,7 +221,7 @@ public: /// Action to run. /// The action target object. /// Enqueued task. - Task* ContinueWith(Function action, Object* target = nullptr); + Task* ContinueWith(const Function& action, Object* target = nullptr); /// /// Continues that task execution with a given action (will spawn new async action). @@ -229,7 +229,7 @@ public: /// Action to run. /// The action target object. /// Enqueued task. - Task* ContinueWith(Function action, Object* target = nullptr); + Task* ContinueWith(const Function& action, Object* target = nullptr); public: @@ -246,7 +246,7 @@ public: /// The action. /// The action target object. /// Task - static Task* StartNew(Function& action, Object* target = nullptr); + static Task* StartNew(const Function& action, Object* target = nullptr); /// /// Starts the new task. @@ -275,7 +275,7 @@ public: /// The action. /// The action target object. /// Task - static Task* StartNew(Function& action, Object* target = nullptr); + static Task* StartNew(const Function& action, Object* target = nullptr); /// /// Starts the new task. diff --git a/Source/Engine/Threading/ThreadPoolTask.h b/Source/Engine/Threading/ThreadPoolTask.h index 9e1cc6fac..6378f3ee4 100644 --- a/Source/Engine/Threading/ThreadPoolTask.h +++ b/Source/Engine/Threading/ThreadPoolTask.h @@ -55,7 +55,7 @@ public: /// /// The action. /// The target object. - ThreadPoolActionTask(Function& action, Object* target = nullptr) + ThreadPoolActionTask(const Function& action, Object* target = nullptr) : ThreadPoolTask() , _action1(action) , _target(target) @@ -79,7 +79,7 @@ public: /// /// The action. /// The target object. - ThreadPoolActionTask(Function& action, Object* target = nullptr) + ThreadPoolActionTask(const Function& action, Object* target = nullptr) : ThreadPoolTask() , _action2(action) , _target(target) From f654d507e5b030190a0982406af75397df219d01 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Sun, 3 Dec 2023 14:09:58 +0100 Subject: [PATCH 47/79] Add `Where`, `Select` and `RemoveAll` to `ArrayExtensions` --- .../Engine/Core/Collections/ArrayExtensions.h | 98 +++++++++++++++++-- 1 file changed, 91 insertions(+), 7 deletions(-) diff --git a/Source/Engine/Core/Collections/ArrayExtensions.h b/Source/Engine/Core/Collections/ArrayExtensions.h index 2ae6da9c8..99d454906 100644 --- a/Source/Engine/Core/Collections/ArrayExtensions.h +++ b/Source/Engine/Core/Collections/ArrayExtensions.h @@ -55,9 +55,7 @@ public: for (int32 i = 0; i < obj.Count(); i++) { if (predicate(obj[i])) - { return i; - } } return INVALID_INDEX; } @@ -74,9 +72,7 @@ public: for (int32 i = 0; i < obj.Count(); i++) { if (predicate(obj[i])) - { return true; - } } return false; } @@ -93,13 +89,101 @@ public: for (int32 i = 0; i < obj.Count(); i++) { if (!predicate(obj[i])) - { return false; - } } return true; } + /// + /// Filters a sequence of values based on a predicate. + /// + /// The target collection. + /// The prediction function. Return true for elements that should be included in result list. + /// The result list with items that passed the predicate. + template + static void Where(const Array& obj, const Function& predicate, Array& result) + { + for (const T& i : obj) + { + if (predicate(i)) + result.Add(i); + } + } + + /// + /// Filters a sequence of values based on a predicate. + /// + /// The target collection. + /// The prediction function. Return true for elements that should be included in result list. + /// The result list with items that passed the predicate. + template + static Array Where(const Array& obj, const Function& predicate) + { + Array result; + Where(obj, predicate, result); + return result; + } + + /// + /// Projects each element of a sequence into a new form. + /// + /// The target collection. + /// A transform function to apply to each source element; the second parameter of the function represents the index of the source element. + /// The result list whose elements are the result of invoking the transform function on each element of source. + template + static void Select(const Array& obj, const Function& selector, Array& result) + { + for (const TSource& i : obj) + result.Add(MoveTemp(selector(i))); + } + + /// + /// Projects each element of a sequence into a new form. + /// + /// The target collection. + /// A transform function to apply to each source element; the second parameter of the function represents the index of the source element. + /// The result list whose elements are the result of invoking the transform function on each element of source. + template + static Array Select(const Array& obj, const Function& selector) + { + Array result; + Select(obj, selector, result); + return result; + } + + /// + /// Removes all the elements that match the conditions defined by the specified predicate. + /// + /// The target collection to modify. + /// A transform function that defines the conditions of the elements to remove. + template + static void RemoveAll(Array& obj, const Function& predicate) + { + for (int32 i = obj.Count() - 1; i >= 0; i--) + { + if (predicate(obj[i])) + obj.RemoveAtKeepOrder(i); + } + } + + /// + /// Removes all the elements that match the conditions defined by the specified predicate. + /// + /// The target collection to process. + /// A transform function that defines the conditions of the elements to remove. + /// The result list whose elements are the result of invoking the transform function on each element of source. + template + static Array RemoveAll(const Array& obj, const Function& predicate) + { + Array result; + for (const T& i : obj) + { + if (!predicate(i)) + result.Ass(i); + } + return result; + } + /// /// Groups the elements of a sequence according to a specified key selector function. /// @@ -109,7 +193,7 @@ public: template static void GroupBy(const Array& obj, const Function& keySelector, Array, AllocationType>& result) { - Dictionary> data(static_cast(obj.Count() * 3.0f)); + Dictionary> data; for (int32 i = 0; i < obj.Count(); i++) { const TKey key = keySelector(obj[i]); From 3e940c28df6c25047f4003753cc09a9a842f2c64 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Mon, 4 Dec 2023 13:56:36 +0100 Subject: [PATCH 48/79] Refactor prefab's `objectsCache` to be explicitly `SceneObject` values --- Source/Engine/Level/Prefabs/Prefab.cpp | 4 ++-- Source/Engine/Level/Prefabs/Prefab.h | 2 +- Source/Engine/Level/Prefabs/PrefabManager.cpp | 4 ++-- Source/Engine/Level/Prefabs/PrefabManager.h | 4 ++-- Source/Engine/Serialization/JsonWriter.cpp | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Source/Engine/Level/Prefabs/Prefab.cpp b/Source/Engine/Level/Prefabs/Prefab.cpp index b72f16633..65da270c0 100644 --- a/Source/Engine/Level/Prefabs/Prefab.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.cpp @@ -90,11 +90,11 @@ SceneObject* Prefab::GetDefaultInstance(const Guid& objectId) if (objectId.IsValid()) { - const void* object; + SceneObject* object; if (ObjectsCache.TryGet(objectId, object)) { // Actor or Script - return (SceneObject*)object; + return object; } } diff --git a/Source/Engine/Level/Prefabs/Prefab.h b/Source/Engine/Level/Prefabs/Prefab.h index 5e0075edf..9fa2b4fd0 100644 --- a/Source/Engine/Level/Prefabs/Prefab.h +++ b/Source/Engine/Level/Prefabs/Prefab.h @@ -44,7 +44,7 @@ public: /// /// The objects cache maps the id of the object contained in the prefab asset (actor or script) to the default instance deserialized from prefab data. Valid only if asset is loaded and GetDefaultInstance was called. /// - Dictionary ObjectsCache; + Dictionary ObjectsCache; public: /// diff --git a/Source/Engine/Level/Prefabs/PrefabManager.cpp b/Source/Engine/Level/Prefabs/PrefabManager.cpp index c7b42b7d2..bb4710bc7 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.cpp +++ b/Source/Engine/Level/Prefabs/PrefabManager.cpp @@ -76,12 +76,12 @@ Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent) return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, nullptr); } -Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization) +Actor* PrefabManager::SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization) { return SpawnPrefab(prefab, Transform(Vector3::Minimum), parent, objectsCache, withSynchronization); } -Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization) +Actor* PrefabManager::SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization) { PROFILE_CPU_NAMED("Prefab.Spawn"); if (prefab == nullptr) diff --git a/Source/Engine/Level/Prefabs/PrefabManager.h b/Source/Engine/Level/Prefabs/PrefabManager.h index 16d4a29cf..e5600bac4 100644 --- a/Source/Engine/Level/Prefabs/PrefabManager.h +++ b/Source/Engine/Level/Prefabs/PrefabManager.h @@ -89,7 +89,7 @@ API_CLASS(Static) class FLAXENGINE_API PrefabManager /// The options output objects cache that can be filled with prefab object id mapping to deserialized object (actor or script). /// True if perform prefab changes synchronization for the spawned objects. It will check if need to add new objects due to nested prefab modifications. /// The created actor (root) or null if failed. - static Actor* SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false); + static Actor* SpawnPrefab(Prefab* prefab, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false); /// /// Spawns the instance of the prefab objects. If parent actor is specified then created actors are fully initialized (OnLoad event and BeginPlay is called if parent actor is already during gameplay). @@ -100,7 +100,7 @@ API_CLASS(Static) class FLAXENGINE_API PrefabManager /// The options output objects cache that can be filled with prefab object id mapping to deserialized object (actor or script). /// True if perform prefab changes synchronization for the spawned objects. It will check if need to add new objects due to nested prefab modifications. /// The created actor (root) or null if failed. - static Actor* SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false); + static Actor* SpawnPrefab(Prefab* prefab, const Transform& transform, Actor* parent, Dictionary* objectsCache, bool withSynchronization = false); #if USE_EDITOR diff --git a/Source/Engine/Serialization/JsonWriter.cpp b/Source/Engine/Serialization/JsonWriter.cpp index 223baf77e..35e4412d4 100644 --- a/Source/Engine/Serialization/JsonWriter.cpp +++ b/Source/Engine/Serialization/JsonWriter.cpp @@ -466,7 +466,7 @@ void JsonWriter::SceneObject(::SceneObject* obj) prefab->GetDefaultInstance(); // Get prefab object instance from the prefab - const void* prefabObject; + ::SceneObject* prefabObject; if (prefab->ObjectsCache.TryGet(obj->GetPrefabObjectID(), prefabObject)) { // Serialize modified properties compared with the default object from prefab From 63ddf53ad3c5330ae011404781af603f37d09f28 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 5 Dec 2023 23:43:54 +0100 Subject: [PATCH 49/79] Fix model asset thumbnail if mesh is not centered around origin --- Source/Editor/Content/Proxy/ModelProxy.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Content/Proxy/ModelProxy.cs b/Source/Editor/Content/Proxy/ModelProxy.cs index 0cf16850d..c3ac3b247 100644 --- a/Source/Editor/Content/Proxy/ModelProxy.cs +++ b/Source/Editor/Content/Proxy/ModelProxy.cs @@ -72,7 +72,10 @@ namespace FlaxEditor.Content { if (_preview == null) { - _preview = new ModelPreview(false); + _preview = new ModelPreview(false) + { + ScaleToFit = false, + }; InitAssetPreview(_preview); } @@ -91,6 +94,7 @@ namespace FlaxEditor.Content _preview.Model = (Model)request.Asset; _preview.Parent = guiRoot; _preview.SyncBackbufferSize(); + _preview.ViewportCamera.SetArcBallView(_preview.Model.GetBox()); _preview.Task.OnDraw(); } From 5575917c4bde8f130242f76b07b734c0c48f969c Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Tue, 5 Dec 2023 23:44:45 +0100 Subject: [PATCH 50/79] Fix prefab window performance with large hierarchies --- Source/Editor/SceneGraph/GUI/ActorTreeNode.cs | 16 ++++++++++++---- .../Windows/Assets/PrefabWindow.Hierarchy.cs | 8 +++----- Source/Editor/Windows/Assets/PrefabWindow.cs | 5 +++++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs index f64e46385..14f11ceef 100644 --- a/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs +++ b/Source/Editor/SceneGraph/GUI/ActorTreeNode.cs @@ -85,12 +85,20 @@ namespace FlaxEditor.SceneGraph.GUI { if (Parent is ActorTreeNode parent) { - for (int i = 0; i < parent.ChildrenCount; i++) + var anyChanged = false; + var children = parent.Children; + for (int i = 0; i < children.Count; i++) { - if (parent.Children[i] is ActorTreeNode child && child.Actor) - child._orderInParent = child.Actor.OrderInParent; + if (children[i] is ActorTreeNode child && child.Actor) + { + var orderInParent = child.Actor.OrderInParent; + anyChanged |= child._orderInParent != orderInParent; + if (anyChanged) + child._orderInParent = orderInParent; + } } - parent.SortChildren(); + if (anyChanged) + parent.SortChildren(); } else if (Actor) { diff --git a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs index a8d9ae1be..be8f09f3a 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.Hierarchy.cs @@ -428,11 +428,9 @@ namespace FlaxEditor.Windows.Assets private void Update(ActorNode actorNode) { - if (actorNode.Actor) - { - actorNode.TreeNode.UpdateText(); - actorNode.TreeNode.OnOrderInParentChanged(); - } + actorNode.TreeNode.UpdateText(); + if (actorNode.TreeNode.IsCollapsed) + return; for (int i = 0; i < actorNode.ChildNodes.Count; i++) { diff --git a/Source/Editor/Windows/Assets/PrefabWindow.cs b/Source/Editor/Windows/Assets/PrefabWindow.cs index f50a832a1..025760b1e 100644 --- a/Source/Editor/Windows/Assets/PrefabWindow.cs +++ b/Source/Editor/Windows/Assets/PrefabWindow.cs @@ -440,6 +440,7 @@ namespace FlaxEditor.Windows.Assets { try { + FlaxEngine.Profiler.BeginEvent("PrefabWindow.Update"); if (Graph.Main != null) { // Due to fact that actors in prefab editor are only created but not added to gameplay @@ -468,6 +469,10 @@ namespace FlaxEditor.Windows.Assets Graph.Root.TreeNode.ExpandAll(true); } } + finally + { + FlaxEngine.Profiler.EndEvent(); + } // Auto fit if (_focusCamera && _viewport.Task.FrameCount > 1) From 2285116baeedb7b5863e845861f13f2b58cbdc15 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 Dec 2023 00:19:48 +0100 Subject: [PATCH 51/79] Remove old warnings about invalid model instance buffer --- Source/Engine/Content/Assets/Model.cpp | 1 - Source/Engine/Content/Assets/SkinnedModel.cpp | 1 - 2 files changed, 2 deletions(-) diff --git a/Source/Engine/Content/Assets/Model.cpp b/Source/Engine/Content/Assets/Model.cpp index 691b00a50..4eee62126 100644 --- a/Source/Engine/Content/Assets/Model.cpp +++ b/Source/Engine/Content/Assets/Model.cpp @@ -34,7 +34,6 @@ #define CHECK_INVALID_BUFFER(model, buffer) \ if (buffer->IsValidFor(model) == false) \ { \ - LOG(Warning, "Invalid Model Instance Buffer size {0} for Model {1}. It should be {2}. Manual update to proper size.", buffer->Count(), model->ToString(), model->MaterialSlots.Count()); \ buffer->Setup(model); \ } diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index b823db5a3..5e94eac49 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -23,7 +23,6 @@ #define CHECK_INVALID_BUFFER(model, buffer) \ if (buffer->IsValidFor(model) == false) \ { \ - LOG(Warning, "Invalid Skinned Model Instance Buffer size {0} for Skinned Model {1}. It should be {2}. Manual update to proper size.", buffer->Count(), model->ToString(), model->MaterialSlots.Count()); \ buffer->Setup(model); \ } From 38a0718b7030699ca35a7d71e779081ec9f2e3d6 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 Dec 2023 00:24:30 +0100 Subject: [PATCH 52/79] Fix invalid tracy events from C# profiling api when profiler gets connected mid-event --- .../Scripting/Internal/EngineInternalCalls.cpp | 12 ++++++++++-- Source/ThirdParty/tracy/client/TracyScoped.hpp | 5 +++-- Source/ThirdParty/tracy/common/TracySystem.hpp | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Scripting/Internal/EngineInternalCalls.cpp b/Source/Engine/Scripting/Internal/EngineInternalCalls.cpp index e3e5b4342..08a93328b 100644 --- a/Source/Engine/Scripting/Internal/EngineInternalCalls.cpp +++ b/Source/Engine/Scripting/Internal/EngineInternalCalls.cpp @@ -108,6 +108,7 @@ namespace }; ChunkedArray ManagedSourceLocations; + uint32 ManagedEventsCount[PLATFORM_THREADS_LIMIT] = { 0 }; #endif #endif } @@ -145,7 +146,9 @@ DEFINE_INTERNAL_CALL(void) ProfilerInternal_BeginEvent(MString* nameObj) srcLoc->color = 0; } //static constexpr tracy::SourceLocationData tracySrcLoc{ nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; - tracy::ScopedZone::Begin(srcLoc); + const bool tracyActive = tracy::ScopedZone::Begin(srcLoc); + if (tracyActive) + ManagedEventsCount[Platform::GetCurrentThreadID()]++; #endif #endif #endif @@ -155,7 +158,12 @@ DEFINE_INTERNAL_CALL(void) ProfilerInternal_EndEvent() { #if COMPILE_WITH_PROFILER #if TRACY_ENABLE - tracy::ScopedZone::End(); + auto& tracyActive = ManagedEventsCount[Platform::GetCurrentThreadID()]; + if (tracyActive > 0) + { + tracyActive--; + tracy::ScopedZone::End(); + } #endif ProfilerCPU::EndEvent(); #endif diff --git a/Source/ThirdParty/tracy/client/TracyScoped.hpp b/Source/ThirdParty/tracy/client/TracyScoped.hpp index bb916aa57..2182bf65b 100644 --- a/Source/ThirdParty/tracy/client/TracyScoped.hpp +++ b/Source/ThirdParty/tracy/client/TracyScoped.hpp @@ -12,15 +12,16 @@ namespace tracy { -void ScopedZone::Begin(const SourceLocationData* srcloc) +bool ScopedZone::Begin(const SourceLocationData* srcloc) { #ifdef TRACY_ON_DEMAND - if (!GetProfiler().IsConnected()) return; + if (!GetProfiler().IsConnected()) return false; #endif TracyLfqPrepare( QueueType::ZoneBegin ); MemWrite( &item->zoneBegin.time, Profiler::GetTime() ); MemWrite( &item->zoneBegin.srcloc, (uint64_t)srcloc ); TracyQueueCommit( zoneBeginThread ); + return true; } void ScopedZone::End() diff --git a/Source/ThirdParty/tracy/common/TracySystem.hpp b/Source/ThirdParty/tracy/common/TracySystem.hpp index 7a88a00b1..497d047e5 100644 --- a/Source/ThirdParty/tracy/common/TracySystem.hpp +++ b/Source/ThirdParty/tracy/common/TracySystem.hpp @@ -39,7 +39,7 @@ struct TRACY_API SourceLocationData class TRACY_API ScopedZone { public: - static void Begin( const SourceLocationData* srcloc ); + static bool Begin( const SourceLocationData* srcloc ); static void End(); ScopedZone( const ScopedZone& ) = delete; From fdfca5156b99b4348f691a8718b535e88a0e893e Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 Dec 2023 00:28:03 +0100 Subject: [PATCH 53/79] Various fixes and stability improvements --- Source/Engine/Graphics/Models/ModelData.cpp | 7 ++++++- Source/Engine/Level/Components/MissingScript.h | 2 -- Source/Engine/Renderer/PostProcessingPass.cpp | 4 ++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Graphics/Models/ModelData.cpp b/Source/Engine/Graphics/Models/ModelData.cpp index 3a31c5771..6e6ae04b5 100644 --- a/Source/Engine/Graphics/Models/ModelData.cpp +++ b/Source/Engine/Graphics/Models/ModelData.cpp @@ -729,7 +729,12 @@ bool ModelData::Pack2ModelHeader(WriteStream* stream) const // Amount of meshes const int32 meshes = lod.Meshes.Count(); - if (meshes == 0 || meshes > MODEL_MAX_MESHES) + if (meshes == 0) + { + LOG(Warning, "Empty LOD."); + return true; + } + if (meshes > MODEL_MAX_MESHES) { LOG(Warning, "Too many meshes per LOD."); return true; diff --git a/Source/Engine/Level/Components/MissingScript.h b/Source/Engine/Level/Components/MissingScript.h index 7b351bd82..d512ba992 100644 --- a/Source/Engine/Level/Components/MissingScript.h +++ b/Source/Engine/Level/Components/MissingScript.h @@ -4,10 +4,8 @@ #if USE_EDITOR -#include "Engine/Core/Cache.h" #include "Engine/Scripting/Script.h" #include "Engine/Scripting/ScriptingObjectReference.h" -#include "Engine/Serialization/JsonWriters.h" /// /// Actor script component that represents missing script. diff --git a/Source/Engine/Renderer/PostProcessingPass.cpp b/Source/Engine/Renderer/PostProcessingPass.cpp index 006927639..7c93177e0 100644 --- a/Source/Engine/Renderer/PostProcessingPass.cpp +++ b/Source/Engine/Renderer/PostProcessingPass.cpp @@ -434,6 +434,10 @@ void PostProcessingPass::Render(RenderContext& renderContext, GPUTexture* input, // Set lens flares output context->BindSR(3, bloomTmp2->View(0, 1)); } + else + { + context->BindSR(3, (GPUResourceView*)nullptr); + } //////////////////////////////////////////////////////////////////////////////////// // Final composite From 4a3be5a743732a497843be7b5227d572e7614a66 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 Dec 2023 00:30:37 +0100 Subject: [PATCH 54/79] Fix crash when updating prefabs from async thread --- Source/Engine/Level/Prefabs/Prefab.Apply.cpp | 35 +++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp index c10ea532d..569a7ce9b 100644 --- a/Source/Engine/Level/Prefabs/Prefab.Apply.cpp +++ b/Source/Engine/Level/Prefabs/Prefab.Apply.cpp @@ -22,6 +22,7 @@ #include "Engine/ContentImporters/CreateJson.h" #include "Engine/Debug/Exceptions/ArgumentNullException.h" #include "Engine/Profiler/ProfilerCPU.h" +#include "Engine/Threading/MainThreadTask.h" #include "Editor/Editor.h" // Apply flow: @@ -174,6 +175,12 @@ public: /// Collection with ids of the objects (actors and scripts) from the prefab after changes apply. Used to find new objects or old objects and use this information during changes sync (eg. generate ids for the new objects to prevent ids collisions). /// True if failed, otherwise false. static bool SynchronizePrefabInstances(PrefabInstancesData& prefabInstancesData, Actor* defaultInstance, SceneObjectsListCacheType& sceneObjects, const Guid& prefabId, rapidjson_flax::StringBuffer& tmpBuffer, const Array& oldObjectsIds, const Array& newObjectIds); + + static void DeletePrefabObject(SceneObject* obj) + { + obj->SetParent(nullptr); + obj->DeleteObject(); + } }; void PrefabInstanceData::CollectPrefabInstances(PrefabInstancesData& prefabInstancesData, const Guid& prefabId, Actor* defaultInstance, Actor* targetActor) @@ -302,14 +309,10 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI { // Remove object LOG(Info, "Removing object {0} from instance {1} (prefab: {2})", obj->GetSceneObjectId(), instance.TargetActor->ToString(), prefabId); - - obj->DeleteObject(); - obj->SetParent(nullptr); - + DeletePrefabObject(obj); sceneObjects.Value->RemoveAtKeepOrder(i); existingObjectsCount--; i--; - continue; } @@ -358,10 +361,7 @@ bool PrefabInstanceData::SynchronizePrefabInstances(PrefabInstancesData& prefabI { // Remove object removed from the prefab LOG(Info, "Removing prefab instance object {0} from instance {1} (prefab object: {2}, prefab: {3})", obj->GetSceneObjectId(), instance.TargetActor->ToString(), obj->GetPrefabObjectID(), prefabId); - - obj->DeleteObject(); - obj->SetParent(nullptr); - + DeletePrefabObject(obj); sceneObjects.Value->RemoveAtKeepOrder(i); deserializeSceneObjectIndex--; existingObjectsCount--; @@ -633,6 +633,19 @@ bool Prefab::ApplyAll(Actor* targetActor) } } } + if (!IsInMainThread()) + { + // Prefabs cannot be updated on async thread so sync it with a Main Thread + bool result = true; + Function action = [&] + { + result = ApplyAll(targetActor); + }; + const auto task = Task::StartNew(New(action)); + if (task->Wait(TimeSpan::FromSeconds(10))) + result = true; + return result; + } // Prevent cyclic references { @@ -921,9 +934,7 @@ bool Prefab::ApplyAllInternal(Actor* targetActor, bool linkTargetActorObjectToPr { // Remove object removed from the prefab LOG(Info, "Removing object {0} from prefab default instance", obj->GetSceneObjectId()); - - obj->DeleteObject(); - obj->SetParent(nullptr); + PrefabInstanceData::DeletePrefabObject(obj); sceneObjects->At(i) = nullptr; } } From 78860697834a61039bfaca34afa1f497d51cc1e2 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 Dec 2023 10:33:14 +0100 Subject: [PATCH 55/79] Update `meshoptimizer` to `v0.20` --- Source/ThirdParty/meshoptimizer/LICENSE.md | 2 +- Source/ThirdParty/meshoptimizer/allocator.cpp | 2 +- .../ThirdParty/meshoptimizer/clusterizer.cpp | 671 +++++++++++-- .../ThirdParty/meshoptimizer/indexcodec.cpp | 82 +- .../meshoptimizer/indexgenerator.cpp | 249 ++++- .../ThirdParty/meshoptimizer/meshoptimizer.h | 436 +++++--- .../meshoptimizer/overdrawanalyzer.cpp | 2 +- .../meshoptimizer/overdrawoptimizer.cpp | 2 +- .../ThirdParty/meshoptimizer/quantization.cpp | 70 ++ .../ThirdParty/meshoptimizer/simplifier.cpp | 934 ++++++++++++------ .../ThirdParty/meshoptimizer/spatialorder.cpp | 4 +- .../meshoptimizer/vcacheoptimizer.cpp | 49 +- .../ThirdParty/meshoptimizer/vertexcodec.cpp | 243 ++--- .../ThirdParty/meshoptimizer/vertexfilter.cpp | 260 ++++- 14 files changed, 2216 insertions(+), 790 deletions(-) create mode 100644 Source/ThirdParty/meshoptimizer/quantization.cpp diff --git a/Source/ThirdParty/meshoptimizer/LICENSE.md b/Source/ThirdParty/meshoptimizer/LICENSE.md index 4fcd766d2..962ed41ff 100644 --- a/Source/ThirdParty/meshoptimizer/LICENSE.md +++ b/Source/ThirdParty/meshoptimizer/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2016-2020 Arseny Kapoulkine +Copyright (c) 2016-2023 Arseny Kapoulkine Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Source/ThirdParty/meshoptimizer/allocator.cpp b/Source/ThirdParty/meshoptimizer/allocator.cpp index da7cc540b..072e8e51a 100644 --- a/Source/ThirdParty/meshoptimizer/allocator.cpp +++ b/Source/ThirdParty/meshoptimizer/allocator.cpp @@ -1,7 +1,7 @@ // This file is part of meshoptimizer library; see meshoptimizer.h for version/license details #include "meshoptimizer.h" -void meshopt_setAllocator(void* (*allocate)(size_t), void (*deallocate)(void*)) +void meshopt_setAllocator(void* (MESHOPTIMIZER_ALLOC_CALLCONV *allocate)(size_t), void (MESHOPTIMIZER_ALLOC_CALLCONV *deallocate)(void*)) { meshopt_Allocator::Storage::allocate = allocate; meshopt_Allocator::Storage::deallocate = deallocate; diff --git a/Source/ThirdParty/meshoptimizer/clusterizer.cpp b/Source/ThirdParty/meshoptimizer/clusterizer.cpp index f7d88c513..c4672ad60 100644 --- a/Source/ThirdParty/meshoptimizer/clusterizer.cpp +++ b/Source/ThirdParty/meshoptimizer/clusterizer.cpp @@ -2,6 +2,7 @@ #include "meshoptimizer.h" #include +#include #include #include @@ -12,6 +13,68 @@ namespace meshopt { +// This must be <= 255 since index 0xff is used internally to indice a vertex that doesn't belong to a meshlet +const size_t kMeshletMaxVertices = 255; + +// A reasonable limit is around 2*max_vertices or less +const size_t kMeshletMaxTriangles = 512; + +struct TriangleAdjacency2 +{ + unsigned int* counts; + unsigned int* offsets; + unsigned int* data; +}; + +static void buildTriangleAdjacency(TriangleAdjacency2& adjacency, const unsigned int* indices, size_t index_count, size_t vertex_count, meshopt_Allocator& allocator) +{ + size_t face_count = index_count / 3; + + // allocate arrays + adjacency.counts = allocator.allocate(vertex_count); + adjacency.offsets = allocator.allocate(vertex_count); + adjacency.data = allocator.allocate(index_count); + + // fill triangle counts + memset(adjacency.counts, 0, vertex_count * sizeof(unsigned int)); + + for (size_t i = 0; i < index_count; ++i) + { + assert(indices[i] < vertex_count); + + adjacency.counts[indices[i]]++; + } + + // fill offset table + unsigned int offset = 0; + + for (size_t i = 0; i < vertex_count; ++i) + { + adjacency.offsets[i] = offset; + offset += adjacency.counts[i]; + } + + assert(offset == index_count); + + // fill triangle data + for (size_t i = 0; i < face_count; ++i) + { + unsigned int a = indices[i * 3 + 0], b = indices[i * 3 + 1], c = indices[i * 3 + 2]; + + adjacency.data[adjacency.offsets[a]++] = unsigned(i); + adjacency.data[adjacency.offsets[b]++] = unsigned(i); + adjacency.data[adjacency.offsets[c]++] = unsigned(i); + } + + // fix offsets that have been disturbed by the previous pass + for (size_t i = 0; i < vertex_count; ++i) + { + assert(adjacency.offsets[i] >= adjacency.counts[i]); + + adjacency.offsets[i] -= adjacency.counts[i]; + } +} + static void computeBoundingSphere(float result[4], const float points[][3], size_t count) { assert(count > 0); @@ -82,13 +145,382 @@ static void computeBoundingSphere(float result[4], const float points[][3], size result[3] = radius; } +struct Cone +{ + float px, py, pz; + float nx, ny, nz; +}; + +static float getMeshletScore(float distance2, float spread, float cone_weight, float expected_radius) +{ + float cone = 1.f - spread * cone_weight; + float cone_clamped = cone < 1e-3f ? 1e-3f : cone; + + return (1 + sqrtf(distance2) / expected_radius * (1 - cone_weight)) * cone_clamped; +} + +static Cone getMeshletCone(const Cone& acc, unsigned int triangle_count) +{ + Cone result = acc; + + float center_scale = triangle_count == 0 ? 0.f : 1.f / float(triangle_count); + + result.px *= center_scale; + result.py *= center_scale; + result.pz *= center_scale; + + float axis_length = result.nx * result.nx + result.ny * result.ny + result.nz * result.nz; + float axis_scale = axis_length == 0.f ? 0.f : 1.f / sqrtf(axis_length); + + result.nx *= axis_scale; + result.ny *= axis_scale; + result.nz *= axis_scale; + + return result; +} + +static float computeTriangleCones(Cone* triangles, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) +{ + (void)vertex_count; + + size_t vertex_stride_float = vertex_positions_stride / sizeof(float); + size_t face_count = index_count / 3; + + float mesh_area = 0; + + for (size_t i = 0; i < face_count; ++i) + { + unsigned int a = indices[i * 3 + 0], b = indices[i * 3 + 1], c = indices[i * 3 + 2]; + assert(a < vertex_count && b < vertex_count && c < vertex_count); + + const float* p0 = vertex_positions + vertex_stride_float * a; + const float* p1 = vertex_positions + vertex_stride_float * b; + const float* p2 = vertex_positions + vertex_stride_float * c; + + float p10[3] = {p1[0] - p0[0], p1[1] - p0[1], p1[2] - p0[2]}; + float p20[3] = {p2[0] - p0[0], p2[1] - p0[1], p2[2] - p0[2]}; + + float normalx = p10[1] * p20[2] - p10[2] * p20[1]; + float normaly = p10[2] * p20[0] - p10[0] * p20[2]; + float normalz = p10[0] * p20[1] - p10[1] * p20[0]; + + float area = sqrtf(normalx * normalx + normaly * normaly + normalz * normalz); + float invarea = (area == 0.f) ? 0.f : 1.f / area; + + triangles[i].px = (p0[0] + p1[0] + p2[0]) / 3.f; + triangles[i].py = (p0[1] + p1[1] + p2[1]) / 3.f; + triangles[i].pz = (p0[2] + p1[2] + p2[2]) / 3.f; + + triangles[i].nx = normalx * invarea; + triangles[i].ny = normaly * invarea; + triangles[i].nz = normalz * invarea; + + mesh_area += area; + } + + return mesh_area; +} + +static void finishMeshlet(meshopt_Meshlet& meshlet, unsigned char* meshlet_triangles) +{ + size_t offset = meshlet.triangle_offset + meshlet.triangle_count * 3; + + // fill 4b padding with 0 + while (offset & 3) + meshlet_triangles[offset++] = 0; +} + +static bool appendMeshlet(meshopt_Meshlet& meshlet, unsigned int a, unsigned int b, unsigned int c, unsigned char* used, meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, size_t meshlet_offset, size_t max_vertices, size_t max_triangles) +{ + unsigned char& av = used[a]; + unsigned char& bv = used[b]; + unsigned char& cv = used[c]; + + bool result = false; + + unsigned int used_extra = (av == 0xff) + (bv == 0xff) + (cv == 0xff); + + if (meshlet.vertex_count + used_extra > max_vertices || meshlet.triangle_count >= max_triangles) + { + meshlets[meshlet_offset] = meshlet; + + for (size_t j = 0; j < meshlet.vertex_count; ++j) + used[meshlet_vertices[meshlet.vertex_offset + j]] = 0xff; + + finishMeshlet(meshlet, meshlet_triangles); + + meshlet.vertex_offset += meshlet.vertex_count; + meshlet.triangle_offset += (meshlet.triangle_count * 3 + 3) & ~3; // 4b padding + meshlet.vertex_count = 0; + meshlet.triangle_count = 0; + + result = true; + } + + if (av == 0xff) + { + av = (unsigned char)meshlet.vertex_count; + meshlet_vertices[meshlet.vertex_offset + meshlet.vertex_count++] = a; + } + + if (bv == 0xff) + { + bv = (unsigned char)meshlet.vertex_count; + meshlet_vertices[meshlet.vertex_offset + meshlet.vertex_count++] = b; + } + + if (cv == 0xff) + { + cv = (unsigned char)meshlet.vertex_count; + meshlet_vertices[meshlet.vertex_offset + meshlet.vertex_count++] = c; + } + + meshlet_triangles[meshlet.triangle_offset + meshlet.triangle_count * 3 + 0] = av; + meshlet_triangles[meshlet.triangle_offset + meshlet.triangle_count * 3 + 1] = bv; + meshlet_triangles[meshlet.triangle_offset + meshlet.triangle_count * 3 + 2] = cv; + meshlet.triangle_count++; + + return result; +} + +static unsigned int getNeighborTriangle(const meshopt_Meshlet& meshlet, const Cone* meshlet_cone, unsigned int* meshlet_vertices, const unsigned int* indices, const TriangleAdjacency2& adjacency, const Cone* triangles, const unsigned int* live_triangles, const unsigned char* used, float meshlet_expected_radius, float cone_weight, unsigned int* out_extra) +{ + unsigned int best_triangle = ~0u; + unsigned int best_extra = 5; + float best_score = FLT_MAX; + + for (size_t i = 0; i < meshlet.vertex_count; ++i) + { + unsigned int index = meshlet_vertices[meshlet.vertex_offset + i]; + + unsigned int* neighbors = &adjacency.data[0] + adjacency.offsets[index]; + size_t neighbors_size = adjacency.counts[index]; + + for (size_t j = 0; j < neighbors_size; ++j) + { + unsigned int triangle = neighbors[j]; + unsigned int a = indices[triangle * 3 + 0], b = indices[triangle * 3 + 1], c = indices[triangle * 3 + 2]; + + unsigned int extra = (used[a] == 0xff) + (used[b] == 0xff) + (used[c] == 0xff); + + // triangles that don't add new vertices to meshlets are max. priority + if (extra != 0) + { + // artificially increase the priority of dangling triangles as they're expensive to add to new meshlets + if (live_triangles[a] == 1 || live_triangles[b] == 1 || live_triangles[c] == 1) + extra = 0; + + extra++; + } + + // since topology-based priority is always more important than the score, we can skip scoring in some cases + if (extra > best_extra) + continue; + + float score = 0; + + // caller selects one of two scoring functions: geometrical (based on meshlet cone) or topological (based on remaining triangles) + if (meshlet_cone) + { + const Cone& tri_cone = triangles[triangle]; + + float distance2 = + (tri_cone.px - meshlet_cone->px) * (tri_cone.px - meshlet_cone->px) + + (tri_cone.py - meshlet_cone->py) * (tri_cone.py - meshlet_cone->py) + + (tri_cone.pz - meshlet_cone->pz) * (tri_cone.pz - meshlet_cone->pz); + + float spread = tri_cone.nx * meshlet_cone->nx + tri_cone.ny * meshlet_cone->ny + tri_cone.nz * meshlet_cone->nz; + + score = getMeshletScore(distance2, spread, cone_weight, meshlet_expected_radius); + } + else + { + // each live_triangles entry is >= 1 since it includes the current triangle we're processing + score = float(live_triangles[a] + live_triangles[b] + live_triangles[c] - 3); + } + + // note that topology-based priority is always more important than the score + // this helps maintain reasonable effectiveness of meshlet data and reduces scoring cost + if (extra < best_extra || score < best_score) + { + best_triangle = triangle; + best_extra = extra; + best_score = score; + } + } + } + + if (out_extra) + *out_extra = best_extra; + + return best_triangle; +} + +struct KDNode +{ + union + { + float split; + unsigned int index; + }; + + // leaves: axis = 3, children = number of extra points after this one (0 if 'index' is the only point) + // branches: axis != 3, left subtree = skip 1, right subtree = skip 1+children + unsigned int axis : 2; + unsigned int children : 30; +}; + +static size_t kdtreePartition(unsigned int* indices, size_t count, const float* points, size_t stride, unsigned int axis, float pivot) +{ + size_t m = 0; + + // invariant: elements in range [0, m) are < pivot, elements in range [m, i) are >= pivot + for (size_t i = 0; i < count; ++i) + { + float v = points[indices[i] * stride + axis]; + + // swap(m, i) unconditionally + unsigned int t = indices[m]; + indices[m] = indices[i]; + indices[i] = t; + + // when v >= pivot, we swap i with m without advancing it, preserving invariants + m += v < pivot; + } + + return m; +} + +static size_t kdtreeBuildLeaf(size_t offset, KDNode* nodes, size_t node_count, unsigned int* indices, size_t count) +{ + assert(offset + count <= node_count); + (void)node_count; + + KDNode& result = nodes[offset]; + + result.index = indices[0]; + result.axis = 3; + result.children = unsigned(count - 1); + + // all remaining points are stored in nodes immediately following the leaf + for (size_t i = 1; i < count; ++i) + { + KDNode& tail = nodes[offset + i]; + + tail.index = indices[i]; + tail.axis = 3; + tail.children = ~0u >> 2; // bogus value to prevent misuse + } + + return offset + count; +} + +static size_t kdtreeBuild(size_t offset, KDNode* nodes, size_t node_count, const float* points, size_t stride, unsigned int* indices, size_t count, size_t leaf_size) +{ + assert(count > 0); + assert(offset < node_count); + + if (count <= leaf_size) + return kdtreeBuildLeaf(offset, nodes, node_count, indices, count); + + float mean[3] = {}; + float vars[3] = {}; + float runc = 1, runs = 1; + + // gather statistics on the points in the subtree using Welford's algorithm + for (size_t i = 0; i < count; ++i, runc += 1.f, runs = 1.f / runc) + { + const float* point = points + indices[i] * stride; + + for (int k = 0; k < 3; ++k) + { + float delta = point[k] - mean[k]; + mean[k] += delta * runs; + vars[k] += delta * (point[k] - mean[k]); + } + } + + // split axis is one where the variance is largest + unsigned int axis = vars[0] >= vars[1] && vars[0] >= vars[2] ? 0 : vars[1] >= vars[2] ? 1 : 2; + + float split = mean[axis]; + size_t middle = kdtreePartition(indices, count, points, stride, axis, split); + + // when the partition is degenerate simply consolidate the points into a single node + if (middle <= leaf_size / 2 || middle >= count - leaf_size / 2) + return kdtreeBuildLeaf(offset, nodes, node_count, indices, count); + + KDNode& result = nodes[offset]; + + result.split = split; + result.axis = axis; + + // left subtree is right after our node + size_t next_offset = kdtreeBuild(offset + 1, nodes, node_count, points, stride, indices, middle, leaf_size); + + // distance to the right subtree is represented explicitly + result.children = unsigned(next_offset - offset - 1); + + return kdtreeBuild(next_offset, nodes, node_count, points, stride, indices + middle, count - middle, leaf_size); +} + +static void kdtreeNearest(KDNode* nodes, unsigned int root, const float* points, size_t stride, const unsigned char* emitted_flags, const float* position, unsigned int& result, float& limit) +{ + const KDNode& node = nodes[root]; + + if (node.axis == 3) + { + // leaf + for (unsigned int i = 0; i <= node.children; ++i) + { + unsigned int index = nodes[root + i].index; + + if (emitted_flags[index]) + continue; + + const float* point = points + index * stride; + + float distance2 = + (point[0] - position[0]) * (point[0] - position[0]) + + (point[1] - position[1]) * (point[1] - position[1]) + + (point[2] - position[2]) * (point[2] - position[2]); + float distance = sqrtf(distance2); + + if (distance < limit) + { + result = index; + limit = distance; + } + } + } + else + { + // branch; we order recursion to process the node that search position is in first + float delta = position[node.axis] - node.split; + unsigned int first = (delta <= 0) ? 0 : node.children; + unsigned int second = first ^ node.children; + + kdtreeNearest(nodes, root + 1 + first, points, stride, emitted_flags, position, result, limit); + + // only process the other node if it can have a match based on closest distance so far + if (fabsf(delta) <= limit) + kdtreeNearest(nodes, root + 1 + second, points, stride, emitted_flags, position, result, limit); + } +} + } // namespace meshopt size_t meshopt_buildMeshletsBound(size_t index_count, size_t max_vertices, size_t max_triangles) { + using namespace meshopt; + assert(index_count % 3 == 0); - assert(max_vertices >= 3); - assert(max_triangles >= 1); + assert(max_vertices >= 3 && max_vertices <= kMeshletMaxVertices); + assert(max_triangles >= 1 && max_triangles <= kMeshletMaxTriangles); + assert(max_triangles % 4 == 0); // ensures the caller will compute output space properly as index data is 4b aligned + + (void)kMeshletMaxVertices; + (void)kMeshletMaxTriangles; // meshlet construction is limited by max vertices and max triangles per meshlet // the worst case is that the input is an unindexed stream since this equally stresses both limits @@ -100,77 +532,181 @@ size_t meshopt_buildMeshletsBound(size_t index_count, size_t max_vertices, size_ return meshlet_limit_vertices > meshlet_limit_triangles ? meshlet_limit_vertices : meshlet_limit_triangles; } -size_t meshopt_buildMeshlets(meshopt_Meshlet* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, size_t max_vertices, size_t max_triangles) +size_t meshopt_buildMeshlets(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t max_triangles, float cone_weight) { + using namespace meshopt; + assert(index_count % 3 == 0); - assert(max_vertices >= 3); - assert(max_triangles >= 1); + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); + assert(vertex_positions_stride % sizeof(float) == 0); + + assert(max_vertices >= 3 && max_vertices <= kMeshletMaxVertices); + assert(max_triangles >= 1 && max_triangles <= kMeshletMaxTriangles); + assert(max_triangles % 4 == 0); // ensures the caller will compute output space properly as index data is 4b aligned + + assert(cone_weight >= 0 && cone_weight <= 1); meshopt_Allocator allocator; - meshopt_Meshlet meshlet; - memset(&meshlet, 0, sizeof(meshlet)); + TriangleAdjacency2 adjacency = {}; + buildTriangleAdjacency(adjacency, indices, index_count, vertex_count, allocator); - assert(max_vertices <= sizeof(meshlet.vertices) / sizeof(meshlet.vertices[0])); - assert(max_triangles <= sizeof(meshlet.indices) / 3); + unsigned int* live_triangles = allocator.allocate(vertex_count); + memcpy(live_triangles, adjacency.counts, vertex_count * sizeof(unsigned int)); + + size_t face_count = index_count / 3; + + unsigned char* emitted_flags = allocator.allocate(face_count); + memset(emitted_flags, 0, face_count); + + // for each triangle, precompute centroid & normal to use for scoring + Cone* triangles = allocator.allocate(face_count); + float mesh_area = computeTriangleCones(triangles, indices, index_count, vertex_positions, vertex_count, vertex_positions_stride); + + // assuming each meshlet is a square patch, expected radius is sqrt(expected area) + float triangle_area_avg = face_count == 0 ? 0.f : mesh_area / float(face_count) * 0.5f; + float meshlet_expected_radius = sqrtf(triangle_area_avg * max_triangles) * 0.5f; + + // build a kd-tree for nearest neighbor lookup + unsigned int* kdindices = allocator.allocate(face_count); + for (size_t i = 0; i < face_count; ++i) + kdindices[i] = unsigned(i); + + KDNode* nodes = allocator.allocate(face_count * 2); + kdtreeBuild(0, nodes, face_count * 2, &triangles[0].px, sizeof(Cone) / sizeof(float), kdindices, face_count, /* leaf_size= */ 8); // index of the vertex in the meshlet, 0xff if the vertex isn't used unsigned char* used = allocator.allocate(vertex_count); memset(used, -1, vertex_count); - size_t offset = 0; + meshopt_Meshlet meshlet = {}; + size_t meshlet_offset = 0; + + Cone meshlet_cone_acc = {}; + + for (;;) + { + Cone meshlet_cone = getMeshletCone(meshlet_cone_acc, meshlet.triangle_count); + + unsigned int best_extra = 0; + unsigned int best_triangle = getNeighborTriangle(meshlet, &meshlet_cone, meshlet_vertices, indices, adjacency, triangles, live_triangles, used, meshlet_expected_radius, cone_weight, &best_extra); + + // if the best triangle doesn't fit into current meshlet, the spatial scoring we've used is not very meaningful, so we re-select using topological scoring + if (best_triangle != ~0u && (meshlet.vertex_count + best_extra > max_vertices || meshlet.triangle_count >= max_triangles)) + { + best_triangle = getNeighborTriangle(meshlet, NULL, meshlet_vertices, indices, adjacency, triangles, live_triangles, used, meshlet_expected_radius, 0.f, NULL); + } + + // when we run out of neighboring triangles we need to switch to spatial search; we currently just pick the closest triangle irrespective of connectivity + if (best_triangle == ~0u) + { + float position[3] = {meshlet_cone.px, meshlet_cone.py, meshlet_cone.pz}; + unsigned int index = ~0u; + float limit = FLT_MAX; + + kdtreeNearest(nodes, 0, &triangles[0].px, sizeof(Cone) / sizeof(float), emitted_flags, position, index, limit); + + best_triangle = index; + } + + if (best_triangle == ~0u) + break; + + unsigned int a = indices[best_triangle * 3 + 0], b = indices[best_triangle * 3 + 1], c = indices[best_triangle * 3 + 2]; + assert(a < vertex_count && b < vertex_count && c < vertex_count); + + // add meshlet to the output; when the current meshlet is full we reset the accumulated bounds + if (appendMeshlet(meshlet, a, b, c, used, meshlets, meshlet_vertices, meshlet_triangles, meshlet_offset, max_vertices, max_triangles)) + { + meshlet_offset++; + memset(&meshlet_cone_acc, 0, sizeof(meshlet_cone_acc)); + } + + live_triangles[a]--; + live_triangles[b]--; + live_triangles[c]--; + + // remove emitted triangle from adjacency data + // this makes sure that we spend less time traversing these lists on subsequent iterations + for (size_t k = 0; k < 3; ++k) + { + unsigned int index = indices[best_triangle * 3 + k]; + + unsigned int* neighbors = &adjacency.data[0] + adjacency.offsets[index]; + size_t neighbors_size = adjacency.counts[index]; + + for (size_t i = 0; i < neighbors_size; ++i) + { + unsigned int tri = neighbors[i]; + + if (tri == best_triangle) + { + neighbors[i] = neighbors[neighbors_size - 1]; + adjacency.counts[index]--; + break; + } + } + } + + // update aggregated meshlet cone data for scoring subsequent triangles + meshlet_cone_acc.px += triangles[best_triangle].px; + meshlet_cone_acc.py += triangles[best_triangle].py; + meshlet_cone_acc.pz += triangles[best_triangle].pz; + meshlet_cone_acc.nx += triangles[best_triangle].nx; + meshlet_cone_acc.ny += triangles[best_triangle].ny; + meshlet_cone_acc.nz += triangles[best_triangle].nz; + + emitted_flags[best_triangle] = 1; + } + + if (meshlet.triangle_count) + { + finishMeshlet(meshlet, meshlet_triangles); + + meshlets[meshlet_offset++] = meshlet; + } + + assert(meshlet_offset <= meshopt_buildMeshletsBound(index_count, max_vertices, max_triangles)); + return meshlet_offset; +} + +size_t meshopt_buildMeshletsScan(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const unsigned int* indices, size_t index_count, size_t vertex_count, size_t max_vertices, size_t max_triangles) +{ + using namespace meshopt; + + assert(index_count % 3 == 0); + + assert(max_vertices >= 3 && max_vertices <= kMeshletMaxVertices); + assert(max_triangles >= 1 && max_triangles <= kMeshletMaxTriangles); + assert(max_triangles % 4 == 0); // ensures the caller will compute output space properly as index data is 4b aligned + + meshopt_Allocator allocator; + + // index of the vertex in the meshlet, 0xff if the vertex isn't used + unsigned char* used = allocator.allocate(vertex_count); + memset(used, -1, vertex_count); + + meshopt_Meshlet meshlet = {}; + size_t meshlet_offset = 0; for (size_t i = 0; i < index_count; i += 3) { unsigned int a = indices[i + 0], b = indices[i + 1], c = indices[i + 2]; assert(a < vertex_count && b < vertex_count && c < vertex_count); - unsigned char& av = used[a]; - unsigned char& bv = used[b]; - unsigned char& cv = used[c]; - - unsigned int used_extra = (av == 0xff) + (bv == 0xff) + (cv == 0xff); - - if (meshlet.vertex_count + used_extra > max_vertices || meshlet.triangle_count >= max_triangles) - { - destination[offset++] = meshlet; - - for (size_t j = 0; j < meshlet.vertex_count; ++j) - used[meshlet.vertices[j]] = 0xff; - - memset(&meshlet, 0, sizeof(meshlet)); - } - - if (av == 0xff) - { - av = meshlet.vertex_count; - meshlet.vertices[meshlet.vertex_count++] = a; - } - - if (bv == 0xff) - { - bv = meshlet.vertex_count; - meshlet.vertices[meshlet.vertex_count++] = b; - } - - if (cv == 0xff) - { - cv = meshlet.vertex_count; - meshlet.vertices[meshlet.vertex_count++] = c; - } - - meshlet.indices[meshlet.triangle_count][0] = av; - meshlet.indices[meshlet.triangle_count][1] = bv; - meshlet.indices[meshlet.triangle_count][2] = cv; - meshlet.triangle_count++; + // appends triangle to the meshlet and writes previous meshlet to the output if full + meshlet_offset += appendMeshlet(meshlet, a, b, c, used, meshlets, meshlet_vertices, meshlet_triangles, meshlet_offset, max_vertices, max_triangles); } if (meshlet.triangle_count) - destination[offset++] = meshlet; + { + finishMeshlet(meshlet, meshlet_triangles); - assert(offset <= meshopt_buildMeshletsBound(index_count, max_vertices, max_triangles)); + meshlets[meshlet_offset++] = meshlet; + } - return offset; + assert(meshlet_offset <= meshopt_buildMeshletsBound(index_count, max_vertices, max_triangles)); + return meshlet_offset; } meshopt_Bounds meshopt_computeClusterBounds(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) @@ -178,18 +714,17 @@ meshopt_Bounds meshopt_computeClusterBounds(const unsigned int* indices, size_t using namespace meshopt; assert(index_count % 3 == 0); - assert(vertex_positions_stride > 0 && vertex_positions_stride <= 256); + assert(index_count / 3 <= kMeshletMaxTriangles); + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); assert(vertex_positions_stride % sizeof(float) == 0); - assert(index_count / 3 <= 256); - (void)vertex_count; size_t vertex_stride_float = vertex_positions_stride / sizeof(float); // compute triangle normals and gather triangle corners - float normals[256][3]; - float corners[256][3][3]; + float normals[kMeshletMaxTriangles][3]; + float corners[kMeshletMaxTriangles][3][3]; size_t triangles = 0; for (size_t i = 0; i < index_count; i += 3) @@ -327,25 +862,23 @@ meshopt_Bounds meshopt_computeClusterBounds(const unsigned int* indices, size_t return bounds; } -meshopt_Bounds meshopt_computeMeshletBounds(const meshopt_Meshlet* meshlet, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) +meshopt_Bounds meshopt_computeMeshletBounds(const unsigned int* meshlet_vertices, const unsigned char* meshlet_triangles, size_t triangle_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) { - assert(vertex_positions_stride > 0 && vertex_positions_stride <= 256); + using namespace meshopt; + + assert(triangle_count <= kMeshletMaxTriangles); + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); assert(vertex_positions_stride % sizeof(float) == 0); - unsigned int indices[sizeof(meshlet->indices) / sizeof(meshlet->indices[0][0])]; + unsigned int indices[kMeshletMaxTriangles * 3]; - for (size_t i = 0; i < meshlet->triangle_count; ++i) + for (size_t i = 0; i < triangle_count * 3; ++i) { - unsigned int a = meshlet->vertices[meshlet->indices[i][0]]; - unsigned int b = meshlet->vertices[meshlet->indices[i][1]]; - unsigned int c = meshlet->vertices[meshlet->indices[i][2]]; + unsigned int index = meshlet_vertices[meshlet_triangles[i]]; + assert(index < vertex_count); - assert(a < vertex_count && b < vertex_count && c < vertex_count); - - indices[i * 3 + 0] = a; - indices[i * 3 + 1] = b; - indices[i * 3 + 2] = c; + indices[i] = index; } - return meshopt_computeClusterBounds(indices, meshlet->triangle_count * 3, vertex_positions, vertex_count, vertex_positions_stride); + return meshopt_computeClusterBounds(indices, triangle_count * 3, vertex_positions, vertex_count, vertex_positions_stride); } diff --git a/Source/ThirdParty/meshoptimizer/indexcodec.cpp b/Source/ThirdParty/meshoptimizer/indexcodec.cpp index eeb541e5b..4cc2fea63 100644 --- a/Source/ThirdParty/meshoptimizer/indexcodec.cpp +++ b/Source/ThirdParty/meshoptimizer/indexcodec.cpp @@ -4,14 +4,6 @@ #include #include -#ifndef TRACE -#define TRACE 0 -#endif - -#if TRACE -#include -#endif - // This work is based on: // Fabian Giesen. Simple lossless index buffer compression & follow-up. 2013 // Conor Stokes. Vertex Cache Optimised Index Buffer Compression. 2014 @@ -21,7 +13,7 @@ namespace meshopt const unsigned char kIndexHeader = 0xe0; const unsigned char kSequenceHeader = 0xd0; -static int gEncodeIndexVersion = 0; +static int gEncodeIndexVersion = 1; typedef unsigned int VertexFifo[16]; typedef unsigned int EdgeFifo[16][2]; @@ -116,7 +108,7 @@ static unsigned int decodeVByte(const unsigned char*& data) for (int i = 0; i < 4; ++i) { unsigned char group = *data++; - result |= (group & 127) << shift; + result |= unsigned(group & 127) << shift; shift += 7; if (group < 128) @@ -167,38 +159,6 @@ static void writeTriangle(void* destination, size_t offset, size_t index_size, u } } -#if TRACE -static size_t sortTop16(unsigned char dest[16], size_t stats[256]) -{ - size_t destsize = 0; - - for (size_t i = 0; i < 256; ++i) - { - size_t j = 0; - for (; j < destsize; ++j) - { - if (stats[i] >= stats[dest[j]]) - { - if (destsize < 16) - destsize++; - - memmove(&dest[j + 1], &dest[j], destsize - 1 - j); - dest[j] = (unsigned char)i; - break; - } - } - - if (j == destsize && destsize < 16) - { - dest[destsize] = (unsigned char)i; - destsize++; - } - } - - return destsize; -} -#endif - } // namespace meshopt size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t buffer_size, const unsigned int* indices, size_t index_count) @@ -207,11 +167,6 @@ size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t buffer_size, cons assert(index_count % 3 == 0); -#if TRACE - size_t codestats[256] = {}; - size_t codeauxstats[256] = {}; -#endif - // the minimum valid encoding is header, 1 byte per triangle and a 16-byte codeaux table if (buffer_size < 1 + index_count / 3 + 16) return 0; @@ -275,10 +230,6 @@ size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t buffer_size, cons *code++ = (unsigned char)((fe << 4) | fec); -#if TRACE - codestats[code[-1]]++; -#endif - // note that we need to update the last index since free indices are delta-encoded if (fec == 15) encodeIndex(data, c, last), last = c; @@ -334,11 +285,6 @@ size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t buffer_size, cons *data++ = codeaux; } -#if TRACE - codestats[code[-1]]++; - codeauxstats[codeaux]++; -#endif - // note that we need to update the last index since free indices are delta-encoded if (fea == 15) encodeIndex(data, a, last), last = a; @@ -387,30 +333,6 @@ size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t buffer_size, cons assert(data >= buffer + index_count / 3 + 16); assert(data <= buffer + buffer_size); -#if TRACE - unsigned char codetop[16], codeauxtop[16]; - size_t codetopsize = sortTop16(codetop, codestats); - size_t codeauxtopsize = sortTop16(codeauxtop, codeauxstats); - - size_t sumcode = 0, sumcodeaux = 0; - for (size_t i = 0; i < 256; ++i) - sumcode += codestats[i], sumcodeaux += codeauxstats[i]; - - size_t acccode = 0, acccodeaux = 0; - - printf("code\t\t\t\t\tcodeaux\n"); - - for (size_t i = 0; i < codetopsize && i < codeauxtopsize; ++i) - { - acccode += codestats[codetop[i]]; - acccodeaux += codeauxstats[codeauxtop[i]]; - - printf("%2d: %02x = %d (%.1f%% ..%.1f%%)\t\t%2d: %02x = %d (%.1f%% ..%.1f%%)\n", - int(i), codetop[i], int(codestats[codetop[i]]), double(codestats[codetop[i]]) / double(sumcode) * 100, double(acccode) / double(sumcode) * 100, - int(i), codeauxtop[i], int(codeauxstats[codeauxtop[i]]), double(codeauxstats[codeauxtop[i]]) / double(sumcodeaux) * 100, double(acccodeaux) / double(sumcodeaux) * 100); - } -#endif - return data - buffer; } diff --git a/Source/ThirdParty/meshoptimizer/indexgenerator.cpp b/Source/ThirdParty/meshoptimizer/indexgenerator.cpp index aa4a30efa..f6728345a 100644 --- a/Source/ThirdParty/meshoptimizer/indexgenerator.cpp +++ b/Source/ThirdParty/meshoptimizer/indexgenerator.cpp @@ -4,6 +4,8 @@ #include #include +// This work is based on: +// John McDonald, Mark Kilgard. Crack-Free Point-Normal Triangles using Adjacent Edge Normals. 2010 namespace meshopt { @@ -83,10 +85,49 @@ struct VertexStreamHasher } }; +struct EdgeHasher +{ + const unsigned int* remap; + + size_t hash(unsigned long long edge) const + { + unsigned int e0 = unsigned(edge >> 32); + unsigned int e1 = unsigned(edge); + + unsigned int h1 = remap[e0]; + unsigned int h2 = remap[e1]; + + const unsigned int m = 0x5bd1e995; + + // MurmurHash64B finalizer + h1 ^= h2 >> 18; + h1 *= m; + h2 ^= h1 >> 22; + h2 *= m; + h1 ^= h2 >> 17; + h1 *= m; + h2 ^= h1 >> 19; + h2 *= m; + + return h2; + } + + bool equal(unsigned long long lhs, unsigned long long rhs) const + { + unsigned int l0 = unsigned(lhs >> 32); + unsigned int l1 = unsigned(lhs); + + unsigned int r0 = unsigned(rhs >> 32); + unsigned int r1 = unsigned(rhs); + + return remap[l0] == remap[r0] && remap[l1] == remap[r1]; + } +}; + static size_t hashBuckets(size_t count) { size_t buckets = 1; - while (buckets < count) + while (buckets < count + count / 4) buckets *= 2; return buckets; @@ -116,7 +157,43 @@ static T* hashLookup(T* table, size_t buckets, const Hash& hash, const T& key, c } assert(false && "Hash table is full"); // unreachable - return 0; + return NULL; +} + +static void buildPositionRemap(unsigned int* remap, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, meshopt_Allocator& allocator) +{ + VertexHasher vertex_hasher = {reinterpret_cast(vertex_positions), 3 * sizeof(float), vertex_positions_stride}; + + size_t vertex_table_size = hashBuckets(vertex_count); + unsigned int* vertex_table = allocator.allocate(vertex_table_size); + memset(vertex_table, -1, vertex_table_size * sizeof(unsigned int)); + + for (size_t i = 0; i < vertex_count; ++i) + { + unsigned int index = unsigned(i); + unsigned int* entry = hashLookup(vertex_table, vertex_table_size, vertex_hasher, index, ~0u); + + if (*entry == ~0u) + *entry = index; + + remap[index] = *entry; + } + + allocator.deallocate(vertex_table); +} + +template +static void remapVertices(void* destination, const void* vertices, size_t vertex_count, size_t vertex_size, const unsigned int* remap) +{ + size_t block_size = BlockSize == 0 ? vertex_size : BlockSize; + assert(block_size == vertex_size); + + for (size_t i = 0; i < vertex_count; ++i) + if (remap[i] != ~0u) + { + assert(remap[i] < vertex_count); + memcpy(static_cast(destination) + remap[i] * block_size, static_cast(vertices) + i * block_size, block_size); + } } } // namespace meshopt @@ -126,7 +203,7 @@ size_t meshopt_generateVertexRemap(unsigned int* destination, const unsigned int using namespace meshopt; assert(indices || index_count == vertex_count); - assert(index_count % 3 == 0); + assert(!indices || index_count % 3 == 0); assert(vertex_size > 0 && vertex_size <= 256); meshopt_Allocator allocator; @@ -227,6 +304,8 @@ size_t meshopt_generateVertexRemapMulti(unsigned int* destination, const unsigne void meshopt_remapVertexBuffer(void* destination, const void* vertices, size_t vertex_count, size_t vertex_size, const unsigned int* remap) { + using namespace meshopt; + assert(vertex_size > 0 && vertex_size <= 256); meshopt_Allocator allocator; @@ -239,14 +318,23 @@ void meshopt_remapVertexBuffer(void* destination, const void* vertices, size_t v vertices = vertices_copy; } - for (size_t i = 0; i < vertex_count; ++i) + // specialize the loop for common vertex sizes to ensure memcpy is compiled as an inlined intrinsic + switch (vertex_size) { - if (remap[i] != ~0u) - { - assert(remap[i] < vertex_count); + case 4: + return remapVertices<4>(destination, vertices, vertex_count, vertex_size, remap); - memcpy(static_cast(destination) + remap[i] * vertex_size, static_cast(vertices) + i * vertex_size, vertex_size); - } + case 8: + return remapVertices<8>(destination, vertices, vertex_count, vertex_size, remap); + + case 12: + return remapVertices<12>(destination, vertices, vertex_count, vertex_size, remap); + + case 16: + return remapVertices<16>(destination, vertices, vertex_count, vertex_size, remap); + + default: + return remapVertices<0>(destination, vertices, vertex_count, vertex_size, remap); } } @@ -345,3 +433,146 @@ void meshopt_generateShadowIndexBufferMulti(unsigned int* destination, const uns destination[i] = remap[index]; } } + +void meshopt_generateAdjacencyIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) +{ + using namespace meshopt; + + assert(index_count % 3 == 0); + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); + assert(vertex_positions_stride % sizeof(float) == 0); + + meshopt_Allocator allocator; + + static const int next[4] = {1, 2, 0, 1}; + + // build position remap: for each vertex, which other (canonical) vertex does it map to? + unsigned int* remap = allocator.allocate(vertex_count); + buildPositionRemap(remap, vertex_positions, vertex_count, vertex_positions_stride, allocator); + + // build edge set; this stores all triangle edges but we can look these up by any other wedge + EdgeHasher edge_hasher = {remap}; + + size_t edge_table_size = hashBuckets(index_count); + unsigned long long* edge_table = allocator.allocate(edge_table_size); + unsigned int* edge_vertex_table = allocator.allocate(edge_table_size); + + memset(edge_table, -1, edge_table_size * sizeof(unsigned long long)); + memset(edge_vertex_table, -1, edge_table_size * sizeof(unsigned int)); + + for (size_t i = 0; i < index_count; i += 3) + { + for (int e = 0; e < 3; ++e) + { + unsigned int i0 = indices[i + e]; + unsigned int i1 = indices[i + next[e]]; + unsigned int i2 = indices[i + next[e + 1]]; + assert(i0 < vertex_count && i1 < vertex_count && i2 < vertex_count); + + unsigned long long edge = ((unsigned long long)i0 << 32) | i1; + unsigned long long* entry = hashLookup(edge_table, edge_table_size, edge_hasher, edge, ~0ull); + + if (*entry == ~0ull) + { + *entry = edge; + + // store vertex opposite to the edge + edge_vertex_table[entry - edge_table] = i2; + } + } + } + + // build resulting index buffer: 6 indices for each input triangle + for (size_t i = 0; i < index_count; i += 3) + { + unsigned int patch[6]; + + for (int e = 0; e < 3; ++e) + { + unsigned int i0 = indices[i + e]; + unsigned int i1 = indices[i + next[e]]; + assert(i0 < vertex_count && i1 < vertex_count); + + // note: this refers to the opposite edge! + unsigned long long edge = ((unsigned long long)i1 << 32) | i0; + unsigned long long* oppe = hashLookup(edge_table, edge_table_size, edge_hasher, edge, ~0ull); + + patch[e * 2 + 0] = i0; + patch[e * 2 + 1] = (*oppe == ~0ull) ? i0 : edge_vertex_table[oppe - edge_table]; + } + + memcpy(destination + i * 2, patch, sizeof(patch)); + } +} + +void meshopt_generateTessellationIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) +{ + using namespace meshopt; + + assert(index_count % 3 == 0); + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); + assert(vertex_positions_stride % sizeof(float) == 0); + + meshopt_Allocator allocator; + + static const int next[3] = {1, 2, 0}; + + // build position remap: for each vertex, which other (canonical) vertex does it map to? + unsigned int* remap = allocator.allocate(vertex_count); + buildPositionRemap(remap, vertex_positions, vertex_count, vertex_positions_stride, allocator); + + // build edge set; this stores all triangle edges but we can look these up by any other wedge + EdgeHasher edge_hasher = {remap}; + + size_t edge_table_size = hashBuckets(index_count); + unsigned long long* edge_table = allocator.allocate(edge_table_size); + memset(edge_table, -1, edge_table_size * sizeof(unsigned long long)); + + for (size_t i = 0; i < index_count; i += 3) + { + for (int e = 0; e < 3; ++e) + { + unsigned int i0 = indices[i + e]; + unsigned int i1 = indices[i + next[e]]; + assert(i0 < vertex_count && i1 < vertex_count); + + unsigned long long edge = ((unsigned long long)i0 << 32) | i1; + unsigned long long* entry = hashLookup(edge_table, edge_table_size, edge_hasher, edge, ~0ull); + + if (*entry == ~0ull) + *entry = edge; + } + } + + // build resulting index buffer: 12 indices for each input triangle + for (size_t i = 0; i < index_count; i += 3) + { + unsigned int patch[12]; + + for (int e = 0; e < 3; ++e) + { + unsigned int i0 = indices[i + e]; + unsigned int i1 = indices[i + next[e]]; + assert(i0 < vertex_count && i1 < vertex_count); + + // note: this refers to the opposite edge! + unsigned long long edge = ((unsigned long long)i1 << 32) | i0; + unsigned long long oppe = *hashLookup(edge_table, edge_table_size, edge_hasher, edge, ~0ull); + + // use the same edge if opposite edge doesn't exist (border) + oppe = (oppe == ~0ull) ? edge : oppe; + + // triangle index (0, 1, 2) + patch[e] = i0; + + // opposite edge (3, 4; 5, 6; 7, 8) + patch[3 + e * 2 + 0] = unsigned(oppe); + patch[3 + e * 2 + 1] = unsigned(oppe >> 32); + + // dominant vertex (9, 10, 11) + patch[9 + e] = remap[i0]; + } + + memcpy(destination + i * 4, patch, sizeof(patch)); + } +} diff --git a/Source/ThirdParty/meshoptimizer/meshoptimizer.h b/Source/ThirdParty/meshoptimizer/meshoptimizer.h index cb030ea29..dbafd4e6e 100644 --- a/Source/ThirdParty/meshoptimizer/meshoptimizer.h +++ b/Source/ThirdParty/meshoptimizer/meshoptimizer.h @@ -1,7 +1,7 @@ /** - * meshoptimizer - version 0.14 + * meshoptimizer - version 0.20 * - * Copyright (C) 2016-2020, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) + * Copyright (C) 2016-2023, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com) * Report bugs and download new versions at https://github.com/zeux/meshoptimizer * * This library is distributed under the MIT License. See notice at the end of this file. @@ -12,13 +12,22 @@ #include /* Version macro; major * 1000 + minor * 10 + patch */ -#define MESHOPTIMIZER_VERSION 140 +#define MESHOPTIMIZER_VERSION 200 /* 0.20 */ /* If no API is defined, assume default */ #ifndef MESHOPTIMIZER_API #define MESHOPTIMIZER_API #endif +/* Set the calling-convention for alloc/dealloc function pointers */ +#ifndef MESHOPTIMIZER_ALLOC_CALLCONV +#ifdef _MSC_VER +#define MESHOPTIMIZER_ALLOC_CALLCONV __cdecl +#else +#define MESHOPTIMIZER_ALLOC_CALLCONV +#endif +#endif + /* Experimental APIs have unstable interface and might have implementation that's not fully tested or optimized */ #define MESHOPTIMIZER_EXPERIMENTAL MESHOPTIMIZER_API @@ -28,8 +37,8 @@ extern "C" { #endif /** - * Vertex attribute stream, similar to glVertexPointer - * Each element takes size bytes, with stride controlling the spacing between successive elements. + * Vertex attribute stream + * Each element takes size bytes, beginning at data, with stride controlling the spacing between successive elements (stride >= size). */ struct meshopt_Stream { @@ -42,6 +51,7 @@ struct meshopt_Stream * Generates a vertex remap table from the vertex buffer and an optional index buffer and returns number of unique vertices * As a result, all vertices that are binary equivalent map to the same (new) location, with no gaps in the resulting sequence. * Resulting remap table maps old vertices to new vertices and can be used in meshopt_remapVertexBuffer/meshopt_remapIndexBuffer. + * Note that binary equivalence considers all vertex_size bytes, including padding which should be zero-initialized. * * destination must contain enough space for the resulting remap table (vertex_count elements) * indices can be NULL if the input is unindexed @@ -53,9 +63,11 @@ MESHOPTIMIZER_API size_t meshopt_generateVertexRemap(unsigned int* destination, * As a result, all vertices that are binary equivalent map to the same (new) location, with no gaps in the resulting sequence. * Resulting remap table maps old vertices to new vertices and can be used in meshopt_remapVertexBuffer/meshopt_remapIndexBuffer. * To remap vertex buffers, you will need to call meshopt_remapVertexBuffer for each vertex stream. + * Note that binary equivalence considers all size bytes in each stream, including padding which should be zero-initialized. * * destination must contain enough space for the resulting remap table (vertex_count elements) * indices can be NULL if the input is unindexed + * stream_count must be <= 16 */ MESHOPTIMIZER_API size_t meshopt_generateVertexRemapMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count); @@ -79,6 +91,7 @@ MESHOPTIMIZER_API void meshopt_remapIndexBuffer(unsigned int* destination, const * Generate index buffer that can be used for more efficient rendering when only a subset of the vertex attributes is necessary * All vertices that are binary equivalent (wrt first vertex_size bytes) map to the first vertex in the original vertex buffer. * This makes it possible to use the index buffer for Z pre-pass or shadowmap rendering, while using the original index buffer for regular rendering. + * Note that binary equivalence considers all vertex_size bytes, including padding which should be zero-initialized. * * destination must contain enough space for the resulting index buffer (index_count elements) */ @@ -88,11 +101,42 @@ MESHOPTIMIZER_API void meshopt_generateShadowIndexBuffer(unsigned int* destinati * Generate index buffer that can be used for more efficient rendering when only a subset of the vertex attributes is necessary * All vertices that are binary equivalent (wrt specified streams) map to the first vertex in the original vertex buffer. * This makes it possible to use the index buffer for Z pre-pass or shadowmap rendering, while using the original index buffer for regular rendering. + * Note that binary equivalence considers all size bytes in each stream, including padding which should be zero-initialized. * * destination must contain enough space for the resulting index buffer (index_count elements) + * stream_count must be <= 16 */ MESHOPTIMIZER_API void meshopt_generateShadowIndexBufferMulti(unsigned int* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, const struct meshopt_Stream* streams, size_t stream_count); +/** + * Generate index buffer that can be used as a geometry shader input with triangle adjacency topology + * Each triangle is converted into a 6-vertex patch with the following layout: + * - 0, 2, 4: original triangle vertices + * - 1, 3, 5: vertices adjacent to edges 02, 24 and 40 + * The resulting patch can be rendered with geometry shaders using e.g. VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST_WITH_ADJACENCY. + * This can be used to implement algorithms like silhouette detection/expansion and other forms of GS-driven rendering. + * + * destination must contain enough space for the resulting index buffer (index_count*2 elements) + * vertex_positions should have float3 position in the first 12 bytes of each vertex + */ +MESHOPTIMIZER_API void meshopt_generateAdjacencyIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); + +/** + * Generate index buffer that can be used for PN-AEN tessellation with crack-free displacement + * Each triangle is converted into a 12-vertex patch with the following layout: + * - 0, 1, 2: original triangle vertices + * - 3, 4: opposing edge for edge 0, 1 + * - 5, 6: opposing edge for edge 1, 2 + * - 7, 8: opposing edge for edge 2, 0 + * - 9, 10, 11: dominant vertices for corners 0, 1, 2 + * The resulting patch can be rendered with hardware tessellation using PN-AEN and displacement mapping. + * See "Tessellation on Any Budget" (John McDonald, GDC 2011) for implementation details. + * + * destination must contain enough space for the resulting index buffer (index_count*4 elements) + * vertex_positions should have float3 position in the first 12 bytes of each vertex + */ +MESHOPTIMIZER_API void meshopt_generateTessellationIndexBuffer(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); + /** * Vertex transform cache optimizer * Reorders indices to reduce the number of GPU vertex shader invocations @@ -129,7 +173,7 @@ MESHOPTIMIZER_API void meshopt_optimizeVertexCacheFifo(unsigned int* destination * * destination must contain enough space for the resulting index buffer (index_count elements) * indices must contain index data that is the result of meshopt_optimizeVertexCache (*not* the original mesh indices!) - * vertex_positions should have float3 position in the first 12 bytes of each vertex - similar to glVertexPointer + * vertex_positions should have float3 position in the first 12 bytes of each vertex * threshold indicates how much the overdraw optimizer can degrade vertex cache efficiency (1.05 = up to 5%) to reduce overdraw more efficiently */ MESHOPTIMIZER_API void meshopt_optimizeOverdraw(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, float threshold); @@ -168,10 +212,10 @@ MESHOPTIMIZER_API size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t MESHOPTIMIZER_API size_t meshopt_encodeIndexBufferBound(size_t index_count, size_t vertex_count); /** - * Experimental: Set index encoder format version + * Set index encoder format version * version must specify the data format version to encode; valid values are 0 (decodable by all library versions) and 1 (decodable by 0.14+) */ -MESHOPTIMIZER_EXPERIMENTAL void meshopt_encodeIndexVersion(int version); +MESHOPTIMIZER_API void meshopt_encodeIndexVersion(int version); /** * Index buffer decoder @@ -184,15 +228,15 @@ MESHOPTIMIZER_EXPERIMENTAL void meshopt_encodeIndexVersion(int version); MESHOPTIMIZER_API int meshopt_decodeIndexBuffer(void* destination, size_t index_count, size_t index_size, const unsigned char* buffer, size_t buffer_size); /** - * Experimental: Index sequence encoder + * Index sequence encoder * Encodes index sequence into an array of bytes that is generally smaller and compresses better compared to original. * Input index sequence can represent arbitrary topology; for triangle lists meshopt_encodeIndexBuffer is likely to be better. * Returns encoded data size on success, 0 on error; the only error condition is if buffer doesn't have enough space * * buffer must contain enough space for the encoded index sequence (use meshopt_encodeIndexSequenceBound to compute worst case size) */ -MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_encodeIndexSequence(unsigned char* buffer, size_t buffer_size, const unsigned int* indices, size_t index_count); -MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_encodeIndexSequenceBound(size_t index_count, size_t vertex_count); +MESHOPTIMIZER_API size_t meshopt_encodeIndexSequence(unsigned char* buffer, size_t buffer_size, const unsigned int* indices, size_t index_count); +MESHOPTIMIZER_API size_t meshopt_encodeIndexSequenceBound(size_t index_count, size_t vertex_count); /** * Index sequence decoder @@ -202,13 +246,14 @@ MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_encodeIndexSequenceBound(size_t index_ * * destination must contain enough space for the resulting index sequence (index_count elements) */ -MESHOPTIMIZER_EXPERIMENTAL int meshopt_decodeIndexSequence(void* destination, size_t index_count, size_t index_size, const unsigned char* buffer, size_t buffer_size); +MESHOPTIMIZER_API int meshopt_decodeIndexSequence(void* destination, size_t index_count, size_t index_size, const unsigned char* buffer, size_t buffer_size); /** * Vertex buffer encoder * Encodes vertex data into an array of bytes that is generally smaller and compresses better compared to original. * Returns encoded data size on success, 0 on error; the only error condition is if buffer doesn't have enough space * This function works for a single vertex stream; for multiple vertex streams, call meshopt_encodeVertexBuffer for each stream. + * Note that all vertex_size bytes of each vertex are encoded verbatim, including padding which should be zero-initialized. * * buffer must contain enough space for the encoded vertex buffer (use meshopt_encodeVertexBufferBound to compute worst case size) */ @@ -216,10 +261,10 @@ MESHOPTIMIZER_API size_t meshopt_encodeVertexBuffer(unsigned char* buffer, size_ MESHOPTIMIZER_API size_t meshopt_encodeVertexBufferBound(size_t vertex_count, size_t vertex_size); /** - * Experimental: Set vertex encoder format version + * Set vertex encoder format version * version must specify the data format version to encode; valid values are 0 (decodable by all library versions) */ -MESHOPTIMIZER_EXPERIMENTAL void meshopt_encodeVertexVersion(int version); +MESHOPTIMIZER_API void meshopt_encodeVertexVersion(int version); /** * Vertex buffer decoder @@ -234,7 +279,6 @@ MESHOPTIMIZER_API int meshopt_decodeVertexBuffer(void* destination, size_t verte /** * Vertex buffer filters * These functions can be used to filter output of meshopt_decodeVertexBuffer in-place. - * count must be aligned by 4 and stride is fixed for each function to facilitate SIMD implementation. * * meshopt_decodeFilterOct decodes octahedral encoding of a unit vector with K-bit (K <= 16) signed X/Y as an input; Z must store 1.0f. * Each component is stored as an 8-bit or 16-bit normalized integer; stride must be equal to 4 or 8. W is preserved as is. @@ -245,12 +289,51 @@ MESHOPTIMIZER_API int meshopt_decodeVertexBuffer(void* destination, size_t verte * meshopt_decodeFilterExp decodes exponential encoding of floating-point data with 8-bit exponent and 24-bit integer mantissa as 2^E*M. * Each 32-bit component is decoded in isolation; stride must be divisible by 4. */ -MESHOPTIMIZER_EXPERIMENTAL void meshopt_decodeFilterOct(void* buffer, size_t vertex_count, size_t vertex_size); -MESHOPTIMIZER_EXPERIMENTAL void meshopt_decodeFilterQuat(void* buffer, size_t vertex_count, size_t vertex_size); -MESHOPTIMIZER_EXPERIMENTAL void meshopt_decodeFilterExp(void* buffer, size_t vertex_count, size_t vertex_size); +MESHOPTIMIZER_EXPERIMENTAL void meshopt_decodeFilterOct(void* buffer, size_t count, size_t stride); +MESHOPTIMIZER_EXPERIMENTAL void meshopt_decodeFilterQuat(void* buffer, size_t count, size_t stride); +MESHOPTIMIZER_EXPERIMENTAL void meshopt_decodeFilterExp(void* buffer, size_t count, size_t stride); /** - * Experimental: Mesh simplifier + * Vertex buffer filter encoders + * These functions can be used to encode data in a format that meshopt_decodeFilter can decode + * + * meshopt_encodeFilterOct encodes unit vectors with K-bit (K <= 16) signed X/Y as an output. + * Each component is stored as an 8-bit or 16-bit normalized integer; stride must be equal to 4 or 8. W is preserved as is. + * Input data must contain 4 floats for every vector (count*4 total). + * + * meshopt_encodeFilterQuat encodes unit quaternions with K-bit (4 <= K <= 16) component encoding. + * Each component is stored as an 16-bit integer; stride must be equal to 8. + * Input data must contain 4 floats for every quaternion (count*4 total). + * + * meshopt_encodeFilterExp encodes arbitrary (finite) floating-point data with 8-bit exponent and K-bit integer mantissa (1 <= K <= 24). + * Exponent can be shared between all components of a given vector as defined by stride or all values of a given component; stride must be divisible by 4. + * Input data must contain stride/4 floats for every vector (count*stride/4 total). + */ +enum meshopt_EncodeExpMode +{ + /* When encoding exponents, use separate values for each component (maximum quality) */ + meshopt_EncodeExpSeparate, + /* When encoding exponents, use shared value for all components of each vector (better compression) */ + meshopt_EncodeExpSharedVector, + /* When encoding exponents, use shared value for each component of all vectors (best compression) */ + meshopt_EncodeExpSharedComponent, +}; + +MESHOPTIMIZER_EXPERIMENTAL void meshopt_encodeFilterOct(void* destination, size_t count, size_t stride, int bits, const float* data); +MESHOPTIMIZER_EXPERIMENTAL void meshopt_encodeFilterQuat(void* destination, size_t count, size_t stride, int bits, const float* data); +MESHOPTIMIZER_EXPERIMENTAL void meshopt_encodeFilterExp(void* destination, size_t count, size_t stride, int bits, const float* data, enum meshopt_EncodeExpMode mode); + +/** + * Simplification options + */ +enum +{ + /* Do not move vertices that are located on the topological border (vertices on triangle edges that don't have a paired triangle). Useful for simplifying portions of the larger mesh. */ + meshopt_SimplifyLockBorder = 1 << 0, +}; + +/** + * Mesh simplifier * Reduces the number of triangles in the mesh, attempting to preserve mesh appearance as much as possible * The algorithm tries to preserve mesh topology and can stop short of the target goal based on topology constraints or target error. * If not all attributes from the input mesh are required, it's recommended to reindex the mesh using meshopt_generateShadowIndexBuffer prior to simplification. @@ -258,23 +341,40 @@ MESHOPTIMIZER_EXPERIMENTAL void meshopt_decodeFilterExp(void* buffer, size_t ver * The resulting index buffer references vertices from the original vertex buffer. * If the original vertex data isn't required, creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended. * - * destination must contain enough space for the *source* index buffer (since optimization is iterative, this means index_count elements - *not* target_index_count!) - * vertex_positions should have float3 position in the first 12 bytes of each vertex - similar to glVertexPointer + * destination must contain enough space for the target index buffer, worst case is index_count elements (*not* target_index_count)! + * vertex_positions should have float3 position in the first 12 bytes of each vertex + * target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1] + * options must be a bitmask composed of meshopt_SimplifyX options; 0 is a safe default + * result_error can be NULL; when it's not NULL, it will contain the resulting (relative) error after simplification */ -MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplify(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error); +MESHOPTIMIZER_API size_t meshopt_simplify(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, unsigned int options, float* result_error); + +/** + * Experimental: Mesh simplifier with attribute metric + * The algorithm ehnahces meshopt_simplify by incorporating attribute values into the error metric used to prioritize simplification order; see meshopt_simplify documentation for details. + * Note that the number of attributes affects memory requirements and running time; this algorithm requires ~1.5x more memory and time compared to meshopt_simplify when using 4 scalar attributes. + * + * vertex_attributes should have attribute_count floats for each vertex + * attribute_weights should have attribute_count floats in total; the weights determine relative priority of attributes between each other and wrt position. The recommended weight range is [1e-3..1e-1], assuming attribute data is in [0..1] range. + * attribute_count must be <= 16 + * TODO target_error/result_error currently use combined distance+attribute error; this may change in the future + */ +MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifyWithAttributes(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_attributes, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, size_t target_index_count, float target_error, unsigned int options, float* result_error); /** * Experimental: Mesh simplifier (sloppy) - * Reduces the number of triangles in the mesh, sacrificing mesh apperance for simplification performance - * The algorithm doesn't preserve mesh topology but is always able to reach target triangle count. + * Reduces the number of triangles in the mesh, sacrificing mesh appearance for simplification performance + * The algorithm doesn't preserve mesh topology but can stop short of the target goal based on target error. * Returns the number of indices after simplification, with destination containing new index data * The resulting index buffer references vertices from the original vertex buffer. * If the original vertex data isn't required, creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended. * - * destination must contain enough space for the target index buffer - * vertex_positions should have float3 position in the first 12 bytes of each vertex - similar to glVertexPointer + * destination must contain enough space for the target index buffer, worst case is index_count elements (*not* target_index_count)! + * vertex_positions should have float3 position in the first 12 bytes of each vertex + * target_error represents the error relative to mesh extents that can be tolerated, e.g. 0.01 = 1% deformation; value range [0..1] + * result_error can be NULL; when it's not NULL, it will contain the resulting (relative) error after simplification */ -MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count); +MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, float* result_error); /** * Experimental: Point cloud simplifier @@ -283,10 +383,19 @@ MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifySloppy(unsigned int* destinati * The resulting index buffer references vertices from the original vertex buffer. * If the original vertex data isn't required, creating a compact vertex buffer using meshopt_optimizeVertexFetch is recommended. * - * destination must contain enough space for the target index buffer - * vertex_positions should have float3 position in the first 12 bytes of each vertex - similar to glVertexPointer + * destination must contain enough space for the target index buffer (target_vertex_count elements) + * vertex_positions should have float3 position in the first 12 bytes of each vertex + * vertex_colors should can be NULL; when it's not NULL, it should have float3 color in the first 12 bytes of each vertex */ -MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifyPoints(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_vertex_count); +MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_simplifyPoints(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_colors, size_t vertex_colors_stride, float color_weight, size_t target_vertex_count); + +/** + * Returns the error scaling factor used by the simplifier to convert between absolute and relative extents + * + * Absolute error must be *divided* by the scaling factor before passing it to meshopt_simplify as target_error + * Relative error returned by meshopt_simplify via result_error must be *multiplied* by the scaling factor to get absolute error. + */ +MESHOPTIMIZER_API float meshopt_simplifyScale(const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); /** * Mesh stripifier @@ -338,7 +447,7 @@ struct meshopt_OverdrawStatistics * Returns overdraw statistics using a software rasterizer * Results may not match actual GPU performance * - * vertex_positions should have float3 position in the first 12 bytes of each vertex - similar to glVertexPointer + * vertex_positions should have float3 position in the first 12 bytes of each vertex */ MESHOPTIMIZER_API struct meshopt_OverdrawStatistics meshopt_analyzeOverdraw(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); @@ -357,23 +466,32 @@ MESHOPTIMIZER_API struct meshopt_VertexFetchStatistics meshopt_analyzeVertexFetc struct meshopt_Meshlet { - unsigned int vertices[64]; - unsigned char indices[126][3]; - unsigned char triangle_count; - unsigned char vertex_count; + /* offsets within meshlet_vertices and meshlet_triangles arrays with meshlet data */ + unsigned int vertex_offset; + unsigned int triangle_offset; + + /* number of vertices and triangles used in the meshlet; data is stored in consecutive range defined by offset and count */ + unsigned int vertex_count; + unsigned int triangle_count; }; /** - * Experimental: Meshlet builder + * Meshlet builder * Splits the mesh into a set of meshlets where each meshlet has a micro index buffer indexing into meshlet vertices that refer to the original vertex buffer * The resulting data can be used to render meshes using NVidia programmable mesh shading pipeline, or in other cluster-based renderers. - * For maximum efficiency the index buffer being converted has to be optimized for vertex cache first. + * When using buildMeshlets, vertex positions need to be provided to minimize the size of the resulting clusters. + * When using buildMeshletsScan, for maximum efficiency the index buffer being converted has to be optimized for vertex cache first. * - * destination must contain enough space for all meshlets, worst case size can be computed with meshopt_buildMeshletsBound - * max_vertices and max_triangles can't exceed limits statically declared in meshopt_Meshlet (max_vertices <= 64, max_triangles <= 126) + * meshlets must contain enough space for all meshlets, worst case size can be computed with meshopt_buildMeshletsBound + * meshlet_vertices must contain enough space for all meshlets, worst case size is equal to max_meshlets * max_vertices + * meshlet_triangles must contain enough space for all meshlets, worst case size is equal to max_meshlets * max_triangles * 3 + * vertex_positions should have float3 position in the first 12 bytes of each vertex + * max_vertices and max_triangles must not exceed implementation limits (max_vertices <= 255 - not 256!, max_triangles <= 512) + * cone_weight should be set to 0 when cone culling is not used, and a value between 0 and 1 otherwise to balance between cluster size and cone culling efficiency */ -MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_buildMeshlets(struct meshopt_Meshlet* destination, const unsigned int* indices, size_t index_count, size_t vertex_count, size_t max_vertices, size_t max_triangles); -MESHOPTIMIZER_EXPERIMENTAL size_t meshopt_buildMeshletsBound(size_t index_count, size_t max_vertices, size_t max_triangles); +MESHOPTIMIZER_API size_t meshopt_buildMeshlets(struct meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t max_triangles, float cone_weight); +MESHOPTIMIZER_API size_t meshopt_buildMeshletsScan(struct meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const unsigned int* indices, size_t index_count, size_t vertex_count, size_t max_vertices, size_t max_triangles); +MESHOPTIMIZER_API size_t meshopt_buildMeshletsBound(size_t index_count, size_t max_vertices, size_t max_triangles); struct meshopt_Bounds { @@ -392,13 +510,13 @@ struct meshopt_Bounds }; /** - * Experimental: Cluster bounds generator + * Cluster bounds generator * Creates bounding volumes that can be used for frustum, backface and occlusion culling. * * For backface culling with orthographic projection, use the following formula to reject backfacing clusters: * dot(view, cone_axis) >= cone_cutoff * - * For perspective projection, you can the formula that needs cone apex in addition to axis & cutoff: + * For perspective projection, you can use the formula that needs cone apex in addition to axis & cutoff: * dot(normalize(cone_apex - camera_position), cone_axis) >= cone_cutoff * * Alternatively, you can use the formula that doesn't need cone apex and uses bounding sphere instead: @@ -407,29 +525,31 @@ struct meshopt_Bounds * dot(center - camera_position, cone_axis) >= cone_cutoff * length(center - camera_position) + radius * * The formula that uses the apex is slightly more accurate but needs the apex; if you are already using bounding sphere - * to do frustum/occlusion culling, the formula that doesn't use the apex may be preferable. + * to do frustum/occlusion culling, the formula that doesn't use the apex may be preferable (for derivation see + * Real-Time Rendering 4th Edition, section 19.3). * - * vertex_positions should have float3 position in the first 12 bytes of each vertex - similar to glVertexPointer - * index_count should be less than or equal to 256*3 (the function assumes clusters of limited size) + * vertex_positions should have float3 position in the first 12 bytes of each vertex + * index_count/3 should be less than or equal to 512 (the function assumes clusters of limited size) */ -MESHOPTIMIZER_EXPERIMENTAL struct meshopt_Bounds meshopt_computeClusterBounds(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); -MESHOPTIMIZER_EXPERIMENTAL struct meshopt_Bounds meshopt_computeMeshletBounds(const struct meshopt_Meshlet* meshlet, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); +MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeClusterBounds(const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); +MESHOPTIMIZER_API struct meshopt_Bounds meshopt_computeMeshletBounds(const unsigned int* meshlet_vertices, const unsigned char* meshlet_triangles, size_t triangle_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); /** - * Experimental: Spatial sorter + * Spatial sorter * Generates a remap table that can be used to reorder points for spatial locality. * Resulting remap table maps old vertices to new vertices and can be used in meshopt_remapVertexBuffer. * * destination must contain enough space for the resulting remap table (vertex_count elements) + * vertex_positions should have float3 position in the first 12 bytes of each vertex */ -MESHOPTIMIZER_EXPERIMENTAL void meshopt_spatialSortRemap(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); +MESHOPTIMIZER_API void meshopt_spatialSortRemap(unsigned int* destination, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); /** * Experimental: Spatial sorter * Reorders triangles for spatial locality, and generates a new index buffer. The resulting index buffer can be used with other functions like optimizeVertexCache. * * destination must contain enough space for the resulting index buffer (index_count elements) - * vertex_positions should have float3 position in the first 12 bytes of each vertex - similar to glVertexPointer + * vertex_positions should have float3 position in the first 12 bytes of each vertex */ MESHOPTIMIZER_EXPERIMENTAL void meshopt_spatialSortTriangles(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); @@ -439,7 +559,7 @@ MESHOPTIMIZER_EXPERIMENTAL void meshopt_spatialSortTriangles(unsigned int* desti * Note that all algorithms only allocate memory for temporary use. * allocate/deallocate are always called in a stack-like order - last pointer to be allocated is deallocated first. */ -MESHOPTIMIZER_API void meshopt_setAllocator(void* (*allocate)(size_t), void (*deallocate)(void*)); +MESHOPTIMIZER_API void meshopt_setAllocator(void* (MESHOPTIMIZER_ALLOC_CALLCONV *allocate)(size_t), void (MESHOPTIMIZER_ALLOC_CALLCONV *deallocate)(void*)); #ifdef __cplusplus } /* extern "C" */ @@ -462,19 +582,25 @@ inline int meshopt_quantizeUnorm(float v, int N); inline int meshopt_quantizeSnorm(float v, int N); /** - * Quantize a float into half-precision floating point value + * Quantize a float into half-precision (as defined by IEEE-754 fp16) floating point value * Generates +-inf for overflow, preserves NaN, flushes denormals to zero, rounds to nearest * Representable magnitude range: [6e-5; 65504] * Maximum relative reconstruction error: 5e-4 */ -inline unsigned short meshopt_quantizeHalf(float v); +MESHOPTIMIZER_API unsigned short meshopt_quantizeHalf(float v); /** - * Quantize a float into a floating point value with a limited number of significant mantissa bits + * Quantize a float into a floating point value with a limited number of significant mantissa bits, preserving the IEEE-754 fp32 binary representation * Generates +-inf for overflow, preserves NaN, flushes denormals to zero, rounds to nearest * Assumes N is in a valid mantissa precision range, which is 1..23 */ -inline float meshopt_quantizeFloat(float v, int N); +MESHOPTIMIZER_API float meshopt_quantizeFloat(float v, int N); + +/** + * Reverse quantization of a half-precision (as defined by IEEE-754 fp16) floating point value + * Preserves Inf/NaN, flushes denormals to zero + */ +MESHOPTIMIZER_API float meshopt_dequantizeHalf(unsigned short h); #endif /** @@ -497,6 +623,10 @@ inline void meshopt_generateShadowIndexBuffer(T* destination, const T* indices, template inline void meshopt_generateShadowIndexBufferMulti(T* destination, const T* indices, size_t index_count, size_t vertex_count, const meshopt_Stream* streams, size_t stream_count); template +inline void meshopt_generateAdjacencyIndexBuffer(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); +template +inline void meshopt_generateTessellationIndexBuffer(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); +template inline void meshopt_optimizeVertexCache(T* destination, const T* indices, size_t index_count, size_t vertex_count); template inline void meshopt_optimizeVertexCacheStrip(T* destination, const T* indices, size_t index_count, size_t vertex_count); @@ -517,9 +647,11 @@ inline size_t meshopt_encodeIndexSequence(unsigned char* buffer, size_t buffer_s template inline int meshopt_decodeIndexSequence(T* destination, size_t index_count, const unsigned char* buffer, size_t buffer_size); template -inline size_t meshopt_simplify(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error); +inline size_t meshopt_simplify(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, unsigned int options = 0, float* result_error = NULL); template -inline size_t meshopt_simplifySloppy(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count); +inline size_t meshopt_simplifyWithAttributes(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_attributes, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, size_t target_index_count, float target_error, unsigned int options = 0, float* result_error = NULL); +template +inline size_t meshopt_simplifySloppy(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, float* result_error = NULL); template inline size_t meshopt_stripify(T* destination, const T* indices, size_t index_count, size_t vertex_count, T restart_index); template @@ -531,7 +663,9 @@ inline meshopt_OverdrawStatistics meshopt_analyzeOverdraw(const T* indices, size template inline meshopt_VertexFetchStatistics meshopt_analyzeVertexFetch(const T* indices, size_t index_count, size_t vertex_count, size_t vertex_size); template -inline size_t meshopt_buildMeshlets(meshopt_Meshlet* destination, const T* indices, size_t index_count, size_t vertex_count, size_t max_vertices, size_t max_triangles); +inline size_t meshopt_buildMeshlets(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t max_triangles, float cone_weight); +template +inline size_t meshopt_buildMeshletsScan(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const T* indices, size_t index_count, size_t vertex_count, size_t max_vertices, size_t max_triangles); template inline meshopt_Bounds meshopt_computeClusterBounds(const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride); template @@ -561,50 +695,6 @@ inline int meshopt_quantizeSnorm(float v, int N) return int(v * scale + round); } - -inline unsigned short meshopt_quantizeHalf(float v) -{ - union { float f; unsigned int ui; } u = {v}; - unsigned int ui = u.ui; - - int s = (ui >> 16) & 0x8000; - int em = ui & 0x7fffffff; - - /* bias exponent and round to nearest; 112 is relative exponent bias (127-15) */ - int h = (em - (112 << 23) + (1 << 12)) >> 13; - - /* underflow: flush to zero; 113 encodes exponent -14 */ - h = (em < (113 << 23)) ? 0 : h; - - /* overflow: infinity; 143 encodes exponent 16 */ - h = (em >= (143 << 23)) ? 0x7c00 : h; - - /* NaN; note that we convert all types of NaN to qNaN */ - h = (em > (255 << 23)) ? 0x7e00 : h; - - return (unsigned short)(s | h); -} - -inline float meshopt_quantizeFloat(float v, int N) -{ - union { float f; unsigned int ui; } u = {v}; - unsigned int ui = u.ui; - - const int mask = (1 << (23 - N)) - 1; - const int round = (1 << (23 - N)) >> 1; - - int e = ui & 0x7f800000; - unsigned int rui = (ui + round) & ~mask; - - /* round all numbers except inf/nan; this is important to make sure nan doesn't overflow into -0 */ - ui = e == 0x7f800000 ? ui : rui; - - /* flush denormals to zero */ - ui = e == 0 ? 0 : ui; - - u.ui = ui; - return u.f; -} #endif /* Internal implementation helpers */ @@ -615,8 +705,8 @@ public: template struct StorageT { - static void* (*allocate)(size_t); - static void (*deallocate)(void*); + static void* (MESHOPTIMIZER_ALLOC_CALLCONV *allocate)(size_t); + static void (MESHOPTIMIZER_ALLOC_CALLCONV *deallocate)(void*); }; typedef StorageT Storage; @@ -641,14 +731,21 @@ public: return result; } + void deallocate(void* ptr) + { + assert(count > 0 && blocks[count - 1] == ptr); + Storage::deallocate(ptr); + count--; + } + private: - void* blocks[16]; + void* blocks[24]; size_t count; }; // This makes sure that allocate/deallocate are lazily generated in translation units that need them and are deduplicated by the linker -template void* (*meshopt_Allocator::StorageT::allocate)(size_t) = operator new; -template void (*meshopt_Allocator::StorageT::deallocate)(void*) = operator delete; +template void* (MESHOPTIMIZER_ALLOC_CALLCONV *meshopt_Allocator::StorageT::allocate)(size_t) = operator new; +template void (MESHOPTIMIZER_ALLOC_CALLCONV *meshopt_Allocator::StorageT::deallocate)(void*) = operator delete; #endif /* Inline implementation for C++ templated wrappers */ @@ -665,7 +762,7 @@ struct meshopt_IndexAdapter meshopt_IndexAdapter(T* result_, const T* input, size_t count_) : result(result_) - , data(0) + , data(NULL) , count(count_) { size_t size = count > size_t(-1) / sizeof(unsigned int) ? size_t(-1) : count * sizeof(unsigned int); @@ -705,33 +802,33 @@ struct meshopt_IndexAdapter template inline size_t meshopt_generateVertexRemap(unsigned int* destination, const T* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size) { - meshopt_IndexAdapter in(0, indices, indices ? index_count : 0); + meshopt_IndexAdapter in(NULL, indices, indices ? index_count : 0); - return meshopt_generateVertexRemap(destination, indices ? in.data : 0, index_count, vertices, vertex_count, vertex_size); + return meshopt_generateVertexRemap(destination, indices ? in.data : NULL, index_count, vertices, vertex_count, vertex_size); } template inline size_t meshopt_generateVertexRemapMulti(unsigned int* destination, const T* indices, size_t index_count, size_t vertex_count, const meshopt_Stream* streams, size_t stream_count) { - meshopt_IndexAdapter in(0, indices, indices ? index_count : 0); + meshopt_IndexAdapter in(NULL, indices, indices ? index_count : 0); - return meshopt_generateVertexRemapMulti(destination, indices ? in.data : 0, index_count, vertex_count, streams, stream_count); + return meshopt_generateVertexRemapMulti(destination, indices ? in.data : NULL, index_count, vertex_count, streams, stream_count); } template inline void meshopt_remapIndexBuffer(T* destination, const T* indices, size_t index_count, const unsigned int* remap) { - meshopt_IndexAdapter in(0, indices, indices ? index_count : 0); + meshopt_IndexAdapter in(NULL, indices, indices ? index_count : 0); meshopt_IndexAdapter out(destination, 0, index_count); - meshopt_remapIndexBuffer(out.data, indices ? in.data : 0, index_count, remap); + meshopt_remapIndexBuffer(out.data, indices ? in.data : NULL, index_count, remap); } template inline void meshopt_generateShadowIndexBuffer(T* destination, const T* indices, size_t index_count, const void* vertices, size_t vertex_count, size_t vertex_size, size_t vertex_stride) { - meshopt_IndexAdapter in(0, indices, index_count); - meshopt_IndexAdapter out(destination, 0, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count); meshopt_generateShadowIndexBuffer(out.data, in.data, index_count, vertices, vertex_count, vertex_size, vertex_stride); } @@ -739,17 +836,35 @@ inline void meshopt_generateShadowIndexBuffer(T* destination, const T* indices, template inline void meshopt_generateShadowIndexBufferMulti(T* destination, const T* indices, size_t index_count, size_t vertex_count, const meshopt_Stream* streams, size_t stream_count) { - meshopt_IndexAdapter in(0, indices, index_count); - meshopt_IndexAdapter out(destination, 0, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count); meshopt_generateShadowIndexBufferMulti(out.data, in.data, index_count, vertex_count, streams, stream_count); } +template +inline void meshopt_generateAdjacencyIndexBuffer(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) +{ + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count * 2); + + meshopt_generateAdjacencyIndexBuffer(out.data, in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride); +} + +template +inline void meshopt_generateTessellationIndexBuffer(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) +{ + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count * 4); + + meshopt_generateTessellationIndexBuffer(out.data, in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride); +} + template inline void meshopt_optimizeVertexCache(T* destination, const T* indices, size_t index_count, size_t vertex_count) { - meshopt_IndexAdapter in(0, indices, index_count); - meshopt_IndexAdapter out(destination, 0, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count); meshopt_optimizeVertexCache(out.data, in.data, index_count, vertex_count); } @@ -757,8 +872,8 @@ inline void meshopt_optimizeVertexCache(T* destination, const T* indices, size_t template inline void meshopt_optimizeVertexCacheStrip(T* destination, const T* indices, size_t index_count, size_t vertex_count) { - meshopt_IndexAdapter in(0, indices, index_count); - meshopt_IndexAdapter out(destination, 0, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count); meshopt_optimizeVertexCacheStrip(out.data, in.data, index_count, vertex_count); } @@ -766,8 +881,8 @@ inline void meshopt_optimizeVertexCacheStrip(T* destination, const T* indices, s template inline void meshopt_optimizeVertexCacheFifo(T* destination, const T* indices, size_t index_count, size_t vertex_count, unsigned int cache_size) { - meshopt_IndexAdapter in(0, indices, index_count); - meshopt_IndexAdapter out(destination, 0, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count); meshopt_optimizeVertexCacheFifo(out.data, in.data, index_count, vertex_count, cache_size); } @@ -775,8 +890,8 @@ inline void meshopt_optimizeVertexCacheFifo(T* destination, const T* indices, si template inline void meshopt_optimizeOverdraw(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, float threshold) { - meshopt_IndexAdapter in(0, indices, index_count); - meshopt_IndexAdapter out(destination, 0, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count); meshopt_optimizeOverdraw(out.data, in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride, threshold); } @@ -784,7 +899,7 @@ inline void meshopt_optimizeOverdraw(T* destination, const T* indices, size_t in template inline size_t meshopt_optimizeVertexFetchRemap(unsigned int* destination, const T* indices, size_t index_count, size_t vertex_count) { - meshopt_IndexAdapter in(0, indices, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); return meshopt_optimizeVertexFetchRemap(destination, in.data, index_count, vertex_count); } @@ -800,7 +915,7 @@ inline size_t meshopt_optimizeVertexFetch(void* destination, T* indices, size_t template inline size_t meshopt_encodeIndexBuffer(unsigned char* buffer, size_t buffer_size, const T* indices, size_t index_count) { - meshopt_IndexAdapter in(0, indices, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); return meshopt_encodeIndexBuffer(buffer, buffer_size, in.data, index_count); } @@ -817,7 +932,7 @@ inline int meshopt_decodeIndexBuffer(T* destination, size_t index_count, const u template inline size_t meshopt_encodeIndexSequence(unsigned char* buffer, size_t buffer_size, const T* indices, size_t index_count) { - meshopt_IndexAdapter in(0, indices, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); return meshopt_encodeIndexSequence(buffer, buffer_size, in.data, index_count); } @@ -832,28 +947,37 @@ inline int meshopt_decodeIndexSequence(T* destination, size_t index_count, const } template -inline size_t meshopt_simplify(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error) +inline size_t meshopt_simplify(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, unsigned int options, float* result_error) { - meshopt_IndexAdapter in(0, indices, index_count); - meshopt_IndexAdapter out(destination, 0, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count); - return meshopt_simplify(out.data, in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride, target_index_count, target_error); + return meshopt_simplify(out.data, in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride, target_index_count, target_error, options, result_error); } template -inline size_t meshopt_simplifySloppy(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count) +inline size_t meshopt_simplifyWithAttributes(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_attributes, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, size_t target_index_count, float target_error, unsigned int options, float* result_error) { - meshopt_IndexAdapter in(0, indices, index_count); - meshopt_IndexAdapter out(destination, 0, target_index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count); - return meshopt_simplifySloppy(out.data, in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride, target_index_count); + return meshopt_simplifyWithAttributes(out.data, in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride, vertex_attributes, vertex_attributes_stride, attribute_weights, attribute_count, target_index_count, target_error, options, result_error); +} + +template +inline size_t meshopt_simplifySloppy(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, float* result_error) +{ + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count); + + return meshopt_simplifySloppy(out.data, in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride, target_index_count, target_error, result_error); } template inline size_t meshopt_stripify(T* destination, const T* indices, size_t index_count, size_t vertex_count, T restart_index) { - meshopt_IndexAdapter in(0, indices, index_count); - meshopt_IndexAdapter out(destination, 0, (index_count / 3) * 5); + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, (index_count / 3) * 5); return meshopt_stripify(out.data, in.data, index_count, vertex_count, unsigned(restart_index)); } @@ -861,8 +985,8 @@ inline size_t meshopt_stripify(T* destination, const T* indices, size_t index_co template inline size_t meshopt_unstripify(T* destination, const T* indices, size_t index_count, T restart_index) { - meshopt_IndexAdapter in(0, indices, index_count); - meshopt_IndexAdapter out(destination, 0, (index_count - 2) * 3); + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, (index_count - 2) * 3); return meshopt_unstripify(out.data, in.data, index_count, unsigned(restart_index)); } @@ -870,7 +994,7 @@ inline size_t meshopt_unstripify(T* destination, const T* indices, size_t index_ template inline meshopt_VertexCacheStatistics meshopt_analyzeVertexCache(const T* indices, size_t index_count, size_t vertex_count, unsigned int cache_size, unsigned int warp_size, unsigned int buffer_size) { - meshopt_IndexAdapter in(0, indices, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); return meshopt_analyzeVertexCache(in.data, index_count, vertex_count, cache_size, warp_size, buffer_size); } @@ -878,7 +1002,7 @@ inline meshopt_VertexCacheStatistics meshopt_analyzeVertexCache(const T* indices template inline meshopt_OverdrawStatistics meshopt_analyzeOverdraw(const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) { - meshopt_IndexAdapter in(0, indices, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); return meshopt_analyzeOverdraw(in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride); } @@ -886,23 +1010,31 @@ inline meshopt_OverdrawStatistics meshopt_analyzeOverdraw(const T* indices, size template inline meshopt_VertexFetchStatistics meshopt_analyzeVertexFetch(const T* indices, size_t index_count, size_t vertex_count, size_t vertex_size) { - meshopt_IndexAdapter in(0, indices, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); return meshopt_analyzeVertexFetch(in.data, index_count, vertex_count, vertex_size); } template -inline size_t meshopt_buildMeshlets(meshopt_Meshlet* destination, const T* indices, size_t index_count, size_t vertex_count, size_t max_vertices, size_t max_triangles) +inline size_t meshopt_buildMeshlets(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride, size_t max_vertices, size_t max_triangles, float cone_weight) { - meshopt_IndexAdapter in(0, indices, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); - return meshopt_buildMeshlets(destination, in.data, index_count, vertex_count, max_vertices, max_triangles); + return meshopt_buildMeshlets(meshlets, meshlet_vertices, meshlet_triangles, in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride, max_vertices, max_triangles, cone_weight); +} + +template +inline size_t meshopt_buildMeshletsScan(meshopt_Meshlet* meshlets, unsigned int* meshlet_vertices, unsigned char* meshlet_triangles, const T* indices, size_t index_count, size_t vertex_count, size_t max_vertices, size_t max_triangles) +{ + meshopt_IndexAdapter in(NULL, indices, index_count); + + return meshopt_buildMeshletsScan(meshlets, meshlet_vertices, meshlet_triangles, in.data, index_count, vertex_count, max_vertices, max_triangles); } template inline meshopt_Bounds meshopt_computeClusterBounds(const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) { - meshopt_IndexAdapter in(0, indices, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); return meshopt_computeClusterBounds(in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride); } @@ -910,15 +1042,15 @@ inline meshopt_Bounds meshopt_computeClusterBounds(const T* indices, size_t inde template inline void meshopt_spatialSortTriangles(T* destination, const T* indices, size_t index_count, const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) { - meshopt_IndexAdapter in(0, indices, index_count); - meshopt_IndexAdapter out(destination, 0, index_count); + meshopt_IndexAdapter in(NULL, indices, index_count); + meshopt_IndexAdapter out(destination, NULL, index_count); meshopt_spatialSortTriangles(out.data, in.data, index_count, vertex_positions, vertex_count, vertex_positions_stride); } #endif /** - * Copyright (c) 2016-2020 Arseny Kapoulkine + * Copyright (c) 2016-2023 Arseny Kapoulkine * * Permission is hereby granted, free of charge, to any person * obtaining a copy of this software and associated documentation diff --git a/Source/ThirdParty/meshoptimizer/overdrawanalyzer.cpp b/Source/ThirdParty/meshoptimizer/overdrawanalyzer.cpp index 8d5859ba3..8b6f25413 100644 --- a/Source/ThirdParty/meshoptimizer/overdrawanalyzer.cpp +++ b/Source/ThirdParty/meshoptimizer/overdrawanalyzer.cpp @@ -147,7 +147,7 @@ meshopt_OverdrawStatistics meshopt_analyzeOverdraw(const unsigned int* indices, using namespace meshopt; assert(index_count % 3 == 0); - assert(vertex_positions_stride > 0 && vertex_positions_stride <= 256); + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); assert(vertex_positions_stride % sizeof(float) == 0); meshopt_Allocator allocator; diff --git a/Source/ThirdParty/meshoptimizer/overdrawoptimizer.cpp b/Source/ThirdParty/meshoptimizer/overdrawoptimizer.cpp index 143656ed7..cc22dbcff 100644 --- a/Source/ThirdParty/meshoptimizer/overdrawoptimizer.cpp +++ b/Source/ThirdParty/meshoptimizer/overdrawoptimizer.cpp @@ -272,7 +272,7 @@ void meshopt_optimizeOverdraw(unsigned int* destination, const unsigned int* ind using namespace meshopt; assert(index_count % 3 == 0); - assert(vertex_positions_stride > 0 && vertex_positions_stride <= 256); + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); assert(vertex_positions_stride % sizeof(float) == 0); meshopt_Allocator allocator; diff --git a/Source/ThirdParty/meshoptimizer/quantization.cpp b/Source/ThirdParty/meshoptimizer/quantization.cpp new file mode 100644 index 000000000..09a314d60 --- /dev/null +++ b/Source/ThirdParty/meshoptimizer/quantization.cpp @@ -0,0 +1,70 @@ +// This file is part of meshoptimizer library; see meshoptimizer.h for version/license details +#include "meshoptimizer.h" + +#include + +unsigned short meshopt_quantizeHalf(float v) +{ + union { float f; unsigned int ui; } u = {v}; + unsigned int ui = u.ui; + + int s = (ui >> 16) & 0x8000; + int em = ui & 0x7fffffff; + + // bias exponent and round to nearest; 112 is relative exponent bias (127-15) + int h = (em - (112 << 23) + (1 << 12)) >> 13; + + // underflow: flush to zero; 113 encodes exponent -14 + h = (em < (113 << 23)) ? 0 : h; + + // overflow: infinity; 143 encodes exponent 16 + h = (em >= (143 << 23)) ? 0x7c00 : h; + + // NaN; note that we convert all types of NaN to qNaN + h = (em > (255 << 23)) ? 0x7e00 : h; + + return (unsigned short)(s | h); +} + +float meshopt_quantizeFloat(float v, int N) +{ + assert(N >= 0 && N <= 23); + + union { float f; unsigned int ui; } u = {v}; + unsigned int ui = u.ui; + + const int mask = (1 << (23 - N)) - 1; + const int round = (1 << (23 - N)) >> 1; + + int e = ui & 0x7f800000; + unsigned int rui = (ui + round) & ~mask; + + // round all numbers except inf/nan; this is important to make sure nan doesn't overflow into -0 + ui = e == 0x7f800000 ? ui : rui; + + // flush denormals to zero + ui = e == 0 ? 0 : ui; + + u.ui = ui; + return u.f; +} + +float meshopt_dequantizeHalf(unsigned short h) +{ + unsigned int s = unsigned(h & 0x8000) << 16; + int em = h & 0x7fff; + + // bias exponent and pad mantissa with 0; 112 is relative exponent bias (127-15) + int r = (em + (112 << 10)) << 13; + + // denormal: flush to zero + r = (em < (1 << 10)) ? 0 : r; + + // infinity/NaN; note that we preserve NaN payload as a byproduct of unifying inf/nan cases + // 112 is an exponent bias fixup; since we already applied it once, applying it twice converts 31 to 255 + r += (em >= (31 << 10)) ? (112 << 23) : 0; + + union { float f; unsigned int ui; } u; + u.ui = s | r; + return u.f; +} diff --git a/Source/ThirdParty/meshoptimizer/simplifier.cpp b/Source/ThirdParty/meshoptimizer/simplifier.cpp index dd0ff9b07..5ba857007 100644 --- a/Source/ThirdParty/meshoptimizer/simplifier.cpp +++ b/Source/ThirdParty/meshoptimizer/simplifier.cpp @@ -14,39 +14,55 @@ #include #endif +#if TRACE +#define TRACESTATS(i) stats[i]++; +#else +#define TRACESTATS(i) (void)0 +#endif + // This work is based on: // Michael Garland and Paul S. Heckbert. Surface simplification using quadric error metrics. 1997 // Michael Garland. Quadric-based polygonal surface simplification. 1999 // Peter Lindstrom. Out-of-Core Simplification of Large Polygonal Models. 2000 // Matthias Teschner, Bruno Heidelberger, Matthias Mueller, Danat Pomeranets, Markus Gross. Optimized Spatial Hashing for Collision Detection of Deformable Objects. 2003 // Peter Van Sandt, Yannis Chronis, Jignesh M. Patel. Efficiently Searching In-Memory Sorted Arrays: Revenge of the Interpolation Search? 2019 +// Hugues Hoppe. New Quadric Metric for Simplifying Meshes with Appearance Attributes. 1999 namespace meshopt { struct EdgeAdjacency { - unsigned int* counts; + struct Edge + { + unsigned int next; + unsigned int prev; + }; + unsigned int* offsets; - unsigned int* data; + Edge* data; }; -static void buildEdgeAdjacency(EdgeAdjacency& adjacency, const unsigned int* indices, size_t index_count, size_t vertex_count, meshopt_Allocator& allocator) +static void prepareEdgeAdjacency(EdgeAdjacency& adjacency, size_t index_count, size_t vertex_count, meshopt_Allocator& allocator) +{ + adjacency.offsets = allocator.allocate(vertex_count + 1); + adjacency.data = allocator.allocate(index_count); +} + +static void updateEdgeAdjacency(EdgeAdjacency& adjacency, const unsigned int* indices, size_t index_count, size_t vertex_count, const unsigned int* remap) { size_t face_count = index_count / 3; - - // allocate arrays - adjacency.counts = allocator.allocate(vertex_count); - adjacency.offsets = allocator.allocate(vertex_count); - adjacency.data = allocator.allocate(index_count); + unsigned int* offsets = adjacency.offsets + 1; + EdgeAdjacency::Edge* data = adjacency.data; // fill edge counts - memset(adjacency.counts, 0, vertex_count * sizeof(unsigned int)); + memset(offsets, 0, vertex_count * sizeof(unsigned int)); for (size_t i = 0; i < index_count; ++i) { - assert(indices[i] < vertex_count); + unsigned int v = remap ? remap[indices[i]] : indices[i]; + assert(v < vertex_count); - adjacency.counts[indices[i]]++; + offsets[v]++; } // fill offset table @@ -54,8 +70,9 @@ static void buildEdgeAdjacency(EdgeAdjacency& adjacency, const unsigned int* ind for (size_t i = 0; i < vertex_count; ++i) { - adjacency.offsets[i] = offset; - offset += adjacency.counts[i]; + unsigned int count = offsets[i]; + offsets[i] = offset; + offset += count; } assert(offset == index_count); @@ -65,18 +82,29 @@ static void buildEdgeAdjacency(EdgeAdjacency& adjacency, const unsigned int* ind { unsigned int a = indices[i * 3 + 0], b = indices[i * 3 + 1], c = indices[i * 3 + 2]; - adjacency.data[adjacency.offsets[a]++] = b; - adjacency.data[adjacency.offsets[b]++] = c; - adjacency.data[adjacency.offsets[c]++] = a; + if (remap) + { + a = remap[a]; + b = remap[b]; + c = remap[c]; + } + + data[offsets[a]].next = b; + data[offsets[a]].prev = c; + offsets[a]++; + + data[offsets[b]].next = c; + data[offsets[b]].prev = a; + offsets[b]++; + + data[offsets[c]].next = a; + data[offsets[c]].prev = b; + offsets[c]++; } - // fix offsets that have been disturbed by the previous pass - for (size_t i = 0; i < vertex_count; ++i) - { - assert(adjacency.offsets[i] >= adjacency.counts[i]); - - adjacency.offsets[i] -= adjacency.counts[i]; - } + // finalize offsets + adjacency.offsets[0] = 0; + assert(adjacency.offsets[vertex_count] == index_count); } struct PositionHasher @@ -86,26 +114,15 @@ struct PositionHasher size_t hash(unsigned int index) const { - // MurmurHash2 - const unsigned int m = 0x5bd1e995; - const int r = 24; - - unsigned int h = 0; const unsigned int* key = reinterpret_cast(vertex_positions + index * vertex_stride_float); - for (size_t i = 0; i < 3; ++i) - { - unsigned int k = key[i]; + // scramble bits to make sure that integer coordinates have entropy in lower bits + unsigned int x = key[0] ^ (key[0] >> 17); + unsigned int y = key[1] ^ (key[1] >> 17); + unsigned int z = key[2] ^ (key[2] >> 17); - k *= m; - k ^= k >> r; - k *= m; - - h *= m; - h ^= k; - } - - return h; + // Optimized Spatial Hashing for Collision Detection of Deformable Objects + return (x * 73856093) ^ (y * 19349663) ^ (z * 83492791); } bool equal(unsigned int lhs, unsigned int rhs) const @@ -117,7 +134,7 @@ struct PositionHasher static size_t hashBuckets2(size_t count) { size_t buckets = 1; - while (buckets < count) + while (buckets < count + count / 4) buckets *= 2; return buckets; @@ -147,7 +164,7 @@ static T* hashLookup2(T* table, size_t buckets, const Hash& hash, const T& key, } assert(false && "Hash table is full"); // unreachable - return 0; + return NULL; } static void buildPositionRemap(unsigned int* remap, unsigned int* wedge, const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride, meshopt_Allocator& allocator) @@ -184,6 +201,8 @@ static void buildPositionRemap(unsigned int* remap, unsigned int* wedge, const f wedge[i] = wedge[r]; wedge[r] = unsigned(i); } + + allocator.deallocate(table); } enum VertexKind @@ -223,60 +242,56 @@ const unsigned char kHasOpposite[Kind_Count][Kind_Count] = { static bool hasEdge(const EdgeAdjacency& adjacency, unsigned int a, unsigned int b) { - unsigned int count = adjacency.counts[a]; - const unsigned int* data = adjacency.data + adjacency.offsets[a]; + unsigned int count = adjacency.offsets[a + 1] - adjacency.offsets[a]; + const EdgeAdjacency::Edge* edges = adjacency.data + adjacency.offsets[a]; for (size_t i = 0; i < count; ++i) - if (data[i] == b) + if (edges[i].next == b) return true; return false; } -static unsigned int findWedgeEdge(const EdgeAdjacency& adjacency, const unsigned int* wedge, unsigned int a, unsigned int b) +static void classifyVertices(unsigned char* result, unsigned int* loop, unsigned int* loopback, size_t vertex_count, const EdgeAdjacency& adjacency, const unsigned int* remap, const unsigned int* wedge, unsigned int options) { - unsigned int v = a; + memset(loop, -1, vertex_count * sizeof(unsigned int)); + memset(loopback, -1, vertex_count * sizeof(unsigned int)); - do - { - if (hasEdge(adjacency, v, b)) - return v; + // incoming & outgoing open edges: ~0u if no open edges, i if there are more than 1 + // note that this is the same data as required in loop[] arrays; loop[] data is only valid for border/seam + // but here it's okay to fill the data out for other types of vertices as well + unsigned int* openinc = loopback; + unsigned int* openout = loop; - v = wedge[v]; - } while (v != a); - - return ~0u; -} - -static size_t countOpenEdges(const EdgeAdjacency& adjacency, unsigned int vertex, unsigned int* last = 0) -{ - size_t result = 0; - - unsigned int count = adjacency.counts[vertex]; - const unsigned int* data = adjacency.data + adjacency.offsets[vertex]; - - for (size_t i = 0; i < count; ++i) - if (!hasEdge(adjacency, data[i], vertex)) - { - result++; - - if (last) - *last = data[i]; - } - - return result; -} - -static void classifyVertices(unsigned char* result, unsigned int* loop, size_t vertex_count, const EdgeAdjacency& adjacency, const unsigned int* remap, const unsigned int* wedge) -{ for (size_t i = 0; i < vertex_count; ++i) - loop[i] = ~0u; + { + unsigned int vertex = unsigned(i); + + unsigned int count = adjacency.offsets[vertex + 1] - adjacency.offsets[vertex]; + const EdgeAdjacency::Edge* edges = adjacency.data + adjacency.offsets[vertex]; + + for (size_t j = 0; j < count; ++j) + { + unsigned int target = edges[j].next; + + if (target == vertex) + { + // degenerate triangles have two distinct edges instead of three, and the self edge + // is bi-directional by definition; this can break border/seam classification by "closing" + // the open edge from another triangle and falsely marking the vertex as manifold + // instead we mark the vertex as having >1 open edges which turns it into locked/complex + openinc[vertex] = openout[vertex] = vertex; + } + else if (!hasEdge(adjacency, target, vertex)) + { + openinc[target] = (openinc[target] == ~0u) ? vertex : target; + openout[vertex] = (openout[vertex] == ~0u) ? target : vertex; + } + } + } #if TRACE - size_t lockedstats[4] = {}; -#define TRACELOCKED(i) lockedstats[i]++; -#else -#define TRACELOCKED(i) (void)0 + size_t stats[4] = {}; #endif for (size_t i = 0; i < vertex_count; ++i) @@ -286,67 +301,57 @@ static void classifyVertices(unsigned char* result, unsigned int* loop, size_t v if (wedge[i] == i) { // no attribute seam, need to check if it's manifold - unsigned int v = 0; - size_t edges = countOpenEdges(adjacency, unsigned(i), &v); + unsigned int openi = openinc[i], openo = openout[i]; // note: we classify any vertices with no open edges as manifold // this is technically incorrect - if 4 triangles share an edge, we'll classify vertices as manifold // it's unclear if this is a problem in practice - // also note that we classify vertices as border if they have *one* open edge, not two - // this is because we only have half-edges - so a border vertex would have one incoming and one outgoing edge - if (edges == 0) + if (openi == ~0u && openo == ~0u) { result[i] = Kind_Manifold; } - else if (edges == 1) + else if (openi != i && openo != i) { result[i] = Kind_Border; - loop[i] = v; } else { result[i] = Kind_Locked; - TRACELOCKED(0); + TRACESTATS(0); } } else if (wedge[wedge[i]] == i) { // attribute seam; need to distinguish between Seam and Locked - unsigned int a = 0; - size_t a_count = countOpenEdges(adjacency, unsigned(i), &a); - unsigned int b = 0; - size_t b_count = countOpenEdges(adjacency, wedge[i], &b); + unsigned int w = wedge[i]; + unsigned int openiv = openinc[i], openov = openout[i]; + unsigned int openiw = openinc[w], openow = openout[w]; // seam should have one open half-edge for each vertex, and the edges need to "connect" - point to the same vertex post-remap - if (a_count == 1 && b_count == 1) + if (openiv != ~0u && openiv != i && openov != ~0u && openov != i && + openiw != ~0u && openiw != w && openow != ~0u && openow != w) { - unsigned int ao = findWedgeEdge(adjacency, wedge, a, wedge[i]); - unsigned int bo = findWedgeEdge(adjacency, wedge, b, unsigned(i)); - - if (ao != ~0u && bo != ~0u) + if (remap[openiv] == remap[openow] && remap[openov] == remap[openiw]) { result[i] = Kind_Seam; - - loop[i] = a; - loop[wedge[i]] = b; } else { result[i] = Kind_Locked; - TRACELOCKED(1); + TRACESTATS(1); } } else { result[i] = Kind_Locked; - TRACELOCKED(2); + TRACESTATS(2); } } else { // more than one vertex maps to this one; we don't have classification available result[i] = Kind_Locked; - TRACELOCKED(3); + TRACESTATS(3); } } else @@ -357,9 +362,14 @@ static void classifyVertices(unsigned char* result, unsigned int* loop, size_t v } } + if (options & meshopt_SimplifyLockBorder) + for (size_t i = 0; i < vertex_count; ++i) + if (result[i] == Kind_Border) + result[i] = Kind_Locked; + #if TRACE printf("locked: many open edges %d, disconnected seam %d, many seam edges %d, many wedges %d\n", - int(lockedstats[0]), int(lockedstats[1]), int(lockedstats[2]), int(lockedstats[3])); + int(stats[0]), int(stats[1]), int(stats[2]), int(stats[3])); #endif } @@ -368,7 +378,7 @@ struct Vector3 float x, y, z; }; -static void rescalePositions(Vector3* result, const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride) +static float rescalePositions(Vector3* result, const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride) { size_t vertex_stride_float = vertex_positions_stride / sizeof(float); @@ -379,9 +389,12 @@ static void rescalePositions(Vector3* result, const float* vertex_positions_data { const float* v = vertex_positions_data + i * vertex_stride_float; - result[i].x = v[0]; - result[i].y = v[1]; - result[i].z = v[2]; + if (result) + { + result[i].x = v[0]; + result[i].y = v[1]; + result[i].z = v[2]; + } for (int j = 0; j < 3; ++j) { @@ -398,30 +411,67 @@ static void rescalePositions(Vector3* result, const float* vertex_positions_data extent = (maxv[1] - minv[1]) < extent ? extent : (maxv[1] - minv[1]); extent = (maxv[2] - minv[2]) < extent ? extent : (maxv[2] - minv[2]); - float scale = extent == 0 ? 0.f : 1.f / extent; + if (result) + { + float scale = extent == 0 ? 0.f : 1.f / extent; + + for (size_t i = 0; i < vertex_count; ++i) + { + result[i].x = (result[i].x - minv[0]) * scale; + result[i].y = (result[i].y - minv[1]) * scale; + result[i].z = (result[i].z - minv[2]) * scale; + } + } + + return extent; +} + +static void rescaleAttributes(float* result, const float* vertex_attributes_data, size_t vertex_count, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count) +{ + size_t vertex_attributes_stride_float = vertex_attributes_stride / sizeof(float); for (size_t i = 0; i < vertex_count; ++i) { - result[i].x = (result[i].x - minv[0]) * scale; - result[i].y = (result[i].y - minv[1]) * scale; - result[i].z = (result[i].z - minv[2]) * scale; + for (size_t k = 0; k < attribute_count; ++k) + { + float a = vertex_attributes_data[i * vertex_attributes_stride_float + k]; + + result[i * attribute_count + k] = a * attribute_weights[k]; + } } } +static const size_t kMaxAttributes = 16; + struct Quadric { + // a00*x^2 + a11*y^2 + a22*z^2 + 2*(a10*xy + a20*xz + a21*yz) + b0*x + b1*y + b2*z + c float a00, a11, a22; float a10, a20, a21; float b0, b1, b2, c; float w; }; +struct QuadricGrad +{ + // gx*x + gy*y + gz*z + gw + float gx, gy, gz, gw; +}; + +struct Reservoir +{ + float x, y, z; + float r, g, b; + float w; +}; + struct Collapse { unsigned int v0; unsigned int v1; - union { + union + { unsigned int bidi; float error; unsigned int errorui; @@ -457,6 +507,17 @@ static void quadricAdd(Quadric& Q, const Quadric& R) Q.w += R.w; } +static void quadricAdd(QuadricGrad* G, const QuadricGrad* R, size_t attribute_count) +{ + for (size_t k = 0; k < attribute_count; ++k) + { + G[k].gx += R[k].gx; + G[k].gy += R[k].gy; + G[k].gz += R[k].gz; + G[k].gw += R[k].gw; + } +} + static float quadricError(const Quadric& Q, const Vector3& v) { float rx = Q.b0; @@ -485,6 +546,45 @@ static float quadricError(const Quadric& Q, const Vector3& v) return fabsf(r) * s; } +static float quadricError(const Quadric& Q, const QuadricGrad* G, size_t attribute_count, const Vector3& v, const float* va) +{ + float rx = Q.b0; + float ry = Q.b1; + float rz = Q.b2; + + rx += Q.a10 * v.y; + ry += Q.a21 * v.z; + rz += Q.a20 * v.x; + + rx *= 2; + ry *= 2; + rz *= 2; + + rx += Q.a00 * v.x; + ry += Q.a11 * v.y; + rz += Q.a22 * v.z; + + float r = Q.c; + r += rx * v.x; + r += ry * v.y; + r += rz * v.z; + + // see quadricFromAttributes for general derivation; here we need to add the parts of (eval(pos) - attr)^2 that depend on attr + for (size_t k = 0; k < attribute_count; ++k) + { + float a = va[k]; + float g = v.x * G[k].gx + v.y * G[k].gy + v.z * G[k].gz + G[k].gw; + + r += a * a * Q.w; + r -= 2 * a * g; + } + + // TODO: weight normalization is breaking attribute error somehow + float s = 1;// Q.w == 0.f ? 0.f : 1.f / Q.w; + + return fabsf(r) * s; +} + static void quadricFromPlane(Quadric& Q, float a, float b, float c, float d, float w) { float aw = a * w; @@ -505,22 +605,6 @@ static void quadricFromPlane(Quadric& Q, float a, float b, float c, float d, flo Q.w = w; } -static void quadricFromPoint(Quadric& Q, float x, float y, float z, float w) -{ - // we need to encode (x - X) ^ 2 + (y - Y)^2 + (z - Z)^2 into the quadric - Q.a00 = w; - Q.a11 = w; - Q.a22 = w; - Q.a10 = 0.f; - Q.a20 = 0.f; - Q.a21 = 0.f; - Q.b0 = -2.f * x * w; - Q.b1 = -2.f * y * w; - Q.b2 = -2.f * z * w; - Q.c = (x * x + y * y + z * z) * w; - Q.w = w; -} - static void quadricFromTriangle(Quadric& Q, const Vector3& p0, const Vector3& p1, const Vector3& p2, float weight) { Vector3 p10 = {p1.x - p0.x, p1.y - p0.y, p1.z - p0.z}; @@ -555,6 +639,82 @@ static void quadricFromTriangleEdge(Quadric& Q, const Vector3& p0, const Vector3 quadricFromPlane(Q, normal.x, normal.y, normal.z, -distance, length * weight); } +static void quadricFromAttributes(Quadric& Q, QuadricGrad* G, const Vector3& p0, const Vector3& p1, const Vector3& p2, const float* va0, const float* va1, const float* va2, size_t attribute_count) +{ + // for each attribute we want to encode the following function into the quadric: + // (eval(pos) - attr)^2 + // where eval(pos) interpolates attribute across the triangle like so: + // eval(pos) = pos.x * gx + pos.y * gy + pos.z * gz + gw + // where gx/gy/gz/gw are gradients + Vector3 p10 = {p1.x - p0.x, p1.y - p0.y, p1.z - p0.z}; + Vector3 p20 = {p2.x - p0.x, p2.y - p0.y, p2.z - p0.z}; + + // weight is scaled linearly with edge length + Vector3 normal = {p10.y * p20.z - p10.z * p20.y, p10.z * p20.x - p10.x * p20.z, p10.x * p20.y - p10.y * p20.x}; + float area = sqrtf(normal.x * normal.x + normal.y * normal.y + normal.z * normal.z); + float w = sqrtf(area); // TODO this needs more experimentation + + // we compute gradients using barycentric coordinates; barycentric coordinates can be computed as follows: + // v = (d11 * d20 - d01 * d21) / denom + // w = (d00 * d21 - d01 * d20) / denom + // u = 1 - v - w + // here v0, v1 are triangle edge vectors, v2 is a vector from point to triangle corner, and dij = dot(vi, vj) + const Vector3& v0 = p10; + const Vector3& v1 = p20; + float d00 = v0.x * v0.x + v0.y * v0.y + v0.z * v0.z; + float d01 = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z; + float d11 = v1.x * v1.x + v1.y * v1.y + v1.z * v1.z; + float denom = d00 * d11 - d01 * d01; + float denomr = denom == 0 ? 0.f : 1.f / denom; + + // precompute gradient factors + // these are derived by directly computing derivative of eval(pos) = a0 * u + a1 * v + a2 * w and factoring out common factors that are shared between attributes + float gx1 = (d11 * v0.x - d01 * v1.x) * denomr; + float gx2 = (d00 * v1.x - d01 * v0.x) * denomr; + float gy1 = (d11 * v0.y - d01 * v1.y) * denomr; + float gy2 = (d00 * v1.y - d01 * v0.y) * denomr; + float gz1 = (d11 * v0.z - d01 * v1.z) * denomr; + float gz2 = (d00 * v1.z - d01 * v0.z) * denomr; + + memset(&Q, 0, sizeof(Quadric)); + + Q.w = w; + + for (size_t k = 0; k < attribute_count; ++k) + { + float a0 = va0[k], a1 = va1[k], a2 = va2[k]; + + // compute gradient of eval(pos) for x/y/z/w + // the formulas below are obtained by directly computing derivative of eval(pos) = a0 * u + a1 * v + a2 * w + float gx = gx1 * (a1 - a0) + gx2 * (a2 - a0); + float gy = gy1 * (a1 - a0) + gy2 * (a2 - a0); + float gz = gz1 * (a1 - a0) + gz2 * (a2 - a0); + float gw = a0 - p0.x * gx - p0.y * gy - p0.z * gz; + + // quadric encodes (eval(pos)-attr)^2; this means that the resulting expansion needs to compute, for example, pos.x * pos.y * K + // since quadrics already encode factors for pos.x * pos.y, we can accumulate almost everything in basic quadric fields + Q.a00 += w * (gx * gx); + Q.a11 += w * (gy * gy); + Q.a22 += w * (gz * gz); + + Q.a10 += w * (gy * gx); + Q.a20 += w * (gz * gx); + Q.a21 += w * (gz * gy); + + Q.b0 += w * (gx * gw); + Q.b1 += w * (gy * gw); + Q.b2 += w * (gz * gw); + + Q.c += w * (gw * gw); + + // the only remaining sum components are ones that depend on attr; these will be addded during error evaluation, see quadricError + G[k].gx = w * gx; + G[k].gy = w * gy; + G[k].gz = w * gz; + G[k].gw = w * gw; + } +} + static void fillFaceQuadrics(Quadric* vertex_quadrics, const unsigned int* indices, size_t index_count, const Vector3* vertex_positions, const unsigned int* remap) { for (size_t i = 0; i < index_count; i += 3) @@ -572,11 +732,11 @@ static void fillFaceQuadrics(Quadric* vertex_quadrics, const unsigned int* indic } } -static void fillEdgeQuadrics(Quadric* vertex_quadrics, const unsigned int* indices, size_t index_count, const Vector3* vertex_positions, const unsigned int* remap, const unsigned char* vertex_kind, const unsigned int* loop) +static void fillEdgeQuadrics(Quadric* vertex_quadrics, const unsigned int* indices, size_t index_count, const Vector3* vertex_positions, const unsigned int* remap, const unsigned char* vertex_kind, const unsigned int* loop, const unsigned int* loopback) { for (size_t i = 0; i < index_count; i += 3) { - static const int next[3] = {1, 2, 0}; + static const int next[4] = {1, 2, 0, 1}; for (int e = 0; e < 3; ++e) { @@ -586,19 +746,30 @@ static void fillEdgeQuadrics(Quadric* vertex_quadrics, const unsigned int* indic unsigned char k0 = vertex_kind[i0]; unsigned char k1 = vertex_kind[i1]; - // check that i0 and i1 are border/seam and are on the same edge loop - // loop[] tracks half edges so we only need to check i0->i1 - if (k0 != k1 || (k0 != Kind_Border && k0 != Kind_Seam) || loop[i0] != i1) + // check that either i0 or i1 are border/seam and are on the same edge loop + // note that we need to add the error even for edged that connect e.g. border & locked + // if we don't do that, the adjacent border->border edge won't have correct errors for corners + if (k0 != Kind_Border && k0 != Kind_Seam && k1 != Kind_Border && k1 != Kind_Seam) continue; - unsigned int i2 = indices[i + next[next[e]]]; + if ((k0 == Kind_Border || k0 == Kind_Seam) && loop[i0] != i1) + continue; + + if ((k1 == Kind_Border || k1 == Kind_Seam) && loopback[i1] != i0) + continue; + + // seam edges should occur twice (i0->i1 and i1->i0) - skip redundant edges + if (kHasOpposite[k0][k1] && remap[i1] > remap[i0]) + continue; + + unsigned int i2 = indices[i + next[e + 1]]; // we try hard to maintain border edge geometry; seam edges can move more freely // due to topological restrictions on collapses, seam quadrics slightly improves collapse structure but aren't critical const float kEdgeWeightSeam = 1.f; const float kEdgeWeightBorder = 10.f; - float edgeWeight = (k0 == Kind_Seam) ? kEdgeWeightSeam : kEdgeWeightBorder; + float edgeWeight = (k0 == Kind_Border || k1 == Kind_Border) ? kEdgeWeightBorder : kEdgeWeightSeam; Quadric Q; quadricFromTriangleEdge(Q, vertex_positions[i0], vertex_positions[i1], vertex_positions[i2], edgeWeight); @@ -609,7 +780,89 @@ static void fillEdgeQuadrics(Quadric* vertex_quadrics, const unsigned int* indic } } -static size_t pickEdgeCollapses(Collapse* collapses, const unsigned int* indices, size_t index_count, const unsigned int* remap, const unsigned char* vertex_kind, const unsigned int* loop) +static void fillAttributeQuadrics(Quadric* attribute_quadrics, QuadricGrad* attribute_gradients, const unsigned int* indices, size_t index_count, const Vector3* vertex_positions, const float* vertex_attributes, size_t attribute_count, const unsigned int* remap) +{ + for (size_t i = 0; i < index_count; i += 3) + { + unsigned int i0 = indices[i + 0]; + unsigned int i1 = indices[i + 1]; + unsigned int i2 = indices[i + 2]; + + Quadric QA; + QuadricGrad G[kMaxAttributes]; + quadricFromAttributes(QA, G, vertex_positions[i0], vertex_positions[i1], vertex_positions[i2], &vertex_attributes[i0 * attribute_count], &vertex_attributes[i1 * attribute_count], &vertex_attributes[i2 * attribute_count], attribute_count); + + // TODO: This blends together attribute weights across attribute discontinuities, which is probably not a great idea + quadricAdd(attribute_quadrics[remap[i0]], QA); + quadricAdd(attribute_quadrics[remap[i1]], QA); + quadricAdd(attribute_quadrics[remap[i2]], QA); + + quadricAdd(&attribute_gradients[remap[i0] * attribute_count], G, attribute_count); + quadricAdd(&attribute_gradients[remap[i1] * attribute_count], G, attribute_count); + quadricAdd(&attribute_gradients[remap[i2] * attribute_count], G, attribute_count); + } +} + +// does triangle ABC flip when C is replaced with D? +static bool hasTriangleFlip(const Vector3& a, const Vector3& b, const Vector3& c, const Vector3& d) +{ + Vector3 eb = {b.x - a.x, b.y - a.y, b.z - a.z}; + Vector3 ec = {c.x - a.x, c.y - a.y, c.z - a.z}; + Vector3 ed = {d.x - a.x, d.y - a.y, d.z - a.z}; + + Vector3 nbc = {eb.y * ec.z - eb.z * ec.y, eb.z * ec.x - eb.x * ec.z, eb.x * ec.y - eb.y * ec.x}; + Vector3 nbd = {eb.y * ed.z - eb.z * ed.y, eb.z * ed.x - eb.x * ed.z, eb.x * ed.y - eb.y * ed.x}; + + return nbc.x * nbd.x + nbc.y * nbd.y + nbc.z * nbd.z <= 0; +} + +static bool hasTriangleFlips(const EdgeAdjacency& adjacency, const Vector3* vertex_positions, const unsigned int* collapse_remap, unsigned int i0, unsigned int i1) +{ + assert(collapse_remap[i0] == i0); + assert(collapse_remap[i1] == i1); + + const Vector3& v0 = vertex_positions[i0]; + const Vector3& v1 = vertex_positions[i1]; + + const EdgeAdjacency::Edge* edges = &adjacency.data[adjacency.offsets[i0]]; + size_t count = adjacency.offsets[i0 + 1] - adjacency.offsets[i0]; + + for (size_t i = 0; i < count; ++i) + { + unsigned int a = collapse_remap[edges[i].next]; + unsigned int b = collapse_remap[edges[i].prev]; + + // skip triangles that will get collapsed by i0->i1 collapse or already got collapsed previously + if (a == i1 || b == i1 || a == b) + continue; + + // early-out when at least one triangle flips due to a collapse + if (hasTriangleFlip(vertex_positions[a], vertex_positions[b], v0, v1)) + return true; + } + + return false; +} + +static size_t boundEdgeCollapses(const EdgeAdjacency& adjacency, size_t vertex_count, size_t index_count, unsigned char* vertex_kind) +{ + size_t dual_count = 0; + + for (size_t i = 0; i < vertex_count; ++i) + { + unsigned char k = vertex_kind[i]; + unsigned int e = adjacency.offsets[i + 1] - adjacency.offsets[i]; + + dual_count += (k == Kind_Manifold || k == Kind_Seam) ? e : 0; + } + + assert(dual_count <= index_count); + + // pad capacity by 3 so that we can check for overflow once per triangle instead of once per edge + return (index_count - dual_count / 2) + 3; +} + +static size_t pickEdgeCollapses(Collapse* collapses, size_t collapse_capacity, const unsigned int* indices, size_t index_count, const unsigned int* remap, const unsigned char* vertex_kind, const unsigned int* loop) { size_t collapse_count = 0; @@ -617,6 +870,10 @@ static size_t pickEdgeCollapses(Collapse* collapses, const unsigned int* indices { static const int next[3] = {1, 2, 0}; + // this should never happen as boundEdgeCollapses should give an upper bound for the collapse count, but in an unlikely event it does we can just drop extra collapses + if (collapse_count + 3 > collapse_capacity) + break; + for (int e = 0; e < 3; ++e) { unsigned int i0 = indices[i + e]; @@ -667,7 +924,7 @@ static size_t pickEdgeCollapses(Collapse* collapses, const unsigned int* indices return collapse_count; } -static void rankEdgeCollapses(Collapse* collapses, size_t collapse_count, const Vector3* vertex_positions, const Quadric* vertex_quadrics, const unsigned int* remap) +static void rankEdgeCollapses(Collapse* collapses, size_t collapse_count, const Vector3* vertex_positions, const float* vertex_attributes, const Quadric* vertex_quadrics, const Quadric* attribute_quadrics, const QuadricGrad* attribute_gradients, size_t attribute_count, const unsigned int* remap) { for (size_t i = 0; i < collapse_count; ++i) { @@ -681,11 +938,14 @@ static void rankEdgeCollapses(Collapse* collapses, size_t collapse_count, const unsigned int j0 = c.bidi ? i1 : i0; unsigned int j1 = c.bidi ? i0 : i1; - const Quadric& qi = vertex_quadrics[remap[i0]]; - const Quadric& qj = vertex_quadrics[remap[j0]]; + float ei = quadricError(vertex_quadrics[remap[i0]], vertex_positions[i1]); + float ej = quadricError(vertex_quadrics[remap[j0]], vertex_positions[j1]); - float ei = quadricError(qi, vertex_positions[i1]); - float ej = quadricError(qj, vertex_positions[j1]); + if (attribute_count) + { + ei += quadricError(attribute_quadrics[remap[i0]], &attribute_gradients[remap[i0] * attribute_count], attribute_count, vertex_positions[i1], &vertex_attributes[i1 * attribute_count]); + ej += quadricError(attribute_quadrics[remap[j0]], &attribute_gradients[remap[j0] * attribute_count], attribute_count, vertex_positions[j1], &vertex_attributes[j1 * attribute_count]); + } // pick edge direction with minimal error c.v0 = ei <= ej ? i0 : j0; @@ -694,61 +954,6 @@ static void rankEdgeCollapses(Collapse* collapses, size_t collapse_count, const } } -#if TRACE > 1 -static void dumpEdgeCollapses(const Collapse* collapses, size_t collapse_count, const unsigned char* vertex_kind) -{ - size_t ckinds[Kind_Count][Kind_Count] = {}; - float cerrors[Kind_Count][Kind_Count] = {}; - - for (int k0 = 0; k0 < Kind_Count; ++k0) - for (int k1 = 0; k1 < Kind_Count; ++k1) - cerrors[k0][k1] = FLT_MAX; - - for (size_t i = 0; i < collapse_count; ++i) - { - unsigned int i0 = collapses[i].v0; - unsigned int i1 = collapses[i].v1; - - unsigned char k0 = vertex_kind[i0]; - unsigned char k1 = vertex_kind[i1]; - - ckinds[k0][k1]++; - cerrors[k0][k1] = (collapses[i].error < cerrors[k0][k1]) ? collapses[i].error : cerrors[k0][k1]; - } - - for (int k0 = 0; k0 < Kind_Count; ++k0) - for (int k1 = 0; k1 < Kind_Count; ++k1) - if (ckinds[k0][k1]) - printf("collapses %d -> %d: %d, min error %e\n", k0, k1, int(ckinds[k0][k1]), cerrors[k0][k1]); -} - -static void dumpLockedCollapses(const unsigned int* indices, size_t index_count, const unsigned char* vertex_kind) -{ - size_t locked_collapses[Kind_Count][Kind_Count] = {}; - - for (size_t i = 0; i < index_count; i += 3) - { - static const int next[3] = {1, 2, 0}; - - for (int e = 0; e < 3; ++e) - { - unsigned int i0 = indices[i + e]; - unsigned int i1 = indices[i + next[e]]; - - unsigned char k0 = vertex_kind[i0]; - unsigned char k1 = vertex_kind[i1]; - - locked_collapses[k0][k1] += !kCanCollapse[k0][k1] && !kCanCollapse[k1][k0]; - } - } - - for (int k0 = 0; k0 < Kind_Count; ++k0) - for (int k1 = 0; k1 < Kind_Count; ++k1) - if (locked_collapses[k0][k1]) - printf("locked collapses %d -> %d: %d\n", k0, k1, int(locked_collapses[k0][k1])); -} -#endif - static void sortEdgeCollapses(unsigned int* sort_order, const Collapse* collapses, size_t collapse_count) { const int sort_bits = 11; @@ -787,22 +992,38 @@ static void sortEdgeCollapses(unsigned int* sort_order, const Collapse* collapse } } -static size_t performEdgeCollapses(unsigned int* collapse_remap, unsigned char* collapse_locked, Quadric* vertex_quadrics, const Collapse* collapses, size_t collapse_count, const unsigned int* collapse_order, const unsigned int* remap, const unsigned int* wedge, const unsigned char* vertex_kind, size_t triangle_collapse_goal, float error_goal, float error_limit) +static size_t performEdgeCollapses(unsigned int* collapse_remap, unsigned char* collapse_locked, Quadric* vertex_quadrics, Quadric* attribute_quadrics, QuadricGrad* attribute_gradients, size_t attribute_count, const Collapse* collapses, size_t collapse_count, const unsigned int* collapse_order, const unsigned int* remap, const unsigned int* wedge, const unsigned char* vertex_kind, const Vector3* vertex_positions, const EdgeAdjacency& adjacency, size_t triangle_collapse_goal, float error_limit, float& result_error) { size_t edge_collapses = 0; size_t triangle_collapses = 0; + // most collapses remove 2 triangles; use this to establish a bound on the pass in terms of error limit + // note that edge_collapse_goal is an estimate; triangle_collapse_goal will be used to actually limit collapses + size_t edge_collapse_goal = triangle_collapse_goal / 2; + +#if TRACE + size_t stats[4] = {}; +#endif + for (size_t i = 0; i < collapse_count; ++i) { const Collapse& c = collapses[collapse_order[i]]; + TRACESTATS(0); + if (c.error > error_limit) break; - if (c.error > error_goal && triangle_collapses > triangle_collapse_goal / 10) + if (triangle_collapses >= triangle_collapse_goal) break; - if (triangle_collapses >= triangle_collapse_goal) + // we limit the error in each pass based on the error of optimal last collapse; since many collapses will be locked + // as they will share vertices with other successfull collapses, we need to increase the acceptable error by some factor + float error_goal = edge_collapse_goal < collapse_count ? 1.5f * collapses[collapse_order[edge_collapse_goal]].error : FLT_MAX; + + // on average, each collapse is expected to lock 6 other collapses; to avoid degenerate passes on meshes with odd + // topology, we only abort if we got over 1/6 collapses accordingly. + if (c.error > error_goal && triangle_collapses > triangle_collapse_goal / 6) break; unsigned int i0 = c.v0; @@ -815,13 +1036,31 @@ static size_t performEdgeCollapses(unsigned int* collapse_remap, unsigned char* // it's important to not move the vertices twice since it complicates the tracking/remapping logic // it's important to not move other vertices towards a moved vertex to preserve error since we don't re-rank collapses mid-pass if (collapse_locked[r0] | collapse_locked[r1]) + { + TRACESTATS(1); continue; + } + + if (hasTriangleFlips(adjacency, vertex_positions, collapse_remap, r0, r1)) + { + // adjust collapse goal since this collapse is invalid and shouldn't factor into error goal + edge_collapse_goal++; + + TRACESTATS(2); + continue; + } assert(collapse_remap[r0] == r0); assert(collapse_remap[r1] == r1); quadricAdd(vertex_quadrics[r1], vertex_quadrics[r0]); + if (attribute_count) + { + quadricAdd(attribute_quadrics[r1], attribute_quadrics[r0]); + quadricAdd(&attribute_gradients[r1 * attribute_count], &attribute_gradients[r0 * attribute_count], attribute_count); + } + if (vertex_kind[i0] == Kind_Complex) { unsigned int v = i0; @@ -857,8 +1096,18 @@ static size_t performEdgeCollapses(unsigned int* collapse_remap, unsigned char* // border edges collapse 1 triangle, other edges collapse 2 or more triangle_collapses += (vertex_kind[i0] == Kind_Border) ? 1 : 2; edge_collapses++; + + result_error = result_error < c.error ? c.error : result_error; } +#if TRACE + float error_goal_perfect = edge_collapse_goal < collapse_count ? collapses[collapse_order[edge_collapse_goal]].error : 0.f; + + printf("removed %d triangles, error %e (goal %e); evaluated %d/%d collapses (done %d, skipped %d, invalid %d)\n", + int(triangle_collapses), sqrtf(result_error), sqrtf(error_goal_perfect), + int(stats[0]), int(collapse_count), int(edge_collapses), int(stats[1]), int(stats[2])); +#endif + return edge_collapses; } @@ -946,7 +1195,7 @@ struct IdHasher struct TriangleHasher { - unsigned int* indices; + const unsigned int* indices; size_t hash(unsigned int i) const { @@ -1074,17 +1323,41 @@ static void fillCellQuadrics(Quadric* cell_quadrics, const unsigned int* indices } } -static void fillCellQuadrics(Quadric* cell_quadrics, const Vector3* vertex_positions, size_t vertex_count, const unsigned int* vertex_cells) +static void fillCellReservoirs(Reservoir* cell_reservoirs, size_t cell_count, const Vector3* vertex_positions, const float* vertex_colors, size_t vertex_colors_stride, size_t vertex_count, const unsigned int* vertex_cells) { + static const float dummy_color[] = { 0.f, 0.f, 0.f }; + + size_t vertex_colors_stride_float = vertex_colors_stride / sizeof(float); + for (size_t i = 0; i < vertex_count; ++i) { - unsigned int c = vertex_cells[i]; + unsigned int cell = vertex_cells[i]; const Vector3& v = vertex_positions[i]; + Reservoir& r = cell_reservoirs[cell]; - Quadric Q; - quadricFromPoint(Q, v.x, v.y, v.z, 1.f); + const float* color = vertex_colors ? &vertex_colors[i * vertex_colors_stride_float] : dummy_color; - quadricAdd(cell_quadrics[c], Q); + r.x += v.x; + r.y += v.y; + r.z += v.z; + r.r += color[0]; + r.g += color[1]; + r.b += color[2]; + r.w += 1.f; + } + + for (size_t i = 0; i < cell_count; ++i) + { + Reservoir& r = cell_reservoirs[i]; + + float iw = r.w == 0.f ? 0.f : 1.f / r.w; + + r.x *= iw; + r.y *= iw; + r.z *= iw; + r.r *= iw; + r.g *= iw; + r.b *= iw; } } @@ -1105,6 +1378,34 @@ static void fillCellRemap(unsigned int* cell_remap, float* cell_errors, size_t c } } +static void fillCellRemap(unsigned int* cell_remap, float* cell_errors, size_t cell_count, const unsigned int* vertex_cells, const Reservoir* cell_reservoirs, const Vector3* vertex_positions, const float* vertex_colors, size_t vertex_colors_stride, float color_weight, size_t vertex_count) +{ + static const float dummy_color[] = { 0.f, 0.f, 0.f }; + + size_t vertex_colors_stride_float = vertex_colors_stride / sizeof(float); + + memset(cell_remap, -1, cell_count * sizeof(unsigned int)); + + for (size_t i = 0; i < vertex_count; ++i) + { + unsigned int cell = vertex_cells[i]; + const Vector3& v = vertex_positions[i]; + const Reservoir& r = cell_reservoirs[cell]; + + const float* color = vertex_colors ? &vertex_colors[i * vertex_colors_stride_float] : dummy_color; + + float pos_error = (v.x - r.x) * (v.x - r.x) + (v.y - r.y) * (v.y - r.y) + (v.z - r.z) * (v.z - r.z); + float col_error = (color[0] - r.r) * (color[0] - r.r) + (color[1] - r.g) * (color[1] - r.g) + (color[2] - r.b) * (color[2] - r.b); + float error = pos_error + color_weight * col_error; + + if (cell_remap[cell] == ~0u || cell_errors[cell] > error) + { + cell_remap[cell] = unsigned(i); + cell_errors[cell] = error; + } + } +} + static size_t filterTriangles(unsigned int* destination, unsigned int* tritable, size_t tritable_size, const unsigned int* indices, size_t index_count, const unsigned int* vertex_cells, const unsigned int* cell_remap) { TriangleHasher hasher = {destination}; @@ -1160,19 +1461,25 @@ static float interpolate(float y, float x0, float y0, float x1, float y1, float } // namespace meshopt -#if TRACE -unsigned char* meshopt_simplifyDebugKind = 0; -unsigned int* meshopt_simplifyDebugLoop = 0; +#ifndef NDEBUG +// Note: this is only exposed for debug visualization purposes; do *not* use these in debug builds +MESHOPTIMIZER_API unsigned char* meshopt_simplifyDebugKind = NULL; +MESHOPTIMIZER_API unsigned int* meshopt_simplifyDebugLoop = NULL; +MESHOPTIMIZER_API unsigned int* meshopt_simplifyDebugLoopBack = NULL; #endif -size_t meshopt_simplify(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error) +size_t meshopt_simplifyEdge(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_attributes_data, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, size_t target_index_count, float target_error, unsigned int options, float* out_result_error) { using namespace meshopt; assert(index_count % 3 == 0); - assert(vertex_positions_stride > 0 && vertex_positions_stride <= 256); + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); assert(vertex_positions_stride % sizeof(float) == 0); assert(target_index_count <= index_count); + assert((options & ~(meshopt_SimplifyLockBorder)) == 0); + assert(vertex_attributes_stride >= attribute_count * sizeof(float) && vertex_attributes_stride <= 256); + assert(vertex_attributes_stride % sizeof(float) == 0); + assert(attribute_count <= kMaxAttributes); meshopt_Allocator allocator; @@ -1180,7 +1487,8 @@ size_t meshopt_simplify(unsigned int* destination, const unsigned int* indices, // build adjacency information EdgeAdjacency adjacency = {}; - buildEdgeAdjacency(adjacency, indices, index_count, vertex_count, allocator); + prepareEdgeAdjacency(adjacency, index_count, vertex_count, allocator); + updateEdgeAdjacency(adjacency, indices, index_count, vertex_count, NULL); // build position remap that maps each vertex to the one with identical position unsigned int* remap = allocator.allocate(vertex_count); @@ -1190,7 +1498,8 @@ size_t meshopt_simplify(unsigned int* destination, const unsigned int* indices, // classify vertices; vertex kind determines collapse rules, see kCanCollapse unsigned char* vertex_kind = allocator.allocate(vertex_count); unsigned int* loop = allocator.allocate(vertex_count); - classifyVertices(vertex_kind, loop, vertex_count, adjacency, remap, wedge); + unsigned int* loopback = allocator.allocate(vertex_count); + classifyVertices(vertex_kind, loop, loopback, vertex_count, adjacency, remap, wedge, options); #if TRACE size_t unique_positions = 0; @@ -1204,132 +1513,147 @@ size_t meshopt_simplify(unsigned int* destination, const unsigned int* indices, kinds[vertex_kind[i]] += remap[i] == i; printf("kinds: manifold %d, border %d, seam %d, complex %d, locked %d\n", - int(kinds[Kind_Manifold]), int(kinds[Kind_Border]), int(kinds[Kind_Seam]), int(kinds[Kind_Complex]), int(kinds[Kind_Locked])); + int(kinds[Kind_Manifold]), int(kinds[Kind_Border]), int(kinds[Kind_Seam]), int(kinds[Kind_Complex]), int(kinds[Kind_Locked])); #endif Vector3* vertex_positions = allocator.allocate(vertex_count); rescalePositions(vertex_positions, vertex_positions_data, vertex_count, vertex_positions_stride); + float* vertex_attributes = NULL; + + if (attribute_count) + { + vertex_attributes = allocator.allocate(vertex_count * attribute_count); + rescaleAttributes(vertex_attributes, vertex_attributes_data, vertex_count, vertex_attributes_stride, attribute_weights, attribute_count); + } + Quadric* vertex_quadrics = allocator.allocate(vertex_count); memset(vertex_quadrics, 0, vertex_count * sizeof(Quadric)); + Quadric* attribute_quadrics = NULL; + QuadricGrad* attribute_gradients = NULL; + + if (attribute_count) + { + attribute_quadrics = allocator.allocate(vertex_count); + memset(attribute_quadrics, 0, vertex_count * sizeof(Quadric)); + + attribute_gradients = allocator.allocate(vertex_count * attribute_count); + memset(attribute_gradients, 0, vertex_count * attribute_count * sizeof(QuadricGrad)); + } + fillFaceQuadrics(vertex_quadrics, indices, index_count, vertex_positions, remap); - fillEdgeQuadrics(vertex_quadrics, indices, index_count, vertex_positions, remap, vertex_kind, loop); + fillEdgeQuadrics(vertex_quadrics, indices, index_count, vertex_positions, remap, vertex_kind, loop, loopback); + + if (attribute_count) + fillAttributeQuadrics(attribute_quadrics, attribute_gradients, indices, index_count, vertex_positions, vertex_attributes, attribute_count, remap); if (result != indices) memcpy(result, indices, index_count * sizeof(unsigned int)); #if TRACE size_t pass_count = 0; - float worst_error = 0; #endif - Collapse* edge_collapses = allocator.allocate(index_count); - unsigned int* collapse_order = allocator.allocate(index_count); + size_t collapse_capacity = boundEdgeCollapses(adjacency, vertex_count, index_count, vertex_kind); + + Collapse* edge_collapses = allocator.allocate(collapse_capacity); + unsigned int* collapse_order = allocator.allocate(collapse_capacity); unsigned int* collapse_remap = allocator.allocate(vertex_count); unsigned char* collapse_locked = allocator.allocate(vertex_count); size_t result_count = index_count; + float result_error = 0; // target_error input is linear; we need to adjust it to match quadricError units float error_limit = target_error * target_error; while (result_count > target_index_count) { - size_t edge_collapse_count = pickEdgeCollapses(edge_collapses, result, result_count, remap, vertex_kind, loop); + // note: throughout the simplification process adjacency structure reflects welded topology for result-in-progress + updateEdgeAdjacency(adjacency, result, result_count, vertex_count, remap); + + size_t edge_collapse_count = pickEdgeCollapses(edge_collapses, collapse_capacity, result, result_count, remap, vertex_kind, loop); + assert(edge_collapse_count <= collapse_capacity); // no edges can be collapsed any more due to topology restrictions if (edge_collapse_count == 0) break; - rankEdgeCollapses(edge_collapses, edge_collapse_count, vertex_positions, vertex_quadrics, remap); - -#if TRACE > 1 - dumpEdgeCollapses(edge_collapses, edge_collapse_count, vertex_kind); -#endif + rankEdgeCollapses(edge_collapses, edge_collapse_count, vertex_positions, vertex_attributes, vertex_quadrics, attribute_quadrics, attribute_gradients, attribute_count, remap); sortEdgeCollapses(collapse_order, edge_collapses, edge_collapse_count); - // most collapses remove 2 triangles; use this to establish a bound on the pass in terms of error limit - // note that edge_collapse_goal is an estimate; triangle_collapse_goal will be used to actually limit collapses size_t triangle_collapse_goal = (result_count - target_index_count) / 3; - size_t edge_collapse_goal = triangle_collapse_goal / 2; - - // we limit the error in each pass based on the error of optimal last collapse; since many collapses will be locked - // as they will share vertices with other successfull collapses, we need to increase the acceptable error by this factor - const float kPassErrorBound = 1.5f; - - float error_goal = edge_collapse_goal < edge_collapse_count ? edge_collapses[collapse_order[edge_collapse_goal]].error * kPassErrorBound : FLT_MAX; for (size_t i = 0; i < vertex_count; ++i) collapse_remap[i] = unsigned(i); memset(collapse_locked, 0, vertex_count); - size_t collapses = performEdgeCollapses(collapse_remap, collapse_locked, vertex_quadrics, edge_collapses, edge_collapse_count, collapse_order, remap, wedge, vertex_kind, triangle_collapse_goal, error_goal, error_limit); +#if TRACE + printf("pass %d: ", int(pass_count++)); +#endif + + size_t collapses = performEdgeCollapses(collapse_remap, collapse_locked, vertex_quadrics, attribute_quadrics, attribute_gradients, attribute_count, edge_collapses, edge_collapse_count, collapse_order, remap, wedge, vertex_kind, vertex_positions, adjacency, triangle_collapse_goal, error_limit, result_error); // no edges can be collapsed any more due to hitting the error limit or triangle collapse limit if (collapses == 0) break; remapEdgeLoops(loop, vertex_count, collapse_remap); + remapEdgeLoops(loopback, vertex_count, collapse_remap); size_t new_count = remapIndexBuffer(result, result_count, collapse_remap); assert(new_count < result_count); -#if TRACE - float pass_error = 0.f; - for (size_t i = 0; i < edge_collapse_count; ++i) - { - Collapse& c = edge_collapses[collapse_order[i]]; - - if (collapse_remap[c.v0] == c.v1) - pass_error = c.error; - } - - pass_count++; - worst_error = (worst_error < pass_error) ? pass_error : worst_error; - - printf("pass %d: triangles: %d -> %d, collapses: %d/%d (goal: %d), error: %e (limit %e goal %e)\n", int(pass_count), int(result_count / 3), int(new_count / 3), int(collapses), int(edge_collapse_count), int(edge_collapse_goal), pass_error, error_limit, error_goal); -#endif - result_count = new_count; } #if TRACE - printf("passes: %d, worst error: %e\n", int(pass_count), worst_error); + printf("result: %d triangles, error: %e; total %d passes\n", int(result_count), sqrtf(result_error), int(pass_count)); #endif -#if TRACE > 1 - dumpLockedCollapses(result, result_count, vertex_kind); -#endif - -#if TRACE +#ifndef NDEBUG if (meshopt_simplifyDebugKind) memcpy(meshopt_simplifyDebugKind, vertex_kind, vertex_count); if (meshopt_simplifyDebugLoop) memcpy(meshopt_simplifyDebugLoop, loop, vertex_count * sizeof(unsigned int)); + + if (meshopt_simplifyDebugLoopBack) + memcpy(meshopt_simplifyDebugLoopBack, loopback, vertex_count * sizeof(unsigned int)); #endif + // result_error is quadratic; we need to remap it back to linear + if (out_result_error) + *out_result_error = sqrtf(result_error); + return result_count; } -size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count) +size_t meshopt_simplify(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, unsigned int options, float* out_result_error) +{ + return meshopt_simplifyEdge(destination, indices, index_count, vertex_positions_data, vertex_count, vertex_positions_stride, NULL, 0, NULL, 0, target_index_count, target_error, options, out_result_error); +} + +size_t meshopt_simplifyWithAttributes(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_attributes_data, size_t vertex_attributes_stride, const float* attribute_weights, size_t attribute_count, size_t target_index_count, float target_error, unsigned int options, float* out_result_error) +{ + return meshopt_simplifyEdge(destination, indices, index_count, vertex_positions_data, vertex_count, vertex_positions_stride, vertex_attributes_data, vertex_attributes_stride, attribute_weights, attribute_count, target_index_count, target_error, options, out_result_error); +} + +size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* indices, size_t index_count, const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride, size_t target_index_count, float target_error, float* out_result_error) { using namespace meshopt; assert(index_count % 3 == 0); - assert(vertex_positions_stride > 0 && vertex_positions_stride <= 256); + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); assert(vertex_positions_stride % sizeof(float) == 0); assert(target_index_count <= index_count); // we expect to get ~2 triangles/vertex in the output size_t target_cell_count = target_index_count / 6; - if (target_cell_count == 0) - return 0; - meshopt_Allocator allocator; Vector3* vertex_positions = allocator.allocate(vertex_count); @@ -1346,18 +1670,25 @@ size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* ind const int kInterpolationPasses = 5; // invariant: # of triangles in min_grid <= target_count - int min_grid = 0; + int min_grid = int(1.f / (target_error < 1e-3f ? 1e-3f : target_error)); int max_grid = 1025; size_t min_triangles = 0; size_t max_triangles = index_count / 3; + // when we're error-limited, we compute the triangle count for the min. size; this accelerates convergence and provides the correct answer when we can't use a larger grid + if (min_grid > 1) + { + computeVertexIds(vertex_ids, vertex_positions, vertex_count, min_grid); + min_triangles = countTriangles(vertex_ids, indices, index_count); + } + // instead of starting in the middle, let's guess as to what the answer might be! triangle count usually grows as a square of grid size... int next_grid_size = int(sqrtf(float(target_cell_count)) + 0.5f); for (int pass = 0; pass < 10 + kInterpolationPasses; ++pass) { - assert(min_triangles < target_index_count / 3); - assert(max_grid - min_grid > 1); + if (min_triangles >= target_index_count / 3 || max_grid - min_grid <= 1) + break; // we clamp the prediction of the grid size to make sure that the search converges int grid_size = next_grid_size; @@ -1368,9 +1699,9 @@ size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* ind #if TRACE printf("pass %d (%s): grid size %d, triangles %d, %s\n", - pass, (pass == 0) ? "guess" : (pass <= kInterpolationPasses) ? "lerp" : "binary", - grid_size, int(triangles), - (triangles <= target_index_count / 3) ? "under" : "over"); + pass, (pass == 0) ? "guess" : (pass <= kInterpolationPasses) ? "lerp" : "binary", + grid_size, int(triangles), + (triangles <= target_index_count / 3) ? "under" : "over"); #endif float tip = interpolate(float(target_index_count / 3), float(min_grid), float(min_triangles), float(grid_size), float(triangles), float(max_grid), float(max_triangles)); @@ -1386,16 +1717,18 @@ size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* ind max_triangles = triangles; } - if (triangles == target_index_count / 3 || max_grid - min_grid <= 1) - break; - // we start by using interpolation search - it usually converges faster // however, interpolation search has a worst case of O(N) so we switch to binary search after a few iterations which converges in O(logN) next_grid_size = (pass < kInterpolationPasses) ? int(tip + 0.5f) : (min_grid + max_grid) / 2; } if (min_triangles == 0) + { + if (out_result_error) + *out_result_error = 1.f; + return 0; + } // build vertex->cell association by mapping all vertices with the same quantized position to the same cell size_t table_size = hashBuckets2(vertex_count); @@ -1418,27 +1751,38 @@ size_t meshopt_simplifySloppy(unsigned int* destination, const unsigned int* ind fillCellRemap(cell_remap, cell_errors, cell_count, vertex_cells, cell_quadrics, vertex_positions, vertex_count); + // compute error + float result_error = 0.f; + + for (size_t i = 0; i < cell_count; ++i) + result_error = result_error < cell_errors[i] ? cell_errors[i] : result_error; + // collapse triangles! // note that we need to filter out triangles that we've already output because we very frequently generate redundant triangles between cells :( size_t tritable_size = hashBuckets2(min_triangles); unsigned int* tritable = allocator.allocate(tritable_size); size_t write = filterTriangles(destination, tritable, tritable_size, indices, index_count, vertex_cells, cell_remap); - assert(write <= target_index_count); #if TRACE - printf("result: %d cells, %d triangles (%d unfiltered)\n", int(cell_count), int(write / 3), int(min_triangles)); + printf("result: %d cells, %d triangles (%d unfiltered), error %e\n", int(cell_count), int(write / 3), int(min_triangles), sqrtf(result_error)); #endif + if (out_result_error) + *out_result_error = sqrtf(result_error); + return write; } -size_t meshopt_simplifyPoints(unsigned int* destination, const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride, size_t target_vertex_count) +size_t meshopt_simplifyPoints(unsigned int* destination, const float* vertex_positions_data, size_t vertex_count, size_t vertex_positions_stride, const float* vertex_colors, size_t vertex_colors_stride, float color_weight, size_t target_vertex_count) { using namespace meshopt; - assert(vertex_positions_stride > 0 && vertex_positions_stride <= 256); + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); assert(vertex_positions_stride % sizeof(float) == 0); + assert(vertex_colors_stride == 0 || (vertex_colors_stride >= 12 && vertex_colors_stride <= 256)); + assert(vertex_colors_stride % sizeof(float) == 0); + assert(vertex_colors == NULL || vertex_colors_stride != 0); assert(target_vertex_count <= vertex_count); size_t target_cell_count = target_vertex_count; @@ -1487,9 +1831,9 @@ size_t meshopt_simplifyPoints(unsigned int* destination, const float* vertex_pos #if TRACE printf("pass %d (%s): grid size %d, vertices %d, %s\n", - pass, (pass == 0) ? "guess" : (pass <= kInterpolationPasses) ? "lerp" : "binary", - grid_size, int(vertices), - (vertices <= target_vertex_count) ? "under" : "over"); + pass, (pass == 0) ? "guess" : (pass <= kInterpolationPasses) ? "lerp" : "binary", + grid_size, int(vertices), + (vertices <= target_vertex_count) ? "under" : "over"); #endif float tip = interpolate(float(target_vertex_count), float(min_grid), float(min_vertices), float(grid_size), float(vertices), float(max_grid), float(max_vertices)); @@ -1522,25 +1866,43 @@ size_t meshopt_simplifyPoints(unsigned int* destination, const float* vertex_pos computeVertexIds(vertex_ids, vertex_positions, vertex_count, min_grid); size_t cell_count = fillVertexCells(table, table_size, vertex_cells, vertex_ids, vertex_count); - // build a quadric for each target cell - Quadric* cell_quadrics = allocator.allocate(cell_count); - memset(cell_quadrics, 0, cell_count * sizeof(Quadric)); + // accumulate points into a reservoir for each target cell + Reservoir* cell_reservoirs = allocator.allocate(cell_count); + memset(cell_reservoirs, 0, cell_count * sizeof(Reservoir)); - fillCellQuadrics(cell_quadrics, vertex_positions, vertex_count, vertex_cells); + fillCellReservoirs(cell_reservoirs, cell_count, vertex_positions, vertex_colors, vertex_colors_stride, vertex_count, vertex_cells); // for each target cell, find the vertex with the minimal error unsigned int* cell_remap = allocator.allocate(cell_count); float* cell_errors = allocator.allocate(cell_count); - fillCellRemap(cell_remap, cell_errors, cell_count, vertex_cells, cell_quadrics, vertex_positions, vertex_count); + fillCellRemap(cell_remap, cell_errors, cell_count, vertex_cells, cell_reservoirs, vertex_positions, vertex_colors, vertex_colors_stride, color_weight * color_weight, vertex_count); // copy results to the output assert(cell_count <= target_vertex_count); memcpy(destination, cell_remap, sizeof(unsigned int) * cell_count); #if TRACE - printf("result: %d cells\n", int(cell_count)); + // compute error + float result_error = 0.f; + + for (size_t i = 0; i < cell_count; ++i) + result_error = result_error < cell_errors[i] ? cell_errors[i] : result_error; + + printf("result: %d cells, %e error\n", int(cell_count), sqrtf(result_error)); #endif return cell_count; } + +float meshopt_simplifyScale(const float* vertex_positions, size_t vertex_count, size_t vertex_positions_stride) +{ + using namespace meshopt; + + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); + assert(vertex_positions_stride % sizeof(float) == 0); + + float extent = rescalePositions(NULL, vertex_positions, vertex_count, vertex_positions_stride); + + return extent; +} diff --git a/Source/ThirdParty/meshoptimizer/spatialorder.cpp b/Source/ThirdParty/meshoptimizer/spatialorder.cpp index b09f80ac6..7b1a06945 100644 --- a/Source/ThirdParty/meshoptimizer/spatialorder.cpp +++ b/Source/ThirdParty/meshoptimizer/spatialorder.cpp @@ -113,7 +113,7 @@ void meshopt_spatialSortRemap(unsigned int* destination, const float* vertex_pos { using namespace meshopt; - assert(vertex_positions_stride > 0 && vertex_positions_stride <= 256); + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); assert(vertex_positions_stride % sizeof(float) == 0); meshopt_Allocator allocator; @@ -144,7 +144,7 @@ void meshopt_spatialSortTriangles(unsigned int* destination, const unsigned int* using namespace meshopt; assert(index_count % 3 == 0); - assert(vertex_positions_stride > 0 && vertex_positions_stride <= 256); + assert(vertex_positions_stride >= 12 && vertex_positions_stride <= 256); assert(vertex_positions_stride % sizeof(float) == 0); (void)vertex_count; diff --git a/Source/ThirdParty/meshoptimizer/vcacheoptimizer.cpp b/Source/ThirdParty/meshoptimizer/vcacheoptimizer.cpp index fb8ade4b7..d4b08ba34 100644 --- a/Source/ThirdParty/meshoptimizer/vcacheoptimizer.cpp +++ b/Source/ThirdParty/meshoptimizer/vcacheoptimizer.cpp @@ -110,7 +110,7 @@ static unsigned int getNextVertexDeadEnd(const unsigned int* dead_end, unsigned return ~0u; } -static unsigned int getNextVertexNeighbour(const unsigned int* next_candidates_begin, const unsigned int* next_candidates_end, const unsigned int* live_triangles, const unsigned int* cache_timestamps, unsigned int timestamp, unsigned int cache_size) +static unsigned int getNextVertexNeighbor(const unsigned int* next_candidates_begin, const unsigned int* next_candidates_end, const unsigned int* live_triangles, const unsigned int* cache_timestamps, unsigned int timestamp, unsigned int cache_size) { unsigned int best_candidate = ~0u; int best_priority = -1; @@ -221,9 +221,9 @@ void meshopt_optimizeVertexCacheTable(unsigned int* destination, const unsigned triangle_scores[i] = vertex_scores[a] + vertex_scores[b] + vertex_scores[c]; } - unsigned int cache_holder[2 * (kCacheSizeMax + 3)]; + unsigned int cache_holder[2 * (kCacheSizeMax + 4)]; unsigned int* cache = cache_holder; - unsigned int* cache_new = cache_holder + kCacheSizeMax + 3; + unsigned int* cache_new = cache_holder + kCacheSizeMax + 4; size_t cache_count = 0; unsigned int current_triangle = 0; @@ -260,10 +260,8 @@ void meshopt_optimizeVertexCacheTable(unsigned int* destination, const unsigned { unsigned int index = cache[i]; - if (index != a && index != b && index != c) - { - cache_new[cache_write++] = index; - } + cache_new[cache_write] = index; + cache_write += (index != a && index != b && index != c); } unsigned int* cache_temp = cache; @@ -281,16 +279,16 @@ void meshopt_optimizeVertexCacheTable(unsigned int* destination, const unsigned { unsigned int index = indices[current_triangle * 3 + k]; - unsigned int* neighbours = &adjacency.data[0] + adjacency.offsets[index]; - size_t neighbours_size = adjacency.counts[index]; + unsigned int* neighbors = &adjacency.data[0] + adjacency.offsets[index]; + size_t neighbors_size = adjacency.counts[index]; - for (size_t i = 0; i < neighbours_size; ++i) + for (size_t i = 0; i < neighbors_size; ++i) { - unsigned int tri = neighbours[i]; + unsigned int tri = neighbors[i]; if (tri == current_triangle) { - neighbours[i] = neighbours[neighbours_size - 1]; + neighbors[i] = neighbors[neighbors_size - 1]; adjacency.counts[index]--; break; } @@ -305,6 +303,10 @@ void meshopt_optimizeVertexCacheTable(unsigned int* destination, const unsigned { unsigned int index = cache[i]; + // no need to update scores if we are never going to use this vertex + if (adjacency.counts[index] == 0) + continue; + int cache_position = i >= cache_size ? -1 : int(i); // update vertex score @@ -314,10 +316,10 @@ void meshopt_optimizeVertexCacheTable(unsigned int* destination, const unsigned vertex_scores[index] = score; // update scores of vertex triangles - const unsigned int* neighbours_begin = &adjacency.data[0] + adjacency.offsets[index]; - const unsigned int* neighbours_end = neighbours_begin + adjacency.counts[index]; + const unsigned int* neighbors_begin = &adjacency.data[0] + adjacency.offsets[index]; + const unsigned int* neighbors_end = neighbors_begin + adjacency.counts[index]; - for (const unsigned int* it = neighbours_begin; it != neighbours_end; ++it) + for (const unsigned int* it = neighbors_begin; it != neighbors_end; ++it) { unsigned int tri = *it; assert(!emitted_flags[tri]); @@ -325,11 +327,8 @@ void meshopt_optimizeVertexCacheTable(unsigned int* destination, const unsigned float tri_score = triangle_scores[tri] + score_diff; assert(tri_score > 0); - if (best_score < tri_score) - { - best_triangle = tri; - best_score = tri_score; - } + best_triangle = best_score < tri_score ? tri : best_triangle; + best_score = best_score < tri_score ? tri_score : best_score; triangle_scores[tri] = tri_score; } @@ -412,11 +411,11 @@ void meshopt_optimizeVertexCacheFifo(unsigned int* destination, const unsigned i { const unsigned int* next_candidates_begin = &dead_end[0] + dead_end_top; - // emit all vertex neighbours - const unsigned int* neighbours_begin = &adjacency.data[0] + adjacency.offsets[current_vertex]; - const unsigned int* neighbours_end = neighbours_begin + adjacency.counts[current_vertex]; + // emit all vertex neighbors + const unsigned int* neighbors_begin = &adjacency.data[0] + adjacency.offsets[current_vertex]; + const unsigned int* neighbors_end = neighbors_begin + adjacency.counts[current_vertex]; - for (const unsigned int* it = neighbours_begin; it != neighbours_end; ++it) + for (const unsigned int* it = neighbors_begin; it != neighbors_end; ++it) { unsigned int triangle = *it; @@ -461,7 +460,7 @@ void meshopt_optimizeVertexCacheFifo(unsigned int* destination, const unsigned i const unsigned int* next_candidates_end = &dead_end[0] + dead_end_top; // get next vertex - current_vertex = getNextVertexNeighbour(next_candidates_begin, next_candidates_end, &live_triangles[0], &cache_timestamps[0], timestamp, cache_size); + current_vertex = getNextVertexNeighbor(next_candidates_begin, next_candidates_end, &live_triangles[0], &cache_timestamps[0], timestamp, cache_size); if (current_vertex == ~0u) { diff --git a/Source/ThirdParty/meshoptimizer/vertexcodec.cpp b/Source/ThirdParty/meshoptimizer/vertexcodec.cpp index 30fbcd454..8ab0662d8 100644 --- a/Source/ThirdParty/meshoptimizer/vertexcodec.cpp +++ b/Source/ThirdParty/meshoptimizer/vertexcodec.cpp @@ -42,16 +42,24 @@ #endif // When targeting Wasm SIMD we can't use runtime cpuid checks so we unconditionally enable SIMD -// Note that we need unimplemented-simd128 subset for a few functions that are implemented de-facto #if defined(__wasm_simd128__) #define SIMD_WASM -#define SIMD_TARGET __attribute__((target("unimplemented-simd128"))) +// Prevent compiling other variant when wasm simd compilation is active +#undef SIMD_NEON +#undef SIMD_SSE +#undef SIMD_AVX #endif #ifndef SIMD_TARGET #define SIMD_TARGET #endif +// When targeting AArch64/x64, optimize for latency to allow decoding of individual 16-byte groups to overlap +// We don't do this for 32-bit systems because we need 64-bit math for this and this will hurt in-order CPUs +#if defined(__x86_64__) || defined(_M_X64) || defined(__aarch64__) || defined(_M_ARM64) +#define SIMD_LATENCYOPT +#endif + #endif // !MESHOPTIMIZER_NO_SIMD #ifdef SIMD_SSE @@ -82,31 +90,14 @@ #include #endif -#ifndef TRACE -#define TRACE 0 -#endif - -#if TRACE -#include -#endif - #ifdef SIMD_WASM -#define wasmx_splat_v32x4(v, i) wasm_v32x4_shuffle(v, v, i, i, i, i) -#define wasmx_unpacklo_v8x16(a, b) wasm_v8x16_shuffle(a, b, 0, 16, 1, 17, 2, 18, 3, 19, 4, 20, 5, 21, 6, 22, 7, 23) -#define wasmx_unpackhi_v8x16(a, b) wasm_v8x16_shuffle(a, b, 8, 24, 9, 25, 10, 26, 11, 27, 12, 28, 13, 29, 14, 30, 15, 31) -#define wasmx_unpacklo_v16x8(a, b) wasm_v16x8_shuffle(a, b, 0, 8, 1, 9, 2, 10, 3, 11) -#define wasmx_unpackhi_v16x8(a, b) wasm_v16x8_shuffle(a, b, 4, 12, 5, 13, 6, 14, 7, 15) -#define wasmx_unpacklo_v64x2(a, b) wasm_v64x2_shuffle(a, b, 0, 2) -#define wasmx_unpackhi_v64x2(a, b) wasm_v64x2_shuffle(a, b, 1, 3) -#endif - -#if defined(SIMD_WASM) -// v128_t wasm_v8x16_swizzle(v128_t a, v128_t b) -SIMD_TARGET -static __inline__ v128_t wasm_v8x16_swizzle(v128_t a, v128_t b) -{ - return (v128_t)__builtin_wasm_swizzle_v8x16((__i8x16)a, (__i8x16)b); -} +#define wasmx_splat_v32x4(v, i) wasm_i32x4_shuffle(v, v, i, i, i, i) +#define wasmx_unpacklo_v8x16(a, b) wasm_i8x16_shuffle(a, b, 0, 16, 1, 17, 2, 18, 3, 19, 4, 20, 5, 21, 6, 22, 7, 23) +#define wasmx_unpackhi_v8x16(a, b) wasm_i8x16_shuffle(a, b, 8, 24, 9, 25, 10, 26, 11, 27, 12, 28, 13, 29, 14, 30, 15, 31) +#define wasmx_unpacklo_v16x8(a, b) wasm_i16x8_shuffle(a, b, 0, 8, 1, 9, 2, 10, 3, 11) +#define wasmx_unpackhi_v16x8(a, b) wasm_i16x8_shuffle(a, b, 4, 12, 5, 13, 6, 14, 7, 15) +#define wasmx_unpacklo_v64x2(a, b) wasm_i64x2_shuffle(a, b, 0, 2) +#define wasmx_unpackhi_v64x2(a, b) wasm_i64x2_shuffle(a, b, 1, 3) #endif namespace meshopt @@ -144,19 +135,6 @@ inline unsigned char unzigzag8(unsigned char v) return -(v & 1) ^ (v >> 1); } -#if TRACE -struct Stats -{ - size_t size; - size_t header; - size_t bitg[4]; - size_t bitb[4]; -}; - -Stats* bytestats; -Stats vertexstats[256]; -#endif - static bool encodeBytesGroupZero(const unsigned char* buffer) { for (size_t i = 0; i < kByteGroupSize; ++i) @@ -242,7 +220,7 @@ static unsigned char* encodeBytes(unsigned char* data, unsigned char* data_end, size_t header_size = (buffer_size / kByteGroupSize + 3) / 4; if (size_t(data_end - data) < header_size) - return 0; + return NULL; data += header_size; @@ -251,7 +229,7 @@ static unsigned char* encodeBytes(unsigned char* data, unsigned char* data_end, for (size_t i = 0; i < buffer_size; i += kByteGroupSize) { if (size_t(data_end - data) < kByteGroupDecodeLimit) - return 0; + return NULL; int best_bits = 8; size_t best_size = encodeBytesGroupMeasure(buffer + i, 8); @@ -278,17 +256,8 @@ static unsigned char* encodeBytes(unsigned char* data, unsigned char* data_end, assert(data + best_size == next); data = next; - -#if TRACE > 1 - bytestats->bitg[bitslog2]++; - bytestats->bitb[bitslog2] += best_size; -#endif } -#if TRACE > 1 - bytestats->header += header_size; -#endif - return data; } @@ -317,19 +286,9 @@ static unsigned char* encodeVertexBlock(unsigned char* data, unsigned char* data vertex_offset += vertex_size; } -#if TRACE - const unsigned char* olddata = data; - bytestats = &vertexstats[k]; -#endif - data = encodeBytes(data, data_end, buffer, (vertex_count + kByteGroupSize - 1) & ~(kByteGroupSize - 1)); if (!data) - return 0; - -#if TRACE - bytestats = 0; - vertexstats[k].size += data - olddata; -#endif + return NULL; } memcpy(last_vertex, &vertex_data[vertex_size * (vertex_count - 1)], vertex_size); @@ -337,7 +296,7 @@ static unsigned char* encodeVertexBlock(unsigned char* data, unsigned char* data return data; } -#if defined(SIMD_FALLBACK) || (!defined(SIMD_SSE) && !defined(SIMD_NEON) && !defined(SIMD_AVX)) +#if defined(SIMD_FALLBACK) || (!defined(SIMD_SSE) && !defined(SIMD_NEON) && !defined(SIMD_AVX) && !defined(SIMD_WASM)) static const unsigned char* decodeBytesGroup(const unsigned char* data, unsigned char* buffer, int bitslog2) { #define READ() byte = *data++ @@ -397,14 +356,14 @@ static const unsigned char* decodeBytes(const unsigned char* data, const unsigne size_t header_size = (buffer_size / kByteGroupSize + 3) / 4; if (size_t(data_end - data) < header_size) - return 0; + return NULL; data += header_size; for (size_t i = 0; i < buffer_size; i += kByteGroupSize) { if (size_t(data_end - data) < kByteGroupDecodeLimit) - return 0; + return NULL; size_t header_offset = i / kByteGroupSize; @@ -429,7 +388,7 @@ static const unsigned char* decodeVertexBlock(const unsigned char* data, const u { data = decodeBytes(data, data_end, buffer, vertex_count_aligned); if (!data) - return 0; + return NULL; size_t vertex_offset = k; @@ -458,7 +417,7 @@ static const unsigned char* decodeVertexBlock(const unsigned char* data, const u static unsigned char kDecodeBytesGroupShuffle[256][8]; static unsigned char kDecodeBytesGroupCount[256]; -#ifdef EMSCRIPTEN +#ifdef __wasm__ __attribute__((cold)) // this saves 500 bytes in the output binary - we don't need to vectorize this loop! #endif static bool @@ -521,6 +480,18 @@ static const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsi typedef int unaligned_int; #endif +#ifdef SIMD_LATENCYOPT + unsigned int data32; + memcpy(&data32, data, 4); + data32 &= data32 >> 1; + + // arrange bits such that low bits of nibbles of data64 contain all 2-bit elements of data32 + unsigned long long data64 = ((unsigned long long)data32 << 30) | (data32 & 0x3fffffff); + + // adds all 1-bit nibbles together; the sum fits in 4 bits because datacnt=16 would have used mode 3 + int datacnt = int(((data64 & 0x1111111111111111ull) * 0x1111111111111111ull) >> 60); +#endif + __m128i sel2 = _mm_cvtsi32_si128(*reinterpret_cast(data)); __m128i rest = _mm_loadu_si128(reinterpret_cast(data + 4)); @@ -539,11 +510,25 @@ static const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsi _mm_storeu_si128(reinterpret_cast<__m128i*>(buffer), result); +#ifdef SIMD_LATENCYOPT + return data + 4 + datacnt; +#else return data + 4 + kDecodeBytesGroupCount[mask0] + kDecodeBytesGroupCount[mask1]; +#endif } case 2: { +#ifdef SIMD_LATENCYOPT + unsigned long long data64; + memcpy(&data64, data, 8); + data64 &= data64 >> 1; + data64 &= data64 >> 2; + + // adds all 1-bit nibbles together; the sum fits in 4 bits because datacnt=16 would have used mode 3 + int datacnt = int(((data64 & 0x1111111111111111ull) * 0x1111111111111111ull) >> 60); +#endif + __m128i sel4 = _mm_loadl_epi64(reinterpret_cast(data)); __m128i rest = _mm_loadu_si128(reinterpret_cast(data + 8)); @@ -561,7 +546,11 @@ static const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsi _mm_storeu_si128(reinterpret_cast<__m128i*>(buffer), result); +#ifdef SIMD_LATENCYOPT + return data + 8 + datacnt; +#else return data + 8 + kDecodeBytesGroupCount[mask0] + kDecodeBytesGroupCount[mask1]; +#endif } case 3: @@ -653,24 +642,13 @@ static uint8x16_t shuffleBytes(unsigned char mask0, unsigned char mask1, uint8x8 static void neonMoveMask(uint8x16_t mask, unsigned char& mask0, unsigned char& mask1) { - static const unsigned char byte_mask_data[16] = {1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128}; + // magic constant found using z3 SMT assuming mask has 8 groups of 0xff or 0x00 + const uint64_t magic = 0x000103070f1f3f80ull; - uint8x16_t byte_mask = vld1q_u8(byte_mask_data); - uint8x16_t masked = vandq_u8(mask, byte_mask); + uint64x2_t mask2 = vreinterpretq_u64_u8(mask); -#ifdef __aarch64__ - // aarch64 has horizontal sums; MSVC doesn't expose this via arm64_neon.h so this path is exclusive to clang/gcc - mask0 = vaddv_u8(vget_low_u8(masked)); - mask1 = vaddv_u8(vget_high_u8(masked)); -#else - // we need horizontal sums of each half of masked, which can be done in 3 steps (yielding sums of sizes 2, 4, 8) - uint8x8_t sum1 = vpadd_u8(vget_low_u8(masked), vget_high_u8(masked)); - uint8x8_t sum2 = vpadd_u8(sum1, sum1); - uint8x8_t sum3 = vpadd_u8(sum2, sum2); - - mask0 = vget_lane_u8(sum3, 0); - mask1 = vget_lane_u8(sum3, 1); -#endif + mask0 = uint8_t((vgetq_lane_u64(mask2, 0) * magic) >> 56); + mask1 = uint8_t((vgetq_lane_u64(mask2, 1) * magic) >> 56); } static const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsigned char* buffer, int bitslog2) @@ -688,6 +666,18 @@ static const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsi case 1: { +#ifdef SIMD_LATENCYOPT + unsigned int data32; + memcpy(&data32, data, 4); + data32 &= data32 >> 1; + + // arrange bits such that low bits of nibbles of data64 contain all 2-bit elements of data32 + unsigned long long data64 = ((unsigned long long)data32 << 30) | (data32 & 0x3fffffff); + + // adds all 1-bit nibbles together; the sum fits in 4 bits because datacnt=16 would have used mode 3 + int datacnt = int(((data64 & 0x1111111111111111ull) * 0x1111111111111111ull) >> 60); +#endif + uint8x8_t sel2 = vld1_u8(data); uint8x8_t sel22 = vzip_u8(vshr_n_u8(sel2, 4), sel2).val[0]; uint8x8x2_t sel2222 = vzip_u8(vshr_n_u8(sel22, 2), sel22); @@ -704,11 +694,25 @@ static const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsi vst1q_u8(buffer, result); +#ifdef SIMD_LATENCYOPT + return data + 4 + datacnt; +#else return data + 4 + kDecodeBytesGroupCount[mask0] + kDecodeBytesGroupCount[mask1]; +#endif } case 2: { +#ifdef SIMD_LATENCYOPT + unsigned long long data64; + memcpy(&data64, data, 8); + data64 &= data64 >> 1; + data64 &= data64 >> 2; + + // adds all 1-bit nibbles together; the sum fits in 4 bits because datacnt=16 would have used mode 3 + int datacnt = int(((data64 & 0x1111111111111111ull) * 0x1111111111111111ull) >> 60); +#endif + uint8x8_t sel4 = vld1_u8(data); uint8x8x2_t sel44 = vzip_u8(vshr_n_u8(sel4, 4), vand_u8(sel4, vdup_n_u8(15))); uint8x16_t sel = vcombine_u8(sel44.val[0], sel44.val[1]); @@ -724,7 +728,11 @@ static const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsi vst1q_u8(buffer, result); +#ifdef SIMD_LATENCYOPT + return data + 8 + datacnt; +#else return data + 8 + kDecodeBytesGroupCount[mask0] + kDecodeBytesGroupCount[mask1]; +#endif } case 3: @@ -747,13 +755,11 @@ static const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsi SIMD_TARGET static v128_t decodeShuffleMask(unsigned char mask0, unsigned char mask1) { - // TODO: 8b buffer overrun - should we use splat or extend buffers? v128_t sm0 = wasm_v128_load(&kDecodeBytesGroupShuffle[mask0]); v128_t sm1 = wasm_v128_load(&kDecodeBytesGroupShuffle[mask1]); - // TODO: we should use v8x16_load_splat v128_t sm1off = wasm_v128_load(&kDecodeBytesGroupCount[mask0]); - sm1off = wasm_v8x16_shuffle(sm1off, sm1off, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + sm1off = wasm_i8x16_shuffle(sm1off, sm1off, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); v128_t sm1r = wasm_i8x16_add(sm1, sm1off); @@ -763,26 +769,16 @@ static v128_t decodeShuffleMask(unsigned char mask0, unsigned char mask1) SIMD_TARGET static void wasmMoveMask(v128_t mask, unsigned char& mask0, unsigned char& mask1) { - v128_t mask_0 = wasm_v32x4_shuffle(mask, mask, 0, 2, 1, 3); + // magic constant found using z3 SMT assuming mask has 8 groups of 0xff or 0x00 + const uint64_t magic = 0x000103070f1f3f80ull; - // TODO: when Chrome supports v128.const we can try doing vectorized and? - uint64_t mask_1a = wasm_i64x2_extract_lane(mask_0, 0) & 0x0804020108040201ull; - uint64_t mask_1b = wasm_i64x2_extract_lane(mask_0, 1) & 0x8040201080402010ull; - - uint64_t mask_2 = mask_1a | mask_1b; - uint64_t mask_4 = mask_2 | (mask_2 >> 16); - uint64_t mask_8 = mask_4 | (mask_4 >> 8); - - mask0 = uint8_t(mask_8); - mask1 = uint8_t(mask_8 >> 32); + mask0 = uint8_t((wasm_i64x2_extract_lane(mask, 0) * magic) >> 56); + mask1 = uint8_t((wasm_i64x2_extract_lane(mask, 1) * magic) >> 56); } SIMD_TARGET static const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsigned char* buffer, int bitslog2) { - unsigned char byte, enc, encv; - const unsigned char* data_var; - switch (bitslog2) { case 0: @@ -796,7 +792,6 @@ static const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsi case 1: { - // TODO: test 4b load splat v128_t sel2 = wasm_v128_load(data); v128_t rest = wasm_v128_load(data + 4); @@ -811,8 +806,7 @@ static const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsi v128_t shuf = decodeShuffleMask(mask0, mask1); - // TODO: test or/andnot - v128_t result = wasm_v128_bitselect(wasm_v8x16_swizzle(rest, shuf), sel, mask); + v128_t result = wasm_v128_bitselect(wasm_i8x16_swizzle(rest, shuf), sel, mask); wasm_v128_store(buffer, result); @@ -821,7 +815,6 @@ static const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsi case 2: { - // TODO: test 8b load splat v128_t sel4 = wasm_v128_load(data); v128_t rest = wasm_v128_load(data + 8); @@ -835,8 +828,7 @@ static const unsigned char* decodeBytesGroupSimd(const unsigned char* data, unsi v128_t shuf = decodeShuffleMask(mask0, mask1); - // TODO: test or/andnot - v128_t result = wasm_v128_bitselect(wasm_v8x16_swizzle(rest, shuf), sel, mask); + v128_t result = wasm_v128_bitselect(wasm_i8x16_swizzle(rest, shuf), sel, mask); wasm_v128_store(buffer, result); @@ -927,8 +919,7 @@ SIMD_TARGET static v128_t unzigzag8(v128_t v) { v128_t xl = wasm_i8x16_neg(wasm_v128_and(v, wasm_i8x16_splat(1))); - // TODO: use wasm_u8x16_shr when v8 fixes codegen for constant shifts - v128_t xr = wasm_v128_and(wasm_u16x8_shr(v, 1), wasm_i8x16_splat(127)); + v128_t xr = wasm_u8x16_shr(v, 1); return wasm_v128_xor(xl, xr); } @@ -947,7 +938,7 @@ static const unsigned char* decodeBytesSimd(const unsigned char* data, const uns size_t header_size = (buffer_size / kByteGroupSize + 3) / 4; if (size_t(data_end - data) < header_size) - return 0; + return NULL; data += header_size; @@ -969,7 +960,7 @@ static const unsigned char* decodeBytesSimd(const unsigned char* data, const uns for (; i < buffer_size; i += kByteGroupSize) { if (size_t(data_end - data) < kByteGroupDecodeLimit) - return 0; + return NULL; size_t header_offset = i / kByteGroupSize; @@ -997,7 +988,7 @@ static const unsigned char* decodeVertexBlockSimd(const unsigned char* data, con { data = decodeBytesSimd(data, data_end, buffer + j * vertex_count_aligned, vertex_count_aligned); if (!data) - return 0; + return NULL; } #if defined(SIMD_SSE) || defined(SIMD_AVX) @@ -1020,7 +1011,7 @@ static const unsigned char* decodeVertexBlockSimd(const unsigned char* data, con #ifdef SIMD_WASM #define TEMP v128_t -#define PREP() v128_t pi = wasm_v128_load(last_vertex + k) // TODO: use wasm_v32x4_load_splat to avoid buffer overrun +#define PREP() v128_t pi = wasm_v128_load(last_vertex + k) #define LOAD(i) v128_t r##i = wasm_v128_load(buffer + j + i * vertex_count_aligned) #define GRP4(i) t0 = wasmx_splat_v32x4(r##i, 0), t1 = wasmx_splat_v32x4(r##i, 1), t2 = wasmx_splat_v32x4(r##i, 2), t3 = wasmx_splat_v32x4(r##i, 3) #define FIXD(i) t##i = pi = wasm_i8x16_add(pi, t##i) @@ -1092,7 +1083,7 @@ static unsigned int getCpuFeatures() return cpuinfo[2]; } -unsigned int cpuid = getCpuFeatures(); +static unsigned int cpuid = getCpuFeatures(); #endif } // namespace meshopt @@ -1104,10 +1095,6 @@ size_t meshopt_encodeVertexBuffer(unsigned char* buffer, size_t buffer_size, con assert(vertex_size > 0 && vertex_size <= 256); assert(vertex_size % 4 == 0); -#if TRACE - memset(vertexstats, 0, sizeof(vertexstats)); -#endif - const unsigned char* vertex_data = static_cast(vertices); unsigned char* data = buffer; @@ -1160,28 +1147,6 @@ size_t meshopt_encodeVertexBuffer(unsigned char* buffer, size_t buffer_size, con assert(data >= buffer + tail_size); assert(data <= buffer + buffer_size); -#if TRACE - size_t total_size = data - buffer; - - for (size_t k = 0; k < vertex_size; ++k) - { - const Stats& vsk = vertexstats[k]; - - printf("%2d: %d bytes\t%.1f%%\t%.1f bpv", int(k), int(vsk.size), double(vsk.size) / double(total_size) * 100, double(vsk.size) / double(vertex_count) * 8); - -#if TRACE > 1 - printf("\t\thdr %d bytes\tbit0 %d (%d bytes)\tbit1 %d (%d bytes)\tbit2 %d (%d bytes)\tbit3 %d (%d bytes)", - int(vsk.header), - int(vsk.bitg[0]), int(vsk.bitb[0]), - int(vsk.bitg[1]), int(vsk.bitb[1]), - int(vsk.bitg[2]), int(vsk.bitb[2]), - int(vsk.bitg[3]), int(vsk.bitb[3])); -#endif - - printf("\n"); - } -#endif - return data - buffer; } @@ -1217,7 +1182,7 @@ int meshopt_decodeVertexBuffer(void* destination, size_t vertex_count, size_t ve assert(vertex_size > 0 && vertex_size <= 256); assert(vertex_size % 4 == 0); - const unsigned char* (*decode)(const unsigned char*, const unsigned char*, unsigned char*, size_t, size_t, unsigned char[256]) = 0; + const unsigned char* (*decode)(const unsigned char*, const unsigned char*, unsigned char*, size_t, size_t, unsigned char[256]) = NULL; #if defined(SIMD_SSE) && defined(SIMD_FALLBACK) decode = (cpuid & (1 << 9)) ? decodeVertexBlockSimd : decodeVertexBlock; diff --git a/Source/ThirdParty/meshoptimizer/vertexfilter.cpp b/Source/ThirdParty/meshoptimizer/vertexfilter.cpp index e07d11a7d..4b5f444f0 100644 --- a/Source/ThirdParty/meshoptimizer/vertexfilter.cpp +++ b/Source/ThirdParty/meshoptimizer/vertexfilter.cpp @@ -2,6 +2,7 @@ #include "meshoptimizer.h" #include +#include // The block below auto-detects SIMD ISA that can be used on the target platform #ifndef MESHOPTIMIZER_NO_SIMD @@ -29,6 +30,9 @@ // When targeting Wasm SIMD we can't use runtime cpuid checks so we unconditionally enable SIMD #if defined(__wasm_simd128__) #define SIMD_WASM +// Prevent compiling other variant when wasm simd compilation is active +#undef SIMD_NEON +#undef SIMD_SSE #endif #endif // !MESHOPTIMIZER_NO_SIMD @@ -51,6 +55,7 @@ #endif #ifdef SIMD_WASM +#undef __DEPRECATED #include #endif @@ -61,6 +66,10 @@ #define wasmx_unziphi_v32x4(a, b) wasm_v32x4_shuffle(a, b, 1, 3, 5, 7) #endif +#ifndef __has_builtin +#define __has_builtin(x) 0 +#endif + namespace meshopt { @@ -143,7 +152,8 @@ static void decodeFilterExp(unsigned int* data, size_t count) int m = int(v << 8) >> 8; int e = int(v) >> 24; - union { + union + { float f; unsigned int ui; } u; @@ -158,11 +168,31 @@ static void decodeFilterExp(unsigned int* data, size_t count) #endif #if defined(SIMD_SSE) || defined(SIMD_NEON) || defined(SIMD_WASM) +template +static void dispatchSimd(void (*process)(T*, size_t), T* data, size_t count, size_t stride) +{ + assert(stride <= 4); + + size_t count4 = count & ~size_t(3); + process(data, count4); + + if (count4 < count) + { + T tail[4 * 4] = {}; // max stride 4, max count 4 + size_t tail_size = (count - count4) * stride * sizeof(T); + assert(tail_size <= sizeof(tail)); + + memcpy(tail, data + count4 * stride, tail_size); + process(tail, count - count4); + memcpy(data + count4 * stride, tail, tail_size); + } +} + inline uint64_t rotateleft64(uint64_t v, int x) { #if defined(_MSC_VER) && !defined(__clang__) return _rotl64(v, x); -#elif defined(__clang__) && __clang_major__ >= 8 +#elif defined(__clang__) && __has_builtin(__builtin_rotateleft64) return __builtin_rotateleft64(v, x); #else return (v << (x & 63)) | (v >> ((64 - x) & 63)); @@ -620,7 +650,7 @@ static void decodeFilterOctSimd(signed char* data, size_t count) static void decodeFilterOctSimd(short* data, size_t count) { const v128_t sign = wasm_f32x4_splat(-0.f); - volatile v128_t zmask = wasm_i32x4_splat(0x7fff); // TODO: volatile works around LLVM shuffle "optimizations" + const v128_t zmask = wasm_i32x4_splat(0x7fff); for (size_t i = 0; i < count; i += 4) { @@ -732,7 +762,8 @@ static void decodeFilterQuatSimd(short* data, size_t count) v128_t res_1 = wasmx_unpackhi_v16x8(wyr, xzr); // compute component index shifted left by 4 (and moved into i32x4 slot) - v128_t cm = wasm_i32x4_shl(cf, 4); + // TODO: volatile here works around LLVM mis-optimizing code; https://github.com/emscripten-core/emscripten/issues/11449 + volatile v128_t cm = wasm_i32x4_shl(cf, 4); // rotate and store uint64_t* out = reinterpret_cast(&data[i * 4]); @@ -765,57 +796,238 @@ static void decodeFilterExpSimd(unsigned int* data, size_t count) } #endif +// optimized variant of frexp +inline int optlog2(float v) +{ + union + { + float f; + unsigned int ui; + } u; + + u.f = v; + // +1 accounts for implicit 1. in mantissa; denormalized numbers will end up clamped to min_exp by calling code + return u.ui == 0 ? 0 : int((u.ui >> 23) & 0xff) - 127 + 1; +} + +// optimized variant of ldexp +inline float optexp2(int e) +{ + union + { + float f; + unsigned int ui; + } u; + + u.ui = unsigned(e + 127) << 23; + return u.f; +} + } // namespace meshopt -void meshopt_decodeFilterOct(void* buffer, size_t vertex_count, size_t vertex_size) +void meshopt_decodeFilterOct(void* buffer, size_t count, size_t stride) { using namespace meshopt; - assert(vertex_count % 4 == 0); - assert(vertex_size == 4 || vertex_size == 8); + assert(stride == 4 || stride == 8); #if defined(SIMD_SSE) || defined(SIMD_NEON) || defined(SIMD_WASM) - if (vertex_size == 4) - decodeFilterOctSimd(static_cast(buffer), vertex_count); + if (stride == 4) + dispatchSimd(decodeFilterOctSimd, static_cast(buffer), count, 4); else - decodeFilterOctSimd(static_cast(buffer), vertex_count); + dispatchSimd(decodeFilterOctSimd, static_cast(buffer), count, 4); #else - if (vertex_size == 4) - decodeFilterOct(static_cast(buffer), vertex_count); + if (stride == 4) + decodeFilterOct(static_cast(buffer), count); else - decodeFilterOct(static_cast(buffer), vertex_count); + decodeFilterOct(static_cast(buffer), count); #endif } -void meshopt_decodeFilterQuat(void* buffer, size_t vertex_count, size_t vertex_size) +void meshopt_decodeFilterQuat(void* buffer, size_t count, size_t stride) { using namespace meshopt; - assert(vertex_count % 4 == 0); - assert(vertex_size == 8); - (void)vertex_size; + assert(stride == 8); + (void)stride; #if defined(SIMD_SSE) || defined(SIMD_NEON) || defined(SIMD_WASM) - decodeFilterQuatSimd(static_cast(buffer), vertex_count); + dispatchSimd(decodeFilterQuatSimd, static_cast(buffer), count, 4); #else - decodeFilterQuat(static_cast(buffer), vertex_count); + decodeFilterQuat(static_cast(buffer), count); #endif } -void meshopt_decodeFilterExp(void* buffer, size_t vertex_count, size_t vertex_size) +void meshopt_decodeFilterExp(void* buffer, size_t count, size_t stride) { using namespace meshopt; - assert(vertex_count % 4 == 0); - assert(vertex_size % 4 == 0); + assert(stride > 0 && stride % 4 == 0); #if defined(SIMD_SSE) || defined(SIMD_NEON) || defined(SIMD_WASM) - decodeFilterExpSimd(static_cast(buffer), vertex_count * (vertex_size / 4)); + dispatchSimd(decodeFilterExpSimd, static_cast(buffer), count * (stride / 4), 1); #else - decodeFilterExp(static_cast(buffer), vertex_count * (vertex_size / 4)); + decodeFilterExp(static_cast(buffer), count * (stride / 4)); #endif } +void meshopt_encodeFilterOct(void* destination, size_t count, size_t stride, int bits, const float* data) +{ + assert(stride == 4 || stride == 8); + assert(bits >= 1 && bits <= 16); + + signed char* d8 = static_cast(destination); + short* d16 = static_cast(destination); + + int bytebits = int(stride * 2); + + for (size_t i = 0; i < count; ++i) + { + const float* n = &data[i * 4]; + + // octahedral encoding of a unit vector + float nx = n[0], ny = n[1], nz = n[2], nw = n[3]; + float nl = fabsf(nx) + fabsf(ny) + fabsf(nz); + float ns = nl == 0.f ? 0.f : 1.f / nl; + + nx *= ns; + ny *= ns; + + float u = (nz >= 0.f) ? nx : (1 - fabsf(ny)) * (nx >= 0.f ? 1.f : -1.f); + float v = (nz >= 0.f) ? ny : (1 - fabsf(nx)) * (ny >= 0.f ? 1.f : -1.f); + + int fu = meshopt_quantizeSnorm(u, bits); + int fv = meshopt_quantizeSnorm(v, bits); + int fo = meshopt_quantizeSnorm(1.f, bits); + int fw = meshopt_quantizeSnorm(nw, bytebits); + + if (stride == 4) + { + d8[i * 4 + 0] = (signed char)(fu); + d8[i * 4 + 1] = (signed char)(fv); + d8[i * 4 + 2] = (signed char)(fo); + d8[i * 4 + 3] = (signed char)(fw); + } + else + { + d16[i * 4 + 0] = short(fu); + d16[i * 4 + 1] = short(fv); + d16[i * 4 + 2] = short(fo); + d16[i * 4 + 3] = short(fw); + } + } +} + +void meshopt_encodeFilterQuat(void* destination_, size_t count, size_t stride, int bits, const float* data) +{ + assert(stride == 8); + assert(bits >= 4 && bits <= 16); + (void)stride; + + short* destination = static_cast(destination_); + + const float scaler = sqrtf(2.f); + + for (size_t i = 0; i < count; ++i) + { + const float* q = &data[i * 4]; + short* d = &destination[i * 4]; + + // establish maximum quaternion component + int qc = 0; + qc = fabsf(q[1]) > fabsf(q[qc]) ? 1 : qc; + qc = fabsf(q[2]) > fabsf(q[qc]) ? 2 : qc; + qc = fabsf(q[3]) > fabsf(q[qc]) ? 3 : qc; + + // we use double-cover properties to discard the sign + float sign = q[qc] < 0.f ? -1.f : 1.f; + + // note: we always encode a cyclical swizzle to be able to recover the order via rotation + d[0] = short(meshopt_quantizeSnorm(q[(qc + 1) & 3] * scaler * sign, bits)); + d[1] = short(meshopt_quantizeSnorm(q[(qc + 2) & 3] * scaler * sign, bits)); + d[2] = short(meshopt_quantizeSnorm(q[(qc + 3) & 3] * scaler * sign, bits)); + d[3] = short((meshopt_quantizeSnorm(1.f, bits) & ~3) | qc); + } +} + +void meshopt_encodeFilterExp(void* destination_, size_t count, size_t stride, int bits, const float* data, enum meshopt_EncodeExpMode mode) +{ + using namespace meshopt; + + assert(stride > 0 && stride % 4 == 0 && stride <= 256); + assert(bits >= 1 && bits <= 24); + + unsigned int* destination = static_cast(destination_); + size_t stride_float = stride / sizeof(float); + + int component_exp[64]; + assert(stride_float <= sizeof(component_exp) / sizeof(int)); + + const int min_exp = -100; + + if (mode == meshopt_EncodeExpSharedComponent) + { + for (size_t j = 0; j < stride_float; ++j) + component_exp[j] = min_exp; + + for (size_t i = 0; i < count; ++i) + { + const float* v = &data[i * stride_float]; + + // use maximum exponent to encode values; this guarantees that mantissa is [-1, 1] + for (size_t j = 0; j < stride_float; ++j) + { + int e = optlog2(v[j]); + + component_exp[j] = (component_exp[j] < e) ? e : component_exp[j]; + } + } + } + + for (size_t i = 0; i < count; ++i) + { + const float* v = &data[i * stride_float]; + unsigned int* d = &destination[i * stride_float]; + + int vector_exp = min_exp; + + if (mode == meshopt_EncodeExpSharedVector) + { + // use maximum exponent to encode values; this guarantees that mantissa is [-1, 1] + for (size_t j = 0; j < stride_float; ++j) + { + int e = optlog2(v[j]); + + vector_exp = (vector_exp < e) ? e : vector_exp; + } + } + else if (mode == meshopt_EncodeExpSeparate) + { + for (size_t j = 0; j < stride_float; ++j) + { + int e = optlog2(v[j]); + + component_exp[j] = (min_exp < e) ? e : min_exp; + } + } + + for (size_t j = 0; j < stride_float; ++j) + { + int exp = (mode == meshopt_EncodeExpSharedVector) ? vector_exp : component_exp[j]; + + // note that we additionally scale the mantissa to make it a K-bit signed integer (K-1 bits for magnitude) + exp -= (bits - 1); + + // compute renormalized rounded mantissa for each component + int mmask = (1 << 24) - 1; + + int m = int(v[j] * optexp2(-exp) + (v[j] >= 0 ? 0.5f : -0.5f)); + + d[j] = (m & mmask) | (unsigned(exp) << 24); + } + } +} + #undef SIMD_SSE #undef SIMD_NEON #undef SIMD_WASM From bcbc1cd413cd562c00650de6fe7e186cc05edfdc Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 Dec 2023 10:33:58 +0100 Subject: [PATCH 56/79] Fix crash in mesh LOD generator if generated mesh has more indices --- Source/Engine/Tools/ModelTool/ModelTool.cpp | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index b71584150..763c10e4b 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -1465,6 +1465,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option baseLodTriangleCount += mesh->Indices.Count() / 3; baseLodVertexCount += mesh->Positions.Count(); } + Array indices; for (int32 lodIndex = Math::Clamp(baseLOD + 1, 1, lodCount - 1); lodIndex < lodCount; lodIndex++) { auto& dstLod = data.LODs[lodIndex]; @@ -1486,16 +1487,18 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option int32 srcMeshIndexCount = srcMesh->Indices.Count(); int32 srcMeshVertexCount = srcMesh->Positions.Count(); int32 dstMeshIndexCountTarget = int32(srcMeshIndexCount * triangleReduction) / 3 * 3; - Array indices; - indices.Resize(dstMeshIndexCountTarget); + if (dstMeshIndexCountTarget < 3 || dstMeshIndexCountTarget >= srcMeshIndexCount) + continue; + indices.Clear(); + indices.Resize(srcMeshIndexCount); int32 dstMeshIndexCount = {}; if (options.SloppyOptimization) - dstMeshIndexCount = (int32)meshopt_simplifySloppy(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), dstMeshIndexCountTarget); + dstMeshIndexCount = (int32)meshopt_simplifySloppy(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), dstMeshIndexCountTarget, options.LODTargetError); else dstMeshIndexCount = (int32)meshopt_simplify(indices.Get(), srcMesh->Indices.Get(), srcMeshIndexCount, (const float*)srcMesh->Positions.Get(), srcMeshVertexCount, sizeof(Float3), dstMeshIndexCountTarget, options.LODTargetError); - indices.Resize(dstMeshIndexCount); - if (dstMeshIndexCount == 0) + if (dstMeshIndexCount <= 0 || dstMeshIndexCount > indices.Count()) continue; + indices.Resize(dstMeshIndexCount); // Generate simplified vertex buffer remapping table (use only vertices from LOD index buffer) Array remap; From 3f632b7d15bd16d994dcfbaa9dcf8a1b16bd91cc Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 Dec 2023 10:34:29 +0100 Subject: [PATCH 57/79] Fix incorrect empty meshes/LODs removal after auto-lod generation --- Source/Engine/Tools/ModelTool/ModelTool.cpp | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 763c10e4b..2bd923f20 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -1565,11 +1565,15 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option generatedLod++; } - // Remove empty meshes + // Remove empty meshes (no LOD was generated for them) for (int32 i = dstLod.Meshes.Count() - 1; i >= 0; i--) { - if (dstLod.Meshes[i]->Indices.IsEmpty()) - dstLod.Meshes.RemoveAt(i--); + MeshData* mesh = dstLod.Meshes[i]; + if (mesh->Indices.IsEmpty() || mesh->Positions.IsEmpty()) + { + Delete(mesh); + dstLod.Meshes.RemoveAtKeepOrder(i); + } } LOG(Info, "Generated LOD{0}: triangles: {1} ({2}% of base LOD), verticies: {3} ({4}% of base LOD)", @@ -1577,6 +1581,13 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option lodTriangleCount, (int32)(lodTriangleCount * 100 / baseLodTriangleCount), lodVertexCount, (int32)(lodVertexCount * 100 / baseLodVertexCount)); } + for (int32 lodIndex = data.LODs.Count() - 1; lodIndex > 0; lodIndex--) + { + if (data.LODs[lodIndex].Meshes.IsEmpty()) + data.LODs.RemoveAt(lodIndex); + else + break; + } if (generatedLod) { auto lodEndTime = DateTime::NowUTC(); From 63773f2ddf71418cf7fb6b86e1a7e33dcb95c9dc Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 Dec 2023 11:19:42 +0100 Subject: [PATCH 58/79] Add **option to import model file as Prefab** #1909 #1329 #1973 --- .../Editor/Content/Import/ModelImportEntry.cs | 11 +- .../AssetsImportingManager.cpp | 2 +- .../Engine/ContentImporters/ImportModel.cpp | 180 +++++++++++++++++- Source/Engine/ContentImporters/ImportModel.h | 1 + Source/Engine/ContentImporters/Types.h | 2 +- Source/Engine/Tools/ModelTool/ModelTool.cpp | 11 +- Source/Engine/Tools/ModelTool/ModelTool.h | 6 +- 7 files changed, 199 insertions(+), 14 deletions(-) diff --git a/Source/Editor/Content/Import/ModelImportEntry.cs b/Source/Editor/Content/Import/ModelImportEntry.cs index bbb384562..484efb1d9 100644 --- a/Source/Editor/Content/Import/ModelImportEntry.cs +++ b/Source/Editor/Content/Import/ModelImportEntry.cs @@ -12,13 +12,14 @@ namespace FlaxEngine.Tools { partial struct Options { - private bool ShowGeometry => Type == ModelTool.ModelType.Model || Type == ModelTool.ModelType.SkinnedModel; - private bool ShowModel => Type == ModelTool.ModelType.Model; - private bool ShowSkinnedModel => Type == ModelTool.ModelType.SkinnedModel; - private bool ShowAnimation => Type == ModelTool.ModelType.Animation; + private bool ShowGeometry => Type == ModelType.Model || Type == ModelType.SkinnedModel || Type == ModelType.Prefab; + private bool ShowModel => Type == ModelType.Model || Type == ModelType.Prefab; + private bool ShowSkinnedModel => Type == ModelType.SkinnedModel || Type == ModelType.Prefab; + private bool ShowAnimation => Type == ModelType.Animation || Type == ModelType.Prefab; private bool ShowSmoothingNormalsAngle => ShowGeometry && CalculateNormals; private bool ShowSmoothingTangentsAngle => ShowGeometry && CalculateTangents; - private bool ShowFramesRange => ShowAnimation && Duration == ModelTool.AnimationDuration.Custom; + private bool ShowFramesRange => ShowAnimation && Duration == AnimationDuration.Custom; + private bool ShowSplitting => Type != ModelType.Prefab; } } } diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.cpp b/Source/Engine/ContentImporters/AssetsImportingManager.cpp index 76b9c211d..4bad26599 100644 --- a/Source/Engine/ContentImporters/AssetsImportingManager.cpp +++ b/Source/Engine/ContentImporters/AssetsImportingManager.cpp @@ -382,7 +382,7 @@ bool AssetsImportingManager::Create(const Function>* meshesByNamePtr = options.Cached ? (Array>*)options.Cached->MeshesByName : nullptr; Array> meshesByNameThis; + String autoImportOutput; if (!data) { String errorMsg; - String autoImportOutput(StringUtils::GetDirectoryName(context.TargetAssetPath)); + autoImportOutput = StringUtils::GetDirectoryName(context.TargetAssetPath); autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath)); if (ModelTool::ImportModel(context.InputPath, dataThis, options, errorMsg, autoImportOutput)) { @@ -246,14 +258,62 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context) Array>& meshesByName = *meshesByNamePtr; // Import objects from file separately - if (options.SplitObjects) + ModelTool::Options::CachedData cached = { data, (void*)meshesByNamePtr }; + Array prefabObjects; + if (options.Type == ModelTool::ModelType::Prefab) + { + // Normalize options + options.SplitObjects = false; + options.ObjectIndex = -1; + + // Import all of the objects recursive but use current model data to skip loading file again + options.Cached = &cached; + Function splitImport = [&context, &autoImportOutput](Options& splitOptions, const StringView& objectName, String& outputPath) + { + // Recursive importing of the split object + String postFix = objectName; + const int32 splitPos = postFix.FindLast(TEXT('|')); + if (splitPos != -1) + postFix = postFix.Substring(splitPos + 1); + // TODO: check for name collisions with material/texture assets + outputPath = autoImportOutput / String(StringUtils::GetFileNameWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax"); + splitOptions.SubAssetFolder = TEXT(" "); // Use the same folder as asset as they all are imported to the subdir for the prefab (see SubAssetFolder usage above) + return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions); + }; + auto splitOptions = options; + LOG(Info, "Splitting imported {0} meshes", meshesByName.Count()); + PrefabObject prefabObject; + for (int32 groupIndex = 0; groupIndex < meshesByName.Count(); groupIndex++) + { + auto& group = meshesByName[groupIndex]; + + // Cache object options (nested sub-object import removes the meshes) + prefabObject.NodeIndex = group.First()->NodeIndex; + prefabObject.Name = group.First()->Name; + + splitOptions.Type = ModelTool::ModelType::Model; + splitOptions.ObjectIndex = groupIndex; + if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath)) + { + prefabObjects.Add(prefabObject); + } + } + LOG(Info, "Splitting imported {0} animations", data->Animations.Count()); + for (int32 i = 0; i < data->Animations.Count(); i++) + { + auto& animation = data->Animations[i]; + splitOptions.Type = ModelTool::ModelType::Animation; + splitOptions.ObjectIndex = i; + splitImport(splitOptions, animation.Name, prefabObject.AssetPath); + } + } + else if (options.SplitObjects) { // Import the first object within this call options.SplitObjects = false; options.ObjectIndex = 0; // Import rest of the objects recursive but use current model data to skip loading file again - ModelTool::Options::CachedData cached = { data, (void*)meshesByNamePtr }; options.Cached = &cached; Function splitImport; splitImport.Bind([&context](Options& splitOptions, const StringView& objectName) @@ -396,6 +456,9 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context) case ModelTool::ModelType::Animation: result = CreateAnimation(context, *data, &options); break; + case ModelTool::ModelType::Prefab: + result = CreatePrefab(context, *data, options, prefabObjects); + break; } for (auto mesh : meshesToDelete) Delete(mesh); @@ -546,4 +609,115 @@ CreateAssetResult ImportModel::CreateAnimation(CreateAssetContext& context, Mode return CreateAssetResult::Ok; } +CreateAssetResult ImportModel::CreatePrefab(CreateAssetContext& context, ModelData& data, const Options& options, const Array& prefabObjects) +{ + PROFILE_CPU(); + if (data.Nodes.Count() == 0) + return CreateAssetResult::Error; + + // If that prefab already exists then we need to use it as base to preserve object IDs and local changes applied by user + const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + DEFAULT_PREFAB_EXTENSION_DOT; + auto* prefab = FileSystem::FileExists(outputPath) ? Content::Load(outputPath) : nullptr; + if (prefab) + { + // Ensure that prefab has Default Instance so ObjectsCache is valid (used below) + prefab->GetDefaultInstance(); + } + + // Create prefab structure + Dictionary nodeToActor; + Array nodeActors; + Actor* rootActor = nullptr; + for (int32 nodeIndex = 0; nodeIndex < data.Nodes.Count(); nodeIndex++) + { + const auto& node = data.Nodes[nodeIndex]; + + // Create actor(s) for this node + nodeActors.Clear(); + for (const PrefabObject& e : prefabObjects) + { + if (e.NodeIndex == nodeIndex) + { + auto* actor = New(); + actor->SetName(e.Name); + if (auto* model = Content::LoadAsync(e.AssetPath)) + { + actor->Model = model; + } + nodeActors.Add(actor); + } + } + Actor* nodeActor = nodeActors.Count() == 1 ? nodeActors[0] : New(); + if (nodeActors.Count() > 1) + { + for (Actor* e : nodeActors) + { + e->SetParent(nodeActor); + } + } + if (nodeActors.Count() != 1) + { + // Include default actor to iterate over it properly in code below + nodeActors.Add(nodeActor); + } + + // Setup node in hierarchy + nodeToActor.Add(nodeIndex, nodeActor); + nodeActor->SetName(node.Name); + nodeActor->SetLocalTransform(node.LocalTransform); + if (nodeIndex == 0) + { + // Special case for root actor to link any unlinked nodes + nodeToActor.Add(-1, nodeActor); + rootActor = nodeActor; + } + else + { + Actor* parentActor; + if (nodeToActor.TryGet(node.ParentIndex, parentActor)) + nodeActor->SetParent(parentActor); + } + + // Link with object from prefab (if reimporting) + if (prefab) + { + for (Actor* a : nodeActors) + { + for (const auto& i : prefab->ObjectsCache) + { + if (i.Value->GetTypeHandle() != a->GetTypeHandle()) // Type match + continue; + auto* o = (Actor*)i.Value; + if (o->GetName() != a->GetName()) // Name match + continue; + + // Mark as this object already exists in prefab so will be preserved when updating it + a->LinkPrefab(o->GetPrefabID(), o->GetPrefabObjectID()); + break; + } + } + } + } + ASSERT_LOW_LAYER(rootActor); + // TODO: add PrefabModel script for asset reimporting + + // Create prefab instead of native asset + bool failed; + if (prefab) + { + failed = prefab->ApplyAll(rootActor); + } + else + { + failed = PrefabManager::CreatePrefab(rootActor, outputPath, false); + } + + // Cleanup objects from memory + rootActor->DeleteObjectNow(); + + if (failed) + return CreateAssetResult::Error; + return CreateAssetResult::Skip; +} + #endif diff --git a/Source/Engine/ContentImporters/ImportModel.h b/Source/Engine/ContentImporters/ImportModel.h index 31a525852..710971fca 100644 --- a/Source/Engine/ContentImporters/ImportModel.h +++ b/Source/Engine/ContentImporters/ImportModel.h @@ -43,6 +43,7 @@ private: static CreateAssetResult CreateModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr); static CreateAssetResult CreateSkinnedModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr); static CreateAssetResult CreateAnimation(CreateAssetContext& context, ModelData& data, const Options* options = nullptr); + static CreateAssetResult CreatePrefab(CreateAssetContext& context, ModelData& data, const Options& options, const Array& prefabObjects); }; #endif diff --git a/Source/Engine/ContentImporters/Types.h b/Source/Engine/ContentImporters/Types.h index 4ebf55583..388c533a8 100644 --- a/Source/Engine/ContentImporters/Types.h +++ b/Source/Engine/ContentImporters/Types.h @@ -18,7 +18,7 @@ class CreateAssetContext; /// /// Create/Import new asset callback result /// -DECLARE_ENUM_7(CreateAssetResult, Ok, Abort, Error, CannotSaveFile, InvalidPath, CannotAllocateChunk, InvalidTypeID); +DECLARE_ENUM_8(CreateAssetResult, Ok, Abort, Error, CannotSaveFile, InvalidPath, CannotAllocateChunk, InvalidTypeID, Skip); /// /// Create/Import new asset callback function diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 2bd923f20..1257d49cc 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -453,7 +453,7 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options options.FramesRange.Y = Math::Max(options.FramesRange.Y, options.FramesRange.X); options.DefaultFrameRate = Math::Max(0.0f, options.DefaultFrameRate); options.SamplingRate = Math::Max(0.0f, options.SamplingRate); - if (options.SplitObjects) + if (options.SplitObjects || options.Type == ModelType::Prefab) options.MergeMeshes = false; // Meshes merging doesn't make sense when we want to import each mesh individually // TODO: maybe we could update meshes merger to collapse meshes within the same name if splitting is enabled? @@ -808,6 +808,13 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option case ModelType::Animation: options.ImportTypes = ImportDataTypes::Animations; break; + case ModelType::Prefab: + options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Animations; + if (options.ImportMaterials) + options.ImportTypes |= ImportDataTypes::Materials; + if (options.ImportTextures) + options.ImportTypes |= ImportDataTypes::Textures; + break; default: return true; } @@ -1279,7 +1286,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option ! #endif } - if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry)) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && options.Type != ModelType::Prefab) { // Perform simple nodes mapping to single node (will transform meshes to model local space) SkeletonMapping skeletonMapping(data.Nodes, nullptr); diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index cec9bdcf2..c0099448d 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -114,6 +114,8 @@ public: SkinnedModel = 1, // The animation asset. Animation = 2, + // The prefab scene. + Prefab = 3, }; /// @@ -282,10 +284,10 @@ public: public: // Splitting // If checked, the imported mesh/animations are split into separate assets. Used if ObjectIndex is set to -1. - API_FIELD(Attributes="EditorOrder(2000), EditorDisplay(\"Splitting\")") + API_FIELD(Attributes="EditorOrder(2000), EditorDisplay(\"Splitting\"), VisibleIf(nameof(ShowSplitting))") bool SplitObjects = false; // The zero-based index for the mesh/animation clip to import. If the source file has more than one mesh/animation it can be used to pick a desired object. Default -1 imports all objects. - API_FIELD(Attributes="EditorOrder(2010), EditorDisplay(\"Splitting\")") + API_FIELD(Attributes="EditorOrder(2010), EditorDisplay(\"Splitting\"), VisibleIf(nameof(ShowSplitting))") int32 ObjectIndex = -1; public: // Other From 7a7a43b8973247f5227f390cc041d616ef254254 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 Dec 2023 11:20:01 +0100 Subject: [PATCH 59/79] Fix selecting prefab object when object from prefab is already selected --- Source/Editor/Gizmo/TransformGizmo.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Gizmo/TransformGizmo.cs b/Source/Editor/Gizmo/TransformGizmo.cs index 4a3fa39ba..741b89d2d 100644 --- a/Source/Editor/Gizmo/TransformGizmo.cs +++ b/Source/Editor/Gizmo/TransformGizmo.cs @@ -201,7 +201,21 @@ namespace FlaxEditor.Gizmo ActorNode prefabRoot = GetPrefabRootInParent(actorNode); if (prefabRoot != null && actorNode != prefabRoot) { - hit = WalkUpAndFindActorNodeBeforeSelection(actorNode, prefabRoot); + bool isPrefabInSelection = false; + foreach (var e in sceneEditing.Selection) + { + if (e is ActorNode ae && GetPrefabRootInParent(ae) == prefabRoot) + { + isPrefabInSelection = true; + break; + } + } + + // Skip selecting prefab root if we already had object from that prefab selected + if (!isPrefabInSelection) + { + hit = WalkUpAndFindActorNodeBeforeSelection(actorNode, prefabRoot); + } } } From 8faaaaaf54f57e36bbfac5499a9d9daa14f5f285 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 Dec 2023 11:20:32 +0100 Subject: [PATCH 60/79] Fix incorrect structure usage for hostfxr params siize #2037 --- Source/Engine/Scripting/Runtime/DotNet.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Engine/Scripting/Runtime/DotNet.cpp b/Source/Engine/Scripting/Runtime/DotNet.cpp index 464e60ba6..db5c57bbc 100644 --- a/Source/Engine/Scripting/Runtime/DotNet.cpp +++ b/Source/Engine/Scripting/Runtime/DotNet.cpp @@ -1657,7 +1657,7 @@ bool InitHostfxr() // Get path to hostfxr library get_hostfxr_parameters get_hostfxr_params; - get_hostfxr_params.size = sizeof(hostfxr_initialize_parameters); + get_hostfxr_params.size = sizeof(get_hostfxr_parameters); get_hostfxr_params.assembly_path = libraryPath.Get(); #if PLATFORM_MAC ::String macOSDotnetRoot = TEXT("/usr/local/share/dotnet"); From 23a72f2ade7ea1b2ceae88e83d2896d59560b3e4 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 Dec 2023 13:03:37 +0100 Subject: [PATCH 61/79] Fix not showing primary context menu on Visject surface if child control handled input event --- Source/Editor/Surface/VisjectSurface.Input.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/Source/Editor/Surface/VisjectSurface.Input.cs b/Source/Editor/Surface/VisjectSurface.Input.cs index ec878a407..419c2d468 100644 --- a/Source/Editor/Surface/VisjectSurface.Input.cs +++ b/Source/Editor/Surface/VisjectSurface.Input.cs @@ -533,6 +533,7 @@ namespace FlaxEditor.Surface UpdateSelectionRectangle(); } } + bool showPrimaryMenu = false; if (_rightMouseDown && button == MouseButton.Right) { _rightMouseDown = false; @@ -546,8 +547,7 @@ namespace FlaxEditor.Surface _cmStartPos = location; if (controlUnderMouse == null) { - // Show primary context menu - ShowPrimaryMenu(_cmStartPos); + showPrimaryMenu = true; } } _mouseMoveAmount = 0; @@ -573,8 +573,13 @@ namespace FlaxEditor.Surface return true; } + // If none of the child controls handled this show the primary context menu + if (showPrimaryMenu) + { + ShowPrimaryMenu(_cmStartPos); + } // Letting go of a connection or right clicking while creating a connection - if (!_isMovingSelection && _connectionInstigator != null && !IsPrimaryMenuOpened) + else if (!_isMovingSelection && _connectionInstigator != null && !IsPrimaryMenuOpened) { _cmStartPos = location; Cursor = CursorType.Default; From 32ced6e68ab0035a5af0c6092d9cee9591a41853 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 Dec 2023 14:27:14 +0100 Subject: [PATCH 62/79] Fix missing surface graph edited flag after removing anim graph state transition #2035 --- Source/Editor/Surface/Archetypes/Animation.StateMachine.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs index 621c7c25e..9d4367303 100644 --- a/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs +++ b/Source/Editor/Surface/Archetypes/Animation.StateMachine.cs @@ -1335,6 +1335,7 @@ namespace FlaxEditor.Surface.Archetypes Surface?.AddBatchedUndoAction(action); action.Do(); Surface?.OnNodesConnected(this, other); + Surface?.MarkAsEdited(); } } @@ -1911,6 +1912,7 @@ namespace FlaxEditor.Surface.Archetypes { var action = new StateMachineStateBase.AddRemoveTransitionAction(this); SourceState.Surface?.AddBatchedUndoAction(action); + SourceState.Surface?.MarkAsEdited(); action.Do(); } From 74b77bfa4ce499a0497526b4e4ea84d48bb8b429 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Wed, 6 Dec 2023 14:34:34 +0100 Subject: [PATCH 63/79] Fix regression from 38a0718b7030699ca35a7d71e779081ec9f2e3d6 --- Source/Engine/Scripting/Internal/EngineInternalCalls.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Source/Engine/Scripting/Internal/EngineInternalCalls.cpp b/Source/Engine/Scripting/Internal/EngineInternalCalls.cpp index 08a93328b..bbdc9abd5 100644 --- a/Source/Engine/Scripting/Internal/EngineInternalCalls.cpp +++ b/Source/Engine/Scripting/Internal/EngineInternalCalls.cpp @@ -108,7 +108,7 @@ namespace }; ChunkedArray ManagedSourceLocations; - uint32 ManagedEventsCount[PLATFORM_THREADS_LIMIT] = { 0 }; + ThreadLocal ManagedEventsCount; #endif #endif } @@ -148,7 +148,7 @@ DEFINE_INTERNAL_CALL(void) ProfilerInternal_BeginEvent(MString* nameObj) //static constexpr tracy::SourceLocationData tracySrcLoc{ nullptr, __FUNCTION__, __FILE__, (uint32_t)__LINE__, 0 }; const bool tracyActive = tracy::ScopedZone::Begin(srcLoc); if (tracyActive) - ManagedEventsCount[Platform::GetCurrentThreadID()]++; + ManagedEventsCount.Get()++; #endif #endif #endif @@ -158,7 +158,7 @@ DEFINE_INTERNAL_CALL(void) ProfilerInternal_EndEvent() { #if COMPILE_WITH_PROFILER #if TRACY_ENABLE - auto& tracyActive = ManagedEventsCount[Platform::GetCurrentThreadID()]; + uint32& tracyActive = ManagedEventsCount.Get(); if (tracyActive > 0) { tracyActive--; From cb9211097672d263169a8414f830416e203cef22 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 7 Dec 2023 10:25:45 +0100 Subject: [PATCH 64/79] Add `ModelPrefab` to imported model prefab for reimporting functionality --- .../Dedicated/ModelPrefabEditor.cs | 79 +++++++++++++++++++ .../Values/CustomValueContainer.cs | 1 - .../Editor/Modules/ContentImportingModule.cs | 44 ++++++----- .../AssetsImportingManager.cpp | 29 +++---- .../ContentImporters/AssetsImportingManager.h | 3 + .../Engine/ContentImporters/ImportModel.cpp | 22 +++++- Source/Engine/Level/SceneObjectsFactory.cpp | 8 +- .../{Components => Scripts}/MissingScript.h | 0 Source/Engine/Level/Scripts/ModelPrefab.h | 29 +++++++ 9 files changed, 179 insertions(+), 36 deletions(-) create mode 100644 Source/Editor/CustomEditors/Dedicated/ModelPrefabEditor.cs rename Source/Engine/Level/{Components => Scripts}/MissingScript.h (100%) create mode 100644 Source/Engine/Level/Scripts/ModelPrefab.h diff --git a/Source/Editor/CustomEditors/Dedicated/ModelPrefabEditor.cs b/Source/Editor/CustomEditors/Dedicated/ModelPrefabEditor.cs new file mode 100644 index 000000000..0a42b952e --- /dev/null +++ b/Source/Editor/CustomEditors/Dedicated/ModelPrefabEditor.cs @@ -0,0 +1,79 @@ +// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. + +using System; +using System.IO; +using FlaxEditor.Content; +using FlaxEditor.CustomEditors.Editors; +using FlaxEngine; +using FlaxEngine.GUI; +using FlaxEngine.Tools; + +namespace FlaxEditor.CustomEditors.Dedicated; + +/// +/// The missing script editor. +/// +[CustomEditor(typeof(ModelPrefab)), DefaultEditor] +public class ModelPrefabEditor : GenericEditor +{ + private Guid _prefabId; + private Button _reimportButton; + private string _importPath; + + /// + public override void Initialize(LayoutElementsContainer layout) + { + base.Initialize(layout); + + var modelPrefab = Values[0] as ModelPrefab; + if (modelPrefab == null) + return; + _prefabId = modelPrefab.PrefabID; + while (true) + { + var prefab = FlaxEngine.Content.Load(_prefabId); + if (prefab) + { + var prefabObjectId = modelPrefab.PrefabObjectID; + var prefabObject = prefab.GetDefaultInstance(ref prefabObjectId); + if (prefabObject.PrefabID == _prefabId) + break; + _prefabId = prefabObject.PrefabID; + } + } + + var button = layout.Button("Reimport", "Reimports the source asset as prefab."); + _reimportButton = button.Button; + _reimportButton.Clicked += OnReimport; + } + + private void OnReimport() + { + var prefab = FlaxEngine.Content.Load(_prefabId); + var modelPrefab = (ModelPrefab)Values[0]; + var importPath = modelPrefab.ImportPath; + var editor = Editor.Instance; + if (editor.ContentImporting.GetReimportPath("Model Prefab", ref importPath)) + return; + var folder = editor.ContentDatabase.Find(Path.GetDirectoryName(prefab.Path)) as ContentFolder; + if (folder == null) + return; + var importOptions = modelPrefab.ImportOptions; + importOptions.Type = ModelTool.ModelType.Prefab; + _importPath = importPath; + _reimportButton.Enabled = false; + editor.ContentImporting.ImportFileEnd += OnImportFileEnd; + editor.ContentImporting.Import(importPath, folder, true, importOptions); + } + + private void OnImportFileEnd(IFileEntryAction entry, bool failed) + { + if (entry.SourceUrl == _importPath) + { + // Restore button + _importPath = null; + _reimportButton.Enabled = true; + Editor.Instance.ContentImporting.ImportFileEnd -= OnImportFileEnd; + } + } +} diff --git a/Source/Editor/CustomEditors/Values/CustomValueContainer.cs b/Source/Editor/CustomEditors/Values/CustomValueContainer.cs index 9a09c4cc2..23b5832e5 100644 --- a/Source/Editor/CustomEditors/Values/CustomValueContainer.cs +++ b/Source/Editor/CustomEditors/Values/CustomValueContainer.cs @@ -73,7 +73,6 @@ namespace FlaxEditor.CustomEditors { if (instanceValues == null || instanceValues.Count != Count) throw new ArgumentException(); - for (int i = 0; i < Count; i++) { var v = instanceValues[i]; diff --git a/Source/Editor/Modules/ContentImportingModule.cs b/Source/Editor/Modules/ContentImportingModule.cs index 85dd50d35..587d3a6c6 100644 --- a/Source/Editor/Modules/ContentImportingModule.cs +++ b/Source/Editor/Modules/ContentImportingModule.cs @@ -126,29 +126,35 @@ namespace FlaxEditor.Modules { if (item != null && !item.GetImportPath(out string importPath)) { - // Check if input file is missing - if (!System.IO.File.Exists(importPath)) - { - Editor.LogWarning(string.Format("Cannot reimport asset \'{0}\'. File \'{1}\' does not exist.", item.Path, importPath)); - if (skipSettingsDialog) - return; - - // Ask user to select new file location - var title = string.Format("Please find missing \'{0}\' file for asset \'{1}\'", importPath, item.ShortName); - if (FileSystem.ShowOpenFileDialog(Editor.Windows.MainWindow, null, "All files (*.*)\0*.*\0", false, title, out var files)) - return; - if (files != null && files.Length > 0) - importPath = files[0]; - - // Validate file path again - if (!System.IO.File.Exists(importPath)) - return; - } - + if (GetReimportPath(item.ShortName, ref importPath, skipSettingsDialog)) + return; Import(importPath, item.Path, true, skipSettingsDialog, settings); } } + internal bool GetReimportPath(string contextName, ref string importPath, bool skipSettingsDialog = false) + { + // Check if input file is missing + if (!System.IO.File.Exists(importPath)) + { + Editor.LogWarning(string.Format("Cannot reimport asset \'{0}\'. File \'{1}\' does not exist.", contextName, importPath)); + if (skipSettingsDialog) + return true; + + // Ask user to select new file location + var title = string.Format("Please find missing \'{0}\' file for asset \'{1}\'", importPath, contextName); + if (FileSystem.ShowOpenFileDialog(Editor.Windows.MainWindow, null, "All files (*.*)\0*.*\0", false, title, out var files)) + return true; + if (files != null && files.Length > 0) + importPath = files[0]; + + // Validate file path again + if (!System.IO.File.Exists(importPath)) + return true; + } + return false; + } + /// /// Imports the specified files. /// diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.cpp b/Source/Engine/ContentImporters/AssetsImportingManager.cpp index 4bad26599..bbefc687a 100644 --- a/Source/Engine/ContentImporters/AssetsImportingManager.cpp +++ b/Source/Engine/ContentImporters/AssetsImportingManager.cpp @@ -165,20 +165,7 @@ bool CreateAssetContext::AllocateChunk(int32 index) void CreateAssetContext::AddMeta(JsonWriter& writer) const { writer.JKEY("ImportPath"); - if (AssetsImportingManager::UseImportPathRelative && !FileSystem::IsRelative(InputPath) -#if PLATFORM_WINDOWS - // Import path from other drive should be stored as absolute on Windows to prevent issues - && InputPath.Length() > 2 && Globals::ProjectFolder.Length() > 2 && InputPath[0] == Globals::ProjectFolder[0] -#endif - ) - { - const String relativePath = FileSystem::ConvertAbsolutePathToRelative(Globals::ProjectFolder, InputPath); - writer.String(relativePath); - } - else - { - writer.String(InputPath); - } + writer.String(AssetsImportingManager::GetImportPath(InputPath)); writer.JKEY("ImportUsername"); writer.String(Platform::GetUserName()); } @@ -304,6 +291,20 @@ bool AssetsImportingManager::ImportIfEdited(const StringView& inputPath, const S return false; } +String AssetsImportingManager::GetImportPath(const String& path) +{ + if (UseImportPathRelative && !FileSystem::IsRelative(path) +#if PLATFORM_WINDOWS + // Import path from other drive should be stored as absolute on Windows to prevent issues + && path.Length() > 2 && Globals::ProjectFolder.Length() > 2 && path[0] == Globals::ProjectFolder[0] +#endif + ) + { + return FileSystem::ConvertAbsolutePathToRelative(Globals::ProjectFolder, path); + } + return path; +} + bool AssetsImportingManager::Create(const Function& callback, const StringView& inputPath, const StringView& outputPath, Guid& assetId, void* arg) { PROFILE_CPU(); diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.h b/Source/Engine/ContentImporters/AssetsImportingManager.h index eb5058650..2b76298a6 100644 --- a/Source/Engine/ContentImporters/AssetsImportingManager.h +++ b/Source/Engine/ContentImporters/AssetsImportingManager.h @@ -236,6 +236,9 @@ public: return ImportIfEdited(inputPath, outputPath, id, arg); } + // Converts source files path into the relative format if enabled by the project settings. Result path can be stored in asset for reimports. + static String GetImportPath(const String& path); + private: static bool Create(const CreateAssetFunction& callback, const StringView& inputPath, const StringView& outputPath, Guid& assetId, void* arg); }; diff --git a/Source/Engine/ContentImporters/ImportModel.cpp b/Source/Engine/ContentImporters/ImportModel.cpp index be58be3ba..baac1defb 100644 --- a/Source/Engine/ContentImporters/ImportModel.cpp +++ b/Source/Engine/ContentImporters/ImportModel.cpp @@ -19,6 +19,7 @@ #include "Engine/Level/Actors/StaticModel.h" #include "Engine/Level/Prefabs/Prefab.h" #include "Engine/Level/Prefabs/PrefabManager.h" +#include "Engine/Level/Scripts/ModelPrefab.h" #include "Engine/Platform/FileSystem.h" #include "Engine/Utilities/RectPack.h" #include "Engine/Profiler/ProfilerCPU.h" @@ -699,7 +700,26 @@ CreateAssetResult ImportModel::CreatePrefab(CreateAssetContext& context, ModelDa } } ASSERT_LOW_LAYER(rootActor); - // TODO: add PrefabModel script for asset reimporting + { + // Add script with import options + auto* modelPrefabScript = New(); + modelPrefabScript->SetParent(rootActor); + modelPrefabScript->ImportPath = AssetsImportingManager::GetImportPath(context.InputPath); + modelPrefabScript->ImportOptions = options; + + // Link with existing prefab instance + if (prefab) + { + for (const auto& i : prefab->ObjectsCache) + { + if (i.Value->GetTypeHandle() == modelPrefabScript->GetTypeHandle()) + { + modelPrefabScript->LinkPrefab(i.Value->GetPrefabID(), i.Value->GetPrefabObjectID()); + break; + } + } + } + } // Create prefab instead of native asset bool failed; diff --git a/Source/Engine/Level/SceneObjectsFactory.cpp b/Source/Engine/Level/SceneObjectsFactory.cpp index 2ae947c85..df30257f9 100644 --- a/Source/Engine/Level/SceneObjectsFactory.cpp +++ b/Source/Engine/Level/SceneObjectsFactory.cpp @@ -16,8 +16,9 @@ #if !BUILD_RELEASE || USE_EDITOR #include "Engine/Level/Level.h" #include "Engine/Threading/Threading.h" -#include "Engine/Level/Components/MissingScript.h" +#include "Engine/Level/Scripts/MissingScript.h" #endif +#include "Engine/Level/Scripts/ModelPrefab.h" #if USE_EDITOR @@ -46,6 +47,11 @@ void MissingScript::SetReferenceScript(const ScriptingObjectReference