// Lot STM1 (STM1-SPEC-17, option T ; pack 1.4.0 ; STM1-DIAG-03 E1 : 1.4.1). Voir LPBuildCitySidewalksCommandlet.h. #include "LPBuildCitySidewalksCommandlet.h" #include "AssetRegistry/AssetRegistryModule.h" #include "CompGeom/PolygonTriangulation.h" #include "Engine/StaticMesh.h" #include "HAL/FileManager.h" #include "IndexTypes.h" #include "LPCityOsm.h" #include "Materials/MaterialInterface.h" #include "Misc/FileHelper.h" #include "Misc/PackageName.h" #include "RawMesh.h" #include "StreetMap.h" #include "UObject/Package.h" #include "UObject/SavePackage.h" DEFINE_LOG_CATEGORY_STATIC(LogLPBuildCitySidewalks, Log, All); namespace LPBuildCitySidewalks { bool IsAllowedPackage(const FString& PackageName) { TArray Parts; PackageName.ParseIntoArray(Parts, TEXT("/")); return Parts.Num() == 4 && Parts[0].StartsWith(TEXT("lpm_city_")) && Parts[1] == TEXT("StreetMap") && Parts[2] == TEXT("Sidewalks"); } enum ELayer : int32 { Sidewalk = 0, Stone = 1, Green = 2, Slab = 3, Light = 4, Sand = 5, Water = 6 }; /** 1.5.2 (STM1-DIAG-06 § 3 ter) : couche d'une surface pietonne (surface fermee ou rue pietonne) selon sa balise surface : * paves (sett, cobblestone) -> pierre ; asphalt -> beton clair ; terre battue et graviers -> stabilise ; autre ou sans * balise -> dalles. */ ELayer SurfaceLayer(const TMap& Tags) { const FString Surface = Tags.FindRef(TEXT("surface")); // 1.7.1 (STM1-DIAG-10 D) : places en boucle (lp:source P3, sans balise surface) en pierre, comme en 1.5.1 (Vendome). if (Surface.IsEmpty() && Tags.FindRef(TEXT("lp:source")).StartsWith(TEXT("P3"))) return Stone; if (Surface == TEXT("sett") || Surface == TEXT("cobblestone") || Surface == TEXT("unhewn_cobblestone")) return Stone; if (Surface == TEXT("asphalt")) return Light; for (const TCHAR* Loose : { TEXT("compacted"), TEXT("gravel"), TEXT("fine_gravel"), TEXT("unpaved"), TEXT("ground"), TEXT("dirt"), TEXT("earth"), TEXT("sand"), TEXT("pebblestone") }) { if (Surface == Loose) return Sand; } return Slab; } /** 1.5.1 (STM1-SPEC-21) : surface de pierre en dalles plutot qu'en paves : cours de monuments (lp:source P4), parvis, * esplanades, cours et terrasses (nom), dalles explicites (surface stone, concrete:plates, marble, granite). */ bool IsSlab(const TMap& Tags) { if (Tags.FindRef(TEXT("lp:source")).StartsWith(TEXT("P4"))) return true; const FString Name = Tags.FindRef(TEXT("name")); for (const TCHAR* Prefix : { TEXT("Parvis"), TEXT("Esplanade"), TEXT("Cour "), TEXT("Cour d"), TEXT("Terrasse") }) { if (Name.StartsWith(Prefix, ESearchCase::CaseSensitive)) return true; } const FString Surface = Tags.FindRef(TEXT("surface")); return Surface == TEXT("stone") || Surface == TEXT("concrete:plates") || Surface == TEXT("marble") || Surface == TEXT("granite"); } /** 1.7.6 (STM1-DIAG-16 § 3, W) : plan d'eau ferme qui n'est pas le fleuve (bassins, fontaines, etangs). */ bool IsStillWater(const TMap& Tags) { if (Tags.FindRef(TEXT("water")) == TEXT("river") || Tags.FindRef(TEXT("waterway")) == TEXT("riverbank")) return false; return Tags.FindRef(TEXT("natural")) == TEXT("water") || Tags.FindRef(TEXT("amenity")) == TEXT("fountain") || Tags.Contains(TEXT("water")); } ELayer AreaLayer(const TMap& Tags, bool& bOut, bool bWater) { bOut = true; if (bWater && IsStillWater(Tags)) return Water; const FString Highway = Tags.FindRef(TEXT("highway")); if ((Highway == TEXT("pedestrian") || Highway == TEXT("footway")) && Tags.FindRef(TEXT("area")) == TEXT("yes")) return Stone; if (Tags.FindRef(TEXT("place")) == TEXT("square") || Tags.Contains(TEXT("area:highway"))) return Stone; const FString Landuse = Tags.FindRef(TEXT("landuse")); if (Tags.FindRef(TEXT("leisure")) == TEXT("garden") || Landuse == TEXT("grass") || Landuse == TEXT("flowerbed") || Landuse == TEXT("village_green") || Landuse == TEXT("recreation_ground")) return Green; bOut = false; return Sidewalk; } struct FBuilder { FRawMesh Mesh; int32 Triangles[7] = { 0, 0, 0, 0, 0, 0, 0 }; /** Triangle face vers le haut (+Z) : aire signee negative en XY, comme AddThick2DLine du plugin. */ void AddTriangle(FVector2f A, FVector2f B, FVector2f C, float Z, int32 Slot) { const float Cross = (B.X - A.X) * (C.Y - A.Y) - (B.Y - A.Y) * (C.X - A.X); if (FMath::Abs(Cross) < 1.f) return; if (Cross > 0.f) Swap(B, C); const FVector2f Corners[3] = { A, B, C }; const int32 First = Mesh.VertexPositions.Num(); for (int32 I = 0; I < 3; ++I) { Mesh.VertexPositions.Add(FVector3f(Corners[I], Z)); Mesh.WedgeIndices.Add(First + I); // Base tangente alignee sur X local, comme l'echantillonnage des textures (position locale XY). Mesh.WedgeTangentX.Add(FVector3f(1.f, 0.f, 0.f)); Mesh.WedgeTangentY.Add(FVector3f(0.f, 1.f, 0.f)); Mesh.WedgeTangentZ.Add(FVector3f::UpVector); Mesh.WedgeTexCoords[0].Add(Corners[I] / 300.f); Mesh.WedgeColors.Add(FColor::White); } Mesh.FaceMaterialIndices.Add(Slot); Mesh.FaceSmoothingMasks.Add(0); ++Triangles[Slot]; } /** Bande d'un segment, prolongee d'une demi-largeur a chaque bout pour fermer les angles. */ void AddStrip(const FVector2f& A, const FVector2f& B, float Half, float Z, int32 Slot) { const float Len = FVector2f::Distance(A, B); const FVector2f Dir = (B - A) / Len; const FVector2f Right(-Dir.Y, Dir.X); const FVector2f S = A - Dir * Half, E = B + Dir * Half; AddTriangle(S - Right * Half, S + Right * Half, E + Right * Half, Z, Slot); AddTriangle(S - Right * Half, E + Right * Half, E - Right * Half, Z, Slot); } }; } ULPBuildCitySidewalksCommandlet::ULPBuildCitySidewalksCommandlet() { IsClient = false; IsServer = false; IsEditor = true; LogToConsole = true; } int32 ULPBuildCitySidewalksCommandlet::Main(const FString& Params) { using namespace LPBuildCitySidewalks; TArray Tokens, Switches; TMap ParamsMap; ParseCommandLine(*Params, Tokens, Switches, ParamsMap); const FString MapPath = ParamsMap.FindRef(TEXT("StreetMap")); const FString OsmPath = ParamsMap.FindRef(TEXT("Osm")); const FString OriginText = ParamsMap.FindRef(TEXT("Origin")); const FString PackageName = ParamsMap.FindRef(TEXT("Package")); const FString ReportPath = ParamsMap.FindRef(TEXT("Report")); auto Number = [&ParamsMap](const TCHAR* Key, double Default) { return ParamsMap.Contains(Key) ? FCString::Atod(*ParamsMap[Key]) : Default; }; const double WidthCm = Number(TEXT("WidthM"), 2.5) * 100.0; const double PedestrianWidthCm = Number(TEXT("PedestrianWidthM"), 6.0) * 100.0; // 1.5.1 : -SlabMaterial ajoute la couche des dalles (sinon ces surfaces restent en pierre) ; -SlabZCm (pierre + 1 par defaut). // 1.5.2 : -BySurface (avec -SlabMaterial, -LightMaterial, -SandMaterial) : couche de chaque surface pietonne selon sa balise // surface (SurfaceLayer) ; -LightZCm et -SandZCm (pierre + 1 et + 0,5 par defaut). const bool bBySurface = Switches.Contains(TEXT("BySurface")); const bool bSlab = ParamsMap.Contains(TEXT("SlabMaterial")); const float StoneZ = float(Number(TEXT("StoneZCm"), -3.0)); // 1.7.6 (W) : -WaterMaterial (avec -BySurface) ajoute la couche des plans d'eau fermes hors fleuve, a -WaterZCm (5 par defaut). const bool bWater = ParamsMap.Contains(TEXT("WaterMaterial")); const float Z[7] = { float(Number(TEXT("ZCm"), -5.0)), StoneZ, float(Number(TEXT("GreenZCm"), -1.0)), float(Number(TEXT("SlabZCm"), StoneZ + 1.0)), float(Number(TEXT("LightZCm"), StoneZ + 1.0)), float(Number(TEXT("SandZCm"), StoneZ + 0.5)), float(Number(TEXT("WaterZCm"), 5.0)) }; const int32 NumSlots = bWater ? 7 : bBySurface ? 6 : bSlab ? 4 : 3; const bool bAreas = !Switches.Contains(TEXT("NoAreas")); const bool bDryRun = Switches.Contains(TEXT("DryRun")); const TCHAR* const SlotNames[7] = { TEXT("Sidewalk"), TEXT("Stone"), TEXT("Green"), TEXT("Slab"), TEXT("Light"), TEXT("Sand"), TEXT("Water") }; const TCHAR* const MaterialKeys[7] = { TEXT("Material"), TEXT("StoneMaterial"), TEXT("GreenMaterial"), TEXT("SlabMaterial"), TEXT("LightMaterial"), TEXT("SandMaterial"), TEXT("WaterMaterial") }; TArray Report; auto Line = [&Report](const FString& Text) { UE_LOG(LogLPBuildCitySidewalks, 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; }; TArray OriginParts; OriginText.ParseIntoArray(OriginParts, TEXT(",")); if (MapPath.IsEmpty() || OsmPath.IsEmpty() || PackageName.IsEmpty() || OriginParts.Num() != 2) { Line(TEXT("usage : -StreetMap= -Osm= -Origin=, -Package=/StreetMap/Sidewalks/> [-Material=] [-StoneMaterial=] [-GreenMaterial=] [-SlabMaterial=] [-BySurface -LightMaterial= -SandMaterial= [-LightZCm] [-SandZCm]] [-WidthM=2.5] [-PedestrianWidthM=6] [-ZCm=-5] [-StoneZCm=-3] [-GreenZCm=-1] [-SlabZCm=] [-WaterMaterial= [-WaterZCm=5]] [-NoAreas] [-DryRun] [-Report=]")); return Finish(2); } Line(FString::Printf(TEXT("# LPBuildCitySidewalks %s <- %s (%s) ; trottoirs %.2f m a %.1f cm ; pierre a %.1f cm (rues pietonnes %.1f m) ; vert a %.1f cm%s%s"), *PackageName, *OsmPath, bDryRun ? TEXT("DryRun") : TEXT("ecriture"), WidthCm / 100.0, Z[0], Z[1], PedestrianWidthCm / 100.0, Z[2], bAreas ? TEXT("") : TEXT(" ; surfaces non traitees (-NoAreas)"), bSlab ? *FString::Printf(TEXT(" ; dalles a %.1f cm%s"), Z[3], bBySurface ? *FString::Printf(TEXT(" ; selon surface : beton clair a %.1f cm, stabilise a %.1f cm"), Z[4], Z[5]) : TEXT("")) : TEXT(""))); if (bWater && !bBySurface) { Line(TEXT("ECHEC : -WaterMaterial demande -BySurface ; aucun asset ecrit")); return Finish(2); } if (bBySurface && !(bSlab && ParamsMap.Contains(TEXT("LightMaterial")) && ParamsMap.Contains(TEXT("SandMaterial")))) { Line(TEXT("ECHEC : -BySurface demande -SlabMaterial, -LightMaterial et -SandMaterial ; aucun asset ecrit")); return Finish(2); } if (!IsAllowedPackage(PackageName) || !FPackageName::IsValidLongPackageName(PackageName)) { Line(TEXT("ECHEC : paquet hors de /lpm_city_*/StreetMap/Sidewalks/ ; 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); } const UStreetMap* Map = LoadObject(nullptr, *MapPath); UMaterialInterface* Materials[7] = { nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr }; for (int32 S = 0; S < NumSlots; ++S) { const FString Path = ParamsMap.FindRef(MaterialKeys[S]); Materials[S] = Path.IsEmpty() ? nullptr : LoadObject(nullptr, *Path); if (!Path.IsEmpty() && !Materials[S]) { Line(FString::Printf(TEXT("ECHEC : materiau %s introuvable ; aucun asset ecrit"), *Path)); return Finish(1); } } if (!Map) { Line(TEXT("ECHEC : carte introuvable ; aucun asset ecrit")); return Finish(1); } // Boite de la carte : routes et batiments. FBox2D Bounds(ForceInit); for (const FStreetMapRoad& Road : Map->GetRoads()) for (const FVector2D& P : Road.RoadPoints) Bounds += P; for (const FStreetMapBuilding& Building : Map->GetBuildings()) for (const FVector2D& P : Building.BuildingPoints) Bounds += P; LPCityOsm::FReader Osm; Osm.OriginLat = FCString::Atod(*OriginParts[0]); Osm.OriginLon = FCString::Atod(*OriginParts[1]); Osm.bLines = true; Osm.bAreas = bAreas; Osm.bWaterAreas = bWater; FString Error; if (!LPCityOsm::Read(Osm, OsmPath, Error)) { Line(FString::Printf(TEXT("ECHEC : %s ; aucun asset ecrit"), *Error)); return Finish(1); } Osm.Nodes.Empty(); FBuilder Builder; int32 SidewalkWays = 0, PedestrianWays = 0, Outside = 0, Areas[7] = { 0, 0, 0, 0, 0, 0, 0 }, FailedAreas = 0; double LengthCm[7] = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }, AreaCm2[7] = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }; 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")); bool bIsArea = false; ELayer Layer = bAreas ? AreaLayer(Tags, bIsArea, bWater) : Sidewalk; if (bIsArea && bBySurface && Layer == Stone) Layer = SurfaceLayer(Tags); else if (bIsArea && bSlab && Layer == Stone && IsSlab(Tags)) Layer = Slab; if (bIsArea) { // Surface fermee : triangulation (dernier point = premier, retire). TArray Poly; for (int32 P = 0; P + 1 < Pts.Num(); ++P) Poly.Add(FVector2d(Pts[P].X, Pts[P].Y)); if (Poly.Num() < 3 || !Bounds.IsInside(LPCityOsm::BoxCenter(Pts))) { ++Outside; continue; } TArray Tris; PolygonTriangulation::TriangulateSimplePolygon(Poly, Tris, false); if (Tris.IsEmpty()) { ++FailedAreas; continue; } double Area = 0.0; for (int32 P = 0; P < Poly.Num(); ++P) Area += Poly[P].X * Poly[(P + 1) % Poly.Num()].Y - Poly[(P + 1) % Poly.Num()].X * Poly[P].Y; for (const UE::Geometry::FIndex3i& T : Tris) { Builder.AddTriangle(FVector2f(Poly[T.A]), FVector2f(Poly[T.B]), FVector2f(Poly[T.C]), Z[Layer], Layer); } ++Areas[Layer]; AreaCm2[Layer] += FMath::Abs(Area) * 0.5; continue; } const bool bSidewalk = Highway == TEXT("footway") && Tags.FindRef(TEXT("footway")) == TEXT("sidewalk"); const bool bPedestrian = bAreas && Highway == TEXT("pedestrian") && Tags.FindRef(TEXT("area")) != TEXT("yes"); if (!bSidewalk && !bPedestrian) continue; const int32 Slot = bSidewalk ? Sidewalk : bBySurface ? int32(SurfaceLayer(Tags)) : int32(Stone); (bSidewalk ? SidewalkWays : PedestrianWays)++; const float Half = float((bSidewalk ? WidthCm : PedestrianWidthCm) * 0.5); for (int32 P = 0; P + 1 < Pts.Num(); ++P) { const FVector2f A(Pts[P]), B(Pts[P + 1]); const float Len = FVector2f::Distance(A, B); if (Len < 1.f) continue; if (!Bounds.IsInside(FVector2D((A + B) * 0.5f))) { ++Outside; continue; } Builder.AddStrip(A, B, Half, Z[Slot], Slot); LengthCm[Slot] += Len; } } Line(FString::Printf(TEXT("trottoirs (footway=sidewalk) %d voies, %.1f km ; %d triangles"), SidewalkWays, LengthCm[Sidewalk] / 1e5, Builder.Triangles[Sidewalk])); Line(FString::Printf(TEXT("pierre : %d surfaces (%.3f km2) et %d rues pietonnes (%.1f km) ; %d triangles"), Areas[Stone], AreaCm2[Stone] / 1e10, PedestrianWays, LengthCm[Stone] / 1e5, Builder.Triangles[Stone])); Line(FString::Printf(TEXT("vert : %d surfaces (%.3f km2) ; %d triangles"), Areas[Green], AreaCm2[Green] / 1e10, Builder.Triangles[Green])); if (bSlab) { Line(FString::Printf(TEXT("dalles : %d surfaces (%.3f km2) ; %d triangles"), Areas[Slab], AreaCm2[Slab] / 1e10, Builder.Triangles[Slab])); } if (bBySurface) { Line(FString::Printf(TEXT("selon surface : rues pietonnes en bandes (km) paves %.1f, dalles %.1f, beton clair %.1f, stabilise %.1f"), LengthCm[Stone] / 1e5, LengthCm[Slab] / 1e5, LengthCm[Light] / 1e5, LengthCm[Sand] / 1e5)); Line(FString::Printf(TEXT("beton clair (asphalt) : %d surfaces (%.3f km2) ; %d triangles"), Areas[Light], AreaCm2[Light] / 1e10, Builder.Triangles[Light])); Line(FString::Printf(TEXT("stabilise (terre battue, graviers) : %d surfaces (%.3f km2) ; %d triangles"), Areas[Sand], AreaCm2[Sand] / 1e10, Builder.Triangles[Sand])); } if (bWater) { Line(FString::Printf(TEXT("eau (plans d'eau fermes hors fleuve) : %d surfaces (%.4f km2) a %.1f cm ; %d triangles"), Areas[Water], AreaCm2[Water] / 1e10, Z[Water], Builder.Triangles[Water])); } Line(FString::Printf(TEXT("hors de la carte %d ; surfaces non triangulees %d ; total %d triangles"), Outside, FailedAreas, Builder.Mesh.WedgeIndices.Num() / 3)); if (Builder.Mesh.WedgeIndices.IsEmpty()) { Line(TEXT("ECHEC : aucun triangle ; aucun asset ecrit")); return Finish(1); } if (bDryRun) { Line(TEXT("DryRun : aucun asset ecrit")); return Finish(0); } UPackage* Package = CreatePackage(*PackageName); UStaticMesh* StaticMesh = NewObject(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.bUseFullPrecisionUVs = false; // Composant Movable pose en jeu : pas de lightmap. SourceModel.BuildSettings.bGenerateLightmapUVs = false; SourceModel.RawMeshBulkData->SaveRawMesh(Builder.Mesh); for (int32 S = 0; S < NumSlots; ++S) { StaticMesh->GetStaticMaterials().Add(FStaticMaterial(Materials[S], SlotNames[S])); } StaticMesh->SetImportVersion(EImportStaticMeshVersion::LastVersion); StaticMesh->Build(/*bSilent=*/true); StaticMesh->PostEditChange(); FAssetRegistryModule::AssetCreated(StaticMesh); Line(FString::Printf(TEXT("Build : sommets rendus LOD0 %d ; sections %d ; materiaux %s, %s, %s%s%s"), StaticMesh->GetNumVertices(0), StaticMesh->GetNumSections(0), *GetPathNameSafe(Materials[0]), *GetPathNameSafe(Materials[1]), *GetPathNameSafe(Materials[2]), bSlab ? TEXT(", ") : TEXT(""), bSlab ? *GetPathNameSafe(Materials[3]) : TEXT(""))); if (bBySurface) { Line(FString::Printf(TEXT("materiaux selon surface : %s, %s"), *GetPathNameSafe(Materials[4]), *GetPathNameSafe(Materials[5]))); } 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); }