IVSmoke 1.0
Loading...
Searching...
No Matches
IVSmokeVoxelVolume.cpp
1// Copyright (c) 2026, Team SDB. All rights reserved.
2
3#include "IVSmokeVoxelVolume.h"
4
5#include "Components/InstancedStaticMeshComponent.h"
6#include "Engine/World.h"
7#include "EngineUtils.h"
8#include "GameFramework/GameStateBase.h"
9#include "IVSmoke.h"
10#include "IVSmokeCollisionComponent.h"
11#include "IVSmokeGridLibrary.h"
12#include "IVSmokeHoleGeneratorComponent.h"
13#include "Components/BillboardComponent.h"
14#include "Net/UnrealNetwork.h"
15
16#if WITH_EDITOR
17#include "Editor.h"
18#endif
19
20DECLARE_CYCLE_STAT(TEXT("Update Expansion"), STAT_IVSmoke_UpdateExpansion, STATGROUP_IVSmoke);
21DECLARE_CYCLE_STAT(TEXT("Update Sustain"), STAT_IVSmoke_UpdateSustain, STATGROUP_IVSmoke);
22DECLARE_CYCLE_STAT(TEXT("Update Dissipation"), STAT_IVSmoke_UpdateDissipation, STATGROUP_IVSmoke);
23DECLARE_CYCLE_STAT(TEXT("Process Expansion"), STAT_IVSmoke_ProcessExpansion, STATGROUP_IVSmoke);
24DECLARE_CYCLE_STAT(TEXT("Prepare Dissipation"), STAT_IVSmoke_PrepareDissipation, STATGROUP_IVSmoke);
25DECLARE_CYCLE_STAT(TEXT("Process Dissipation"), STAT_IVSmoke_ProcessDissipation, STATGROUP_IVSmoke);
26
27DECLARE_DWORD_COUNTER_STAT(TEXT("Active Voxel Count"), STAT_IVSmoke_ActiveVoxelCount, STATGROUP_IVSmoke);
28DECLARE_DWORD_COUNTER_STAT(TEXT("Created Voxel Count (Per Frame)"), STAT_IVSmoke_CreatedVoxel, STATGROUP_IVSmoke);
29DECLARE_DWORD_COUNTER_STAT(TEXT("Destroyed Voxel Count (Per Frame)"), STAT_IVSmoke_DestroyedVoxel, STATGROUP_IVSmoke);
30
31namespace IVSmokeVoxelVolumeCVars
32{
33 static void ForEachVoxelVolume(UWorld* World, TFunctionRef<void(AIVSmokeVoxelVolume*)> Func)
34 {
35 if (!World)
36 {
37 return;
38 }
39
40 for (TActorIterator<AIVSmokeVoxelVolume> Iter(World); Iter; ++Iter)
41 {
42 if (AIVSmokeVoxelVolume* Volume = *Iter)
43 {
44 Func(Volume);
45 }
46 }
47 }
48
49 static void SetVoxelVolumeDebug(const TArray<FString>& Args, UWorld* World, bool FIVSmokeDebugSettings::* MemberProp, const TCHAR* PropName)
50 {
51 int32 UpdatedCount = 0;
52 bool bLastState = false;
53
54 ForEachVoxelVolume(World, [&](AIVSmokeVoxelVolume* Volume)
55 {
56 bool& bValue = Volume->DebugSettings.*MemberProp;
57
58 bool bNewValue = (Args.Num() > 0 ? Args[0].ToBool() : !bValue);
59 bValue = bNewValue;
60 bLastState = bNewValue;
61
62 UpdatedCount++;
63 });
64
65#if WITH_EDITOR
66 if (UpdatedCount > 0 && Args.Num() == 0)
67 {
68 UE_LOG(LogIVSmoke, Log, TEXT("[IVSmoke.Volume] %s toggled to: %s (%d Actors updated)"),
69 PropName, bLastState ? TEXT("ON") : TEXT("OFF"), UpdatedCount);
70 }
71#endif
72 }
73
74 static FAutoConsoleCommandWithWorldAndArgs Cmd_Volume_Debug(
75 TEXT("IVSmoke.Volume.Debug"),
76 TEXT("Master toggle for Voxel Volume debug visualization.\nUsage: IVSmoke.Volume.Debug [0/1]"),
77 FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& Args, UWorld* World)
78 {
79 SetVoxelVolumeDebug(Args, World, &FIVSmokeDebugSettings::bDebugEnabled, TEXT("DebugEnabled"));
80 })
81 );
82
83 static FAutoConsoleCommandWithWorldAndArgs Cmd_Volume_ShowWireframe(
84 TEXT("IVSmoke.Volume.ShowWireframe"),
85 TEXT("Toggle wireframe cubes for active voxels.\nUsage: IVSmoke.Volume.ShowWireframe [0/1]"),
86 FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& Args, UWorld* World)
87 {
88 SetVoxelVolumeDebug(Args, World, &FIVSmokeDebugSettings::bShowVoxelWireframe, TEXT("ShowVoxelWireframe"));
89 })
90 );
91
92 static FAutoConsoleCommandWithWorldAndArgs Cmd_Volume_ShowMesh(
93 TEXT("IVSmoke.Volume.ShowMesh"),
94 TEXT("Toggle instanced static meshes for voxels (Expensive).\nUsage: IVSmoke.Volume.ShowMesh [0/1]"),
95 FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& Args, UWorld* World)
96 {
97 SetVoxelVolumeDebug(Args, World, &FIVSmokeDebugSettings::bShowVoxelMesh, TEXT("ShowVoxelMesh"));
98 })
99 );
100
101 static FAutoConsoleCommandWithWorldAndArgs Cmd_Volume_ShowStatus(
102 TEXT("IVSmoke.Volume.ShowStatus"),
103 TEXT("Toggle floating status text above the volume.\nUsage: IVSmoke.Volume.ShowStatus [0/1]"),
104 FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& Args, UWorld* World)
105 {
106 SetVoxelVolumeDebug(Args, World, &FIVSmokeDebugSettings::bShowStatusText, TEXT("ShowStatusText"));
107 })
108 );
109
110 static FAutoConsoleCommandWithWorldAndArgs Cmd_Volume_SetViewMode(
111 TEXT("IVSmoke.Volume.SetViewMode"),
112 TEXT("Set visualization mode.\n0: SolidColor, 1: Heatmap\nUsage: IVSmoke.Volume.SetViewMode <ModeID>"),
113 FConsoleCommandWithWorldAndArgsDelegate::CreateLambda([](const TArray<FString>& Args, UWorld* World)
114 {
115 if (Args.Num() == 0) return;
116
117 int32 ModeIndex = FCString::Atoi(*Args[0]);
118 EIVSmokeDebugViewMode NewMode = (ModeIndex == 1) ? EIVSmokeDebugViewMode::Heatmap : EIVSmokeDebugViewMode::SolidColor;
119
120 ForEachVoxelVolume(World, [NewMode](AIVSmokeVoxelVolume* Volume)
121 {
122 Volume->DebugSettings.ViewMode = NewMode;
123 });
124
125 UE_LOG(LogIVSmoke, Log, TEXT("[IVSmoke.Volume] ViewMode changed to: %s"),
126 (NewMode == EIVSmokeDebugViewMode::Heatmap) ? TEXT("Heatmap") : TEXT("SolidColor"));
127 })
128 );
129}
130
131static const FIntVector FloodFillDirections[] = {
132 FIntVector(1, 0, 0), FIntVector(-1, 0, 0),
133 FIntVector(0, 1, 0), FIntVector(0, -1, 0),
134 FIntVector(0, 0, 1), FIntVector(0, 0, -1)
135};
136
137//~==============================================================================
138// Actor Lifecycle
139#pragma region Lifecycle
140AIVSmokeVoxelVolume::AIVSmokeVoxelVolume()
141{
142 PrimaryActorTick.bCanEverTick = true;
143 bReplicates = true;
144
145 Tags.Add(IVSmokeVoxelVolumeTag);
146
147 BillboardComponent = CreateDefaultSubobject<UBillboardComponent>(TEXT("BillboardComponent"));
148
149 RootComponent = BillboardComponent;
150
151#if WITH_EDITORONLY_DATA
152 BillboardComponent->SetEditorScale(1.0f);
153
154 ConstructorHelpers::FObjectFinder<UTexture2D> IconTexture(TEXT("/IVSmoke/Icons/T_IVSmoke_VoxelVolumeIcon.T_IVSmoke_VoxelVolumeICon"));
155 if (IconTexture.Succeeded())
156 {
157 BillboardComponent->SetSprite(IconTexture.Object);
158 }
159#endif
160
161#if WITH_EDITORONLY_DATA
162 DebugMeshComponent = CreateDefaultSubobject<UInstancedStaticMeshComponent>(TEXT("DebugMeshComponent"));
163 DebugMeshComponent->SetupAttachment(RootComponent);
164 DebugMeshComponent->SetCastShadow(false);
165 DebugMeshComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
166 DebugMeshComponent->SetGenerateOverlapEvents(false);
167 DebugMeshComponent->NumCustomDataFloats = 1;
168#endif
169}
170
171void AIVSmokeVoxelVolume::BeginPlay()
172{
173#if WITH_EDITOR
174 // Lock all Transform changes during PIE to prevent World Partition errors
175 // caused by CollisionComponent's dynamic BodySetup
176 bLockLocation = true;
177#endif
178
179 if (HasAuthority())
180 {
181 ServerState = FIVSmokeServerState();
182 }
183
184 Initialize();
185
186 ClearSimulationData();
187
188 Super::BeginPlay();
189
190 HoleGeneratorComponent = FindComponentByClass<UIVSmokeHoleGeneratorComponent>();
191
192 CollisionComponent = FindComponentByClass<UIVSmokeCollisionComponent>();
193
194 if (HasAuthority())
195 {
196 if (bAutoStart)
197 {
199 }
200 }
201}
202
203void AIVSmokeVoxelVolume::EndPlay(const EEndPlayReason::Type EndPlayReason)
204{
205 // Reset state so ShouldRender() returns false (prevents rendering after PIE exit)
206 ServerState.State = EIVSmokeVoxelVolumeState::Idle;
207
208 Super::EndPlay(EndPlayReason);
209}
210
211void AIVSmokeVoxelVolume::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
212{
213 Super::GetLifetimeReplicatedProps(OutLifetimeProps);
214
215 DOREPLIFETIME(AIVSmokeVoxelVolume, ServerState);
216}
217
218void AIVSmokeVoxelVolume::Tick(float DeltaTime)
219{
220 UWorld* World = GetWorld();
221 if (World && World->GetNetMode() == NM_Client)
222 {
223 if (World->GetGameState() == nullptr)
224 {
225 return;
226 }
227 }
228
229 Super::Tick(DeltaTime);
230
231 if (ActiveVoxelNum > 0)
232 {
233 INC_DWORD_STAT_BY(STAT_IVSmoke_ActiveVoxelCount, ActiveVoxelNum);
234 }
235
236 switch (ServerState.State)
237 {
238 case EIVSmokeVoxelVolumeState::Expansion:
239 UpdateExpansion();
240 break;
241 case EIVSmokeVoxelVolumeState::Sustain:
242 UpdateSustain();
243 break;
244 case EIVSmokeVoxelVolumeState::Dissipation:
245 UpdateDissipation();
246 break;
247 case EIVSmokeVoxelVolumeState::Finished:
248 [[fallthrough]];
249 case EIVSmokeVoxelVolumeState::Idle:
250 [[fallthrough]];
251 default:
252 break;
253 }
254
255 TryUpdateCollision();
256
257#if WITH_EDITOR
258 if (DebugSettings.bDebugEnabled)
259 {
260 DrawDebugVisualization();
261 }
262 else if (DebugMeshComponent && DebugMeshComponent->GetInstanceCount() > 0)
263 {
264 DebugMeshComponent->ClearInstances();
265 }
266#endif
267}
268
269bool AIVSmokeVoxelVolume::ShouldTickIfViewportsOnly() const
270{
271 if (GetWorld() != nullptr && GetWorld()->WorldType == EWorldType::Editor && DebugSettings.bDebugEnabled)
272 {
273 return bIsEditorPreviewing;
274 }
275 return false;
276}
277
278void AIVSmokeVoxelVolume::OnConstruction(const FTransform& Transform)
279{
280 Super::OnConstruction(Transform);
281}
282
283#if WITH_EDITOR
284void AIVSmokeVoxelVolume::PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent)
285{
286 Super::PostEditChangeProperty(PropertyChangedEvent);
287
288 FName PropertyName = (PropertyChangedEvent.Property != nullptr) ? PropertyChangedEvent.Property->GetFName() : NAME_None;
289 bool bStructuralChange =
290 PropertyName == GET_MEMBER_NAME_CHECKED(AIVSmokeVoxelVolume, VolumeExtent) ||
291 PropertyName == GET_MEMBER_NAME_CHECKED(AIVSmokeVoxelVolume, MaxVoxelNum);
292
293 bool bParamChange =
294 PropertyName == GET_MEMBER_NAME_CHECKED(AIVSmokeVoxelVolume, VoxelSize) ||
295 PropertyName == GET_MEMBER_NAME_CHECKED(AIVSmokeVoxelVolume, Radii) ||
296 PropertyName == GET_MEMBER_NAME_CHECKED(AIVSmokeVoxelVolume, ExpansionNoise) ||
297 PropertyName == GET_MEMBER_NAME_CHECKED(AIVSmokeVoxelVolume, DissipationNoise);
298
299 // Handle bDebugEnabled toggle: stop preview if disabled during preview
300 if (PropertyName == GET_MEMBER_NAME_CHECKED(FIVSmokeDebugSettings, bDebugEnabled))
301 {
302 if (!DebugSettings.bDebugEnabled && bIsEditorPreviewing)
303 {
305 }
306 }
307
308 if (DebugSettings.bDebugEnabled)
309 {
310 if (bStructuralChange || bParamChange)
311 {
314 }
315 }
316}
317
318void AIVSmokeVoxelVolume::PostEditMove(bool bFinished)
319{
320 Super::PostEditMove(bFinished);
321
322 if (bFinished && DebugSettings.bDebugEnabled)
323 {
325 }
326}
327
328bool AIVSmokeVoxelVolume::CanEditChange(const FProperty* InProperty) const
329{
330 if (!Super::CanEditChange(InProperty))
331 {
332 return false;
333 }
334
335 if (!InProperty)
336 {
337 return true;
338 }
339
340 // Block Rotation and Scale editing in Details panel
341 const FName PropertyName = InProperty->GetFName();
342 if (PropertyName == TEXT("RelativeRotation") ||
343 PropertyName == TEXT("RelativeScale3D"))
344 {
345 return false;
346 }
347
348 return true;
349}
350
351void AIVSmokeVoxelVolume::EditorApplyRotation(const FRotator& DeltaRotation, bool bAltDown, bool bShiftDown, bool bCtrlDown)
352{
353 // Block rotation via gizmo - do nothing
354 UE_LOG(LogIVSmoke, Warning, TEXT("[AIVSmokeVoxelVolume] Rotation is not supported."));
355}
356
357void AIVSmokeVoxelVolume::EditorApplyScale(const FVector& DeltaScale, const FVector* PivotLocation, bool bAltDown, bool bShiftDown, bool bCtrlDown)
358{
359 // Block scale via gizmo - do nothing
360 UE_LOG(LogIVSmoke, Warning, TEXT("[AIVSmokeVoxelVolume] Scale is not supported."));
361}
362
363void AIVSmokeVoxelVolume::EditorApplyTranslation(const FVector& DeltaTranslation, bool bAltDown, bool bShiftDown, bool bCtrlDown)
364{
365 // Block translation during PIE (editor world actor doesn't have bLockLocation set)
366 if (GEditor && GEditor->IsPlaySessionInProgress())
367 {
368 return;
369 }
370
371 Super::EditorApplyTranslation(DeltaTranslation, bAltDown, bShiftDown, bCtrlDown);
372}
373
374bool AIVSmokeVoxelVolume::IsSelectable() const
375{
376 return Super::IsSelectable();
377}
378#endif
379
380#pragma endregion
381
382//~==============================================================================
383// Flood Fill Simulation
384#pragma region Simulation
385
387{
388 FIntVector GridResolution = GetGridResolution();
389
390 const int32 TotalGridSizeYZ = GridResolution.Y * GridResolution.Z;
391 const int32 TotalGridSize = GridResolution.X * TotalGridSizeYZ;
392
393 if (VoxelBirthTimes.Num() != TotalGridSize)
394 {
395 VoxelBirthTimes.SetNumZeroed(TotalGridSize);
396 }
397
398 if (VoxelDeathTimes.Num() != TotalGridSize)
399 {
400 VoxelDeathTimes.SetNumZeroed(TotalGridSize);
401 }
402
403 if (VoxelCosts.Num() != TotalGridSize)
404 {
405 VoxelCosts.SetNumUninitialized(TotalGridSize);
406 }
407
408 if (VoxelBits.Num() != TotalGridSizeYZ)
409 {
410 VoxelBits.SetNumUninitialized(TotalGridSizeYZ);
411 }
412
413 if (VoxelPenetrationFlags.Num() != TotalGridSize)
414 {
415 VoxelPenetrationFlags.Init(false, TotalGridSize);
416 }
417
418 GeneratedVoxelIndices.Reserve(MaxVoxelNum);
419
420 ExpansionHeap.Reserve(MaxVoxelNum);
421 DissipationHeap.Reserve(MaxVoxelNum);
422
423 bIsInitialized = true;
424}
425
426void AIVSmokeVoxelVolume::StartSimulation_Implementation()
427{
428 StartSimulationInternal();
429}
430
431void AIVSmokeVoxelVolume::StopSimulation_Implementation(bool bImmediate)
432{
433 StopSimulationInternal(bImmediate);
434}
435
436void AIVSmokeVoxelVolume::ResetSimulation_Implementation()
437{
438 ResetSimulationInternal();
439}
440
441void AIVSmokeVoxelVolume::OnRep_ServerState()
442{
443 UWorld* World = GetWorld();
444 if (World && World->GetNetMode() == NM_Client)
445 {
446 AGameStateBase* GameState = World->GetGameState();
447
448 if (!bIsInitialized || !GameState || GameState->GetServerWorldTimeSeconds() == 0.0f)
449 {
450 FTimerHandle RetryHandle;
451 World->GetTimerManager().SetTimer(RetryHandle, this, &AIVSmokeVoxelVolume::OnRep_ServerState, 0.1f, false);
452
453 UE_LOG(LogIVSmoke, Warning, TEXT("[AIVSmokeVoxelVolume::OnRep_ServerState] GameState not ready yet. Retrying in 0.1s..."));
454 return;
455 }
456 }
457
458 if (LocalGeneration != ServerState.Generation)
459 {
460 FastForwardSimulation();
461
462 LocalGeneration = ServerState.Generation;
463
464 TryUpdateCollision(true);
465
466 return;
467 }
468
469 HandleStateTransition(ServerState.State);
470}
471
472void AIVSmokeVoxelVolume::HandleStateTransition(EIVSmokeVoxelVolumeState NewState)
473{
474 if (LocalState == NewState)
475 {
476 return;
477 }
478
479 SimTime = 0.0f;
480
481 switch (NewState)
482 {
483 case EIVSmokeVoxelVolumeState::Idle:
484 ClearSimulationData();
485 break;
486 case EIVSmokeVoxelVolumeState::Expansion:
487 {
488 if (LocalState != EIVSmokeVoxelVolumeState::Idle &&
489 LocalState != EIVSmokeVoxelVolumeState::Finished)
490 {
491 ClearSimulationData();
492 }
493
494 RandomStream.Initialize(ServerState.RandomSeed);
495
497
498 if (VoxelCosts.IsValidIndex(CenterIndex))
499 {
500 VoxelCosts[CenterIndex] = 0.0f;
501 ExpansionHeap.HeapPush({CenterIndex, INDEX_NONE, 0.0f});
502
503 }
504 break;
505 }
506 case EIVSmokeVoxelVolumeState::Sustain:
507 TryUpdateCollision(true);
508 break;
509 case EIVSmokeVoxelVolumeState::Dissipation:
510 break;
511 case EIVSmokeVoxelVolumeState::Finished:
513 {
514 if (GetWorld() && GetWorld()->IsGameWorld())
515 {
516 Destroy();
517 }
518 else
519 {
520 ClearSimulationData();
521 bIsEditorPreviewing = false;
522 }
523 }
524 ClearSimulationData();
525 break;
526 }
527
528 LocalState = NewState;
529}
530
531void AIVSmokeVoxelVolume::ClearSimulationData()
532{
533 if (!bIsInitialized)
534 {
535 Initialize();
536 }
537
538 FIntVector GridResolution = GetGridResolution();
539
540 const int32 TotalGridSizeYZ = GridResolution.Y * GridResolution.Z;
541 const int32 TotalGridSize = GridResolution.X * TotalGridSizeYZ;
542
543 if (VoxelBirthTimes.Num() != TotalGridSize || VoxelDeathTimes.Num() != TotalGridSize || VoxelBits.Num() != TotalGridSizeYZ)
544 {
545 UE_LOG(LogIVSmoke, Warning, TEXT("[ClearSimulationData] Buffer size mismatch detected. Re-initializing..."));
546 Initialize();
547 }
548
549 FMemory::Memzero(VoxelBirthTimes.GetData(), VoxelBirthTimes.Num() * sizeof(float));
550
551 FMemory::Memzero(VoxelDeathTimes.GetData(), VoxelDeathTimes.Num() * sizeof(float));
552
553 FMemory::Memzero(VoxelBits.GetData(), VoxelBits.Num() * sizeof(uint64));
554
555 VoxelPenetrationFlags.Reset();
556 VoxelPenetrationFlags.Init(false, VoxelCosts.Num());
557
558 VoxelCosts.Init(FLT_MAX, VoxelCosts.Num());
559
560 GeneratedVoxelIndices.Reset();
561
562 ExpansionHeap.Reset();
563 DissipationHeap.Reset();
564
565 ActiveVoxelNum = 0;
566 SimTime = 0.0f;
567 DirtyLevel = EIVSmokeDirtyLevel::Dirty;
568
569 // Reset AABB to invalid state so new simulation starts fresh
570 VoxelWorldAABBMin = FVector(FLT_MAX, FLT_MAX, FLT_MAX);
571 VoxelWorldAABBMax = FVector(-FLT_MAX, -FLT_MAX, -FLT_MAX);
572
573 if (CollisionComponent)
574 {
575 CollisionComponent->ResetCollision();
576 }
577
578 if (HoleGeneratorComponent)
579 {
580 HoleGeneratorComponent->Reset();
581 }
582}
583
584bool AIVSmokeVoxelVolume::IsConnectionBlocked(const UWorld* World, const FVector& BeginPos, const FVector& EndPos) const
585{
586 TRACE_CPUPROFILER_EVENT_SCOPE_TEXT("IVSmoke::AIVSmokeVoxelVolume::IsConnectionBlocked");
587
589 {
590 return false;
591 }
592
593 if (!World)
594 {
595 return false;
596 }
597
598 FCollisionQueryParams CollisionParams;
599 CollisionParams.bTraceComplex = false;
600 CollisionParams.AddIgnoredActor(this);
601
602 FHitResult HitResult;
603 bool bIsBlocked = World->LineTraceSingleByChannel(
604 HitResult,
605 BeginPos,
606 EndPos,
608 CollisionParams
609 );
610
611 if (bIsBlocked)
612 {
613 return true;
614 }
615
616 return World->LineTraceSingleByChannel(
617 HitResult,
618 EndPos,
619 BeginPos,
621 CollisionParams
622 );
623}
624
625void AIVSmokeVoxelVolume::StartSimulationInternal()
626{
627 if (!bIsInitialized)
628 {
629 Initialize();
630 }
631
632 ResetSimulationInternal();
633
634 ServerState.RandomSeed = FMath::Rand();
636
637 ServerState.SustainStartTime = 0.0f;
638 ServerState.DissipationStartTime = 0.0f;
639
640 ServerState.State = EIVSmokeVoxelVolumeState::Expansion;
641
642 HandleStateTransition(ServerState.State);
643}
644
645void AIVSmokeVoxelVolume::StopSimulationInternal(bool bImmediate)
646{
647 if (ServerState.State == EIVSmokeVoxelVolumeState::Finished)
648 {
649 return;
650 }
651
652 if (bImmediate)
653 {
654 ServerState.State = EIVSmokeVoxelVolumeState::Finished;
655 }
656 else if (ServerState.State == EIVSmokeVoxelVolumeState::Expansion ||
657 ServerState.State == EIVSmokeVoxelVolumeState::Sustain)
658 {
659 ServerState.State = EIVSmokeVoxelVolumeState::Dissipation;
661 }
662
663 HandleStateTransition(ServerState.State);
664}
665
666void AIVSmokeVoxelVolume::ResetSimulationInternal()
667{
668 ServerState.State = EIVSmokeVoxelVolumeState::Idle;
669 ServerState.Generation += 1;
670
671 ServerState.ExpansionStartTime = 0.0f;
672 ServerState.SustainStartTime = 0.0f;
673 ServerState.DissipationStartTime = 0.0f;
674
675 ClearSimulationData();
676 LocalState = EIVSmokeVoxelVolumeState::Idle;
677}
678
679void AIVSmokeVoxelVolume::FastForwardSimulation()
680{
681 bIsFastForwarding = true;
682
683 if (ServerState.State == EIVSmokeVoxelVolumeState::Expansion ||
684 ServerState.State == EIVSmokeVoxelVolumeState::Sustain ||
685 ServerState.State == EIVSmokeVoxelVolumeState::Dissipation)
686 {
687 HandleStateTransition(EIVSmokeVoxelVolumeState::Expansion);
688 UpdateExpansion();
689 }
690 if (ServerState.State == EIVSmokeVoxelVolumeState::Sustain ||
691 ServerState.State == EIVSmokeVoxelVolumeState::Dissipation)
692 {
693 HandleStateTransition(EIVSmokeVoxelVolumeState::Sustain);
694 UpdateSustain();
695 }
696 if (ServerState.State == EIVSmokeVoxelVolumeState::Dissipation)
697 {
698 HandleStateTransition(EIVSmokeVoxelVolumeState::Dissipation);
699 UpdateDissipation();
700 }
701
702 HandleStateTransition(ServerState.State);
703
704 bIsFastForwarding = false;
705}
706
707void AIVSmokeVoxelVolume::UpdateExpansion()
708{
709 SCOPE_CYCLE_COUNTER(STAT_IVSmoke_UpdateExpansion);
710 TRACE_CPUPROFILER_EVENT_SCOPE_TEXT("IVSmoke::AIVSmokeVoxelVolume::UpdateExpansion");
711
712 const float CurrentSyncTime = GetSyncWorldTimeSeconds();
713 const float CurrentSimTime = CurrentSyncTime - ServerState.ExpansionStartTime;
714
715 float StartSimTime = SimTime;
716 float EndSimTime = CurrentSimTime;
717
718 SimTime = CurrentSimTime;
719
720 int32 TargetSpawnNum = 0;
721
722 if (EndSimTime < ExpansionDuration)
723 {
724 float CurveValue = GetCurveValue(CurrentSimTime, ExpansionDuration, ExpansionCurve);
725 TargetSpawnNum = FMath::FloorToInt(MaxVoxelNum * CurveValue);
726 }
727 else
728 {
729 EndSimTime = ExpansionDuration;
730 TargetSpawnNum = MaxVoxelNum;
731 }
732
733 int32 SpawnNum = TargetSpawnNum - ActiveVoxelNum;
734
735 if (!ExpansionHeap.IsEmpty() && SpawnNum > 0)
736 {
737 ProcessExpansion(SpawnNum, StartSimTime, EndSimTime);
738 }
739
740 if (CurrentSimTime >= ExpansionDuration + FadeInDuration)
741 {
742 if (HasAuthority())
743 {
744 ServerState.State = EIVSmokeVoxelVolumeState::Sustain;
746
747 HandleStateTransition(ServerState.State);
748 }
749 }
750}
751
752void AIVSmokeVoxelVolume::UpdateSustain()
753{
754 SCOPE_CYCLE_COUNTER(STAT_IVSmoke_UpdateSustain);
755 TRACE_CPUPROFILER_EVENT_SCOPE_TEXT("IVSmoke::AIVSmokeVoxelVolume::UpdateSustain");
756
757 const float CurrentSyncTime = GetSyncWorldTimeSeconds();
758 const float CurrentSimTime = CurrentSyncTime - ServerState.SustainStartTime;
759
760 SimTime = CurrentSimTime;
761
762 if (!bIsInfinite && CurrentSimTime >= SustainDuration)
763 {
764 if (HasAuthority())
765 {
766 ServerState.State = EIVSmokeVoxelVolumeState::Dissipation;
768
769 HandleStateTransition(ServerState.State);
770 }
771 }
772}
773
774void AIVSmokeVoxelVolume::UpdateDissipation()
775{
776 SCOPE_CYCLE_COUNTER(STAT_IVSmoke_UpdateDissipation);
777 TRACE_CPUPROFILER_EVENT_SCOPE_TEXT("IVSmoke::AIVSmokeVoxelVolume::UpdateDissipation");
778
779 const float CurrentSyncTime = GetSyncWorldTimeSeconds();
780 const float CurrentSimTime = CurrentSyncTime - ServerState.DissipationStartTime;
781
782 float StartSimTime = SimTime;
783 float EndSimTime = CurrentSimTime;
784
785 SimTime = CurrentSimTime;
786
787 int32 TargetAliveNum = GeneratedVoxelIndices.Num();
788
789 if (CurrentSimTime < DissipationDuration)
790 {
791 float CurveValue = 1.0f;
793 {
794 CurveValue = GetCurveValue(CurrentSimTime, DissipationDuration, DissipationCurve);
795 }
796 else
797 {
798 CurveValue = FMath::Max(0.0f, 1.0f - GetCurveValue(CurrentSimTime, DissipationDuration, DissipationCurve));
799 }
800 TargetAliveNum = FMath::FloorToInt(GeneratedVoxelIndices.Num() * CurveValue);
801 }
802 else
803 {
804 EndSimTime = DissipationDuration;
805 TargetAliveNum = 0;
806 }
807
808 int32 RemoveNum = DissipationHeap.Num() - TargetAliveNum;
809
810 if (RemoveNum > 0)
811 {
812 ProcessDissipation(RemoveNum, StartSimTime, EndSimTime);
813 }
814
815 if (CurrentSimTime >= DissipationDuration + FadeOutDuration)
816 {
817 SimTime = 0.0f;
818
819 TryUpdateCollision(true);
820
821 if (HasAuthority())
822 {
823 ServerState.State = EIVSmokeVoxelVolumeState::Finished;
824
825 HandleStateTransition(ServerState.State);
826 }
827 }
828}
829
830void AIVSmokeVoxelVolume::ProcessExpansion(int32 SpawnNum, float StartSimTime, float EndSimTime)
831{
832 SCOPE_CYCLE_COUNTER(STAT_IVSmoke_ProcessExpansion);
833 TRACE_CPUPROFILER_EVENT_SCOPE_TEXT("IVSmoke::AIVSmokeVoxelVolume::ProcessExpansion");
834
835 if (SpawnNum <= 0)
836 {
837 return;
838 }
839
840 UWorld* World = GetWorld();
841 if (!World)
842 {
843 return;
844 }
845
846 const FTransform ActorTrans = GetActorTransform();
847 const FIntVector GridResolution = GetGridResolution();
848 const FIntVector CenterOffset = GetCenterOffset();
849
850 FVector InvRadii;
851 InvRadii.X = 1.0f / FMath::Max(UE_KINDA_SMALL_NUMBER, Radii.X);
852 InvRadii.Y = 1.0f / FMath::Max(UE_KINDA_SMALL_NUMBER, Radii.Y);
853 InvRadii.Z = 1.0f / FMath::Max(UE_KINDA_SMALL_NUMBER, Radii.Z);
854
855 const float InvSpawnNum = 1.0f / SpawnNum;
856 int32 SpawnCount = 0;
857
858 while (SpawnCount < SpawnNum && !ExpansionHeap.IsEmpty())
859 {
860 FIVSmokeVoxelNode CurrentNode;
861 ExpansionHeap.HeapPop(CurrentNode);
862
863 if (!VoxelCosts.IsValidIndex(CurrentNode.Index))
864 {
865 continue;
866 }
867
868 if (CurrentNode.Cost > VoxelCosts[CurrentNode.Index])
869 {
870 continue;
871 }
872
873 if (IsVoxelActive(CurrentNode.Index))
874 {
875 continue;
876 }
877
878 int32 ResolvedParentIndex = CurrentNode.ParentIndex;
879 float ResolvedCost = CurrentNode.Cost;
880
881 const FIntVector CurrentGrid = UIVSmokeGridLibrary::IndexToGrid(CurrentNode.Index, GridResolution);
882 const FIntVector ParentGrid = UIVSmokeGridLibrary::IndexToGrid(CurrentNode.ParentIndex, GridResolution);
883 const FVector CurrentLocal = UIVSmokeGridLibrary::GridToLocal(CurrentGrid, VoxelSize, CenterOffset);
884 const FVector ParentLocal = UIVSmokeGridLibrary::GridToLocal(ParentGrid, VoxelSize, CenterOffset);
885 const FVector CurrentWorld = ActorTrans.TransformPositionNoScale(CurrentLocal);
886 const FVector ParentWorld = ActorTrans.TransformPositionNoScale(ParentLocal);
887
888 bool bIsPenetrating = false;
889
890 if (CurrentNode.ParentIndex != INDEX_NONE && VoxelCosts.IsValidIndex(CurrentNode.ParentIndex))
891 {
892 const int32 ManhattanDist = FMath::Abs(CurrentGrid.X - ParentGrid.X) +
893 FMath::Abs(CurrentGrid.Y - ParentGrid.Y) +
894 FMath::Abs(CurrentGrid.Z - ParentGrid.Z);
895 if (ManhattanDist > 1)
896 {
897 if (IsConnectionBlocked(World, ParentWorld, CurrentWorld))
898 {
899 float BestRewireCost = FLT_MAX;
900 int32 BestRewireParent = INDEX_NONE;
901
902 for (const FIntVector& Direction : FloodFillDirections)
903 {
904 const FIntVector NeighborGrid = CurrentGrid + Direction;
905 const int32 NeighborIndex = UIVSmokeGridLibrary::GridToIndex(NeighborGrid, GridResolution);
906
907 if (!VoxelCosts.IsValidIndex(NeighborIndex))
908 {
909 continue;
910 }
911
912 if (IsVoxelActive(NeighborIndex))
913 {
914 if (VoxelPenetrationFlags[NeighborIndex])
915 {
916 continue;
917 }
918
919 const float Dist = CalculateWeightedDistance(NeighborIndex, CurrentNode.Index, InvRadii);
920 const float NewCost = VoxelCosts[NeighborIndex] + Dist;
921
922 if (NewCost < BestRewireCost)
923 {
924 BestRewireCost = NewCost;
925 BestRewireParent = NeighborIndex;
926 }
927 }
928 }
929
930 if (BestRewireParent != INDEX_NONE)
931 {
932 ResolvedParentIndex = BestRewireParent;
933 ResolvedCost = BestRewireCost;
934 VoxelCosts[CurrentNode.Index] = ResolvedCost;
935
936 const FIntVector NeighborGrid = UIVSmokeGridLibrary::IndexToGrid(BestRewireParent, GridResolution);
937 const FVector NeighborLocal = UIVSmokeGridLibrary::GridToLocal(NeighborGrid, VoxelSize, CenterOffset);
938 const FVector NeighborWorld = ActorTrans.TransformPosition(NeighborLocal);
939
940 if (IsConnectionBlocked(World, CurrentWorld, NeighborWorld))
941 {
942 bIsPenetrating = true;
943 }
944 }
945 else
946 {
947 continue;
948 }
949 }
950 }
951 else
952 {
953 if (IsConnectionBlocked(World, CurrentWorld, ParentWorld))
954 {
955 bIsPenetrating = true;
956 }
957 }
958 }
959
960 const float Alpha = SpawnCount * InvSpawnNum;
961 const float BirthTime = ServerState.ExpansionStartTime + FMath::Lerp(StartSimTime, EndSimTime, Alpha);
962 SetVoxelBirthTime(CurrentNode.Index, BirthTime);
963
964 GeneratedVoxelIndices.Add(CurrentNode.Index);
965 ++SpawnCount;
966
967 const float DissipationCost = ResolvedCost + RandomStream.FRandRange(0.0f, DissipationNoise);
968 DissipationHeap.HeapPush({ CurrentNode.Index, INDEX_NONE, DissipationCost });
969
971 {
972 return;
973 }
974
975 if (bIsPenetrating)
976 {
977 VoxelPenetrationFlags[CurrentNode.Index] = true;
978
979 continue;
980 }
981
982 for (const FIntVector& Direction : FloodFillDirections)
983 {
984 const FIntVector NextGrid = CurrentGrid + Direction;
985
986 if (NextGrid.X < 0 || NextGrid.X >= GridResolution.X ||
987 NextGrid.Y < 0 || NextGrid.Y >= GridResolution.Y ||
988 NextGrid.Z < 0 || NextGrid.Z >= GridResolution.Z)
989 {
990 continue;
991 }
992
993 const int32 NextIndex = UIVSmokeGridLibrary::GridToIndex(NextGrid, GridResolution);
994
995 if (!VoxelCosts.IsValidIndex(NextIndex))
996 {
997 continue;
998 }
999
1000 if (ResolvedParentIndex != INDEX_NONE)
1001 {
1002 const float GrandparentDist = CalculateWeightedDistance(ResolvedParentIndex, NextIndex, InvRadii);
1003 const float Noise = RandomStream.FRandRange(0.0f, ExpansionNoise);
1004 const float NewCost = VoxelCosts[ResolvedParentIndex] + GrandparentDist + Noise;
1005
1006 if (NewCost < VoxelCosts[NextIndex])
1007 {
1008 VoxelCosts[NextIndex] = NewCost;
1009 ExpansionHeap.HeapPush({NextIndex, ResolvedParentIndex, NewCost});
1010 }
1011 }
1012 else
1013 {
1014 const float CurrentDist = CalculateWeightedDistance(CurrentNode.Index, NextIndex, InvRadii);
1015 const float Noise = RandomStream.FRandRange(0.0f, ExpansionNoise);
1016 const float NewCost = ResolvedCost + CurrentDist + Noise;
1017
1018 if (NewCost < VoxelCosts[NextIndex])
1019 {
1020 VoxelCosts[NextIndex] = NewCost;
1021 ExpansionHeap.HeapPush({NextIndex, CurrentNode.Index, NewCost});
1022 }
1023 }
1024
1025 }
1026 }
1027}
1028
1029void AIVSmokeVoxelVolume::ProcessDissipation(int32 RemoveNum, float StartSimTime, float EndSimTime)
1030{
1031 SCOPE_CYCLE_COUNTER(STAT_IVSmoke_ProcessDissipation);
1032 TRACE_CPUPROFILER_EVENT_SCOPE_TEXT("IVSmoke::AIVSmokeVoxelVolume::ProcessDissipation");
1033
1034 if (RemoveNum <= 0)
1035 {
1036 return;
1037 }
1038
1039 float InvRemoveNum = 1.0f / RemoveNum;
1040
1041 int32 RemoveCount = 0;
1042 while (RemoveCount < RemoveNum && !DissipationHeap.IsEmpty())
1043 {
1044 FIVSmokeVoxelNode CurrentNode;
1045 DissipationHeap.HeapPop(CurrentNode);
1046
1047 float Alpha = RemoveCount * InvRemoveNum;
1048 float DeathTime = ServerState.DissipationStartTime + FMath::Lerp(StartSimTime, EndSimTime, Alpha);
1049
1050 SetVoxelDeathTime(CurrentNode.Index, DeathTime);
1051
1052 ++RemoveCount;
1053 }
1054}
1055
1056void AIVSmokeVoxelVolume::SetVoxelBirthTime(int32 Index, float BirthTime)
1057{
1058 if (!VoxelBirthTimes.IsValidIndex(Index))
1059 {
1060 return;
1061 }
1062
1063 if (VoxelBirthTimes[Index] > 0.0f)
1064 {
1065 return;
1066 }
1067
1068 float SafeBirthTime = FMath::Max(BirthTime, 0.001f);
1069 VoxelBirthTimes[Index] = SafeBirthTime;
1070
1071 if (VoxelDeathTimes.IsValidIndex(Index))
1072 {
1073 VoxelDeathTimes[Index] = 0.0f;
1074 }
1075
1076 FIntVector GridResolution = GetGridResolution();
1077 FIntVector CenterOffset = GetCenterOffset();
1078
1079 UIVSmokeGridLibrary::SetVoxelBit(VoxelBits, Index, GridResolution, true);
1080
1081 ++ActiveVoxelNum;
1082 INC_DWORD_STAT(STAT_IVSmoke_CreatedVoxel);
1083
1084 DirtyLevel = EIVSmokeDirtyLevel::Dirty;
1085
1086 const FIntVector GridPos = UIVSmokeGridLibrary::IndexToGrid(Index, GridResolution);
1087 const FVector LocalPos = UIVSmokeGridLibrary::GridToLocal(GridPos, VoxelSize, CenterOffset);
1088 const FVector WorldPos = GetActorTransform().TransformPosition(LocalPos);
1089 VoxelWorldAABBMin = FVector::Min(WorldPos, VoxelWorldAABBMin);
1090 VoxelWorldAABBMax = FVector::Max(WorldPos, VoxelWorldAABBMax);
1091}
1092
1093void AIVSmokeVoxelVolume::SetVoxelDeathTime(int32 Index, float DeathTime)
1094{
1095 if (!VoxelDeathTimes.IsValidIndex(Index))
1096 {
1097 return;
1098 }
1099
1100 if (VoxelDeathTimes[Index] > 0.0f)
1101 {
1102 return;
1103 }
1104
1105 const float SafeDeathTime = FMath::Max(DeathTime, 0.001f);
1106 VoxelDeathTimes[Index] = SafeDeathTime;
1107
1108 FIntVector GridResolution = GetGridResolution();
1109
1110 UIVSmokeGridLibrary::SetVoxelBit(VoxelBits, Index, GridResolution, false);
1111
1112 --ActiveVoxelNum;
1113 INC_DWORD_STAT(STAT_IVSmoke_DestroyedVoxel)
1114
1115 DirtyLevel = EIVSmokeDirtyLevel::Dirty;
1116}
1117
1118float AIVSmokeVoxelVolume::CalculateWeightedDistance(int32 IndexA, int32 IndexB, const FVector& InvRadii) const
1119{
1120 if (!VoxelCosts.IsValidIndex(IndexA) || !VoxelCosts.IsValidIndex(IndexB))
1121 {
1122 return FLT_MAX;
1123 }
1124
1125 const FIntVector GridResolution = GetGridResolution();
1126 const FIntVector CenterOffset = GetCenterOffset();
1127
1128 const FIntVector GridA = UIVSmokeGridLibrary::IndexToGrid(IndexA, GridResolution);
1129 const FIntVector GridB = UIVSmokeGridLibrary::IndexToGrid(IndexB, GridResolution);
1130
1131 const FVector DeltaGrid(GridB - GridA);
1132 const FVector DeltaLocal = DeltaGrid * VoxelSize;
1133
1134 const float NormX = DeltaLocal.X * InvRadii.X;
1135 const float NormY = DeltaLocal.Y * InvRadii.Y;
1136 const float NormZ = DeltaLocal.Z * InvRadii.Z;
1137
1138 return FMath::Sqrt(NormX * NormX + NormY * NormY + NormZ * NormZ);
1139}
1140
1141#pragma endregion
1142
1143//~==============================================================================
1144// Collision
1145#pragma region Collision
1146
1147void AIVSmokeVoxelVolume::TryUpdateCollision(bool bForce)
1148{
1149 if (bIsFastForwarding)
1150 {
1151 return;
1152 }
1153
1154 if (CollisionComponent)
1155 {
1156 CollisionComponent->TryUpdateCollision(
1157 VoxelBits,
1159 VoxelSize,
1160 ActiveVoxelNum,
1162 bForce
1163 );
1164 }
1165}
1166
1167#pragma endregion
1168
1169//~==============================================================================
1170// Data Access
1171#pragma region DataAccess
1172
1174{
1175 // Must be initialized with valid voxel buffers
1176 if (!bIsInitialized)
1177 {
1178 return false;
1179 }
1180
1181 // Respect Actor visibility
1182#if WITH_EDITOR
1183 // Editor: check both editor visibility (Outliner eye icon) and game visibility
1184 if (IsHiddenEd() || IsHidden())
1185 {
1186 return false;
1187 }
1188
1189 // Editor preview mode: special handling
1190 if (bIsEditorPreviewing)
1191 {
1192 // If PIE is running, don't render editor preview volumes
1193 // This prevents conflicts between editor and PIE worlds
1194 if (GEditor && GEditor->IsPlayingSessionInEditor())
1195 {
1196 return false;
1197 }
1198
1199 // Check debug settings
1200 if (!DebugSettings.bDebugEnabled || !DebugSettings.bRenderSmokeInPreview)
1201 {
1202 return false;
1203 }
1204 }
1205#else
1206 // Runtime: check game visibility only
1207 if (IsHidden())
1208 {
1209 return false;
1210 }
1211#endif
1212
1213 const EIVSmokeVoxelVolumeState State = ServerState.State;
1214 return State == EIVSmokeVoxelVolumeState::Expansion
1215 || State == EIVSmokeVoxelVolumeState::Sustain
1216 || State == EIVSmokeVoxelVolumeState::Dissipation;
1217}
1218
1219TObjectPtr<UIVSmokeHoleGeneratorComponent> AIVSmokeVoxelVolume::GetHoleGeneratorComponent()
1220{
1221 if (!IsValid(HoleGeneratorComponent))
1222 {
1223 HoleGeneratorComponent = FindComponentByClass<UIVSmokeHoleGeneratorComponent>();
1224 }
1225
1226 return HoleGeneratorComponent;
1227}
1228
1229TObjectPtr<UIVSmokeCollisionComponent> AIVSmokeVoxelVolume::GetCollisionComponent()
1230{
1231 if (!IsValid(CollisionComponent))
1232 {
1233 CollisionComponent = FindComponentByClass<UIVSmokeCollisionComponent>();
1234 }
1235
1236 return CollisionComponent;
1237}
1238
1240{
1241 if (HoleGeneratorComponent)
1242 {
1243 return HoleGeneratorComponent->GetHoleTextureRHI();
1244 }
1245 return nullptr;
1246}
1247
1249{
1250 UWorld* World = GetWorld();
1251 if (!World)
1252 {
1253 return 0.0f;
1254 }
1255
1256 if (World->GetNetMode() == NM_Client)
1257 {
1258 if (AGameStateBase* GameState = World->GetGameState())
1259 {
1260 return GameState->GetServerWorldTimeSeconds();
1261 }
1262 }
1263
1264 return World->GetTimeSeconds();
1265}
1266
1267#pragma endregion
1268
1269//~==============================================================================
1270// Debug
1271#pragma region Debug
1272
1274{
1275#if WITH_EDITOR
1276 if (!DebugSettings.bDebugEnabled)
1277 {
1278 return;
1279 }
1280
1281 bIsEditorPreviewing = true;
1282
1283 StartSimulationInternal();
1284#endif
1285}
1286
1288{
1289#if WITH_EDITOR
1290 bIsEditorPreviewing = false;
1291
1292 ResetSimulationInternal();
1293
1294 if (DebugMeshComponent)
1295 {
1296 DebugMeshComponent->ClearInstances();
1297 }
1298#endif
1299}
1300
1301void AIVSmokeVoxelVolume::DrawDebugVisualization() const
1302{
1303#if WITH_EDITOR
1304 if (!DebugSettings.bDebugEnabled)
1305 {
1306 return;
1307 }
1308
1309 DrawDebugVolumeBounds();
1310 DrawDebugVoxelWireframes();
1311 DrawDebugVoxelMeshes();
1312 DrawDebugStatusText();
1313
1314 if (CollisionComponent)
1315 {
1316 CollisionComponent->DrawDebugVisualization();
1317 }
1318#endif
1319}
1320
1321void AIVSmokeVoxelVolume::DrawDebugVolumeBounds() const
1322{
1323#if WITH_EDITOR
1324 if (!DebugSettings.bShowVolumeBounds)
1325 {
1326 return;
1327 }
1328
1329 UWorld* World = GetWorld();
1330 if (!World)
1331 {
1332 return;
1333 }
1334
1335 FIntVector GridResolution = GetGridResolution();
1336
1337 FVector TotalSize;
1338 TotalSize.X = GridResolution.X * VoxelSize;
1339 TotalSize.Y = GridResolution.Y * VoxelSize;
1340 TotalSize.Z = GridResolution.Z * VoxelSize;
1341
1342 FVector HalfExtent = TotalSize * 0.5f;
1343
1344 FVector CenterPos = GetActorLocation();
1345 FQuat Rotation = GetActorRotation().Quaternion();
1346
1347 DrawDebugBox(
1348 World,
1349 CenterPos,
1350 HalfExtent,
1351 Rotation,
1352 FColor(100, 255, 100),
1353 false,
1354 -1.0f,
1355 0,
1356 2.0f
1357 );
1358#endif
1359}
1360
1361void AIVSmokeVoxelVolume::DrawDebugVoxelWireframes() const
1362{
1363#if WITH_EDITOR
1364 if (!DebugSettings.bShowVoxelWireframe || GeneratedVoxelIndices.IsEmpty())
1365 {
1366 return;
1367 }
1368
1369 UWorld* World = GetWorld();
1370 if (!World)
1371 {
1372 return;
1373 }
1374
1375 FTransform ActorTrans = GetActorTransform();
1376 int32 VoxelNum = GeneratedVoxelIndices.Num();
1377 int32 MaxVisibleIndex = FMath::Clamp(VoxelNum * DebugSettings.VisibleStepCountPercent / 100.0f, 0, VoxelNum);
1378
1379 const FVector HalfVoxelSize(VoxelSize * 0.5f);
1380 for (int32 i = 0; i < MaxVisibleIndex; ++i)
1381 {
1382 int32 VoxelIndex = GeneratedVoxelIndices[i];
1383 if (!IsVoxelActive(VoxelIndex))
1384 {
1385 continue;
1386 }
1387
1388 FIntVector GridResolution = GetGridResolution();
1389 FIntVector CenterOffset = GetCenterOffset();
1390
1391 FIntVector GridPos = UIVSmokeGridLibrary::IndexToGrid(VoxelIndex, GridResolution);
1392 float NormHeight = static_cast<float>(GridPos.Z) / static_cast<float>(GridResolution.Z);
1393 if (NormHeight > DebugSettings.SliceHeight)
1394 {
1395 continue;
1396 }
1397
1398 FVector LocalPos = UIVSmokeGridLibrary::GridToLocal(GridPos, VoxelSize, CenterOffset);
1399 FVector WorldPos = ActorTrans.TransformPosition(LocalPos);
1400
1401 DrawDebugBox(
1402 World,
1403 WorldPos,
1404 HalfVoxelSize,
1405 ActorTrans.GetRotation(),
1406 DebugSettings.DebugWireframeColor,
1407 false, -1.0f, 0, 1.5f
1408 );
1409 }
1410#endif
1411}
1412
1413void AIVSmokeVoxelVolume::DrawDebugVoxelMeshes() const
1414{
1415#if WITH_EDITOR
1416 if (!DebugMeshComponent)
1417 {
1418 return;
1419 }
1420
1421 if (!DebugSettings.bShowVoxelMesh || GeneratedVoxelIndices.IsEmpty())
1422 {
1423 DebugMeshComponent->ClearInstances();
1424 return;
1425 }
1426
1427 if (DebugVoxelMesh && DebugMeshComponent->GetStaticMesh() != DebugVoxelMesh)
1428 {
1429 DebugMeshComponent->SetStaticMesh(DebugVoxelMesh);
1430 }
1431 if (DebugVoxelMaterial && DebugMeshComponent->GetMaterial(0) != DebugVoxelMaterial)
1432 {
1433 DebugMeshComponent->SetMaterial(0, DebugVoxelMaterial);
1434 }
1435
1436 DebugMeshComponent->ClearInstances();
1437
1438 int32 VoxelNum = GeneratedVoxelIndices.Num();
1439 int32 MaxVisibleIndex = FMath::Clamp(static_cast<int32>(VoxelNum * DebugSettings.VisibleStepCountPercent / 100.0f), 0, VoxelNum);
1440
1441 TArray<FTransform> InstanceTransforms;
1442 InstanceTransforms.Reserve(MaxVisibleIndex);
1443
1444 TArray<float> InstanceCustomData;
1445 InstanceCustomData.Reserve(MaxVisibleIndex);
1446
1447 const FVector Scale3D(VoxelSize / 100.0f * 0.98f);
1448
1449 for (int32 i = 0; i < MaxVisibleIndex; ++i)
1450 {
1451 int32 VoxelIndex = GeneratedVoxelIndices[i];
1452
1453 if (!IsVoxelActive(VoxelIndex))
1454 {
1455 continue;
1456 }
1457
1458 FIntVector GridResolution = GetGridResolution();
1459 FIntVector CenterOffset = GetCenterOffset();
1460
1461 FIntVector GridPos = UIVSmokeGridLibrary::IndexToGrid(VoxelIndex, GridResolution);
1462
1463 float NormHeight = static_cast<float>(GridPos.Z) / static_cast<float>(GridResolution.Z);
1464 if (NormHeight > DebugSettings.SliceHeight)
1465 {
1466 continue;
1467 }
1468
1469 FVector LocalPos = UIVSmokeGridLibrary::GridToLocal(GridPos, VoxelSize, CenterOffset);
1470
1471 FTransform InstanceTrans;
1472 InstanceTrans.SetLocation(LocalPos);
1473 InstanceTrans.SetRotation(FQuat::Identity);
1474 InstanceTrans.SetScale3D(Scale3D);
1475
1476 InstanceTransforms.Add(InstanceTrans);
1477
1478 float DataValue = 0.0f;
1479 if (DebugSettings.ViewMode == EIVSmokeDebugViewMode::Heatmap)
1480 {
1481 DataValue = (VoxelNum > 1) ? static_cast<float>(i) / static_cast<float>(VoxelNum - 1) : 0.0f;
1482 }
1483 InstanceCustomData.Add(DataValue);
1484 }
1485
1486 if (InstanceTransforms.Num() > 0)
1487 {
1488 DebugMeshComponent->AddInstances(InstanceTransforms, false, false);
1489
1490 int32 InstanceNum = InstanceTransforms.Num();
1491 for (int32 i = 0; i < InstanceNum; ++i)
1492 {
1493 bool bIsLast = (i == InstanceNum - 1);
1494 DebugMeshComponent->SetCustomDataValue(i, 0, InstanceCustomData[i], bIsLast);
1495 }
1496 }
1497#endif
1498}
1499
1500void AIVSmokeVoxelVolume::DrawDebugStatusText() const
1501{
1502#if WITH_EDITOR
1503 if (!DebugSettings.bDebugEnabled || !DebugSettings.bShowStatusText)
1504 {
1505 return;
1506 }
1507
1508 UWorld* World = GetWorld();
1509 if (!World)
1510 {
1511 return;
1512 }
1513
1514 FString StateStr;
1515 switch (ServerState.State)
1516 {
1517 case EIVSmokeVoxelVolumeState::Idle: StateStr = TEXT("Idle"); break;
1518 case EIVSmokeVoxelVolumeState::Expansion: StateStr = TEXT("Expansion"); break;
1519 case EIVSmokeVoxelVolumeState::Sustain: StateStr = TEXT("Sustain"); break;
1520 case EIVSmokeVoxelVolumeState::Dissipation: StateStr = TEXT("Dissipation"); break;
1521 case EIVSmokeVoxelVolumeState::Finished: StateStr = TEXT("Finished"); break;
1522 default: StateStr = TEXT("Unknown"); break;
1523 }
1524
1525 float Percent = MaxVoxelNum > 0 ? (static_cast<float>(ActiveVoxelNum) / MaxVoxelNum * 100.0f) : 0.0f;
1526
1527 FString DebugMsg = FString::Printf(
1528 TEXT("State: %s\nSeed: %d\nTime: %.2fs\nVoxels: %d / %d (%.1f%%)\nHeap: %d\nChecksum: %u"),
1529 *StateStr,
1530 ServerState.RandomSeed,
1531 SimTime,
1532 ActiveVoxelNum,
1534 Percent,
1535 ExpansionHeap.Num(),
1536 CalculateSimulationChecksum()
1537 );
1538
1539 FIntVector GridResolution = GetGridResolution();
1540
1541 FVector TextPos = GetActorLocation();
1542 TextPos.Z += GridResolution.Z * VoxelSize * 0.25f;
1543
1544 DrawDebugString(World, TextPos, DebugMsg, nullptr, FColor::White, 0.0f, true, 1.2f);
1545#endif
1546}
1547
1548uint32 AIVSmokeVoxelVolume::CalculateSimulationChecksum() const
1549{
1550 uint32 Checksum = 0;
1551
1552 Checksum = FCrc::MemCrc32(&ActiveVoxelNum, sizeof(int32), Checksum);
1553
1554 int32 StateInt = (int32)ServerState.State;
1555 Checksum = FCrc::MemCrc32(&StateInt, sizeof(int32), Checksum);
1556
1557 if (VoxelBits.Num() > 0)
1558 {
1559 Checksum = FCrc::MemCrc32(VoxelBits.GetData(), VoxelBits.Num() * sizeof(uint64), Checksum);
1560 }
1561
1562 return Checksum;
1563}
1564
1565#pragma endregion
TObjectPtr< UBillboardComponent > BillboardComponent
float GetSyncWorldTimeSeconds() const
TObjectPtr< UIVSmokeCollisionComponent > GetCollisionComponent()
FIVSmokeDebugSettings DebugSettings
TObjectPtr< UMaterialInterface > DebugVoxelMaterial
FORCEINLINE bool IsVoxelActive(int32 Index) const
TEnumAsByte< ECollisionChannel > VoxelCollisionChannel
TObjectPtr< UStaticMesh > DebugVoxelMesh
TObjectPtr< UIVSmokeHoleGeneratorComponent > GetHoleGeneratorComponent()
TObjectPtr< UCurveFloat > ExpansionCurve
FORCEINLINE int32 GetActiveVoxelNum() const
FTextureRHIRef GetHoleTexture() const
FORCEINLINE FIntVector GetGridResolution() const
FORCEINLINE FIntVector GetCenterOffset() const
TObjectPtr< UCurveFloat > DissipationCurve
static FORCEINLINE void SetVoxelBit(TArray< uint64 > &VoxelBitArray, int32 Index, const FIntVector &Resolution, bool bValue)
static FORCEINLINE FIntVector IndexToGrid(int32 Index, const FIntVector &Resolution)
static FORCEINLINE FVector GridToLocal(const FIntVector &GridPos, float VoxelSize, const FIntVector &CenterOffset)
static FORCEINLINE int32 GridToIndex(const FIntVector &GridPos, const FIntVector &Resolution)
EIVSmokeVoxelVolumeState State