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"
10#include "Metadata/PCGMetadata.h"
11#include "Metadata/PCGMetadataAttributeTpl.h"
12#include "Misc/FileHelper.h"
13#include "PCGComponent.h"
14#include "PCGContext.h"
16#include "PCGSubsystem.h"
17#include "Async/TaskGraphInterfaces.h"
18#include "Serialization/JsonReader.h"
19#include "Serialization/JsonSerializer.h"
21#include "StaticMeshCompiler.h"
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;
44bool AProceduralVegetationTool::ImportPlan()
46 if (IsGenerating()) { Status = TEXT(
"Wait for PCG generation to finish.");
return false; }
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;
63 FStaticMeshCompilingManager::Get().FinishAllCompilation();
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)
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; }
101 FVector
Position = FVector(X,Y,Z)*100.0;
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; }
110 TOptional<FCarlaVegetationPoint> Footing;
111 const TSharedPtr<FJsonObject>* FootingJson =
nullptr;
112 if (Obj->TryGetObjectField(TEXT(
"footing"), FootingJson))
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; }
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)
143 const double A = I*PI/4;
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; }
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);
159 Point.Transform = FTransform(FRotator(0, Yaw, 0),
Position, FVector(Scale));
160 Point.Seed =
static_cast<int32
>(Seed);
162 Candidate.Add(MoveTemp(Point));
165 if (Ids.Contains(Footing->Id)) { Status = TEXT(
"Duplicate footing ID.");
return false; }
166 Ids.Add(Footing->Id);
167 Candidate.Add(MoveTemp(Footing.GetValue()));
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());
183bool AProceduralVegetationTool::IsGenerating()
const {
return PCG && PCG->IsGenerating(); }
185void AProceduralVegetationTool::TickCommandletGeneration()
187 if (!IsRunningCommandlet())
return;
188 FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
189 if (UPCGSubsystem* Subsystem = GetWorld()->GetSubsystem<UPCGSubsystem>()) Subsystem->Tick(0.016f);
192void AProceduralVegetationTool::Preview()
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; }
198 bPreviewReady =
false;
199 PCG->CleanupLocalImmediate(
true);
200 for (UInstancedStaticMeshComponent* C : BakedComponents) if (C) C->SetVisibility(false);
201 PCG->SetGraph(LoadedGraph);
205 PCG->DirtyGenerated(EPCGComponentDirtyFlag::All);
207 Status = TEXT(
"Generating PCG vegetation preview...");
208 PCG->GenerateLocal(
true);
211int32 AProceduralVegetationTool::PreviewInstanceCount()
const
214 TArray<UInstancedStaticMeshComponent*> Components;
215 GetComponents(Components);
216 for (
auto* C : Components) if (!BakedComponents.Contains(C)) Count += C->GetInstanceCount();
220void AProceduralVegetationTool::OnGenerated(UPCGComponent* Component)
222 const int32 Count = PreviewInstanceCount();
223 bPreviewReady = Count == Points.Num();
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);
230bool AProceduralVegetationTool::Bake()
232 if (IsGenerating() || !bPreviewReady || PreviewInstanceCount() != Points.Num())
233 { Status = TEXT(
"Generate a complete preview before baking.");
return false; }
237 RootComponent->Modify();
238 RootComponent->SetMobility(EComponentMobility::Static);
239 TArray<UInstancedStaticMeshComponent*> PreviewComponents;
240 GetComponents(PreviewComponents);
241 TArray<TObjectPtr<UInstancedStaticMeshComponent>> NewBaked;
243 for (
auto* Source : PreviewComponents)
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)
260 Source->GetInstanceTransform(I, Transform,
true);
261 Dest->AddInstance(Transform,
true);
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;
273 Status = FString::Printf(TEXT(
"Baked %d plant and surround instances. Save the level."), Count);
274 return Count == Points.Num();
277void AProceduralVegetationTool::ClearGenerated()
279 if (IsGenerating()) { Status = TEXT(
"Wait for generation to finish before clearing.");
return; }
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.");
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)
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);
314 FPCGTaggedData& Tagged = Context->OutputData.TaggedData.Emplace_GetRef();
315 Tagged.Pin = PCGPinConstants::DefaultOutputLabel;
316 Tagged.Tags.Add(TEXT(
"Vegetation"));
static void TagActor(const AActor &Actor, bool bTagForSemanticSegmentation)
Set the tag of an actor.
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
DoublyConnectedEdgeList Graph
geom::Transform Transform
std::vector< cg::Location > Path
bg::model::d2::point_xy< double > Point