CARLA
Loading...
Searching...
No Matches
ProceduralVegetationTool.cpp
Go to the documentation of this file.
2
3#include "Components/InstancedStaticMeshComponent.h"
4#include "Components/BoxComponent.h"
5#include "Misc/PackageName.h"
6#include "Data/PCGPointData.h"
7#include "Engine/StaticMesh.h"
8#include "EngineUtils.h"
9#include "Game/Tagger.h"
10#include "Metadata/PCGMetadata.h"
11#include "Metadata/PCGMetadataAttributeTpl.h"
12#include "Misc/FileHelper.h"
13#include "PCGComponent.h"
14#include "PCGContext.h"
15#include "PCGGraph.h"
16#include "PCGSubsystem.h"
17#include "Async/TaskGraphInterfaces.h"
18#include "Serialization/JsonReader.h"
19#include "Serialization/JsonSerializer.h"
20#if WITH_EDITOR
21#include "StaticMeshCompiler.h"
22#endif
23
25{
26 RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT("Root"));
27 RootComponent->SetMobility(EComponentMobility::Static);
28 GenerationBounds = CreateDefaultSubobject<UBoxComponent>(TEXT("GenerationBounds"));
29 GenerationBounds->SetupAttachment(RootComponent);
30 GenerationBounds->SetBoxExtent(FVector(100));
31 GenerationBounds->SetCollisionEnabled(ECollisionEnabled::NoCollision);
32 GenerationBounds->SetHiddenInGame(true);
33 GenerationBounds->SetCanEverAffectNavigation(false);
34 PCG = CreateDefaultSubobject<UPCGComponent>(TEXT("VegetationPCG"));
35 PCG->GenerationTrigger = EPCGComponentGenerationTrigger::GenerateOnDemand;
36 PCG->OnPCGGraphGeneratedExternal.AddDynamic(this, &AProceduralVegetationTool::OnGenerated);
37 Graph = TSoftObjectPtr<UPCGGraphInterface>(FSoftObjectPath(TEXT("/Game/Carla/Blueprints/PCG/VegetationTool/PCG_TwinVegetation.PCG_TwinVegetation")));
38 PrimaryActorTick.bCanEverTick = false;
39#if WITH_EDITORONLY_DATA
40 bIsSpatiallyLoaded = false;
41#endif
42}
43
44bool AProceduralVegetationTool::ImportPlan()
45{
46 if (IsGenerating()) { Status = TEXT("Wait for PCG generation to finish."); return false; }
47 FString Text;
48 TSharedPtr<FJsonObject> Root;
49 if (!FFileHelper::LoadFileToString(Text, *PlanFile.FilePath) ||
50 !FJsonSerializer::Deserialize(TJsonReaderFactory<>::Create(Text), Root) || !Root.IsValid())
51 { Status = TEXT("Cannot read vegetation plan JSON."); return false; }
52 FString Schema, Coordinates;
53 const TArray<TSharedPtr<FJsonValue>>* Entries = nullptr;
54 if (!Root->TryGetStringField(TEXT("schema"), Schema) || Schema != TEXT("twin-vegetation-plan/1") ||
55 !Root->TryGetStringField(TEXT("coordinate_system"), Coordinates) || Coordinates != TEXT("carla-metres") ||
56 !Root->TryGetArrayField(TEXT("points"), Entries) || Entries->Num() > 200000)
57 { Status = TEXT("Unsupported plan schema, coordinates or point count."); return false; }
58 TArray<FCarlaVegetationPoint> Candidate;
59 TSet<FString> Ids;
60#if WITH_EDITOR
61 // Spatial ground actors can have just been loaded by the commandlet. Their
62 // asynchronous mesh compilation must finish before collision bodies exist.
63 FStaticMeshCompilingManager::Get().FinishAllCompilation();
64#endif
65 FCollisionQueryParams Query(SCENE_QUERY_STAT(VegetationGround), true, this);
66 for (TActorIterator<AProceduralVegetationTool> It(GetWorld()); It; ++It) Query.AddIgnoredActor(*It);
67 for (const auto& Entry : *Entries)
68 {
69 const TSharedPtr<FJsonObject>* ObjPtr = nullptr;
70 if (!Entry->TryGetObject(ObjPtr)) { Status = TEXT("Invalid point object."); return false; }
71 const auto& Obj = *ObjPtr;
73 FString MeshPath, State, Kind;
74 double X, Y, Z, Scale, Yaw, Seed, PitRadius, CrownRadius;
75 if (!Obj->TryGetStringField(TEXT("id"), Point.Id) || Point.Id.IsEmpty() || Ids.Contains(Point.Id) ||
76 !Obj->TryGetStringField(TEXT("status"), State) || State != TEXT("accepted") ||
77 !Obj->TryGetStringField(TEXT("mesh"), MeshPath) || !MeshPath.StartsWith(TEXT("/Game/Carla/Static/Vegetation/")) ||
78 !Obj->TryGetStringField(TEXT("source"), Point.Provenance) ||
79 !Obj->TryGetStringField(TEXT("kind"), Kind) ||
80 !Obj->TryGetNumberField(TEXT("pit_radius_m"), PitRadius) ||
81 !Obj->TryGetNumberField(TEXT("crown_radius_m"), CrownRadius) ||
82 !FMath::IsFinite(PitRadius) || !FMath::IsFinite(CrownRadius) || PitRadius <= 0 || CrownRadius <= 0 ||
83 !Obj->TryGetNumberField(TEXT("x"), X) || !Obj->TryGetNumberField(TEXT("y"), Y) ||
84 !Obj->TryGetNumberField(TEXT("z"), Z) || !Obj->TryGetNumberField(TEXT("scale"), Scale) ||
85 !Obj->TryGetNumberField(TEXT("yaw"), Yaw) || !Obj->TryGetNumberField(TEXT("seed"), Seed) ||
86 !FMath::IsFinite(X) || !FMath::IsFinite(Y) || !FMath::IsFinite(Z) ||
87 !FMath::IsFinite(Scale) || Scale <= 0 || Scale > 10 || !FMath::IsFinite(Yaw) ||
88 !FMath::IsFinite(Seed) || Seed < 0 || Seed > MAX_int32)
89 { Status = TEXT("Invalid or duplicate vegetation point; previous plan retained."); return false; }
90 if (!MeshPath.Contains(TEXT("."))) MeshPath += TEXT(".") + FPackageName::GetShortName(MeshPath);
91 Point.Mesh = TSoftObjectPtr<UStaticMesh>(FSoftObjectPath(MeshPath));
92 if (!Point.Mesh.LoadSynchronous())
93 { Status = FString::Printf(TEXT("Missing mesh: %s; previous plan retained."), *MeshPath); return false; }
94 const FBoxSphereBounds MeshBounds = Point.Mesh.Get()->GetBounds();
95 const double Radius = (FVector2D(MeshBounds.BoxExtent).Size() + FVector2D(MeshBounds.Origin).Size()) * Scale / 100.0;
96 if ((Kind == TEXT("tree") ? CrownRadius : PitRadius) + 0.01 < Radius)
97 { Status = FString::Printf(TEXT("Mesh footprint exceeds planned clearance for %s (%.2f m). Update the preset and regenerate."), *Point.Id, Radius); return false; }
98 // Project only onto an explicit planting surface, never onto arbitrary WorldStatic
99 // (roofs, road traffic, or other generated vegetation). A missing/mismatched surface
100 // invalidates the import before any existing bake is replaced.
101 FVector Position = FVector(X,Y,Z)*100.0;
102 FHitResult Hit;
103 if (!GetWorld()->LineTraceSingleByChannel(Hit, Position+FVector(0,0,100), Position-FVector(0,0,200), ECC_Visibility, Query))
104 { Status = FString::Printf(TEXT("No planting surface beneath %s; load the region before importing."), *Point.Id); return false; }
105 const UStaticMeshComponent* Ground = Cast<UStaticMeshComponent>(Hit.GetComponent());
106 const FString GroundPath = Ground && Ground->GetStaticMesh() ? Ground->GetStaticMesh()->GetPathName() : FString();
107 if (!GroundPath.StartsWith(TEXT("/Game/Carla/Static/SideWalk/")) && !GroundPath.StartsWith(TEXT("/Game/Carla/Static/Terrain/")))
108 { Status = FString::Printf(TEXT("Unexpected planting surface beneath %s: %s"), *Point.Id, *GroundPath); return false; }
109 Position.Z = Hit.ImpactPoint.Z;
110 TOptional<FCarlaVegetationPoint> Footing;
111 const TSharedPtr<FJsonObject>* FootingJson = nullptr;
112 if (Obj->TryGetObjectField(TEXT("footing"), FootingJson))
113 {
114 FString Path;
115 double FootScale, FootRadius, Offset, SoilHeight, FootYaw;
116 const auto& F = *FootingJson;
117 if (Kind != TEXT("tree") || !F->TryGetStringField(TEXT("mesh"), Path) ||
118 Path != TEXT("/Game/Carla/Static/Static/SM_TreeBase02") ||
119 !F->TryGetNumberField(TEXT("scale"), FootScale) || !FMath::IsFinite(FootScale) || FootScale <= 0 || FootScale > 3 ||
120 !F->TryGetNumberField(TEXT("radius_m"), FootRadius) || !FMath::IsFinite(FootRadius) || FootRadius <= 0 ||
121 !F->TryGetNumberField(TEXT("ground_offset_m"), Offset) || !FMath::IsFinite(Offset) || Offset < -0.04 || Offset > -0.005 ||
122 !F->TryGetNumberField(TEXT("soil_height_m"), SoilHeight) || !FMath::IsFinite(SoilHeight) || FMath::Abs(SoilHeight-0.14744362) > 0.0001 ||
123 !F->TryGetNumberField(TEXT("yaw"), FootYaw) || !FMath::IsFinite(FootYaw) ||
124 !GroundPath.StartsWith(TEXT("/Game/Carla/Static/SideWalk/")))
125 { Status = FString::Printf(TEXT("Invalid sidewalk footing for %s."), *Point.Id); return false; }
127 Base.Id = Point.Id + TEXT(":footing");
128 Base.Provenance = Point.Provenance + TEXT(":footing");
129 Path += TEXT(".") + FPackageName::GetShortName(Path);
130 Base.Mesh = TSoftObjectPtr<UStaticMesh>(FSoftObjectPath(Path));
131 if (!Base.Mesh.LoadSynchronous()) { Status = TEXT("Missing tree footing mesh."); return false; }
132 const auto B = Base.Mesh.Get()->GetBounds();
133 const double RequiredRadius = (FVector2D(B.BoxExtent).Size()+FVector2D(B.Origin).Size())*FootScale/100.0;
134 if (PitRadius+0.001 < RequiredRadius || FootRadius*FootScale+0.001 < RequiredRadius)
135 { Status = FString::Printf(TEXT("Footing exceeds reserved pit for %s."), *Point.Id); return false; }
136 // Embed the raised stone surround in the sidewalk plane. Its soil surface
137 // covers the paving without coplanar flicker; seat the upright trunk on
138 // that soil. The measured center height belongs to this specific asset.
139 const FQuat Rotation = FRotationMatrix::MakeFromZX(Hit.ImpactNormal, FRotator(0, FootYaw, 0).Vector()).ToQuat();
140 const double EdgeRadius = FMath::Max(B.BoxExtent.X, B.BoxExtent.Y)*FootScale;
141 for (int32 I=0; I<8; ++I)
142 {
143 const double A = I*PI/4;
144 const FVector Edge = Position + Rotation.RotateVector(FVector(FMath::Cos(A)*EdgeRadius, FMath::Sin(A)*EdgeRadius, 0));
145 FHitResult EdgeHit;
146 const bool bHit = GetWorld()->LineTraceSingleByChannel(EdgeHit, Edge+FVector(0,0,50), Edge-FVector(0,0,50), ECC_Visibility, Query);
147 const auto* EdgeMesh = bHit ? Cast<UStaticMeshComponent>(EdgeHit.GetComponent()) : nullptr;
148 if (!EdgeMesh || !EdgeMesh->GetStaticMesh() ||
149 !EdgeMesh->GetStaticMesh()->GetPathName().StartsWith(TEXT("/Game/Carla/Static/SideWalk/")) ||
150 FMath::Abs(EdgeHit.ImpactPoint.Z-Edge.Z) > 1.0)
151 { Status = FString::Printf(TEXT("Footing crosses a sidewalk edge or uneven surface at %s."), *Point.Id); return false; }
152 }
153 const FVector BasePosition = Position + FVector(0, 0, Offset*100.0);
154 Base.Transform = FTransform(Rotation, BasePosition, FVector(FootScale));
155 Base.Seed = static_cast<int32>(Seed);
156 Position = BasePosition + FVector(0, 0, SoilHeight*FootScale*100.0/Hit.ImpactNormal.Z);
157 Footing = MoveTemp(Base);
158 }
159 Point.Transform = FTransform(FRotator(0, Yaw, 0), Position, FVector(Scale));
160 Point.Seed = static_cast<int32>(Seed);
161 Ids.Add(Point.Id);
162 Candidate.Add(MoveTemp(Point));
163 if (Footing.IsSet())
164 {
165 if (Ids.Contains(Footing->Id)) { Status = TEXT("Duplicate footing ID."); return false; }
166 Ids.Add(Footing->Id);
167 Candidate.Add(MoveTemp(Footing.GetValue()));
168 }
169 }
170 Modify();
171 Points = MoveTemp(Candidate);
172 FBox Bounds(ForceInit);
173 for (const auto& Point : Points) Bounds += Point.Transform.GetLocation();
174 if (!Bounds.IsValid) Bounds = FBox(FVector(-100), FVector(100));
175 Bounds = Bounds.ExpandBy(1000.0);
176 GenerationBounds->SetWorldLocation(Bounds.GetCenter());
177 GenerationBounds->SetBoxExtent(Bounds.GetExtent());
178 bPreviewReady = false;
179 Status = FString::Printf(TEXT("Imported %d validated points. Preview, then Bake."), Points.Num());
180 return true;
181}
182
183bool AProceduralVegetationTool::IsGenerating() const { return PCG && PCG->IsGenerating(); }
184
185void AProceduralVegetationTool::TickCommandletGeneration()
186{
187 if (!IsRunningCommandlet()) return;
188 FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
189 if (UPCGSubsystem* Subsystem = GetWorld()->GetSubsystem<UPCGSubsystem>()) Subsystem->Tick(0.016f);
190}
191
192void AProceduralVegetationTool::Preview()
193{
194 if (IsGenerating()) { Status = TEXT("Generation already running."); return; }
195 UPCGGraphInterface* LoadedGraph = Graph.LoadSynchronous();
196 if (!LoadedGraph) { Status = TEXT("Missing PCG graph; run setup_vegetation_tool.py."); return; }
197 Modify();
198 bPreviewReady = false;
199 PCG->CleanupLocalImmediate(true);
200 for (UInstancedStaticMeshComponent* C : BakedComponents) if (C) C->SetVisibility(false);
201 PCG->SetGraph(LoadedGraph);
202#if WITH_EDITOR
203 // DirtyGenerated updates editor caches and is absent from packaged PCG.
204 // GenerateLocal(true) below already forces runtime regeneration.
205 PCG->DirtyGenerated(EPCGComponentDirtyFlag::All);
206#endif
207 Status = TEXT("Generating PCG vegetation preview...");
208 PCG->GenerateLocal(true);
209}
210
211int32 AProceduralVegetationTool::PreviewInstanceCount() const
212{
213 int32 Count = 0;
214 TArray<UInstancedStaticMeshComponent*> Components;
215 GetComponents(Components);
216 for (auto* C : Components) if (!BakedComponents.Contains(C)) Count += C->GetInstanceCount();
217 return Count;
218}
219
220void AProceduralVegetationTool::OnGenerated(UPCGComponent* Component)
221{
222 const int32 Count = PreviewInstanceCount();
223 bPreviewReady = Count == Points.Num();
224 ATagger::TagActor(*this, true);
225 Status = FString::Printf(TEXT("PCG preview: %d/%d instances. %s"), Count, Points.Num(),
226 bPreviewReady ? TEXT("Ready to bake.") : TEXT("Incomplete; inspect PCG errors."));
227 if (!bPreviewReady) for (UInstancedStaticMeshComponent* C : BakedComponents) if (C) C->SetVisibility(true);
228}
229
230bool AProceduralVegetationTool::Bake()
231{
232 if (IsGenerating() || !bPreviewReady || PreviewInstanceCount() != Points.Num())
233 { Status = TEXT("Generate a complete preview before baking."); return false; }
234 Modify();
235 // Upgrade existing tool actors too: static instances cannot attach to a
236 // movable root, and must follow the region actor's transform after baking.
237 RootComponent->Modify();
238 RootComponent->SetMobility(EComponentMobility::Static);
239 TArray<UInstancedStaticMeshComponent*> PreviewComponents;
240 GetComponents(PreviewComponents);
241 TArray<TObjectPtr<UInstancedStaticMeshComponent>> NewBaked;
242 int32 Count = 0;
243 for (auto* Source : PreviewComponents)
244 {
245 if (BakedComponents.Contains(Source) || Source->GetInstanceCount() == 0) continue;
246 auto* Dest = NewObject<UInstancedStaticMeshComponent>(this, NAME_None, RF_Transactional);
247 Dest->SetStaticMesh(Source->GetStaticMesh());
248 Dest->SetMobility(EComponentMobility::Static);
249 Dest->SetupAttachment(RootComponent);
250 Dest->SetRelativeTransform(FTransform::Identity);
251 Dest->SetCollisionProfileName(TEXT("BlockAll"));
252 Dest->SetGenerateOverlapEvents(false);
253 Dest->SetCanEverAffectNavigation(true);
254 for (int32 M=0; M<Source->GetNumMaterials(); ++M) Dest->SetMaterial(M, Source->GetMaterial(M));
255 AddInstanceComponent(Dest);
256 Dest->RegisterComponent();
257 for (int32 I=0; I<Source->GetInstanceCount(); ++I)
258 {
259 FTransform Transform;
260 Source->GetInstanceTransform(I, Transform, true);
261 Dest->AddInstance(Transform, true);
262 ++Count;
263 }
264 NewBaked.Add(Dest);
265 }
266 for (UInstancedStaticMeshComponent* Old : BakedComponents) if (Old) { RemoveInstanceComponent(Old); Old->DestroyComponent(); }
267 BakedComponents = MoveTemp(NewBaked);
268 BakedInstanceCount = Count;
269 PCG->CleanupLocalImmediate(true);
270 bPreviewReady = false;
271 ATagger::TagActor(*this, true);
272 MarkPackageDirty();
273 Status = FString::Printf(TEXT("Baked %d plant and surround instances. Save the level."), Count);
274 return Count == Points.Num();
275}
276
277void AProceduralVegetationTool::ClearGenerated()
278{
279 if (IsGenerating()) { Status = TEXT("Wait for generation to finish before clearing."); return; }
280 Modify();
281 PCG->CleanupLocalImmediate(true);
282 for (UInstancedStaticMeshComponent* C : BakedComponents) if (C) { RemoveInstanceComponent(C); C->DestroyComponent(); }
283 BakedComponents.Empty();
284 BakedInstanceCount = 0;
285 bPreviewReady = false;
286 Status = TEXT("Generated vegetation cleared; imported plan retained.");
287 MarkPackageDirty();
288}
289
291{
292 virtual bool CanExecuteOnlyOnMainThread(FPCGContext* Context) const override { return true; }
293 virtual bool IsCacheable(const UPCGSettings* Settings) const override { return false; }
294 virtual bool ExecuteInternal(FPCGContext* Context) const override
295 {
296 const auto* Tool = Cast<AProceduralVegetationTool>(Context->GetTargetActor(nullptr));
297 if (!Tool) return true;
298 UPCGPointData* Data = FPCGContext::NewObject_AnyThread<UPCGPointData>(Context);
299 auto* Mesh = Data->Metadata->CreateAttribute<FString>(TEXT("Mesh"), FString(), false, false);
300 auto* Id = Data->Metadata->CreateAttribute<FString>(TEXT("SourceId"), FString(), false, false);
301 auto* Provenance = Data->Metadata->CreateAttribute<FString>(TEXT("Provenance"), FString(), false, false);
302 auto& Output = Data->GetMutablePoints();
303 for (const auto& Record : Tool->Points)
304 {
305 FPCGPoint& Point = Output.Emplace_GetRef();
306 Point.Transform = Record.Transform;
307 Point.Seed = Record.Seed;
308 Point.Density = 1.0f;
309 Point.MetadataEntry = Data->Metadata->AddEntry();
310 Mesh->SetValue(Point.MetadataEntry, Record.Mesh.ToSoftObjectPath().ToString());
311 Id->SetValue(Point.MetadataEntry, Record.Id);
312 Provenance->SetValue(Point.MetadataEntry, Record.Provenance);
313 }
314 FPCGTaggedData& Tagged = Context->OutputData.TaggedData.Emplace_GetRef();
315 Tagged.Pin = PCGPinConstants::DefaultOutputLabel;
316 Tagged.Tags.Add(TEXT("Vegetation"));
317 Tagged.Data = Data;
318 return true;
319 }
320};
321
322FPCGElementPtr UPCGVegetationPlanSettings::CreateElement() const { return MakeShared<FPCGVegetationPlanElement>(); }
static void TagActor(const AActor &Actor, bool bTagForSemanticSegmentation)
Set the tag of an actor.
Definition Tagger.cpp:130
virtual bool ExecuteInternal(FPCGContext *Context) const override
virtual bool IsCacheable(const UPCGSettings *Settings) const override
virtual bool CanExecuteOnlyOnMainThread(FPCGContext *Context) const override
virtual FPCGElementPtr CreateElement() const override
void RotateVector(Vector3D &in_point) const
Definition Rotation.h:87
DoublyConnectedEdgeList Graph
geom::Transform Transform
geom::Rotation Rotation
std::vector< cg::Location > Path
bg::model::d2::point_xy< double > Point