CARLA
Loading...
Searching...
No Matches
LightDefaultsJsonUtils.cpp
Go to the documentation of this file.
1// Copyright (c) 2026 Computer Vision Center (CVC) at the Universitat Autonoma
2// de Barcelona (UAB).
3//
4// This work is licensed under the terms of the MIT license.
5// For a copy, see <https://opensource.org/licenses/MIT>.
6
8
11
12#include "Components/InstancedStaticMeshComponent.h"
13#include "Components/LightComponent.h"
14#include "Components/StaticMeshComponent.h"
15#include "Dom/JsonObject.h"
16#include "Engine/Engine.h"
17#include "Engine/Level.h"
18#include "Engine/World.h"
19#include "EngineUtils.h"
20#include "HAL/FileManager.h"
21#include "JsonObjectConverter.h"
22#include "Kismet/KismetMaterialLibrary.h"
23#include "Materials/MaterialInstanceDynamic.h"
24#include "Materials/MaterialInterface.h"
25#include "Materials/MaterialParameterCollection.h"
26#include "Misc/FileHelper.h"
27#include "Serialization/JsonSerializer.h"
28#include "UObject/UObjectIterator.h"
29
30#if WITH_EDITOR
31#include "Editor.h"
32#include "LevelEditorViewport.h"
33#endif
34
35namespace
36{
37 TSharedRef<FJsonObject> LoadLightJsonRoot(const FString& Path)
38 {
39 FString InputString;
40 TSharedPtr<FJsonObject> Root;
41 if (FFileHelper::LoadFileToString(InputString, *Path))
42 {
43 TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(InputString);
44 FJsonSerializer::Deserialize(Reader, Root);
45 }
46 return Root.IsValid() ? Root.ToSharedRef() : MakeShared<FJsonObject>();
47 }
48
49 bool SaveLightJsonRoot(const TSharedRef<FJsonObject>& Root, const FString& Path)
50 {
51 FString OutputString;
52 TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString);
53 if (!FJsonSerializer::Serialize(Root, Writer))
54 return false;
55 IFileManager::Get().MakeDirectory(*FPaths::GetPath(Path), true);
56 return FFileHelper::SaveStringToFile(OutputString, *Path);
57 }
58
59 // "perClass" or "perGroup" sub-object of the shared Defaults.json,
60 // creating it if this is the first default saved in that catalog.
61 TSharedRef<FJsonObject> GetOrCreateLightCatalog(const TSharedRef<FJsonObject>& Root, const TCHAR* CatalogKey)
62 {
63 const TSharedPtr<FJsonObject>* Existing = nullptr;
64 if (Root->TryGetObjectField(CatalogKey, Existing) && Existing->IsValid())
65 return Existing->ToSharedRef();
66 TSharedRef<FJsonObject> Catalog = MakeShared<FJsonObject>();
67 Root->SetObjectField(CatalogKey, Catalog);
68 return Catalog;
69 }
70
71 bool SaveDefault(const FLightAssetDefault& Default, const FString& Key, const TCHAR* CatalogKey)
72 {
73 TSharedRef<FJsonObject> Root = LoadLightJsonRoot(ULightDefaultsJsonUtils::GetLightDefaultsPath());
74 TSharedRef<FJsonObject> Catalog = GetOrCreateLightCatalog(Root, CatalogKey);
75
76 TSharedPtr<FJsonObject> Object = FJsonObjectConverter::UStructToJsonObject(Default);
77 if (!Object.IsValid())
78 return false;
79 Catalog->SetObjectField(Key, Object);
80
81 return SaveLightJsonRoot(Root, ULightDefaultsJsonUtils::GetLightDefaultsPath());
82 }
83
84 bool LoadDefault(FLightAssetDefault& OutDefault, const FString& Key, const TCHAR* CatalogKey)
85 {
86 TSharedRef<FJsonObject> Root = LoadLightJsonRoot(ULightDefaultsJsonUtils::GetLightDefaultsPath());
87 const TSharedPtr<FJsonObject>* Catalog = nullptr;
88 if (!Root->TryGetObjectField(CatalogKey, Catalog) || !Catalog->IsValid())
89 return false;
90
91 const TSharedPtr<FJsonObject>* Object = nullptr;
92 if (!(*Catalog)->TryGetObjectField(Key, Object) || !Object->IsValid())
93 return false;
94
95 return FJsonObjectConverter::JsonObjectToUStruct(Object->ToSharedRef(), &OutDefault);
96 }
97
98 // Calls Func on every MaterialInstanceDynamic already assigned to one of
99 // TrafficLight's StaticMeshComponent slots that exposes a "lightvalue"
100 // scalar parameter -- i.e. a signal lens material, not the housing/body.
101 // Never creates a MID (see ApplyTrafficLightDefaultToClassLive): only
102 // touches what BP_TLOpenDrive's Construction Script already made and
103 // still holds array references to.
104 template <typename FuncT>
105 void ForEachTrafficLightSignalMID(AActor* TrafficLight, FuncT&& Func)
106 {
107 TArray<UStaticMeshComponent*> MeshComponents;
108 TrafficLight->GetComponents<UStaticMeshComponent>(MeshComponents);
109 for (UStaticMeshComponent* MeshComponent : MeshComponents)
110 {
111 const int32 NumMaterials = MeshComponent->GetNumMaterials();
112 for (int32 MaterialIndex = 0; MaterialIndex < NumMaterials; ++MaterialIndex)
113 {
114 if (UMaterialInstanceDynamic* MID = Cast<UMaterialInstanceDynamic>(MeshComponent->GetMaterial(MaterialIndex)))
115 {
116 float Dummy = 0.0f;
117 if (MID->GetScalarParameterValue(TEXT("lightvalue"), Dummy))
118 {
119 Func(MID);
120 }
121 }
122 }
123 }
124 }
125
126 const FString ProceduralBuildingsKey(TEXT("ProceduralBuildings"));
127 const FString LegacyBuildingsKey(TEXT("LegacyBuildings"));
128
129 // Pulls a "<Letters><Digits>" family token out of a building material's
130 // own name where one exists (e.g. "MI_Apt20_withEmissive" -> "Apt20",
131 // "MI_Apt21" -> "Apt21") -- the naming convention floor/shape variants
132 // of the same family share (BP_Apt20_A_C..F_C all reference some
133 // MI_Apt20* material). Empty if the name has no digit-suffixed word.
134 FString ExtractNumberedFamilyKey(const FString& MaterialName)
135 {
136 for (int32 Idx = 0; Idx < MaterialName.Len(); ++Idx)
137 {
138 if (!FChar::IsAlpha(MaterialName[Idx]))
139 continue;
140 if (Idx > 0 && FChar::IsAlpha(MaterialName[Idx - 1]))
141 continue; // not the start of a word
142
143 int32 WordEnd = Idx;
144 while (WordEnd < MaterialName.Len() && FChar::IsAlpha(MaterialName[WordEnd]))
145 ++WordEnd;
146 int32 DigitsEnd = WordEnd;
147 while (DigitsEnd < MaterialName.Len() && FChar::IsDigit(MaterialName[DigitsEnd]))
148 ++DigitsEnd;
149 if (DigitsEnd > WordEnd)
150 return MaterialName.Mid(Idx, DigitsEnd - Idx);
151 }
152 return FString();
153 }
154
155 // True (and fills OutFamilyKey) if Owner has any StaticMeshComponent
156 // material living under /Game/Carla/Static/Building/ -- a real
157 // light-driven building (BP_Apt*, BP_Factory*, BP_Museum*, ...), not
158 // just something an artist happened to tag LightGroup::Building/Other.
159 // Unwraps a live-edited slot's MID back to its static parent the same
160 // way ResolveStaticProceduralBuildingAsset does, so a repeat scan after
161 // an edit still recognizes it.
162 //
163 // Scans every matching slot (not just the first): confirmed the Apt12
164 // family's slot order isn't consistent between variants (Apt12_A's
165 // first building slot is MI_Apt12_FakeInterior, Apt12_B's is plain
166 // MI_Apt12), so picking only the first slot could group inconsistently.
167 // Prefers a digit-suffixed family match wherever found (Apt-style);
168 // falls back to the first matching slot's exact material name otherwise
169 // -- confirmed BP_Factory01_C/02_C/03_C and BP_Museum01_C/02_C share one
170 // identically-named MI_Factory/MI_Museum with no digit suffix at all, so
171 // the exact name alone is what groups them.
172 bool TryGetLightBuildingFamilyKey(const AActor* Owner, FString& OutFamilyKey)
173 {
174 if (Owner == nullptr)
175 return false;
176
177 TArray<UStaticMeshComponent*> MeshComponents;
178 Owner->GetComponents<UStaticMeshComponent>(MeshComponents);
179
180 FString FirstMatchedMaterialName;
181 for (UStaticMeshComponent* MeshComponent : MeshComponents)
182 {
183 const int32 NumMaterials = MeshComponent->GetNumMaterials();
184 for (int32 MaterialIndex = 0; MaterialIndex < NumMaterials; ++MaterialIndex)
185 {
186 UMaterialInterface* Material = MeshComponent->GetMaterial(MaterialIndex);
187 if (UMaterialInstanceDynamic* MID = Cast<UMaterialInstanceDynamic>(Material))
188 Material = MID->Parent;
189 if (Material == nullptr)
190 continue;
191 if (!Material->GetPathName().StartsWith(TEXT("/Game/Carla/Static/Building/")))
192 continue;
193
194 if (FirstMatchedMaterialName.IsEmpty())
195 FirstMatchedMaterialName = Material->GetName();
196
197 const FString Family = ExtractNumberedFamilyKey(Material->GetName());
198 if (!Family.IsEmpty())
199 {
200 OutFamilyKey = Family;
201 return true;
202 }
203 }
204 }
205
206 if (!FirstMatchedMaterialName.IsEmpty())
207 {
208 OutFamilyKey = FirstMatchedMaterialName;
209 return true;
210 }
211 return false;
212 }
213
214 // Every building-piece material instance in the project (walls, doors,
215 // corners, windows alike) resolves, several parents up, to the same
216 // shared M_MaterialMaster used by nearly everything else in Carla content
217 // -- matching on the base material would touch far more than windows.
218 // What's actually meant to glow is the "FakeInterior" family specifically
219 // (MI_Skysc_FakeInterior_A/B/C, ...), all living under
220 // /Game/Carla/Static/Building/ -- so require both the content path and
221 // "FakeInterior" in the asset name.
222 //
223 // A slot that's already been live-edited holds a transient
224 // MaterialInstanceDynamic (see CollectProceduralBuildingActors) whose own
225 // GetPathName() is no longer under /Game/Carla/... -- walk back to the
226 // static instance it wraps (MID->Parent) so repeated scans/selects/edits
227 // keep recognizing it instead of only matching once, before the first
228 // apply. Returns that static asset (not just a bool) since callers also
229 // need its own baked default below.
230 UMaterialInterface* ResolveStaticProceduralBuildingAsset(UMaterialInterface* Material)
231 {
232 if (Material == nullptr)
233 return nullptr;
234
235 UMaterialInterface* StaticAsset = Material;
236 if (UMaterialInstanceDynamic* MID = Cast<UMaterialInstanceDynamic>(Material))
237 {
238 StaticAsset = MID->Parent;
239 }
240 if (StaticAsset == nullptr)
241 return nullptr;
242
243 if (!StaticAsset->GetPathName().StartsWith(TEXT("/Game/Carla/Static/Building/")))
244 return nullptr;
245 if (!StaticAsset->GetName().Contains(TEXT("FakeInterior")))
246 return nullptr;
247
248 float Dummy = 0.0f;
249 if (!Material->GetScalarParameterValue(TEXT("EmissiveIntensity"), Dummy))
250 return nullptr;
251
252 return StaticAsset;
253 }
254
255 // One matched FakeInterior mesh slot.
256 struct FBuildingSlotRef
257 {
258 TWeakObjectPtr<UStaticMeshComponent> Component;
259 int32 MaterialIndex = 0;
260 };
261
262 // For one actor that owns at least one FakeInterior slot on a component
263 // whose instanced-ness matches bInstancedOnly, hands the whole per-actor
264 // slot list to Callback. The modular building tool bakes into HISM
265 // components, and Town15's baked buildings (SM_university*) into plain
266 // ISM ones; every pre-modular building/placed piece we've found uses a
267 // plain (non-instanced) StaticMeshComponent instead -- so this one flag
268 // is what actually separates the two mechanisms. HISM derives from ISM,
269 // and PerInstanceRandom varies per instance on both.
270 template <typename FuncT>
271 void CollectBuildingSlotsOnActor(AActor* Actor, bool bInstancedOnly, FuncT&& Callback)
272 {
273 if (Actor == nullptr)
274 return;
275
276 TArray<UStaticMeshComponent*> MeshComponents;
277 Actor->GetComponents<UStaticMeshComponent>(MeshComponents);
278 TArray<FBuildingSlotRef> Slots;
279 for (UStaticMeshComponent* MeshComponent : MeshComponents)
280 {
281 const bool bIsInstanced = MeshComponent->IsA<UInstancedStaticMeshComponent>();
282 if (bIsInstanced != bInstancedOnly)
283 continue;
284
285 const int32 NumMaterials = MeshComponent->GetNumMaterials();
286 for (int32 MaterialIndex = 0; MaterialIndex < NumMaterials; ++MaterialIndex)
287 {
288 if (ResolveStaticProceduralBuildingAsset(MeshComponent->GetMaterial(MaterialIndex)) == nullptr)
289 continue;
290
291 FBuildingSlotRef Slot;
292 Slot.Component = MeshComponent;
293 Slot.MaterialIndex = MaterialIndex;
294 Slots.Add(Slot);
295 }
296 }
297 if (Slots.Num() > 0)
298 {
299 Callback(Actor, Slots);
300 }
301 }
302
303 // Neither baked buildings nor the older pre-modular ones have a
304 // Blueprint class worth iterating by (see ScanProceduralBuildingsInLevel
305 // / ScanLegacyBuildingsInLevel), so this walks every actor in the world
306 // (see CollectBuildingSlotsOnActor).
307 template <typename FuncT>
308 void CollectBuildingActorsByMechanism(UWorld* World, bool bInstancedOnly, FuncT&& Callback)
309 {
310 for (TActorIterator<AActor> It(World); It; ++It)
311 {
312 CollectBuildingSlotsOnActor(*It, bInstancedOnly, Callback);
313 }
314 }
315
316 // Applies EmissiveIntensity to one actor's already-collected slot list
317 // (see CollectProceduralBuildingActors). Per-window on/off variation is
318 // no longer decided here: each matched slot's material is a shared MID
319 // covering every instance in that HISM component at once (hundreds of
320 // physical windows in some cases, confirmed via NumCustomDataFloats==0 --
321 // no per-instance data buffer exists to vary through C++), so a
322 // per-slot coin flip here would only ever turn whole component-sized
323 // blocks of windows on/off together, not individual ones.
324 //
325 // M_FakeInterior (see PR discussion) was extended with a
326 // "PercentLitThreshold" parameter gated by a PerInstanceRandom node in
327 // its own graph: Gate = (PerInstanceRandom < PercentLitThreshold) ? 1 : 0,
328 // multiplied into the existing emissive output. PerInstanceRandom is
329 // resolved per physical instance by the renderer itself, so this is what
330 // actually achieves individual-window variation; PercentLit here just
331 // becomes the threshold we hand it (as a 0..1 fraction).
332 void ApplyToActorSlots(AActor* /*Actor*/, const TArray<FBuildingSlotRef>& Slots, float EmissiveIntensity, float PercentLit, const FLinearColor& Color)
333 {
334 const float Threshold = FMath::Clamp(PercentLit / 100.0f, 0.0f, 1.0f);
335 for (const FBuildingSlotRef& Slot : Slots)
336 {
337 UStaticMeshComponent* Component = Slot.Component.Get();
338 if (Component == nullptr)
339 continue;
340
341 UMaterialInstanceDynamic* MID = Component->CreateAndSetMaterialInstanceDynamic(Slot.MaterialIndex);
342 if (MID == nullptr)
343 continue;
344
345 MID->SetScalarParameterValue(TEXT("EmissiveIntensity"), EmissiveIntensity);
346 MID->SetVectorParameterValue(TEXT("Emissive Color"), Color);
347 MID->SetScalarParameterValue(TEXT("On/Off"), 1.0f);
348 MID->SetScalarParameterValue(TEXT("PercentLitThreshold"), Threshold);
349 }
350 }
351
352 // A stable pseudo-random value per actor, from a hash of its path (fixed
353 // for as long as the actor isn't renamed/moved between levels) -- used
354 // to decide whether a whole legacy building is lit, see
355 // ApplyToLegacyActorSlots. Tried gating legacy buildings with the same
356 // PercentLitThreshold + PerInstanceRandom the baked HISM path uses
357 // (single StaticMeshComponent = single "instance", so the per-window
358 // math degrades to per-building) -- confirmed live in-editor that it
359 // doesn't work: PerInstanceRandom came back effectively identical across
360 // different legacy actors, so every building snapped on together at any
361 // nonzero threshold instead of a graduated percentage. Deciding it here
362 // in C++ instead, per actor, sidesteps whatever that material-side
363 // constant turned out to be.
364 float StableActorRandom01(const AActor* Actor)
365 {
366 const uint32 Hash = GetTypeHash(Actor->GetPathName());
367 return (Hash % 100000) / 100000.0f;
368 }
369
370 // Legacy (non-HISM) equivalent of ApplyToActorSlots. PercentLit still
371 // picks what fraction of buildings are lit, just resolved once per actor
372 // in C++ (see StableActorRandom01) rather than by the shader -- the
373 // whole building is deterministically on or off, no PercentLitThreshold
374 // gating needed (pinned to 1.0, i.e. always-pass, since it's irrelevant
375 // here).
376 void ApplyToLegacyActorSlots(AActor* Actor, const TArray<FBuildingSlotRef>& Slots, float EmissiveIntensity, float PercentLit, const FLinearColor& Color)
377 {
378 const float Threshold = FMath::Clamp(PercentLit / 100.0f, 0.0f, 1.0f);
379 const bool bLit = StableActorRandom01(Actor) < Threshold;
380 for (const FBuildingSlotRef& Slot : Slots)
381 {
382 UStaticMeshComponent* Component = Slot.Component.Get();
383 if (Component == nullptr)
384 continue;
385
386 UMaterialInstanceDynamic* MID = Component->CreateAndSetMaterialInstanceDynamic(Slot.MaterialIndex);
387 if (MID == nullptr)
388 continue;
389
390 MID->SetScalarParameterValue(TEXT("EmissiveIntensity"), bLit ? EmissiveIntensity : 0.0f);
391 MID->SetVectorParameterValue(TEXT("Emissive Color"), Color);
392 MID->SetScalarParameterValue(TEXT("On/Off"), bLit ? 1.0f : 0.0f);
393 MID->SetScalarParameterValue(TEXT("PercentLitThreshold"), 1.0f);
394 }
395 }
396
397 // Confirmed identical, same order, across every vehicle's shared
398 // "*_Emissive" MaterialInstance checked (Lincoln/MiniCooper/Ambulance) --
399 // a project convention, not something to detect per-vehicle. Vehicles
400 // with no lights at all (bicycles) simply don't expose these names;
401 // GetScalarParameterValue below just returns false for them, no special
402 // casing needed. "Special1" is the siren/lightbar group -- a real
403 // "special1-N" light component AND a matching material param exist on
404 // police/fire/ambulance vehicles, same convention as every other group,
405 // just on the sibling M_VehicleLightsMaster_Sirens material (also
406 // matched by ResolveVehicleMaterialBase's "VehicleLightsMaster"
407 // substring check) instead of the main one. The Sirens material's other
408 // params (Orange/Blue/Red/White Frequency, flash rate/color rather than
409 // brightness) don't fit this "one float per group" model and are left
410 // alone -- artist-tuned in the material itself if needed.
411 const TArray<FString> VehicleLightGroupNames = {
412 TEXT("Position"), TEXT("Low Beam"), TEXT("High Beam"), TEXT("Fog"),
413 TEXT("Brake"), TEXT("Reverse"), TEXT("Left Blinker"), TEXT("Right Blinker"),
414 TEXT("Special1"), TEXT("Interior"),
415 };
416
417 // Real light Intensity values (Ambulance's Low Beam ~500000) are
418 // physically-scaled UE5 photometric lux, same story as CarlaLight.cpp's
419 // own scale cvars for street lamps -- awkward to type/read directly.
420 // The panel divides by this on scan and multiplies back on apply, so an
421 // artist works in ~10-100 instead.
422 constexpr float VehicleLightIntensityDisplayScale = 10000.0f;
423
424 // Walks a MaterialInstance's Parent chain to the ultimate base UMaterial
425 // (not just one level -- an "*_Emissive" MI can itself be instanced
426 // again per vehicle color/variant). Also unwraps a live-edited slot's
427 // MID first, so a repeat scan/apply after an edit still resolves it.
428 UMaterialInterface* ResolveVehicleMaterialBase(UMaterialInterface* Material)
429 {
430 if (UMaterialInstanceDynamic* MID = Cast<UMaterialInstanceDynamic>(Material))
431 Material = MID->Parent;
432 UMaterialInterface* Base = Material;
433 int32 SafetyCount = 0;
434 while (UMaterialInstance* Instance = Cast<UMaterialInstance>(Base))
435 {
436 if (Instance->Parent == nullptr || ++SafetyCount > 10)
437 break;
438 Base = Instance->Parent;
439 }
440 return Base;
441 }
442
443 // One matched vehicle-light material slot: NOT detected by the
444 // individual MaterialInstance's own name (confirmed too fragile --
445 // SM_RBlinker/SM_LBlinker's blinker meshes reuse the SAME
446 // MI_Ambulance_Emissive instance as the main light mesh, so that part
447 // was fine, but the same "Emissive"-in-the-name heuristic also wrongly
448 // pulled in MI_Ambulance_Glass_Emissive_Ext/Int, a completely different
449 // system -- interior/exterior glass glow -- with none of the 8 group
450 // params). Instead walks to the material's ultimate base and checks
451 // that against the shared master both real vehicle-light MIs actually
452 // derive from: /Game/.../M_VehicleLightsMaster(_Sirens) (confirmed via
453 // the full parent chain on BP_Ambulance). One check, both variants,
454 // since "Sirens" is just a name suffix on the same "VehicleLightsMaster"
455 // token. Works identically on a live actor or a Blueprint's CDO --
456 // CreateDefaultSubobject components are reachable through GetComponents
457 // either way, which is what lets ScanVehicleCatalog sample a class's
458 // defaults with no level or running episode at all.
459 struct FVehicleEmissiveSlotRef
460 {
461 TWeakObjectPtr<UStaticMeshComponent> Component;
462 int32 MaterialIndex = 0;
463 };
464
465 TArray<FVehicleEmissiveSlotRef> CollectVehicleEmissiveSlots(AActor* Actor)
466 {
467 TArray<FVehicleEmissiveSlotRef> Result;
468 if (Actor == nullptr)
469 return Result;
470
471 TArray<UStaticMeshComponent*> MeshComponents;
472 Actor->GetComponents<UStaticMeshComponent>(MeshComponents);
473 for (UStaticMeshComponent* MeshComponent : MeshComponents)
474 {
475 const int32 NumMaterials = MeshComponent->GetNumMaterials();
476 for (int32 MaterialIndex = 0; MaterialIndex < NumMaterials; ++MaterialIndex)
477 {
478 UMaterialInterface* Base = ResolveVehicleMaterialBase(MeshComponent->GetMaterial(MaterialIndex));
479 if (Base == nullptr || !Base->GetName().Contains(TEXT("VehicleLightsMaster")))
480 continue;
481
482 FVehicleEmissiveSlotRef Slot;
483 Slot.Component = MeshComponent;
484 Slot.MaterialIndex = MaterialIndex;
485 Result.Add(Slot);
486 }
487 }
488 return Result;
489 }
490
491 // True if a real light component's own name (e.g. "front-low_beam-l-1",
492 // "back-brake-r-1", confirmed convention across all 24 vehicles) belongs
493 // to GroupName. Matches by substring on the same tokens the naming
494 // convention itself uses, front+back together under one group (mirrors
495 // how the Emissive material already groups its own "Fog"/"Position"/...
496 // parameters -- one param lights both the front and back fog lenses),
497 // except the two Blinker groups, which the "-l-"/"-r-" token splits.
498 // "Position" excludes "plate-position" (the license plate light) --
499 // deliberately its own thing, not a car light group.
500 bool ComponentNameMatchesGroup(const FString& ComponentName, const FString& GroupName)
501 {
502 const FString Lower = ComponentName.ToLower();
503 if (GroupName == TEXT("Left Blinker"))
504 return Lower.Contains(TEXT("blinker")) && Lower.Contains(TEXT("-l-"));
505 if (GroupName == TEXT("Right Blinker"))
506 return Lower.Contains(TEXT("blinker")) && Lower.Contains(TEXT("-r-"));
507 if (GroupName == TEXT("Low Beam"))
508 return Lower.Contains(TEXT("low_beam"));
509 if (GroupName == TEXT("High Beam"))
510 return Lower.Contains(TEXT("high_beam"));
511 if (GroupName == TEXT("Position"))
512 return Lower.Contains(TEXT("position")) && !Lower.Contains(TEXT("plate"));
513 if (GroupName == TEXT("Fog"))
514 return Lower.Contains(TEXT("fog"));
515 if (GroupName == TEXT("Brake"))
516 return Lower.Contains(TEXT("brake"));
517 if (GroupName == TEXT("Reverse"))
518 return Lower.Contains(TEXT("reverse"));
519 if (GroupName == TEXT("Special1"))
520 return Lower.Contains(TEXT("special1"));
521 if (GroupName == TEXT("Interior"))
522 return Lower.Contains(TEXT("interior"));
523 return false;
524 }
525
526 TArray<ULightComponent*> CollectVehicleLightComponentsForGroup(AActor* Actor, const FString& GroupName)
527 {
528 TArray<ULightComponent*> Result;
529 if (Actor == nullptr)
530 return Result;
531
532 TArray<ULightComponent*> AllLights;
533 Actor->GetComponents<ULightComponent>(AllLights);
534 for (ULightComponent* LightComponent : AllLights)
535 if (LightComponent != nullptr && ComponentNameMatchesGroup(LightComponent->GetName(), GroupName))
536 Result.Add(LightComponent);
537 return Result;
538 }
539
540#if WITH_EDITOR
541 TWeakObjectPtr<AActor> CurrentVehiclePreviewActor;
542#endif
543}
544
546{
547 // GetNameStringByValue returns it qualified ("ELightType::Street" on some
548 // UE flavors), so strip anything before the last "::".
549 const UEnum* Enum = StaticEnum<ELightType>();
550 FString Name = Enum ? Enum->GetNameStringByValue(static_cast<int64>(Group)) : FString();
551 int32 Idx;
552 if (Name.FindLastChar(TEXT(':'), Idx))
553 Name = Name.RightChop(Idx + 1);
554 return Name.IsEmpty() ? TEXT("Other") : Name;
555}
556
558{
559 return SaveDefault(Default, ClassName, TEXT("perClass"));
560}
561
562bool ULightDefaultsJsonUtils::LoadClassDefault(const FString& ClassName, FLightAssetDefault& OutDefault)
563{
564 return LoadDefault(OutDefault, ClassName, TEXT("perClass"));
565}
566
568{
569 return SaveDefault(Default, ULightDefaultsJsonUtils::GetLightGroupName(Group), TEXT("perGroup"));
570}
571
573{
574 return LoadDefault(OutDefault, ULightDefaultsJsonUtils::GetLightGroupName(Group), TEXT("perGroup"));
575}
576
577bool ULightDefaultsJsonUtils::GetLightDefault(const FString& ClassName, ELightType Group, FLightAssetDefault& OutDefault)
578{
579 if (LoadClassDefault(ClassName, OutDefault))
580 return true;
581 return LoadGroupDefault(Group, OutDefault);
582}
583
584FString ULightDefaultsJsonUtils::GetLightClassKey(AActor* Owner, bool& bOutIsBuildingClass)
585{
586 if (Owner == nullptr)
587 {
588 bOutIsBuildingClass = false;
589 return FString();
590 }
591
592 FString FamilyKey;
593 if (TryGetLightBuildingFamilyKey(Owner, FamilyKey))
594 {
595 bOutIsBuildingClass = true;
596 return FamilyKey;
597 }
598
599 bOutIsBuildingClass = false;
600 // Keyed by the owning actor's class (BP_StreetLight01_C, ...): every
601 // light-bearing actor places the same BP_Lights component, so keying by
602 // the component's own class would collapse every light in the level
603 // into one bucket.
604 return Owner->GetClass()->GetName();
605}
606
607TArray<FLightAssetSummary> ULightDefaultsJsonUtils::ScanLightAssetsInLevel(const UObject* WorldContextObject)
608{
609 TArray<FLightAssetSummary> Result;
610
611 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
612 UCarlaLightSubsystem* Subsystem = World ? World->GetSubsystem<UCarlaLightSubsystem>() : nullptr;
613 if (Subsystem == nullptr)
614 return Result;
615
616 TMap<FString, int32> ClassNameToIndex;
617 for (const auto& Pair : Subsystem->GetLights())
618 {
619 UCarlaLight* Light = Pair.Value;
620 if (Light == nullptr || Light->GetOwner() == nullptr)
621 continue;
622
623 bool bIsBuildingClass = false;
624 const FString ClassName = GetLightClassKey(Light->GetOwner(), bIsBuildingClass);
625 if (int32* ExistingIndex = ClassNameToIndex.Find(ClassName))
626 {
627 Result[*ExistingIndex].InstanceCount++;
628 continue;
629 }
630
631 FLightAssetSummary Summary;
632 Summary.ClassName = ClassName;
633 Summary.LightGroup = Light->GetLightType();
634 Summary.InstanceCount = 1;
635 Summary.bIsBuildingClass = bIsBuildingClass;
636 Summary.Current.Color = Light->GetLightColor();
637 Summary.Current.Intensity = Light->GetLightIntensity();
638 Summary.Current.EmissiveIntensity = Light->GetEmissiveIntensity();
639
640 FLightAssetDefault SavedDefault;
641 if (GetLightDefault(ClassName, Summary.LightGroup, SavedDefault))
642 {
643 Summary.Current = SavedDefault;
644 Summary.bHasSavedDefault = true;
645 }
646
647 ClassNameToIndex.Add(ClassName, Result.Num());
648 Result.Add(Summary);
649 }
650
651 Result.Sort([](const FLightAssetSummary& A, const FLightAssetSummary& B)
652 {
653 return A.ClassName < B.ClassName;
654 });
655 return Result;
656}
657
659{
660 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
661 UCarlaLightSubsystem* Subsystem = World ? World->GetSubsystem<UCarlaLightSubsystem>() : nullptr;
662 if (Subsystem == nullptr)
663 return;
664
665 for (const auto& Pair : Subsystem->GetLights())
666 {
667 UCarlaLight* Light = Pair.Value;
668 if (Light == nullptr || Light->GetOwner() == nullptr)
669 continue;
670
671 bool bIsBuildingClass = false;
673 if (GetLightDefault(GetLightClassKey(Light->GetOwner(), bIsBuildingClass), Light->GetLightType(), Default))
674 {
675 Light->SetLightColor(Default.Color);
676 Light->SetLightIntensity(Default.Intensity);
677 Light->SetEmissiveIntensity(Default.EmissiveIntensity);
678 }
679 }
680}
681
682void ULightDefaultsJsonUtils::ApplyLightDefaultToClassLive(const UObject* WorldContextObject, const FString& ClassName, const FLightAssetDefault& Value)
683{
684 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
685 UCarlaLightSubsystem* Subsystem = World ? World->GetSubsystem<UCarlaLightSubsystem>() : nullptr;
686 if (Subsystem == nullptr)
687 return;
688
689 for (const auto& Pair : Subsystem->GetLights())
690 {
691 UCarlaLight* Light = Pair.Value;
692 if (Light == nullptr || Light->GetOwner() == nullptr)
693 continue;
694 bool bIsBuildingClass = false;
695 if (GetLightClassKey(Light->GetOwner(), bIsBuildingClass) != ClassName)
696 continue;
697
698 Light->SetLightColor(Value.Color);
699 Light->SetLightIntensity(Value.Intensity);
700 Light->SetEmissiveIntensity(Value.EmissiveIntensity);
701 }
702}
703
704void ULightDefaultsJsonUtils::SetClassActorsSelected(const UObject* WorldContextObject, const FString& ClassName, bool bSelected)
705{
706#if WITH_EDITOR
707 if (GEditor == nullptr)
708 return;
709
710 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
711 UCarlaLightSubsystem* Subsystem = World ? World->GetSubsystem<UCarlaLightSubsystem>() : nullptr;
712 if (Subsystem == nullptr)
713 return;
714
715 for (const auto& Pair : Subsystem->GetLights())
716 {
717 UCarlaLight* Light = Pair.Value;
718 AActor* Owner = Light ? Light->GetOwner() : nullptr;
719 if (Owner == nullptr)
720 continue;
721 bool bIsBuildingClass = false;
722 if (GetLightClassKey(Owner, bIsBuildingClass) != ClassName)
723 continue;
724 GEditor->SelectActor(Owner, bSelected, /*bNotify=*/false);
725 }
726 GEditor->NoteSelectionChange();
727#endif
728}
729
730TArray<FLightAssetSummary> ULightDefaultsJsonUtils::ScanTrafficLightAssetsInLevel(const UObject* WorldContextObject)
731{
732 TArray<FLightAssetSummary> Result;
733
734 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
735 if (World == nullptr)
736 return Result;
737
738 TMap<FString, int32> ClassNameToIndex;
739 for (TActorIterator<ATrafficLightBase> It(World); It; ++It)
740 {
742 if (TrafficLight == nullptr)
743 continue;
744
745 const FString ClassName = TrafficLight->GetClass()->GetName();
746 if (int32* ExistingIndex = ClassNameToIndex.Find(ClassName))
747 {
748 Result[*ExistingIndex].InstanceCount++;
749 continue;
750 }
751
752 FLightAssetSummary Summary;
753 Summary.ClassName = ClassName;
754 Summary.InstanceCount = 1;
755 Summary.Current.Color = FLinearColor::White;
756 Summary.Current.Intensity = 0.0f;
757
758 // Sample the first signal MID found as the starting value if there's
759 // no saved default yet.
760 ForEachTrafficLightSignalMID(TrafficLight, [&Summary](UMaterialInstanceDynamic* MID)
761 {
762 if (Summary.Current.EmissiveIntensity == 0.0f)
763 {
764 float Value = 0.0f;
765 if (MID->GetScalarParameterValue(TEXT("lightvalue"), Value))
766 {
767 Summary.Current.EmissiveIntensity = Value;
768 }
769 }
770 });
771
772 FLightAssetDefault SavedDefault;
773 if (LoadClassDefault(ClassName, SavedDefault))
774 {
775 Summary.Current.EmissiveIntensity = SavedDefault.EmissiveIntensity;
776 Summary.bHasSavedDefault = true;
777 }
778
779 ClassNameToIndex.Add(ClassName, Result.Num());
780 Result.Add(Summary);
781 }
782
783 Result.Sort([](const FLightAssetSummary& A, const FLightAssetSummary& B)
784 {
785 return A.ClassName < B.ClassName;
786 });
787 return Result;
788}
789
790void ULightDefaultsJsonUtils::ApplyTrafficLightDefaultToClassLive(const UObject* WorldContextObject, const FString& ClassName, float EmissiveIntensity)
791{
792 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
793 if (World == nullptr)
794 return;
795
796 for (TActorIterator<ATrafficLightBase> It(World); It; ++It)
797 {
799 if (TrafficLight == nullptr || TrafficLight->GetClass()->GetName() != ClassName)
800 continue;
801
802 ForEachTrafficLightSignalMID(TrafficLight, [EmissiveIntensity](UMaterialInstanceDynamic* MID)
803 {
804 MID->SetScalarParameterValue(TEXT("lightvalue"), EmissiveIntensity);
805 });
806 }
807}
808
810{
811 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
812 if (World == nullptr)
813 return;
814
815 for (TActorIterator<ATrafficLightBase> It(World); It; ++It)
816 {
818 if (TrafficLight == nullptr)
819 continue;
820
822 if (!LoadClassDefault(TrafficLight->GetClass()->GetName(), Default))
823 continue;
824
825 ForEachTrafficLightSignalMID(TrafficLight, [&Default](UMaterialInstanceDynamic* MID)
826 {
827 MID->SetScalarParameterValue(TEXT("lightvalue"), Default.EmissiveIntensity);
828 });
829 }
830}
831
832void ULightDefaultsJsonUtils::SetTrafficLightClassPreviewOn(const UObject* WorldContextObject, const FString& ClassName, bool bOn)
833{
834 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
835 if (World == nullptr)
836 return;
837
838 const float OnOffValue = bOn ? 1.0f : 0.0f;
839 for (TActorIterator<ATrafficLightBase> It(World); It; ++It)
840 {
842 if (TrafficLight == nullptr || TrafficLight->GetClass()->GetName() != ClassName)
843 continue;
844
845 ForEachTrafficLightSignalMID(TrafficLight, [OnOffValue](UMaterialInstanceDynamic* MID)
846 {
847 MID->SetScalarParameterValue(TEXT("On/Off"), OnOffValue);
848 });
849 }
850}
851
852void ULightDefaultsJsonUtils::SetTrafficLightClassActorsSelected(const UObject* WorldContextObject, const FString& ClassName, bool bSelected)
853{
854#if WITH_EDITOR
855 if (GEditor == nullptr)
856 return;
857
858 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
859 if (World == nullptr)
860 return;
861
862 for (TActorIterator<ATrafficLightBase> It(World); It; ++It)
863 {
865 if (TrafficLight == nullptr || TrafficLight->GetClass()->GetName() != ClassName)
866 continue;
867 GEditor->SelectActor(TrafficLight, bSelected, /*bNotify=*/false);
868 }
869 GEditor->NoteSelectionChange();
870#endif
871}
872
873TArray<FLightAssetSummary> ULightDefaultsJsonUtils::ScanProceduralBuildingsInLevel(const UObject* WorldContextObject)
874{
875 TArray<FLightAssetSummary> Result;
876
877 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
878 if (World == nullptr)
879 return Result;
880
881 int32 ActorCount = 0;
882 float SampleEmissive = 0.0f;
883 bool bFoundSample = false;
884 CollectBuildingActorsByMechanism(World, /*bInstancedOnly=*/true, [&](AActor*, const TArray<FBuildingSlotRef>& Slots)
885 {
886 ++ActorCount;
887 if (bFoundSample)
888 return;
889 for (const FBuildingSlotRef& Slot : Slots)
890 {
891 UStaticMeshComponent* Component = Slot.Component.Get();
892 UMaterialInterface* Material = Component ? Component->GetMaterial(Slot.MaterialIndex) : nullptr;
893 float Value = 0.0f;
894 if (Material != nullptr && Material->GetScalarParameterValue(TEXT("EmissiveIntensity"), Value))
895 {
896 SampleEmissive = Value;
897 bFoundSample = true;
898 break;
899 }
900 }
901 });
902
903 if (ActorCount == 0)
904 return Result;
905
906 FLightAssetSummary Summary;
907 Summary.ClassName = ProceduralBuildingsKey;
909 Summary.InstanceCount = ActorCount;
910 Summary.Current.Color = FLinearColor::White;
911 Summary.Current.Intensity = 50.0f; // Percent Lit -- see ApplyToActorSlots.
912 Summary.Current.EmissiveIntensity = SampleEmissive;
913
914 FLightAssetDefault SavedDefault;
915 if (LoadClassDefault(ProceduralBuildingsKey, SavedDefault))
916 {
917 Summary.Current = SavedDefault;
918 Summary.bHasSavedDefault = true;
919 }
920
921 Result.Add(Summary);
922 return Result;
923}
924
925void ULightDefaultsJsonUtils::ApplyProceduralBuildingsEmissiveLive(const UObject* WorldContextObject, float EmissiveIntensity, float PercentLit, const FLinearColor& Color)
926{
927 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
928 if (World == nullptr)
929 return;
930
931 CollectBuildingActorsByMechanism(World, /*bInstancedOnly=*/true, [EmissiveIntensity, PercentLit, Color](AActor* Actor, const TArray<FBuildingSlotRef>& Slots)
932 {
933 ApplyToActorSlots(Actor, Slots, EmissiveIntensity, PercentLit, Color);
934 });
935}
936
938{
940 if (!LoadClassDefault(ProceduralBuildingsKey, Default))
941 return;
942 ApplyProceduralBuildingsEmissiveLive(WorldContextObject, Default.EmissiveIntensity, Default.Intensity, Default.Color);
943}
944
945void ULightDefaultsJsonUtils::SetProceduralBuildingsSelected(const UObject* WorldContextObject, bool bSelected)
946{
947#if WITH_EDITOR
948 if (GEditor == nullptr)
949 return;
950
951 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
952 if (World == nullptr)
953 return;
954
955 CollectBuildingActorsByMechanism(World, /*bInstancedOnly=*/true, [bSelected](AActor* Actor, const TArray<FBuildingSlotRef>&)
956 {
957 GEditor->SelectActor(Actor, bSelected, /*bNotify=*/false);
958 });
959 GEditor->NoteSelectionChange();
960#endif
961}
962
963TArray<FLightAssetSummary> ULightDefaultsJsonUtils::ScanLegacyBuildingsInLevel(const UObject* WorldContextObject)
964{
965 TArray<FLightAssetSummary> Result;
966
967 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
968 if (World == nullptr)
969 return Result;
970
971 int32 ActorCount = 0;
972 float SampleEmissive = 0.0f;
973 bool bFoundSample = false;
974 CollectBuildingActorsByMechanism(World, /*bInstancedOnly=*/false, [&](AActor*, const TArray<FBuildingSlotRef>& Slots)
975 {
976 ++ActorCount;
977 if (bFoundSample)
978 return;
979 for (const FBuildingSlotRef& Slot : Slots)
980 {
981 UStaticMeshComponent* Component = Slot.Component.Get();
982 UMaterialInterface* Material = Component ? Component->GetMaterial(Slot.MaterialIndex) : nullptr;
983 float Value = 0.0f;
984 if (Material != nullptr && Material->GetScalarParameterValue(TEXT("EmissiveIntensity"), Value))
985 {
986 SampleEmissive = Value;
987 bFoundSample = true;
988 break;
989 }
990 }
991 });
992
993 if (ActorCount == 0)
994 return Result;
995
996 FLightAssetSummary Summary;
997 Summary.ClassName = LegacyBuildingsKey;
999 Summary.InstanceCount = ActorCount;
1000 Summary.Current.Color = FLinearColor::White;
1001 Summary.Current.Intensity = 50.0f; // Percent Lit -- per-building, see ApplyToActorSlots.
1002 Summary.Current.EmissiveIntensity = SampleEmissive;
1003
1004 FLightAssetDefault SavedDefault;
1005 if (LoadClassDefault(LegacyBuildingsKey, SavedDefault))
1006 {
1007 Summary.Current = SavedDefault;
1008 Summary.bHasSavedDefault = true;
1009 }
1010
1011 Result.Add(Summary);
1012 return Result;
1013}
1014
1015void ULightDefaultsJsonUtils::ApplyLegacyBuildingsEmissiveLive(const UObject* WorldContextObject, float EmissiveIntensity, float PercentLit, const FLinearColor& Color)
1016{
1017 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
1018 if (World == nullptr)
1019 return;
1020
1021 CollectBuildingActorsByMechanism(World, /*bInstancedOnly=*/false, [EmissiveIntensity, PercentLit, Color](AActor* Actor, const TArray<FBuildingSlotRef>& Slots)
1022 {
1023 ApplyToLegacyActorSlots(Actor, Slots, EmissiveIntensity, PercentLit, Color);
1024 });
1025}
1026
1028{
1030 if (!LoadClassDefault(LegacyBuildingsKey, Default))
1031 return;
1032 ApplyLegacyBuildingsEmissiveLive(WorldContextObject, Default.EmissiveIntensity, Default.Intensity, Default.Color);
1033}
1034
1036{
1037 if (Level == nullptr)
1038 return;
1039
1040 FLightAssetDefault Procedural;
1041 FLightAssetDefault Legacy;
1042 const bool bProcedural = LoadClassDefault(ProceduralBuildingsKey, Procedural);
1043 const bool bLegacy = LoadClassDefault(LegacyBuildingsKey, Legacy);
1044 if (!bProcedural && !bLegacy)
1045 return;
1046
1047 int32 NumProcedural = 0;
1048 int32 NumLegacy = 0;
1049 for (AActor* Actor : Level->Actors)
1050 {
1051 if (bProcedural)
1052 {
1053 CollectBuildingSlotsOnActor(Actor, /*bInstancedOnly=*/true, [&](AActor* Owner, const TArray<FBuildingSlotRef>& Slots)
1054 {
1055 ApplyToActorSlots(Owner, Slots, Procedural.EmissiveIntensity, Procedural.Intensity, Procedural.Color);
1056 ++NumProcedural;
1057 });
1058 }
1059 if (bLegacy)
1060 {
1061 CollectBuildingSlotsOnActor(Actor, /*bInstancedOnly=*/false, [&](AActor* Owner, const TArray<FBuildingSlotRef>& Slots)
1062 {
1063 ApplyToLegacyActorSlots(Owner, Slots, Legacy.EmissiveIntensity, Legacy.Intensity, Legacy.Color);
1064 ++NumLegacy;
1065 });
1066 }
1067 }
1068 if (NumProcedural + NumLegacy > 0)
1069 {
1070 UE_LOG(LogCarla, Log, TEXT("Light defaults: %d instanced and %d legacy building actors in %s"),
1071 NumProcedural, NumLegacy, *Level->GetOuter()->GetName());
1072 }
1073}
1074
1075namespace
1076{
1077 FDelegateHandle GBuildingActorsInitializedHandle;
1078 FDelegateHandle GBuildingLevelAddedHandle;
1079}
1080
1082{
1083 // The editor applies these on map open (LightDefaultsEditorPanel.cpp),
1084 // and PIE inherits them from the editor world, but only for what was
1085 // loaded then. A World Partition cell streamed in during Play, and any
1086 // -game / server run, loads its buildings from disk with the materials
1087 // as authored. So apply them to every game-world level as it arrives:
1088 // the levels present when the world's actors initialize, and every
1089 // level (streaming level or World Partition cell) added after that.
1090 GBuildingActorsInitializedHandle = FWorldDelegates::OnWorldInitializedActors.AddLambda([](const FActorsInitializedParams& Params)
1091 {
1092 if (Params.World == nullptr || !Params.World->IsGameWorld())
1093 return;
1094 for (ULevel* Level : Params.World->GetLevels())
1096 });
1097 GBuildingLevelAddedHandle = FWorldDelegates::LevelAddedToWorld.AddLambda([](ULevel* Level, UWorld* World)
1098 {
1099 if (World != nullptr && World->IsGameWorld() && World->AreActorsInitialized())
1101 });
1102}
1103
1105{
1106 FWorldDelegates::OnWorldInitializedActors.Remove(GBuildingActorsInitializedHandle);
1107 FWorldDelegates::LevelAddedToWorld.Remove(GBuildingLevelAddedHandle);
1108}
1109
1110void ULightDefaultsJsonUtils::SetLegacyBuildingsSelected(const UObject* WorldContextObject, bool bSelected)
1111{
1112#if WITH_EDITOR
1113 if (GEditor == nullptr)
1114 return;
1115
1116 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
1117 if (World == nullptr)
1118 return;
1119
1120 CollectBuildingActorsByMechanism(World, /*bInstancedOnly=*/false, [bSelected](AActor* Actor, const TArray<FBuildingSlotRef>&)
1121 {
1122 GEditor->SelectActor(Actor, bSelected, /*bNotify=*/false);
1123 });
1124 GEditor->NoteSelectionChange();
1125#endif
1126}
1127
1129{
1130 return VehicleLightGroupNames;
1131}
1132
1133TArray<FVehicleLightSummary> ULightDefaultsJsonUtils::ScanVehicleCatalog(const UObject* WorldContextObject)
1134{
1135 TArray<FVehicleLightSummary> Result;
1136
1137 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
1138 if (World == nullptr)
1139 return Result;
1140
1141 FString JsonString;
1142 if (!FFileHelper::LoadFileToString(JsonString, *GetVehicleParametersPath()))
1143 return Result;
1144
1145 TSharedPtr<FJsonObject> Root;
1146 TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
1147 if (!FJsonSerializer::Deserialize(Reader, Root) || !Root.IsValid())
1148 return Result;
1149
1150 const TArray<TSharedPtr<FJsonValue>>* VehiclesArray = nullptr;
1151 if (!Root->TryGetArrayField(TEXT("Vehicles"), VehiclesArray))
1152 return Result;
1153
1154 for (const TSharedPtr<FJsonValue>& Entry : *VehiclesArray)
1155 {
1156 const TSharedPtr<FJsonObject>* VehicleObj = nullptr;
1157 if (!Entry->TryGetObject(VehicleObj) || !VehicleObj->IsValid())
1158 continue;
1159
1160 bool bHasLights = false;
1161 (*VehicleObj)->TryGetBoolField(TEXT("HasLights"), bHasLights);
1162 if (!bHasLights)
1163 continue;
1164
1165 FString ClassPath;
1166 if (!(*VehicleObj)->TryGetStringField(TEXT("Class"), ClassPath))
1167 continue;
1168
1169 UClass* VehicleClass = LoadObject<UClass>(nullptr, *ClassPath);
1170 if (VehicleClass == nullptr)
1171 {
1172 UE_LOG(LogCarla, Warning, TEXT("ScanVehicleCatalog: failed to load class %s"), *ClassPath);
1173 continue;
1174 }
1175
1176 // Spawned (not the CDO): a vehicle Blueprint assembles its meshes
1177 // and materials in its Construction Script, which the bare class
1178 // default object never runs -- confirmed headless, every one of the
1179 // 24 vehicles scanned via the CDO alone came back with zero groups
1180 // present. Far below the map and destroyed before this function
1181 // returns, so nothing actually renders it.
1182 FActorSpawnParameters SpawnParams;
1183 SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
1184 SpawnParams.ObjectFlags |= RF_Transient;
1185 AActor* TempActor = World->SpawnActor<AActor>(VehicleClass, FVector(0.0f, 0.0f, -1000000.0f), FRotator::ZeroRotator, SpawnParams);
1186 if (TempActor == nullptr)
1187 continue;
1188
1189 FString Make, Model;
1190 (*VehicleObj)->TryGetStringField(TEXT("Make"), Make);
1191 (*VehicleObj)->TryGetStringField(TEXT("Model"), Model);
1192
1193 FVehicleLightSummary Summary;
1194 Summary.ClassName = VehicleClass->GetName();
1195 Summary.DisplayName = (Make.IsEmpty() && Model.IsEmpty())
1196 ? Summary.ClassName
1197 : FString::Printf(TEXT("%s %s"), *Make, *Model);
1198 Summary.Intensity = 1.0f;
1199 for (const FString& GroupName : VehicleLightGroupNames)
1200 Summary.GroupPresent.Add(GroupName, false);
1201
1202 // Global Intensity is still sampled off the material's own
1203 // "Intensity" param as a starting value (its role at apply time has
1204 // changed though -- see ApplyVehicleLightsLive -- it now writes into
1205 // every exposed group param, not just this one, which confirmed
1206 // doing nothing visible on its own).
1207 bool bFoundIntensity = false;
1208 for (const FVehicleEmissiveSlotRef& Slot : CollectVehicleEmissiveSlots(TempActor))
1209 {
1210 UStaticMeshComponent* Component = Slot.Component.Get();
1211 UMaterialInterface* Material = Component ? Component->GetMaterial(Slot.MaterialIndex) : nullptr;
1212 if (Material == nullptr || bFoundIntensity)
1213 continue;
1214
1215 float Value = 0.0f;
1216 if (Material->GetScalarParameterValue(TEXT("Intensity"), Value))
1217 {
1218 Summary.Intensity = Value;
1219 bFoundIntensity = true;
1220 }
1221 }
1222
1223 // Per-group columns: the real Spot/Rect/Point light components'
1224 // physical Intensity, not the material -- see
1225 // CollectVehicleLightComponentsForGroup. Present/initial value
1226 // sampled from whichever matching component is found first.
1227 TArray<ULightComponent*> AllLightComponents;
1228 TempActor->GetComponents<ULightComponent>(AllLightComponents);
1229 for (ULightComponent* LightComponent : AllLightComponents)
1230 {
1231 if (LightComponent == nullptr)
1232 continue;
1233 for (const FString& GroupName : VehicleLightGroupNames)
1234 {
1235 if (!ComponentNameMatchesGroup(LightComponent->GetName(), GroupName))
1236 continue;
1237 Summary.GroupPresent[GroupName] = true;
1238 if (!Summary.GroupIntensity.Contains(GroupName))
1239 Summary.GroupIntensity.Add(GroupName, LightComponent->Intensity / VehicleLightIntensityDisplayScale);
1240 }
1241 }
1242
1243 float SavedIntensity = Summary.Intensity;
1244 TMap<FString, float> SavedGroups;
1245 if (LoadVehicleLightDefault(Summary.ClassName, SavedIntensity, SavedGroups))
1246 {
1247 Summary.Intensity = SavedIntensity;
1248 for (const TPair<FString, float>& Pair : SavedGroups)
1249 if (Summary.GroupPresent.Contains(Pair.Key))
1250 Summary.GroupIntensity.Add(Pair.Key, Pair.Value);
1251 Summary.bHasSavedDefault = true;
1252 }
1253
1254 TempActor->Destroy();
1255 Result.Add(Summary);
1256 }
1257
1258 Result.Sort([](const FVehicleLightSummary& A, const FVehicleLightSummary& B)
1259 {
1260 return A.DisplayName < B.DisplayName;
1261 });
1262 return Result;
1263}
1264
1265namespace
1266{
1267 // The actual per-actor work, factored out so ACarlaWheeledVehicle's own
1268 // BeginPlay (ActivateVehicleLightComponents) can apply a saved default
1269 // to just itself -- calling the world-sweeping ApplyVehicleLightsLive
1270 // from every spawning vehicle's BeginPlay would rescan every
1271 // already-spawned vehicle of the same class each time (O(n^2) for n
1272 // vehicles of one class, e.g. a generate_traffic.py run), pointless
1273 // since each of those already applied this exact same thing to itself
1274 // when IT spawned.
1275 void ApplyVehicleLightsToActor(AActor* Actor, float Intensity, const TMap<FString, float>& GroupIntensity)
1276 {
1277 if (Actor == nullptr)
1278 return;
1279
1280 // Global Intensity -> every exposed emissive group parameter at
1281 // once (Position, Low Beam, ...): the material's own standalone
1282 // "Intensity" param confirmed doing nothing visible by itself, so
1283 // this is what actually makes the single global field useful.
1284 for (const FVehicleEmissiveSlotRef& Slot : CollectVehicleEmissiveSlots(Actor))
1285 {
1286 UStaticMeshComponent* Component = Slot.Component.Get();
1287 if (Component == nullptr)
1288 continue;
1289
1290 UMaterialInstanceDynamic* MID = Component->CreateAndSetMaterialInstanceDynamic(Slot.MaterialIndex);
1291 if (MID == nullptr)
1292 continue;
1293
1294 for (const FString& GroupName : VehicleLightGroupNames)
1295 MID->SetScalarParameterValue(*GroupName, Intensity);
1296 }
1297
1298 // Per-group columns -> the real Spot/Rect/Point light components'
1299 // physical Intensity (see CollectVehicleLightComponentsForGroup),
1300 // not the material.
1301 for (const TPair<FString, float>& Pair : GroupIntensity)
1302 {
1303 for (ULightComponent* LightComponent : CollectVehicleLightComponentsForGroup(Actor, Pair.Key))
1304 {
1305 LightComponent->SetIntensity(Pair.Value * VehicleLightIntensityDisplayScale);
1306 }
1307 }
1308 }
1309}
1310
1311void ULightDefaultsJsonUtils::ApplyVehicleLightsLive(const UObject* WorldContextObject, const FString& ClassName, float Intensity, const TMap<FString, float>& GroupIntensity)
1312{
1313 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
1314 if (World == nullptr)
1315 return;
1316
1317 for (TActorIterator<AActor> It(World); It; ++It)
1318 {
1319 AActor* Actor = *It;
1320 if (Actor == nullptr || Actor->GetClass()->GetName() != ClassName)
1321 continue;
1322 ApplyVehicleLightsToActor(Actor, Intensity, GroupIntensity);
1323 }
1324}
1325
1326void ULightDefaultsJsonUtils::ApplyVehicleLightsToSingleActor(AActor* Actor, float Intensity, const TMap<FString, float>& GroupIntensity)
1327{
1328 ApplyVehicleLightsToActor(Actor, Intensity, GroupIntensity);
1329}
1330
1331void ULightDefaultsJsonUtils::ApplyVehicleLightsRuntimeState(AActor* Actor, const TMap<FString, float>& MaterialGroupValue, const TMap<FString, float>& RealLightGroupIntensity)
1332{
1333 if (Actor == nullptr)
1334 return;
1335
1336 for (const FVehicleEmissiveSlotRef& Slot : CollectVehicleEmissiveSlots(Actor))
1337 {
1338 UStaticMeshComponent* Component = Slot.Component.Get();
1339 if (Component == nullptr)
1340 continue;
1341
1342 UMaterialInstanceDynamic* MID = Component->CreateAndSetMaterialInstanceDynamic(Slot.MaterialIndex);
1343 if (MID == nullptr)
1344 continue;
1345
1346 for (const FString& GroupName : VehicleLightGroupNames)
1347 {
1348 const float* Value = MaterialGroupValue.Find(GroupName);
1349 MID->SetScalarParameterValue(*GroupName, Value != nullptr ? *Value : 0.0f);
1350 }
1351 }
1352
1353 for (const TPair<FString, float>& Pair : RealLightGroupIntensity)
1354 {
1355 for (ULightComponent* LightComponent : CollectVehicleLightComponentsForGroup(Actor, Pair.Key))
1356 {
1357 LightComponent->SetIntensity(Pair.Value * VehicleLightIntensityDisplayScale);
1358 }
1359 }
1360}
1361
1362bool ULightDefaultsJsonUtils::SaveVehicleLightDefault(const FString& ClassName, float Intensity, const TMap<FString, float>& GroupIntensity)
1363{
1364 TSharedRef<FJsonObject> Root = LoadLightJsonRoot(GetLightDefaultsPath());
1365 TSharedRef<FJsonObject> Catalog = GetOrCreateLightCatalog(Root, TEXT("perVehicleClass"));
1366
1367 TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
1368 Entry->SetNumberField(TEXT("Intensity"), Intensity);
1369 TSharedRef<FJsonObject> GroupsObj = MakeShared<FJsonObject>();
1370 for (const TPair<FString, float>& Pair : GroupIntensity)
1371 GroupsObj->SetNumberField(Pair.Key, Pair.Value);
1372 Entry->SetObjectField(TEXT("GroupIntensity"), GroupsObj);
1373 Catalog->SetObjectField(ClassName, Entry);
1374
1375 return SaveLightJsonRoot(Root, GetLightDefaultsPath());
1376}
1377
1378bool ULightDefaultsJsonUtils::LoadVehicleLightDefault(const FString& ClassName, float& OutIntensity, TMap<FString, float>& OutGroupIntensity)
1379{
1380 TSharedRef<FJsonObject> Root = LoadLightJsonRoot(GetLightDefaultsPath());
1381 const TSharedPtr<FJsonObject>* Catalog = nullptr;
1382 if (!Root->TryGetObjectField(TEXT("perVehicleClass"), Catalog) || !Catalog->IsValid())
1383 return false;
1384
1385 const TSharedPtr<FJsonObject>* Entry = nullptr;
1386 if (!(*Catalog)->TryGetObjectField(ClassName, Entry) || !Entry->IsValid())
1387 return false;
1388
1389 double IntensityValue = OutIntensity;
1390 (*Entry)->TryGetNumberField(TEXT("Intensity"), IntensityValue);
1391 OutIntensity = static_cast<float>(IntensityValue);
1392
1393 const TSharedPtr<FJsonObject>* GroupsObj = nullptr;
1394 if ((*Entry)->TryGetObjectField(TEXT("GroupIntensity"), GroupsObj) && GroupsObj->IsValid())
1395 {
1396 for (const auto& Pair : (*GroupsObj)->Values)
1397 {
1398 double Value = 0.0;
1399 if (Pair.Value->TryGetNumber(Value))
1400 OutGroupIntensity.Add(FString(*Pair.Key), static_cast<float>(Value));
1401 }
1402 }
1403 return true;
1404}
1405
1407{
1408 TSharedRef<FJsonObject> Root = LoadLightJsonRoot(GetLightDefaultsPath());
1409 const TSharedPtr<FJsonObject>* Catalog = nullptr;
1410 if (!Root->TryGetObjectField(TEXT("perVehicleClass"), Catalog) || !Catalog->IsValid())
1411 return;
1412
1413 for (const auto& Pair : (*Catalog)->Values)
1414 {
1415 const FString ClassName(*Pair.Key);
1416 float Intensity = 1.0f;
1417 TMap<FString, float> GroupIntensity;
1418 if (LoadVehicleLightDefault(ClassName, Intensity, GroupIntensity))
1419 ApplyVehicleLightsLive(WorldContextObject, ClassName, Intensity, GroupIntensity);
1420 }
1421}
1422
1423AActor* ULightDefaultsJsonUtils::SpawnVehiclePreview(const UObject* WorldContextObject, const FString& ClassName)
1424{
1425#if WITH_EDITOR
1426 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
1427 if (World == nullptr || GEditor == nullptr)
1428 return nullptr;
1429
1430 ClearVehiclePreview(WorldContextObject);
1431
1432 UClass* VehicleClass = nullptr;
1433 for (TObjectIterator<UClass> ClassIt; ClassIt; ++ClassIt)
1434 {
1435 if (ClassIt->GetName() == ClassName)
1436 {
1437 VehicleClass = *ClassIt;
1438 break;
1439 }
1440 }
1441 if (VehicleClass == nullptr)
1442 return nullptr;
1443
1444 FLevelEditorViewportClient* ViewportClient = GCurrentLevelEditingViewportClient;
1445 if (ViewportClient == nullptr)
1446 return nullptr;
1447
1448 // Straight down from wherever the artist last navigated the viewport --
1449 // no fixed world coordinate needed, and it stays put across repeated
1450 // Preview clicks as long as the viewport doesn't move in between.
1451 const FVector CameraLocation = ViewportClient->GetViewLocation();
1452 const FVector TraceStart(CameraLocation.X, CameraLocation.Y, CameraLocation.Z + 2000.0f);
1453 const FVector TraceEnd(CameraLocation.X, CameraLocation.Y, CameraLocation.Z - 100000.0f);
1454
1455 FHitResult Hit;
1456 FCollisionQueryParams QueryParams;
1457 QueryParams.bTraceComplex = true;
1458 if (!World->LineTraceSingleByChannel(Hit, TraceStart, TraceEnd, ECC_WorldStatic, QueryParams))
1459 return nullptr;
1460
1461 const FVector SpawnLocation = Hit.Location + FVector(0.0f, 0.0f, 50.0f);
1462 FActorSpawnParameters SpawnParams;
1463 SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
1464 AActor* Spawned = World->SpawnActor<AActor>(VehicleClass, SpawnLocation, FRotator::ZeroRotator, SpawnParams);
1465 if (Spawned == nullptr)
1466 return nullptr;
1467
1468 CurrentVehiclePreviewActor = Spawned;
1469
1470 // Fixed offset behind/above/to the side, framing the whole car -- same
1471 // idea as the lw_suite.py test script's cam_pose, just via the editor
1472 // viewport instead of a spawned camera actor.
1473 const FVector CamOffset(-700.0f, -500.0f, 350.0f);
1474 const FVector CamLocation = SpawnLocation + CamOffset;
1475 ViewportClient->SetViewLocation(CamLocation);
1476 ViewportClient->SetViewRotation((SpawnLocation - CamLocation).Rotation());
1477 ViewportClient->Invalidate();
1478
1479 return Spawned;
1480#else
1481 return nullptr;
1482#endif
1483}
1484
1486{
1487#if WITH_EDITOR
1488 if (AActor* Existing = CurrentVehiclePreviewActor.Get())
1489 Existing->Destroy();
1490 CurrentVehiclePreviewActor = nullptr;
1491#endif
1492}
1493
1495{
1496 UWorld* World = GEngine ? GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::ReturnNull) : nullptr;
1497 if (World == nullptr)
1498 return;
1499 UMaterialParameterCollection* Collection = LoadObject<UMaterialParameterCollection>(
1500 nullptr, TEXT("/Game/Carla/Blueprints/Game/CarlaParameters.CarlaParameters"));
1501 if (Collection == nullptr)
1502 return;
1503 UKismetMaterialLibrary::SetScalarParameterValue(World, Collection, TEXT("VisualTime"), 0.25f);
1504}
ELightType
Definition CarlaLight.h:30
std::vector< carla::rpc::LightState > GetLights(FString Client)
static TArray< FVehicleLightSummary > ScanVehicleCatalog(const UObject *WorldContextObject)
static void ReapplyLegacyBuildingsToLevel(const UObject *WorldContextObject)
static void PreviewVehicleBlinkers(const UObject *WorldContextObject)
static AActor * SpawnVehiclePreview(const UObject *WorldContextObject, const FString &ClassName)
static void SetLegacyBuildingsSelected(const UObject *WorldContextObject, bool bSelected)
static void ApplyVehicleLightsRuntimeState(AActor *Actor, const TMap< FString, float > &MaterialGroupValue, const TMap< FString, float > &RealLightGroupIntensity)
static void SetTrafficLightClassPreviewOn(const UObject *WorldContextObject, const FString &ClassName, bool bOn)
static bool SaveClassDefault(const FString &ClassName, const FLightAssetDefault &Default)
static void ApplyLightDefaultToClassLive(const UObject *WorldContextObject, const FString &ClassName, const FLightAssetDefault &Value)
static FString GetLightGroupName(ELightType Group)
static bool LoadGroupDefault(ELightType Group, UPARAM(ref) FLightAssetDefault &OutDefault)
static FString GetLightClassKey(AActor *Owner, bool &bOutIsBuildingClass)
static TArray< FLightAssetSummary > ScanLegacyBuildingsInLevel(const UObject *WorldContextObject)
static bool GetLightDefault(const FString &ClassName, ELightType Group, UPARAM(ref) FLightAssetDefault &OutDefault)
static void ReapplyProceduralBuildingsToLevel(const UObject *WorldContextObject)
static FString GetVehicleParametersPath()
static void ApplyProceduralBuildingsEmissiveLive(const UObject *WorldContextObject, float EmissiveIntensity, float PercentLit, const FLinearColor &Color)
static void ApplyTrafficLightDefaultToClassLive(const UObject *WorldContextObject, const FString &ClassName, float EmissiveIntensity)
static bool SaveGroupDefault(ELightType Group, const FLightAssetDefault &Default)
static void ApplyVehicleLightsToSingleActor(AActor *Actor, float Intensity, const TMap< FString, float > &GroupIntensity)
static void SetTrafficLightClassActorsSelected(const UObject *WorldContextObject, const FString &ClassName, bool bSelected)
static void UnregisterRuntimeBuildingDefaultsHooks()
static bool LoadVehicleLightDefault(const FString &ClassName, UPARAM(ref) float &OutIntensity, UPARAM(ref) TMap< FString, float > &OutGroupIntensity)
static void ReapplyLightDefaultsToLevel(const UObject *WorldContextObject)
static TArray< FLightAssetSummary > ScanTrafficLightAssetsInLevel(const UObject *WorldContextObject)
static TArray< FLightAssetSummary > ScanLightAssetsInLevel(const UObject *WorldContextObject)
static bool LoadClassDefault(const FString &ClassName, UPARAM(ref) FLightAssetDefault &OutDefault)
static void ReapplyTrafficLightDefaultsToLevel(const UObject *WorldContextObject)
static void ClearVehiclePreview(const UObject *WorldContextObject)
static void ReapplyBuildingDefaultsToLevel(ULevel *Level)
static void ApplyLegacyBuildingsEmissiveLive(const UObject *WorldContextObject, float EmissiveIntensity, float PercentLit, const FLinearColor &Color)
static TArray< FString > GetVehicleLightGroupNames()
static TArray< FLightAssetSummary > ScanProceduralBuildingsInLevel(const UObject *WorldContextObject)
static void ReapplyVehicleLightsToLevel(const UObject *WorldContextObject)
static void SetProceduralBuildingsSelected(const UObject *WorldContextObject, bool bSelected)
static void ApplyVehicleLightsLive(const UObject *WorldContextObject, const FString &ClassName, float Intensity, const TMap< FString, float > &GroupIntensity)
static void SetClassActorsSelected(const UObject *WorldContextObject, const FString &ClassName, bool bSelected)
static bool SaveVehicleLightDefault(const FString &ClassName, float Intensity, const TMap< FString, float > &GroupIntensity)
carla::SharedPtr< cc::Actor > Actor
FLightAssetDefault Current
TMap< FString, bool > GroupPresent
TMap< FString, float > GroupIntensity