From 475453aa601d4f8ced173ae4a05c913e21ac1c0f Mon Sep 17 00:00:00 2001 From: nothingTVatYT Date: Tue, 28 Nov 2023 00:06:28 +0100 Subject: [PATCH 001/118] add load scene add. to context menus --- Source/Editor/Content/Proxy/SceneProxy.cs | 7 ++++++ .../Windows/SceneTreeWindow.ContextMenu.cs | 22 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/Source/Editor/Content/Proxy/SceneProxy.cs b/Source/Editor/Content/Proxy/SceneProxy.cs index 004c2aed7..a232c672b 100644 --- a/Source/Editor/Content/Proxy/SceneProxy.cs +++ b/Source/Editor/Content/Proxy/SceneProxy.cs @@ -1,6 +1,7 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; +using FlaxEditor.GUI.ContextMenu; using FlaxEditor.Windows; using FlaxEngine; @@ -68,5 +69,11 @@ namespace FlaxEditor.Content { return new SceneItem(path, id); } + + /// + public override void OnContentWindowContextMenu(ContextMenu menu, ContentItem item) + { + menu.AddButton("Open additionally", () => { Editor.Instance.Scene.OpenScene(((SceneItem)item).ID, true); }); + } } } diff --git a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs index a99c3ac7d..3e078bcb8 100644 --- a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs +++ b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs @@ -1,6 +1,8 @@ // Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; +using System.Collections.Generic; +using System.Linq; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.SceneGraph; using FlaxEngine; @@ -148,6 +150,26 @@ namespace FlaxEditor.Windows // Spawning actors options + if (!hasSthSelected) + { + var allScenes = FlaxEngine.Content.GetAllAssetsByType(typeof(SceneAsset)); + var loadedSceneIds = Editor.Instance.Scene.Root.ChildNodes.Select(node => node.ID).ToList(); + var unloadedScenes = allScenes.Where(sceneId => !loadedSceneIds.Contains(sceneId)).ToList(); + if (unloadedScenes.Count > 0) + { + contextMenu.AddSeparator(); + var childCM = contextMenu.GetOrAddChildMenu("Open Scene additionally"); + foreach (var sceneGuid in unloadedScenes.Where(sceneGuid => !Level.FindScene(sceneGuid))) + { + if (FlaxEngine.Content.GetAssetInfo(sceneGuid, out var unloadedScene)) + { + var splitPath = unloadedScene.Path.Split('/'); + childCM.ContextMenu.AddButton(splitPath[^1], () => { Editor.Instance.Scene.OpenScene(sceneGuid, true); }); + } + } + } + } + contextMenu.AddSeparator(); // go through each actor and add it to the context menu if it has the ActorContextMenu attribute From a06a0798041e22ee6f48a9b5195f33be75982190 Mon Sep 17 00:00:00 2001 From: nothingTVatYT Date: Tue, 28 Nov 2023 00:37:29 +0100 Subject: [PATCH 002/118] change submenu name to the shorter and less complicated "Add Scene" --- Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs index 3e078bcb8..6261c0c07 100644 --- a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs +++ b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs @@ -158,13 +158,16 @@ namespace FlaxEditor.Windows if (unloadedScenes.Count > 0) { contextMenu.AddSeparator(); - var childCM = contextMenu.GetOrAddChildMenu("Open Scene additionally"); + var childCM = contextMenu.GetOrAddChildMenu("Add Scene"); foreach (var sceneGuid in unloadedScenes.Where(sceneGuid => !Level.FindScene(sceneGuid))) { if (FlaxEngine.Content.GetAssetInfo(sceneGuid, out var unloadedScene)) { var splitPath = unloadedScene.Path.Split('/'); - childCM.ContextMenu.AddButton(splitPath[^1], () => { Editor.Instance.Scene.OpenScene(sceneGuid, true); }); + var sceneName = splitPath[^1]; + if (splitPath[^1].EndsWith(".scene")) + sceneName = sceneName[..^6]; + childCM.ContextMenu.AddButton(sceneName, () => { Editor.Instance.Scene.OpenScene(sceneGuid, true); }); } } } From 84f3d509252bac9d351376c3bb0e2d7e39092846 Mon Sep 17 00:00:00 2001 From: nothingTVatYT Date: Tue, 28 Nov 2023 00:43:55 +0100 Subject: [PATCH 003/118] moved a comment line back to the suitable place --- Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs index 6261c0c07..ea4714df8 100644 --- a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs +++ b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs @@ -148,7 +148,7 @@ namespace FlaxEditor.Windows contextMenu.AddButton("Break Prefab Link", Editor.Prefabs.BreakLinks); } - // Spawning actors options + // Load additional scenes option if (!hasSthSelected) { @@ -173,6 +173,8 @@ namespace FlaxEditor.Windows } } + // Spawning actors options + contextMenu.AddSeparator(); // go through each actor and add it to the context menu if it has the ActorContextMenu attribute From a3f1dc269474eaee3de64334d9ddf6d0319fcf76 Mon Sep 17 00:00:00 2001 From: nothingTVatYT Date: Tue, 28 Nov 2023 00:50:44 +0100 Subject: [PATCH 004/118] removed unnecessary check --- Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs index ea4714df8..8b5318190 100644 --- a/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs +++ b/Source/Editor/Windows/SceneTreeWindow.ContextMenu.cs @@ -159,7 +159,7 @@ namespace FlaxEditor.Windows { contextMenu.AddSeparator(); var childCM = contextMenu.GetOrAddChildMenu("Add Scene"); - foreach (var sceneGuid in unloadedScenes.Where(sceneGuid => !Level.FindScene(sceneGuid))) + foreach (var sceneGuid in unloadedScenes) { if (FlaxEngine.Content.GetAssetInfo(sceneGuid, out var unloadedScene)) { From c5df7ad689bd1e5efa15637a8f23a7145d1f316d Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Thu, 30 Nov 2023 11:31:58 +0100 Subject: [PATCH 005/118] 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 006/118] 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 007/118] 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 008/118] 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 009/118] 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: Fri, 1 Dec 2023 21:25:00 +0100 Subject: [PATCH 010/118] skip WM for non-regular windows and add mouse tracking --- .../Engine/Platform/Linux/LinuxPlatform.cpp | 33 ++++++++++++++++--- Source/Engine/Platform/Linux/LinuxPlatform.h | 2 ++ Source/Engine/Platform/Linux/LinuxWindow.cpp | 19 ++++++++--- 3 files changed, 45 insertions(+), 9 deletions(-) diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.cpp b/Source/Engine/Platform/Linux/LinuxPlatform.cpp index a7634710f..83c3e3ae6 100644 --- a/Source/Engine/Platform/Linux/LinuxPlatform.cpp +++ b/Source/Engine/Platform/Linux/LinuxPlatform.cpp @@ -94,6 +94,7 @@ X11::XcursorImage* CursorsImg[(int32)CursorType::MAX]; Dictionary KeyNameMap; Array KeyCodeMap; Delegate LinuxPlatform::xEventRecieved; +const Window* mouseTrackingWindow; // Message boxes configuration #define LINUX_DIALOG_MIN_BUTTON_WIDTH 64 @@ -1917,6 +1918,8 @@ bool LinuxPlatform::Init() if (PlatformBase::Init()) return true; + mouseTrackingWindow = nullptr; + char fileNameBuffer[1024]; // Init timing @@ -2260,6 +2263,17 @@ bool LinuxPlatform::Init() return false; } +void LinuxPlatform::StartTrackingMouse(const Window* window) +{ + mouseTrackingWindow = window; +} + +void LinuxPlatform::EndTrackingMouse(const Window* window) +{ + if (mouseTrackingWindow == window) + mouseTrackingWindow = nullptr; +} + void LinuxPlatform::BeforeRun() { } @@ -2398,7 +2412,7 @@ void LinuxPlatform::Tick() // Update input context focus X11::XSetICFocus(IC); window = WindowsManager::GetByNativePtr((void*)event.xfocus.window); - if (window) + if (window && mouseTrackingWindow == nullptr) { window->OnGotFocus(); } @@ -2407,7 +2421,7 @@ void LinuxPlatform::Tick() // Update input context focus X11::XUnsetICFocus(IC); window = WindowsManager::GetByNativePtr((void*)event.xfocus.window); - if (window) + if (window && mouseTrackingWindow == nullptr) { window->OnLostFocus(); } @@ -2514,23 +2528,32 @@ void LinuxPlatform::Tick() break; case ButtonPress: window = WindowsManager::GetByNativePtr((void*)event.xbutton.window); - if (window) + if (mouseTrackingWindow) + ((LinuxWindow*)mouseTrackingWindow)->OnButtonPress(&event.xbutton); + else if (window) window->OnButtonPress(&event.xbutton); break; case ButtonRelease: window = WindowsManager::GetByNativePtr((void*)event.xbutton.window); - if (window) + if (mouseTrackingWindow) + ((LinuxWindow*)mouseTrackingWindow)->OnButtonRelease(&event.xbutton); + else if (window) window->OnButtonRelease(&event.xbutton); break; case MotionNotify: window = WindowsManager::GetByNativePtr((void*)event.xmotion.window); - if (window) + if (mouseTrackingWindow) + ((LinuxWindow*)mouseTrackingWindow)->OnMotionNotify(&event.xmotion); + else if (window) window->OnMotionNotify(&event.xmotion); break; case EnterNotify: + // nothing? break; case LeaveNotify: window = WindowsManager::GetByNativePtr((void*)event.xcrossing.window); + if (mouseTrackingWindow) + ((LinuxWindow*)mouseTrackingWindow)->OnLeaveNotify(&event.xcrossing); if (window) window->OnLeaveNotify(&event.xcrossing); break; diff --git a/Source/Engine/Platform/Linux/LinuxPlatform.h b/Source/Engine/Platform/Linux/LinuxPlatform.h index 54590adf2..00e7743d7 100644 --- a/Source/Engine/Platform/Linux/LinuxPlatform.h +++ b/Source/Engine/Platform/Linux/LinuxPlatform.h @@ -139,6 +139,8 @@ public: static String GetWorkingDirectory(); static bool SetWorkingDirectory(const String& path); static Window* CreateWindow(const CreateWindowSettings& settings); + static void StartTrackingMouse(const Window* window); + static void EndTrackingMouse(const Window *window); static void GetEnvironmentVariables(Dictionary& result); static bool GetEnvironmentVariable(const String& name, String& value); static bool SetEnvironmentVariable(const String& name, const String& value); diff --git a/Source/Engine/Platform/Linux/LinuxWindow.cpp b/Source/Engine/Platform/Linux/LinuxWindow.cpp index b3bae0276..9168beb29 100644 --- a/Source/Engine/Platform/Linux/LinuxWindow.cpp +++ b/Source/Engine/Platform/Linux/LinuxWindow.cpp @@ -54,7 +54,7 @@ LinuxWindow::LinuxWindow(const CreateWindowSettings& settings) return; auto screen = XDefaultScreen(display); - // Cache data + // Cache data int32 width = Math::TruncToInt(settings.Size.X); int32 height = Math::TruncToInt(settings.Size.Y); _clientSize = Float2((float)width, (float)height); @@ -111,6 +111,12 @@ LinuxWindow::LinuxWindow(const CreateWindowSettings& settings) windowAttributes.border_pixel = XBlackPixel(display, screen); windowAttributes.event_mask = KeyPressMask | KeyReleaseMask | StructureNotifyMask | ExposureMask; + if (!settings.IsRegularWindow) + { + windowAttributes.save_under = true; + windowAttributes.override_redirect = true; + } + // TODO: implement all window settings /* bool Fullscreen; @@ -118,11 +124,16 @@ LinuxWindow::LinuxWindow(const CreateWindowSettings& settings) bool AllowMaximize; */ + unsigned long valueMask = CWBackPixel | CWBorderPixel | CWEventMask | CWColormap; + if (!settings.IsRegularWindow) + { + valueMask |= CWOverrideRedirect | CWSaveUnder; + } const X11::Window window = X11::XCreateWindow( display, X11::XRootWindow(display, screen), x, y, width, height, 0, visualInfo->depth, InputOutput, visualInfo->visual, - CWBackPixel | CWBorderPixel | CWEventMask | CWColormap, &windowAttributes); + valueMask, &windowAttributes); _window = window; LinuxWindow::SetTitle(settings.Title); @@ -811,12 +822,12 @@ void LinuxWindow::SetTitle(const StringView& title) void LinuxWindow::StartTrackingMouse(bool useMouseScreenOffset) { - // TODO: impl this + LinuxPlatform::StartTrackingMouse(this); } void LinuxWindow::EndTrackingMouse() { - // TODO: impl this + LinuxPlatform::EndTrackingMouse(this); } void LinuxWindow::SetCursor(CursorType type) From 3ab7d7fcc455573b62f134011f27dd9d80c500a7 Mon Sep 17 00:00:00 2001 From: Wiktor Kocielski Date: Sat, 2 Dec 2023 15:29:49 +0300 Subject: [PATCH 011/118] 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 012/118] 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 013/118] 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 014/118] 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 015/118] 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 016/118] 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 017/118] 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 018/118] 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 019/118] 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 020/118] 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 021/118] 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 022/118] 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 023/118] 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 024/118] 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 025/118] 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 026/118] 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 027/118] 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 028/118] 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 029/118] 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 030/118] 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 031/118] 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 032/118] 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 033/118] 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 034/118] 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 035/118] 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 036/118] 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 037/118] 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 038/118] 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 039/118] 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 040/118] 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