Agrarsense
InstancedActorRenderer.cpp
Go to the documentation of this file.
1// Copyright (c) 2025 FrostBit Software Lab at the Lapland University of Applied Sciences
2//
3// This work is licensed under the terms of the MIT license.
4// For a copy, see <https://opensource.org/licenses/MIT>.
5
7
11
12#include "Materials/Material.h"
13#include "TimerManager.h"
14
15AInstancedActorRenderer::AInstancedActorRenderer(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
16{
17 PrimaryActorTick.bCanEverTick = false;
18}
19
21{
22 Super::BeginPlay();
23
24 UWorld* World = GetWorld();
25
27 if (Settings)
28 {
31 }
32
34 if (Weather)
35 {
38 }
39}
40
41void AInstancedActorRenderer::EndPlay(const EEndPlayReason::Type EndPlayReason)
42{
43 Super::EndPlay(EndPlayReason);
44
45 UWorld* World = GetWorld();
46
48 if (Settings)
49 {
51 }
52
54 if (Weather)
55 {
57 }
58}
59
61{
62 if (!InstancedActor)
63 {
64 return;
65 }
66
67 UStaticMeshComponent* StaticMeshComponent = InstancedActor->GetStaticMeshComponent();
68 UStaticMesh* Mesh = StaticMeshComponent->GetStaticMesh();
69 UClass* ClassPtr = InstancedActor->GetClass();
70
71 FString Name = IInteractable::Execute_GetInteractableName(InstancedActor).ToString();
72
73 if (Name.IsEmpty())
74 {
75 Name = ClassPtr->GetAuthoredName();
76 if (Name.EndsWith(TEXT("_C")))
77 {
78 Name = Name.LeftChop(2);
79 }
80 if (Name.StartsWith(TEXT("BP_")))
81 {
82 Name = Name.RightChop(3);
83 }
84 Name.ReplaceInline(TEXT("_"), TEXT(" "));
85 Name.ReplaceInline(TEXT("Instanced"), TEXT(" "));
86 }
87
88#if WITH_EDITOR
89 FString ActorName = Name;
90 ActorName.ReplaceInline(TEXT(","), TEXT(""));
91 ActorName.ReplaceInline(TEXT(" "), TEXT("_"));
92 ActorName += "_Instanced";
93 SetActorLabel(ActorName);
94#endif
95
96 // Generate a 4-character GUID
97 FString Guid = FGuid::NewGuid().ToString().Left(4);
98 Name = FString::Printf(TEXT("%s(_%s_)"), *Name, *Guid);
99
100 InstancedStaticMeshComponent = NewObject<UInstancedStaticMeshComponent>(this);
101 InstancedStaticMeshComponent->AttachToComponent(GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
102 AddInstanceComponent(InstancedStaticMeshComponent);
103
104 // Add new instance.
105 const FTransform& Transform = InstancedActor->GetActorTransform();
107
108 InstancedStaticMeshComponent->Rename(*Name);
109 InstancedStaticMeshComponent->bEditableWhenInherited = false;
110 InstancedStaticMeshComponent->RegisterComponent();
111 InstancedStaticMeshComponent->SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
112 InstancedStaticMeshComponent->CreatePhysicsState(false);
113 InstancedStaticMeshComponent->SetSimulatePhysics(false);
114 InstancedStaticMeshComponent->SetStaticMesh(Mesh);
115 InstancedStaticMeshComponent->SetMobility(EComponentMobility::Static);
116 InstancedStaticMeshComponent->SetHiddenInGame(false);
117 InstancedStaticMeshComponent->SetVisibility(true);
118 InstancedStaticMeshComponent->AttachToComponent(RootComponent, FAttachmentTransformRules::KeepWorldTransform);
119 InstancedStaticMeshComponent->InstanceStartCullDistance = InstancedActor->GetInstanceStartCullDistance();
120 InstancedStaticMeshComponent->InstanceEndCullDistance = InstancedActor->GetInstanceEndCullDistance();
121
122 bool AllowWorldPositionOffsetDisable = InstancedActor->GetAllowWorldPositionOffsetDisable();
123
124 // For whatever reason this only works when done a bit later..
125 FTimerHandle Handle;
126 GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([this, AllowWorldPositionOffsetDisable] {
127 if (InstancedStaticMeshComponent && AllowWorldPositionOffsetDisable && CurrentWeatherParameters.WindIntensity < 0.05f)
128 {
129 InstancedStaticMeshComponent->SetEvaluateWorldPositionOffset(false);
130 InstancedStaticMeshComponent->SetEvaluateWorldPositionOffsetInRayTracing(false);
131 }}), 0.350f, false);
132
133 // Thermal and Semantic camera use custom depth.
134 // RenderCustomDepth is set to false, if no Thermal or Semantic camera exists in the level,
135 // and is automatically set to true/false for all instances when they are spawned/destroyed.
136 // Doing this saves a lot rendering performance.
137 InstancedStaticMeshComponent->SetRenderCustomDepth(true);
138 InstancedStaticMeshComponent->SetCustomDepthStencilValue(StaticMeshComponent->CustomDepthStencilValue);
139
140 // Huge performance implication, recommend is Rigid (especially for Foliage with WPO).
141 // https://docs.unrealengine.com/5.3/en-US/PythonAPI/class/ShadowCacheInvalidationBehavior.html
142 InstancedStaticMeshComponent->ShadowCacheInvalidationBehavior = EShadowCacheInvalidationBehavior::Rigid;
143
144 const int32 NumberOfMaterials = StaticMeshComponent->GetNumMaterials();
145 TArray<UMaterialInterface*> MeshMaterials;
146 MeshMaterials.Reserve(NumberOfMaterials);
147 for (int32 i = 0; i < NumberOfMaterials; i++)
148 {
149 MeshMaterials.Add(StaticMeshComponent->GetMaterial(i));
150 }
151
154 InstancedActorParameters.Materials = MeshMaterials;
155 InstancedActorParameters.CustomDepthStencilValue = StaticMeshComponent->CustomDepthStencilValue;
156 InstancedActorParameters.AllowWorldPositionOffsetDisable = AllowWorldPositionOffsetDisable;
157 InstancedActorParameters.IsTree = InstancedActor->IsTreeActor();
158
159 // Set materials to newly created UInstancedStaticMeshComponent
160 const int32 MaterialCount = MeshMaterials.Num();
161 for (int32 MaterialIndex = 0; MaterialIndex < MaterialCount; ++MaterialIndex)
162 {
163 UMaterialInterface* MaterialInterface = InstancedActorParameters.Materials[MaterialIndex];
164
165 if (MaterialInterface)
166 {
167 InstancedStaticMeshComponent->SetMaterial(MaterialIndex, MaterialInterface);
168
169#if WITH_EDITOR
170 // Validate in Editor that material has bUsedWithInstancedStaticMeshes on
171 UMaterial* Material = Cast<UMaterial>(MaterialInterface);
172 if (Material)
173 {
174 if (!Material->bUsedWithInstancedStaticMeshes)
175 {
176 UE_LOG(LogTemp, Warning, TEXT("AInstancedRendererManager: Material '%s' bUsedWithInstancedStaticMeshes is not set to true!"), *Material->GetName());
177 }
178 }
179#endif
180 }
181 }
182
183 InstancedActor->InstanceAdded();
184 InstancedActor->Destroy();
185}
186
187bool AInstancedActorRenderer::SpawnInstanceBackToActor(UInstancedStaticMeshComponent* ISM, int32 Index, bool OnlyTree)
188{
189 if (!ISM || !InstancedStaticMeshComponent|| Index < 0 || Index >= ISM->GetInstanceCount())
190 {
191 return false;
192 }
193
195 {
196 return false;
197 }
198
199 UWorld* World = GetWorld();
200 if (!World)
201 {
202 return false;
203 }
204
205 if (OnlyTree && !InstancedActorParameters.IsTree)
206 {
207 return false;
208 }
209
210 UClass* ActorClass = InstancedActorParameters.ActorClass;
211 if (!ActorClass)
212 {
213 return false;
214 }
215
216 // Get the transform for the specified instance
217 FTransform InstanceTransform;
218 InstancedStaticMeshComponent->GetInstanceTransform(Index, InstanceTransform);
219
220 // Spawn the actor
221 AActor* SpawnedActor = World->SpawnActorDeferred<AActor>(ActorClass, InstanceTransform);
222 if (SpawnedActor)
223 {
224 AInstancedActor* InstancedActor = Cast<AInstancedActor>(SpawnedActor);
225 if (InstancedActor)
226 {
227 InstancedActor->AddToInstancedRenderer = false;
228 }
229
230 SpawnedActor->FinishSpawning(InstanceTransform);
231
232 // Remove this ISM instance.
233 return InstancedStaticMeshComponent->RemoveInstance(Index);
234 }
235
236 return true;
237}
238
240{
242 {
243 return InstancedStaticMeshComponent->GetStaticMesh();
244 }
245
246 return nullptr;
247}
248
250{
251 UWorld* World = GetWorld();
252
253 if (!World)
254 {
255 return false;
256 }
257
259 {
260 return false;
261 }
262
263 UClass* Class = InstancedActorParameters.ActorClass;
264 if (!Class)
265 {
266 UE_LOG(LogTemp, Warning, TEXT("AInstancedActorRenderer: No spawn class setup."));
267 return false;
268 }
269
270 InstancedStaticMeshComponent->SetVisibility(false);
271
272 // Extract transforms from ISM component into TArray
273 TArray<FTransform> Transforms;
274
275 const int32 InstanceCount = InstancedStaticMeshComponent->GetInstanceCount();
276
277 for (int32 i = 0; i < InstanceCount; i++)
278 {
279 FTransform Transform;
280 InstancedStaticMeshComponent->GetInstanceTransform(i, Transform);
281 Transforms.Add(Transform);
282 }
283
284 // Spawn actors to level
285 for (const FTransform& Transform : Transforms)
286 {
287 if (Class)
288 {
289 AActor* SpawnedActor = World->SpawnActorDeferred<AActor>(Class, Transform);
290 if (SpawnedActor)
291 {
292 SpawnedActor->FinishSpawning(Transform);
293 }
294 }
295 }
296
297 InstancedStaticMeshComponent->DestroyComponent();
298
299 return true;
300}
301
302bool AInstancedActorRenderer::Matches(UStaticMeshComponent* StaticMeshComponent, const bool CanHaveAlternativeMaterial) const
303{
304 if (!StaticMeshComponent)
305 {
306 return false;
307 }
308
309 UStaticMesh* StaticMesh = StaticMeshComponent->GetStaticMesh();
310 if (!StaticMesh)
311 {
312 return false;
313 }
314
315 if (!CanHaveAlternativeMaterial)
316 {
317 if (InstancedActorParameters.Mesh == StaticMesh)
318 {
319 return true;
320 }
321 }
322 else
323 {
324 const int32 NumberOfMaterials = StaticMeshComponent->GetNumMaterials();
325 TArray<UMaterialInterface*> MeshMaterials;
326 MeshMaterials.Reserve(NumberOfMaterials);
327 for (int32 i = 0; i < NumberOfMaterials; i++)
328 {
329 MeshMaterials.Add(StaticMeshComponent->GetMaterial(i));
330 }
331
332 if (InstancedActorParameters.Mesh == StaticMesh)
333 {
334 bool bMaterialsMatch = true;
335
336 for (int32 MaterialIndex = 0; MaterialIndex < InstancedActorParameters.Materials.Num(); ++MaterialIndex)
337 {
338 UMaterialInterface* UniqueMeshMaterial = InstancedActorParameters.Materials[MaterialIndex];
339 UMaterialInterface* MeshMaterial = MeshMaterials[MaterialIndex];
340 if (UniqueMeshMaterial != MeshMaterial)
341 {
342 bMaterialsMatch = false;
343 }
344 }
345 MeshMaterials.Empty();
346
347 if (bMaterialsMatch)
348 {
349 return true;
350 }
351 }
352 }
353
354 return false;
355}
356
357bool AInstancedActorRenderer::Matches(UInstancedStaticMeshComponent* ISM) const
358{
360 {
361 return false;
362 }
363
364 if (ISM->GetStaticMesh() == InstancedActorParameters.Mesh)
365 {
366 const int32 NumberOfMaterials = ISM->GetNumMaterials();
367 if (NumberOfMaterials != InstancedActorParameters.Materials.Num())
368 {
369 return false;
370 }
371
372 for (int32 i = 0; i < NumberOfMaterials; i++)
373 {
374 if (ISM->GetMaterial(i) != InstancedActorParameters.Materials[i])
375 {
376 return false;
377 }
378 }
379
380 return true;
381 }
382
383 return false;
384}
385
387{
388 if (WorldPositionOffsetDistance == NewWPODistance)
389 {
390 return;
391 }
392
393 WorldPositionOffsetDistance = NewWPODistance;
394
396 {
397 InstancedStaticMeshComponent->SetWorldPositionOffsetDisableDistance(WorldPositionOffsetDistance);
398 }
399}
400
401void AInstancedActorRenderer::SetShadowCacheBehaviour(EShadowCacheInvalidationBehavior ShadowCacheInvalidationBehaviour)
402{
404 {
405 InstancedStaticMeshComponent->ShadowCacheInvalidationBehavior = ShadowCacheInvalidationBehaviour;
406 }
407}
408
410{
412 {
413 InstancedStaticMeshComponent->SetEvaluateWorldPositionOffset(RenderWPO);
414 InstancedStaticMeshComponent->SetEvaluateWorldPositionOffsetInRayTracing(RenderWPO);
415 }
416}
417
419{
422}
423
425{
426 CurrentWeatherParameters = WeatherParameters;
427
428 bool HideLeaves = WeatherParameters.HideTreeLeaves;
429 bool IsWinter = WeatherParameters.IsWinterSeason();
430 bool IsWindy = WeatherParameters.WindIntensity > 0.05f;
431
432 // Should render tree wind (WPO)
433 bool ShouldRenderTreeWPO = IsWinter || IsWindy;
434
435 // Should render snow (WPO), like rocks etc.
436 bool ShouldRenderSnowWPO = WeatherParameters.SnowAmount != 0.0f;
437
438 bool ShouldRenderWPO = IsWinter || IsWindy;
439
440 // If HideLeaves is true, we need to ensure RenderingWPO is set to true
441 bool NewRenderingWPO = ShouldRenderWPO || HideLeaves;
442
443 // Only update if the rendering state has changed
444 if (RenderingWPO != NewRenderingWPO)
445 {
446 RenderingWPO = NewRenderingWPO;
447 SetRenderWorldPositionOffet(NewRenderingWPO);
448 }
449}
AInstancedActorRenderer(const FObjectInitializer &ObjectInitializer)
void SetupFromInstancedActor(AInstancedActor *InstancedActor)
bool Matches(UStaticMeshComponent *StaticMeshComponent, const bool CanHaveAlternativeMaterial) const
void OnGraphicsSettingsChanged(FGlobalGraphicsSettings GraphicsSettings)
void OnWeatherParametersChanged(FWeatherParameters WeatherParameters)
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override
FWeatherParameters CurrentWeatherParameters
void SetShadowCacheBehaviour(EShadowCacheInvalidationBehavior ShadowCacheInvalidationBehaviour)
void SetRenderWorldPositionOffet(bool RenderWPO)
FInstancedActorParameters InstancedActorParameters
bool SpawnInstanceBackToActor(UInstancedStaticMeshComponent *ISM, int32 Index, bool OnlyTree)
void SetWorldPositionOffsetDistance(int32 NewWPODistance)
virtual void BeginPlay() override
UInstancedStaticMeshComponent * InstancedStaticMeshComponent
UStaticMesh * GetStaticMesh() const
int32 GetInstanceEndCullDistance() const
int32 GetInstanceStartCullDistance() const
UStaticMeshComponent * GetStaticMeshComponent() const
bool GetAllowWorldPositionOffsetDisable() const
bool IsTreeActor() const
const FWeatherParameters & GetCurrentWeather() const
Definition: Weather.h:76
FLevelEventDelegate_WeatherChanged OnWeatherChanged
Definition: Weather.h:91
int32 GetWorldPositionOffsetRenderDistance() const
FGraphicsSettingsDelegate OnGraphicsSettingsChanged
static UAgrarsenseSettings * GetAgrarsenseSettings()
static AWeather * GetWeatherActor(const UObject *WorldContextObject)
EShadowCacheInvalidationBehavior FoliageShadowCacheInvalidationBehaviour
TSubclassOf< AActor > ActorClass
TWeakObjectPtr< UStaticMesh > Mesh
TArray< UMaterialInterface * > Materials