CARLA
Loading...
Searching...
No Matches
DigitalTwinsTrafficLight.cpp
Go to the documentation of this file.
1// Copyright (c) 2024 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#include "Carla.h"
9#include "Components/StaticMeshComponent.h"
10#include "Engine/StaticMesh.h"
11#include "Materials/MaterialInstanceDynamic.h"
12
13namespace
14{
15 // ---------------------------------------------------------------- lamp classification
16 //
17 // Every lamp of a Traffic Light Tool rig is one material slot named "led_<i>" on a module
18 // mesh (ATrafficLightActor::AddModule / FMaterialFactory::CreateLightMaterialInstanceDynamic),
19 // driven by a MID off M_TrafficLights_Inst with four parameters that survive Bake()'s
20 // K2_CopyMaterialInstanceParameters: "Emissive Intensity", "Emissive Color" and the atlas
21 // offsets "Offset U" / "Offset V".
22 //
23 // The atlas offset is the *glyph*, not the state. In the TrafficLights2025 "LightTypes"
24 // DataTable, ArrowLeftGreen / ArrowLeftAmber / ArrowLeftRed all carry AtlasCoords (0, 1) and
25 // differ only in Color, and the same holds for every other symbol -- so a UV table cannot
26 // say which state a lamp belongs to. Colour can: the table uses exactly three of them.
27 // The atlas offset is still useful for the six symbols that carry no state at all (Tram,
28 // the four tram-line glyphs, Circle, Triangle): those are pure white and stay dark.
29 const FLinearColor LampGreen(0.100902f, 1.000000f, 0.297537f, 1.0f);
30 const FLinearColor LampAmber(0.930111f, 0.356400f, 0.014444f, 1.0f);
31 const FLinearColor LampRed(1.000000f, 0.052026f, 0.061974f, 1.0f);
32
33 // The three colours are far apart (nearest pair, amber<->red, is 0.41 apart in RGB), so a
34 // generous radius still cannot confuse them and tolerates a slightly re-authored table.
35 constexpr float LampColorToleranceSqr = 0.06f;
36
37 float ColorDistSqr(const FLinearColor& A, const FLinearColor& B)
38 {
39 return FMath::Square(A.R - B.R) + FMath::Square(A.G - B.G) + FMath::Square(A.B - B.B);
40 }
41
42 bool ClassifyLampByColor(const FLinearColor& Color, ETrafficLightState& OutState)
43 {
44 const float DGreen = ColorDistSqr(Color, LampGreen);
45 const float DAmber = ColorDistSqr(Color, LampAmber);
46 const float DRed = ColorDistSqr(Color, LampRed);
47 const float Best = FMath::Min3(DGreen, DAmber, DRed);
48 if (Best > LampColorToleranceSqr)
49 {
50 return false; // white (stateless glyph) or a material whose colour was never set
51 }
52 OutState = (Best == DGreen) ? ETrafficLightState::Green
53 : (Best == DAmber) ? ETrafficLightState::Yellow
55 return true;
56 }
57
58 /// "Pole_00_Head_01_Mod_02" -> ("Pole_00_Head_01", 2). Component names are assigned by
59 /// ATrafficLightActor::Build via UniqueRename and preserved by Bake
60 /// (BakedMeshComponent->Rename(*Source->GetName(), ...)), so they survive into the level.
61 bool SplitHeadAndModule(const FString& ComponentName, FString& OutHeadKey, int32& OutModuleIndex)
62 {
63 int32 At = INDEX_NONE;
64 if (!ComponentName.FindLastChar(TEXT('_'), At))
65 {
66 return false;
67 }
68 const FString Tail = ComponentName.Mid(At + 1);
69 if (Tail.IsEmpty() || !Tail.IsNumeric())
70 {
71 return false;
72 }
73 const FString Head = ComponentName.Left(At);
74 if (!Head.EndsWith(TEXT("_Mod")))
75 {
76 return false;
77 }
78 OutHeadKey = Head.LeftChop(4); // drop "_Mod"
79 OutModuleIndex = FCString::Atoi(*Tail);
80 return true;
81 }
82}
83
84ADigitalTwinsTrafficLight::ADigitalTwinsTrafficLight(const FObjectInitializer &ObjectInitializer)
85 : Super(ObjectInitializer)
86{
87}
88
90{
91 Super::BeginPlay();
92
93 UTrafficLightComponent* TLComponent = GetTrafficLightComponent();
94 if (TLComponent)
95 {
96 TLComponent->LightChangeDispatcher.AddDynamic(this, &ADigitalTwinsTrafficLight::OnLightStateChanged);
97 ETrafficLightState InitialState = TLComponent->GetLightState();
98 UpdateDigitalTwinsMaterials(InitialState);
99 }
100 else
101 {
102 UE_LOG(LogCarla, Error, TEXT("DigitalTwinsTrafficLight: No TrafficLightComponent found"));
103 }
104}
105
107{
108 UTrafficLightComponent* TLComponent = GetTrafficLightComponent();
109 if (TLComponent)
110 {
111 ETrafficLightState NewState = TLComponent->GetLightState();
113 }
114}
115
117{
118 MaterialToLightType.Empty();
119 DarkMaterials.Empty();
120
121 TArray<UStaticMeshComponent*> MeshComponents;
122 GetComponents<UStaticMeshComponent>(MeshComponents);
123 CachedMeshComponentCount = MeshComponents.Num();
124
125 struct FLamp
126 {
127 UMaterialInstanceDynamic* Material = nullptr;
128 FString HeadKey;
129 int32 ModuleIndex = 0;
130 int32 SlotIndex = 0;
131 bool bClassified = false;
132 ETrafficLightState State = ETrafficLightState::Red;
133 bool bStatelessGlyph = false;
134 };
135 TArray<FLamp> Lamps;
136
137 for (UStaticMeshComponent* MeshComp : MeshComponents)
138 {
139 if (!MeshComp || !MeshComp->GetStaticMesh())
140 {
141 continue;
142 }
143
144 // A lamp is identified by its *material slot* name, not by the material's name: the
145 // material on a baked rig is an auto-named MID ("MID_M_TrafficLights_Inst_7"), which only
146 // happens to contain "TrafficLight".
147 const TArray<FName> SlotNames = MeshComp->GetMaterialSlotNames();
148 for (int32 Slot = 0; Slot < SlotNames.Num(); ++Slot)
149 {
150 if (!SlotNames[Slot].ToString().StartsWith(TEXT("led_")))
151 {
152 continue;
153 }
154 const int32 MatIdx = MeshComp->GetMaterialIndex(SlotNames[Slot]);
155 if (MatIdx == INDEX_NONE)
156 {
157 continue;
158 }
159 UMaterialInterface* Material = MeshComp->GetMaterial(MatIdx);
160 if (!Material)
161 {
162 continue;
163 }
164 UMaterialInstanceDynamic* DynMaterial = Cast<UMaterialInstanceDynamic>(Material);
165 if (!DynMaterial)
166 {
167 DynMaterial = UMaterialInstanceDynamic::Create(Material, MeshComp);
168 if (!DynMaterial)
169 {
170 continue;
171 }
172 MeshComp->SetMaterial(MatIdx, DynMaterial);
173 }
174
175 FLamp Lamp;
176 Lamp.Material = DynMaterial;
177 Lamp.SlotIndex = Slot;
178 if (!SplitHeadAndModule(MeshComp->GetName(), Lamp.HeadKey, Lamp.ModuleIndex))
179 {
180 Lamp.HeadKey = MeshComp->GetName();
181 Lamp.ModuleIndex = 0;
182 }
183
184 FLinearColor EmissiveColor;
185 if (DynMaterial->GetVectorParameterValue(FName("Emissive Color"), EmissiveColor))
186 {
187 Lamp.bClassified = ClassifyLampByColor(EmissiveColor, Lamp.State);
188 if (!Lamp.bClassified)
189 {
190 // A pure-white lamp is one of the stateless glyphs (Tram / tram lines / Circle /
191 // Triangle); anything else is a lamp whose colour never made it through the bake.
192 Lamp.bStatelessGlyph = EmissiveColor.R > 0.9f && EmissiveColor.G > 0.9f && EmissiveColor.B > 0.9f;
193 }
194 }
195 Lamps.Add(Lamp);
196 }
197 }
198
199 // Fallback for a head whose colours did not survive: order its lamps by module index and
200 // read them as a plain 3-aspect head. The Traffic Light Tool builds a vertical head bottom
201 // up and every rig in ue/rigs follows the same order (see ue/rigs/eu_pole.json:
202 // Mod_00 SolidColorGreen, Mod_01 SolidColorAmber, Mod_02 SolidColorRed).
203 static const ETrafficLightState ThreeAspect[3] = {
204 ETrafficLightState::Green, ETrafficLightState::Yellow, ETrafficLightState::Red};
205 int32 NumFallback = 0;
206 TMap<FString, TArray<int32>> ByHead;
207 for (int32 i = 0; i < Lamps.Num(); ++i)
208 {
209 ByHead.FindOrAdd(Lamps[i].HeadKey).Add(i);
210 }
211 for (auto& HeadPair : ByHead)
212 {
213 TArray<int32>& Indices = HeadPair.Value;
214 if (Indices.ContainsByPredicate([&](int32 i) { return Lamps[i].bClassified || Lamps[i].bStatelessGlyph; }))
215 {
216 continue; // this head classified fine (or is deliberately dark)
217 }
218 if (Indices.Num() != 3)
219 {
220 continue;
221 }
222 Indices.Sort([&](int32 A, int32 B)
223 {
224 return Lamps[A].ModuleIndex != Lamps[B].ModuleIndex
225 ? Lamps[A].ModuleIndex < Lamps[B].ModuleIndex
226 : Lamps[A].SlotIndex < Lamps[B].SlotIndex;
227 });
228 for (int32 k = 0; k < 3; ++k)
229 {
230 Lamps[Indices[k]].bClassified = true;
231 Lamps[Indices[k]].State = ThreeAspect[k];
232 ++NumFallback;
233 }
234 }
235
236 int32 NumRed = 0, NumYellow = 0, NumGreen = 0, NumStateless = 0, NumUnknown = 0;
237 for (const FLamp& Lamp : Lamps)
238 {
239 if (!Lamp.bClassified)
240 {
241 DarkMaterials.Add(Lamp.Material);
242 (Lamp.bStatelessGlyph ? NumStateless : NumUnknown)++;
243 continue;
244 }
245 MaterialToLightType.Add(Lamp.Material, Lamp.State);
246 switch (Lamp.State)
247 {
248 case ETrafficLightState::Red: ++NumRed; break;
249 case ETrafficLightState::Yellow: ++NumYellow; break;
250 default: ++NumGreen; break;
251 }
252 }
253
254 // Nothing else drives these, and they are baked at "Emissive Intensity" 50000 (i.e. lit),
255 // so an unclassified lamp must be switched off once or it stays permanently on.
256 for (UMaterialInstanceDynamic* Dark : DarkMaterials)
257 {
258 Dark->SetScalarParameterValue(FName("Emissive Intensity"), 0.0f);
259 }
260
261 UE_LOG(LogCarla, Log,
262 TEXT("DigitalTwinsTrafficLight '%s' lamp census: %d mesh components, %d lamp slots -> "
263 "%d red, %d yellow, %d green, %d stateless, %d unclassified (%d by module order)"),
264 *GetTrafficLightComponent()->GetSignId(), CachedMeshComponentCount, Lamps.Num(),
265 NumRed, NumYellow, NumGreen, NumStateless, NumUnknown, NumFallback);
266
267 if (Lamps.Num() == 0)
268 {
269 UE_LOG(LogCarla, Warning,
270 TEXT("DigitalTwinsTrafficLight '%s': no 'led_*' material slot on any of its %d mesh "
271 "components; the rig will not animate"),
273 }
274}
275
277{
278 // Rebuild when the actor has never been scanned, and again if it has grown components
279 // since (UMapLogicParser re-parents the baked rig's meshes onto this actor).
280 TArray<UStaticMeshComponent*> MeshComponents;
281 GetComponents<UStaticMeshComponent>(MeshComponents);
282 if (MaterialToLightType.Num() == 0 || MeshComponents.Num() != CachedMeshComponentCount)
283 {
284 BuildLampMap();
285 }
286
287 for (const auto& Pair : MaterialToLightType)
288 {
289 UMaterialInstanceDynamic* DynMaterial = Pair.Key;
290 ETrafficLightState LightType = Pair.Value;
291
292 bool bShouldBeOn = (LightType == State);
293 float IntensityValue = bShouldBeOn ? 50000.0f : 0.0f;
294
295 DynMaterial->SetScalarParameterValue(FName("Emissive Intensity"), IntensityValue);
296 }
297}
298
300{
301 switch (State)
302 {
303 case ETrafficLightState::Red:
304 return LampRed;
305
306 case ETrafficLightState::Yellow:
307 return LampAmber;
308
309 case ETrafficLightState::Green:
310 return LampGreen;
311
312 default:
313 return FLinearColor(0.05f, 0.05f, 0.05f, 1.0f);
314 }
315}
FLinearColor GetColorForState(ETrafficLightState State) const
Get the emissive color for a given traffic light state.
TArray< UMaterialInstanceDynamic * > DarkMaterials
Lamps that belong to no state – the stateless atlas glyphs (Tram, tram lines, Circle,...
void UpdateDigitalTwinsMaterials(ETrafficLightState State)
Update material parameters for DigitalTwins meshes based on traffic light state.
ADigitalTwinsTrafficLight(const FObjectInitializer &ObjectInitializer)
TMap< UMaterialInstanceDynamic *, ETrafficLightState > MaterialToLightType
Every lamp this actor drives, and the state it belongs to.
void BuildLampMap()
Scan every "led_*" material slot of this actor's meshes and sort the lamps into states.
int32 CachedMeshComponentCount
Mesh component count the lamp map was built from.
UTrafficLightComponent * GetTrafficLightComponent()