// 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 "LPCityOsm.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; // STM1-SPEC-09 (B6) : carte d'un pack (racine /lpm_city_/StreetMap) et origine connue a l'import (moyenne des noeuds // du .osm, rapport de LPClipCityOSM) : pas de recalage par les batiments nommes. L'eau est ecrite sous /Water. const FString MapRoot = ParamsMap.Contains(TEXT("MapRoot")) ? ParamsMap[TEXT("MapRoot")] : FString(TEXT("/Game/Heavy/StreetMap")); const FString OriginText = ParamsMap.FindRef(TEXT("Origin")); const bool bFixedOrigin = !OriginText.IsEmpty(); // STM1-SPEC-10 (W1) / SPEC-11 (M1) : eau coupee a la boite du pack (lon/lat), sans marge. const FString ClipBoxText = ParamsMap.FindRef(TEXT("ClipBox")); 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() && !bFixedOrigin)) { Line(TEXT("usage : -City= -Water= (-Buildings= | -Origin=,) [-MapRoot=/StreetMap>] [-ClipBox=,,,] [-DryRun] [-Report=] [-ListBuildingsInWater=] [-ThresholdM2=] [-MaskOnly [-TreeBlockOsm=] [-HideAllTrees]] [-SkipParkWaterMaxM2=] [-PackMaterial= [-WaterRoughness=0.7] [-WaterSpecular=0.5]] [-WaterZOffsetCm=]")); return Finish(2); } Line(FString::Printf(TEXT("# LPBuildCityWater %s (%s) ; racine %s"), *City, bDryRun ? TEXT("DryRun") : TEXT("ecriture"), *MapRoot)); { TArray RootParts; MapRoot.ParseIntoArray(RootParts, TEXT("/")); const bool bBaseRoot = MapRoot == TEXT("/Game/Heavy/StreetMap"); const bool bPackRoot = RootParts.Num() == 2 && RootParts[0].StartsWith(TEXT("lpm_city_")) && RootParts[1] == TEXT("StreetMap"); if (!bBaseRoot && !bPackRoot) { Line(TEXT("ECHEC : -MapRoot doit etre /Game/Heavy/StreetMap ou /lpm_city_/StreetMap ; aucun asset ecrit")); return Finish(1); } if (!bBaseRoot && Switches.Contains(TEXT("CreateMaterial"))) { Line(TEXT("ECHEC : -CreateMaterial seulement pour le jeu de base (le pack reference M_LP_Water du jeu) ; aucun asset ecrit")); return Finish(1); } } // 1. Carte et maillage de reference. const UStreetMap* StreetMap = LoadObject(nullptr, *FString::Printf(TEXT("%s/%s.%s"), *MapRoot, *City, *City)); const UStaticMesh* CityMesh = LoadObject(nullptr, *FString::Printf(TEXT("%s/Static/%s.%s"), *MapRoot, *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) || (!bFixedOrigin && !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())); FLonLat Origin(0.0, 0.0); if (bFixedOrigin) { TArray OriginParts; OriginText.ParseIntoArray(OriginParts, TEXT(",")); if (OriginParts.Num() != 2) { Line(TEXT("ECHEC : -Origin=, attendu ; aucun asset ecrit")); return Finish(1); } Origin = FLonLat(FCString::Atod(*OriginParts[1]), FCString::Atod(*OriginParts[0])); Line(FString::Printf(TEXT("origine donnee (moyenne des noeuds du .osm importe) : lat %.9f lon %.9f ; pas de recalage"), Origin.Y, Origin.X)); } else { // 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]); } } 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); FLonLat BoxMinLL(0.0, 0.0), BoxMaxLL(0.0, 0.0); const bool bClipBox = !ClipBoxText.IsEmpty(); if (bClipBox) { TArray BoxParts; ClipBoxText.ParseIntoArray(BoxParts, TEXT(",")); if (BoxParts.Num() != 4) { Line(TEXT("ECHEC : -ClipBox=,,, attendu ; aucun asset ecrit")); return Finish(1); } BoxMinLL = FLonLat(FCString::Atod(*BoxParts[1]), FCString::Atod(*BoxParts[0])); BoxMaxLL = FLonLat(FCString::Atod(*BoxParts[3]), FCString::Atod(*BoxParts[2])); Line(FString::Printf(TEXT("eau coupee a la boite : sud %.5f ouest %.5f nord %.5f est %.5f (sans marge)"), BoxMinLL.Y, BoxMinLL.X, BoxMaxLL.Y, BoxMaxLL.X)); } auto ToMap = [&](const TArray& Ring) { TArray LL; for (int32 Index = 0; Index + 1 < Ring.Num(); ++Index) // sans le point de fermeture { LL.Add(Ring[Index]); } if (bClipBox) { LL = ClipToBox(LL, BoxMinLL, BoxMaxLL); } TArray P; for (const FVector2d& Point : LL) { P.Add(Project(Point, 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)); // 1.3.0 (COMMUN-26, bassins noirs des Tuileries) : -SkipParkWaterMaxM2= retire du maillage d'eau les plans d'eau plus // petits que ce seuil dont le centre tombe dans un parc de l'UStreetMap (bassins, fontaines de jardin). La Seine reste. const double SkipParkWaterMaxCm2 = ParamsMap.Contains(TEXT("SkipParkWaterMaxM2")) ? FCString::Atod(*ParamsMap[TEXT("SkipParkWaterMaxM2")]) * 1e4 : 0.0; if (SkipParkWaterMaxCm2 > 0.0) { TArray> Parks; for (const FStreetMapPark& Park : StreetMap->GetParks()) { if (Park.ParkPoints.Num() < 3) continue; TArray P; for (const FVector2D& V : Park.ParkPoints) P.Add(FVector2d(V.X, V.Y)); Parks.Emplace(P); } int32 Skipped = 0; double SkippedCm2 = 0.0; for (int32 Index = Outers.Num() - 1; Index >= 0; --Index) { const double Area = FMath::Abs(Outers[Index].SignedArea()); if (Area >= SkipParkWaterMaxCm2) continue; FVector2d Mean(0.0, 0.0); for (const FVector2d& V : Outers[Index].GetVertices()) Mean += V; Mean /= double(FMath::Max(1, Outers[Index].VertexCount())); const bool bInPark = Parks.ContainsByPredicate([&Mean](const TPolygon2& Park) { return Park.Contains(Mean); }); if (!bInPark) continue; Outers.RemoveAt(Index); ++Skipped; SkippedCm2 += Area; } Line(FString::Printf(TEXT("plans d'eau dans un parc, sous %.0f m2 : %d retires (%.0f m2) ; parcs %d ; restent %d exterieurs"), SkipParkWaterMaxCm2 / 1e4, Skipped, SkippedCm2 / 1e4, Parks.Num(), Outers.Num())); } // 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("%s/Water/%s_Mask"), *MapRoot, *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)); } // STM1-SPEC-10 (G3) : polygones d'eau, pour masquer en jeu les arbres de MapMarkers poses dans l'eau. for (const TGeneralPolygon2& Water : WaterPolygons) { const int32 OuterRing = Mask->WaterRingStarts.Num(); Mask->WaterRingStarts.Add(Mask->WaterPoints.Num()); Mask->WaterRingOuter.Add(INDEX_NONE); for (const FVector2d& V : Water.GetOuter().GetVertices()) Mask->WaterPoints.Add(V); for (const TPolygon2& Hole : Water.GetHoles()) { Mask->WaterRingStarts.Add(Mask->WaterPoints.Num()); Mask->WaterRingOuter.Add(OuterRing); for (const FVector2d& V : Hole.GetVertices()) Mask->WaterPoints.Add(V); } } // 1.4.2 (STM1-SPEC-19 B1) : arbres de MapMarkers qui tombent sur une chaussee, un trottoir ou la pierre du pack. // Regle du BP (MapMarkers CustomEvent_3) : routes de l'UStreetMap au nom non vide, sans « Bridge », de plus de // 100 m d'un bout a l'autre ; un arbre a chaque point sauf le dernier, a 10 m de l'axe d'un cote tire au hasard. // Les deux positions possibles qui genent recoivent un carre de 1 m dans les anneaux, que le jeu traite comme // l'eau (IsInWater, arbres seulement). const FString TreeBlockOsm = ParamsMap.FindRef(TEXT("TreeBlockOsm")); FString TreeBlockText; if (!TreeBlockOsm.IsEmpty()) { if (!bFixedOrigin) { Line(TEXT("ECHEC : -TreeBlockOsm demande -Origin ; aucun asset ecrit")); return Finish(1); } struct FStrip { FVector2d A, B; double Half; bool bExtend; int32 Kind; }; TArray Strips; TArray> StonePolys; for (const FStreetMapRoad& Road : StreetMap->GetRoads()) { // Largeurs du plugin (FStreetMapMeshBuildSettings) : rues 8 m, grandes routes 10 m, voies rapides 14 m. const double Half = (Road.RoadType == EStreetMapRoadType::Highway ? 1400.0 : Road.RoadType == EStreetMapRoadType::MajorRoad ? 1000.0 : 800.0) * 0.5; for (int32 P = 0; P + 1 < Road.RoadPoints.Num(); ++P) { Strips.Add({ FVector2d(Road.RoadPoints[P]), FVector2d(Road.RoadPoints[P + 1]), Half, false, 0 }); } } LPCityOsm::FReader Osm; Osm.OriginLat = Origin.Y; Osm.OriginLon = Origin.X; Osm.bLines = true; Osm.bAreas = true; FString OsmError; if (!LPCityOsm::Read(Osm, TreeBlockOsm, OsmError)) { Line(FString::Printf(TEXT("ECHEC : %s ; aucun asset ecrit"), *OsmError)); return Finish(1); } Osm.Nodes.Empty(); // Memes couches que LPBuildCitySidewalks : trottoirs 2,5 m, rues pietonnes 6 m (prolongees), surfaces de pierre. for (int32 L = 0; L < Osm.Lines.Num(); ++L) { const TMap& Tags = Osm.LineTags[L]; const TArray& Pts = Osm.Lines[L]; const FString Highway = Tags.FindRef(TEXT("highway")); const bool bAreaYes = Tags.FindRef(TEXT("area")) == TEXT("yes"); const bool bClosed = Pts.Num() >= 4 && Pts[0].Equals(Pts.Last(), KINDA_SMALL_NUMBER); const bool bStone = ((Highway == TEXT("pedestrian") || Highway == TEXT("footway")) && bAreaYes) || Tags.FindRef(TEXT("place")) == TEXT("square") || Tags.Contains(TEXT("area:highway")); if (bStone) { if (bClosed) StonePolys.Add(TArray(Pts.GetData(), Pts.Num() - 1)); continue; } const bool bSidewalk = Highway == TEXT("footway") && Tags.FindRef(TEXT("footway")) == TEXT("sidewalk"); const bool bPedestrian = Highway == TEXT("pedestrian") && !bAreaYes; if (!bSidewalk && !bPedestrian) continue; for (int32 P = 0; P + 1 < Pts.Num(); ++P) { Strips.Add({ FVector2d(Pts[P]), FVector2d(Pts[P + 1]), bSidewalk ? 125.0 : 300.0, true, bSidewalk ? 1 : 2 }); } } // Grille de 20 m : bandes et polygones par case de leur boite. constexpr double Cell = 2000.0; auto CellOf = [](double X, double Y) { return FIntPoint(FMath::FloorToInt32(X / Cell), FMath::FloorToInt32(Y / Cell)); }; TMultiMap StripCells, PolyCells; for (int32 I = 0; I < Strips.Num(); ++I) { const FStrip& S = Strips[I]; const double M = S.Half * (S.bExtend ? 2.0 : 1.0); const FIntPoint Min = CellOf(FMath::Min(S.A.X, S.B.X) - M, FMath::Min(S.A.Y, S.B.Y) - M); const FIntPoint Max = CellOf(FMath::Max(S.A.X, S.B.X) + M, FMath::Max(S.A.Y, S.B.Y) + M); for (int32 X = Min.X; X <= Max.X; ++X) for (int32 Y = Min.Y; Y <= Max.Y; ++Y) StripCells.Add(FIntPoint(X, Y), I); } TArray PolyBoxes; for (int32 I = 0; I < StonePolys.Num(); ++I) { const FBox2D Box(StonePolys[I]); PolyBoxes.Add(Box); const FIntPoint Min = CellOf(Box.Min.X, Box.Min.Y), Max = CellOf(Box.Max.X, Box.Max.Y); for (int32 X = Min.X; X <= Max.X; ++X) for (int32 Y = Min.Y; Y <= Max.Y; ++Y) PolyCells.Add(FIntPoint(X, Y), I); } // Couche genante au point : 0 chaussee, 1 trottoir, 2 pierre ; -1 aucune. auto Blocking = [&](const FVector2d& P) -> int32 { int32 Best = -1; for (auto It = StripCells.CreateConstKeyIterator(CellOf(P.X, P.Y)); It; ++It) { const FStrip& S = Strips[It.Value()]; const FVector2d D = S.B - S.A; const double Len = D.Length(); if (Len < 1.0) continue; const FVector2d U = D / Len; const double Along = (P - S.A).Dot(U), Across = FMath::Abs((P - S.A).Dot(FVector2d(-U.Y, U.X))); const double Lo = S.bExtend ? -S.Half : 0.0, Hi = S.bExtend ? Len + S.Half : Len; if (Along >= Lo && Along <= Hi && Across <= S.Half && (Best < 0 || S.Kind < Best)) Best = S.Kind; if (Best == 0) return 0; } if (Best < 0) { for (auto It = PolyCells.CreateConstKeyIterator(CellOf(P.X, P.Y)); It; ++It) { if (PolyBoxes[It.Value()].IsInside(FVector2D(P)) && LPCityOsm::InsidePolygon(StonePolys[It.Value()], FVector2D(P))) return 2; } } return Best; }; int32 Roads = 0, Candidates = 0, Blocked[3] = { 0, 0, 0 }; for (const FStreetMapRoad& Road : StreetMap->GetRoads()) { const TArray& Pts = Road.RoadPoints; if (Road.RoadName.IsEmpty() || Road.RoadName.Contains(TEXT("Bridge"), ESearchCase::IgnoreCase) || Pts.Num() < 2 || FVector2D::Distance(Pts[0], Pts.Last()) <= 10000.0) { continue; } ++Roads; for (int32 P = 0; P + 1 < Pts.Num(); ++P) { // FindLookAtRotation(point, point suivant) puis (0, +-1000, 0) tourne de ce lacet. const double Yaw = FMath::Atan2(Pts[P + 1].Y - Pts[P].Y, Pts[P + 1].X - Pts[P].X); const FVector2d Side(-FMath::Sin(Yaw) * 1000.0, FMath::Cos(Yaw) * 1000.0); for (const double Sign : { 1.0, -1.0 }) { const FVector2d Tree = FVector2d(Pts[P]) + Side * Sign; ++Candidates; const int32 Kind = Blocking(Tree); if (Kind < 0) continue; ++Blocked[Kind]; Mask->WaterRingStarts.Add(Mask->WaterPoints.Num()); Mask->WaterRingOuter.Add(INDEX_NONE); for (const FVector2d& Corner : { FVector2d(-50, -50), FVector2d(50, -50), FVector2d(50, 50), FVector2d(-50, 50) }) { Mask->WaterPoints.Add(Tree + Corner); } } } } TreeBlockText = FString::Printf(TEXT(" ; B1 : %d routes, %d positions d'arbre (2 par point), genantes %d (chaussee %d, trottoir %d, pierre %d) -> %d carres de 1 m ; %d bandes, %d surfaces de pierre"), Roads, Candidates, Blocked[0] + Blocked[1] + Blocked[2], Blocked[0], Blocked[1], Blocked[2], Blocked[0] + Blocked[1] + Blocked[2], Strips.Num(), StonePolys.Num()); } // 1.4.3 (STM1-SPEC-19 A1) : -HideAllTrees, un anneau couvrant toute la carte (bornes de l'UStreetMap + 100 m) ; tous les // arbres de MapMarkers sont masques, remplaces par les arbres du pack (variantes de la table Houses). if (Switches.Contains(TEXT("HideAllTrees"))) { const FVector2d Min = FVector2d(StreetMap->GetBoundsMin()) - FVector2d(10000.0), Max = FVector2d(StreetMap->GetBoundsMax()) + FVector2d(10000.0); Mask->WaterRingStarts.Add(Mask->WaterPoints.Num()); Mask->WaterRingOuter.Add(INDEX_NONE); for (const FVector2d& Corner : { Min, FVector2d(Max.X, Min.Y), Max, FVector2d(Min.X, Max.Y) }) Mask->WaterPoints.Add(Corner); TreeBlockText += FString::Printf(TEXT(" ; tous les arbres masques : anneau (%.0f, %.0f) a (%.0f, %.0f) cm"), Min.X, Min.Y, Max.X, Max.Y); } 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 ; polygones d'eau : %d anneaux, %d points ; maillage d'eau non touche%s"), *MaskPackageName, *MaskFile, InWater.Num(), Buildings.Num(), Mask->WaterRingStarts.Num(), Mask->WaterPoints.Num(), *TreeBlockText)); 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("%s/Water/%s"), *MapRoot, *City); if (FPackageName::DoesPackageExist(PackageName)) { Line(FString::Printf(TEXT("ECHEC : %s existe deja ; aucun asset ecrit"), *PackageName)); return Finish(1); } // 1.7.3b (STM1-DIAG-18) : -PackMaterial= [-WaterRoughness=0.7] [-WaterSpecular=0.5] : materiau d'eau propre au pack, // cree dans /Water/ (jamais ecrase), meme couleur que M_LP_Water ; une rugosite au-dessus de // r.SSR.MaxRoughness (0,6 par defaut) sort l'eau des reflexions SSR, qui scintillaient sous TAA. const FString PackMaterialName = ParamsMap.FindRef(TEXT("PackMaterial")); const FString PackMaterialPackage = PackMaterialName.IsEmpty() ? FString() : FString::Printf(TEXT("%s/Water/%s"), *MapRoot, *PackMaterialName); if (!PackMaterialName.IsEmpty() && (MapRoot == TEXT("/Game/Heavy/StreetMap") || FPackageName::DoesPackageExist(PackMaterialPackage))) { Line(FString::Printf(TEXT("ECHEC : -PackMaterial seulement pour un pack, et %s ne doit pas exister ; aucun asset ecrit"), *PackMaterialPackage)); return Finish(1); } const float WaterRoughness = ParamsMap.Contains(TEXT("WaterRoughness")) ? FCString::Atof(*ParamsMap[TEXT("WaterRoughness")]) : 0.7f; const float WaterSpecular = ParamsMap.Contains(TEXT("WaterSpecular")) ? FCString::Atof(*ParamsMap[TEXT("WaterSpecular")]) : 0.5f; const bool bPackMaterial = !PackMaterialName.IsEmpty(); const FString MaterialPackageString = bPackMaterial ? PackMaterialPackage : FString(TEXT("/Game/Heavy/StreetMap/Water/M_LP_Water")); const TCHAR* MaterialPackageName = *MaterialPackageString; UMaterialInterface* WaterMaterial = bPackMaterial ? nullptr : LoadObject(nullptr, TEXT("/Game/Heavy/StreetMap/Water/M_LP_Water.M_LP_Water")); if (!WaterMaterial && (bPackMaterial || 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, bPackMaterial ? FName(*PackMaterialName) : FName(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 = bPackMaterial ? WaterRoughness : 0.15f; Specular->R = bPackMaterial ? WaterSpecular : 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 ; rugosite %.2f ; speculaire %.2f"), MaterialPackageName, *MaterialFile, Roughness->R, Specular->R)); 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"); // 1.7.5 (STM1-DIAG-20) : -WaterZOffsetCm= releve tous les sommets de l'eau. Le jeu la pose au plan de pave de MapMarkers // + 2 cm ; ce plan (BasicShapes/Plane a l'echelle 1e6, sommets a ~5e7 cm) n'a qu'une resolution de quelques cm en flottant : // a 2 cm, l'eau et le pave se disputaient les pixels (scintillement de toute la surface). const float WaterZOffsetCm = ParamsMap.Contains(TEXT("WaterZOffsetCm")) ? FCString::Atof(*ParamsMap[TEXT("WaterZOffsetCm")]) : 0.f; if (WaterZOffsetCm != 0.f) { Line(FString::Printf(TEXT("decalage Z de l'eau : %.1f cm (%d sommets)"), WaterZOffsetCm, Vertices.Num())); } TArray VertexIds; VertexIds.Reserve(Vertices.Num()); for (const FVector3f& V : Vertices) { const FVertexID Id = Mesh.CreateVertex(); Attributes.GetVertexPositions()[Id] = V + FVector3f(0.f, 0.f, WaterZOffsetCm); 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); }