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(
"FurniturePCG"));
35 PCG->GenerationTrigger = EPCGComponentGenerationTrigger::GenerateOnDemand;
36 PCG->OnPCGGraphGeneratedExternal.AddDynamic(
this, &AProceduralFurnitureTool::OnGenerated);
37 Graph = TSoftObjectPtr<UPCGGraphInterface>(FSoftObjectPath(TEXT(
"/Game/Carla/Blueprints/PCG/FurnitureTool/PCG_TwinFurniture.PCG_TwinFurniture")));
38 PrimaryActorTick.bCanEverTick =
false;
39#if WITH_EDITORONLY_DATA
40 bIsSpatiallyLoaded =
false;
44bool AProceduralFurnitureTool::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 furniture 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-furniture-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<FCarlaFurniturePoint> Candidate;
63 FStaticMeshCompilingManager::Get().FinishAllCompilation();
65 FCollisionQueryParams Query(SCENE_QUERY_STAT(FurnitureGround),
true,
this);
66 for (TActorIterator<AProceduralFurnitureTool> 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, SupportDelta;
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) ||
78 !Obj->TryGetStringField(TEXT(
"source"),
Point.Provenance) ||
79 !Obj->TryGetStringField(TEXT(
"kind"), Kind) ||
80 !((Kind == TEXT(
"bench") && MeshPath == TEXT(
"/Game/Carla/Static/Static/SM_Bench01")) ||
81 (Kind == TEXT(
"bin") && MeshPath == TEXT(
"/Game/Carla/Static/Static/SM_TrashCan03")) ||
82 (Kind == TEXT(
"bus_shelter") && MeshPath == TEXT(
"/Game/Carla/Static/Static/Materials/BusStop02/SM_BusStop02")) ||
83 (Kind == TEXT(
"bus_glass") && MeshPath == TEXT(
"/Game/Carla/Static/Static/Materials/BusStop02/SM_BusStop02_Glass")) ||
84 (Kind == TEXT(
"bus_pole") && MeshPath == TEXT(
"/Game/Carla/Static/Pole/SM_Pole08")) ||
85 (Kind == TEXT(
"bus_panel") && MeshPath == TEXT(
"/Game/Carla/Static/TrafficSign/TwinFurniture/SM_BusStopMarker")) ||
86 (Kind == TEXT(
"banner_pole") && MeshPath == TEXT(
"/Game/Carla/Static/Pole/SM_Pole05")) ||
87 (Kind == TEXT(
"banner") && MeshPath == TEXT(
"/Game/Carla/Static/Other/Banner/SM_Banner01"))) ||
88 !Obj->TryGetNumberField(TEXT(
"max_support_delta_m"), SupportDelta) ||
89 !FMath::IsFinite(SupportDelta) || SupportDelta <= 0 || SupportDelta > 0.05 ||
90 !Obj->TryGetNumberField(TEXT(
"x"), X) || !Obj->TryGetNumberField(TEXT(
"y"), Y) ||
91 !Obj->TryGetNumberField(TEXT(
"z"), Z) || !Obj->TryGetNumberField(TEXT(
"scale"), Scale) ||
92 !Obj->TryGetNumberField(TEXT(
"yaw"), Yaw) || !Obj->TryGetNumberField(TEXT(
"seed"), Seed) ||
93 !FMath::IsFinite(X) || !FMath::IsFinite(Y) || !FMath::IsFinite(Z) ||
94 !FMath::IsFinite(Scale) || Scale != 1.0 || !FMath::IsFinite(Yaw) ||
95 !FMath::IsFinite(Seed) || Seed < 0 || Seed > MAX_int32)
96 { Status = TEXT(
"Invalid or duplicate furniture point; previous plan retained.");
return false; }
97 if (!MeshPath.Contains(TEXT(
"."))) MeshPath += TEXT(
".") + FPackageName::GetShortName(MeshPath);
98 Point.Mesh = TSoftObjectPtr<UStaticMesh>(FSoftObjectPath(MeshPath));
99 if (!
Point.Mesh.LoadSynchronous())
100 { Status = FString::Printf(TEXT(
"Missing mesh: %s; previous plan retained."), *MeshPath);
return false; }
101 const FBoxSphereBounds MeshBounds =
Point.Mesh.Get()->GetBounds();
103 for (
const FString& Field : {FString(TEXT(
"min_m")), FString(TEXT(
"max_m"))})
105 const TArray<TSharedPtr<FJsonValue>>* Values =
nullptr;
106 if (!Obj->TryGetArrayField(Field, Values) || Values->Num() != 3)
107 { Status = TEXT(
"Missing measured mesh bounds.");
return false; }
108 const FVector Expected = Field == TEXT(
"min_m") ? MeshBounds.Origin-MeshBounds.BoxExtent : MeshBounds.Origin+MeshBounds.BoxExtent;
109 for (
int Axis=0; Axis<3; ++Axis)
112 if (!(*Values)[Axis]->TryGetNumber(Value) || !FMath::IsFinite(Value) || FMath::Abs(Value*100-Expected[Axis]) > 0.1)
113 { Status = TEXT(
"Catalog bounds changed; regenerate the furniture plan.");
return false; }
117 Obj->TryGetStringField(TEXT(
"parent_id"), ParentId);
118 const bool bAttached = Kind == TEXT(
"bus_glass") || Kind == TEXT(
"bus_panel") || Kind == TEXT(
"banner");
119 if (bAttached != !ParentId.IsEmpty())
120 { Status = TEXT(
"Furniture attachment is missing or unexpected.");
return false; }
124 const FString ParentKind = Kind == TEXT(
"bus_glass") ? TEXT(
"bus_shelter") :
125 Kind == TEXT(
"bus_panel") ? TEXT(
"bus_pole") : TEXT(
"banner_pole");
126 const double Height = Kind == TEXT(
"bus_glass") ? 0.0 : Kind == TEXT(
"bus_panel") ? 2.05 : 4.47;
127 const FVector MountOffset(0,Kind == TEXT(
"bus_panel") ? 0.025 : 0.0,Height);
128 const double MountYaw = 0.0;
129 const TArray<TSharedPtr<FJsonValue>>* Offset =
nullptr;
130 if (!Obj->TryGetStringField(TEXT(
"group_id"), GroupId) || ParentId != GroupId+TEXT(
"/")+ParentKind ||
131 Point.Id != GroupId+TEXT(
"/")+Kind || !Obj->TryGetArrayField(TEXT(
"attachment_m"), Offset) || Offset->Num()!=3)
132 { Status = TEXT(
"Invalid furniture assembly parent.");
return false; }
133 for (
int Axis=0; Axis<3; ++Axis)
136 if (!(*Offset)[Axis]->TryGetNumber(Value) || !FMath::IsFinite(Value) || FMath::Abs(Value-MountOffset[Axis])>0.0001)
137 { Status = TEXT(
"Unreviewed furniture attachment offset.");
return false; }
140 if (!Parent || FVector2D(Parent->Transform.TransformPosition(MountOffset*100)-FVector(X,Y,Z)*100).Size()>0.1 ||
141 FMath::Abs(FMath::FindDeltaAngleDegrees(Parent->Transform.Rotator().Yaw+MountYaw,Yaw))>0.1)
142 { Status = FString::Printf(TEXT(
"Assembly parent pose mismatch for %s: parent=%s yaw=%.4f expected=%.4f"), *
Point.Id, *ParentId, Parent ? Parent->Transform.Rotator().Yaw : 0.0, Yaw);
return false; }
145 Point.Transform = FTransform(Parent->Transform.GetRotation()*FRotator(0,MountYaw,0).Quaternion(),
146 Parent->Transform.TransformPosition(MountOffset*100),FVector(1));
147 Point.Seed =
static_cast<int32
>(Seed);
149 Candidate.Add(MoveTemp(Point));
152 FVector
Position = FVector(X,Y,Z)*100.0;
154 FQuat GroundRotation =
Rotation.Quaternion();
155 double Heights[3][3];
156 double MinZ = TNumericLimits<double>::Max(), MaxZ = -TNumericLimits<double>::Max();
159 for (
int I=-1; I<=1; ++I)
for (
int J=-1; J<=1; ++J)
161 const FVector Local(MeshBounds.Origin.X+I*MeshBounds.BoxExtent.X,
162 MeshBounds.Origin.Y+J*MeshBounds.BoxExtent.Y,0);
165 const bool bHit = GetWorld()->LineTraceSingleByChannel(Hit, Probe+FVector(0,0,30), Probe-FVector(0,0,30), ECC_Visibility, Query);
166 const auto*
Ground = bHit ? Cast<UStaticMeshComponent>(Hit.GetComponent()) : nullptr;
167 if (!Ground || !
Ground->GetStaticMesh() ||
168 !
Ground->GetStaticMesh()->GetPathName().StartsWith(TEXT(
"/Game/Carla/Static/SideWalk/")) ||
169 FMath::Abs(Hit.ImpactPoint.Z-
Position.Z)>15.0)
170 { Status = FString::Printf(TEXT(
"Unsupported furniture footprint: %s"), *
Point.Id);
return false; }
171 Heights[I+1][J+1] = Hit.ImpactPoint.Z;
172 MinZ = FMath::Min(MinZ, Hit.ImpactPoint.Z);
173 MaxZ = FMath::Max(MaxZ, Hit.ImpactPoint.Z);
175 double GroundZ = (MinZ+MaxZ)/2;
176 if (Kind == TEXT(
"bus_shelter"))
180 const double SX = (Heights[2][1]-Heights[0][1])/(2*MeshBounds.BoxExtent.X);
181 const double SY = (Heights[1][2]-Heights[1][0])/(2*MeshBounds.BoxExtent.Y);
183 for (
int I=-1; I<=1; ++I)
for (
int J=-1; J<=1; ++J)
184 Residual = FMath::Max(Residual,FMath::Abs(Heights[I+1][J+1]-Heights[1][1]-I*MeshBounds.BoxExtent.X*SX-J*MeshBounds.BoxExtent.Y*SY));
185 if (FMath::RadiansToDegrees(FMath::Atan(FMath::Sqrt(SX*SX+SY*SY)))>3 || Residual>SupportDelta*100)
186 { Status = FString::Printf(TEXT(
"Uneven furniture support: %s (shelter plane)"), *
Point.Id);
return false; }
188 GroundRotation = FRotationMatrix::MakeFromZX(Normal,
Rotation.Vector()).ToQuat();
189 GroundZ = Heights[1][1]-MeshBounds.Origin.X*SX-MeshBounds.Origin.Y*SY;
191 else if (MaxZ-MinZ > SupportDelta*100)
192 { Status = FString::Printf(TEXT(
"Uneven furniture support: %s (%.3f m)"), *
Point.Id, (MaxZ-MinZ)/100);
return false; }
193 Position.Z = GroundZ - (MeshBounds.Origin.Z-MeshBounds.BoxExtent.Z);
194 Point.Transform = FTransform(GroundRotation,
Position, FVector(Scale));
195 Point.Seed =
static_cast<int32
>(Seed);
197 Candidate.Add(MoveTemp(Point));
201 Points = MoveTemp(Candidate);
202 FBox Bounds(ForceInit);
203 for (
const auto& Point : Points) Bounds +=
Point.
Transform.GetLocation();
204 if (!Bounds.IsValid) Bounds = FBox(FVector(-100), FVector(100));
205 Bounds = Bounds.ExpandBy(1000.0);
206 GenerationBounds->SetWorldLocation(Bounds.GetCenter());
207 GenerationBounds->SetBoxExtent(Bounds.GetExtent());
208 bPreviewReady =
false;
209 Status = FString::Printf(TEXT(
"Imported %d validated points. Preview, then Bake."), Points.Num());
213bool AProceduralFurnitureTool::IsGenerating()
const {
return PCG && PCG->IsGenerating(); }
215void AProceduralFurnitureTool::TickCommandletGeneration()
217 if (!IsRunningCommandlet())
return;
218 FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
219 if (UPCGSubsystem* Subsystem = GetWorld()->GetSubsystem<UPCGSubsystem>()) Subsystem->Tick(0.016f);
222void AProceduralFurnitureTool::Preview()
224 if (IsGenerating()) { Status = TEXT(
"Generation already running.");
return; }
225 UPCGGraphInterface* LoadedGraph =
Graph.LoadSynchronous();
226 if (!LoadedGraph) { Status = TEXT(
"Missing PCG graph; run setup_furniture_tool.py.");
return; }
228 bPreviewReady =
false;
229 PCG->CleanupLocalImmediate(
true);
230 for (UInstancedStaticMeshComponent* C : BakedComponents) if (C) C->SetVisibility(false);
231 PCG->SetGraph(LoadedGraph);
235 PCG->DirtyGenerated(EPCGComponentDirtyFlag::All);
237 Status = TEXT(
"Generating PCG furniture preview...");
238 PCG->GenerateLocal(
true);
241int32 AProceduralFurnitureTool::PreviewInstanceCount()
const
244 TArray<UInstancedStaticMeshComponent*> Components;
245 GetComponents(Components);
246 for (
auto* C : Components) if (!BakedComponents.Contains(C)) Count += C->GetInstanceCount();
250void AProceduralFurnitureTool::OnGenerated(UPCGComponent* Component)
252 const int32 Count = PreviewInstanceCount();
253 bPreviewReady = Count == Points.Num();
255 Status = FString::Printf(TEXT(
"PCG preview: %d/%d instances. %s"), Count, Points.Num(),
256 bPreviewReady ? TEXT(
"Ready to bake.") : TEXT(
"Incomplete; inspect PCG errors."));
257 if (!bPreviewReady)
for (UInstancedStaticMeshComponent* C : BakedComponents) if (C) C->SetVisibility(true);
260bool AProceduralFurnitureTool::Bake()
262 if (IsGenerating() || !bPreviewReady || PreviewInstanceCount() != Points.Num())
263 { Status = TEXT(
"Generate a complete preview before baking.");
return false; }
267 RootComponent->Modify();
268 RootComponent->SetMobility(EComponentMobility::Static);
269 TArray<UInstancedStaticMeshComponent*> PreviewComponents;
270 GetComponents(PreviewComponents);
271 TArray<TObjectPtr<UInstancedStaticMeshComponent>> NewBaked;
273 for (
auto* Source : PreviewComponents)
275 if (BakedComponents.Contains(Source) || Source->GetInstanceCount() == 0)
continue;
276 auto* Dest = NewObject<UInstancedStaticMeshComponent>(
this, NAME_None, RF_Transactional);
277 Dest->SetStaticMesh(Source->GetStaticMesh());
278 Dest->SetMobility(EComponentMobility::Static);
279 Dest->SetupAttachment(RootComponent);
280 Dest->SetRelativeTransform(FTransform::Identity);
281 Dest->SetCollisionProfileName(TEXT(
"BlockAll"));
282 Dest->SetGenerateOverlapEvents(
false);
283 Dest->SetCanEverAffectNavigation(
true);
284 for (int32 M=0; M<Source->GetNumMaterials(); ++M) Dest->SetMaterial(M, Source->GetMaterial(M));
285 AddInstanceComponent(Dest);
286 Dest->RegisterComponent();
287 for (int32 I=0; I<Source->GetInstanceCount(); ++I)
290 Source->GetInstanceTransform(I, Transform,
true);
291 Dest->AddInstance(Transform,
true);
296 for (UInstancedStaticMeshComponent* Old : BakedComponents) if (Old) { RemoveInstanceComponent(Old); Old->DestroyComponent(); }
297 BakedComponents = MoveTemp(NewBaked);
298 BakedInstanceCount = Count;
299 PCG->CleanupLocalImmediate(
true);
300 bPreviewReady =
false;
303 Status = FString::Printf(TEXT(
"Baked %d furniture instances. Save the level."), Count);
304 return Count == Points.Num();
307void AProceduralFurnitureTool::ClearGenerated()
309 if (IsGenerating()) { Status = TEXT(
"Wait for generation to finish before clearing.");
return; }
311 PCG->CleanupLocalImmediate(
true);
312 for (UInstancedStaticMeshComponent* C : BakedComponents) if (C) { RemoveInstanceComponent(C); C->DestroyComponent(); }
313 BakedComponents.Empty();
314 BakedInstanceCount = 0;
315 bPreviewReady =
false;
316 Status = TEXT(
"Generated furniture cleared; imported plan retained.");
326 const auto* Tool = Cast<AProceduralFurnitureTool>(Context->GetTargetActor(
nullptr));
327 if (!Tool)
return true;
328 UPCGPointData* Data = FPCGContext::NewObject_AnyThread<UPCGPointData>(Context);
329 auto* Mesh = Data->Metadata->CreateAttribute<FString>(TEXT(
"Mesh"), FString(),
false,
false);
330 auto* Id = Data->Metadata->CreateAttribute<FString>(TEXT(
"SourceId"), FString(),
false,
false);
331 auto* Provenance = Data->Metadata->CreateAttribute<FString>(TEXT(
"Provenance"), FString(),
false,
false);
332 auto& Output = Data->GetMutablePoints();
333 for (
const auto& Record : Tool->Points)
335 FPCGPoint& Point = Output.Emplace_GetRef();
336 Point.Transform = Record.Transform;
337 Point.Seed = Record.Seed;
338 Point.Density = 1.0f;
339 Point.MetadataEntry = Data->Metadata->AddEntry();
340 Mesh->SetValue(Point.MetadataEntry, Record.Mesh.ToSoftObjectPath().ToString());
341 Id->SetValue(Point.MetadataEntry, Record.Id);
342 Provenance->SetValue(Point.MetadataEntry, Record.Provenance);
344 FPCGTaggedData& Tagged = Context->OutputData.TaggedData.Emplace_GetRef();
345 Tagged.Pin = PCGPinConstants::DefaultOutputLabel;
346 Tagged.Tags.Add(TEXT(
"Furniture"));
static void TagActor(const AActor &Actor, bool bTagForSemanticSegmentation)
Set the tag of an actor.
virtual bool CanExecuteOnlyOnMainThread(FPCGContext *Context) const override
virtual bool ExecuteInternal(FPCGContext *Context) const override
virtual bool IsCacheable(const UPCGSettings *Settings) const override
virtual FPCGElementPtr CreateElement() const override
void RotateVector(Vector3D &in_point) const
DoublyConnectedEdgeList Graph
geom::Transform Transform
bg::model::d2::point_xy< double > Point