// Lot STM1 (STM1-SPEC-11, M3 ; 1.4.3, STM1-SPEC-19 A1). Voir LPImportLandmarkMeshCommandlet.h. #include "LPImportLandmarkMeshCommandlet.h" #include "AssetRegistry/AssetRegistryModule.h" #include "AutomatedAssetImportData.h" #include "Engine/StaticMesh.h" #include "Engine/Texture2D.h" #include "Materials/Material.h" #include "Factories/FbxFactory.h" #include "Factories/FbxImportUI.h" #include "Factories/FbxStaticMeshImportData.h" #include "HAL/FileManager.h" #include "MeshDescription.h" #include "Misc/FileHelper.h" #include "Misc/PackageName.h" #include "StaticMeshAttributes.h" #include "UObject/Package.h" #include "UObject/SavePackage.h" #include "UObject/UObjectIterator.h" DEFINE_LOG_CATEGORY_STATIC(LogLPImportLandmarkMesh, Log, All); namespace LPImportLandmarkMesh { /** Dossier autorise : /lpm_city_/(Landmarks|Houses)/ ou /Game/Heavy/StreetMap/(Landmarks|Houses)/ (Houses : 1.2.1, immeubles). */ bool IsAllowedFolder(const FString& Folder) { if (Folder.StartsWith(TEXT("/Game/Heavy/StreetMap/Landmarks/")) || Folder.StartsWith(TEXT("/Game/Heavy/StreetMap/Houses/"))) { return true; } TArray Parts; Folder.ParseIntoArray(Parts, TEXT("/")); return Parts.Num() == 3 && Parts[0].StartsWith(TEXT("lpm_city_")) && (Parts[1] == TEXT("Landmarks") || Parts[1] == TEXT("Houses")); } /** Triangles et bornes par groupe de polygones de la description source. */ void DescribeSections(const FMeshDescription& Desc, TArray& Out) { FStaticMeshConstAttributes Attributes(Desc); const TPolygonGroupAttributesConstRef SlotNames = Attributes.GetPolygonGroupMaterialSlotNames(); const TVertexAttributesConstRef Positions = Attributes.GetVertexPositions(); TMap Tris; TMap Boxes; for (const FTriangleID T : Desc.Triangles().GetElementIDs()) { const FPolygonGroupID G = Desc.GetTrianglePolygonGroup(T); Tris.FindOrAdd(G)++; FBox3f& Box = Boxes.FindOrAdd(G, FBox3f(ForceInit)); for (const FVertexID V : Desc.GetTriangleVertices(T)) { Box += Positions[V]; } } for (const FPolygonGroupID G : Desc.PolygonGroups().GetElementIDs()) { const FBox3f Box = Boxes.FindRef(G); Out.Add(FString::Printf(TEXT("| %d | %s | %d | (%.0f, %.0f, %.0f) a (%.0f, %.0f, %.0f) |"), G.GetValue(), *SlotNames[G].ToString(), Tris.FindRef(G), Box.Min.X, Box.Min.Y, Box.Min.Z, Box.Max.X, Box.Max.Y, Box.Max.Z)); } } } ULPImportLandmarkMeshCommandlet::ULPImportLandmarkMeshCommandlet() { IsClient = false; IsServer = false; IsEditor = true; LogToConsole = true; } int32 ULPImportLandmarkMeshCommandlet::Main(const FString& Params) { using namespace LPImportLandmarkMesh; TArray Tokens, Switches; TMap ParamsMap; ParseCommandLine(*Params, Tokens, Switches, ParamsMap); const FString FbxPath = ParamsMap.FindRef(TEXT("Fbx")); const FString PackageName = ParamsMap.FindRef(TEXT("Package")); const FString ReportPath = ParamsMap.FindRef(TEXT("Report")); const int32 MaxTriangles = ParamsMap.Contains(TEXT("MaxTriangles")) ? FCString::Atoi(*ParamsMap[TEXT("MaxTriangles")]) : 20000; const int32 MaxTexture = ParamsMap.Contains(TEXT("MaxTexture")) ? FCString::Atoi(*ParamsMap[TEXT("MaxTexture")]) : 2048; TArray RemoveText; ParamsMap.FindRef(TEXT("RemoveSections")).ParseIntoArray(RemoveText, TEXT(",")); // 1.2.1 : LOD supplementaires, budgets de triangles decroissants (ex. -Lods=400,80), reduits depuis le LOD0 source. TArray LodText; ParamsMap.FindRef(TEXT("Lods")).ParseIntoArray(LodText, TEXT(",")); // 1.4.3 : -Materials==;... : materiaux existants poses sur les emplacements du FBX (ni materiau ni // texture importes) ; les variantes d'un meme modele partagent ainsi les materiaux et les textures du premier import. TMap SlotMaterials; { TArray Pairs; ParamsMap.FindRef(TEXT("Materials")).ParseIntoArray(Pairs, TEXT(";")); for (const FString& Pair : Pairs) { FString Key, Value; if (Pair.Split(TEXT("="), &Key, &Value)) SlotMaterials.Add(Key, Value); } } TArray Report; auto Line = [&Report](const FString& Text) { UE_LOG(LogLPImportLandmarkMesh, Display, TEXT("%s"), *Text); Report.Add(Text); }; auto Finish = [&](int32 Code) { Line(FString::Printf(TEXT("CODE %d"), Code)); if (!ReportPath.IsEmpty()) { FFileHelper::SaveStringArrayToFile(Report, *ReportPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM); } return Code; }; if (FbxPath.IsEmpty() || PackageName.IsEmpty()) { Line(TEXT("usage : -Fbx= -Package=/(Landmarks|Houses)//> [-MaxTriangles=20000] [-Lods=,] [-MaxTexture=2048] [-RemoveSections=,] [-Materials==;...] [-Report=]")); return Finish(2); } const FString Folder = FPackageName::GetLongPackagePath(PackageName); const FString AssetName = FPackageName::GetLongPackageAssetName(PackageName); Line(FString::Printf(TEXT("# LPImportLandmarkMesh %s <- %s"), *PackageName, *FbxPath)); if (!IsAllowedFolder(Folder) || !FPackageName::IsValidLongPackageName(PackageName)) { Line(TEXT("ECHEC : dossier hors de /lpm_city_*/(Landmarks|Houses)/ et de /Game/Heavy/StreetMap/(Landmarks|Houses)/ ; rien importe")); return Finish(1); } TArray Existing; IAssetRegistry& Registry = FModuleManager::LoadModuleChecked(TEXT("AssetRegistry")).Get(); Registry.ScanPathsSynchronous({ Folder }, /*bForceRescan=*/true); Registry.GetAssetsByPath(FName(*Folder), Existing, true); if (!Existing.IsEmpty() || FPackageName::DoesPackageExist(PackageName)) { Line(FString::Printf(TEXT("ECHEC : le dossier %s n'est pas neuf (%d asset(s)) ; rien importe"), *Folder, Existing.Num())); return Finish(1); } const int64 FbxBytes = IFileManager::Get().FileSize(*FbxPath); if (FbxBytes <= 0) { Line(TEXT("ECHEC : FBX introuvable ou vide")); return Finish(1); } // Import automatise (aucune boite de dialogue), un seul maillage statique, materiaux et textures. UFbxFactory* Factory = NewObject(GetTransientPackage()); Factory->AddToRoot(); UAutomatedAssetImportData* Automated = NewObject(GetTransientPackage()); Automated->DestinationPath = Folder; Automated->bReplaceExisting = false; Factory->SetAutomatedAssetImportData(Automated); Factory->ImportUI->bImportAsSkeletal = false; Factory->ImportUI->bImportAnimations = false; Factory->ImportUI->bImportMesh = true; Factory->ImportUI->bImportMaterials = SlotMaterials.IsEmpty(); Factory->ImportUI->bImportTextures = SlotMaterials.IsEmpty(); Factory->ImportUI->bAutomatedImportShouldDetectType = false; Factory->ImportUI->MeshTypeToImport = FBXIT_StaticMesh; Factory->ImportUI->StaticMeshImportData->bCombineMeshes = true; Factory->ImportUI->StaticMeshImportData->bAutoGenerateCollision = false; Factory->ImportUI->StaticMeshImportData->bGenerateLightmapUVs = false; UPackage* Package = CreatePackage(*PackageName); bool bCanceled = false; UObject* Imported = Factory->ImportObject(UStaticMesh::StaticClass(), Package, *AssetName, RF_Public | RF_Standalone, FbxPath, nullptr, bCanceled); Factory->RemoveFromRoot(); UStaticMesh* Mesh = Cast(Imported); if (!Mesh || bCanceled) { Line(FString::Printf(TEXT("ECHEC : import (%s) ; FBX %lld octets"), bCanceled ? TEXT("annule") : TEXT("pas de maillage statique"), FbxBytes)); return Finish(1); } FMeshDescription* Desc = Mesh->GetMeshDescription(0); const FBox Bounds = Mesh->GetBoundingBox(); Line(FString::Printf(TEXT("importe : FBX %lld octets ; %d section(s) ; description %d triangles ; bornes (%.0f, %.0f, %.0f) a (%.0f, %.0f, %.0f) cm ; taille %.0f x %.0f x %.0f cm"), FbxBytes, Mesh->GetStaticMaterials().Num(), Desc ? Desc->Triangles().Num() : -1, Bounds.Min.X, Bounds.Min.Y, Bounds.Min.Z, Bounds.Max.X, Bounds.Max.Y, Bounds.Max.Z, Bounds.GetSize().X, Bounds.GetSize().Y, Bounds.GetSize().Z)); if (Desc) { Line(TEXT("| section | materiau | triangles | bornes (cm) |")); Line(TEXT("|---|---|---|---|")); DescribeSections(*Desc, Report); } if (Switches.Contains(TEXT("InspectOnly"))) { Line(TEXT("InspectOnly : rien enregistre (import en memoire seulement)")); return Finish(0); } // 1.4.3 : materiaux existants sur les emplacements nommes (tous les emplacements doivent etre couverts). if (!SlotMaterials.IsEmpty()) { for (FStaticMaterial& Slot : Mesh->GetStaticMaterials()) { const FString SlotName = Slot.ImportedMaterialSlotName.ToString(); const FString* Path = SlotMaterials.Find(SlotName); UMaterialInterface* Material = Path ? LoadObject(nullptr, **Path) : nullptr; if (!Material) { Line(FString::Printf(TEXT("ECHEC : emplacement %s sans materiau existant (%s) ; rien enregistre"), *SlotName, Path ? **Path : TEXT("absent de -Materials"))); return Finish(1); } Slot.MaterialInterface = Material; Line(FString::Printf(TEXT("emplacement %s -> %s"), *SlotName, *Material->GetPathName())); } } // Retrait de sections (par exemple la pyramide du Louvre, oeuvre protegee). if (Desc && !RemoveText.IsEmpty()) { TArray ToDelete; for (const FString& Text : RemoveText) { const FPolygonGroupID Group(FCString::Atoi(*Text)); if (!Desc->IsPolygonGroupValid(Group)) { Line(FString::Printf(TEXT("ECHEC : section %s inconnue ; rien enregistre"), *Text)); return Finish(1); } ToDelete.Append(Desc->GetPolygonGroupPolygonIDs(Group)); } TArray OrphanEdges; TArray OrphanInstances; for (const FPolygonID Polygon : ToDelete) Desc->DeletePolygon(Polygon, &OrphanEdges, &OrphanInstances, nullptr); for (const FVertexInstanceID I : OrphanInstances) if (Desc->IsVertexInstanceValid(I)) Desc->DeleteVertexInstance(I); for (const FEdgeID E : OrphanEdges) if (Desc->IsEdgeValid(E)) Desc->DeleteEdge(E); Mesh->CommitMeshDescription(0); Line(FString::Printf(TEXT("sections retirees : %s (%d polygones) ; reste %d triangles"), *FString::Join(RemoveText, TEXT(",")), ToDelete.Num(), Desc->Triangles().Num())); } // Reduction du LOD0 au budget, puis construction. const int32 SourceTris = Desc ? Desc->Triangles().Num() : 0; if (SourceTris > MaxTriangles && MaxTriangles > 0) { FStaticMeshSourceModel& Source = Mesh->GetSourceModel(0); Source.ReductionSettings.TerminationCriterion = EStaticMeshReductionTerimationCriterion::Triangles; Source.ReductionSettings.PercentTriangles = float(MaxTriangles) / float(SourceTris); Source.ReductionSettings.MaxNumOfTriangles = uint32(MaxTriangles); } if (!LodText.IsEmpty() && SourceTris > 0) { // La reduction d'un LOD part du LOD0 deja reduit : le pourcentage se calcule sur son budget. const int32 Lod0Tris = (MaxTriangles > 0) ? FMath::Min(SourceTris, MaxTriangles) : SourceTris; TArray ScreenText; ParamsMap.FindRef(TEXT("LodScreen")).ParseIntoArray(ScreenText, TEXT(",")); Mesh->SetNumSourceModels(1 + LodText.Num()); Mesh->SetAutoComputeLODScreenSize(false); Mesh->GetSourceModel(0).ScreenSize = 1.f; for (int32 L = 0; L < LodText.Num(); ++L) { const int32 Budget = FMath::Max(1, FCString::Atoi(*LodText[L])); FStaticMeshSourceModel& Lod = Mesh->GetSourceModel(1 + L); Lod.BuildSettings = Mesh->GetSourceModel(0).BuildSettings; Lod.ReductionSettings.BaseLODModel = 0; Lod.ReductionSettings.TerminationCriterion = EStaticMeshReductionTerimationCriterion::Triangles; Lod.ReductionSettings.PercentTriangles = FMath::Min(1.f, float(Budget) / float(Lod0Tris)); Lod.ReductionSettings.MaxNumOfTriangles = uint32(Budget); // Tailles d'ecran : -LodScreen=, ; par defaut 0,25 puis divisee par 3 a chaque LOD. Lod.ScreenSize = ScreenText.IsValidIndex(L) ? FCString::Atof(*ScreenText[L]) : 0.25f / FMath::Pow(3.f, float(L)); } } Mesh->Build(/*bSilent=*/true); Mesh->PostEditChange(); const int32 LodTris = Mesh->GetRenderData() && Mesh->GetRenderData()->LODResources.Num() ? Mesh->GetRenderData()->LODResources[0].GetNumTriangles() : -1; Line(FString::Printf(TEXT("LOD0 rendu : %d triangles (source %d, budget %d)"), LodTris, SourceTris, MaxTriangles)); for (int32 L = 1; Mesh->GetRenderData() && L < Mesh->GetRenderData()->LODResources.Num(); ++L) { Line(FString::Printf(TEXT("LOD%d rendu : %d triangles (budget %s) ; taille d'ecran %.3f"), L, Mesh->GetRenderData()->LODResources[L].GetNumTriangles(), LodText.IsValidIndex(L - 1) ? *LodText[L - 1] : TEXT("-"), Mesh->GetRenderData()->ScreenSize[L].Default)); } // Textures du dossier : plafond de taille. Enregistrement de tous les paquets du dossier. int32 Textures = 0, Saved = 0; TArray Packages; for (TObjectIterator It; It; ++It) { UPackage* Outer = It->GetPackage(); if (!Outer || !Outer->GetName().StartsWith(Folder + TEXT("/")) || !It->IsAsset()) continue; // 1.2.2 : les immeubles sont poses en HISM ; sans ce drapeau, le jeu cuit retombe sur le materiau par defaut. if (UMaterial* Material = Cast(*It); Material && Folder.Contains(TEXT("/Houses/")) && !Material->GetUsageByFlag(MATUSAGE_InstancedStaticMeshes)) { Material->SetUsageByFlag(MATUSAGE_InstancedStaticMeshes, true); Material->PostEditChange(); Line(FString::Printf(TEXT("materiau %s : bUsedWithInstancedStaticMeshes coche"), *Material->GetName())); } if (UTexture2D* Texture = Cast(*It)) { Texture->MaxTextureSize = MaxTexture; Texture->PostEditChange(); ++Textures; Line(FString::Printf(TEXT("texture %s : source %d x %d, plafond %d"), *Texture->GetName(), Texture->Source.GetSizeX(), Texture->Source.GetSizeY(), MaxTexture)); } Packages.AddUnique(Outer); } for (UPackage* Pkg : Packages) { UObject* Main = Pkg->FindAssetInPackage(); if (!Main) continue; FAssetRegistryModule::AssetCreated(Main); FSavePackageArgs SaveArgs; SaveArgs.TopLevelFlags = RF_Public | RF_Standalone; const FString File = FPackageName::LongPackageNameToFilename(Pkg->GetName(), FPackageName::GetAssetPackageExtension()); if (!UPackage::SavePackage(Pkg, Main, *File, SaveArgs)) { Line(FString::Printf(TEXT("ECHEC : sauvegarde de %s"), *Pkg->GetName())); return Finish(1); } ++Saved; Line(FString::Printf(TEXT("ecrit : %s (%s), %lld octets"), *Pkg->GetName(), *Main->GetClass()->GetName(), IFileManager::Get().FileSize(*File))); } Line(FString::Printf(TEXT("textures plafonnees %d ; paquets ecrits %d"), Textures, Saved)); return Finish(0); }