CARLA
Loading...
Searching...
No Matches
MeshToLandscape.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma de Barcelona (UAB). This work is licensed under the terms of the MIT license. For a copy, see <https://opensource.org/licenses/MIT>.
2
3#include "MeshToLandscape.h"
4
5#include <util/ue-header-guard-begin.h>
6#include "Landscape.h"
7#include "CoreMinimal.h"
8#include "Engine/StaticMeshActor.h"
9#include "Components/StaticMeshComponent.h"
10#include "StaticMeshLODResourcesAdapter.h"
11#include "Physics/Experimental/PhysScene_Chaos.h"
12#include "PhysicsEngine/PhysicsObjectExternalInterface.h"
13#include "Async/ParallelFor.h"
14#include "Kismet/KismetMathLibrary.h"
15#include "Engine/CollisionProfile.h"
16#include <util/ue-header-guard-end.h>
17
18static TAutoConsoleVariable<int32> CVDrawDebugLandscapeTraces(
19 TEXT("CARLA.DrawDebugLandscapeTraces"),
20 0,
21 TEXT("Whether to debug-draw the traces during landscape construction."));
22
23static TAutoConsoleVariable<int32> CVDrawDebugLandscapeTraceFailures(
24 TEXT("CARLA.DrawDebugLandscapeTraceFailures"),
25 0,
26 TEXT("Whether to debug-draw trace failures during landscape construction."));
27
28static TAutoConsoleVariable<int32> CVMeshToLandscapeMaxTraceRetries(
29 TEXT("CARLA.MeshToLandscape.MaxTraceRetries"),
30 8,
31 TEXT("Max parallel line trace retries."));
32
33static TAutoConsoleVariable<int32> CVDrawDebugBoxes(
34 TEXT("CARLA.MeshToLandscape.DrawDebugBoxes"),
35 0,
36 TEXT("Whether to debug-draw the bounding box used for tracing."));
37
38constexpr int32 KernelSide = 3;
39
41 TArray<UActorComponent*>& Components,
42 const TArray<UClass*>& Blacklist,
43 const TArray<UClass*>& Whitelist)
44{
45 for (int32 i = 0; i != Components.Num();)
46 {
47 UActorComponent* Component = Components[i];
48
49 bool IsWhitelisted = false;
50 bool IsBlacklisted = false;
51
52 for (UClass* Class : Whitelist)
53 {
54 IsWhitelisted = Component->IsA(Class);
55 if (IsWhitelisted)
56 break;
57 }
58
59 if (IsWhitelisted)
60 {
61 ++i;
62 continue;
63 }
64
65 for (UClass* Class : Blacklist)
66 {
67 IsBlacklisted = Component->IsA(Class);
68 if (IsBlacklisted)
69 break;
70 }
71
72 if (!IsBlacklisted)
73 {
74 ++i;
75 continue;
76 }
77
78 Components.RemoveAtSwap(i, EAllowShrinking::No);
79 }
80}
81
83 TArray<UActorComponent*>& Components)
84{
85 for (int32 i = 0; i != Components.Num();)
86 {
87 UActorComponent* Component = Components[i];
88 UStaticMeshComponent* SMC = Cast<UStaticMeshComponent>(Component);
89 if (SMC == nullptr)
90 {
91 ++i;
92 continue;
93 }
94 UStaticMesh* SM = SMC->GetStaticMesh();
95 if (SM != nullptr) // Some SMCs have NULL SM.
96 {
97 bool OK = SM->HasValidRenderData();
98 OK = OK && (
99#if WITH_EDITOR
100 !SM->IsNaniteEnabled() ||
101#endif
102 SM->HasValidNaniteData());
103 if (OK)
104 {
105 ++i;
106 continue;
107 }
108 else
109 {
110 UE_LOG(
111 LogCarla, Warning,
112 TEXT("Skipping static mesh asset %s due to invalid RenderData or invalid Nanite data."),
113 *UKismetSystemLibrary::GetDisplayName(Component));
114 }
115 }
116 else
117 {
118 UE_LOG(
119 LogCarla, Warning,
120 TEXT("Skipping static mesh asset %s due to missing static mesh data (GetStaticMesh returned nullptr)."),
121 *UKismetSystemLibrary::GetDisplayName(Component));
122 }
123 Components.RemoveAtSwap(i, EAllowShrinking::No);
124 }
125}
126
128 TArray<UActorComponent*>& Components,
129 double MaxZVariance)
130{
131 if (MaxZVariance < 0.0)
132 return;
133 for (int32 i = 0; i != Components.Num();)
134 {
135 UStaticMeshComponent* SMC = Cast<UStaticMeshComponent>(Components[i]);
136 if (SMC == nullptr)
137 {
138 ++i;
139 continue;
140 }
141 UStaticMesh* SM = SMC->GetStaticMesh();
142 if (SM == nullptr)
143 {
144 ++i;
145 continue;
146 }
147 if (SM->GetNumLODs() == 0)
148 {
149 ++i;
150 continue;
151 }
152 const FStaticMeshLODResources& LOD = SM->GetLODForExport(0);
153 FStaticMeshLODResourcesMeshAdapter Adapter(&LOD);
154 int32 VertexCount = LOD.GetNumVertices();
155 double M, M2;
156 M = M2 = 0.0;
157 for (int32 j = 0; j != VertexCount; ++j)
158 {
159 double Z = Adapter.GetVertex(j).Z;
160 double D = Z - M;
161 M = std::fma(D, 1.0 / (double)j, M);
162 double D2 = Z - M;
163 M2 = std::fma(D, D2, M2);
164 }
165 double V = M2 / (double)(VertexCount - 1);
166 if (V >= MaxZVariance)
167 Components.RemoveAtSwap(i, EAllowShrinking::No);
168 else
169 ++i;
170 }
171}
172
174 TArray<UActorComponent*>& Components,
175 const TArray<FString>& PatternWhitelist,
176 const TArray<FString>& PatternBlacklist)
177{
178 if (PatternWhitelist.IsEmpty() &&
179 PatternBlacklist.IsEmpty())
180 return;
181 for (int32 i = 0; i != Components.Num();)
182 {
183 UActorComponent* Component = Components[i];
184
185 bool Match = false;
186
187 FString ComponentName =
188 UKismetSystemLibrary::GetDisplayName(Component);
189
190 FString ActorName =
191 UKismetSystemLibrary::GetDisplayName(Component->GetOwner());
192
193 for (const FString& Pattern : PatternBlacklist)
194 {
195 Match = ComponentName.MatchesWildcard(Pattern);
196 if (Match)
197 break;
198 }
199
200 if (Match)
201 {
202 Components.RemoveAtSwap(i, EAllowShrinking::No);
203 continue;
204 }
205
206 for (const FString& Pattern : PatternBlacklist)
207 {
208 Match = ActorName.MatchesWildcard(Pattern);
209 if (Match)
210 break;
211 }
212
213 if (Match)
214 {
215 Components.RemoveAtSwap(i, EAllowShrinking::No);
216 continue;
217 }
218
219 for (const FString& Pattern : PatternWhitelist)
220 {
221 Match = ComponentName.MatchesWildcard(Pattern);
222 if (Match)
223 break;
224 }
225
226 if (Match)
227 {
228 ++i;
229 continue;
230 }
231
232 for (const FString& Pattern : PatternWhitelist)
233 {
234 Match = ActorName.MatchesWildcard(Pattern);
235 if (Match)
236 break;
237 }
238
239 if (Match)
240 {
241 ++i;
242 continue;
243 }
244 Components.RemoveAtSwap(i, EAllowShrinking::No);
245 }
246}
247
249 const TArray<UActorComponent*>& Components,
250 int32 SubsectionSizeQuads,
251 int32 NumSubsections,
252 int32 SampleFrequency,
253 const TArray<FHitDisplacementEntry>& HitDisplacementMap)
254{
255#if WITH_EDITOR
256 if (Components.Num() == 0)
257 return nullptr;
258
259 int32 ComponentSizeQuads = SubsectionSizeQuads * NumSubsections;
260
261 if (ComponentSizeQuads == 0)
262 {
263 UE_LOG(
264 LogCarla, Warning,
265 TEXT("Skipping landscape generation, SubsectionSizeQuads=%i NumSubsections=%i"),
266 SubsectionSizeQuads,
267 NumSubsections);
268 return nullptr;
269 }
270
271 UWorld* World = Components[0]->GetWorld();
272
273 FBox3d Bounds = FBox3d(ForceInit);
274
275 for (UActorComponent* Component : Components)
276 {
277 UPrimitiveComponent* PrimitiveComponent = Cast<UPrimitiveComponent>(Component);
278 if (PrimitiveComponent != nullptr)
279 {
280 FBox BoxBounds = PrimitiveComponent->Bounds.GetBox();
281 if (CVDrawDebugBoxes.GetValueOnAnyThread())
282 {
283 DrawDebugBox(
284 World,
285 BoxBounds.GetCenter(),
286 BoxBounds.GetExtent(),
287 FColor::Cyan,
288 false,
289 10.0F);
290 }
291 Bounds += BoxBounds;
292 }
293 }
294
295 FVector3d Max = Bounds.Max;
296 FVector3d Min = Bounds.Min;
297 FVector3d Range = Max - Min;
298 FVector3d Center = Bounds.GetCenter();
299
300 FVector3d RangeMeters = Range * UE_CM_TO_M;
301 FIntPoint DesiredVertexCount = FIntPoint(
302 FMath::Max(1, (int32)FMath::CeilToInt(RangeMeters.X * SampleFrequency) + 1),
303 FMath::Max(1, (int32)FMath::CeilToInt(RangeMeters.Y * SampleFrequency) + 1));
304
305 FIntPoint DesiredQuadCount = (DesiredVertexCount - FIntPoint(1)).ComponentMax(FIntPoint(1));
306 FIntPoint ComponentSizeQuads2 = FIntPoint(ComponentSizeQuads);
307 FIntPoint RequiredQuadCount =
308 ((DesiredQuadCount + ComponentSizeQuads2 - 1) / ComponentSizeQuads2) *
309 ComponentSizeQuads2;
310 FIntPoint HeightmapExtent =
311 RequiredQuadCount +
312 FIntPoint(1);
313
314 TArray<uint16_t> HeightmapData;
315 HeightmapData.SetNumZeroed(HeightmapExtent.X * HeightmapExtent.Y);
316
317 FVector2d CellSize = FVector2d(Range) / (FVector2d(HeightmapExtent) - FVector2d(1.0));
318
319 {
320 const TSet<UActorComponent*> ComponentMap(Components);
321 TArray<TPair<FVector3d, FVector3d>> Failures;
322 FCriticalSection FailuresCS;
323
324 int32 MaxRetries = CVMeshToLandscapeMaxTraceRetries.GetValueOnAnyThread();
325
326 constexpr auto RingSampleCount = 8U;
327 const double I2R = 2.0 * PI / (double)RingSampleCount;
328
329 double MaxOffsetRadius = CellSize.GetMin() * 0.5;
330
331 FVector2d OffsetRing[RingSampleCount];
332 for (size_t i = 0; i != RingSampleCount; ++i)
333 {
334 FVector2d D;
335 FMath::SinCos(&D.Y, &D.X, (double)i * I2R);
336 OffsetRing[i] = D;
337 }
338
339 FCollisionShape TestGeometry;
340 TestGeometry.SetBox(
341 FVector3f(
342 CellSize.X,
343 CellSize.Y,
344 UE_M_TO_CM) * 2.5F);
345
346 auto LockedPhysObject = FPhysicsObjectExternalInterface::LockRead(
347 World->GetPhysicsScene());
348
349 ParallelFor(HeightmapData.Num(), [&](int32 Index)
350 {
351 int32 Y = Index / HeightmapExtent.X;
352 int32 X = Index % HeightmapExtent.X;
353
354 FVector2d XY = FVector2d(Min) + CellSize * FVector2d(X, Y);
355 FVector3d Begin0 = FVector3d(XY.X, XY.Y, Max.Z);
356 FVector3d End0 = FVector3d(XY.X, XY.Y, Min.Z);
357 FVector3d Begin = Begin0;
358 FVector3d End = End0;
359 FHitResult Hit;
360 double HitZ = Min.Z;
361
362 FCollisionQueryParams CQParams = FCollisionQueryParams::DefaultQueryParam;
363 CQParams.bTraceComplex = true;
364 CQParams.bFindInitialOverlaps = true;
365 CQParams.bReturnPhysicalMaterial = false;
366 CQParams.MobilityType = EQueryMobilityType::Any;
367 CQParams.bIgnoreTouches = true;
368
369 FRandomStream PRNG(Index);
370
371 if (!World->ParallelSweepSingleByChannel(
372 Hit,
373 Begin,
374 End,
375 FQuat::Identity,
376 ECollisionChannel::ECC_WorldStatic,
377 TestGeometry,
378 CQParams))
379 {
380 HeightmapData[Index] = 0;
381 return;
382 }
383
384 int32 Retry = 0;
385 for (; Retry < MaxRetries; ++Retry) // Sometimes, LTSBC fails on geometry seams
386 {
387 if (World->ParallelLineTraceSingleByChannel(
388 Hit,
389 Begin, End,
390 ECollisionChannel::ECC_GameTraceChannel2,
391 CQParams))
392 {
393 if (ComponentMap.Contains(Hit.GetComponent())) // Done
394 {
395 HitZ = Hit.Location.Z;
396 break;
397 }
398 CQParams.AddIgnoredComponent(Hit.GetComponent());
399 }
400 else
401 {
402 int32 Index = PRNG.RandRange(0, (int32)RingSampleCount - 1);
403 FVector3d Offset = FVector3d(OffsetRing[Index], 0.0);
404 Offset *= PRNG.FRandRange(0.0, MaxOffsetRadius);
405 Begin = Begin0 + Offset;
406 End = End0 + Offset;
407 }
408 }
409
410 if (Retry != MaxRetries)
411 {
412 UPrimitiveComponent* HitComponent = Hit.GetComponent();
413 FString TestName;
414 double ZDisplacement = 0.0;
415 for (auto& [Pattern, Displacement] : HitDisplacementMap)
416 {
417 if (std::abs(Displacement) <= std::abs(ZDisplacement))
418 continue;
419 TestName = UKismetSystemLibrary::GetDisplayName(HitComponent);
420 if (TestName.MatchesWildcard(Pattern))
421 {
422 ZDisplacement = Displacement;
423 continue;
424 }
425 TestName = UKismetSystemLibrary::GetDisplayName(HitComponent->GetOwner());
426 if (TestName.MatchesWildcard(Pattern))
427 {
428 ZDisplacement = Displacement;
429 continue;
430 }
431 }
432 HitZ += ZDisplacement;
433 HitZ -= Min.Z;
434 HitZ *= UE_CM_TO_M;
435 HitZ += 256.0;
436 HitZ /= 512.0;
437 HitZ = std::clamp(HitZ, 0.0, 1.0);
438 HeightmapData[Index] = (uint16)std::lround(
439 HitZ * TNumericLimits<uint16>::Max());
440 }
441 else
442 {
443 FailuresCS.Lock();
444 Failures.Add({ Begin, End });
445 FailuresCS.Unlock();
446 HeightmapData[Index] = 0;
447 }
448 }, EParallelForFlags::Unbalanced);
449 LockedPhysObject.Release();
450
451 if (CVDrawDebugBoxes.GetValueOnAnyThread())
452 {
453 DrawDebugBox(
454 World,
455 Bounds.GetCenter(),
456 Bounds.GetExtent(),
457 FColor::Yellow,
458 false,
459 15.0F);
460 }
461
462 if (CVDrawDebugLandscapeTraces.GetValueOnAnyThread() != 0)
463 {
464 for (int32 Index = 0; Index != HeightmapData.Num(); ++Index)
465 {
466 int32 Y = Index / HeightmapExtent.X;
467 int32 X = Index % HeightmapExtent.X;
468 FVector2d XY = FVector2d(Min) + CellSize * FVector2d(X, Y);
469 FVector3d Begin = FVector3d(XY.X, XY.Y, Max.Z);
470 FVector3d End = FVector3d(XY.X, XY.Y, Min.Z);
471 DrawDebugLine(World, Begin, End, FColor::Green, false, 10.0F);
472 }
473 }
474
475 if (CVDrawDebugLandscapeTraceFailures.GetValueOnAnyThread())
476 {
477 for (auto [Begin, End] : Failures)
478 DrawDebugLine(World, Begin, End, FColor::Red, false, 20.0F);
479 }
480
481 if (Failures.Num() != 0 && false)
482 {
483 FString FailureString;
484 for (auto [Begin, End] : Failures)
485 {
486 FailureString += FString::Printf(
487 TEXT("Failed to trace against world from (%f, %f, %f) to (%f, %f, %f), too many objects.\n"),
488 Begin.X, Begin.Y, Begin.Z,
489 End.X, End.Y, End.Z);
490 }
491 FFileHelper::SaveStringToFile(
492 FailureString,
493 *FString::Printf(TEXT("TraceFailures.txt")));
494 }
495 }
496
497 FActorSpawnParameters SpawnParams;
498 ALandscape* Landscape = World->SpawnActor<ALandscape>(
499 ALandscape::StaticClass(),
500 FVector3d::ZeroVector,
501 FRotator::ZeroRotator,
502 SpawnParams);
503
504 FGuid LandscapeGUID = FGuid::NewGuid();
505
506 Landscape->ComponentSizeQuads = ComponentSizeQuads;
507 Landscape->SubsectionSizeQuads = SubsectionSizeQuads;
508 Landscape->NumSubsections = NumSubsections;
509
510 TMap<FGuid, TArray<uint16>> LayerHeightMaps;
511 LayerHeightMaps.Add(FGuid(), MoveTemp(HeightmapData));
512
513 TMap<FGuid, TArray<FLandscapeImportLayerInfo>> LayerImportInfos;
514 LayerImportInfos.Add(FGuid(), TArray<FLandscapeImportLayerInfo>());
515
516 FVector3d LandscapeScale = FVector3d(CellSize, 100.0);
517 Landscape->SetActorTransform(FTransform(
518 FQuat::Identity,
519 Min,
520 LandscapeScale));
521
522 Landscape->Import(
523 LandscapeGUID,
524 0, 0,
525 HeightmapExtent.X - 1, HeightmapExtent.Y - 1,
526 NumSubsections, SubsectionSizeQuads,
527 LayerHeightMaps,
528 nullptr,
529 LayerImportInfos,
530 ELandscapeImportAlphamapType::Additive,
531 TArrayView<const FLandscapeLayer>());
532
533 Landscape->SetLandscapeGuid(LandscapeGUID);
534
535 Landscape->CreateLandscapeInfo();
536
537 Landscape->ReregisterAllComponents();
538 Landscape->RecreateCollisionComponents();
539 Landscape->PostEditChange();
540 Landscape->MarkPackageDirty();
541
542 return Landscape;
543#else
544 return nullptr;
545#endif
546}
547
549 UObject* WorldContextObject,
550 const TArray<FString>& PatternWhitelist,
551 const TArray<FString>& PatternBlacklist,
552 const TArray<UClass*>& ClassWhitelist,
553 const TArray<UClass*>& ClassBlacklist,
554 double MaxZVariance,
555 TArray<UActorComponent*>& OutComponents)
556{
557 UWorld* World = WorldContextObject->GetWorld();
558 TArray<AActor*> Actors;
559 TArray<UActorComponent*> Components;
560 UGameplayStatics::GetAllActorsOfClass(
561 WorldContextObject,
562 AActor::StaticClass(),
563 Actors);
564 for (AActor* Actor : Actors)
565 {
566 bool AnySMC = false;
567
568 Actor->GetComponents(Components);
569 for (UActorComponent* PrimitiveComponent : Components)
570 {
571 AnySMC = PrimitiveComponent->IsA<UStaticMeshComponent>();
572 if (AnySMC)
573 break;
574 }
575
576 FilterByClassList(Components, ClassBlacklist, ClassWhitelist);
577 if (AnySMC)
578 {
580 FilterStaticMeshComponentsByVariance(Components, MaxZVariance);
581 }
583 Components,
584 PatternWhitelist,
585 PatternBlacklist);
586 OutComponents.Append(Components);
587 Components.Reset();
588 }
589 OutComponents.Shrink();
590}
static TAutoConsoleVariable< int32 > CVDrawDebugLandscapeTraces(TEXT("CARLA.DrawDebugLandscapeTraces"), 0, TEXT("Whether to debug-draw the traces during landscape construction."))
constexpr int32 KernelSide
static TAutoConsoleVariable< int32 > CVDrawDebugLandscapeTraceFailures(TEXT("CARLA.DrawDebugLandscapeTraceFailures"), 0, TEXT("Whether to debug-draw trace failures during landscape construction."))
static TAutoConsoleVariable< int32 > CVDrawDebugBoxes(TEXT("CARLA.MeshToLandscape.DrawDebugBoxes"), 0, TEXT("Whether to debug-draw the bounding box used for tracing."))
static TAutoConsoleVariable< int32 > CVMeshToLandscapeMaxTraceRetries(TEXT("CARLA.MeshToLandscape.MaxTraceRetries"), 8, TEXT("Max parallel line trace retries."))
static void FilterStaticMeshComponentsByVariance(TArray< UActorComponent * > &Components, double MaxZVariance)
static void FilterByClassList(TArray< UActorComponent * > &Components, const TArray< UClass * > &Blacklist, const TArray< UClass * > &Whitelist)
static void EnumerateLandscapeLikeStaticMeshComponents(UObject *WorldContextObject, const TArray< FString > &PatternWhitelist, const TArray< FString > &PatternBlacklist, const TArray< UClass * > &ClassWhitelist, const TArray< UClass * > &ClassBlacklist, double MaxZVariance, TArray< UActorComponent * > &OutComponents)
static void FilterInvalidStaticMeshComponents(TArray< UActorComponent * > &Components)
static void FilterComponentsByPatterns(TArray< UActorComponent * > &Components, const TArray< FString > &PatternWhitelist, const TArray< FString > &PatternBlacklist)
static ALandscape * ConvertMeshesToLandscape(const TArray< UActorComponent * > &Components, int32 SubsectionSizeQuads, int32 NumSubsections, int32 SampleFrequency, const TArray< FHitDisplacementEntry > &HitDisplacementMap)