CARLA
Loading...
Searching...
No Matches
ProceduralFurnitureTool.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("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;
41#endif
42}
43
44bool AProceduralFurnitureTool::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 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;
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(FurnitureGround), true, this);
66 for (TActorIterator<AProceduralFurnitureTool> 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, 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();
102 // Check catalog bounds against loaded assets, including off-center pivots.
103 for (const FString& Field : {FString(TEXT("min_m")), FString(TEXT("max_m"))})
104 {
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)
110 {
111 double Value;
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; }
114 }
115 }
116 FString ParentId;
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; }
121 if (bAttached)
122 {
123 FString GroupId;
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)
134 {
135 double Value;
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; }
138 }
139 const FCarlaFurniturePoint* Parent = Candidate.FindByPredicate([&](const FCarlaFurniturePoint& P){ return P.Id==ParentId; });
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; }
143 // Glass retains its authored elevated bounds. Banners and panels use the
144 // reviewed support-relative mount; none of these is independently grounded.
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);
148 Ids.Add(Point.Id);
149 Candidate.Add(MoveTemp(Point));
150 continue;
151 }
152 FVector Position = FVector(X,Y,Z)*100.0;
153 const FRotator Rotation(0,Yaw,0);
154 FQuat GroundRotation = Rotation.Quaternion();
155 double Heights[3][3];
156 double MinZ = TNumericLimits<double>::Max(), MaxZ = -TNumericLimits<double>::Max();
157 // Trace the footprint corners, edge midpoints and center. Never snap onto
158 // a road, another prop, a roof or a different elevation layer.
159 for (int I=-1; I<=1; ++I) for (int J=-1; J<=1; ++J)
160 {
161 const FVector Local(MeshBounds.Origin.X+I*MeshBounds.BoxExtent.X,
162 MeshBounds.Origin.Y+J*MeshBounds.BoxExtent.Y,0);
163 const FVector Probe = Position+Rotation.RotateVector(Local);
164 FHitResult Hit;
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);
174 }
175 double GroundZ = (MinZ+MaxZ)/2;
176 if (Kind == TEXT("bus_shelter"))
177 {
178 // Fit a mild pavement plane across the shelter's full support footprint.
179 // This preserves glass alignment while avoiding floating end legs.
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);
182 double Residual = 0;
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; }
187 const FVector Normal = Rotation.RotateVector(FVector(-SX,-SY,1).GetSafeNormal());
188 GroundRotation = FRotationMatrix::MakeFromZX(Normal,Rotation.Vector()).ToQuat();
189 GroundZ = Heights[1][1]-MeshBounds.Origin.X*SX-MeshBounds.Origin.Y*SY;
190 }
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);
196 Ids.Add(Point.Id);
197 Candidate.Add(MoveTemp(Point));
198 }
199
200 Modify();
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());
210 return true;
211}
212
213bool AProceduralFurnitureTool::IsGenerating() const { return PCG && PCG->IsGenerating(); }
214
215void AProceduralFurnitureTool::TickCommandletGeneration()
216{
217 if (!IsRunningCommandlet()) return;
218 FTaskGraphInterface::Get().ProcessThreadUntilIdle(ENamedThreads::GameThread);
219 if (UPCGSubsystem* Subsystem = GetWorld()->GetSubsystem<UPCGSubsystem>()) Subsystem->Tick(0.016f);
220}
221
222void AProceduralFurnitureTool::Preview()
223{
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; }
227 Modify();
228 bPreviewReady = false;
229 PCG->CleanupLocalImmediate(true);
230 for (UInstancedStaticMeshComponent* C : BakedComponents) if (C) C->SetVisibility(false);
231 PCG->SetGraph(LoadedGraph);
232#if WITH_EDITOR
233 // DirtyGenerated updates editor caches and is absent from packaged PCG.
234 // GenerateLocal(true) below already forces runtime regeneration.
235 PCG->DirtyGenerated(EPCGComponentDirtyFlag::All);
236#endif
237 Status = TEXT("Generating PCG furniture preview...");
238 PCG->GenerateLocal(true);
239}
240
241int32 AProceduralFurnitureTool::PreviewInstanceCount() const
242{
243 int32 Count = 0;
244 TArray<UInstancedStaticMeshComponent*> Components;
245 GetComponents(Components);
246 for (auto* C : Components) if (!BakedComponents.Contains(C)) Count += C->GetInstanceCount();
247 return Count;
248}
249
250void AProceduralFurnitureTool::OnGenerated(UPCGComponent* Component)
251{
252 const int32 Count = PreviewInstanceCount();
253 bPreviewReady = Count == Points.Num();
254 ATagger::TagActor(*this, true);
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);
258}
259
260bool AProceduralFurnitureTool::Bake()
261{
262 if (IsGenerating() || !bPreviewReady || PreviewInstanceCount() != Points.Num())
263 { Status = TEXT("Generate a complete preview before baking."); return false; }
264 Modify();
265 // Upgrade existing tool actors too: static instances cannot attach to a
266 // movable root, and must follow the region actor's transform after baking.
267 RootComponent->Modify();
268 RootComponent->SetMobility(EComponentMobility::Static);
269 TArray<UInstancedStaticMeshComponent*> PreviewComponents;
270 GetComponents(PreviewComponents);
271 TArray<TObjectPtr<UInstancedStaticMeshComponent>> NewBaked;
272 int32 Count = 0;
273 for (auto* Source : PreviewComponents)
274 {
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)
288 {
289 FTransform Transform;
290 Source->GetInstanceTransform(I, Transform, true);
291 Dest->AddInstance(Transform, true);
292 ++Count;
293 }
294 NewBaked.Add(Dest);
295 }
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;
301 ATagger::TagActor(*this, true);
302 MarkPackageDirty();
303 Status = FString::Printf(TEXT("Baked %d furniture instances. Save the level."), Count);
304 return Count == Points.Num();
305}
306
307void AProceduralFurnitureTool::ClearGenerated()
308{
309 if (IsGenerating()) { Status = TEXT("Wait for generation to finish before clearing."); return; }
310 Modify();
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.");
317 MarkPackageDirty();
318}
319
321{
322 virtual bool CanExecuteOnlyOnMainThread(FPCGContext* Context) const override { return true; }
323 virtual bool IsCacheable(const UPCGSettings* Settings) const override { return false; }
324 virtual bool ExecuteInternal(FPCGContext* Context) const override
325 {
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)
334 {
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);
343 }
344 FPCGTaggedData& Tagged = Context->OutputData.TaggedData.Emplace_GetRef();
345 Tagged.Pin = PCGPinConstants::DefaultOutputLabel;
346 Tagged.Tags.Add(TEXT("Furniture"));
347 Tagged.Data = Data;
348 return true;
349 }
350};
351
352FPCGElementPtr UPCGFurniturePlanSettings::CreateElement() const { return MakeShared<FPCGFurniturePlanElement>(); }
static void TagActor(const AActor &Actor, bool bTagForSemanticSegmentation)
Set the tag of an actor.
Definition Tagger.cpp:130
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
Definition Rotation.h:87
DoublyConnectedEdgeList Graph
geom::Transform Transform
geom::Rotation Rotation
bg::model::d2::point_xy< double > Point