3#include "IVSmokeVoxelVolume.h"
5#include "Components/InstancedStaticMeshComponent.h"
6#include "Engine/World.h"
7#include "EngineUtils.h"
8#include "GameFramework/GameStateBase.h"
10#include "IVSmokeCollisionComponent.h"
11#include "IVSmokeGridLibrary.h"
12#include "IVSmokeHoleGeneratorComponent.h"
13#include "Components/BillboardComponent.h"
14#include "Net/UnrealNetwork.h"
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);
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);
31namespace IVSmokeVoxelVolumeCVars
33 static void ForEachVoxelVolume(UWorld* World, TFunctionRef<
void(
AIVSmokeVoxelVolume*)> Func)
40 for (TActorIterator<AIVSmokeVoxelVolume> Iter(World); Iter; ++Iter)
49 static void SetVoxelVolumeDebug(
const TArray<FString>& Args, UWorld* World,
bool FIVSmokeDebugSettings::* MemberProp, const TCHAR* PropName)
51 int32 UpdatedCount = 0;
52 bool bLastState =
false;
58 bool bNewValue = (Args.Num() > 0 ? Args[0].ToBool() : !bValue);
60 bLastState = bNewValue;
66 if (UpdatedCount > 0 && Args.Num() == 0)
68 UE_LOG(LogIVSmoke, Log, TEXT(
"[IVSmoke.Volume] %s toggled to: %s (%d Actors updated)"),
69 PropName, bLastState ? TEXT(
"ON") : TEXT(
"OFF"), UpdatedCount);
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)
79 SetVoxelVolumeDebug(Args, World, &FIVSmokeDebugSettings::bDebugEnabled, TEXT(
"DebugEnabled"));
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)
88 SetVoxelVolumeDebug(Args, World, &FIVSmokeDebugSettings::bShowVoxelWireframe, TEXT(
"ShowVoxelWireframe"));
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)
97 SetVoxelVolumeDebug(Args, World, &FIVSmokeDebugSettings::bShowVoxelMesh, TEXT(
"ShowVoxelMesh"));
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)
106 SetVoxelVolumeDebug(Args, World, &FIVSmokeDebugSettings::bShowStatusText, TEXT(
"ShowStatusText"));
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)
115 if (Args.Num() == 0)
return;
117 int32 ModeIndex = FCString::Atoi(*Args[0]);
118 EIVSmokeDebugViewMode NewMode = (ModeIndex == 1) ? EIVSmokeDebugViewMode::Heatmap : EIVSmokeDebugViewMode::SolidColor;
125 UE_LOG(LogIVSmoke, Log, TEXT(
"[IVSmoke.Volume] ViewMode changed to: %s"),
126 (NewMode == EIVSmokeDebugViewMode::Heatmap) ? TEXT(
"Heatmap") : TEXT(
"SolidColor"));
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)
139#pragma region Lifecycle
140AIVSmokeVoxelVolume::AIVSmokeVoxelVolume()
142 PrimaryActorTick.bCanEverTick =
true;
145 Tags.Add(IVSmokeVoxelVolumeTag);
147 BillboardComponent = CreateDefaultSubobject<UBillboardComponent>(TEXT(
"BillboardComponent"));
151#if WITH_EDITORONLY_DATA
154 ConstructorHelpers::FObjectFinder<UTexture2D> IconTexture(TEXT(
"/IVSmoke/Icons/T_IVSmoke_VoxelVolumeIcon.T_IVSmoke_VoxelVolumeICon"));
155 if (IconTexture.Succeeded())
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;
171void AIVSmokeVoxelVolume::BeginPlay()
176 bLockLocation =
true;
186 ClearSimulationData();
190 HoleGeneratorComponent = FindComponentByClass<UIVSmokeHoleGeneratorComponent>();
192 CollisionComponent = FindComponentByClass<UIVSmokeCollisionComponent>();
203void AIVSmokeVoxelVolume::EndPlay(
const EEndPlayReason::Type EndPlayReason)
206 ServerState.
State = EIVSmokeVoxelVolumeState::Idle;
208 Super::EndPlay(EndPlayReason);
211void AIVSmokeVoxelVolume::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps)
const
213 Super::GetLifetimeReplicatedProps(OutLifetimeProps);
218void AIVSmokeVoxelVolume::Tick(
float DeltaTime)
220 UWorld* World = GetWorld();
221 if (World && World->GetNetMode() == NM_Client)
223 if (World->GetGameState() ==
nullptr)
229 Super::Tick(DeltaTime);
231 if (ActiveVoxelNum > 0)
233 INC_DWORD_STAT_BY(STAT_IVSmoke_ActiveVoxelCount, ActiveVoxelNum);
236 switch (ServerState.
State)
238 case EIVSmokeVoxelVolumeState::Expansion:
241 case EIVSmokeVoxelVolumeState::Sustain:
244 case EIVSmokeVoxelVolumeState::Dissipation:
247 case EIVSmokeVoxelVolumeState::Finished:
249 case EIVSmokeVoxelVolumeState::Idle:
255 TryUpdateCollision();
260 DrawDebugVisualization();
262 else if (DebugMeshComponent && DebugMeshComponent->GetInstanceCount() > 0)
264 DebugMeshComponent->ClearInstances();
269bool AIVSmokeVoxelVolume::ShouldTickIfViewportsOnly()
const
271 if (GetWorld() !=
nullptr && GetWorld()->WorldType == EWorldType::Editor &&
DebugSettings.bDebugEnabled)
273 return bIsEditorPreviewing;
278void AIVSmokeVoxelVolume::OnConstruction(
const FTransform& Transform)
280 Super::OnConstruction(Transform);
284void AIVSmokeVoxelVolume::PostEditChangeProperty(
struct FPropertyChangedEvent& PropertyChangedEvent)
286 Super::PostEditChangeProperty(PropertyChangedEvent);
288 FName PropertyName = (PropertyChangedEvent.Property !=
nullptr) ? PropertyChangedEvent.Property->GetFName() : NAME_None;
289 bool bStructuralChange =
310 if (bStructuralChange || bParamChange)
318void AIVSmokeVoxelVolume::PostEditMove(
bool bFinished)
320 Super::PostEditMove(bFinished);
328bool AIVSmokeVoxelVolume::CanEditChange(
const FProperty* InProperty)
const
330 if (!Super::CanEditChange(InProperty))
341 const FName PropertyName = InProperty->GetFName();
342 if (PropertyName == TEXT(
"RelativeRotation") ||
343 PropertyName == TEXT(
"RelativeScale3D"))
351void AIVSmokeVoxelVolume::EditorApplyRotation(
const FRotator& DeltaRotation,
bool bAltDown,
bool bShiftDown,
bool bCtrlDown)
354 UE_LOG(LogIVSmoke, Warning, TEXT(
"[AIVSmokeVoxelVolume] Rotation is not supported."));
357void AIVSmokeVoxelVolume::EditorApplyScale(
const FVector& DeltaScale,
const FVector* PivotLocation,
bool bAltDown,
bool bShiftDown,
bool bCtrlDown)
360 UE_LOG(LogIVSmoke, Warning, TEXT(
"[AIVSmokeVoxelVolume] Scale is not supported."));
363void AIVSmokeVoxelVolume::EditorApplyTranslation(
const FVector& DeltaTranslation,
bool bAltDown,
bool bShiftDown,
bool bCtrlDown)
366 if (GEditor && GEditor->IsPlaySessionInProgress())
371 Super::EditorApplyTranslation(DeltaTranslation, bAltDown, bShiftDown, bCtrlDown);
374bool AIVSmokeVoxelVolume::IsSelectable()
const
376 return Super::IsSelectable();
384#pragma region Simulation
390 const int32 TotalGridSizeYZ = GridResolution.Y * GridResolution.Z;
391 const int32 TotalGridSize = GridResolution.X * TotalGridSizeYZ;
393 if (VoxelBirthTimes.Num() != TotalGridSize)
395 VoxelBirthTimes.SetNumZeroed(TotalGridSize);
398 if (VoxelDeathTimes.Num() != TotalGridSize)
400 VoxelDeathTimes.SetNumZeroed(TotalGridSize);
403 if (VoxelCosts.Num() != TotalGridSize)
405 VoxelCosts.SetNumUninitialized(TotalGridSize);
408 if (VoxelBits.Num() != TotalGridSizeYZ)
410 VoxelBits.SetNumUninitialized(TotalGridSizeYZ);
413 if (VoxelPenetrationFlags.Num() != TotalGridSize)
415 VoxelPenetrationFlags.Init(
false, TotalGridSize);
423 bIsInitialized =
true;
426void AIVSmokeVoxelVolume::StartSimulation_Implementation()
428 StartSimulationInternal();
431void AIVSmokeVoxelVolume::StopSimulation_Implementation(
bool bImmediate)
433 StopSimulationInternal(bImmediate);
436void AIVSmokeVoxelVolume::ResetSimulation_Implementation()
438 ResetSimulationInternal();
441void AIVSmokeVoxelVolume::OnRep_ServerState()
443 UWorld* World = GetWorld();
444 if (World && World->GetNetMode() == NM_Client)
446 AGameStateBase* GameState = World->GetGameState();
448 if (!bIsInitialized || !GameState || GameState->GetServerWorldTimeSeconds() == 0.0f)
450 FTimerHandle RetryHandle;
451 World->GetTimerManager().SetTimer(RetryHandle,
this, &AIVSmokeVoxelVolume::OnRep_ServerState, 0.1f,
false);
453 UE_LOG(LogIVSmoke, Warning, TEXT(
"[AIVSmokeVoxelVolume::OnRep_ServerState] GameState not ready yet. Retrying in 0.1s..."));
458 if (LocalGeneration != ServerState.
Generation)
460 FastForwardSimulation();
464 TryUpdateCollision(
true);
469 HandleStateTransition(ServerState.
State);
472void AIVSmokeVoxelVolume::HandleStateTransition(EIVSmokeVoxelVolumeState NewState)
474 if (LocalState == NewState)
483 case EIVSmokeVoxelVolumeState::Idle:
484 ClearSimulationData();
486 case EIVSmokeVoxelVolumeState::Expansion:
488 if (LocalState != EIVSmokeVoxelVolumeState::Idle &&
489 LocalState != EIVSmokeVoxelVolumeState::Finished)
491 ClearSimulationData();
494 RandomStream.Initialize(ServerState.
RandomSeed);
498 if (VoxelCosts.IsValidIndex(CenterIndex))
500 VoxelCosts[CenterIndex] = 0.0f;
501 ExpansionHeap.HeapPush({CenterIndex, INDEX_NONE, 0.0f});
506 case EIVSmokeVoxelVolumeState::Sustain:
507 TryUpdateCollision(
true);
509 case EIVSmokeVoxelVolumeState::Dissipation:
511 case EIVSmokeVoxelVolumeState::Finished:
514 if (GetWorld() && GetWorld()->IsGameWorld())
520 ClearSimulationData();
521 bIsEditorPreviewing =
false;
524 ClearSimulationData();
528 LocalState = NewState;
531void AIVSmokeVoxelVolume::ClearSimulationData()
540 const int32 TotalGridSizeYZ = GridResolution.Y * GridResolution.Z;
541 const int32 TotalGridSize = GridResolution.X * TotalGridSizeYZ;
543 if (VoxelBirthTimes.Num() != TotalGridSize || VoxelDeathTimes.Num() != TotalGridSize || VoxelBits.Num() != TotalGridSizeYZ)
545 UE_LOG(LogIVSmoke, Warning, TEXT(
"[ClearSimulationData] Buffer size mismatch detected. Re-initializing..."));
549 FMemory::Memzero(VoxelBirthTimes.GetData(), VoxelBirthTimes.Num() *
sizeof(
float));
551 FMemory::Memzero(VoxelDeathTimes.GetData(), VoxelDeathTimes.Num() *
sizeof(
float));
553 FMemory::Memzero(VoxelBits.GetData(), VoxelBits.Num() *
sizeof(uint64));
555 VoxelPenetrationFlags.Reset();
556 VoxelPenetrationFlags.Init(
false, VoxelCosts.Num());
558 VoxelCosts.Init(FLT_MAX, VoxelCosts.Num());
560 GeneratedVoxelIndices.Reset();
562 ExpansionHeap.Reset();
563 DissipationHeap.Reset();
567 DirtyLevel = EIVSmokeDirtyLevel::Dirty;
570 VoxelWorldAABBMin = FVector(FLT_MAX, FLT_MAX, FLT_MAX);
571 VoxelWorldAABBMax = FVector(-FLT_MAX, -FLT_MAX, -FLT_MAX);
573 if (CollisionComponent)
575 CollisionComponent->ResetCollision();
578 if (HoleGeneratorComponent)
580 HoleGeneratorComponent->Reset();
584bool AIVSmokeVoxelVolume::IsConnectionBlocked(
const UWorld* World,
const FVector& BeginPos,
const FVector& EndPos)
const
586 TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(
"IVSmoke::AIVSmokeVoxelVolume::IsConnectionBlocked");
598 FCollisionQueryParams CollisionParams;
599 CollisionParams.bTraceComplex =
false;
600 CollisionParams.AddIgnoredActor(
this);
602 FHitResult HitResult;
603 bool bIsBlocked = World->LineTraceSingleByChannel(
616 return World->LineTraceSingleByChannel(
625void AIVSmokeVoxelVolume::StartSimulationInternal()
632 ResetSimulationInternal();
640 ServerState.
State = EIVSmokeVoxelVolumeState::Expansion;
642 HandleStateTransition(ServerState.
State);
645void AIVSmokeVoxelVolume::StopSimulationInternal(
bool bImmediate)
647 if (ServerState.
State == EIVSmokeVoxelVolumeState::Finished)
654 ServerState.
State = EIVSmokeVoxelVolumeState::Finished;
656 else if (ServerState.
State == EIVSmokeVoxelVolumeState::Expansion ||
657 ServerState.
State == EIVSmokeVoxelVolumeState::Sustain)
659 ServerState.
State = EIVSmokeVoxelVolumeState::Dissipation;
663 HandleStateTransition(ServerState.
State);
666void AIVSmokeVoxelVolume::ResetSimulationInternal()
668 ServerState.
State = EIVSmokeVoxelVolumeState::Idle;
675 ClearSimulationData();
676 LocalState = EIVSmokeVoxelVolumeState::Idle;
679void AIVSmokeVoxelVolume::FastForwardSimulation()
681 bIsFastForwarding =
true;
683 if (ServerState.
State == EIVSmokeVoxelVolumeState::Expansion ||
684 ServerState.
State == EIVSmokeVoxelVolumeState::Sustain ||
685 ServerState.
State == EIVSmokeVoxelVolumeState::Dissipation)
687 HandleStateTransition(EIVSmokeVoxelVolumeState::Expansion);
690 if (ServerState.
State == EIVSmokeVoxelVolumeState::Sustain ||
691 ServerState.
State == EIVSmokeVoxelVolumeState::Dissipation)
693 HandleStateTransition(EIVSmokeVoxelVolumeState::Sustain);
696 if (ServerState.
State == EIVSmokeVoxelVolumeState::Dissipation)
698 HandleStateTransition(EIVSmokeVoxelVolumeState::Dissipation);
702 HandleStateTransition(ServerState.
State);
704 bIsFastForwarding =
false;
707void AIVSmokeVoxelVolume::UpdateExpansion()
709 SCOPE_CYCLE_COUNTER(STAT_IVSmoke_UpdateExpansion);
710 TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(
"IVSmoke::AIVSmokeVoxelVolume::UpdateExpansion");
715 float StartSimTime = SimTime;
716 float EndSimTime = CurrentSimTime;
718 SimTime = CurrentSimTime;
720 int32 TargetSpawnNum = 0;
725 TargetSpawnNum = FMath::FloorToInt(
MaxVoxelNum * CurveValue);
733 int32 SpawnNum = TargetSpawnNum - ActiveVoxelNum;
735 if (!ExpansionHeap.IsEmpty() && SpawnNum > 0)
737 ProcessExpansion(SpawnNum, StartSimTime, EndSimTime);
744 ServerState.
State = EIVSmokeVoxelVolumeState::Sustain;
747 HandleStateTransition(ServerState.
State);
752void AIVSmokeVoxelVolume::UpdateSustain()
754 SCOPE_CYCLE_COUNTER(STAT_IVSmoke_UpdateSustain);
755 TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(
"IVSmoke::AIVSmokeVoxelVolume::UpdateSustain");
758 const float CurrentSimTime = CurrentSyncTime - ServerState.
SustainStartTime;
760 SimTime = CurrentSimTime;
766 ServerState.
State = EIVSmokeVoxelVolumeState::Dissipation;
769 HandleStateTransition(ServerState.
State);
774void AIVSmokeVoxelVolume::UpdateDissipation()
776 SCOPE_CYCLE_COUNTER(STAT_IVSmoke_UpdateDissipation);
777 TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(
"IVSmoke::AIVSmokeVoxelVolume::UpdateDissipation");
782 float StartSimTime = SimTime;
783 float EndSimTime = CurrentSimTime;
785 SimTime = CurrentSimTime;
787 int32 TargetAliveNum = GeneratedVoxelIndices.Num();
791 float CurveValue = 1.0f;
800 TargetAliveNum = FMath::FloorToInt(GeneratedVoxelIndices.Num() * CurveValue);
808 int32 RemoveNum = DissipationHeap.Num() - TargetAliveNum;
812 ProcessDissipation(RemoveNum, StartSimTime, EndSimTime);
819 TryUpdateCollision(
true);
823 ServerState.
State = EIVSmokeVoxelVolumeState::Finished;
825 HandleStateTransition(ServerState.
State);
830void AIVSmokeVoxelVolume::ProcessExpansion(int32 SpawnNum,
float StartSimTime,
float EndSimTime)
832 SCOPE_CYCLE_COUNTER(STAT_IVSmoke_ProcessExpansion);
833 TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(
"IVSmoke::AIVSmokeVoxelVolume::ProcessExpansion");
840 UWorld* World = GetWorld();
846 const FTransform ActorTrans = GetActorTransform();
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);
855 const float InvSpawnNum = 1.0f / SpawnNum;
856 int32 SpawnCount = 0;
858 while (SpawnCount < SpawnNum && !ExpansionHeap.IsEmpty())
860 FIVSmokeVoxelNode CurrentNode;
861 ExpansionHeap.HeapPop(CurrentNode);
863 if (!VoxelCosts.IsValidIndex(CurrentNode.Index))
868 if (CurrentNode.Cost > VoxelCosts[CurrentNode.Index])
878 int32 ResolvedParentIndex = CurrentNode.ParentIndex;
879 float ResolvedCost = CurrentNode.Cost;
885 const FVector CurrentWorld = ActorTrans.TransformPositionNoScale(CurrentLocal);
886 const FVector ParentWorld = ActorTrans.TransformPositionNoScale(ParentLocal);
888 bool bIsPenetrating =
false;
890 if (CurrentNode.ParentIndex != INDEX_NONE && VoxelCosts.IsValidIndex(CurrentNode.ParentIndex))
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)
897 if (IsConnectionBlocked(World, ParentWorld, CurrentWorld))
899 float BestRewireCost = FLT_MAX;
900 int32 BestRewireParent = INDEX_NONE;
902 for (
const FIntVector& Direction : FloodFillDirections)
904 const FIntVector NeighborGrid = CurrentGrid + Direction;
907 if (!VoxelCosts.IsValidIndex(NeighborIndex))
914 if (VoxelPenetrationFlags[NeighborIndex])
919 const float Dist = CalculateWeightedDistance(NeighborIndex, CurrentNode.Index, InvRadii);
920 const float NewCost = VoxelCosts[NeighborIndex] + Dist;
922 if (NewCost < BestRewireCost)
924 BestRewireCost = NewCost;
925 BestRewireParent = NeighborIndex;
930 if (BestRewireParent != INDEX_NONE)
932 ResolvedParentIndex = BestRewireParent;
933 ResolvedCost = BestRewireCost;
934 VoxelCosts[CurrentNode.Index] = ResolvedCost;
938 const FVector NeighborWorld = ActorTrans.TransformPosition(NeighborLocal);
940 if (IsConnectionBlocked(World, CurrentWorld, NeighborWorld))
942 bIsPenetrating =
true;
953 if (IsConnectionBlocked(World, CurrentWorld, ParentWorld))
955 bIsPenetrating =
true;
960 const float Alpha = SpawnCount * InvSpawnNum;
961 const float BirthTime = ServerState.
ExpansionStartTime + FMath::Lerp(StartSimTime, EndSimTime, Alpha);
962 SetVoxelBirthTime(CurrentNode.Index, BirthTime);
964 GeneratedVoxelIndices.Add(CurrentNode.Index);
967 const float DissipationCost = ResolvedCost + RandomStream.FRandRange(0.0f,
DissipationNoise);
968 DissipationHeap.HeapPush({ CurrentNode.Index, INDEX_NONE, DissipationCost });
977 VoxelPenetrationFlags[CurrentNode.Index] =
true;
982 for (
const FIntVector& Direction : FloodFillDirections)
984 const FIntVector NextGrid = CurrentGrid + Direction;
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)
995 if (!VoxelCosts.IsValidIndex(NextIndex))
1000 if (ResolvedParentIndex != INDEX_NONE)
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;
1006 if (NewCost < VoxelCosts[NextIndex])
1008 VoxelCosts[NextIndex] = NewCost;
1009 ExpansionHeap.HeapPush({NextIndex, ResolvedParentIndex, NewCost});
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;
1018 if (NewCost < VoxelCosts[NextIndex])
1020 VoxelCosts[NextIndex] = NewCost;
1021 ExpansionHeap.HeapPush({NextIndex, CurrentNode.Index, NewCost});
1029void AIVSmokeVoxelVolume::ProcessDissipation(int32 RemoveNum,
float StartSimTime,
float EndSimTime)
1031 SCOPE_CYCLE_COUNTER(STAT_IVSmoke_ProcessDissipation);
1032 TRACE_CPUPROFILER_EVENT_SCOPE_TEXT(
"IVSmoke::AIVSmokeVoxelVolume::ProcessDissipation");
1039 float InvRemoveNum = 1.0f / RemoveNum;
1041 int32 RemoveCount = 0;
1042 while (RemoveCount < RemoveNum && !DissipationHeap.IsEmpty())
1044 FIVSmokeVoxelNode CurrentNode;
1045 DissipationHeap.HeapPop(CurrentNode);
1047 float Alpha = RemoveCount * InvRemoveNum;
1048 float DeathTime = ServerState.
DissipationStartTime + FMath::Lerp(StartSimTime, EndSimTime, Alpha);
1050 SetVoxelDeathTime(CurrentNode.Index, DeathTime);
1056void AIVSmokeVoxelVolume::SetVoxelBirthTime(int32 Index,
float BirthTime)
1058 if (!VoxelBirthTimes.IsValidIndex(Index))
1063 if (VoxelBirthTimes[Index] > 0.0f)
1068 float SafeBirthTime = FMath::Max(BirthTime, 0.001f);
1069 VoxelBirthTimes[Index] = SafeBirthTime;
1071 if (VoxelDeathTimes.IsValidIndex(Index))
1073 VoxelDeathTimes[Index] = 0.0f;
1082 INC_DWORD_STAT(STAT_IVSmoke_CreatedVoxel);
1084 DirtyLevel = EIVSmokeDirtyLevel::Dirty;
1088 const FVector WorldPos = GetActorTransform().TransformPosition(LocalPos);
1089 VoxelWorldAABBMin = FVector::Min(WorldPos, VoxelWorldAABBMin);
1090 VoxelWorldAABBMax = FVector::Max(WorldPos, VoxelWorldAABBMax);
1093void AIVSmokeVoxelVolume::SetVoxelDeathTime(int32 Index,
float DeathTime)
1095 if (!VoxelDeathTimes.IsValidIndex(Index))
1100 if (VoxelDeathTimes[Index] > 0.0f)
1105 const float SafeDeathTime = FMath::Max(DeathTime, 0.001f);
1106 VoxelDeathTimes[Index] = SafeDeathTime;
1113 INC_DWORD_STAT(STAT_IVSmoke_DestroyedVoxel)
1115 DirtyLevel = EIVSmokeDirtyLevel::Dirty;
1118float AIVSmokeVoxelVolume::CalculateWeightedDistance(int32 IndexA, int32 IndexB,
const FVector& InvRadii)
const
1120 if (!VoxelCosts.IsValidIndex(IndexA) || !VoxelCosts.IsValidIndex(IndexB))
1131 const FVector DeltaGrid(GridB - GridA);
1132 const FVector DeltaLocal = DeltaGrid *
VoxelSize;
1134 const float NormX = DeltaLocal.X * InvRadii.X;
1135 const float NormY = DeltaLocal.Y * InvRadii.Y;
1136 const float NormZ = DeltaLocal.Z * InvRadii.Z;
1138 return FMath::Sqrt(NormX * NormX + NormY * NormY + NormZ * NormZ);
1145#pragma region Collision
1147void AIVSmokeVoxelVolume::TryUpdateCollision(
bool bForce)
1149 if (bIsFastForwarding)
1154 if (CollisionComponent)
1156 CollisionComponent->TryUpdateCollision(
1171#pragma region DataAccess
1176 if (!bIsInitialized)
1184 if (IsHiddenEd() || IsHidden())
1190 if (bIsEditorPreviewing)
1194 if (GEditor && GEditor->IsPlayingSessionInEditor())
1213 const EIVSmokeVoxelVolumeState State = ServerState.
State;
1214 return State == EIVSmokeVoxelVolumeState::Expansion
1215 || State == EIVSmokeVoxelVolumeState::Sustain
1216 || State == EIVSmokeVoxelVolumeState::Dissipation;
1221 if (!IsValid(HoleGeneratorComponent))
1223 HoleGeneratorComponent = FindComponentByClass<UIVSmokeHoleGeneratorComponent>();
1226 return HoleGeneratorComponent;
1231 if (!IsValid(CollisionComponent))
1233 CollisionComponent = FindComponentByClass<UIVSmokeCollisionComponent>();
1236 return CollisionComponent;
1241 if (HoleGeneratorComponent)
1243 return HoleGeneratorComponent->GetHoleTextureRHI();
1250 UWorld* World = GetWorld();
1256 if (World->GetNetMode() == NM_Client)
1258 if (AGameStateBase* GameState = World->GetGameState())
1260 return GameState->GetServerWorldTimeSeconds();
1264 return World->GetTimeSeconds();
1281 bIsEditorPreviewing =
true;
1283 StartSimulationInternal();
1290 bIsEditorPreviewing =
false;
1292 ResetSimulationInternal();
1294 if (DebugMeshComponent)
1296 DebugMeshComponent->ClearInstances();
1301void AIVSmokeVoxelVolume::DrawDebugVisualization()
const
1309 DrawDebugVolumeBounds();
1310 DrawDebugVoxelWireframes();
1311 DrawDebugVoxelMeshes();
1312 DrawDebugStatusText();
1314 if (CollisionComponent)
1316 CollisionComponent->DrawDebugVisualization();
1321void AIVSmokeVoxelVolume::DrawDebugVolumeBounds()
const
1329 UWorld* World = GetWorld();
1338 TotalSize.X = GridResolution.X *
VoxelSize;
1339 TotalSize.Y = GridResolution.Y *
VoxelSize;
1340 TotalSize.Z = GridResolution.Z *
VoxelSize;
1342 FVector HalfExtent = TotalSize * 0.5f;
1344 FVector CenterPos = GetActorLocation();
1345 FQuat Rotation = GetActorRotation().Quaternion();
1352 FColor(100, 255, 100),
1361void AIVSmokeVoxelVolume::DrawDebugVoxelWireframes()
const
1364 if (!
DebugSettings.bShowVoxelWireframe || GeneratedVoxelIndices.IsEmpty())
1369 UWorld* World = GetWorld();
1375 FTransform ActorTrans = GetActorTransform();
1376 int32 VoxelNum = GeneratedVoxelIndices.Num();
1377 int32 MaxVisibleIndex = FMath::Clamp(VoxelNum *
DebugSettings.VisibleStepCountPercent / 100.0f, 0, VoxelNum);
1379 const FVector HalfVoxelSize(
VoxelSize * 0.5f);
1380 for (int32 i = 0; i < MaxVisibleIndex; ++i)
1382 int32 VoxelIndex = GeneratedVoxelIndices[i];
1392 float NormHeight =
static_cast<float>(GridPos.Z) /
static_cast<float>(GridResolution.Z);
1399 FVector WorldPos = ActorTrans.TransformPosition(LocalPos);
1405 ActorTrans.GetRotation(),
1407 false, -1.0f, 0, 1.5f
1413void AIVSmokeVoxelVolume::DrawDebugVoxelMeshes()
const
1416 if (!DebugMeshComponent)
1421 if (!
DebugSettings.bShowVoxelMesh || GeneratedVoxelIndices.IsEmpty())
1423 DebugMeshComponent->ClearInstances();
1436 DebugMeshComponent->ClearInstances();
1438 int32 VoxelNum = GeneratedVoxelIndices.Num();
1439 int32 MaxVisibleIndex = FMath::Clamp(
static_cast<int32
>(VoxelNum *
DebugSettings.VisibleStepCountPercent / 100.0f), 0, VoxelNum);
1441 TArray<FTransform> InstanceTransforms;
1442 InstanceTransforms.Reserve(MaxVisibleIndex);
1444 TArray<float> InstanceCustomData;
1445 InstanceCustomData.Reserve(MaxVisibleIndex);
1447 const FVector Scale3D(
VoxelSize / 100.0f * 0.98f);
1449 for (int32 i = 0; i < MaxVisibleIndex; ++i)
1451 int32 VoxelIndex = GeneratedVoxelIndices[i];
1463 float NormHeight =
static_cast<float>(GridPos.Z) /
static_cast<float>(GridResolution.Z);
1471 FTransform InstanceTrans;
1472 InstanceTrans.SetLocation(LocalPos);
1473 InstanceTrans.SetRotation(FQuat::Identity);
1474 InstanceTrans.SetScale3D(Scale3D);
1476 InstanceTransforms.Add(InstanceTrans);
1478 float DataValue = 0.0f;
1479 if (
DebugSettings.ViewMode == EIVSmokeDebugViewMode::Heatmap)
1481 DataValue = (VoxelNum > 1) ?
static_cast<float>(i) /
static_cast<float>(VoxelNum - 1) : 0.0f;
1483 InstanceCustomData.Add(DataValue);
1486 if (InstanceTransforms.Num() > 0)
1488 DebugMeshComponent->AddInstances(InstanceTransforms,
false,
false);
1490 int32 InstanceNum = InstanceTransforms.Num();
1491 for (int32 i = 0; i < InstanceNum; ++i)
1493 bool bIsLast = (i == InstanceNum - 1);
1494 DebugMeshComponent->SetCustomDataValue(i, 0, InstanceCustomData[i], bIsLast);
1500void AIVSmokeVoxelVolume::DrawDebugStatusText()
const
1508 UWorld* World = GetWorld();
1515 switch (ServerState.
State)
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;
1527 FString DebugMsg = FString::Printf(
1528 TEXT(
"State: %s\nSeed: %d\nTime: %.2fs\nVoxels: %d / %d (%.1f%%)\nHeap: %d\nChecksum: %u"),
1535 ExpansionHeap.Num(),
1536 CalculateSimulationChecksum()
1541 FVector TextPos = GetActorLocation();
1542 TextPos.Z += GridResolution.Z *
VoxelSize * 0.25f;
1544 DrawDebugString(World, TextPos, DebugMsg,
nullptr, FColor::White, 0.0f,
true, 1.2f);
1548uint32 AIVSmokeVoxelVolume::CalculateSimulationChecksum()
const
1550 uint32 Checksum = 0;
1552 Checksum = FCrc::MemCrc32(&ActiveVoxelNum,
sizeof(int32), Checksum);
1554 int32 StateInt = (int32)ServerState.
State;
1555 Checksum = FCrc::MemCrc32(&StateInt,
sizeof(int32), Checksum);
1557 if (VoxelBits.Num() > 0)
1559 Checksum = FCrc::MemCrc32(VoxelBits.GetData(), VoxelBits.Num() *
sizeof(uint64), Checksum);
TObjectPtr< UBillboardComponent > BillboardComponent
float GetSyncWorldTimeSeconds() const
TObjectPtr< UIVSmokeCollisionComponent > GetCollisionComponent()
bool ShouldRender() const
void StopPreviewSimulation()
FIVSmokeDebugSettings DebugSettings
float DissipationDuration
TObjectPtr< UMaterialInterface > DebugVoxelMaterial
FORCEINLINE bool IsVoxelActive(int32 Index) const
TEnumAsByte< ECollisionChannel > VoxelCollisionChannel
TObjectPtr< UStaticMesh > DebugVoxelMesh
void StartPreviewSimulation()
TObjectPtr< UIVSmokeHoleGeneratorComponent > GetHoleGeneratorComponent()
TObjectPtr< UCurveFloat > ExpansionCurve
FORCEINLINE int32 GetActiveVoxelNum() const
FTextureRHIRef GetHoleTexture() const
FORCEINLINE FIntVector GetGridResolution() const
FORCEINLINE FIntVector GetCenterOffset() const
bool bEnableSimulationCollision
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)
bool bRenderSmokeInPreview
float DissipationStartTime
EIVSmokeVoxelVolumeState State