// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. #include "ImportModel.h" #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" #include "Engine/Content/Assets/Model.h" #include "Engine/Content/Assets/SkinnedModel.h" #include "Engine/Content/Storage/ContentStorageManager.h" #include "Engine/Content/Assets/Animation.h" #include "Engine/Content/Content.h" #include "Engine/Platform/FileSystem.h" #include "AssetsImportingManager.h" bool ImportModel::TryGetImportOptions(const StringView& path, Options& options) { if (FileSystem::FileExists(path)) { // Try to load asset file and asset info auto tmpFile = ContentStorageManager::GetStorage(path); AssetInitData data; if (tmpFile && tmpFile->GetEntriesCount() == 1 && ( (tmpFile->GetEntry(0).TypeName == Model::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 4) || (tmpFile->GetEntry(0).TypeName == SkinnedModel::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1) || (tmpFile->GetEntry(0).TypeName == Animation::TypeName && !tmpFile->LoadAssetHeader(0, data) && data.SerializedVersion >= 1) )) { // Check import meta rapidjson_flax::Document metadata; metadata.Parse((const char*)data.Metadata.Get(), data.Metadata.Length()); if (metadata.HasParseError() == false) { options.Deserialize(metadata, nullptr); return true; } } } return false; } void TryRestoreMaterials(CreateAssetContext& context, ModelData& modelData) { // Skip if file is missing if (!FileSystem::FileExists(context.TargetAssetPath)) return; // Try to load asset that gets reimported AssetReference asset = Content::LoadAsync(context.TargetAssetPath); if (asset == nullptr) return; if (asset->WaitForLoaded()) return; // Get model object ModelBase* model = nullptr; if (asset.Get()->GetTypeName() == Model::TypeName) { model = ((Model*)asset.Get()); } else if (asset.Get()->GetTypeName() == SkinnedModel::TypeName) { model = ((SkinnedModel*)asset.Get()); } if (!model) return; // Peek materials for (int32 i = 0; i < modelData.Materials.Count(); i++) { auto& dstSlot = modelData.Materials[i]; if (model->MaterialSlots.Count() > i) { auto& srcSlot = model->MaterialSlots[i]; dstSlot.Name = srcSlot.Name; dstSlot.ShadowsMode = srcSlot.ShadowsMode; dstSlot.AssetID = srcSlot.Material.GetID(); } } } 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; if (context.CustomArg != nullptr) { // Copy import options from argument options = *static_cast(context.CustomArg); } else { // Restore the previous settings or use default ones if (!TryGetImportOptions(context.TargetAssetPath, options)) { 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) { // 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; const int32 splitPos = postFix.FindLast(TEXT('|')); if (splitPos != -1) postFix = postFix.Substring(splitPos + 1); 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; } } // 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)) { 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 && data->Materials.HasItems()) { TryRestoreMaterials(context, *data); } // Create destination asset type CreateAssetResult result = CreateAssetResult::InvalidTypeID; switch (options.Type) { case ModelTool::ModelType::Model: result = CreateModel(context, *data, &options); break; case ModelTool::ModelType::SkinnedModel: result = CreateSkinnedModel(context, *data, &options); break; case ModelTool::ModelType::Animation: result = CreateAnimation(context, *data, &options); break; } for (auto mesh : meshesToDelete) Delete(mesh); if (result != CreateAssetResult::Ok) return result; // Create json with import context rapidjson_flax::StringBuffer importOptionsMetaBuffer; importOptionsMetaBuffer.Reserve(256); CompactJsonWriter importOptionsMetaObj(importOptionsMetaBuffer); JsonWriter& importOptionsMeta = importOptionsMetaObj; importOptionsMeta.StartObject(); { context.AddMeta(importOptionsMeta); options.Serialize(importOptionsMeta, nullptr); } importOptionsMeta.EndObject(); context.Data.Metadata.Copy((const byte*)importOptionsMetaBuffer.GetString(), (uint32)importOptionsMetaBuffer.GetSize()); return CreateAssetResult::Ok; } CreateAssetResult ImportModel::Create(CreateAssetContext& context) { ASSERT(context.CustomArg != nullptr); auto& modelData = *(ModelData*)context.CustomArg; // Ensure model has any meshes if ((modelData.LODs.IsEmpty() || modelData.LODs[0].Meshes.IsEmpty())) { LOG(Warning, "Models has no valid meshes"); return CreateAssetResult::Error; } // Auto calculate LODs transition settings modelData.CalculateLODsScreenSizes(); return CreateModel(context, modelData); } CreateAssetResult ImportModel::CreateModel(CreateAssetContext& context, ModelData& modelData, const Options* options) { IMPORT_SETUP(Model, Model::SerializedVersion); // Save model header MemoryWriteStream stream(4096); if (modelData.Pack2ModelHeader(&stream)) return CreateAssetResult::Error; if (context.AllocateChunk(0)) return CreateAssetResult::CannotAllocateChunk; context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition()); // Pack model LODs data const auto lodCount = modelData.GetLODsCount(); for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++) { stream.SetPosition(0); // Pack meshes auto& meshes = modelData.LODs[lodIndex].Meshes; for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++) { if (meshes[meshIndex]->Pack2Model(&stream)) { LOG(Warning, "Cannot pack mesh."); return CreateAssetResult::Error; } } const int32 chunkIndex = lodIndex + 1; if (context.AllocateChunk(chunkIndex)) return CreateAssetResult::CannotAllocateChunk; context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition()); } // Generate SDF if (options && options->GenerateSDF) { stream.SetPosition(0); if (!ModelTool::GenerateModelSDF(nullptr, &modelData, options->SDFResolution, lodCount - 1, nullptr, &stream, context.TargetAssetPath)) { if (context.AllocateChunk(15)) return CreateAssetResult::CannotAllocateChunk; context.Data.Header.Chunks[15]->Data.Copy(stream.GetHandle(), stream.GetPosition()); } } return CreateAssetResult::Ok; } CreateAssetResult ImportModel::CreateSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options) { IMPORT_SETUP(SkinnedModel, SkinnedModel::SerializedVersion); // Save skinned model header MemoryWriteStream stream(4096); if (modelData.Pack2SkinnedModelHeader(&stream)) return CreateAssetResult::Error; if (context.AllocateChunk(0)) return CreateAssetResult::CannotAllocateChunk; context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition()); // Pack model LODs data const auto lodCount = modelData.GetLODsCount(); for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++) { stream.SetPosition(0); // Mesh Data Version stream.WriteByte(1); // Pack meshes auto& meshes = modelData.LODs[lodIndex].Meshes; for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++) { if (meshes[meshIndex]->Pack2SkinnedModel(&stream)) { LOG(Warning, "Cannot pack mesh."); return CreateAssetResult::Error; } } const int32 chunkIndex = lodIndex + 1; if (context.AllocateChunk(chunkIndex)) return CreateAssetResult::CannotAllocateChunk; context.Data.Header.Chunks[chunkIndex]->Data.Copy(stream.GetHandle(), stream.GetPosition()); } return CreateAssetResult::Ok; } CreateAssetResult ImportModel::CreateAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options) { IMPORT_SETUP(Animation, Animation::SerializedVersion); // Save animation data MemoryWriteStream stream(8182); 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; context.Data.Header.Chunks[0]->Data.Copy(stream.GetHandle(), stream.GetPosition()); return CreateAssetResult::Ok; } #endif