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

710 lines
34 KiB
C++

// Lot STM1 (STM1-SPEC-09, B5 ; STM1-SPEC-17 : 1.4.0). Voir LPBuildCityStaticCommandlet.h.
#include "LPBuildCityStaticCommandlet.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Dom/JsonObject.h"
#include "Engine/StaticMesh.h"
#include "HAL/FileManager.h"
#include "HAL/PlatformTime.h"
#include "LPCityLandmarks.h"
#include "LPCityOsm.h"
#include "LPCityWaterMask.h"
#include "Materials/MaterialInterface.h"
#include "MeshDescription.h"
#include "Misc/FileHelper.h"
#include "Misc/PackageName.h"
#include "RawMesh.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"
#include "StaticMeshAttributes.h"
#include "StreetMap.h"
#include "StreetMapComponent.h"
#include "UObject/Package.h"
#include "UObject/SavePackage.h"
DEFINE_LOG_CATEGORY_STATIC(LogLPBuildCityStatic, Log, All);
namespace LPBuildCityStatic
{
/** Paquets autorises : pack de ville lpm_city_* ou paquet d'essai _Pilot. */
bool IsAllowedPackage(const FString& PackageName)
{
if (PackageName.StartsWith(TEXT("/Game/Heavy/StreetMap/_Pilot/")))
{
return true;
}
TArray<FString> Parts;
PackageName.ParseIntoArray(Parts, TEXT("/"));
return Parts.Num() >= 4 && Parts[0].StartsWith(TEXT("lpm_city_")) && Parts[1] == TEXT("StreetMap") && (Parts[2] == TEXT("Static") || Parts[2] == TEXT("Classic"));
}
/** Reglages du composant : bWant3DBuildings et bWantLitBuildings (champs proteges, ecrits par reflexion) ; 1.3.2 :
* couleur de bordure des batiments (le remplissage des murs et des toits en vaut 0,33). */
struct FBuildOptions
{
bool bWant3DBuildings = true;
bool bWantLitBuildings = true;
bool bSetBuildingColor = false;
FLinearColor BuildingBorderColor = FLinearColor(0.85f, 0.85f, 0.85f);
// 1.4.1 (STM1-DIAG-03 f) : hauteur des routes (RoadOffesetZ du plugin) ; les parcs restent a 0 (ParkFillZ code en dur).
float RoadZCm = 0.f;
};
bool SetBool(UStruct* Struct, void* Container, const TCHAR* Name, bool bValue)
{
FBoolProperty* Prop = CastField<FBoolProperty>(Struct->FindPropertyByName(Name));
if (!Prop) return false;
Prop->SetPropertyValue_InContainer(Container, bValue);
return true;
}
/** Maillage brut genere par le composant du plugin. */
UStreetMapComponent* BuildComponent(UStreetMap* Map, const FBuildOptions& Options)
{
UStreetMapComponent* Component = NewObject<UStreetMapComponent>(GetTransientPackage());
FStructProperty* SettingsProp = CastField<FStructProperty>(UStreetMapComponent::StaticClass()->FindPropertyByName(TEXT("MeshBuildSettings")));
void* Settings = SettingsProp ? SettingsProp->ContainerPtrToValuePtr<void>(Component) : nullptr;
const bool bSet = Settings
&& SetBool(SettingsProp->Struct, Settings, TEXT("bWant3DBuildings"), Options.bWant3DBuildings)
&& SetBool(SettingsProp->Struct, Settings, TEXT("bWantLitBuildings"), Options.bWantLitBuildings);
check(bSet);
if (Options.RoadZCm != 0.f)
{
FFloatProperty* RoadZProp = CastField<FFloatProperty>(SettingsProp->Struct->FindPropertyByName(TEXT("RoadOffesetZ")));
check(RoadZProp);
RoadZProp->SetPropertyValue_InContainer(Settings, Options.RoadZCm);
}
if (Options.bSetBuildingColor)
{
FStructProperty* ColorProp = CastField<FStructProperty>(SettingsProp->Struct->FindPropertyByName(TEXT("BuildingBorderLinearColor")));
check(ColorProp);
*ColorProp->ContainerPtrToValuePtr<FLinearColor>(Settings) = Options.BuildingBorderColor;
}
Component->SetStreetMap(Map, true, true);
return Component;
}
/** 1.4.0 (STM1-SPEC-17) : surfaces OSM des routes pavees (le materiau du pack choisit les paves par U > 1,5). */
bool IsPavedSurface(const FString& Surface)
{
return Surface == TEXT("sett") || Surface == TEXT("cobblestone") || Surface == TEXT("unhewn_cobblestone");
}
}
ULPBuildCityStaticCommandlet::ULPBuildCityStaticCommandlet()
{
IsClient = false;
IsServer = false;
IsEditor = true;
LogToConsole = true;
}
int32 ULPBuildCityStaticCommandlet::Main(const FString& Params)
{
using namespace LPBuildCityStatic;
TArray<FString> Tokens, Switches;
TMap<FString, FString> ParamsMap;
ParseCommandLine(*Params, Tokens, Switches, ParamsMap);
const FString MapPath = ParamsMap.FindRef(TEXT("StreetMap"));
const FString PackageName = ParamsMap.FindRef(TEXT("Package"));
const FString ReferencePath = ParamsMap.Contains(TEXT("Reference")) ? ParamsMap[TEXT("Reference")] : FString(TEXT("/Game/Heavy/StreetMap/Static/CentralLondon.CentralLondon"));
const FString ComparePath = ParamsMap.FindRef(TEXT("CompareStreetMap"));
const FString ReportPath = ParamsMap.FindRef(TEXT("Report"));
const bool bDryRun = Switches.Contains(TEXT("DryRun"));
// Ecart admis (en %) entre les triangles de la carte de comparaison reconstruite et ceux du maillage de reference : la
// description source du moteur ecarte les triangles degeneres du FRawMesh (bordures de longueur nulle).
const double TolerancePercent = ParamsMap.Contains(TEXT("TolerancePercent")) ? FCString::Atod(*ParamsMap[TEXT("TolerancePercent")]) : 0.0;
FBuildOptions Options;
Options.bWant3DBuildings = !Switches.Contains(TEXT("Flat"));
Options.bWantLitBuildings = !Switches.Contains(TEXT("Unlit"));
// 1.4.1 (STM1-DIAG-03 f) : -RoadZCm=<cm> eleve les routes au-dessus des parcs (meme hauteur 0 par defaut : scintillement).
Options.RoadZCm = ParamsMap.Contains(TEXT("RoadZCm")) ? FCString::Atof(*ParamsMap[TEXT("RoadZCm")]) : 0.f;
// 1.3.0 : carte classique en 3D. -Osm -Origin : etages OSM reels (sinon height / 3 m, sinon -DefaultLevels ; petits types et
// emprises sous 30 m2 sans etages : 1), batiments filtres comme la table (souterrains, layer < 0, tunnels, toits, emprises
// sous 20 m2) retires ; -BuildingsOnly : routes, parcs et eaux retires (maillage de batiments seuls, pose a part en jeu).
// Ces changements portent sur une copie transitoire de l'UStreetMap, jamais sur l'asset.
const FString OsmPath = ParamsMap.FindRef(TEXT("Osm"));
const FString OriginText = ParamsMap.FindRef(TEXT("Origin"));
const bool bBuildingsOnly = Switches.Contains(TEXT("BuildingsOnly"));
// -ExcludeHidden : retire aussi les batiments que le jeu masque (centres des monuments et du masque d'eau de la ville).
const bool bExcludeHidden = Switches.Contains(TEXT("ExcludeHidden"));
const FString MaterialPath = ParamsMap.FindRef(TEXT("Material"));
// 1.3.2 : -BuildingColor=R,G,B (octets / 255, ecrits tels quels par le plugin et relus comme du sRGB) : couleur des murs et des toits (le plugin les remplit avec
// 0,33 x BuildingBorderLinearColor) ; -SampleColors : couleurs moyennes des toits et des murs du maillage de reference.
const FString BuildingColorText = ParamsMap.FindRef(TEXT("BuildingColor"));
const int32 DefaultLevels = ParamsMap.Contains(TEXT("DefaultLevels")) ? FCString::Atoi(*ParamsMap[TEXT("DefaultLevels")]) : 6;
// 1.4.0 (STM1-SPEC-17) : -PavedOsm=<f.osm.xml> (avec -Origin) : U + 2 sur les quadrilateres des routes pavees. Couleurs de
// sommet et geometrie inchangees : la carte classique (StreetMapClassic, couleur de sommet seule) ne change pas.
const FString PavedOsmPath = ParamsMap.FindRef(TEXT("PavedOsm"));
// 1.5.1 (STM1-DIAG-06) : -PavedZCm=<cm> (avec -PavedOsm) : hauteur des routes pavees, sous les autres (-RoadZCm). Aux carrefours et
// sous les ponts, l'asphalte et les paves ne sont plus coplanaires (z-fighting de deux textures).
const bool bPavedZ = ParamsMap.Contains(TEXT("PavedZCm"));
const float PavedZCm = bPavedZ ? FCString::Atof(*ParamsMap[TEXT("PavedZCm")]) : 0.f;
TArray<FString> Report;
auto Line = [&Report](const FString& Text)
{
UE_LOG(LogLPBuildCityStatic, 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 (MapPath.IsEmpty() || PackageName.IsEmpty())
{
Line(TEXT("usage : -StreetMap=<objet> -Package=</lpm_city_<v>/StreetMap/(Static|Classic)/<Ville>> [-Reference=<objet>] [-CompareStreetMap=<objet>] [-Flat] [-Unlit] [-Osm=<f.osm.xml> -Origin=<lat>,<lon> [-DefaultLevels=6]] [-BuildingsOnly] [-ExcludeHidden] [-Material=<objet>] [-BuildingColor=R,G,B] [-SampleColors] [-PavedOsm=<f.osm.xml> -Origin=<lat>,<lon> [-PavedZCm=<cm>]] [-RoadZCm=0] [-CutBridgeRoads=<liste.json> -Origin=<lat>,<lon>] [-DryRun] [-Report=<f.md>]"));
return Finish(2);
}
Line(FString::Printf(TEXT("# LPBuildCityStatic %s <- %s (%s) ; bWant3DBuildings=%d bWantLitBuildings=%d ; routes a %.1f cm"), *PackageName, *MapPath,
bDryRun ? TEXT("DryRun") : TEXT("ecriture"), Options.bWant3DBuildings ? 1 : 0, Options.bWantLitBuildings ? 1 : 0, Options.RoadZCm));
if (!IsAllowedPackage(PackageName) || !FPackageName::IsValidLongPackageName(PackageName))
{
Line(TEXT("ECHEC : paquet hors de /lpm_city_*/StreetMap/(Static|Classic)/ et de /Game/Heavy/StreetMap/_Pilot/ ; aucun asset ecrit"));
return Finish(1);
}
if (FPackageName::DoesPackageExist(PackageName))
{
Line(FString::Printf(TEXT("ECHEC : %s existe deja ; aucun asset ecrit"), *PackageName));
return Finish(1);
}
UStreetMap* Map = LoadObject<UStreetMap>(nullptr, *MapPath);
const UStaticMesh* Reference = LoadObject<UStaticMesh>(nullptr, *ReferencePath);
if (!Map || !Reference)
{
Line(FString::Printf(TEXT("ECHEC : carte (%d) ou maillage de reference (%d) introuvable"), Map ? 1 : 0, Reference ? 1 : 0));
return Finish(1);
}
if (!OsmPath.IsEmpty() || bBuildingsOnly || bExcludeHidden)
{
Map = DuplicateObject<UStreetMap>(Map, GetTransientPackage());
// 1.3.2 : parcs de la carte (avant leur retrait), pour masquer les musees et batiments historiques qui s'y trouvent.
TArray<TArray<FVector2D>> ParkPolygons;
TArray<FBox2D> ParkBoxes;
for (const FStreetMapPark& Park : Map->GetParks())
{
if (Park.ParkPoints.Num() < 3) continue;
ParkPolygons.Add(Park.ParkPoints);
ParkBoxes.Add(FBox2D(Park.ParkPoints));
}
if (bBuildingsOnly)
{
Map->GetRoads().Empty();
Map->GetNodes().Empty();
Map->GetWaters().Empty();
Map->GetParks().Empty();
}
if (!OsmPath.IsEmpty())
{
TArray<FString> OriginParts;
OriginText.ParseIntoArray(OriginParts, TEXT(","));
LPCityOsm::FReader Osm;
FString Error;
if (OriginParts.Num() != 2)
{
Line(TEXT("ECHEC : -Osm demande -Origin=<lat>,<lon> ; aucun asset ecrit"));
return Finish(1);
}
Osm.OriginLat = FCString::Atod(*OriginParts[0]);
Osm.OriginLon = FCString::Atod(*OriginParts[1]);
if (!LPCityOsm::Read(Osm, OsmPath, Error))
{
Line(FString::Printf(TEXT("ECHEC : %s ; aucun asset ecrit"), *Error));
return Finish(1);
}
Osm.Nodes.Empty();
LPCityOsm::FWayIndex Index;
Index.Build(Osm.WayCenters);
static const TSet<FString> SmallTypes = { TEXT("kiosk"), TEXT("shed"), TEXT("toilets"), TEXT("hut"), TEXT("cabin"), TEXT("garage"), TEXT("garages"), TEXT("carport") };
int32 Matched = 0, Removed = 0, Real = 0, Small = 0, Default = 0, ParkMonuments = 0;
TArray<FStreetMapBuilding>& Buildings = Map->GetBuildings();
for (int32 I = Buildings.Num() - 1; I >= 0; --I)
{
FStreetMapBuilding& Building = Buildings[I];
if (Building.BuildingPoints.Num() < 3) { Buildings.RemoveAt(I); ++Removed; continue; }
const int32 Way = Index.Find(LPCityOsm::BoxCenter(Building.BuildingPoints));
const TMap<FString, FString>* Tags = Way != INDEX_NONE ? &Osm.WayTags[Way] : nullptr;
Matched += Tags ? 1 : 0;
double Area = 0.0;
for (int32 P = 0; P < Building.BuildingPoints.Num(); ++P)
{
const FVector2D& A = Building.BuildingPoints[P];
const FVector2D& B = Building.BuildingPoints[(P + 1) % Building.BuildingPoints.Num()];
Area += A.X * B.Y - B.X * A.Y;
}
Area = FMath::Abs(Area) * 0.5;
if (LPCityOsm::IsExcludedBuilding(Tags) || Area < 20.0 * 1e4) { Buildings.RemoveAt(I); ++Removed; continue; }
if (LPCityOsm::IsMuseumOrHistoric(Tags) && LPCityOsm::InAny(ParkPolygons, ParkBoxes, LPCityOsm::BoxCenter(Building.BuildingPoints)))
{
Buildings.RemoveAt(I); ++ParkMonuments; continue;
}
int32 Levels = LPCityOsm::KnownLevels(Tags, 300.0);
if (Levels > 0) ++Real;
else if ((Tags && SmallTypes.Contains(Tags->FindRef(TEXT("building")))) || Area < 30.0 * 1e4) { Levels = 1; ++Small; }
else { Levels = DefaultLevels; ++Default; }
Building.BuildingLevels = Levels;
Building.Height = 0.0f;
}
Line(FString::Printf(TEXT("OSM %s : batiments associes %d ; retires %d (filtres et emprises sous 20 m2) ; musees et batiments historiques de parc retires %d ; etages reels %d, petits a 1 etage %d, defaut (%d) %d ; restent %d"),
*OsmPath, Matched, Removed, ParkMonuments, Real, Small, DefaultLevels, Default, Buildings.Num()));
}
if (bExcludeHidden)
{
// Racine et ville d'apres l'objet de la carte : </racine>/<Ville>.<Ville>.
const FString MapPackage = FPackageName::ObjectPathToPackageName(MapPath);
const FString Root = FPackageName::GetLongPackagePath(MapPackage);
const FString CityName = FPackageName::GetLongPackageAssetName(MapPackage);
TArray<FVector2D> Hidden;
if (const ULPCityLandmarks* Landmarks = LoadObject<ULPCityLandmarks>(nullptr, *FString::Printf(TEXT("%s/Landmarks/%s.%s"), *Root, *CityName, *CityName)))
{
for (const FLPCityLandmark& L : Landmarks->Landmarks) Hidden.Append(L.HideCenters);
}
if (const ULPCityWaterMask* Mask = LoadObject<ULPCityWaterMask>(nullptr, *FString::Printf(TEXT("%s/Water/%s_Mask.%s_Mask"), *Root, *CityName, *CityName)))
{
Hidden.Append(Mask->Centers);
}
TArray<FStreetMapBuilding>& Buildings = Map->GetBuildings();
const int32 Before = Buildings.Num();
Buildings.RemoveAll([&Hidden](const FStreetMapBuilding& Building)
{
if (Building.BuildingPoints.Num() < 3) return false;
const FVector2D C = LPCityOsm::BoxCenter(Building.BuildingPoints);
return Hidden.ContainsByPredicate([&C](const FVector2D& H) { return FVector2D::Distance(H, C) <= 50.0; });
});
Line(FString::Printf(TEXT("batiments masques par le jeu (monuments et eau, %d centres) : %d retires"), Hidden.Num(), Before - Buildings.Num()));
}
Line(FString::Printf(TEXT("copie transitoire de la carte : %d batiments, %d routes, %d parcs, %d eaux%s"), Map->GetBuildings().Num(), Map->GetRoads().Num(),
Map->GetParks().Num(), Map->GetWaters().Num(), bBuildingsOnly ? TEXT(" (batiments seuls)") : TEXT("")));
}
if (!BuildingColorText.IsEmpty())
{
TArray<FString> Rgb;
BuildingColorText.ParseIntoArray(Rgb, TEXT(","));
if (Rgb.Num() != 3)
{
Line(TEXT("ECHEC : -BuildingColor=R,G,B attendu ; aucun asset ecrit"));
return Finish(1);
}
const FLinearColor Fill(FCString::Atof(*Rgb[0]), FCString::Atof(*Rgb[1]), FCString::Atof(*Rgb[2]), 1.f);
Options.bSetBuildingColor = true;
Options.BuildingBorderColor = FLinearColor(Fill.R / 0.33f, Fill.G / 0.33f, Fill.B / 0.33f, 1.f);
Line(FString::Printf(TEXT("couleur des batiments (murs et toits) : %.3f, %.3f, %.3f (bordure %.3f, %.3f, %.3f)"), Fill.R, Fill.G, Fill.B,
Options.BuildingBorderColor.R, Options.BuildingBorderColor.G, Options.BuildingBorderColor.B));
}
// Reference : materiaux et taille de la description source.
const FMeshDescription* RefDescription = Reference->GetMeshDescription(0);
Line(FString::Printf(TEXT("reference %s : %d materiau(x), description source %d sommets / %d triangles"), *ReferencePath,
Reference->GetStaticMaterials().Num(), RefDescription ? RefDescription->Vertices().Num() : -1, RefDescription ? RefDescription->Triangles().Num() : -1));
for (const FStaticMaterial& Material : Reference->GetStaticMaterials())
{
Line(FString::Printf(TEXT(" materiau de reference : %s (emplacement %s)"), *GetPathNameSafe(Material.MaterialInterface), *Material.MaterialSlotName.ToString()));
}
// 1.3.2 : couleurs de sommet moyennes du maillage de reference, par orientation de la face (toits, murs, sol).
// 1.3.3 (COMMUN-28 S6) : -SampleMesh=<objet> echantillonne un autre maillage que la reference ; octets sRGB, UV0 et normales
// en plus (le plugin ecrit des octets sans conversion ; la description source les relit en lineaire avec conversion sRGB).
const UStaticMesh* SampleMesh = ParamsMap.Contains(TEXT("SampleMesh")) ? LoadObject<UStaticMesh>(nullptr, *ParamsMap[TEXT("SampleMesh")]) : Reference;
const FMeshDescription* SampleDescription = SampleMesh ? SampleMesh->GetMeshDescription(0) : nullptr;
if (Switches.Contains(TEXT("SampleColors")) && SampleDescription)
{
Line(FString::Printf(TEXT("echantillon : %s ; materiau %s"), *SampleMesh->GetPathName(),
SampleMesh->GetStaticMaterials().Num() ? *GetPathNameSafe(SampleMesh->GetStaticMaterials()[0].MaterialInterface) : TEXT("-")));
FStaticMeshConstAttributes Attributes(*SampleDescription);
const TVertexInstanceAttributesConstRef<FVector2f> UVs = Attributes.GetVertexInstanceUVs();
FVector2f UvMin[3] = { FVector2f(1e9f), FVector2f(1e9f), FVector2f(1e9f) }, UvMax[3] = { FVector2f(-1e9f), FVector2f(-1e9f), FVector2f(-1e9f) };
double NormalZ[3] = { 0.0, 0.0, 0.0 };
const TVertexInstanceAttributesConstRef<FVector4f> Colors = Attributes.GetVertexInstanceColors();
const TVertexInstanceAttributesConstRef<FVector3f> Normals = Attributes.GetVertexInstanceNormals();
const TVertexAttributesConstRef<FVector3f> Positions = Attributes.GetVertexPositions();
FVector4f Sum[3] = { FVector4f(0), FVector4f(0), FVector4f(0) };
int64 Num[3] = { 0, 0, 0 };
TMap<uint32, int64> Histogram;
for (const FVertexInstanceID I : SampleDescription->VertexInstances().GetElementIDs())
{
const FVector3f N = Normals[I];
const float Z = Positions[SampleDescription->GetVertexInstanceVertex(I)].Z;
// 0 : toits (face vers le haut, au-dessus du sol) ; 1 : murs ; 2 : sol (routes, parcs, emprises).
const int32 Kind = FMath::Abs(N.Z) < 0.3f ? 1 : (Z > 50.f ? 0 : 2);
Sum[Kind] += Colors[I];
++Num[Kind];
NormalZ[Kind] += N.Z;
if (UVs.GetNumChannels() > 0)
{
const FVector2f Uv = UVs.Get(I, 0);
UvMin[Kind] = FVector2f::Min(UvMin[Kind], Uv);
UvMax[Kind] = FVector2f::Max(UvMax[Kind], Uv);
}
if (Kind != 2)
{
const FColor C = FLinearColor(Colors[I]).ToFColor(false);
++Histogram.FindOrAdd(C.DWColor());
}
}
const TCHAR* Names[3] = { TEXT("toits"), TEXT("murs"), TEXT("sol") };
for (int32 K = 0; K < 3; ++K)
{
const FVector4f Mean = Num[K] ? Sum[K] / float(Num[K]) : FVector4f(0);
const FColor Srgb = FLinearColor(Mean).ToFColor(true);
Line(FString::Printf(TEXT("couleurs de %s : %lld sommets ; moyenne lineaire %.3f, %.3f, %.3f, %.3f (octets sRGB %d, %d, %d) ; normale Z moyenne %.2f ; UV0 de (%.2f, %.2f) a (%.2f, %.2f)"),
Names[K], Num[K], Mean.X, Mean.Y, Mean.Z, Mean.W, Srgb.R, Srgb.G, Srgb.B, Num[K] ? NormalZ[K] / double(Num[K]) : 0.0,
UvMin[K].X, UvMin[K].Y, UvMax[K].X, UvMax[K].Y));
}
Histogram.ValueSort([](int64 A, int64 B) { return A > B; });
int32 Shown = 0;
for (const TPair<uint32, int64>& Pair : Histogram)
{
if (Shown++ >= 5) break;
FColor C;
C.DWColor() = Pair.Key;
Line(FString::Printf(TEXT(" couleur frequente (toits et murs), lineaire quantifiee : %d, %d, %d, %d ; en octets sRGB du FRawMesh : %d, %d, %d : %lld sommets"), C.R, C.G, C.B, C.A,
FLinearColor(C.R / 255.f, C.G / 255.f, C.B / 255.f).ToFColor(true).R, FLinearColor(C.R / 255.f, C.G / 255.f, C.B / 255.f).ToFColor(true).G,
FLinearColor(C.R / 255.f, C.G / 255.f, C.B / 255.f).ToFColor(true).B, Pair.Value));
}
Line(TEXT("SampleColors : aucun asset ecrit"));
return Finish(0);
}
// Preuve des reglages : la carte de reference, reconstruite en memoire, doit redonner les memes triangles.
if (!ComparePath.IsEmpty())
{
UStreetMap* CompareMap = LoadObject<UStreetMap>(nullptr, *ComparePath);
if (!CompareMap)
{
Line(FString::Printf(TEXT("ECHEC : carte de comparaison %s introuvable"), *ComparePath));
return Finish(1);
}
const UStreetMapComponent* CompareComponent = BuildComponent(CompareMap, Options);
const int32 Vertices = CompareComponent->GetRawMeshVertices().Num();
const int32 Triangles = CompareComponent->GetRawMeshIndices().Num() / 3;
const int32 RefTriangles = RefDescription ? RefDescription->Triangles().Num() : -1;
const double GapPercent = RefTriangles > 0 ? 100.0 * FMath::Abs(double(Triangles - RefTriangles)) / RefTriangles : 100.0;
const bool bSame = RefTriangles > 0 && GapPercent <= TolerancePercent;
Line(FString::Printf(TEXT("comparaison : %s reconstruite en memoire = %d sommets / %d triangles ; reference %d triangles ; ecart %.2f %% (tolerance %.2f %%) -> %s"),
*ComparePath, Vertices, Triangles, RefTriangles, GapPercent, TolerancePercent, bSame ? TEXT("CONFORME") : TEXT("DIFFERENT")));
if (!bSame)
{
Line(TEXT("ECHEC : les reglages par defaut ne redonnent pas le maillage de reference ; aucun asset ecrit"));
return Finish(1);
}
}
// Maillage brut de la carte (meme copie que OnCreateStaticMeshAssetClicked).
const double Start = FPlatformTime::Seconds();
const UStreetMapComponent* Component = BuildComponent(Map, Options);
TArray<FStreetMapVertex> RawVertices = Component->GetRawMeshVertices();
TArray<uint32> RawIndices = Component->GetRawMeshIndices();
Line(FString::Printf(TEXT("maillage brut : %d sommets, %d triangles (%.1f s)"), RawVertices.Num(), RawIndices.Num() / 3, FPlatformTime::Seconds() - Start));
if (RawVertices.Num() <= 3 || RawIndices.Num() <= 3)
{
Line(TEXT("ECHEC : maillage vide ; aucun asset ecrit"));
return Finish(1);
}
// 1.4.0 : routes pavees. Le composant ecrit les routes en premier, dans l'ordre de GetRoads(), 4 sommets par segment
// (AddThick2DLine) ; chaque route est controlee (milieu de ses deux premiers sommets = son premier point).
if (!PavedOsmPath.IsEmpty())
{
TArray<FString> OriginParts;
OriginText.ParseIntoArray(OriginParts, TEXT(","));
if (OriginParts.Num() != 2)
{
Line(TEXT("ECHEC : -PavedOsm demande -Origin=<lat>,<lon> ; aucun asset ecrit"));
return Finish(1);
}
LPCityOsm::FReader Osm;
Osm.OriginLat = FCString::Atod(*OriginParts[0]);
Osm.OriginLon = FCString::Atod(*OriginParts[1]);
Osm.bLines = true;
FString Error;
if (!LPCityOsm::Read(Osm, PavedOsmPath, Error))
{
Line(FString::Printf(TEXT("ECHEC : %s ; aucun asset ecrit"), *Error));
return Finish(1);
}
Osm.Nodes.Empty();
TMultiMap<FIntPoint, int32> ByFirst;
int32 PavedWays = 0;
for (int32 L = 0; L < Osm.Lines.Num(); ++L)
{
if (!IsPavedSurface(Osm.LineTags[L].FindRef(TEXT("surface")))) continue;
ByFirst.Add(LPCityOsm::Cell(Osm.Lines[L][0]), L);
++PavedWays;
}
const TArray<FStreetMapRoad>& Roads = Map->GetRoads();
int32 Base = 0, Paved = 0, BadOrder = 0;
double PavedCm = 0.0, TotalCm = 0.0;
for (const FStreetMapRoad& Road : Roads)
{
const int32 Segments = FMath::Max(0, Road.RoadPoints.Num() - 1);
if (Base + Segments * 4 > RawVertices.Num()) { ++BadOrder; break; }
double LengthCm = 0.0;
for (int32 P = 0; P + 1 < Road.RoadPoints.Num(); ++P) LengthCm += FVector2D::Distance(Road.RoadPoints[P], Road.RoadPoints[P + 1]);
TotalCm += LengthCm;
if (Segments > 0)
{
const FVector2f Mid = (FVector2f(RawVertices[Base].Position) + FVector2f(RawVertices[Base + 1].Position)) * 0.5f;
if (FVector2f::Distance(Mid, FVector2f(Road.RoadPoints[0])) > 1.f) { ++BadOrder; }
bool bPaved = false;
const FVector2D First = Road.RoadPoints[0], Last = Road.RoadPoints.Last();
for (int32 DX = -1; DX <= 1 && !bPaved; ++DX)
for (int32 DY = -1; DY <= 1 && !bPaved; ++DY)
for (auto It = ByFirst.CreateConstKeyIterator(LPCityOsm::Cell(First) + FIntPoint(DX, DY)); It && !bPaved; ++It)
{
const TArray<FVector2D>& Pts = Osm.Lines[It.Value()];
bPaved = Pts.Num() == Road.RoadPoints.Num() && FVector2D::Distance(Pts[0], First) <= 50.0 && FVector2D::Distance(Pts.Last(), Last) <= 50.0;
}
if (bPaved)
{
for (int32 V = Base; V < Base + Segments * 4; ++V)
{
RawVertices[V].TextureCoordinate.X += 2.f;
if (bPavedZ) RawVertices[V].Position.Z = PavedZCm;
}
++Paved;
PavedCm += LengthCm;
}
}
Base += Segments * 4;
}
Line(FString::Printf(TEXT("routes pavees (%s) : voies OSM pavees %d ; routes de la carte %d, pavees %d (%.1f km sur %.1f, %.1f %%) ; sommets de routes %d ; ordre non conforme %d ; hauteur des pavees %s"),
*PavedOsmPath, PavedWays, Roads.Num(), Paved, PavedCm / 1e5, TotalCm / 1e5, TotalCm > 0.0 ? 100.0 * PavedCm / TotalCm : 0.0, Base, BadOrder,
bPavedZ ? *FString::Printf(TEXT("%.1f cm"), PavedZCm) : TEXT("celle des routes")));
if (BadOrder > 0)
{
Line(TEXT("ECHEC : les sommets des routes ne suivent pas l'ordre attendu ; aucun asset ecrit"));
return Finish(1);
}
}
// 1.7.5 (STM1-DIAG-20, option B) : -CutBridgeRoads=<liste.json> (avec -Origin) retire du dessin les routes portees par un
// modele de pont. Chaque entree de la liste qui a "road_cut_m": [longueur, largeur, axe] (m, m, degres depuis l'est vers le
// nord, boite centree sur lat / lon) coupe la ligne mediane des segments de route a moins de 30 degres de l'axe, dans la
// boite ; les morceaux hors de la boite restent (segment raccourci, ou scinde en deux). Seul le maillage change : les
// donnees du UStreetMap (voitures, noms de rues) ne sont pas touchees.
const FString CutPath = ParamsMap.FindRef(TEXT("CutBridgeRoads"));
if (!CutPath.IsEmpty())
{
TArray<FString> OriginParts;
OriginText.ParseIntoArray(OriginParts, TEXT(","));
FString JsonText;
TSharedPtr<FJsonObject> Root;
const TArray<TSharedPtr<FJsonValue>>* Entries = nullptr;
if (OriginParts.Num() != 2 || !FFileHelper::LoadFileToString(JsonText, *CutPath)
|| !FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(JsonText), Root) || !Root.IsValid()
|| !Root->TryGetArrayField(TEXT("landmarks"), Entries))
{
Line(FString::Printf(TEXT("ECHEC : -CutBridgeRoads=%s illisible (liste landmarks) ou -Origin absent ; aucun asset ecrit"), *CutPath));
return Finish(1);
}
struct FCutBox { FString Name; FVector2D Center, Dir, Perp; double HalfLen, HalfWid; int32 Cut = 0, Shortened = 0, Split = 0; double RemovedCm = 0.0; };
TArray<FCutBox> Boxes;
for (const TSharedPtr<FJsonValue>& Value : *Entries)
{
const TSharedPtr<FJsonObject> Obj = Value->AsObject();
const TArray<TSharedPtr<FJsonValue>>* Box = nullptr;
if (!Obj.IsValid() || !Obj->TryGetArrayField(TEXT("road_cut_m"), Box) || Box->Num() != 3) continue;
FCutBox B;
B.Name = Obj->GetStringField(TEXT("name"));
B.Center = LPCityOsm::Project(Obj->GetNumberField(TEXT("lat")), Obj->GetNumberField(TEXT("lon")), FCString::Atod(*OriginParts[0]), FCString::Atod(*OriginParts[1]));
const double AxisRad = FMath::DegreesToRadians((*Box)[2]->AsNumber());
B.Dir = FVector2D(FMath::Cos(AxisRad), -FMath::Sin(AxisRad)); // repere de la carte : Y vers le sud
B.Perp = FVector2D(FMath::Sin(AxisRad), FMath::Cos(AxisRad));
B.HalfLen = (*Box)[0]->AsNumber() * 50.0;
B.HalfWid = (*Box)[1]->AsNumber() * 50.0;
Boxes.Add(B);
}
const double MinCos = FMath::Cos(FMath::DegreesToRadians(30.0));
TSet<int32> RemovedQuads;
int32 Base = 0, BadOrder = 0;
for (const FStreetMapRoad& Road : Map->GetRoads())
{
const int32 Segments = FMath::Max(0, Road.RoadPoints.Num() - 1);
if (Base + Segments * 4 > RawVertices.Num()) { ++BadOrder; break; }
if (Segments > 0)
{
const FVector2f Mid = (FVector2f(RawVertices[Base].Position) + FVector2f(RawVertices[Base + 1].Position)) * 0.5f;
if (FVector2f::Distance(Mid, FVector2f(Road.RoadPoints[0])) > 1.f) { ++BadOrder; }
}
for (int32 S = 0; S < Segments; ++S)
{
const int32 Q = Base + S * 4;
const FVector2D A = Road.RoadPoints[S], B2 = Road.RoadPoints[S + 1];
const double Len = FVector2D::Distance(A, B2);
if (Len < 1.0) continue;
const FVector2D U = (B2 - A) / Len;
for (FCutBox& Box : Boxes)
{
if (FMath::Abs(FVector2D::DotProduct(U, Box.Dir)) < MinCos) continue;
// Liang-Barsky : intervalle [T0, T1] de la ligne mediane dans la boite.
double T0 = 0.0, T1 = 1.0;
bool bIn = true;
for (int32 Axis = 0; Axis < 2 && bIn; ++Axis)
{
const FVector2D& N = Axis == 0 ? Box.Dir : Box.Perp;
const double Half = Axis == 0 ? Box.HalfLen : Box.HalfWid;
const double P0 = FVector2D::DotProduct(A - Box.Center, N), D = FVector2D::DotProduct(B2 - A, N);
if (FMath::Abs(D) < 1e-9) { bIn = FMath::Abs(P0) <= Half; continue; }
double Ta = (-Half - P0) / D, Tb = (Half - P0) / D;
if (Ta > Tb) Swap(Ta, Tb);
T0 = FMath::Max(T0, Ta);
T1 = FMath::Min(T1, Tb);
bIn = T1 - T0 > 1.0 / Len;
}
if (!bIn) continue;
// Sommets du quadrilatere (AddThick2DLine) : Q debut gauche, Q+1 debut droite, Q+2 fin droite, Q+3 fin gauche.
auto Lerp = [&RawVertices](int32 From, int32 To, double T)
{
FStreetMapVertex V = RawVertices[From];
V.Position = FMath::Lerp(RawVertices[From].Position, RawVertices[To].Position, float(T));
V.TextureCoordinate.Y = float(T);
return V;
};
Box.RemovedCm += (T1 - T0) * Len;
if (T0 <= 0.0 && T1 >= 1.0)
{
RemovedQuads.Add(Q);
++Box.Cut;
}
else if (T0 <= 0.0)
{
const FStreetMapVertex V0 = Lerp(Q, Q + 3, T1), V1 = Lerp(Q + 1, Q + 2, T1);
RawVertices[Q] = V0;
RawVertices[Q + 1] = V1;
++Box.Shortened;
}
else if (T1 >= 1.0)
{
const FStreetMapVertex V3 = Lerp(Q, Q + 3, T0), V2 = Lerp(Q + 1, Q + 2, T0);
RawVertices[Q + 3] = V3;
RawVertices[Q + 2] = V2;
++Box.Shortened;
}
else
{
// Morceau [T1, 1] : nouveau quadrilatere en fin de tableau ; l'original garde [0, T0].
const uint32 N0 = RawVertices.Num();
const FStreetMapVertex S0 = Lerp(Q, Q + 3, T1), S1 = Lerp(Q + 1, Q + 2, T1), S2 = RawVertices[Q + 2], S3 = RawVertices[Q + 3];
RawVertices.Append({ S0, S1, S2, S3 });
RawIndices.Append({ N0, N0 + 1, N0 + 2, N0, N0 + 2, N0 + 3 });
const FStreetMapVertex V3 = Lerp(Q, Q + 3, T0), V2 = Lerp(Q + 1, Q + 2, T0);
RawVertices[Q + 3] = V3;
RawVertices[Q + 2] = V2;
++Box.Split;
}
break;
}
}
Base += Segments * 4;
}
if (BadOrder > 0)
{
Line(TEXT("ECHEC : -CutBridgeRoads : les sommets des routes ne suivent pas l'ordre attendu ; aucun asset ecrit"));
return Finish(1);
}
const int32 TrianglesBefore = RawIndices.Num() / 3;
TArray<uint32> Kept;
Kept.Reserve(RawIndices.Num());
for (int32 T = 0; T + 2 < RawIndices.Num(); T += 3)
{
const uint32 First = RawIndices[T];
if (First < uint32(Base) && RemovedQuads.Contains(int32(First - First % 4))) continue;
Kept.Append({ RawIndices[T], RawIndices[T + 1], RawIndices[T + 2] });
}
RawIndices = MoveTemp(Kept);
int32 Cut = 0, Shortened = 0, Split = 0, Touched = 0;
double RemovedCm = 0.0;
for (const FCutBox& Box : Boxes)
{
Line(FString::Printf(TEXT(" pont %s : segments retires %d, raccourcis %d, scindes %d ; %.1f m de route retires"), *Box.Name, Box.Cut, Box.Shortened, Box.Split, Box.RemovedCm / 100.0));
Cut += Box.Cut; Shortened += Box.Shortened; Split += Box.Split; RemovedCm += Box.RemovedCm;
Touched += (Box.Cut + Box.Shortened + Box.Split) > 0 ? 1 : 0;
}
Line(FString::Printf(TEXT("routes des ponts (%s) : boites %d, ponts touches %d ; segments retires %d, raccourcis %d, scindes %d ; %.1f m retires ; triangles %d -> %d"),
*CutPath, Boxes.Num(), Touched, Cut, Shortened, Split, RemovedCm / 100.0, TrianglesBefore, RawIndices.Num() / 3));
}
if (bDryRun)
{
Line(TEXT("DryRun : aucun asset ecrit"));
return Finish(0);
}
FRawMesh RawMesh;
for (const FStreetMapVertex& Vertex : RawVertices)
{
RawMesh.VertexPositions.Add(Vertex.Position);
}
for (const uint32 VertexIndex : RawIndices)
{
const FStreetMapVertex& Vertex = RawVertices[VertexIndex];
RawMesh.WedgeIndices.Add(VertexIndex);
RawMesh.WedgeTangentX.Add(Vertex.TangentX);
RawMesh.WedgeTangentY.Add((Vertex.TangentX ^ Vertex.TangentZ).GetSafeNormal());
RawMesh.WedgeTangentZ.Add(Vertex.TangentZ);
RawMesh.WedgeTexCoords[0].Add(Vertex.TextureCoordinate);
RawMesh.WedgeColors.Add(Vertex.Color);
}
for (int32 Tri = 0; Tri < RawIndices.Num() / 3; ++Tri)
{
RawMesh.FaceMaterialIndices.Add(0);
RawMesh.FaceSmoothingMasks.Add(0);
}
UPackage* Package = CreatePackage(*PackageName);
UStaticMesh* StaticMesh = NewObject<UStaticMesh>(Package, *FPackageName::GetLongPackageAssetName(PackageName), RF_Public | RF_Standalone);
StaticMesh->InitResources();
StaticMesh->SetLightingGuid(FGuid::NewGuid());
FStaticMeshSourceModel& SourceModel = StaticMesh->AddSourceModel();
SourceModel.BuildSettings.bRecomputeNormals = false;
SourceModel.BuildSettings.bRecomputeTangents = false;
SourceModel.BuildSettings.bRemoveDegenerates = false;
SourceModel.BuildSettings.bUseHighPrecisionTangentBasis = false;
SourceModel.BuildSettings.bUseFullPrecisionUVs = false;
SourceModel.BuildSettings.bGenerateLightmapUVs = true;
SourceModel.BuildSettings.SrcLightmapIndex = 0;
SourceModel.BuildSettings.DstLightmapIndex = 1;
SourceModel.RawMeshBulkData->SaveRawMesh(RawMesh);
// 1.3.1 (COMMUN-27 S4) : -Material=<objet> remplace le materiau de reference. StreetMapMasked (Londres) masque les
// batiments ; la carte classique en 3D prend StreetMapClassic, celui que le jeu met sur la carte en mode classique.
UMaterialInterface* Override = MaterialPath.IsEmpty() ? nullptr : LoadObject<UMaterialInterface>(nullptr, *MaterialPath);
if (!MaterialPath.IsEmpty() && !Override)
{
Line(FString::Printf(TEXT("ECHEC : materiau %s introuvable ; aucun asset ecrit"), *MaterialPath));
return Finish(1);
}
for (const FStaticMaterial& Material : Reference->GetStaticMaterials())
{
StaticMesh->GetStaticMaterials().Add(FStaticMaterial(Override ? Override : Material.MaterialInterface.Get(), Material.MaterialSlotName));
}
if (Override)
{
Line(FString::Printf(TEXT("materiau impose : %s"), *Override->GetPathName()));
}
StaticMesh->SetImportVersion(EImportStaticMeshVersion::LastVersion);
const double BuildStart = FPlatformTime::Seconds();
StaticMesh->Build(/*bSilent=*/true);
StaticMesh->PostEditChange();
FAssetRegistryModule::AssetCreated(StaticMesh);
Line(FString::Printf(TEXT("Build : %.1f s ; sommets rendus LOD0 %d"), FPlatformTime::Seconds() - BuildStart, StaticMesh->GetNumVertices(0)));
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
const FString FileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension());
if (!UPackage::SavePackage(Package, StaticMesh, *FileName, SaveArgs))
{
Line(FString::Printf(TEXT("ECHEC : sauvegarde de %s"), *FileName));
return Finish(1);
}
Line(FString::Printf(TEXT("asset ecrit : %s (%s), %lld octets"), *PackageName, *FileName, IFileManager::Get().FileSize(*FileName)));
return Finish(0);
}