CARLA
Loading...
Searching...
No Matches
OpenDriveGenerator.h
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#pragma once
7
9
11#include "carla/road/Map.h"
13
14#include <util/ue-header-guard-begin.h>
15#include "CoreMinimal.h"
16#include "GameFramework/Actor.h"
17#include "ProceduralMeshComponent.h"
18#include <util/ue-header-guard-end.h>
19
20#include <optional>
21
22#include "OpenDriveGenerator.generated.h"
23
24class UMaterialInterface;
25class UBoxComponent;
26class UPCGComponent;
27class UPCGGraphInterface;
28
29UCLASS()
30class CARLA_API ACarlaProceduralMeshActor : public AActor
31{
32 GENERATED_BODY()
33public:
35
36 UPROPERTY(Category = "Procedural Mesh Actor", VisibleDefaultsOnly, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
37 TObjectPtr<UProceduralMeshComponent> MeshComponent;
38};
39
40/// Invisible marker placed along a generated road to anchor PCG-scattered
41/// street furniture (lamps, signage, vegetation). Carries no mesh of its
42/// own -- the PCG graph selects these by tag (see
43/// AOpenDriveGenerator::GeneratePoles) via a "Get Actor Data" node and
44/// spawns the actual content at each one. Does nothing on its own, same
45/// spirit as AWalkerAIController's server-side stub.
46UCLASS()
47class CARLA_API AProceduralFurnitureAnchor : public AActor
48{
49 GENERATED_BODY()
50public:
52};
53
54UCLASS()
55class CARLA_API AOpenDriveGenerator : public AActor
56{
57 GENERATED_BODY()
58
59public:
60
61 AOpenDriveGenerator(const FObjectInitializer &ObjectInitializer);
62
63 /// Set the OpenDRIVE information as string and generates the
64 /// queryable map structure.
65 bool LoadOpenDrive(const FString &OpenDrive);
66
67 /// Get the OpenDRIVE information as string.
68 const FString &GetOpenDrive() const;
69
70 /// Checks if the OpenDrive has been loaded and it's valid.
71 bool IsOpenDriveValid() const;
72
73 /// Generates the road and sidewalk mesh based on the OpenDRIVE information.
74 void GenerateRoadMesh();
75
76 /// Generates a terrain-following heightfield beneath the generated road
77 /// network so gaps in the road/sidewalk mesh (roundabout centers, area
78 /// beyond the shoulders, etc.) don't show blue sky through the ground.
79 /// The terrain surface is a multilevel B-spline approximation (Lee,
80 /// Wolberg & Shin 1997) fit to the road height samples in
81 /// GroundHeightSampleGrid: a coarse-to-fine hierarchy of uniform cubic
82 /// B-spline lattices, each level fit to the residual left by the previous
83 /// one, giving a C2-smooth surface that follows the road network's
84 /// vertical curvature instead of a piecewise-linear patchwork that pokes
85 /// through sag curves and drops away under crests. Near roads the surface
86 /// is additionally clamped to GroundPlaneZOffset below the lowest nearby
87 /// road sample so smoothing can never push terrain up through the road.
88 /// Must run after GenerateRoadMesh(), which is what populates
89 /// RoadMeshBounds and GroundHeightSampleGrid.
90 void GenerateGroundPlane();
91
92 /// Annotates the narrow unmapped strips between parallel carriageway
93 /// roads so the terrain lofts flush across them. Real-world OpenDRIVE
94 /// exports (NuRec DeepMap) model each direction of a dual carriageway
95 /// as a separate road and leave the median/gore strip between them
96 /// unmapped -- the sunken terrain showed through as a pit below road
97 /// level and read as broken road surface (and paving the strip with
98 /// asphalt quads instead left floating plates wherever the gap widened
99 /// and the free terrain fell away underneath). Finds gap cells via a
100 /// morphological closing of the genuinely-covered driving footprint
101 /// (within MedianFillMaxWidth, rejecting overpass gaps) and writes a
102 /// lofted road-surface height per cell (FCell::GapSurfaceZ);
103 /// GenerateGroundPlane raises the grass flush to it. Must run after
104 /// GenerateRoadMesh and before GenerateGroundPlane.
105 void GenerateMedianFill();
106
107 /// Generates the crosswalk mesh based on the OpenDRIVE information.
108 void GenerateCrosswalkMesh();
109
110 /// Generates the lane-marking meshes (white/yellow) based on the
111 /// OpenDRIVE information.
112 void GenerateLaneMarkings();
113
114 /// Scatters street furniture (lamp poles, roadside vegetation, signage)
115 /// along the generated road network. Emits tagged AProceduralFurnitureAnchor
116 /// markers via Map::GetTreesTransform (reused as-is: it already walks each
117 /// non-junction road's outermost driving lane at regular s-intervals and
118 /// offsets outward past the sidewalk -- exactly what furniture placement
119 /// needs, no new LibCarla code required), then triggers the runtime PCG
120 /// graph (see PCGComponent/StreetFurnitureGraph) that samples them by tag
121 /// and spawns the actual content.
122 void GeneratePoles();
123
124 /// True while the street-furniture PCG scatter is still spawning; it finishes over the next frames.
125 bool IsStreetFurnitureGenerating() const;
126
127 /// Generates spawn points along the road.
128 void GenerateSpawnPoints();
129
130 void GenerateAll();
131
132protected:
133
134 virtual void BeginPlay() override;
135
136 /// Determine the height where the spawners will be placed, relative to each
137 /// RoutePlanner
138 UPROPERTY(Category = "Spawners", EditAnywhere)
139 float SpawnersHeight = 300.f;
140
141 UPROPERTY(Category = "Spawners", EditAnywhere)
142 TArray<TObjectPtr<AVehicleSpawnPoint>> VehicleSpawners;
143
144 UPROPERTY(EditAnywhere)
145 FString OpenDriveData;
146
147 UPROPERTY(EditAnywhere)
148 TArray<TObjectPtr<AActor>> ActorMeshList;
149
150 /// World-space bounding box of every road/sidewalk vertex generated by
151 /// GenerateRoadMesh, accumulated there and consumed by
152 /// GenerateGroundPlane to size and position the fill quad -- mirrors how
153 /// FurnitureAnchorBounds drives PCGBoundsComponent below.
154 FBox RoadMeshBounds = FBox(ForceInit);
155
156 /// The authoritative "what is road surface, where, at what height"
157 /// tool the rest of generation consumes. GenerateRoadMesh rasterizes
158 /// every road/sidewalk *triangle* (vertices alone leave unconstrained
159 /// holes inside lanes -- the mesh is dense along s but a full lane wide
160 /// laterally) into a regular XY grid with barycentric-interpolated
161 /// surface heights, then a chamfer distance transform gives every cell
162 /// its distance to the driving footprint and to any pavement, plus the
163 /// height of the nearest pavement. Terrain generation, furniture
164 /// placement and the generation QA all query this one structure instead
165 /// of re-deriving geometry their own (subtly different) ways.
167 {
168 struct FCell
169 {
170 float DriveMinZ = 0.0f;
171 float DriveMaxZ = 0.0f;
172 float PavedMinZ = 0.0f; // min over driving + sidewalk surfaces
173 float DistToDrive = -1.0f; // cm to the driving footprint, 0 inside
174 float DistToPaved = -1.0f; // cm to any pavement, 0 inside
175 float NearestPavedMinZ = 0.0f;
176 /// Lofted road-surface height over a median/junction gap cell
177 /// (FLT_MAX elsewhere). Written by GenerateMedianFill, which runs
178 /// before GenerateGroundPlane; the terrain raises itself flush to
179 /// this so the gap reads as level grass, not a sunken pit.
180 float GapSurfaceZ = FLT_MAX;
181 uint8 bDrive = 0;
182 uint8 bPaved = 0;
183 /// Like bDrive but WITHOUT the rasterizer's negative barycentric
184 /// tolerance: set only when the cell center is genuinely inside a
185 /// driving triangle. bDrive's tolerance lets grazing triangles claim
186 /// cells whose surface is mostly uncovered; gap-filling must use the
187 /// strict flag or those cells read as "road" and never get filled.
188 uint8 bDriveCovered = 0;
189 };
190
191 float CellSize = 250.0f;
192 float OriginX = 0.0f;
193 float OriginY = 0.0f;
194 int32 Width = 0;
195 int32 Height = 0;
196 TArray<FCell> Cells;
197
198 bool IsValid() const { return Width > 0 && Height > 0; }
199 void Initialize(const FBox &Bounds, float InCellSize);
200 /// Splats one triangle of road ("drive") or sidewalk surface into the
201 /// grid: conservative 2D rasterization, per-cell-center barycentric z.
202 void RasterizeTriangle(const FVector &A, const FVector &B, const FVector &C, bool bIsDrive);
203 /// Two-pass chamfer distance transform filling DistToDrive/DistToPaved
204 /// and NearestPavedMinZ. Call once after all triangles are in.
205 void BuildDistanceFields();
206 const FCell *CellAtWorld(float X, float Y) const;
207 };
208
209public:
210 /// Debug: log the raster's weld/height record in a 3x3 cell block around
211 /// (X, Y) in UE cm. Bound to the `carla.DumpRasterAt` console command.
212 void DumpRasterAt(float X, float Y) const;
213
214protected:
216
217 /// Material applied to driving-lane mesh sections.
218 UPROPERTY(Category = "Materials", EditAnywhere)
219 TSoftObjectPtr<UMaterialInterface> RoadMaterial = TSoftObjectPtr<UMaterialInterface>(
220 FSoftObjectPath(TEXT("/Game/Carla/Static/GenericMaterials/Roads/MI_RoadAsphalt_Town15.MI_RoadAsphalt_Town15")));
221
222 /// Material applied to sidewalk mesh sections.
223 UPROPERTY(Category = "Materials", EditAnywhere)
224 TSoftObjectPtr<UMaterialInterface> SidewalkMaterial = TSoftObjectPtr<UMaterialInterface>(
225 FSoftObjectPath(TEXT("/Game/Carla/Static/GenericMaterials/Sidewalk/MI_Sidewalk_Apartment.MI_Sidewalk_Apartment")));
226
227 /// Material applied to the ground plane (see GenerateGroundPlane). Picked
228 /// from Content/Carla/Static/GenericMaterials/Ground -- a plain
229 /// M_GeneralMaster instance (not a landscape auto-material, which expects
230 /// per-vertex layer weightmaps this flat quad doesn't have) authored for
231 /// large tiled areas. Alternates in the same folder: MI_Grass_Cutted_Yard
232 /// (more manicured), MI_Dirt, MI_LargeLandscape_Rural/_Highway/_Mountain
233 /// (biome variants) -- swap per-map if grass doesn't fit.
234 UPROPERTY(Category = "Materials", EditAnywhere)
235 TSoftObjectPtr<UMaterialInterface> GroundMaterial = TSoftObjectPtr<UMaterialInterface>(
236 FSoftObjectPath(TEXT("/Game/Carla/Static/GenericMaterials/Ground/MI_LargeLandscape_Grass.MI_LargeLandscape_Grass")));
237
238 /// Material applied to the zebra-bar crosswalk meshes. The bars are real
239 /// geometry (GenerateCrosswalkMesh), so this is just the same clean white
240 /// paint the lane markings use -- no dedicated crosswalk texture needed.
241 /// (MI_Crosswalk_SpeedBump was tried first as a whole-slab texture: its
242 /// diffuse is the speed bump's red field, which read as a flat pink slab.)
243 UPROPERTY(Category = "Materials", EditAnywhere)
244 TSoftObjectPtr<UMaterialInterface> CrosswalkMaterial = TSoftObjectPtr<UMaterialInterface>(
245 FSoftObjectPath(TEXT("/Game/Carla/Static/GenericMaterials/Roads/MI_Road_Asphalt_B_LaneMarkingWhite.MI_Road_Asphalt_B_LaneMarkingWhite")));
246
247 /// Material applied to white lane-marking meshes. The Rural_A marking
248 /// variants carry a heavy paint-damage mask keyed to the road atlas UV
249 /// layout; on the generated markings' along-road UVs the mask samples as
250 /// dense black blotches across the line, so use the clean Asphalt_B
251 /// variants instead.
252 UPROPERTY(Category = "Materials", EditAnywhere)
253 TSoftObjectPtr<UMaterialInterface> LaneMarkingWhiteMaterial = TSoftObjectPtr<UMaterialInterface>(
254 FSoftObjectPath(TEXT("/Game/Carla/Static/GenericMaterials/Roads/MI_Road_Asphalt_B_LaneMarkingWhite.MI_Road_Asphalt_B_LaneMarkingWhite")));
255
256 /// Material applied to yellow lane-marking meshes (centre line).
257 UPROPERTY(Category = "Materials", EditAnywhere)
258 TSoftObjectPtr<UMaterialInterface> LaneMarkingYellowMaterial = TSoftObjectPtr<UMaterialInterface>(
259 FSoftObjectPath(TEXT("/Game/Carla/Static/GenericMaterials/Roads/MI_Road_Asphalt_B_LaneMarkingYellow.MI_Road_Asphalt_B_LaneMarkingYellow")));
260
261 /// Vertical offset (cm) applied to crosswalk and lane-marking meshes above
262 /// the road surface, to avoid z-fighting with the road mesh section.
263 UPROPERTY(Category = "Materials", EditAnywhere)
264 float DecalZOffset = 1.5f;
265
266 /// Vertical offset (cm) applied *below* the road surface height where
267 /// the terrain runs under road geometry -- large enough to avoid
268 /// z-fighting, small enough that the road visibly rests on the ground
269 /// (the generation QA requires terrain within 10cm of the road
270 /// underside, see RunGenerationQA).
271 UPROPERTY(Category = "Materials", EditAnywhere)
272 float GroundPlaneZOffset = 5.0f;
273
274 /// Widest gap (cm) between opposing driving surfaces that
275 /// GenerateMedianFill closes with road surface; wider separations are
276 /// genuine verges and stay terrain.
277 UPROPERTY(Category = "Materials", EditAnywhere)
278 float MedianFillMaxWidth = 600.0f;
279
280 /// Max height split (cm) of the driving surface samples WITHIN a single
281 /// raster cell near a gap cell for it to be filled -- a gap under an
282 /// overpass sees
283 /// both decks and must not be bridged at the wrong height.
284 UPROPERTY(Category = "Materials", EditAnywhere)
285 float MedianFillMaxHeightDelta = 100.0f;
286
287 /// Width (cm) of the blend band past the road footprint over which the
288 /// terrain transitions from exactly hugging the road underside to the
289 /// free B-spline surface. Within the band the surface is also clamped
290 /// below the nearest road sample, so the transition can never rise
291 /// through a road.
292 UPROPERTY(Category = "Materials", EditAnywhere)
293 float GroundSkirtWidth = 1000.0f;
294
295 /// XY cell size (cm) of RoadRaster. 2.5m resolves both the vertical
296 /// curvature of ramps/crests and lane-scale footprint detail.
297 UPROPERTY(Category = "Materials", EditAnywhere)
298 float GroundHeightSampleCellSize = 250.0f;
299
300 /// Per-category clearance (cm) beyond the lane half-width that street
301 /// furniture must keep from the nearest driving-lane centerline; anchors
302 /// closer than this are discarded so scattered objects can't stand on or
303 /// overhang the roadway where lanes widen, merge or overlap. Sized to
304 /// the spawned content's *lateral* half-extent (the clearance is
305 /// measured across the road, so a shelter's 8 m length along the curb
306 /// doesn't count -- its ~1 m depth does). It must stay below the
307 /// category's anchor offset: the old 450 cm signage clearance against a
308 /// 4 m offset discarded 100 % of the signage anchors on every map.
309 UPROPERTY(Category = "Street Furniture", EditAnywhere)
310 float LampRoadClearance = 50.0f;
311
312 UPROPERTY(Category = "Street Furniture", EditAnywhere)
313 float VegetationRoadClearance = 250.0f;
314
315 UPROPERTY(Category = "Street Furniture", EditAnywhere)
316 float SignageRoadClearance = 50.0f;
317
318 /// Evaluated ground heightfield (final terrain surface z per grid vertex,
319 /// row-major, GroundGridNumY rows of GroundGridNumX), retained after
320 /// GenerateGroundPlane so GenerateFurnitureAnchors can re-ground
321 /// scattered objects onto the terrain they'll actually stand on.
322 TArray<float> GroundGridHeights;
323 int32 GroundGridNumX = 0;
324 int32 GroundGridNumY = 0;
325 FVector2D GroundGridOrigin = FVector2D::ZeroVector;
326 float GroundGridStepX = 0.0f;
327 float GroundGridStepY = 0.0f;
328
329 /// Bilinear interpolation of GroundGridHeights at a world XY position.
330 /// Only valid once GenerateGroundPlane has run (GroundGridNumX > 0).
331 float SampleGroundGridHeight(float X, float Y) const;
332
333 /// How far (cm) the ground plane extends past the generated road/sidewalk
334 /// bounding box on each side, so the fill doesn't end exactly at the
335 /// outermost shoulder.
336 UPROPERTY(Category = "Materials", EditAnywhere)
337 float GroundPlanePadding = 5000.0f;
338
339 /// World-space size (cm) of one UV tile on the ground plane, i.e. how
340 /// large the material's texture repeats on the ground -- the plane spans
341 /// the whole map, so a single 0-1 UV tile would stretch the texture to
342 /// the point of being unrecognizable.
343 UPROPERTY(Category = "Materials", EditAnywhere)
344 float GroundUVTileSize = 500.0f;
345
346 /// Drives the runtime PCG scatter (see GeneratePoles). Non-partitioned by
347 /// default (bIsComponentPartitioned = false on UPCGComponent), so this
348 /// works on legacy non-World-Partition levels like OpenDriveMap.umap --
349 /// no PCGWorldActor is required. Generation is triggered explicitly by
350 /// calling PCGComponent->Generate() once GeneratePoles() has spawned all
351 /// the anchor markers; the graph asset's own GenerationTrigger should be
352 /// left at "Generate On Demand" (not "Generate On Load" or "Generate At
353 /// Runtime", which assume a World-Partition streaming source).
354 UPROPERTY(Category = "Street Furniture", VisibleDefaultsOnly, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
355 TObjectPtr<UPCGComponent> PCGComponent;
356
357 /// Provides the PCG component's generation bounds: PCG derives its grid
358 /// bounds from the owner actor's component bounds, and this actor has no
359 /// visible geometry of its own -- without a bounded component the graph
360 /// aborts at Generate() with "Component has invalid bounds, not registered
361 /// nor updated" and never schedules a task. Sized to the generated road
362 /// network (anchor bounding box, padded) right before the scatter runs.
363 UPROPERTY(Category = "Street Furniture", VisibleDefaultsOnly, BlueprintReadOnly, meta = (AllowPrivateAccess = "true"))
364 TObjectPtr<UBoxComponent> PCGBoundsComponent;
365
366 /// Accumulated bounding box of every furniture anchor spawned for the
367 /// current generation, in world space; drives PCGBoundsComponent.
368 FBox FurnitureAnchorBounds = FBox(ForceInit);
369
370 /// PCG graph asset that samples the tagged anchors below (via a "Get
371 /// Actor Data" node per category, ActorSelector = {ByTag, AllWorldActors,
372 /// <tag>, bSelectMultiple=true}, Mode = GetSinglePoint) and spawns lamp,
373 /// vegetation, and signage actors/meshes at each point. Left unset by
374 /// default -- author this graph in-editor (see design doc) and assign it
375 /// on the placed AOpenDriveGenerator instance in OpenDriveMap.umap.
376 UPROPERTY(Category = "Street Furniture", EditAnywhere)
377 TSoftObjectPtr<UPCGGraphInterface> StreetFurnitureGraph;
378
379 /// PCG seed of the furniture scatter; with it, every server and run places the same furniture and banners.
380 UPROPERTY(Category = "Street Furniture", EditAnywhere)
381 int32 StreetFurnitureSeed = 42;
382
383 /// Tag used to mark lamp-pole anchor points for the PCG graph to sample.
384 UPROPERTY(Category = "Street Furniture", EditAnywhere)
385 FName LampAnchorTag = FName("PCG_StreetLampAnchor");
386
387 /// Spacing (m) between lamp anchors along each road.
388 UPROPERTY(Category = "Street Furniture", EditAnywhere)
389 float LampAnchorSpacing = 30.0f;
390
391 /// Lateral offset (m) of lamp anchors past the curb line (the outer
392 /// edge of the last roadway lane: parking/bike/border lanes included),
393 /// clamped to the middle of the sidewalk lane that follows when there is
394 /// one -- the pole base stands on the pavement next to the curb. The old
395 /// 4 m from the *driving* lane put poles mid-parking-lane on streets
396 /// with kerbside parking and 2 m into the buildings on streets with a
397 /// bare 1.5 m sidewalk.
398 UPROPERTY(Category = "Street Furniture", EditAnywhere)
399 float LampAnchorOffset = 1.0f;
400
401 /// Tag used to mark roadside-vegetation anchor points.
402 UPROPERTY(Category = "Street Furniture", EditAnywhere)
403 FName VegetationAnchorTag = FName("PCG_VegetationAnchor");
404
405 /// Spacing (m) between vegetation anchors along each road. Denser than
406 /// lamps by default; the PCG graph can layer jitter/density-filter nodes
407 /// on top of these seed points for a less regular look.
408 UPROPERTY(Category = "Street Furniture", EditAnywhere)
409 float VegetationAnchorSpacing = 15.0f;
410
411 /// Lateral offset (m) of vegetation anchors -- set past the lamp row so
412 /// poles and trees don't overlap.
413 UPROPERTY(Category = "Street Furniture", EditAnywhere)
414 float VegetationAnchorOffset = 6.0f;
415
416 /// Tag used to mark decorative-signage anchor points. Traffic-control
417 /// signs (stop/yield/speed limit) already come from OpenDRIVE <signal>
418 /// data via the existing traffic-light/-sign pipeline; this is only for
419 /// ambient roadside signage PCG can scatter for visual density.
420 UPROPERTY(Category = "Street Furniture", EditAnywhere)
421 FName SignageAnchorTag = FName("PCG_SignageAnchor");
422
423 /// Spacing (m) between signage anchors along each road. Sparse: this row
424 /// spawns full bus-stop shelters, and a shelter every 60m read as a
425 /// repeating wall of street furniture.
426 UPROPERTY(Category = "Street Furniture", EditAnywhere)
427 float SignageAnchorSpacing = 150.0f;
428
429 /// Lateral offset (m) of signage anchors past the curb line, clamped
430 /// onto the sidewalk like the lamps (same row, phased half a lamp
431 /// spacing along the road so the two never coincide).
432 UPROPERTY(Category = "Street Furniture", EditAnywhere)
433 float SignageAnchorOffset = 1.0f;
434
435 /// Spawns one AProceduralFurnitureAnchor per transform returned by
436 /// Map::GetTreesTransform for the given spacing/offset, tagged for the
437 /// PCG graph to pick up, re-grounded onto pavement/terrain and filtered
438 /// against driving lanes with the given clearance (cm) past the lane
439 /// half-width. bMeasureFromCurb/bKeepOnSidewalk select the curb-based
440 /// placement (see LampAnchorOffset); SOffset (m) phases the row along
441 /// each road. Returns the number of anchors spawned.
442 int32 GenerateFurnitureAnchors(
443 const FName &Tag, float Spacing, float Offset, float RoadClearance,
444 bool bMeasureFromCurb, bool bKeepOnSidewalk, float SOffset);
445
446public:
447
448 /// Systematic post-generation validation, run automatically shortly
449 /// after GenerateAll (delayed so the async PCG scatter has spawned its
450 /// content) and re-runnable on demand via the console command
451 /// "carla.MapGenQA". Checks, exhaustively rather than by spot samples:
452 /// A) road support -- every road/sidewalk sample cell must have
453 /// terrain 0..QASupportGapMax below its lowest surface (floating
454 /// road if the gap is larger, terrain invasion if negative);
455 /// stacked-road cells (MaxZ - MinZ > QAStackedRoadGap) only check
456 /// the lower deck, the upper one is a legitimate bridge.
457 /// B) object placement -- the bounding-box footprint of every static
458 /// primitive not generated by this actor (street furniture from
459 /// the PCG scatter, incl. per-instance foliage ISMs) must stay
460 /// clear of every driving lane's corridor.
461 /// Logs a summary and writes the full violation list with world
462 /// locations to Saved/mapgen_qa.json for external tooling.
463 void RunGenerationQA();
464
465protected:
466
467 /// Max allowed gap (cm) between a road cell's lowest surface and the
468 /// terrain below it before the cell counts as a floating road. Must stay
469 /// above the ground plane's near-pavement clearance (GroundPlaneZOffset)
470 /// plus the fill-quad clamp's reach (fill sits ~2cm under the lowest
471 /// neighbouring lane, terrain 10cm under the fill, so a graded cell's
472 /// own surface can be ~grade x 2.5m + 12cm above the terrain).
473 UPROPERTY(Category = "Generation QA", EditAnywhere)
474 float QASupportGapMax = 30.0f;
475
476 /// Min z spread (cm) within one sample cell that marks it as stacked
477 /// roads (bridge over road) rather than a single surface.
478 UPROPERTY(Category = "Generation QA", EditAnywhere)
479 float QAStackedRoadGap = 400.0f;
480
481};
FRoadSurfaceRaster RoadRaster
Invisible marker placed along a generated road to anchor PCG-scattered street furniture (lamps,...
Base class for spawner locations for walkers.
The authoritative "what is road surface, where, at what height" tool the rest of generation consumes.