314 lines
14 KiB
C++
314 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);
|
||
|
|
}
|