LifePlay-Revival/Source/LifePlayEditorTools/LPImportLandmarkMeshCommandlet.cpp
2026-10-22 21:10:39 +02:00

313 lines
14 KiB
C++

// 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_<v>/(Landmarks|Houses)/<Nom> ou /Game/Heavy/StreetMap/(Landmarks|Houses)/<Nom> (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<FString> 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<FString>& Out)
{
FStaticMeshConstAttributes Attributes(Desc);
const TPolygonGroupAttributesConstRef<FName> SlotNames = Attributes.GetPolygonGroupMaterialSlotNames();
const TVertexAttributesConstRef<FVector3f> Positions = Attributes.GetVertexPositions();
TMap<FPolygonGroupID, int32> Tris;
TMap<FPolygonGroupID, FBox3f> 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<FString> Tokens, Switches;
TMap<FString, FString> 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<FString> 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<FString> LodText;
ParamsMap.FindRef(TEXT("Lods")).ParseIntoArray(LodText, TEXT(","));
// 1.4.3 : -Materials=<emplacement>=<objet>;... : 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<FString, FString> SlotMaterials;
{
TArray<FString> 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<FString> 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=<fichier .fbx ou .obj> -Package=</lpm_city_<v>/(Landmarks|Houses)/<Nom>/<Nom>> [-MaxTriangles=20000] [-Lods=<t1>,<t2>] [-MaxTexture=2048] [-RemoveSections=<i>,<j>] [-Materials=<emplacement>=<objet>;...] [-Report=<f.md>]"));
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)/<Nom> et de /Game/Heavy/StreetMap/(Landmarks|Houses)/ ; rien importe"));
return Finish(1);
}
TArray<FAssetData> Existing;
IAssetRegistry& Registry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(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<UFbxFactory>(GetTransientPackage());
Factory->AddToRoot();
UAutomatedAssetImportData* Automated = NewObject<UAutomatedAssetImportData>(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<UStaticMesh>(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<UMaterialInterface>(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<FPolygonID> 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<FEdgeID> OrphanEdges;
TArray<FVertexInstanceID> 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<FString> 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=<s1>,<s2> ; 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<UPackage*> Packages;
for (TObjectIterator<UObject> 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<UMaterial>(*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<UTexture2D>(*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);
}