CARLA
Loading...
Searching...
No Matches
OpenDriveGenerator.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma
2// de Barcelona (UAB).
3//
4// This work is licensed under the terms of the MIT license.
5// For a copy, see <https://opensource.org/licenses/MIT>.
6
9#include "Carla/Game/Tagger.h"
10#include "Carla.h"
14#include "Materials/MaterialInterface.h"
15#include "PCGComponent.h"
16#include "Components/BoxComponent.h"
17#include "Components/InstancedStaticMeshComponent.h"
18#include "EngineUtils.h"
19#include "TimerManager.h"
20#include "PCGGraph.h"
21#include "Misc/FileHelper.h"
22#include "Misc/Paths.h"
23#include "UObject/UObjectIterator.h"
24#include "HAL/IConsoleManager.h"
25
26// carla.OpenDrive.StreetFurniture 0 skips the PCG lamp/vegetation/signage
27// scatter of generate_opendrive_world. Hybrid rendering over a neural scene
28// needs a bare proxy world: furniture that only exists in the proxy casts
29// shadows on the synthetic actors and shows up in their reflections.
30static TAutoConsoleVariable<int32> CVarOpenDriveStreetFurniture(
31 TEXT("carla.OpenDrive.StreetFurniture"), 1,
32 TEXT("1 (default): scatter street furniture (lamps, vegetation, signage) on generated OpenDRIVE worlds. 0: bare road only."),
33 ECVF_Default);
34
37#include <carla/rpc/String.h>
39
40// #include "Engine/Classes/Interfaces/Interface_CollisionDataProvider.h"
41// #include "PhysicsCore/Public/BodySetupEnums.h"
42
43namespace crp = carla::rpc;
44
45namespace {
46
47 /// Tags a generated procedural-mesh component for semantic segmentation
48 /// and CPU-side tag queries. ATagger::TagActor (Game/Tagger.cpp) only
49 /// walks UStaticMeshComponent/USkeletalMeshComponent, inferring the label
50 /// from the mesh asset's content folder, so runtime-generated
51 /// UProceduralMeshComponents were left with unset/uninitialized
52 /// CustomPrimitiveData (garbage semantic-segmentation labels). Here the
53 /// label is already known from the OpenDRIVE material name, so this
54 /// replicates ATagger's exact per-component tagging steps directly
55 /// instead of going through the asset-path inference path.
56 void TagGeneratedComponent(UPrimitiveComponent &Component, uint32 ActorId, crp::CityObjectLabel Label)
57 {
58 ATagger::SetStencilValue(Component, ActorId, Label, true);
59 Component.ComponentTags.Add(FName(*ATagger::GetTagAsString(Label)));
60 }
61
62 /// Appends Src into Dst, rebasing Src's triangle indices by Dst's current
63 /// vertex count so the combined buffers stay a single valid mesh.
64 void AppendProceduralMesh(FProceduralCustomMesh &Dst, const FProceduralCustomMesh &Src)
65 {
66 const int32 VertexOffset = Dst.Vertices.Num();
67 Dst.Vertices.Append(Src.Vertices);
68 Dst.Normals.Append(Src.Normals);
69 Dst.UV0.Append(Src.UV0);
70 Dst.Triangles.Reserve(Dst.Triangles.Num() + Src.Triangles.Num());
71 for (const int32 Index : Src.Triangles)
72 {
73 Dst.Triangles.Add(Index + VertexOffset);
74 }
75 }
76
77 /// Extracts the sub-mesh covering a single material range
78 /// ([Range.index_start, Range.index_end) into Mesh.GetIndexes()) from a
79 /// full carla::geom::Mesh, converting to Unreal space/units and rebasing
80 /// indices into a self-contained, densely-packed vertex/index buffer.
81 /// Mirrors the m->cm scale, the "-1" rebase (carla::geom::Mesh indices are
82 /// 1-based) and the (i, i+2, i+1) winding flip that
83 /// carla::geom::Mesh::operator FProceduralCustomMesh() applies to the
84 /// whole mesh (LibCarla/source/carla/geom/Mesh.h:162-227), scoped down to
85 /// only the triangles belonging to this material.
86 FProceduralCustomMesh ExtractMaterialRange(
87 const carla::geom::Mesh &Mesh,
88 const carla::geom::MeshMaterial &Range,
89 bool bSourceHasPerVertexUVs)
90 {
92
93 const auto &Vertices = Mesh.GetVertices();
94 const auto &Indexes = Mesh.GetIndexes();
95 const auto &UVs = Mesh.GetUVs();
96
97 if (Range.index_end <= Range.index_start || Range.index_end > Indexes.size())
98 {
99 return Result;
100 }
101
102 TMap<size_t, int32> GlobalToLocal;
103 GlobalToLocal.Reserve(static_cast<int32>(Range.index_end - Range.index_start));
104
105 for (size_t i = Range.index_start; i < Range.index_end; i += 3)
106 {
107 int32 LocalIdx[3];
108 for (int32 k = 0; k < 3; ++k)
109 {
110 const size_t GlobalIdx = Indexes[i + k];
111 if (const int32 *Found = GlobalToLocal.Find(GlobalIdx))
112 {
113 LocalIdx[k] = *Found;
114 }
115 else
116 {
117 // carla::geom::Mesh indices are 1-based.
118 const size_t VertexIdx = GlobalIdx - 1;
119 const auto &V = Vertices[VertexIdx];
120 const int32 NewLocalIdx = Result.Vertices.Add(
121 FVector{1e2f * V.x, 1e2f * V.y, 1e2f * V.z});
122 if (bSourceHasPerVertexUVs && VertexIdx < UVs.size())
123 {
124 Result.UV0.SetNum(NewLocalIdx + 1);
125 Result.UV0[NewLocalIdx] = FVector2D{UVs[VertexIdx].x, UVs[VertexIdx].y};
126 }
127 GlobalToLocal.Add(GlobalIdx, NewLocalIdx);
128 LocalIdx[k] = NewLocalIdx;
129 }
130 }
131 // Unreal is left-handed; flip the last two indices, same as the
132 // full-mesh conversion.
133 Result.Triangles.Add(LocalIdx[0]);
134 Result.Triangles.Add(LocalIdx[2]);
135 Result.Triangles.Add(LocalIdx[1]);
136 }
137
138 // Recompute per-vertex normals over the extracted sub-mesh: accumulate
139 // the (area-weighted) up-biased face normal of every incident triangle
140 // and normalize, so shared vertices shade smoothly. Assigning the raw
141 // face normal per triangle (last writer wins) made every 2 m strip
142 // quad a separately-lit facet and the whole road read as shattered.
143 TArray<FVector> Accum;
144 Accum.Init(FVector::ZeroVector, Result.Vertices.Num());
145 for (int32 i = 0; i < Result.Triangles.Num(); i += 3)
146 {
147 const FVector &V0 = Result.Vertices[Result.Triangles[i]];
148 const FVector &V1 = Result.Vertices[Result.Triangles[i + 1]];
149 const FVector &V2 = Result.Vertices[Result.Triangles[i + 2]];
150 const FVector U = V1 - V0;
151 const FVector V = V2 - V0;
152 // Unnormalized cross product: magnitude = 2x triangle area, giving
153 // the area weighting for free.
154 FVector Normal;
155 Normal.X = (U.Y * V.Z) - (U.Z * V.Y);
156 Normal.Y = (U.Z * V.X) - (U.X * V.Z);
157 Normal.Z = (U.X * V.Y) - (U.Y * V.X);
158 Normal = -Normal;
159 if (Normal.Z < 0.0f)
160 {
161 // Road geometry always faces up; mirrored winding otherwise
162 // cancels neighbouring contributions instead of averaging.
163 Normal = -Normal;
164 }
165 Accum[Result.Triangles[i]] += Normal;
166 Accum[Result.Triangles[i + 1]] += Normal;
167 Accum[Result.Triangles[i + 2]] += Normal;
168 }
169 Result.Normals.Init(FVector::UpVector, Result.Vertices.Num());
170 for (int32 i = 0; i < Accum.Num(); ++i)
171 {
172 const FVector Normal = Accum[i].GetSafeNormal(.0001f);
173 if (Normal != FVector::ZeroVector)
174 {
175 Result.Normals[i] = Normal;
176 }
177 }
178
179 return Result;
180 }
181
182 /// One level of the multilevel B-spline approximation (Lee, Wolberg &
183 /// Shin, "Scattered Data Interpolation with Multilevel B-Splines", IEEE
184 /// TVCG 1997) used to fit the ground terrain surface to the road height
185 /// samples: a uniform bicubic B-spline over a regular control lattice,
186 /// fit in closed form to scattered points by distributing each point's
187 /// value onto the 4x4 control points whose basis functions cover it,
188 /// weighted so points with overlapping support average by influence.
189 struct FGroundBSplineLattice
190 {
191 float OriginX = 0.0f;
192 float OriginY = 0.0f;
193 float Spacing = 1.0f;
194 int32 CellsX = 1;
195 int32 CellsY = 1;
196 TArray<float> Control; // (CellsX + 3) * (CellsY + 3), row-major in Y
197
198 static void Basis(float T, float B[4])
199 {
200 const float T2 = T * T;
201 const float T3 = T2 * T;
202 B[0] = (1.0f - T) * (1.0f - T) * (1.0f - T) / 6.0f;
203 B[1] = (3.0f * T3 - 6.0f * T2 + 4.0f) / 6.0f;
204 B[2] = (-3.0f * T3 + 3.0f * T2 + 3.0f * T + 1.0f) / 6.0f;
205 B[3] = T3 / 6.0f;
206 }
207
208 void LocalCoords(float X, float Y, int32 &I, int32 &J, float &S, float &T) const
209 {
210 const float FX = FMath::Clamp((X - OriginX) / Spacing, 0.0f, static_cast<float>(CellsX) - KINDA_SMALL_NUMBER);
211 const float FY = FMath::Clamp((Y - OriginY) / Spacing, 0.0f, static_cast<float>(CellsY) - KINDA_SMALL_NUMBER);
212 I = FMath::Clamp(FMath::FloorToInt(FX), 0, CellsX - 1);
213 J = FMath::Clamp(FMath::FloorToInt(FY), 0, CellsY - 1);
214 S = FX - static_cast<float>(I);
215 T = FY - static_cast<float>(J);
216 }
217
218 float Eval(float X, float Y) const
219 {
220 int32 I, J;
221 float S, T, BS[4], BT[4];
222 LocalCoords(X, Y, I, J, S, T);
223 Basis(S, BS);
224 Basis(T, BT);
225 const int32 NumCtrlX = CellsX + 3;
226 float Z = 0.0f;
227 for (int32 L = 0; L < 4; ++L)
228 {
229 for (int32 K = 0; K < 4; ++K)
230 {
231 Z += BS[K] * BT[L] * Control[(J + L) * NumCtrlX + (I + K)];
232 }
233 }
234 return Z;
235 }
236
237 void Fit(const TArray<FVector> &Points)
238 {
239 const int32 NumCtrlX = CellsX + 3;
240 const int32 NumCtrlY = CellsY + 3;
241 TArray<float> Num, Den;
242 Num.Init(0.0f, NumCtrlX * NumCtrlY);
243 Den.Init(0.0f, NumCtrlX * NumCtrlY);
244 for (const FVector &P : Points)
245 {
246 int32 I, J;
247 float S, T, BS[4], BT[4];
248 LocalCoords(P.X, P.Y, I, J, S, T);
249 Basis(S, BS);
250 Basis(T, BT);
251 float SumW2 = 0.0f;
252 for (int32 L = 0; L < 4; ++L)
253 {
254 for (int32 K = 0; K < 4; ++K)
255 {
256 SumW2 += FMath::Square(BS[K] * BT[L]);
257 }
258 }
259 if (SumW2 <= KINDA_SMALL_NUMBER)
260 {
261 continue;
262 }
263 for (int32 L = 0; L < 4; ++L)
264 {
265 for (int32 K = 0; K < 4; ++K)
266 {
267 const float W = BS[K] * BT[L];
268 const float Phi = W * static_cast<float>(P.Z) / SumW2;
269 const int32 Idx = (J + L) * NumCtrlX + (I + K);
270 Num[Idx] += W * W * Phi;
271 Den[Idx] += W * W;
272 }
273 }
274 }
275 Control.Init(0.0f, NumCtrlX * NumCtrlY);
276 for (int32 Idx = 0; Idx < Control.Num(); ++Idx)
277 {
278 if (Den[Idx] > KINDA_SMALL_NUMBER)
279 {
280 Control[Idx] = Num[Idx] / Den[Idx];
281 }
282 }
283 }
284 };
285
286} // namespace
287
288void AOpenDriveGenerator::FRoadSurfaceRaster::Initialize(const FBox &Bounds, float InCellSize)
289{
290 CellSize = FMath::Max(InCellSize, 50.0f);
291 OriginX = Bounds.Min.X;
292 OriginY = Bounds.Min.Y;
293 Width = FMath::Max(1, FMath::CeilToInt((Bounds.Max.X - Bounds.Min.X) / CellSize)) + 1;
294 Height = FMath::Max(1, FMath::CeilToInt((Bounds.Max.Y - Bounds.Min.Y) / CellSize)) + 1;
295 Cells.Empty();
296 Cells.SetNum(Width * Height);
297}
298
300 const FVector &A, const FVector &B, const FVector &C, bool bIsDrive)
301{
302 const float MinX = FMath::Min3(A.X, B.X, C.X);
303 const float MaxX = FMath::Max3(A.X, B.X, C.X);
304 const float MinY = FMath::Min3(A.Y, B.Y, C.Y);
305 const float MaxY = FMath::Max3(A.Y, B.Y, C.Y);
306 const int32 X0 = FMath::Clamp(FMath::FloorToInt((MinX - OriginX) / CellSize), 0, Width - 1);
307 const int32 X1 = FMath::Clamp(FMath::CeilToInt((MaxX - OriginX) / CellSize), 0, Width - 1);
308 const int32 Y0 = FMath::Clamp(FMath::FloorToInt((MinY - OriginY) / CellSize), 0, Height - 1);
309 const int32 Y1 = FMath::Clamp(FMath::CeilToInt((MaxY - OriginY) / CellSize), 0, Height - 1);
310
311 const float Denom = (B.Y - C.Y) * (A.X - C.X) + (C.X - B.X) * (A.Y - C.Y);
312 if (FMath::Abs(Denom) < KINDA_SMALL_NUMBER)
313 {
314 return; // degenerate in XY (vertical face); carries no surface footprint
315 }
316
317 for (int32 CY = Y0; CY <= Y1; ++CY)
318 {
319 for (int32 CX = X0; CX <= X1; ++CX)
320 {
321 const float PX = OriginX + (static_cast<float>(CX) + 0.5f) * CellSize;
322 const float PY = OriginY + (static_cast<float>(CY) + 0.5f) * CellSize;
323 float W0 = ((B.Y - C.Y) * (PX - C.X) + (C.X - B.X) * (PY - C.Y)) / Denom;
324 float W1 = ((C.Y - A.Y) * (PX - C.X) + (A.X - C.X) * (PY - C.Y)) / Denom;
325 float W2 = 1.0f - W0 - W1;
326 // Tolerance of ~1/8 cell in barycentric space keeps thin triangles
327 // whose interior never covers a cell center from leaving footprint
328 // holes along lane edges.
329 constexpr float Tolerance = -0.05f;
330 if (W0 < Tolerance || W1 < Tolerance || W2 < Tolerance)
331 {
332 continue;
333 }
334 // Genuine containment (no tolerance): the cell center is inside this
335 // triangle, so the cell really is covered by driving surface.
336 const bool bInside = (W0 >= 0.0f && W1 >= 0.0f && W2 >= 0.0f);
337 W0 = FMath::Max(W0, 0.0f);
338 W1 = FMath::Max(W1, 0.0f);
339 W2 = FMath::Max(W2, 0.0f);
340 const float WSum = W0 + W1 + W2;
341 const float Z = (W0 * A.Z + W1 * B.Z + W2 * C.Z) / FMath::Max(WSum, KINDA_SMALL_NUMBER);
342
343 FCell &Cell = Cells[CY * Width + CX];
344 if (!Cell.bPaved)
345 {
346 Cell.PavedMinZ = Z;
347 Cell.bPaved = 1;
348 }
349 else
350 {
351 Cell.PavedMinZ = FMath::Min(Cell.PavedMinZ, Z);
352 }
353 if (bIsDrive)
354 {
355 if (!Cell.bDrive)
356 {
357 Cell.DriveMinZ = Z;
358 Cell.DriveMaxZ = Z;
359 Cell.bDrive = 1;
360 }
361 else
362 {
363 Cell.DriveMinZ = FMath::Min(Cell.DriveMinZ, Z);
364 Cell.DriveMaxZ = FMath::Max(Cell.DriveMaxZ, Z);
365 }
366 if (bInside)
367 {
368 Cell.bDriveCovered = 1;
369 }
370 }
371 }
372 }
373}
374
376{
377 // Two-pass chamfer distance transform (3-4 weights scaled to cell size,
378 // ~4% max error -- fine for meter-scale clearances). Runs twice over the
379 // same storage: once seeded by the driving footprint (DistToDrive), once
380 // by any pavement (DistToPaved, also propagating the seed cell's
381 // PavedMinZ as NearestPavedMinZ for the terrain skirt blend).
382 const float Straight = CellSize;
383 const float Diagonal = CellSize * 1.41421356f;
384 const float Infinite = 1e12f;
385
386 const auto Chamfer = [&](auto GetSeed, auto GetDist, auto SetDist, auto GetZ, auto SetZ)
387 {
388 for (int32 Idx = 0; Idx < Cells.Num(); ++Idx)
389 {
390 if (GetSeed(Cells[Idx]))
391 {
392 SetDist(Cells[Idx], 0.0f);
393 SetZ(Cells[Idx], Cells[Idx].PavedMinZ);
394 }
395 else
396 {
397 SetDist(Cells[Idx], Infinite);
398 }
399 }
400 const auto Relax = [&](FCell &Cell, int32 NX, int32 NY, float Cost)
401 {
402 if (NX < 0 || NX >= Width || NY < 0 || NY >= Height)
403 {
404 return;
405 }
406 const FCell &N = Cells[NY * Width + NX];
407 if (GetDist(N) + Cost < GetDist(Cell))
408 {
409 SetDist(Cell, GetDist(N) + Cost);
410 SetZ(Cell, GetZ(N));
411 }
412 };
413 for (int32 CY = 0; CY < Height; ++CY)
414 {
415 for (int32 CX = 0; CX < Width; ++CX)
416 {
417 FCell &Cell = Cells[CY * Width + CX];
418 Relax(Cell, CX - 1, CY, Straight);
419 Relax(Cell, CX, CY - 1, Straight);
420 Relax(Cell, CX - 1, CY - 1, Diagonal);
421 Relax(Cell, CX + 1, CY - 1, Diagonal);
422 }
423 }
424 for (int32 CY = Height - 1; CY >= 0; --CY)
425 {
426 for (int32 CX = Width - 1; CX >= 0; --CX)
427 {
428 FCell &Cell = Cells[CY * Width + CX];
429 Relax(Cell, CX + 1, CY, Straight);
430 Relax(Cell, CX, CY + 1, Straight);
431 Relax(Cell, CX + 1, CY + 1, Diagonal);
432 Relax(Cell, CX - 1, CY + 1, Diagonal);
433 }
434 }
435 };
436
437 float ScratchZ = 0.0f;
438 Chamfer(
439 [](const FCell &C) { return C.bDrive != 0; },
440 [](const FCell &C) { return C.DistToDrive; },
441 [](FCell &C, float V) { C.DistToDrive = V; },
442 [&ScratchZ](const FCell &) { return ScratchZ; },
443 [](FCell &, float) {});
444 Chamfer(
445 [](const FCell &C) { return C.bPaved != 0; },
446 [](const FCell &C) { return C.DistToPaved; },
447 [](FCell &C, float V) { C.DistToPaved = V; },
448 [](const FCell &C) { return C.NearestPavedMinZ; },
449 [](FCell &C, float V) { C.NearestPavedMinZ = V; });
450}
451
452void AOpenDriveGenerator::DumpRasterAt(float X, float Y) const
453{
454 if (!RoadRaster.IsValid())
455 {
456 UE_LOG(LogCarla, Warning, TEXT("DumpRasterAt: raster not built"));
457 return;
458 }
459 const int32 CX0 = FMath::FloorToInt((X - RoadRaster.OriginX) / RoadRaster.CellSize);
460 const int32 CY0 = FMath::FloorToInt((Y - RoadRaster.OriginY) / RoadRaster.CellSize);
461 for (int32 DY = -1; DY <= 1; ++DY)
462 {
463 for (int32 DX = -1; DX <= 1; ++DX)
464 {
465 const int32 CX = CX0 + DX;
466 const int32 CY = CY0 + DY;
467 if (CX < 0 || CX >= RoadRaster.Width || CY < 0 || CY >= RoadRaster.Height)
468 {
469 continue;
470 }
471 const auto &Cell = RoadRaster.Cells[CY * RoadRaster.Width + CX];
472 UE_LOG(LogCarla, Log,
473 TEXT("cell(%d,%d) ctr(%.1f,%.1f) drive=%d min=%.1f max=%.1f"),
474 CX, CY,
475 RoadRaster.OriginX + (CX + 0.5f) * RoadRaster.CellSize,
476 RoadRaster.OriginY + (CY + 0.5f) * RoadRaster.CellSize,
477 Cell.bDrive, Cell.DriveMinZ, Cell.DriveMaxZ);
478 }
479 }
480}
481
484{
485 if (!IsValid())
486 {
487 return nullptr;
488 }
489 const int32 CX = FMath::FloorToInt((X - OriginX) / CellSize);
490 const int32 CY = FMath::FloorToInt((Y - OriginY) / CellSize);
491 if (CX < 0 || CX >= Width || CY < 0 || CY >= Height)
492 {
493 return nullptr;
494 }
495 return &Cells[CY * Width + CX];
496}
497
498/// Dump the road-raster chunk table around a point (client metres):
499/// `carla.DumpRasterAt <x> <y>` -- debugging aid for stacked-surface work.
500static FAutoConsoleCommandWithWorldAndArgs GDumpRasterAtCommand(
501 TEXT("carla.DumpRasterAt"),
502 TEXT("Dump road-raster chunk info around x y (metres)"),
503 FConsoleCommandWithWorldAndArgsDelegate::CreateStatic(
504 [](const TArray<FString> &Args, UWorld *World)
505 {
506 if (Args.Num() < 2)
507 {
508 UE_LOG(LogCarla, Warning, TEXT("usage: carla.DumpRasterAt <x> <y>"));
509 return;
510 }
511 const float CX = FCString::Atof(*Args[0]) * 100.0f;
512 const float CY = FCString::Atof(*Args[1]) * 100.0f;
513 for (TActorIterator<AOpenDriveGenerator> It(World); It; ++It)
514 {
515 It->DumpRasterAt(CX, CY);
516 return;
517 }
518 }));
519
520/// Re-run the generation QA on the placed generator from the console:
521/// `carla.MapGenQA` (also runs automatically after GenerateAll).
522static FAutoConsoleCommandWithWorld GMapGenQACommand(
523 TEXT("carla.MapGenQA"),
524 TEXT("Run the generated-map QA checks (road support, terrain invasion, objects on roadway)"),
525 FConsoleCommandWithWorldDelegate::CreateStatic([](UWorld *World)
526 {
527 for (TActorIterator<AOpenDriveGenerator> It(World); It; ++It)
528 {
529 It->RunGenerationQA();
530 return;
531 }
532 UE_LOG(LogCarla, Warning, TEXT("carla.MapGenQA: no AOpenDriveGenerator in world"));
533 }));
534
536{
537 PrimaryActorTick.bCanEverTick = false;
538 MeshComponent = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("RootComponent"));
539 RootComponent = MeshComponent;
540}
541
543{
544 PrimaryActorTick.bCanEverTick = false;
545 RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("RootComponent"));
546}
547
548AOpenDriveGenerator::AOpenDriveGenerator(const FObjectInitializer &ObjectInitializer)
549 : Super(ObjectInitializer)
550{
551 PrimaryActorTick.bCanEverTick = false;
552 RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("SceneComponent"));
553 SetRootComponent(RootComponent);
554 RootComponent->Mobility = EComponentMobility::Static;
555
556 // Non-partitioned (default bIsComponentPartitioned = false) so this works
557 // on OpenDriveMap.umap's legacy non-World-Partition level with no
558 // PCGWorldActor required. Generation is triggered explicitly from
559 // GeneratePoles() once all furniture anchors exist, not on load/runtime
560 // streaming -- see StreetFurnitureGraph's doc comment.
561 PCGComponent = CreateDefaultSubobject<UPCGComponent>(TEXT("PCGComponent"));
562 PCGBoundsComponent = CreateDefaultSubobject<UBoxComponent>(TEXT("PCGBoundsComponent"));
563 PCGBoundsComponent->SetupAttachment(RootComponent);
564 PCGBoundsComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
565 PCGBoundsComponent->SetGenerateOverlapEvents(false);
566}
567
568bool AOpenDriveGenerator::LoadOpenDrive(const FString &OpenDrive)
569{
570 using OpenDriveLoader = carla::opendrive::OpenDriveParser;
571
572 if (OpenDrive.IsEmpty())
573 {
574 UE_LOG(LogCarla, Error, TEXT("The OpenDrive is empty"));
575 return false;
576 }
577
578 OpenDriveData = OpenDrive;
579 return true;
580}
581
583{
584 return OpenDriveData;
585}
586
588{
589 return UCarlaStatics::GetGameMode(GetWorld())->GetMap().has_value();
590}
591
593{
594 if (!IsOpenDriveValid())
595 {
596 UE_LOG(LogCarla, Error, TEXT("The OpenDrive has not been loaded"));
597 return;
598 }
599
601 UCarlaGameInstance * GameInstance = UCarlaStatics::GetGameInstance(GetWorld());
602 if(GameInstance)
603 {
604 Parameters = GameInstance->GetOpendriveGenerationParameters();
605 }
606 else
607 {
608 carla::log_warning("Missing game instance");
609 }
610
611 auto& CarlaMap = UCarlaStatics::GetGameMode(GetWorld())->GetMap();
612 const auto Meshes = CarlaMap->GenerateChunkedMesh(Parameters);
613
614 UMaterialInterface* ResolvedRoadMaterial = RoadMaterial.LoadSynchronous();
615 UMaterialInterface* ResolvedSidewalkMaterial = SidewalkMaterial.LoadSynchronous();
616
617 // Reset so re-running GenerateRoadMesh (e.g. regenerating the map) doesn't
618 // leave GenerateGroundPlane sizing the fill heightfield off stale geometry.
619 RoadMeshBounds = FBox(ForceInit);
620
621 // Two-phase: first gather every material section (accumulating bounds --
622 // the road raster needs the full footprint extent before any triangle
623 // can be splatted), then rasterize + spawn.
624 TArray<TPair<FProceduralCustomMesh, bool>> PendingSections; // mesh, bIsSidewalk
625
626 for (const auto &Mesh : Meshes) {
627 if (!Mesh->GetVertices().size())
628 {
629 continue;
630 }
631
632 // Split the chunk into one actor per OpenDRIVE material tag ("road",
633 // "sidewalk") instead of dumping everything into a single actor's
634 // section 0 with no material assigned, which made every generated map
635 // render flat grey. This also gives semantic segmentation a single,
636 // unambiguous label per component to tag: CustomPrimitiveData (read by
637 // TagGeneratedComponent below) is set per UPrimitiveComponent, not per
638 // mesh section, so a chunk mixing road+sidewalk geometry in one
639 // component could only ever be tagged as one of the two. Ranges sharing
640 // the same material name (e.g. several lanes merged into one junction
641 // mesh) are combined before spawning, to keep the actor/draw-call count
642 // down.
643 const auto &MaterialRanges = Mesh->GetMaterials();
644 const bool bHasPerVertexUVs = Mesh->GetUVs().size() == Mesh->GetVertices().size();
645
646 TArray<FString> SectionOrder;
647 TMap<FString, FProceduralCustomMesh> SectionsByMaterial;
648
649 if (MaterialRanges.empty())
650 {
651 // No material tags on this chunk (shouldn't normally happen --
652 // MeshFactory always tags "road"/"sidewalk"); fall back to treating
653 // the whole chunk as "road" rather than dropping the geometry.
654 SectionOrder.Add(TEXT("road"));
655 SectionsByMaterial.Add(TEXT("road"), *Mesh);
656 }
657 else
658 {
659 for (const auto &MatRange : MaterialRanges)
660 {
661 FProceduralCustomMesh Extracted = ExtractMaterialRange(*Mesh, MatRange, bHasPerVertexUVs);
662 if (Extracted.Vertices.Num() == 0)
663 {
664 continue;
665 }
666 const FString MatName(MatRange.name.c_str());
667 if (FProceduralCustomMesh *Existing = SectionsByMaterial.Find(MatName))
668 {
669 AppendProceduralMesh(*Existing, Extracted);
670 }
671 else
672 {
673 SectionOrder.Add(MatName);
674 SectionsByMaterial.Add(MatName, MoveTemp(Extracted));
675 }
676 }
677 }
678
679 for (const FString &MatName : SectionOrder)
680 {
681 FProceduralCustomMesh &SectionMesh = SectionsByMaterial[MatName];
682 const bool bIsSidewalk = (MatName == TEXT("sidewalk"));
683 for (const FVector &Vertex : SectionMesh.Vertices)
684 {
685 RoadMeshBounds += Vertex;
686 }
687 PendingSections.Emplace(MoveTemp(SectionMesh), bIsSidewalk);
688 }
689 }
690
691 // Build the road-surface raster before spawning anything: every road and
692 // sidewalk triangle splatted with barycentric-interpolated heights, then
693 // the distance fields that terrain generation, furniture placement and
694 // the generation QA all query.
695 if (RoadMeshBounds.IsValid)
696 {
697 const float Pad = GroundPlanePadding + 2.0f * GroundSkirtWidth;
699 RoadMeshBounds.ExpandBy(FVector(Pad, Pad, 0.0f)),
701 for (const auto &Section : PendingSections)
702 {
703 const FProceduralCustomMesh &SectionMesh = Section.Key;
704 const bool bIsDrive = !Section.Value;
705 for (int32 T = 0; T + 2 < SectionMesh.Triangles.Num(); T += 3)
706 {
708 SectionMesh.Vertices[SectionMesh.Triangles[T]],
709 SectionMesh.Vertices[SectionMesh.Triangles[T + 1]],
710 SectionMesh.Vertices[SectionMesh.Triangles[T + 2]],
711 bIsDrive);
712 }
713 }
715 }
716
717 for (const auto &Section : PendingSections)
718 {
719 const FProceduralCustomMesh &SectionMesh = Section.Key;
720 const bool bIsSidewalk = Section.Value;
721
722 ACarlaProceduralMeshActor* TempActor = GetWorld()->SpawnActor<ACarlaProceduralMeshActor>();
723 UProceduralMeshComponent *TempPMC = TempActor->MeshComponent;
724 TempPMC->bUseAsyncCooking = true;
725 TempPMC->bUseComplexAsSimpleCollision = true;
726 TempPMC->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
727
728 TempPMC->CreateMeshSection_LinearColor(
729 0,
730 SectionMesh.Vertices,
731 SectionMesh.Triangles,
732 SectionMesh.Normals,
733 SectionMesh.UV0,
734 TArray<FLinearColor>(), // VertexColor
735 TArray<FProcMeshTangent>(), // Tangents
736 true); // Create collision
737
738 UMaterialInterface* SectionMaterial = bIsSidewalk ? ResolvedSidewalkMaterial : ResolvedRoadMaterial;
739 if (SectionMaterial)
740 {
741 TempPMC->SetMaterial(0, SectionMaterial);
742 }
743
744 TagGeneratedComponent(
745 *TempPMC,
746 TempActor->GetUniqueID(),
747 bIsSidewalk ? crp::CityObjectLabel::Sidewalks : crp::CityObjectLabel::Roads);
748
749 ActorMeshList.Add(TempActor);
750 }
751
752 if(!Parameters.enable_mesh_visibility)
753 {
754 for(AActor * actor : ActorMeshList)
755 {
756 actor->SetActorHiddenInGame(true);
757 }
758 }
759
760 // // Build collision data
761 // FTriMeshCollisionData CollisitonData;
762 // CollisitonData.bDeformableMesh = false;
763 // CollisitonData.bDisableActiveEdgePrecompute = false;
764 // CollisitonData.bFastCook = false;
765 // CollisitonData.bFlipNormals = false;
766 // CollisitonData.Indices = TriIndices;
767 // CollisitonData.Vertices = Vertices;
768
769 // RoadMesh->ContainsPhysicsTriMeshData(true);
770 // bool Success = RoadMesh->GetPhysicsTriMeshData(&CollisitonData, true);
771 // if (!Success)
772 // {
773 // UE_LOG(LogCarla, Error, TEXT("The road collision mesh could not be generated!"));
774 // }
775}
776
778{
779 if (!IsOpenDriveValid())
780 {
781 UE_LOG(LogCarla, Error, TEXT("The OpenDrive has not been loaded"));
782 return;
783 }
784
785 if (!RoadMeshBounds.IsValid)
786 {
787 // GenerateRoadMesh produced no geometry to size the heightfield against
788 // (not called yet, or the OpenDRIVE has no road mesh).
789 UE_LOG(LogCarla, Warning, TEXT("AOpenDriveGenerator: RoadMeshBounds is empty, skipping ground plane"));
790 return;
791 }
792
794 UCarlaGameInstance * GameInstance = UCarlaStatics::GetGameInstance(GetWorld());
795 if (GameInstance)
796 {
797 Parameters = GameInstance->GetOpendriveGenerationParameters();
798 }
799
800 // A heightfield grid spanning the padded road-network footprint, sampled
801 // from a C2-smooth terrain surface fit to the road heights. A flat quad
802 // at global min-z closed the blue-sky holes on CARLA's flat towns but
803 // left real-elevation OpenDRIVE roads (NuRec: Stuttgart z~293m, graded
804 // motorways) flying tens of meters above it; the follow-up piecewise-
805 // linear interpolation tracked elevation on average but couldn't follow
806 // vertical road curvature -- terrain bulged through the road in sag
807 // curves and dropped away under crests, and its weighted averaging
808 // between roads at different heights clipped the lower road.
809 const FBox PaddedBounds = RoadMeshBounds.ExpandBy(FVector(GroundPlanePadding, GroundPlanePadding, 0.0f));
810 const float MinX = PaddedBounds.Min.X;
811 const float MaxX = PaddedBounds.Max.X;
812 const float MinY = PaddedBounds.Min.Y;
813 const float MaxY = PaddedBounds.Max.Y;
814
815 // Terrain grid aligned 1:1 with the road raster, vertices at cell
816 // centers, so every paved cell owns exactly one terrain vertex and the
817 // footprint hug below cannot miss narrow strips between vertices (a 5m
818 // grid over a 2.5m raster left boundary cells keyed to a neighbouring
819 // road's blend height -- meters wrong near stacked roads). Decimated by
820 // an integer factor only if a very large map would blow the vertex
821 // budget.
822 constexpr int32 MaxGridVerticesPerAxis = 1024;
823 int32 Decimate = 1;
824 while (RoadRaster.Width / Decimate > MaxGridVerticesPerAxis ||
825 RoadRaster.Height / Decimate > MaxGridVerticesPerAxis)
826 {
827 ++Decimate;
828 }
829 const int32 NumX = FMath::Max(2, RoadRaster.Width / Decimate);
830 const int32 NumY = FMath::Max(2, RoadRaster.Height / Decimate);
831 const float StepX = RoadRaster.CellSize * static_cast<float>(Decimate);
832 const float StepY = StepX;
833 const float Vertex0X = RoadRaster.OriginX + 0.5f * RoadRaster.CellSize;
834 const float Vertex0Y = RoadRaster.OriginY + 0.5f * RoadRaster.CellSize;
835
836 if (!RoadRaster.IsValid())
837 {
838 UE_LOG(LogCarla, Warning, TEXT("AOpenDriveGenerator: road raster is empty, skipping ground plane"));
839 return;
840 }
841
842 // Scattered fit points for the free-terrain spline: one per paved raster
843 // cell, at the cell center, carrying the lowest surface z recorded there.
844 TArray<FVector> FitPoints;
845 for (int32 CY = 0; CY < RoadRaster.Height; ++CY)
846 {
847 for (int32 CX = 0; CX < RoadRaster.Width; ++CX)
848 {
849 const auto &Cell = RoadRaster.Cells[CY * RoadRaster.Width + CX];
850 if (Cell.bPaved)
851 {
852 FitPoints.Add(FVector(
853 RoadRaster.OriginX + (static_cast<float>(CX) + 0.5f) * RoadRaster.CellSize,
854 RoadRaster.OriginY + (static_cast<float>(CY) + 0.5f) * RoadRaster.CellSize,
855 Cell.PavedMinZ));
856 }
857 }
858 }
859 const float SampleCellSize = RoadRaster.CellSize;
860
861 // Multilevel B-spline approximation: start with a control lattice a few
862 // cells across the whole padded map (broad rolling shape, also the
863 // smooth extrapolation into the padding and across road-free voids),
864 // then repeatedly halve the control spacing, fitting each finer level to
865 // the residual the coarser ones left behind, until the control cells
866 // approach the road-sample resolution. The summed hierarchy is a
867 // C2-smooth surface that passes through the road elevations where there
868 // is road and rolls smoothly where there isn't.
869 TArray<FGroundBSplineLattice> SplineLevels;
870 const float ExtentX = MaxX - MinX;
871 const float ExtentY = MaxY - MinY;
872 float ControlSpacing = FMath::Max(ExtentX, ExtentY) / 4.0f;
873 const float FinestControlSpacing = FMath::Max(4.0f * SampleCellSize, StepX);
874 constexpr int32 MaxSplineLevels = 12;
875 TArray<FVector> Residuals = FitPoints;
876 while (SplineLevels.Num() < MaxSplineLevels)
877 {
878 FGroundBSplineLattice Level;
879 Level.OriginX = MinX;
880 Level.OriginY = MinY;
881 Level.Spacing = ControlSpacing;
882 Level.CellsX = FMath::Max(1, FMath::CeilToInt(ExtentX / ControlSpacing));
883 Level.CellsY = FMath::Max(1, FMath::CeilToInt(ExtentY / ControlSpacing));
884 Level.Fit(Residuals);
885 for (FVector &P : Residuals)
886 {
887 P.Z -= Level.Eval(P.X, P.Y);
888 }
889 SplineLevels.Add(MoveTemp(Level));
890 if (ControlSpacing <= FinestControlSpacing)
891 {
892 break;
893 }
894 ControlSpacing *= 0.5f;
895 }
896
897 const auto EvalSpline = [&SplineLevels](float X, float Y) -> float
898 {
899 float Z = 0.0f;
900 for (const FGroundBSplineLattice &Level : SplineLevels)
901 {
902 Z += Level.Eval(X, Y);
903 }
904 return Z;
905 };
906
907 // Evaluate the terrain at each grid vertex from the raster's distance
908 // field, by construction rather than by approximation:
909 // - inside the paved footprint: exactly GroundPlaneZOffset below the
910 // lowest surface there (the road demonstrably rests on the ground,
911 // and terrain can never rise through it);
912 // - within GroundSkirtWidth of pavement: blend from the nearest
913 // pavement's underside height out to the free spline surface, so
914 // shoulders stay attached instead of dropping away;
915 // - beyond the skirt: the free C2 spline surface.
916 const float SkirtWidth = FMath::Max(GroundSkirtWidth, GroundHeightSampleCellSize);
917 auto &CarlaMap = UCarlaStatics::GetGameMode(GetWorld())->GetMap();
918 TArray<float> Heights;
919 Heights.SetNumUninitialized(NumX * NumY);
920 for (int32 IY = 0; IY < NumY; ++IY)
921 {
922 const float Y = Vertex0Y + static_cast<float>(IY) * StepY;
923 for (int32 IX = 0; IX < NumX; ++IX)
924 {
925 const float X = Vertex0X + static_cast<float>(IX) * StepX;
926 float GroundZ;
927 const auto *Cell = RoadRaster.CellAtWorld(X, Y);
928 // Median / junction gap zone (cells annotated by GenerateMedianFill):
929 // loft the terrain flush to the surrounding road surface, so the
930 // strip between carriageways reads as level grass instead of a
931 // sunken pit. The two flanking carriageways are independent roads
932 // whose heights can genuinely differ several dm, so the loft must
933 // hug the NEAREST edge tightly and only blend mid-gap -- a gentle
934 // average left a shadow ledge along the higher road. Sharp
935 // inverse-distance^4 weighting over the genuinely-covered surface
936 // samples around the vertex does both.
937 float GapWeight = 0.0f;
938 float GapZSum = 0.0f;
939 bool bGapZone = false;
940 const int32 VCellX = FMath::FloorToInt((X - RoadRaster.OriginX) / RoadRaster.CellSize);
941 const int32 VCellY = FMath::FloorToInt((Y - RoadRaster.OriginY) / RoadRaster.CellSize);
942 for (int32 DY = -1; DY <= 1 && !bGapZone; ++DY)
943 {
944 for (int32 DX = -1; DX <= 1; ++DX)
945 {
946 const int32 NX = VCellX + DX;
947 const int32 NY = VCellY + DY;
948 if (NX >= 0 && NX < RoadRaster.Width && NY >= 0 && NY < RoadRaster.Height &&
949 RoadRaster.Cells[NY * RoadRaster.Width + NX].GapSurfaceZ != FLT_MAX)
950 {
951 bGapZone = true;
952 break;
953 }
954 }
955 }
956 if (bGapZone)
957 {
958 for (int32 DY = -3; DY <= 3; ++DY)
959 {
960 for (int32 DX = -3; DX <= 3; ++DX)
961 {
962 const int32 NX = VCellX + DX;
963 const int32 NY = VCellY + DY;
964 if (NX < 0 || NX >= RoadRaster.Width || NY < 0 || NY >= RoadRaster.Height)
965 {
966 continue;
967 }
968 const auto &N = RoadRaster.Cells[NY * RoadRaster.Width + NX];
969 if (!N.bDriveCovered)
970 {
971 continue;
972 }
973 const float CtrX = RoadRaster.OriginX + (NX + 0.5f) * RoadRaster.CellSize;
974 const float CtrY = RoadRaster.OriginY + (NY + 0.5f) * RoadRaster.CellSize;
975 const float D2 = (FMath::Square(X - CtrX) + FMath::Square(Y - CtrY))
976 / FMath::Square(RoadRaster.CellSize);
977 const float Weight = 1.0f / (D2 * D2 + 0.01f);
978 GapWeight += Weight;
979 GapZSum += Weight * N.DriveMinZ;
980 }
981 }
982 }
983 // The terrain shares space with two other surfaces near roads: the
984 // road mesh itself and the median-fill quads. The fill used to be
985 // flat per-cell plates, which forced a 3x (15cm) clearance to stop
986 // the 5m terrain triangles from z-fighting through them mid-gap --
987 // and that 15cm showed up as a dark ledge under every exposed road
988 // edge (junction islands read as sunken pits). The fill is lofted
989 // now and fill-adjacent vertices are clamped FillClearance below it
990 // explicitly (see below), so the terrain can hug the pavement at
991 // the plain offset and exposed edges sit nearly flush.
992 const float NearPavementClearance = GroundPlaneZOffset;
993 const float HoldDist = 300.0f;
994 bool bGapHandled = false;
995 if ((bGapZone || (Cell && Cell->bPaved)) && CarlaMap.has_value())
996 {
997 // Near-pavement vertices: the raster cannot resolve road edges at
998 // sub-cell precision -- a 2.5m cell straddling an edge mixes both
999 // flanking surfaces (or road and shoulder) in its min, and
1000 // hugging that min dug a shadow trench along medians and a ledge
1001 // along outer shoulders. Ask the road map itself: the closest
1002 // driving waypoint's transform gives the exact local surface
1003 // height of the nearest carriageway, and its lane width tells
1004 // whether this vertex is actually under the road at all.
1005 const FVector Probe(X, Y, Cell ? Cell->NearestPavedMinZ : 0.0f);
1006 const auto ClosestWp = CarlaMap->GetClosestWaypointOnRoad(carla::geom::Location(Probe));
1007 if (ClosestWp)
1008 {
1009 const FTransform LaneTransform = CarlaMap->ComputeTransform(*ClosestWp);
1010 const float LaneHalfWidth = 0.5f * 1e2f * static_cast<float>(CarlaMap->GetLaneWidth(*ClosestWp));
1011 const FVector LaneLoc = LaneTransform.GetLocation();
1012 const float Dist = FVector::Dist2D(Probe, LaneLoc);
1013 if (Dist < LaneHalfWidth + 60.0f)
1014 {
1015 // Actually under (or overhung by) the road: stay decisively
1016 // below the cell's lowest surface so the terrain can't poke
1017 // through a cross-sloped lane's low edge.
1018 GroundZ = ((Cell && Cell->bPaved) ? Cell->PavedMinZ
1019 : LaneLoc.Z)
1020 - NearPavementClearance;
1021 }
1022 else
1023 {
1024 // Between carriageways. "Closest waypoint" measures to the
1025 // LANE CENTER, ~2m inside the road, so a vertex hugging the
1026 // higher carriageway's edge can still be assigned the lower
1027 // road across the gap (a 2dm shadow trench along the median).
1028 // Blend between the two flanking edges instead: probe again
1029 // past the vertex, away from the first road, to find the
1030 // opposite carriageway, and interpolate by edge distance.
1031 const float EdgeDistA = FMath::Max(Dist - LaneHalfWidth, 1.0f);
1032 float ZA = LaneLoc.Z;
1033 float GapZ = ZA;
1034 FVector2D Away(X - LaneLoc.X, Y - LaneLoc.Y);
1035 if (Away.Normalize())
1036 {
1037 const FVector ProbeB(
1038 X + Away.X * 250.0f, Y + Away.Y * 250.0f, Probe.Z);
1039 const auto WpB = CarlaMap->GetClosestWaypointOnRoad(carla::geom::Location(ProbeB));
1040 if (WpB)
1041 {
1042 const FTransform LaneB = CarlaMap->ComputeTransform(*WpB);
1043 const float HalfB = 0.5f * 1e2f * static_cast<float>(CarlaMap->GetLaneWidth(*WpB));
1044 const float EdgeDistB = FMath::Max(
1045 FVector::Dist2D(Probe, LaneB.GetLocation()) - HalfB, 1.0f);
1046 const float Alpha = EdgeDistA / (EdgeDistA + EdgeDistB);
1047 GapZ = FMath::Lerp(ZA, LaneB.GetLocation().Z, Alpha);
1048 }
1049 }
1050 GroundZ = GapZ - GroundPlaneZOffset;
1051 }
1052 bGapHandled = true;
1053 }
1054 }
1055 if (bGapHandled)
1056 {
1057 // done
1058 }
1059 else if (GapWeight > 0.0f)
1060 {
1061 // Fallback loft when the map query fails: sharp inverse-distance
1062 // weighting hugs the nearest carriageway's samples.
1063 GroundZ = GapZSum / GapWeight - GroundPlaneZOffset;
1064 }
1065 else if (Cell && Cell->bPaved)
1066 {
1067 GroundZ = Cell->PavedMinZ - NearPavementClearance;
1068 }
1069 else if (Cell && Cell->DistToPaved >= 0.0f && Cell->DistToPaved < SkirtWidth)
1070 {
1071 const float Alpha = FMath::Clamp(
1072 (Cell->DistToPaved - HoldDist) / FMath::Max(SkirtWidth - HoldDist, 1.0f),
1073 0.0f, 1.0f);
1074 GroundZ = FMath::Lerp(
1075 Cell->NearestPavedMinZ - NearPavementClearance,
1076 EvalSpline(X, Y) - GroundPlaneZOffset,
1077 Alpha);
1078 }
1079 else
1080 {
1081 GroundZ = EvalSpline(X, Y) - GroundPlaneZOffset;
1082 }
1083 // Final invasion guards over the 3x3 raster neighbourhood:
1084 // - discontinuous pavement (a deck sitting >50cm lower: stacked
1085 // roads, embankment edges): drop below the lower surface or the
1086 // terrain triangle would cut up through it. Continuous
1087 // neighbours are exempt -- clamping to a laterally-lower
1088 // neighbour on an 8% grade would open a same-size gap under the
1089 // vertex's own cell, which is exactly what the QA support check
1090 // flags as floating road.
1091 constexpr float ContinuousDropTolerance = 50.0f;
1092 const float OwnPavedZ = (Cell && Cell->bPaved) ? Cell->PavedMinZ : FLT_MAX;
1093 const int32 CellX = FMath::FloorToInt((X - RoadRaster.OriginX) / RoadRaster.CellSize);
1094 const int32 CellY = FMath::FloorToInt((Y - RoadRaster.OriginY) / RoadRaster.CellSize);
1095 for (int32 DY = -1; DY <= 1; ++DY)
1096 {
1097 for (int32 DX = -1; DX <= 1; ++DX)
1098 {
1099 const int32 NX = CellX + DX;
1100 const int32 NY = CellY + DY;
1101 if (NX < 0 || NX >= RoadRaster.Width || NY < 0 || NY >= RoadRaster.Height)
1102 {
1103 continue;
1104 }
1105 const auto &Neighbour = RoadRaster.Cells[NY * RoadRaster.Width + NX];
1106 if (!Neighbour.bPaved)
1107 {
1108 continue;
1109 }
1110 const bool bContinuous =
1111 OwnPavedZ != FLT_MAX &&
1112 (OwnPavedZ - Neighbour.PavedMinZ) <= ContinuousDropTolerance;
1113 // Gap-handled vertices already track the flanking carriageway
1114 // edges exactly (waypoint blend); clamping them below every
1115 // neighbouring cell's min re-dug the median trench, because a
1116 // straddle cell's PavedMinZ mixes the LOWER road's samples.
1117 // Only a genuinely lower deck (well below the blend) still
1118 // clamps, so medians can never bury an underpass.
1119 const bool bGenuinelyLowerDeck =
1120 Neighbour.PavedMinZ < GroundZ - ContinuousDropTolerance;
1121 if (!bContinuous && (!bGapHandled || bGenuinelyLowerDeck))
1122 {
1123 GroundZ = FMath::Min(GroundZ, Neighbour.PavedMinZ - GroundPlaneZOffset);
1124 }
1125 }
1126 }
1127 Heights[IY * NumX + IX] = GroundZ;
1128 }
1129 }
1130
1131 // Retain the evaluated grid so GenerateFurnitureAnchors can re-ground
1132 // scattered objects onto the terrain surface they will stand on.
1133 GroundGridHeights = Heights;
1134 GroundGridNumX = NumX;
1135 GroundGridNumY = NumY;
1136 GroundGridOrigin = FVector2D(Vertex0X, Vertex0Y);
1137 GroundGridStepX = StepX;
1138 GroundGridStepY = StepY;
1139
1140 FProceduralCustomMesh MeshData;
1141 MeshData.Vertices.Reserve(NumX * NumY);
1142 MeshData.UV0.Reserve(NumX * NumY);
1143 const float UVTile = FMath::Max(GroundUVTileSize, 1.0f);
1144 for (int32 IY = 0; IY < NumY; ++IY)
1145 {
1146 const float Y = Vertex0Y + static_cast<float>(IY) * StepY;
1147 for (int32 IX = 0; IX < NumX; ++IX)
1148 {
1149 const float X = Vertex0X + static_cast<float>(IX) * StepX;
1150 MeshData.Vertices.Add(FVector(X, Y, Heights[IY * NumX + IX]));
1151 // World-space UV tiling (GroundUVTileSize cm per texture repeat)
1152 // instead of stretching one tile across the whole map.
1153 MeshData.UV0.Add(FVector2D((X - MinX) / UVTile, (Y - MinY) / UVTile));
1154 }
1155 }
1156
1157 // Per-vertex normals from the heightfield gradient (central differences,
1158 // one-sided at the borders) so lighting shades the slopes instead of
1159 // treating the whole ground as one flat plane.
1160 MeshData.Normals.Reserve(NumX * NumY);
1161 const auto VertexZ = [&](int32 IX, int32 IY) -> float
1162 {
1163 return MeshData.Vertices[IY * NumX + IX].Z;
1164 };
1165 for (int32 IY = 0; IY < NumY; ++IY)
1166 {
1167 for (int32 IX = 0; IX < NumX; ++IX)
1168 {
1169 const int32 XPrev = FMath::Max(IX - 1, 0);
1170 const int32 XNext = FMath::Min(IX + 1, NumX - 1);
1171 const int32 YPrev = FMath::Max(IY - 1, 0);
1172 const int32 YNext = FMath::Min(IY + 1, NumY - 1);
1173 const float DZDX = (VertexZ(XNext, IY) - VertexZ(XPrev, IY)) / (static_cast<float>(XNext - XPrev) * StepX);
1174 const float DZDY = (VertexZ(IX, YNext) - VertexZ(IX, YPrev)) / (static_cast<float>(YNext - YPrev) * StepY);
1175 MeshData.Normals.Add(FVector(-DZDX, -DZDY, 1.0f).GetSafeNormal());
1176 }
1177 }
1178
1179 // Winding per cell mirrors the previous single quad (corner order -X+Y,
1180 // +X+Y, +X-Y, -X-Y with triangles {0,1,3},{1,2,3}, same as the engine's
1181 // top-face quad in UKismetProceduralMeshLibrary::GenerateBoxMesh), so the
1182 // surface faces up rather than being backface-culled from above.
1183 MeshData.Triangles.Reserve((NumX - 1) * (NumY - 1) * 6);
1184 for (int32 IY = 0; IY + 1 < NumY; ++IY)
1185 {
1186 for (int32 IX = 0; IX + 1 < NumX; ++IX)
1187 {
1188 const int32 C0 = (IY + 1) * NumX + IX; // -X +Y
1189 const int32 C1 = (IY + 1) * NumX + IX + 1; // +X +Y
1190 const int32 C2 = IY * NumX + IX + 1; // +X -Y
1191 const int32 C3 = IY * NumX + IX; // -X -Y
1192 MeshData.Triangles.Append({ C0, C1, C3, C1, C2, C3 });
1193 }
1194 }
1195
1196 ACarlaProceduralMeshActor* TempActor = GetWorld()->SpawnActor<ACarlaProceduralMeshActor>();
1197 UProceduralMeshComponent *TempPMC = TempActor->MeshComponent;
1198 TempPMC->bUseAsyncCooking = true;
1199 TempPMC->bUseComplexAsSimpleCollision = true;
1200 TempPMC->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
1201
1202 TempPMC->CreateMeshSection_LinearColor(
1203 0,
1204 MeshData.Vertices,
1205 MeshData.Triangles,
1206 MeshData.Normals,
1207 MeshData.UV0,
1208 TArray<FLinearColor>(), // VertexColor
1209 TArray<FProcMeshTangent>(), // Tangents
1210 true); // Create collision
1211
1212 if (UMaterialInterface* ResolvedGroundMaterial = GroundMaterial.LoadSynchronous())
1213 {
1214 TempPMC->SetMaterial(0, ResolvedGroundMaterial);
1215 }
1216
1217 TagGeneratedComponent(*TempPMC, TempActor->GetUniqueID(), crp::CityObjectLabel::Terrain);
1218
1219 if (!Parameters.enable_mesh_visibility)
1220 {
1221 TempActor->SetActorHiddenInGame(true);
1222 }
1223
1224 ActorMeshList.Add(TempActor);
1225}
1226
1228{
1229 if (!RoadRaster.IsValid())
1230 {
1231 return;
1232 }
1233
1234 // Morphological closing of the driving footprint: dilate by K cells,
1235 // erode by K. Closes any gap narrower than ~2K cells regardless of
1236 // corridor orientation and produces a connected ribbon by construction.
1237 // (A previous per-axis flanking scan qualified cells alternately along
1238 // diagonal medians, perforating the fill into a checkerboard.)
1239 const int32 W = RoadRaster.Width;
1240 const int32 H = RoadRaster.Height;
1241 const int32 K = FMath::Max(1, FMath::CeilToInt(0.5f * MedianFillMaxWidth / RoadRaster.CellSize));
1242 TArray<uint8> Mask;
1243 Mask.SetNumZeroed(W * H);
1244 for (int32 Idx = 0; Idx < W * H; ++Idx)
1245 {
1246 // Strict coverage: cells that only grazing triangles claimed (bDrive
1247 // via the rasterizer's tolerance) are mostly bare and must count as
1248 // gap, or the ribbon skips them and leaves holes in the median.
1249 Mask[Idx] = RoadRaster.Cells[Idx].bDriveCovered ? 1 : 0;
1250 }
1251 const auto Morph = [&](uint8 Grow)
1252 {
1253 TArray<uint8> Out = Mask;
1254 for (int32 CY = 0; CY < H; ++CY)
1255 {
1256 for (int32 CX = 0; CX < W; ++CX)
1257 {
1258 if (Mask[CY * W + CX] == Grow)
1259 {
1260 continue;
1261 }
1262 for (int32 DY = -1; DY <= 1; ++DY)
1263 {
1264 for (int32 DX = -1; DX <= 1; ++DX)
1265 {
1266 const int32 NX = CX + DX;
1267 const int32 NY = CY + DY;
1268 if (NX >= 0 && NX < W && NY >= 0 && NY < H && Mask[NY * W + NX] == Grow)
1269 {
1270 Out[CY * W + CX] = Grow;
1271 DY = 2;
1272 break;
1273 }
1274 }
1275 }
1276 }
1277 }
1278 Mask = MoveTemp(Out);
1279 };
1280 for (int32 I = 0; I < K; ++I) { Morph(1); } // dilate
1281 for (int32 I = 0; I < K; ++I) { Morph(0); } // erode
1282
1283 // Reduce to the cells the closing *added* (the gap ribbon), then grow
1284 // that ribbon by one cell. The driving footprint's rasterized edge is
1285 // stairstepped on diagonals and the erosion bites back into it,
1286 // perforating the ribbon with alternating holes; one dilation of the
1287 // ribbon alone patches those notches (the overlap lands under the road
1288 // mesh or 1cm below it) without laying an apron around every outer road
1289 // edge the way an extra dilation of the whole footprint would.
1290 for (int32 Idx = 0; Idx < W * H; ++Idx)
1291 {
1292 Mask[Idx] = (Mask[Idx] && !RoadRaster.Cells[Idx].bDriveCovered) ? 1 : 0;
1293 }
1294 Morph(1);
1295
1296
1297 // Per-grid-node road-surface height, lofted from the surrounding
1298 // driving surface samples so the estimate follows the corridor's grade
1299 // and cross-slope across the gap.
1300 TMap<int32, float> NodeZCache;
1301 const auto NodeZ = [&](int32 NX, int32 NY) -> float
1302 {
1303 const int32 Key = NY * (W + 1) + NX;
1304 if (const float *Found = NodeZCache.Find(Key))
1305 {
1306 return *Found;
1307 }
1308 // 1) Cells genuinely covered by road that touch this node: hug the
1309 // lowest of their surface samples.
1310 float Result = FLT_MAX;
1311 float TouchMin = FLT_MAX;
1312 for (int32 DY = -1; DY <= 0; ++DY)
1313 {
1314 for (int32 DX = -1; DX <= 0; ++DX)
1315 {
1316 const int32 CX = NX + DX;
1317 const int32 CY = NY + DY;
1318 if (CX < 0 || CX >= W || CY < 0 || CY >= H)
1319 {
1320 continue;
1321 }
1322 const auto &N = RoadRaster.Cells[CY * W + CX];
1323 if (N.bDriveCovered)
1324 {
1325 TouchMin = FMath::Min(TouchMin, N.DriveMinZ);
1326 }
1327 }
1328 }
1329 if (TouchMin != FLT_MAX)
1330 {
1331 Result = TouchMin;
1332 }
1333 else
1334 {
1335 // 2) Inverse-distance-weighted loft over covered cells nearby: the
1336 // node sits mid-gap, interpolate the two road edges smoothly.
1337 float WeightSum = 0.0f;
1338 float ZSum = 0.0f;
1339 for (int32 DY = -3; DY <= 2; ++DY)
1340 {
1341 for (int32 DX = -3; DX <= 2; ++DX)
1342 {
1343 const int32 CX = NX + DX;
1344 const int32 CY = NY + DY;
1345 if (CX < 0 || CX >= W || CY < 0 || CY >= H)
1346 {
1347 continue;
1348 }
1349 const auto &N = RoadRaster.Cells[CY * W + CX];
1350 if (!N.bDriveCovered)
1351 {
1352 continue;
1353 }
1354 const float DCX = static_cast<float>(DX) + 0.5f;
1355 const float DCY = static_cast<float>(DY) + 0.5f;
1356 const float Weight = 1.0f / (DCX * DCX + DCY * DCY);
1357 WeightSum += Weight;
1358 ZSum += Weight * N.DriveMinZ;
1359 }
1360 }
1361 if (WeightSum > 0.0f)
1362 {
1363 Result = ZSum / WeightSum;
1364 }
1365 }
1366 NodeZCache.Add(Key, Result);
1367 return Result;
1368 };
1369
1370 int32 GapCells = 0;
1371 for (int32 CY = 0; CY < H; ++CY)
1372 {
1373 for (int32 CX = 0; CX < W; ++CX)
1374 {
1375 if (!Mask[CY * W + CX])
1376 {
1377 continue;
1378 }
1379 // Height guard: a gap under an overpass sees both decks. Two decks
1380 // land in the SAME cell (its DriveMinZ/DriveMaxZ split apart), so
1381 // test per cell -- a window-wide min/max spread also swallows the
1382 // corridor's own grade and skipped legitimate median cells on
1383 // sloped roads.
1384 bool bOverpass = false;
1385 for (int32 DY = -K - 1; DY <= K + 1 && !bOverpass; ++DY)
1386 {
1387 for (int32 DX = -K - 1; DX <= K + 1; ++DX)
1388 {
1389 const int32 NX = CX + DX;
1390 const int32 NY = CY + DY;
1391 if (NX < 0 || NX >= W || NY < 0 || NY >= H)
1392 {
1393 continue;
1394 }
1395 const auto &N = RoadRaster.Cells[NY * W + NX];
1396 if (N.bDrive && (N.DriveMaxZ - N.DriveMinZ) > MedianFillMaxHeightDelta)
1397 {
1398 bOverpass = true;
1399 break;
1400 }
1401 }
1402 }
1403 if (bOverpass)
1404 {
1405 continue;
1406 }
1407 // Lofted surface height at the cell center (mean of the four corner
1408 // nodes). GenerateGroundPlane raises the terrain flush to this, so
1409 // the median presents as grass level with the road edges instead of
1410 // asphalt quads (which hovered as floating plates wherever the gap
1411 // widened and the free terrain fell away underneath) or a sunken
1412 // pit (the old 15cm clearance ledge).
1413 const float Z00 = NodeZ(CX, CY);
1414 const float Z10 = NodeZ(CX + 1, CY);
1415 const float Z11 = NodeZ(CX + 1, CY + 1);
1416 const float Z01 = NodeZ(CX, CY + 1);
1417 if (Z00 == FLT_MAX || Z10 == FLT_MAX || Z11 == FLT_MAX || Z01 == FLT_MAX)
1418 {
1419 continue; // no driving surface anywhere near -- nothing to loft from
1420 }
1421 RoadRaster.Cells[CY * W + CX].GapSurfaceZ =
1422 0.25f * (Z00 + Z10 + Z11 + Z01);
1423 ++GapCells;
1424 }
1425 }
1426 UE_LOG(LogCarla, Log,
1427 TEXT("AOpenDriveGenerator: %d median gap cells lofted for the terrain"), GapCells);
1428}
1429
1431{
1432 if (!IsOpenDriveValid())
1433 {
1434 UE_LOG(LogCarla, Error, TEXT("The OpenDrive has not been loaded"));
1435 return;
1436 }
1437
1439 UCarlaGameInstance * GameInstance = UCarlaStatics::GetGameInstance(GetWorld());
1440 if (GameInstance)
1441 {
1442 Parameters = GameInstance->GetOpendriveGenerationParameters();
1443 }
1444
1445 // Zebra bars built from the crosswalk outline polygons instead of one
1446 // texture-mapped cover slab. The only crosswalk-ish texture in the
1447 // content is the speed-bump set (red field + stripe band), which rendered
1448 // the slabs as pink; real bar geometry with the same clean white
1449 // marking material the lane markings use needs no texture at all and
1450 // scales to any crosswalk size. The road surface below stays visible
1451 // between the bars, exactly like the painted original.
1452 auto& CarlaMap = UCarlaStatics::GetGameMode(GetWorld())->GetMap();
1453 const std::vector<carla::geom::Location> Zones = CarlaMap->GetAllCrosswalkZones();
1454 if (Zones.empty())
1455 {
1456 return;
1457 }
1458
1459 constexpr float BarWidth = 0.4f; // m, along the crossing direction
1460 constexpr float BarGap = 0.4f; // m, between bars
1461 constexpr float EdgeMargin = 0.1f; // m, kept clear along the bar ends
1462
1463 FProceduralCustomMesh MeshData;
1464
1465 auto AddZebraPolygon = [&MeshData](const std::vector<carla::geom::Location> &Poly, float ZOffsetCm)
1466 {
1467 if (Poly.size() < 3)
1468 {
1469 return;
1470 }
1471 // Local frame: U along the longest edge (the crossing direction), V
1472 // across it (the crosswalk's depth along the road).
1473 size_t Longest = 0;
1474 float LongestLen2 = -1.0f;
1475 for (size_t k = 0; k < Poly.size(); ++k)
1476 {
1477 const auto &P = Poly[k];
1478 const auto &Q = Poly[(k + 1) % Poly.size()];
1479 const float DX = Q.x - P.x, DY = Q.y - P.y;
1480 const float Len2 = DX * DX + DY * DY;
1481 if (Len2 > LongestLen2)
1482 {
1483 LongestLen2 = Len2;
1484 Longest = k;
1485 }
1486 }
1487 if (LongestLen2 < 1e-4f)
1488 {
1489 return;
1490 }
1491 const float InvLen = 1.0f / FMath::Sqrt(LongestLen2);
1492 const FVector2D U((Poly[(Longest + 1) % Poly.size()].x - Poly[Longest].x) * InvLen,
1493 (Poly[(Longest + 1) % Poly.size()].y - Poly[Longest].y) * InvLen);
1494 const FVector2D V(-U.Y, U.X);
1495
1496 struct FLocalPt { float A, B, Z; };
1497 TArray<FLocalPt> Ring;
1498 float AMin = FLT_MAX, AMax = -FLT_MAX, BMin = FLT_MAX, BMax = -FLT_MAX;
1499 for (const auto &P : Poly)
1500 {
1501 const float A = P.x * U.X + P.y * U.Y;
1502 const float B = P.x * V.X + P.y * V.Y;
1503 Ring.Add({A, B, P.z});
1504 AMin = FMath::Min(AMin, A); AMax = FMath::Max(AMax, A);
1505 BMin = FMath::Min(BMin, B); BMax = FMath::Max(BMax, B);
1506 }
1507
1508 // Surface height at a local point: barycentric over the outline's
1509 // triangle fan (the corners already carry the road elevation at their
1510 // own s -- see Map::GetAllCrosswalkZones); nearest corner as fallback.
1511 auto SurfaceZ = [&Ring](float A, float B) -> float
1512 {
1513 for (int32 K = 1; K + 1 < Ring.Num(); ++K)
1514 {
1515 const FLocalPt &P0 = Ring[0], &P1 = Ring[K], &P2 = Ring[K + 1];
1516 const float Denom = (P1.B - P2.B) * (P0.A - P2.A) + (P2.A - P1.A) * (P0.B - P2.B);
1517 if (FMath::Abs(Denom) < 1e-9f)
1518 {
1519 continue;
1520 }
1521 const float W0 = ((P1.B - P2.B) * (A - P2.A) + (P2.A - P1.A) * (B - P2.B)) / Denom;
1522 const float W1 = ((P2.B - P0.B) * (A - P2.A) + (P0.A - P2.A) * (B - P2.B)) / Denom;
1523 const float W2 = 1.0f - W0 - W1;
1524 if (W0 >= -1e-3f && W1 >= -1e-3f && W2 >= -1e-3f)
1525 {
1526 return W0 * P0.Z + W1 * P1.Z + W2 * P2.Z;
1527 }
1528 }
1529 float Best = FLT_MAX, Z = Ring[0].Z;
1530 for (const FLocalPt &P : Ring)
1531 {
1532 const float D = FMath::Square(P.A - A) + FMath::Square(P.B - B);
1533 if (D < Best) { Best = D; Z = P.Z; }
1534 }
1535 return Z;
1536 };
1537
1538 // Sutherland-Hodgman clip of one bar rectangle against the outline
1539 // (crosswalk outlines are rectangles, so convex clipping is exact).
1540 auto ClipBar = [&Ring](TArray<FVector2D> Subject) -> TArray<FVector2D>
1541 {
1542 for (int32 K = 0; K < Ring.Num(); ++K)
1543 {
1544 const FVector2D E0(Ring[K].A, Ring[K].B);
1545 const FVector2D E1(Ring[(K + 1) % Ring.Num()].A, Ring[(K + 1) % Ring.Num()].B);
1546 const FVector2D Edge = E1 - E0;
1547 // Ring orientation is unknown; use the ring centroid to pick the
1548 // inward side of each edge.
1549 FVector2D Centroid(0.0f, 0.0f);
1550 for (const FLocalPt &P : Ring) { Centroid += FVector2D(P.A, P.B); }
1551 Centroid /= static_cast<float>(Ring.Num());
1552 const float CentroidSide = Edge.X * (Centroid.Y - E0.Y) - Edge.Y * (Centroid.X - E0.X);
1553 const float Sign = CentroidSide >= 0.0f ? 1.0f : -1.0f;
1554 TArray<FVector2D> Out;
1555 for (int32 I = 0; I < Subject.Num(); ++I)
1556 {
1557 const FVector2D &P = Subject[I];
1558 const FVector2D &Q = Subject[(I + 1) % Subject.Num()];
1559 const float SideP = Sign * (Edge.X * (P.Y - E0.Y) - Edge.Y * (P.X - E0.X));
1560 const float SideQ = Sign * (Edge.X * (Q.Y - E0.Y) - Edge.Y * (Q.X - E0.X));
1561 if (SideP >= 0.0f)
1562 {
1563 Out.Add(P);
1564 }
1565 if ((SideP >= 0.0f) != (SideQ >= 0.0f))
1566 {
1567 const float T = SideP / (SideP - SideQ);
1568 Out.Add(P + T * (Q - P));
1569 }
1570 }
1571 Subject = MoveTemp(Out);
1572 if (Subject.Num() < 3)
1573 {
1574 return {};
1575 }
1576 }
1577 return Subject;
1578 };
1579
1580 const float Extent = AMax - AMin;
1581 const int32 NumBars = FMath::Max(1, FMath::FloorToInt((Extent - BarWidth) / (BarWidth + BarGap)) + 1);
1582 const float Start = AMin + 0.5f * (Extent - (NumBars * (BarWidth + BarGap) - BarGap));
1583 for (int32 Bar = 0; Bar < NumBars; ++Bar)
1584 {
1585 const float A0 = Start + Bar * (BarWidth + BarGap);
1586 const float A1 = A0 + BarWidth;
1587 TArray<FVector2D> Quad;
1588 Quad.Add(FVector2D(A0, BMin + EdgeMargin));
1589 Quad.Add(FVector2D(A1, BMin + EdgeMargin));
1590 Quad.Add(FVector2D(A1, BMax - EdgeMargin));
1591 Quad.Add(FVector2D(A0, BMax - EdgeMargin));
1592 TArray<FVector2D> Clipped = ClipBar(MoveTemp(Quad));
1593 if (Clipped.Num() < 3)
1594 {
1595 continue;
1596 }
1597 const int32 First = MeshData.Vertices.Num();
1598 for (const FVector2D &L : Clipped)
1599 {
1600 const float X = L.X * U.X + L.Y * V.X;
1601 const float Y = L.X * U.Y + L.Y * V.Y;
1602 const float Z = SurfaceZ(L.X, L.Y);
1603 MeshData.Vertices.Add(FVector(1e2f * X, 1e2f * Y, 1e2f * Z + ZOffsetCm));
1604 MeshData.Normals.Add(FVector::UpVector);
1605 MeshData.UV0.Add(FVector2D((L.X - A0) / BarWidth, (L.Y - BMin) / FMath::Max(BMax - BMin, 0.1f)));
1606 }
1607 for (int32 K = 1; K + 1 < Clipped.Num(); ++K)
1608 {
1609 // Same winding convention as the converted LibCarla meshes
1610 // (flip the last two indices for Unreal's left-handed frame).
1611 MeshData.Triangles.Add(First);
1612 MeshData.Triangles.Add(First + K + 1);
1613 MeshData.Triangles.Add(First + K);
1614 }
1615 }
1616 };
1617
1618 // Split the flat vertex list into outline polygons: each polygon ends
1619 // when its first vertex repeats (same convention as GetAllCrosswalkMesh).
1620 std::vector<carla::geom::Location> Current;
1621 size_t StartIdx = 0;
1622 for (size_t i = 0; i < Zones.size(); ++i)
1623 {
1624 if (i != StartIdx && Zones[StartIdx] == Zones[i])
1625 {
1626 AddZebraPolygon(Current, DecalZOffset);
1627 Current.clear();
1628 if (i >= Zones.size() - 1)
1629 {
1630 break;
1631 }
1632 StartIdx = i + 1;
1633 continue;
1634 }
1635 Current.push_back(Zones[i]);
1636 }
1637
1638 if (MeshData.Vertices.Num() == 0)
1639 {
1640 return;
1641 }
1642
1643 ACarlaProceduralMeshActor* TempActor = GetWorld()->SpawnActor<ACarlaProceduralMeshActor>();
1644 UProceduralMeshComponent *TempPMC = TempActor->MeshComponent;
1645 TempPMC->bUseAsyncCooking = true;
1646 TempPMC->bUseComplexAsSimpleCollision = true;
1647 // Markings are paint: the road mesh below carries the physics. Thin bar
1648 // meshes with collision would only add pointless micro-ledges.
1649 TempPMC->SetCollisionEnabled(ECollisionEnabled::NoCollision);
1650
1651 TempPMC->CreateMeshSection_LinearColor(
1652 0,
1653 MeshData.Vertices,
1654 MeshData.Triangles,
1655 MeshData.Normals,
1656 MeshData.UV0,
1657 TArray<FLinearColor>(), // VertexColor
1658 TArray<FProcMeshTangent>(), // Tangents
1659 false); // no collision
1660
1661 if (UMaterialInterface* ResolvedCrosswalkMaterial = CrosswalkMaterial.LoadSynchronous())
1662 {
1663 TempPMC->SetMaterial(0, ResolvedCrosswalkMaterial);
1664 }
1665
1666 // CARLA has no dedicated CityObjectLabel for crosswalks; the ue4 tooling
1667 // (CarlaTools OpenDriveToMap) tags road markings as RoadLines, mirrored here.
1668 TagGeneratedComponent(*TempPMC, TempActor->GetUniqueID(), crp::CityObjectLabel::RoadLines);
1669
1670 if (!Parameters.enable_mesh_visibility)
1671 {
1672 TempActor->SetActorHiddenInGame(true);
1673 }
1674
1675 ActorMeshList.Add(TempActor);
1676}
1677
1679{
1680 if (!IsOpenDriveValid())
1681 {
1682 UE_LOG(LogCarla, Error, TEXT("The OpenDrive has not been loaded"));
1683 return;
1684 }
1685
1687 UCarlaGameInstance * GameInstance = UCarlaStatics::GetGameInstance(GetWorld());
1688 if (GameInstance)
1689 {
1690 Parameters = GameInstance->GetOpendriveGenerationParameters();
1691 }
1692 else
1693 {
1694 carla::log_warning("Missing game instance");
1695 }
1696
1697 auto& CarlaMap = UCarlaStatics::GetGameMode(GetWorld())->GetMap();
1698
1699 // Map::GenerateLineMarkings filters roads by a min/max bounding box; it
1700 // backs the tiled/streamed generation path (GenerateOrderedChunkedMeshInLocations).
1701 // generate_opendrive_world builds the whole map in one shot, so pass an
1702 // effectively unbounded box instead. Map::FilterRoadsByPosition expects
1703 // minpos.y > maxpos.y (Unreal's left-handed Y vs. OpenDRIVE), mirrored here.
1704 constexpr float BoundsExtentMeters = 1e6f;
1705 const carla::geom::Vector3D MinPos(-BoundsExtentMeters, BoundsExtentMeters, -BoundsExtentMeters);
1706 const carla::geom::Vector3D MaxPos(BoundsExtentMeters, -BoundsExtentMeters, BoundsExtentMeters);
1707
1708 std::vector<std::string> MarkInfo;
1709 const auto Markings = CarlaMap->GenerateLineMarkings(Parameters, MinPos, MaxPos, MarkInfo);
1710
1711 UMaterialInterface* ResolvedWhite = LaneMarkingWhiteMaterial.LoadSynchronous();
1712 UMaterialInterface* ResolvedYellow = LaneMarkingYellowMaterial.LoadSynchronous();
1713
1714 size_t Index = 0;
1715 for (const auto &Mark : Markings)
1716 {
1717 // Map::GenerateLineMarkings/MeshFactory::GenerateLaneMarkForRoad push one
1718 // color tag per lane pass but only push a mesh when it produced valid
1719 // geometry, so MarkInfo and Markings can fall out of sync (matches
1720 // CarlaTools OpenDriveToMap::GenerateLaneMarks, which assumes the same
1721 // 1:1 index). Guard the lookup so a mismatch can't read out of bounds.
1722 const bool bIsYellow = (Index < MarkInfo.size()) &&
1723 (MarkInfo[Index].find("yellow") != std::string::npos);
1724 ++Index;
1725
1726 if (!Mark || !Mark->GetVertices().size())
1727 {
1728 continue;
1729 }
1730
1731 ACarlaProceduralMeshActor* TempActor = GetWorld()->SpawnActor<ACarlaProceduralMeshActor>();
1732 UProceduralMeshComponent *TempPMC = TempActor->MeshComponent;
1733 TempPMC->SetCollisionEnabled(ECollisionEnabled::NoCollision);
1734
1735 FProceduralCustomMesh MeshData = *Mark;
1736 for (FVector &Vertex : MeshData.Vertices)
1737 {
1738 // Small z-offset above the road surface to avoid z-fighting.
1739 Vertex.Z += DecalZOffset;
1740 }
1741
1742 TempPMC->CreateMeshSection_LinearColor(
1743 0,
1744 MeshData.Vertices,
1745 MeshData.Triangles,
1746 MeshData.Normals,
1747 MeshData.UV0,
1748 TArray<FLinearColor>(), // VertexColor
1749 TArray<FProcMeshTangent>(), // Tangents
1750 false); // Create collision -- flush with the road, no separate collision needed
1751
1752 UMaterialInterface* SectionMaterial = bIsYellow ? ResolvedYellow : ResolvedWhite;
1753 if (SectionMaterial)
1754 {
1755 TempPMC->SetMaterial(0, SectionMaterial);
1756 }
1757
1758 TagGeneratedComponent(*TempPMC, TempActor->GetUniqueID(), crp::CityObjectLabel::RoadLines);
1759
1760 if (!Parameters.enable_mesh_visibility)
1761 {
1762 TempActor->SetActorHiddenInGame(true);
1763 }
1764
1765 ActorMeshList.Add(TempActor);
1766 }
1767}
1768
1770{
1771 const float FX = FMath::Clamp(
1772 (X - static_cast<float>(GroundGridOrigin.X)) / FMath::Max(GroundGridStepX, 1.0f),
1773 0.0f, static_cast<float>(GroundGridNumX - 1) - KINDA_SMALL_NUMBER);
1774 const float FY = FMath::Clamp(
1775 (Y - static_cast<float>(GroundGridOrigin.Y)) / FMath::Max(GroundGridStepY, 1.0f),
1776 0.0f, static_cast<float>(GroundGridNumY - 1) - KINDA_SMALL_NUMBER);
1777 const int32 IX = FMath::Min(FMath::FloorToInt(FX), GroundGridNumX - 2);
1778 const int32 IY = FMath::Min(FMath::FloorToInt(FY), GroundGridNumY - 2);
1779 const float S = FX - static_cast<float>(IX);
1780 const float T = FY - static_cast<float>(IY);
1781 const float Z00 = GroundGridHeights[IY * GroundGridNumX + IX];
1782 const float Z10 = GroundGridHeights[IY * GroundGridNumX + IX + 1];
1783 const float Z01 = GroundGridHeights[(IY + 1) * GroundGridNumX + IX];
1784 const float Z11 = GroundGridHeights[(IY + 1) * GroundGridNumX + IX + 1];
1785 return FMath::Lerp(FMath::Lerp(Z00, Z10, S), FMath::Lerp(Z01, Z11, S), T);
1786}
1787
1789 const FName &Tag, float Spacing, float Offset, float RoadClearance,
1790 bool bMeasureFromCurb, bool bKeepOnSidewalk, float SOffset)
1791{
1792 auto& CarlaMap = UCarlaStatics::GetGameMode(GetWorld())->GetMap();
1793
1794 // Map::GetTreesTransform filters roads by a min/max bounding box (same
1795 // pattern as GenerateLineMarkings/GenerateLaneMarkings); generate_opendrive_world
1796 // builds the whole map in one shot, so pass an effectively unbounded box
1797 // instead of a real tile. Map::FilterRoadsByPosition expects
1798 // minpos.y > maxpos.y (Unreal's left-handed Y vs. OpenDRIVE), mirrored here.
1799 constexpr float BoundsExtentMeters = 1e6f;
1800 const carla::geom::Vector3D MinPos(-BoundsExtentMeters, BoundsExtentMeters, -BoundsExtentMeters);
1801 const carla::geom::Vector3D MaxPos(BoundsExtentMeters, -BoundsExtentMeters, BoundsExtentMeters);
1802
1803 // Reused as-is from the editor-only OpenDriveToMap tool's tree placement
1804 // (CarlaTools/OpenDriveToMap.cpp:478-506, GenerateMiscActors): it already
1805 // walks each non-junction road's outermost driving lane at regular
1806 // s-intervals and offsets outward past it -- exactly the "spacing along
1807 // road, offset past the sidewalk" placement street furniture needs, with
1808 // zero new LibCarla code. The second element of each pair is a road-type
1809 // string (from RoadInfoSpeed, e.g. "Town"/"Highway"/"Rural"), unused here
1810 // but available on the anchor's tags if the PCG graph wants to vary
1811 // furniture density by road type later.
1812 // bMeasureFromCurb: Offset is taken from the outer edge of the last
1813 // roadway lane (past parking/bike/border lanes) instead of the driving
1814 // lane, and bKeepOnSidewalk clamps it to the middle of the sidewalk lane
1815 // that follows, so the anchor stands on the pavement rather than in the
1816 // roadway (parking lane in between) or in the buildings (no lane in
1817 // between and a narrow sidewalk).
1818 const auto Anchors = CarlaMap->GetTreesTransform(
1819 MinPos, MaxPos, Spacing, Offset, SOffset, bMeasureFromCurb, bKeepOnSidewalk);
1820
1821 int32 NumSpawned = 0;
1822 int32 NumFilteredOnRoad = 0;
1823
1824 for (const auto &AnchorPair : Anchors)
1825 {
1826 const FTransform AnchorTransform = AnchorPair.first;
1827 FVector Location = AnchorTransform.GetLocation();
1828
1829 // Road-occupancy filter: GetTreesTransform offsets each anchor a fixed
1830 // lateral distance past its own road's outermost driving lane, which is
1831 // wrong wherever roads widen, merge, overlap or fan into junctions --
1832 // the anchor can land on the roadway of *another* lane/road and the
1833 // spawned furniture then blocks traffic. The anchor is a point but the
1834 // content it spawns is not, so the required clearance is the content's
1835 // bounding-box half-extent (per category), measured against the
1836 // rasterized driving footprint -- the actual mesh, junction fans and
1837 // widenings included, not a centerline heuristic.
1838 // The raster is 2.5 m-celled and marks a cell as roadway as soon as
1839 // its centre is: an anchor that sits 0.5-1 m onto the pavement is in a
1840 // "road" cell about half the time, so curb-measured anchors (which are
1841 // placed on exact lane geometry) rely on the exact centreline test
1842 // below instead of this coarse gate.
1843 const auto *Cell = RoadRaster.CellAtWorld(Location.X, Location.Y);
1844 if (!bMeasureFromCurb && Cell && Cell->DistToDrive >= 0.0f && Cell->DistToDrive < RoadClearance)
1845 {
1846 ++NumFilteredOnRoad;
1847 continue;
1848 }
1849 // Belt-and-braces centerline check for lanes whose mesh is missing
1850 // from the raster (some imported maps skip generating a driving
1851 // lane's geometry; the lane is still drivable).
1852 const auto ClosestWp = CarlaMap->GetClosestWaypointOnRoad(carla::geom::Location(Location));
1853 if (ClosestWp)
1854 {
1855 const FTransform LaneTransform = CarlaMap->ComputeTransform(*ClosestWp);
1856 const float LaneHalfWidth = 0.5f * 1e2f * static_cast<float>(CarlaMap->GetLaneWidth(*ClosestWp));
1857 if (FVector::Dist2D(Location, LaneTransform.GetLocation()) < LaneHalfWidth + RoadClearance)
1858 {
1859 ++NumFilteredOnRoad;
1860 continue;
1861 }
1862 }
1863
1864 // Re-ground: the anchor inherits the road-edge elevation from
1865 // GetTreesTransform, but it stands offset onto pavement or terrain
1866 // that is generally lower (ground offset plus lateral grade), leaving
1867 // spawned furniture floating. If the anchor's raster cell saw road/
1868 // sidewalk geometry, snap to that surface; otherwise drop it onto the
1869 // ground heightfield generated by GenerateGroundPlane.
1870 if (Cell && Cell->bPaved)
1871 {
1872 Location.Z = Cell->PavedMinZ;
1873 }
1874 else if (GroundGridNumX > 1 && GroundGridNumY > 1)
1875 {
1876 Location.Z = SampleGroundGridHeight(Location.X, Location.Y);
1877 }
1878
1879 AProceduralFurnitureAnchor* Anchor = GetWorld()->SpawnActor<AProceduralFurnitureAnchor>(
1880 Location, AnchorTransform.Rotator());
1881 Anchor->Tags.Add(Tag);
1882 FurnitureAnchorBounds += Location;
1883 ActorMeshList.Add(Anchor);
1884 ++NumSpawned;
1885 }
1886
1887 if (NumFilteredOnRoad > 0)
1888 {
1889 UE_LOG(LogCarla, Log, TEXT("AOpenDriveGenerator: %s: discarded %d/%d anchors too close to a driving lane"),
1890 *Tag.ToString(), NumFilteredOnRoad, static_cast<int32>(Anchors.size()));
1891 }
1892
1893 return NumSpawned;
1894}
1895
1897{
1898 if (!IsOpenDriveValid())
1899 {
1900 UE_LOG(LogCarla, Error, TEXT("The OpenDrive has not been loaded"));
1901 return;
1902 }
1903
1904 if (CVarOpenDriveStreetFurniture.GetValueOnGameThread() == 0)
1905 {
1906 UE_LOG(LogCarla, Log, TEXT("AOpenDriveGenerator: carla.OpenDrive.StreetFurniture=0, skipping street furniture"));
1907 return;
1908 }
1909
1910 // Lamps and signage stand on the pavement (measured from the curb,
1911 // clamped onto the sidewalk); vegetation keeps the legacy driving-edge
1912 // offset so trees stay in the verge/yard past the sidewalk. Signage is
1913 // phased half a lamp spacing along s so a sign never lands on a lamp.
1914 const int32 NumLampAnchors = GenerateFurnitureAnchors(
1916 const int32 NumVegetationAnchors = GenerateFurnitureAnchors(
1918 const int32 NumSignageAnchors = GenerateFurnitureAnchors(
1920 const int32 TotalAnchors = NumLampAnchors + NumVegetationAnchors + NumSignageAnchors;
1921 UE_LOG(LogCarla, Log, TEXT("AOpenDriveGenerator: furniture anchors spawned: %d lamp, %d vegetation, %d signage"),
1922 NumLampAnchors, NumVegetationAnchors, NumSignageAnchors);
1923
1924 if (TotalAnchors == 0)
1925 {
1926 return;
1927 }
1928
1929 // Trigger the runtime PCG scatter now that every anchor exists. The graph
1930 // itself does the actual spawning (lamp/vegetation/signage actors),
1931 // sampling the tagged anchors above via "Get Actor Data" nodes -- see
1932 // StreetFurnitureGraph's doc comment in the header for the expected
1933 // graph layout. PCGComponent is non-partitioned, so this call works with
1934 // no PCGWorldActor / World Partition involved.
1935 if (UPCGGraphInterface* Graph = StreetFurnitureGraph.LoadSynchronous())
1936 {
1937 // PCG refuses to schedule with invalid owner bounds (this actor has no
1938 // visible geometry), so size the bounds box to the anchor cloud first.
1939 if (FurnitureAnchorBounds.IsValid)
1940 {
1941 const FBox Padded = FurnitureAnchorBounds.ExpandBy(2000.0f);
1942 PCGBoundsComponent->SetWorldLocation(Padded.GetCenter());
1943 PCGBoundsComponent->SetBoxExtent(Padded.GetExtent());
1944 PCGBoundsComponent->UpdateBounds();
1945 }
1946 PCGComponent->SetGraph(Graph);
1948 PCGComponent->Generate(true);
1949 }
1950 else
1951 {
1952 UE_LOG(LogCarla, Warning, TEXT("AOpenDriveGenerator: StreetFurnitureGraph is not ")
1953 TEXT("set, skipping PCG street-furniture scatter (%d lamp/vegetation/signage ")
1954 TEXT("anchors were still spawned and are ready to sample once a graph is assigned)."),
1955 TotalAnchors);
1956 }
1957}
1958
1960{
1961 return PCGComponent != nullptr && PCGComponent->IsGenerating();
1962}
1963
1965{
1966 if (!IsOpenDriveValid())
1967 {
1968 UE_LOG(LogCarla, Error, TEXT("The OpenDrive has not been loaded"));
1969 return;
1970 }
1971 auto& CarlaMap = UCarlaStatics::GetGameMode(GetWorld())->GetMap();
1972 const auto Waypoints = CarlaMap->GenerateWaypointsOnRoadEntries();
1973 for (const auto &Wp : Waypoints)
1974 {
1975 const FTransform Trans = CarlaMap->ComputeTransform(Wp);
1976 AVehicleSpawnPoint *Spawner = GetWorld()->SpawnActor<AVehicleSpawnPoint>();
1977 Spawner->SetActorRotation(Trans.GetRotation());
1978 Spawner->SetActorLocation(Trans.GetTranslation() + FVector(0.f, 0.f, SpawnersHeight));
1979 VehicleSpawners.Add(Spawner);
1980 }
1981}
1982
1984{
1991 GeneratePoles();
1992
1993 // Deferred so the asynchronous PCG scatter has spawned its content
1994 // before the object-placement pass measures it.
1995 FTimerHandle QATimerHandle;
1996 GetWorldTimerManager().SetTimer(
1997 QATimerHandle, this, &AOpenDriveGenerator::RunGenerationQA, 3.0f, false);
1998}
1999
2001{
2002 if (!RoadRaster.IsValid() || GroundGridNumX < 2 || GroundGridNumY < 2)
2003 {
2004 UE_LOG(LogCarla, Warning, TEXT("MapGenQA: no generated map data to check"));
2005 return;
2006 }
2007
2008 constexpr int32 MaxReportedLocations = 50;
2009 const auto AppendLoc = [](FString &Out, int32 &Count, float X, float Y, float Z, const TCHAR *Extra = nullptr)
2010 {
2011 if (Count >= MaxReportedLocations)
2012 {
2013 return;
2014 }
2015 if (!Out.IsEmpty())
2016 {
2017 Out += TEXT(",");
2018 }
2019 // Meters, CARLA client frame, directly usable to move the spectator.
2020 Out += FString::Printf(TEXT("{\"x\":%.1f,\"y\":%.1f,\"z\":%.1f%s%s}"),
2021 X / 100.0f, Y / 100.0f, Z / 100.0f,
2022 Extra ? TEXT(",") : TEXT(""), Extra ? Extra : TEXT(""));
2023 ++Count;
2024 };
2025
2026 // --- A) road support / terrain invasion: every paved raster cell ---
2027 int32 PavedCells = 0;
2028 int32 FloatingCells = 0;
2029 int32 InvadedCells = 0;
2030 int32 StackedCells = 0;
2031 int32 BridgeEdgeCells = 0;
2032 float WorstGap = 0.0f;
2033 float WorstInvasion = 0.0f;
2034 FString FloatingLocs, InvadedLocs;
2035 int32 FloatingLocCount = 0, InvadedLocCount = 0;
2036 for (int32 CY = 0; CY < RoadRaster.Height; ++CY)
2037 {
2038 for (int32 CX = 0; CX < RoadRaster.Width; ++CX)
2039 {
2040 const auto &Cell = RoadRaster.Cells[CY * RoadRaster.Width + CX];
2041 if (!Cell.bPaved)
2042 {
2043 continue;
2044 }
2045 ++PavedCells;
2046 if (Cell.bDrive && (Cell.DriveMaxZ - Cell.DriveMinZ) > QAStackedRoadGap)
2047 {
2048 ++StackedCells; // upper deck is a bridge; only the lower is checked
2049 }
2050 // A cell whose 3x3 neighbourhood contains pavement a deck lower is a
2051 // bridge/embankment edge: one terrain surface cannot simultaneously
2052 // support this deck and stay below the lower one, so a support gap
2053 // here is structural (wants bridge skirt geometry), not a terrain
2054 // defect -- tally it separately from genuine floating.
2055 bool bBridgeEdge = false;
2056 bool bBoundary = false;
2057 for (int32 DY = -1; DY <= 1; ++DY)
2058 {
2059 for (int32 DX = -1; DX <= 1; ++DX)
2060 {
2061 const int32 NX = CX + DX;
2062 const int32 NY = CY + DY;
2063 if (NX < 0 || NX >= RoadRaster.Width || NY < 0 || NY >= RoadRaster.Height)
2064 {
2065 bBoundary = true;
2066 continue;
2067 }
2068 const auto &Neighbour = RoadRaster.Cells[NY * RoadRaster.Width + NX];
2069 if (!Neighbour.bPaved)
2070 {
2071 bBoundary = true;
2072 }
2073 else if ((Cell.PavedMinZ - Neighbour.PavedMinZ) > 50.0f)
2074 {
2075 bBridgeEdge = true;
2076 }
2077 }
2078 }
2079 const float X = RoadRaster.OriginX + (static_cast<float>(CX) + 0.5f) * RoadRaster.CellSize;
2080 const float Y = RoadRaster.OriginY + (static_cast<float>(CY) + 0.5f) * RoadRaster.CellSize;
2081 const float Gap = Cell.PavedMinZ - SampleGroundGridHeight(X, Y);
2082 if (Gap > QASupportGapMax)
2083 {
2084 if (bBridgeEdge)
2085 {
2086 ++BridgeEdgeCells;
2087 }
2088 else if (bBoundary)
2089 {
2090 // Only footprint-boundary cells can show visible daylight under a
2091 // road edge. Interior cells sit under an intact road mesh and the
2092 // terrain deliberately holds below the pavement -- a deep
2093 // interior gap there is construction, not a defect.
2094 ++FloatingCells;
2095 WorstGap = FMath::Max(WorstGap, Gap);
2096 AppendLoc(FloatingLocs, FloatingLocCount, X, Y, Cell.PavedMinZ);
2097 }
2098 }
2099 else if (Gap < -1.0f)
2100 {
2101 ++InvadedCells;
2102 WorstInvasion = FMath::Max(WorstInvasion, -Gap);
2103 AppendLoc(InvadedLocs, InvadedLocCount, X, Y, Cell.PavedMinZ);
2104 }
2105 }
2106 }
2107
2108 // --- B) objects on the roadway: BB footprints vs driving footprint ---
2109 int32 ObjectsChecked = 0;
2110 int32 ObjectViolations = 0;
2111 FString ObjectLocs;
2112 int32 ObjectLocCount = 0;
2113 // Pivot-based test: an object stands *in* the roadway when its support
2114 // point (tree trunk, lamp pole, shelter structure -- the placed
2115 // transform's origin) is on or within a meter of the driving footprint
2116 // at road level. Bounding boxes are deliberately not used for the
2117 // verdict: a lamp arm or tree canopy legitimately overhangs the lanes
2118 // meters above traffic, and its BB reaches the ground via the off-road
2119 // pole/trunk, which made every mature roadside tree a false positive.
2120 const auto PivotBlocksRoad = [&](const FVector &Pivot) -> bool
2121 {
2122 const auto *Cell = RoadRaster.CellAtWorld(Pivot.X, Pivot.Y);
2123 if (!Cell)
2124 {
2125 return false;
2126 }
2127 const bool bOnFootprint =
2128 Cell->bDrive || (Cell->DistToDrive >= 0.0f && Cell->DistToDrive < 100.0f);
2129 // z gate: only content at road level counts -- furniture on a deck
2130 // above or below an overpass shares XY with the lanes legitimately.
2131 return bOnFootprint && FMath::Abs(Pivot.Z - Cell->NearestPavedMinZ) < 500.0f;
2132 };
2133 const auto IsExemptOwner = [](const AActor *Owner) -> bool
2134 {
2135 // Roads/terrain/markings are this generator's own output (identified
2136 // by component tags below); traffic signals legitimately stand at the
2137 // roadside per the OpenDRIVE data; spawn points and pawns are not
2138 // scenery. The sky sphere's bounds span the whole map -- size-gated.
2139 return Owner == nullptr
2140 || Owner->IsA<ATrafficSignBase>()
2141 || Owner->IsA<AVehicleSpawnPoint>()
2142 || Owner->IsA<APawn>();
2143 };
2144 for (TObjectIterator<UStaticMeshComponent> It; It; ++It)
2145 {
2146 UStaticMeshComponent *Comp = *It;
2147 if (!IsValid(Comp) || Comp->GetWorld() != GetWorld() || !Comp->GetStaticMesh())
2148 {
2149 continue;
2150 }
2151 if (IsExemptOwner(Comp->GetOwner()))
2152 {
2153 continue;
2154 }
2155 const auto CheckOnePivot = [&](const FVector &Pivot)
2156 {
2157 ++ObjectsChecked;
2158 if (PivotBlocksRoad(Pivot))
2159 {
2160 ++ObjectViolations;
2161 const FString Extra = FString::Printf(TEXT("\"class\":\"%s\""),
2162 *Comp->GetOwner()->GetClass()->GetName());
2163 AppendLoc(ObjectLocs, ObjectLocCount, Pivot.X, Pivot.Y, Pivot.Z, *Extra);
2164 }
2165 };
2166 if (const UInstancedStaticMeshComponent *ISM = Cast<UInstancedStaticMeshComponent>(Comp))
2167 {
2168 for (int32 Inst = 0; Inst < ISM->GetInstanceCount(); ++Inst)
2169 {
2170 FTransform InstTransform;
2171 if (ISM->GetInstanceTransform(Inst, InstTransform, /*bWorldSpace=*/true))
2172 {
2173 CheckOnePivot(InstTransform.GetLocation());
2174 }
2175 }
2176 }
2177 else
2178 {
2179 // Size gate: skip map-spanning geometry (sky sphere & co).
2180 if (Comp->Bounds.GetBox().GetExtent().X > 50000.0f ||
2181 Comp->Bounds.GetBox().GetExtent().Y > 50000.0f)
2182 {
2183 continue;
2184 }
2185 CheckOnePivot(Comp->GetComponentLocation());
2186 }
2187 }
2188
2189 UE_LOG(LogCarla, Log,
2190 TEXT("MapGenQA: paved cells %d (stacked %d, bridge-edge %d) | floating %d (worst gap %.0fcm) | ")
2191 TEXT("terrain invasion %d (worst %.0fcm) | objects checked %d, on roadway %d"),
2192 PavedCells, StackedCells, BridgeEdgeCells, FloatingCells, WorstGap,
2193 InvadedCells, WorstInvasion, ObjectsChecked, ObjectViolations);
2194
2195 const FString Report = FString::Printf(
2196 TEXT("{\n")
2197 TEXT(" \"support\": {\"paved_cells\": %d, \"stacked_cells\": %d, \"bridge_edge_cells\": %d, ")
2198 TEXT("\"floating_cells\": %d, \"worst_gap_cm\": %.1f, \"floating_locations\": [%s]},\n")
2199 TEXT(" \"invasion\": {\"cells\": %d, \"worst_cm\": %.1f, \"locations\": [%s]},\n")
2200 TEXT(" \"objects\": {\"checked\": %d, \"on_roadway\": %d, \"items\": [%s]}\n")
2201 TEXT("}\n"),
2202 PavedCells, StackedCells, BridgeEdgeCells, FloatingCells, WorstGap, *FloatingLocs,
2203 InvadedCells, WorstInvasion, *InvadedLocs,
2204 ObjectsChecked, ObjectViolations, *ObjectLocs);
2205 const FString ReportPath = FPaths::ProjectSavedDir() / TEXT("mapgen_qa.json");
2206 FFileHelper::SaveStringToFile(Report, *ReportPath);
2207 UE_LOG(LogCarla, Log, TEXT("MapGenQA: report written to %s"), *ReportPath);
2208}
2209
2211{
2212 Super::BeginPlay();
2213
2214 const FString XodrContent = UOpenDrive::GetXODR(GetWorld());
2215 LoadOpenDrive(XodrContent);
2216
2217 GenerateAll();
2218
2219 auto World = GetWorld();
2220 check(World != nullptr);
2221
2222 // Autogenerate signals
2223 AActor* TrafficLightManagerActor = UGameplayStatics::GetActorOfClass(World, ATrafficLightManager::StaticClass());
2224 if(TrafficLightManagerActor == nullptr) {
2225 World->SpawnActor<ATrafficLightManager>();
2226 }
2227}
static TAutoConsoleVariable< int32 > CVarOpenDriveStreetFurniture(TEXT("carla.OpenDrive.StreetFurniture"), 1, TEXT("1 (default): scatter street furniture (lamps, vegetation, signage) on generated OpenDRIVE worlds. 0: bare road only."), ECVF_Default)
static FAutoConsoleCommandWithWorld GMapGenQACommand(TEXT("carla.MapGenQA"), TEXT("Run the generated-map QA checks (road support, terrain invasion, objects on roadway)"), FConsoleCommandWithWorldDelegate::CreateStatic([](UWorld *World) { for(TActorIterator< AOpenDriveGenerator > It(World);It;++It) { It->RunGenerationQA();return;} UE_LOG(LogCarla, Warning, TEXT("carla.MapGenQA: no AOpenDriveGenerator in world"));}))
Re-run the generation QA on the placed generator from the console: carla.MapGenQA (also runs automati...
static FAutoConsoleCommandWithWorldAndArgs GDumpRasterAtCommand(TEXT("carla.DumpRasterAt"), TEXT("Dump road-raster chunk info around x y (metres)"), FConsoleCommandWithWorldAndArgsDelegate::CreateStatic([](const TArray< FString > &Args, UWorld *World) { if(Args.Num()< 2) { UE_LOG(LogCarla, Warning, TEXT("usage: carla.DumpRasterAt <x> <y>"));return;} const float CX=FCString::Atof(*Args[0]) *100.0f;const float CY=FCString::Atof(*Args[1]) *100.0f;for(TActorIterator< AOpenDriveGenerator > It(World);It;++It) { It->DumpRasterAt(CX, CY);return;} }))
Dump the road-raster chunk table around a point (client metres): carla.DumpRasterAt <x> <y> – debuggi...
static bool IsValid(const ACarlaWheeledVehicle *Vehicle)
const std::optional< carla::road::Map > & GetMap() const
TObjectPtr< UProceduralMeshComponent > MeshComponent
bool LoadOpenDrive(const FString &OpenDrive)
Set the OpenDRIVE information as string and generates the queryable map structure.
TSoftObjectPtr< UMaterialInterface > LaneMarkingWhiteMaterial
Material applied to white lane-marking meshes.
FBox RoadMeshBounds
World-space bounding box of every road/sidewalk vertex generated by GenerateRoadMesh,...
const FString & GetOpenDrive() const
Get the OpenDRIVE information as string.
void RunGenerationQA()
Systematic post-generation validation, run automatically shortly after GenerateAll (delayed so the as...
float MedianFillMaxHeightDelta
Max height split (cm) of the driving surface samples WITHIN a single raster cell near a gap cell for ...
AOpenDriveGenerator(const FObjectInitializer &ObjectInitializer)
float GroundUVTileSize
World-space size (cm) of one UV tile on the ground plane, i.e.
void GenerateGroundPlane()
Generates a terrain-following heightfield beneath the generated road network so gaps in the road/side...
void GeneratePoles()
Scatters street furniture (lamp poles, roadside vegetation, signage) along the generated road network...
FBox FurnitureAnchorBounds
Accumulated bounding box of every furniture anchor spawned for the current generation,...
TArray< float > GroundGridHeights
Evaluated ground heightfield (final terrain surface z per grid vertex, row-major, GroundGridNumY rows...
FRoadSurfaceRaster RoadRaster
TSoftObjectPtr< UMaterialInterface > GroundMaterial
Material applied to the ground plane (see GenerateGroundPlane).
void DumpRasterAt(float X, float Y) const
Debug: log the raster's weld/height record in a 3x3 cell block around (X, Y) in UE cm.
FName LampAnchorTag
Tag used to mark lamp-pole anchor points for the PCG graph to sample.
void GenerateRoadMesh()
Generates the road and sidewalk mesh based on the OpenDRIVE information.
TObjectPtr< UBoxComponent > PCGBoundsComponent
Provides the PCG component's generation bounds: PCG derives its grid bounds from the owner actor's co...
float SignageAnchorOffset
Lateral offset (m) of signage anchors past the curb line, clamped onto the sidewalk like the lamps (s...
float GroundSkirtWidth
Width (cm) of the blend band past the road footprint over which the terrain transitions from exactly ...
float LampAnchorSpacing
Spacing (m) between lamp anchors along each road.
float LampAnchorOffset
Lateral offset (m) of lamp anchors past the curb line (the outer edge of the last roadway lane: parki...
FName VegetationAnchorTag
Tag used to mark roadside-vegetation anchor points.
TSoftObjectPtr< UMaterialInterface > SidewalkMaterial
Material applied to sidewalk mesh sections.
void GenerateLaneMarkings()
Generates the lane-marking meshes (white/yellow) based on the OpenDRIVE information.
float SampleGroundGridHeight(float X, float Y) const
Bilinear interpolation of GroundGridHeights at a world XY position.
TObjectPtr< UPCGComponent > PCGComponent
Drives the runtime PCG scatter (see GeneratePoles).
float DecalZOffset
Vertical offset (cm) applied to crosswalk and lane-marking meshes above the road surface,...
float VegetationAnchorSpacing
Spacing (m) between vegetation anchors along each road.
int32 GenerateFurnitureAnchors(const FName &Tag, float Spacing, float Offset, float RoadClearance, bool bMeasureFromCurb, bool bKeepOnSidewalk, float SOffset)
Spawns one AProceduralFurnitureAnchor per transform returned by Map::GetTreesTransform for the given ...
int32 StreetFurnitureSeed
PCG seed of the furniture scatter; with it, every server and run places the same furniture and banner...
void GenerateCrosswalkMesh()
Generates the crosswalk mesh based on the OpenDRIVE information.
TSoftObjectPtr< UPCGGraphInterface > StreetFurnitureGraph
PCG graph asset that samples the tagged anchors below (via a "Get Actor Data" node per category,...
float LampRoadClearance
Per-category clearance (cm) beyond the lane half-width that street furniture must keep from the neare...
virtual void BeginPlay() override
float QAStackedRoadGap
Min z spread (cm) within one sample cell that marks it as stacked roads (bridge over road) rather tha...
FName SignageAnchorTag
Tag used to mark decorative-signage anchor points.
void GenerateMedianFill()
Annotates the narrow unmapped strips between parallel carriageway roads so the terrain lofts flush ac...
float MedianFillMaxWidth
Widest gap (cm) between opposing driving surfaces that GenerateMedianFill closes with road surface; w...
TArray< TObjectPtr< AVehicleSpawnPoint > > VehicleSpawners
float QASupportGapMax
Max allowed gap (cm) between a road cell's lowest surface and the terrain below it before the cell co...
float SpawnersHeight
Determine the height where the spawners will be placed, relative to each RoutePlanner.
TSoftObjectPtr< UMaterialInterface > RoadMaterial
Material applied to driving-lane mesh sections.
TSoftObjectPtr< UMaterialInterface > CrosswalkMaterial
Material applied to the zebra-bar crosswalk meshes.
float GroundPlaneZOffset
Vertical offset (cm) applied below the road surface height where the terrain runs under road geometry...
bool IsStreetFurnitureGenerating() const
True while the street-furniture PCG scatter is still spawning; it finishes over the next frames.
float GroundHeightSampleCellSize
XY cell size (cm) of RoadRaster.
void GenerateSpawnPoints()
Generates spawn points along the road.
bool IsOpenDriveValid() const
Checks if the OpenDrive has been loaded and it's valid.
float GroundPlanePadding
How far (cm) the ground plane extends past the generated road/sidewalk bounding box on each side,...
float VegetationAnchorOffset
Lateral offset (m) of vegetation anchors – set past the lamp row so poles and trees don't overlap.
float SignageAnchorSpacing
Spacing (m) between signage anchors along each road.
TSoftObjectPtr< UMaterialInterface > LaneMarkingYellowMaterial
Material applied to yellow lane-marking meshes (centre line).
TArray< TObjectPtr< AActor > > ActorMeshList
Invisible marker placed along a generated road to anchor PCG-scattered street furniture (lamps,...
static FString GetTagAsString(crp::CityObjectLabel Tag)
Transform a CityObjectLabel into a string.
Definition Tagger.cpp:269
static void SetStencilValue(UPrimitiveComponent &Component, const uint32 ActorID, const crp::CityObjectLabel &Label, const bool bSetRenderCustomDepth)
Definition Tagger.cpp:63
Class In charge of creating and assigning traffic light groups, controllers and components.
Base class for spawner locations for walkers.
The game instance contains elements that must be kept alive in between levels.
const carla::rpc::OpendriveGenerationParameters & GetOpendriveGenerationParameters() const
static UCarlaGameInstance * GetGameInstance(const UObject *WorldContextObject)
static ACarlaGameModeBase * GetGameMode(const UObject *WorldContextObject)
static FString GetXODR(const UWorld *World)
Return the OpenDrive XML associated to MapName, or empty if the file is not found.
Mesh data container, validator and exporter.
Definition Mesh.h:44
const std::vector< uv_type > & GetUVs() const
Definition Mesh.cpp:334
const std::vector< index_type > & GetIndexes() const
Definition Mesh.cpp:323
const std::vector< material_type > & GetMaterials() const
Definition Mesh.cpp:338
const std::vector< vertex_type > & GetVertices() const
Definition Mesh.cpp:307
static void log_warning(Args &&... args)
Definition Logging.h:96
uint8 bDriveCovered
Like bDrive but WITHOUT the rasterizer's negative barycentric tolerance: set only when the cell cente...
const FCell * CellAtWorld(float X, float Y) const
void Initialize(const FBox &Bounds, float InCellSize)
void BuildDistanceFields()
Two-pass chamfer distance transform filling DistToDrive/DistToPaved and NearestPavedMinZ.
void RasterizeTriangle(const FVector &A, const FVector &B, const FVector &C, bool bIsDrive)
Splats one triangle of road ("drive") or sidewalk surface into the grid: conservative 2D rasterizatio...
A definition of a Carla Mesh.
TArray< FVector2D > UV0
Material that references the vertex index start and end of a mesh where it is affecting.
Definition Mesh.h:25
Seting for map generation from opendrive without additional geometry.