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