// Lot STM1 (STM1-SPEC-03, pilote O-B ; STM1-IMPL-02). Voir LPBuildCityWaterCommandlet.h. #include "LPBuildCityWaterCommandlet.h" #include "AssetRegistry/AssetRegistryModule.h" #include "CompGeom/PolygonTriangulation.h" #include "ConstrainedDelaunay2.h" #include "Curve/GeneralPolygon2.h" #include "LPCityWaterMask.h" #include "Engine/StaticMesh.h" #include "Materials/MaterialInterface.h" #include "MeshDescription.h" #include "Misc/FileHelper.h" #include "Misc/PackageName.h" #include "Polygon2.h" #include "StaticMeshAttributes.h" #include "StreetMap.h" #include "UObject/Package.h" #include "UObject/SavePackage.h" #include "XmlFile.h" #include "MaterialEditingLibrary.h" #include "Materials/Material.h" #include "Materials/MaterialExpressionConstant.h" #include "Materials/MaterialExpressionConstant3Vector.h" DEFINE_LOG_CATEGORY_STATIC(LogLPBuildCityWater, Log, All); using namespace UE::Geometry; namespace LPCityWater { // Meme constante que le plugin (StreetMapFactory.cpp:8-9), en centimetres par degre. constexpr double CmPerDegree = 40075036.0 / 360.0 * 100.0; /** Point OSM : X = longitude, Y = latitude. */ using FLonLat = FVector2d; struct FOsmWay { int64 Id = 0; TArray Points; TMap Tags; // STM1-B2 (R1) : centre de . Une sortie Overpass "out tags geom" ne donne que la boite, pas les noeuds. FLonLat BoundsCenter = FLonLat::ZeroVector; bool bHasBounds = false; }; struct FOsmMember { FString Role; TArray Points; }; struct FOsmRelation { int64 Id = 0; TArray Members; TMap Tags; }; /** Points d'un noeud XML (voie ou membre). */ void ReadPoints(const FXmlNode* Node, TArray& Out) { for (const FXmlNode* Child : Node->GetChildrenNodes()) { if (Child->GetTag() == TEXT("nd")) { const FString Lat = Child->GetAttribute(TEXT("lat")); const FString Lon = Child->GetAttribute(TEXT("lon")); if (!Lat.IsEmpty() && !Lon.IsEmpty()) { Out.Add(FLonLat(FCString::Atod(*Lon), FCString::Atod(*Lat))); } } } } void ReadTags(const FXmlNode* Node, TMap& Out) { for (const FXmlNode* Child : Node->GetChildrenNodes()) { if (Child->GetTag() == TEXT("tag")) { Out.Add(Child->GetAttribute(TEXT("k")), Child->GetAttribute(TEXT("v"))); } } } /** Lit une reponse Overpass "out geom" (voies et relations avec geometrie en ligne). Echec : false et message. */ bool ReadOverpass(const FString& Path, TArray& Ways, TArray& Relations, FString& Error) { FXmlFile File(Path); if (!File.IsValid()) { Error = FString::Printf(TEXT("XML illisible : %s (%s)"), *Path, *File.GetLastError()); return false; } const FXmlNode* Root = File.GetRootNode(); for (const FXmlNode* Node : Root->GetChildrenNodes()) { if (Node->GetTag() == TEXT("remark")) { Error = FString::Printf(TEXT("reponse Overpass en erreur : %s"), *Node->GetContent()); return false; } if (Node->GetTag() == TEXT("way")) { FOsmWay& Way = Ways.AddDefaulted_GetRef(); Way.Id = FCString::Atoi64(*Node->GetAttribute(TEXT("id"))); ReadPoints(Node, Way.Points); ReadTags(Node, Way.Tags); if (const FXmlNode* Bounds = Node->FindChildNode(TEXT("bounds"))) { const double MinLat = FCString::Atod(*Bounds->GetAttribute(TEXT("minlat"))); const double MinLon = FCString::Atod(*Bounds->GetAttribute(TEXT("minlon"))); const double MaxLat = FCString::Atod(*Bounds->GetAttribute(TEXT("maxlat"))); const double MaxLon = FCString::Atod(*Bounds->GetAttribute(TEXT("maxlon"))); Way.BoundsCenter = FLonLat(0.5 * (MinLon + MaxLon), 0.5 * (MinLat + MaxLat)); Way.bHasBounds = true; } } else if (Node->GetTag() == TEXT("relation")) { FOsmRelation& Rel = Relations.AddDefaulted_GetRef(); Rel.Id = FCString::Atoi64(*Node->GetAttribute(TEXT("id"))); ReadTags(Node, Rel.Tags); for (const FXmlNode* Child : Node->GetChildrenNodes()) { if (Child->GetTag() == TEXT("member") && Child->GetAttribute(TEXT("type")) == TEXT("way")) { FOsmMember& Member = Rel.Members.AddDefaulted_GetRef(); Member.Role = Child->GetAttribute(TEXT("role")); ReadPoints(Child, Member.Points); } } } } return true; } /** Centroide par moyenne des sommets distincts (le point de fermeture repete n'est pas compte), comme l'importeur. */ template VecType Centroid(const TArray& Points) { int32 Count = Points.Num(); if (Count > 1 && Points[0].Equals(Points[Count - 1], 1e-9)) { --Count; } VecType Sum(0, 0); for (int32 Index = 0; Index < Count; ++Index) { Sum += Points[Index]; } return Count > 0 ? Sum / double(Count) : Sum; } /** Projection du plugin (Sanson-Flamsteed), relative a l'origine, en cm. */ FVector2d Project(const FLonLat& P, const FLonLat& Origin) { return FVector2d( (P.X - Origin.X) * FMath::Cos(FMath::DegreesToRadians(P.Y)) * CmPerDegree, -(P.Y - Origin.Y) * CmPerDegree); } double Median(TArray Values) { if (Values.IsEmpty()) { return 0.0; } Values.Sort(); const int32 Mid = Values.Num() / 2; return (Values.Num() % 2) ? Values[Mid] : 0.5 * (Values[Mid - 1] + Values[Mid]); } /** Chaine des segments ouverts en anneaux fermes (extremites communes). Anneaux non fermables : comptes dans Broken. */ void ChainRings(TArray> Segments, TArray>& Rings, int32& Broken) { constexpr double Tol = 1e-7; while (Segments.Num() > 0) { TArray Ring = Segments.Pop(); bool bGrew = true; while (!Ring[0].Equals(Ring.Last(), Tol) && bGrew) { bGrew = false; for (int32 Index = 0; Index < Segments.Num(); ++Index) { TArray& Seg = Segments[Index]; if (Seg[0].Equals(Ring.Last(), Tol)) { Ring.Append(&Seg[1], Seg.Num() - 1); } else if (Seg.Last().Equals(Ring.Last(), Tol)) { for (int32 P = Seg.Num() - 2; P >= 0; --P) { Ring.Add(Seg[P]); } } else { continue; } Segments.RemoveAtSwap(Index); bGrew = true; break; } } if (Ring.Num() >= 4 && Ring[0].Equals(Ring.Last(), Tol)) { Rings.Add(MoveTemp(Ring)); } else { ++Broken; } } } /** Sutherland-Hodgman contre un rectangle (repere de la carte, cm). */ TArray ClipToBox(const TArray& In, const FVector2d& Min, const FVector2d& Max) { TArray Out = In; auto ClipEdge = [&Out](TFunctionRef Inside, TFunctionRef Cut) { TArray Src = MoveTemp(Out); Out.Reset(); for (int32 Index = 0; Index < Src.Num(); ++Index) { const FVector2d& A = Src[Index]; const FVector2d& B = Src[(Index + 1) % Src.Num()]; const bool bA = Inside(A); const bool bB = Inside(B); if (bA) { Out.Add(A); } if (bA != bB) { Out.Add(Cut(A, B)); } } }; auto LerpX = [](const FVector2d& A, const FVector2d& B, double X) { const double T = (X - A.X) / (B.X - A.X); return FVector2d(X, A.Y + T * (B.Y - A.Y)); }; auto LerpY = [](const FVector2d& A, const FVector2d& B, double Y) { const double T = (Y - A.Y) / (B.Y - A.Y); return FVector2d(A.X + T * (B.X - A.X), Y); }; ClipEdge([&](const FVector2d& P) { return P.X >= Min.X; }, [&](const FVector2d& A, const FVector2d& B) { return LerpX(A, B, Min.X); }); if (Out.Num() >= 3) ClipEdge([&](const FVector2d& P) { return P.X <= Max.X; }, [&](const FVector2d& A, const FVector2d& B) { return LerpX(A, B, Max.X); }); if (Out.Num() >= 3) ClipEdge([&](const FVector2d& P) { return P.Y >= Min.Y; }, [&](const FVector2d& A, const FVector2d& B) { return LerpY(A, B, Min.Y); }); if (Out.Num() >= 3) ClipEdge([&](const FVector2d& P) { return P.Y <= Max.Y; }, [&](const FVector2d& A, const FVector2d& B) { return LerpY(A, B, Max.Y); }); return Out.Num() >= 3 ? Out : TArray(); } } ULPBuildCityWaterCommandlet::ULPBuildCityWaterCommandlet() { IsClient = false; IsServer = false; IsEditor = true; LogToConsole = true; } int32 ULPBuildCityWaterCommandlet::Main(const FString& Params) { using namespace LPCityWater; TArray Tokens; TArray Switches; TMap ParamsMap; ParseCommandLine(*Params, Tokens, Switches, ParamsMap); const FString City = ParamsMap.FindRef(TEXT("City")); const FString WaterPath = ParamsMap.FindRef(TEXT("Water")); const FString BuildingsPath = ParamsMap.FindRef(TEXT("Buildings")); const FString ReportPath = ParamsMap.FindRef(TEXT("Report")); const bool bDryRun = Switches.Contains(TEXT("DryRun")); const int32 MinPairs = ParamsMap.Contains(TEXT("MinPairs")) ? FCString::Atoi(*ParamsMap[TEXT("MinPairs")]) : 500; const double MaxMedianCm = ParamsMap.Contains(TEXT("MaxMedianCm")) ? FCString::Atod(*ParamsMap[TEXT("MaxMedianCm")]) : 200.0; TArray Report; auto Line = [&Report](const FString& Text) { UE_LOG(LogLPBuildCityWater, 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 (City.IsEmpty() || WaterPath.IsEmpty() || BuildingsPath.IsEmpty()) { Line(TEXT("usage : -City= -Water= -Buildings= [-DryRun] [-Report=] [-ListBuildingsInWater=] [-ThresholdM2=] [-MaskOnly]")); return Finish(2); } Line(FString::Printf(TEXT("# LPBuildCityWater %s (%s)"), *City, bDryRun ? TEXT("DryRun") : TEXT("ecriture"))); // 1. Carte et maillage de reference. const UStreetMap* StreetMap = LoadObject(nullptr, *FString::Printf(TEXT("/Game/Heavy/StreetMap/%s.%s"), *City, *City)); const UStaticMesh* CityMesh = LoadObject(nullptr, *FString::Printf(TEXT("/Game/Heavy/StreetMap/Static/%s.%s"), *City, *City)); if (!StreetMap || !CityMesh) { Line(TEXT("ECHEC : UStreetMap ou maillage Static introuvable")); return Finish(1); } const FVector2d MapMin = StreetMap->GetBoundsMin(); const FVector2d MapMax = StreetMap->GetBoundsMax(); const FBox MeshBox = CityMesh->GetBoundingBox(); Line(FString::Printf(TEXT("bornes UStreetMap (cm) : (%.0f, %.0f) a (%.0f, %.0f)"), MapMin.X, MapMin.Y, MapMax.X, MapMax.Y)); Line(FString::Printf(TEXT("bornes maillage (cm) : (%.0f, %.0f) a (%.0f, %.0f)"), MeshBox.Min.X, MeshBox.Min.Y, MeshBox.Max.X, MeshBox.Max.Y)); const double BoundsGap = FMath::Max( FMath::Max(FMath::Abs(MeshBox.Min.X - MapMin.X), FMath::Abs(MeshBox.Min.Y - MapMin.Y)), FMath::Max(FMath::Abs(MeshBox.Max.X - MapMax.X), FMath::Abs(MeshBox.Max.Y - MapMax.Y))); Line(FString::Printf(TEXT("ecart des bornes maillage / UStreetMap : %.0f cm (critere <= 100 ; indicatif : les bornes UStreetMap ne couvrent que les objets importes)"), BoundsGap)); // 2. Extraits OSM. TArray WaterWays, BuildingWays; TArray WaterRelations, UnusedRelations; FString Error; if (!ReadOverpass(WaterPath, WaterWays, WaterRelations, Error) || !ReadOverpass(BuildingsPath, BuildingWays, UnusedRelations, Error)) { Line(FString::Printf(TEXT("ECHEC : %s"), *Error)); return Finish(1); } Line(FString::Printf(TEXT("extraits : eau %d voies, %d relations ; batiments %d voies"), WaterWays.Num(), WaterRelations.Num(), BuildingWays.Num())); // 3. Recalage : batiments nommes, noms uniques des deux cotes. // STM1-B2 (R1) : l'extrait Overpass des batiments ne donne que (pas de noeuds). On compare donc des centres de // boite des deux cotes (meme definition) : moins precis qu'un centroide, biais possible sur les grands batiments // irreguliers ; la mediane et la MAD des residus le mesurent. auto BoxCenter = [](const TArray& Points) { FVector2d Min(TNumericLimits::Max(), TNumericLimits::Max()); FVector2d Max(TNumericLimits::Lowest(), TNumericLimits::Lowest()); for (const FVector2d& P : Points) { Min = FVector2d(FMath::Min(Min.X, P.X), FMath::Min(Min.Y, P.Y)); Max = FVector2d(FMath::Max(Max.X, P.X), FMath::Max(Max.Y, P.Y)); } return 0.5 * (Min + Max); }; TMap AssetCount, OsmCount; TMap AssetCentroid; TMap OsmCentroid; for (const FStreetMapBuilding& Building : StreetMap->GetBuildings()) { if (Building.BuildingName.IsEmpty() || Building.BuildingPoints.Num() < 3) continue; const FString Key = Building.BuildingName.ToLower(); AssetCount.FindOrAdd(Key)++; AssetCentroid.Add(Key, BoxCenter(Building.BuildingPoints)); } int32 OsmFromBounds = 0, OsmFromNodes = 0; for (const FOsmWay& Way : BuildingWays) { const FString* Name = Way.Tags.Find(TEXT("name")); if (!Name || (!Way.bHasBounds && Way.Points.Num() < 3)) continue; const FString Key = Name->ToLower(); OsmCount.FindOrAdd(Key)++; if (Way.Points.Num() >= 3) { OsmCentroid.Add(Key, BoxCenter(Way.Points)); ++OsmFromNodes; } else { OsmCentroid.Add(Key, Way.BoundsCenter); ++OsmFromBounds; } } Line(FString::Printf(TEXT("batiments nommes : asset %d noms, OSM %d noms (centre de : %d, centre de boite des noeuds : %d) ; methode : centres de boite des deux cotes"), AssetCount.Num(), OsmCount.Num(), OsmFromBounds, OsmFromNodes)); TArray> Pairs; for (const TPair& Entry : AssetCount) { if (Entry.Value == 1 && OsmCount.FindRef(Entry.Key) == 1) { Pairs.Add({ AssetCentroid[Entry.Key], OsmCentroid[Entry.Key] }); } } // STM1-B2 (S1, decision de Big Boss) : deux passes. Passe 1 : origine = mediane sur toutes les paires. Passe 2 : rejet des // paires au-dela de mediane + 3 x MAD, origine recalculee sur les paires retenues. Criteres d'acceptation non circulaires : // au moins MinPairs paires retenues, residu median (paires retenues) <= MaxMedianCm, taux de paires retenues >= 70 %. // Pour information : part sous 5 m sur TOUTES les paires avec l'origine corrigee, et deplacement de l'origine (cm). auto EstimateOrigin = [](const TArray>& InPairs) { TArray RelLats, RelLons; for (const TPair& Pair : InPairs) { const FVector2d& XY = Pair.Key; const FLonLat& LL = Pair.Value; RelLats.Add(LL.Y + XY.Y / CmPerDegree); RelLons.Add(LL.X - XY.X / (CmPerDegree * FMath::Cos(FMath::DegreesToRadians(LL.Y)))); } return FLonLat(Median(RelLons), Median(RelLats)); }; auto ResidualsOf = [](const TArray>& InPairs, const FLonLat& InOrigin) { TArray Out; for (const TPair& Pair : InPairs) { Out.Add(FVector2d::Distance(Project(Pair.Value, InOrigin), Pair.Key)); } return Out; }; auto MadOf = [](const TArray& Values, double Center) { TArray Deviations; for (const double V : Values) { Deviations.Add(FMath::Abs(V - Center)); } return Median(Deviations); }; auto ShareUnder = [](const TArray& Values, double Limit) { int32 Count = 0; for (const double V : Values) { Count += V <= Limit ? 1 : 0; } return Values.Num() ? double(Count) / Values.Num() : 0.0; }; // Passe 1. const FLonLat Origin1 = EstimateOrigin(Pairs); const TArray Residuals1 = ResidualsOf(Pairs, Origin1); const double Median1 = Median(Residuals1); const double Mad1 = MadOf(Residuals1, Median1); Line(FString::Printf(TEXT("passe 1 : %d paires ; origine lat %.7f lon %.7f ; residu median %.0f cm ; MAD %.0f cm ; %.1f %% sous 5 m"), Pairs.Num(), Origin1.Y, Origin1.X, Median1, Mad1, 100.0 * ShareUnder(Residuals1, 500.0))); // Passe 2 : rejet a mediane + 3 x MAD. const double RejectCm = Median1 + 3.0 * Mad1; TArray> Kept; for (int32 Index = 0; Index < Pairs.Num(); ++Index) { if (Residuals1[Index] <= RejectCm) { Kept.Add(Pairs[Index]); } } const FLonLat Origin = Kept.Num() ? EstimateOrigin(Kept) : Origin1; const TArray ResidualsKept = ResidualsOf(Kept, Origin); const double MedianKept = Median(ResidualsKept); const double KeptRate = Pairs.Num() ? double(Kept.Num()) / Pairs.Num() : 0.0; const double ShareAllUnder5m = ShareUnder(ResidualsOf(Pairs, Origin), 500.0); const double OriginShiftCm = Project(Origin1, Origin).Length(); // Preuve que la passe 2 recalcule l'origine (demande de Big Boss) : valeurs en pleine precision et decalage en mm. Line(FString::Printf(TEXT("origines pleine precision : passe 1 lat %.12f lon %.12f ; passe 2 lat %.12f lon %.12f ; ecart lat %.3e deg, lon %.3e deg ; decalage %.3f mm ; passe 2 calculee sur %d paires (passe 1 : %d)"), Origin1.Y, Origin1.X, Origin.Y, Origin.X, Origin.Y - Origin1.Y, Origin.X - Origin1.X, OriginShiftCm * 10.0, Kept.Num(), Pairs.Num())); Line(FString::Printf(TEXT("passe 2 : seuil de rejet %.0f cm (mediane + 3 x MAD) ; %d paires retenues, %d ecartees (taux retenu %.1f %%) ; origine lat %.7f lon %.7f ; residu median (retenues) %.0f cm"), RejectCm, Kept.Num(), Pairs.Num() - Kept.Num(), 100.0 * KeptRate, Origin.Y, Origin.X, MedianKept)); Line(FString::Printf(TEXT("information : %.1f %% sous 5 m sur toutes les paires avec l'origine corrigee ; deplacement de l'origine passe 1 -> passe 2 : %.0f cm"), 100.0 * ShareAllUnder5m, OriginShiftCm)); Line(TEXT("note : centres de boite moins precis qu'un centroide (biais possible sur les grands batiments) ; le juge final est l'essai visuel (eau sur les quais)")); if (Kept.Num() < MinPairs || MedianKept > MaxMedianCm || KeptRate < 0.70) { Line(FString::Printf(TEXT("ECHEC : criteres de recalage non atteints (paires retenues %d >= %d ; residu median %.0f <= %.0f cm ; taux retenu %.1f >= 70 %%) ; aucun asset ecrit"), Kept.Num(), MinPairs, MedianKept, MaxMedianCm, 100.0 * KeptRate)); return Finish(1); } Line(TEXT("recalage accepte (criteres : paires retenues, residu median, taux retenu)")); // 4. Polygones d'eau (anneaux en lon/lat), puis projection et decoupe a la boite de la carte + 50 m. TArray> OuterLL, InnerLL; int32 Broken = 0; for (const FOsmWay& Way : WaterWays) { if (Way.Points.Num() >= 4 && Way.Points[0].Equals(Way.Points.Last(), 1e-7)) { OuterLL.Add(Way.Points); } } for (const FOsmRelation& Rel : WaterRelations) { TArray> OuterSegs, InnerSegs; for (const FOsmMember& Member : Rel.Members) { if (Member.Points.Num() < 2) continue; (Member.Role == TEXT("inner") ? InnerSegs : OuterSegs).Add(Member.Points); } ChainRings(MoveTemp(OuterSegs), OuterLL, Broken); ChainRings(MoveTemp(InnerSegs), InnerLL, Broken); } const FVector2d ClipMin = MapMin - FVector2d(5000.0, 5000.0); const FVector2d ClipMax = MapMax + FVector2d(5000.0, 5000.0); auto ToMap = [&](const TArray& Ring) { TArray P; for (int32 Index = 0; Index + 1 < Ring.Num(); ++Index) // sans le point de fermeture { P.Add(Project(Ring[Index], Origin)); } return ClipToBox(P, ClipMin, ClipMax); }; TArray> Outers, Inners; for (const TArray& Ring : OuterLL) { TArray P = ToMap(Ring); if (P.Num() >= 3) Outers.Emplace(P); } for (const TArray& Ring : InnerLL) { TArray P = ToMap(Ring); if (P.Num() >= 3) Inners.Emplace(P); } Line(FString::Printf(TEXT("polygones dans la carte : %d exterieurs, %d trous ; anneaux non fermes ignores : %d"), Outers.Num(), Inners.Num(), Broken)); // 5. Triangulation (exterieur anti-horaire, trous horaires, trou rattache a l'exterieur qui contient son premier sommet). TArray Vertices; TArray Triangles; double AreaCm2 = 0.0; TArray> WaterPolygons; for (TPolygon2& Outer : Outers) { if (Outer.IsClockwise()) Outer.Reverse(); TGeneralPolygon2 General(Outer); for (TPolygon2 Hole : Inners) { if (Outer.Contains(Hole.GetVertices()[0])) { if (!Hole.IsClockwise()) Hole.Reverse(); General.AddHole(Hole, false, false); } } const TArray Tris = ConstrainedDelaunayTriangulate(General); const int32 Base = Vertices.Num(); for (const FVector2d& V : General.GetOuter().GetVertices()) Vertices.Emplace(float(V.X), float(V.Y), 0.f); for (const TPolygon2& H : General.GetHoles()) for (const FVector2d& V : H.GetVertices()) Vertices.Emplace(float(V.X), float(V.Y), 0.f); for (const FIndex3i& T : Tris) Triangles.Emplace(Base + T.A, Base + T.B, Base + T.C); AreaCm2 += FMath::Abs(General.SignedArea()); WaterPolygons.Add(MoveTemp(General)); } Line(FString::Printf(TEXT("maillage : %d sommets, %d triangles, aire %.3f km2"), Vertices.Num(), Triangles.Num(), AreaCm2 / 1e10)); if (Triangles.IsEmpty()) { Line(TEXT("ECHEC : aucun triangle")); return Finish(1); } // STM1-SPEC-04 (O3) : batiments de la carte dont le centre tombe dans l'eau. Centre et aire comme MapMarkers // (GI.FindCenterOfPolygon / calculateArea : boite englobante en float, aire = dX x dY / 1000) ; aire du polygone en m2 en plus. const FString ListPath = ParamsMap.FindRef(TEXT("ListBuildingsInWater")); const bool bMaskOnly = Switches.Contains(TEXT("MaskOnly")); if (!ListPath.IsEmpty() || bMaskOnly) { const double ThresholdM2 = ParamsMap.Contains(TEXT("ThresholdM2")) ? FCString::Atod(*ParamsMap[TEXT("ThresholdM2")]) : 1500.0; struct FInWater { FString Name; FString Amenity; double PolyM2; double BoxM2; double GameArea; FVector2d Center; int32 Index; }; TArray InWater; const TArray& Buildings = StreetMap->GetBuildings(); for (int32 Index = 0; Index < Buildings.Num(); ++Index) { const FStreetMapBuilding& Building = Buildings[Index]; if (Building.BuildingPoints.Num() < 3) continue; float MinX = float(Building.BuildingPoints[0].X), MaxX = MinX, MinY = float(Building.BuildingPoints[0].Y), MaxY = MinY; double Shoelace = 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()]; MinX = FMath::Min(MinX, float(A.X)); MaxX = FMath::Max(MaxX, float(A.X)); MinY = FMath::Min(MinY, float(A.Y)); MaxY = FMath::Max(MaxY, float(A.Y)); Shoelace += A.X * B.Y - B.X * A.Y; } const FVector2d Center((double(MinX) + double(MaxX)) / 2.0, (double(MinY) + double(MaxY)) / 2.0); bool bInside = false; for (const TGeneralPolygon2& Water : WaterPolygons) { if (Water.Bounds().Contains(Center) && Water.Contains(Center)) { bInside = true; break; } } if (!bInside) continue; const double BoxCm2 = (double(MaxX) - double(MinX)) * (double(MaxY) - double(MinY)); InWater.Add({ Building.BuildingName, Building.Amenity, FMath::Abs(Shoelace) / 2.0 / 1e4, BoxCm2 / 1e4, BoxCm2 / 1000.0, Center, Index }); } InWater.Sort([](const FInWater& A, const FInWater& B) { return A.PolyM2 > B.PolyM2; }); int32 Above = 0, Named = 0; for (const FInWater& B : InWater) { Above += B.PolyM2 >= ThresholdM2 ? 1 : 0; Named += B.Name.IsEmpty() ? 0 : 1; } TArray List; List.Add(FString::Printf(TEXT("# Batiments de %s dont le centre tombe dans l'eau (seuil %.0f m2)"), *City, ThresholdM2)); List.Add(TEXT("")); List.Add(FString::Printf(TEXT("total %d sur %d batiments ; nommes %d ; au-dessus du seuil (aire du polygone) %d ; en dessous %d"), InWater.Num(), Buildings.Num(), Named, Above, InWater.Num() - Above)); List.Add(TEXT("")); List.Add(TEXT("| # | indice | nom | amenite | aire polygone (m2) | aire boite (m2) | aire du jeu (dX*dY/1000) | echelle XY du jeu | centre X (cm) | centre Y (cm) | seuil |")); List.Add(TEXT("|---|---|---|---|---|---|---|---|---|---|---|")); for (int32 Row = 0; Row < InWater.Num(); ++Row) { const FInWater& B = InWater[Row]; const double Scale = FMath::GetMappedRangeValueClamped(FVector2d(0.0, 100000.0), FVector2d(2.0, 20.0), B.GameArea); List.Add(FString::Printf(TEXT("| %d | %d | %s | %s | %.0f | %.0f | %.0f | %.2f | %.0f | %.0f | %s |"), Row + 1, B.Index, B.Name.IsEmpty() ? TEXT("-") : *B.Name.Replace(TEXT("|"), TEXT("/")), B.Amenity.IsEmpty() ? TEXT("-") : *B.Amenity, B.PolyM2, B.BoxM2, B.GameArea, Scale, B.Center.X, B.Center.Y, B.PolyM2 >= ThresholdM2 ? TEXT("epargne") : TEXT("masque"))); } if (!ListPath.IsEmpty()) { FFileHelper::SaveStringArrayToFile(List, *ListPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM); } Line(FString::Printf(TEXT("O3 : %d batiments dans l'eau (au-dessus de %.0f m2 : %d) ; liste %s"), InWater.Num(), ThresholdM2, Above, ListPath.IsEmpty() ? TEXT("(non ecrite)") : *ListPath)); // STM1-SPEC-08 (O2) : masque /Game/Heavy/StreetMap/Water/_Mask, sans relire ni reecrire le maillage d'eau. if (bMaskOnly) { const FString MaskPackageName = FString::Printf(TEXT("/Game/Heavy/StreetMap/Water/%s_Mask"), *City); if (bDryRun) { Line(FString::Printf(TEXT("DryRun : masque %s non ecrit (%d centres)"), *MaskPackageName, InWater.Num())); return Finish(0); } if (FPackageName::DoesPackageExist(MaskPackageName)) { Line(FString::Printf(TEXT("ECHEC : %s existe deja ; aucun asset ecrit"), *MaskPackageName)); return Finish(1); } UPackage* MaskPackage = CreatePackage(*MaskPackageName); ULPCityWaterMask* Mask = NewObject(MaskPackage, *FPackageName::GetLongPackageAssetName(MaskPackageName), RF_Public | RF_Standalone); Mask->City = City; Mask->SourceBuildingCount = Buildings.Num(); for (const FInWater& B : InWater) { Mask->Centers.Add(B.Center); Mask->AreasM2.Add(float(B.PolyM2)); } FAssetRegistryModule::AssetCreated(Mask); FSavePackageArgs MaskSaveArgs; MaskSaveArgs.TopLevelFlags = RF_Public | RF_Standalone; const FString MaskFile = FPackageName::LongPackageNameToFilename(MaskPackageName, FPackageName::GetAssetPackageExtension()); if (!UPackage::SavePackage(MaskPackage, Mask, *MaskFile, MaskSaveArgs)) { Line(FString::Printf(TEXT("ECHEC : sauvegarde de %s"), *MaskFile)); return Finish(1); } Line(FString::Printf(TEXT("asset ecrit : %s (%s), %d centres sur %d batiments ; maillage d'eau non touche"), *MaskPackageName, *MaskFile, InWater.Num(), Buildings.Num())); return Finish(0); } } if (bDryRun) { Line(TEXT("DryRun : aucun asset ecrit")); return Finish(0); } // 6. Asset /Game/Heavy/StreetMap/Water/ (plat, Z = 0, une section, M_LP_Water, sans collision). // Garde : ne jamais ecraser un maillage d'eau existant. const FString PackageName = FString::Printf(TEXT("/Game/Heavy/StreetMap/Water/%s"), *City); if (FPackageName::DoesPackageExist(PackageName)) { Line(FString::Printf(TEXT("ECHEC : %s existe deja ; aucun asset ecrit"), *PackageName)); return Finish(1); } const TCHAR* MaterialPackageName = TEXT("/Game/Heavy/StreetMap/Water/M_LP_Water"); UMaterialInterface* WaterMaterial = LoadObject(nullptr, TEXT("/Game/Heavy/StreetMap/Water/M_LP_Water.M_LP_Water")); if (!WaterMaterial && Switches.Contains(TEXT("CreateMaterial"))) { // STM1-SPEC-03 § 2.5 : opaque, deux faces, DefaultLit, couleur (0,03 ; 0,18 ; 0,32), rugosite 0,15, speculaire 0,5. UPackage* MaterialPackage = CreatePackage(MaterialPackageName); UMaterial* NewMaterial = NewObject(MaterialPackage, TEXT("M_LP_Water"), RF_Public | RF_Standalone); NewMaterial->TwoSided = true; UMaterialExpressionConstant3Vector* Color = Cast( UMaterialEditingLibrary::CreateMaterialExpression(NewMaterial, UMaterialExpressionConstant3Vector::StaticClass(), -300, 0)); UMaterialExpressionConstant* Roughness = Cast( UMaterialEditingLibrary::CreateMaterialExpression(NewMaterial, UMaterialExpressionConstant::StaticClass(), -300, 150)); UMaterialExpressionConstant* Specular = Cast( UMaterialEditingLibrary::CreateMaterialExpression(NewMaterial, UMaterialExpressionConstant::StaticClass(), -300, 250)); if (!Color || !Roughness || !Specular) { Line(TEXT("ECHEC : creation des expressions du materiau ; aucun asset ecrit")); return Finish(1); } Color->Constant = FLinearColor(0.03f, 0.18f, 0.32f); Roughness->R = 0.15f; Specular->R = 0.5f; const bool bLinked = UMaterialEditingLibrary::ConnectMaterialProperty(Color, TEXT(""), MP_BaseColor) && UMaterialEditingLibrary::ConnectMaterialProperty(Roughness, TEXT(""), MP_Roughness) && UMaterialEditingLibrary::ConnectMaterialProperty(Specular, TEXT(""), MP_Specular); UMaterialEditingLibrary::RecompileMaterial(NewMaterial); FAssetRegistryModule::AssetCreated(NewMaterial); FSavePackageArgs MaterialSaveArgs; MaterialSaveArgs.TopLevelFlags = RF_Public | RF_Standalone; const FString MaterialFile = FPackageName::LongPackageNameToFilename(MaterialPackageName, FPackageName::GetAssetPackageExtension()); if (!bLinked || !UPackage::SavePackage(MaterialPackage, NewMaterial, *MaterialFile, MaterialSaveArgs)) { Line(FString::Printf(TEXT("ECHEC : materiau (liaisons %d) ou sauvegarde de %s"), bLinked ? 1 : 0, *MaterialFile)); return Finish(1); } Line(FString::Printf(TEXT("materiau cree : %s (%s), deux faces"), MaterialPackageName, *MaterialFile)); WaterMaterial = NewMaterial; } if (!WaterMaterial) { Line(TEXT("ECHEC : materiau /Game/Heavy/StreetMap/Water/M_LP_Water absent (option -CreateMaterial, STM1-SPEC-03 § 2.5)")); return Finish(1); } FMeshDescription Mesh; FStaticMeshAttributes Attributes(Mesh); Attributes.Register(); const FPolygonGroupID Group = Mesh.CreatePolygonGroup(); Attributes.GetPolygonGroupMaterialSlotNames()[Group] = TEXT("Water"); TArray VertexIds; VertexIds.Reserve(Vertices.Num()); for (const FVector3f& V : Vertices) { const FVertexID Id = Mesh.CreateVertex(); Attributes.GetVertexPositions()[Id] = V; VertexIds.Add(Id); } for (const FIntVector3& T : Triangles) { TArray Corners; for (const int32 Index : { T.X, T.Y, T.Z }) { const FVertexInstanceID Instance = Mesh.CreateVertexInstance(VertexIds[Index]); Attributes.GetVertexInstanceNormals()[Instance] = FVector3f::UpVector; Attributes.GetVertexInstanceUVs().Set(Instance, 0, FVector2f(Vertices[Index].X / 100.f, Vertices[Index].Y / 100.f)); Corners.Add(Instance); } Mesh.CreatePolygon(Group, Corners); } UPackage* Package = CreatePackage(*PackageName); UStaticMesh* WaterMesh = NewObject(Package, *City, RF_Public | RF_Standalone); WaterMesh->GetStaticMaterials().Add(FStaticMaterial(WaterMaterial, TEXT("Water"))); UStaticMesh::FBuildMeshDescriptionsParams BuildParams; BuildParams.bBuildSimpleCollision = false; BuildParams.bCommitMeshDescription = true; BuildParams.bMarkPackageDirty = true; WaterMesh->BuildFromMeshDescriptions({ &Mesh }, BuildParams); FAssetRegistryModule::AssetCreated(WaterMesh); FSavePackageArgs SaveArgs; SaveArgs.TopLevelFlags = RF_Public | RF_Standalone; const FString FileName = FPackageName::LongPackageNameToFilename(PackageName, FPackageName::GetAssetPackageExtension()); if (!UPackage::SavePackage(Package, WaterMesh, *FileName, SaveArgs)) { Line(FString::Printf(TEXT("ECHEC : sauvegarde de %s"), *FileName)); return Finish(1); } Line(FString::Printf(TEXT("asset ecrit : %s (%s)"), *PackageName, *FileName)); return Finish(0); }