IVSmoke 1.0
Loading...
Searching...
No Matches
IVSmokeVoxelVolume.h
1// Copyright (c) 2026, Team SDB. All rights reserved.
2
3#pragma once
4
5#include "CoreMinimal.h"
6#include "Curves/CurveFloat.h"
7#include "GameFramework/Actor.h"
8#include "IVSmokeGridLibrary.h"
9#include "RHI.h"
10#include "RHIResources.h"
11#include "TimerManager.h"
12#include "UObject/ObjectMacros.h"
13#include "IVSmokeVoxelVolume.generated.h"
14
15class UBoxComponent;
16class UBillboardComponent;
20
21/**
22 * Represents the current phase of the smoke simulation lifecycle.
23 */
24UENUM(BlueprintType)
25enum class EIVSmokeVoxelVolumeState : uint8
26{
27 /** Simulation is inactive. */
28 Idle,
29
30 /** Smoke is spreading via flood-fill. */
31 Expansion,
32
33 /** Smoke maintains its shape. */
34 Sustain,
35
36 /** Smoke is fading out and voxels are being removed. */
37 Dissipation,
38
39 /** Simulation has ended. */
40 Finished
41};
42
43/**
44 * Replicated state structure to synchronize simulation timing and random seeds across the network.
45 */
46USTRUCT(BlueprintType)
48{
49 GENERATED_BODY()
50
51 /** Current phase of the simulation state machine. */
52 UPROPERTY()
53 EIVSmokeVoxelVolumeState State = EIVSmokeVoxelVolumeState::Idle;
54
55 /** World time (synced) when the expansion phase began. */
56 UPROPERTY()
57 float ExpansionStartTime = 0.0f;
58
59 /** World time (synced) when the sustain phase began. */
60 UPROPERTY()
61 float SustainStartTime = 0.0f;
62
63 /** World time (synced) when the dissipation phase began. */
64 UPROPERTY()
65 float DissipationStartTime = 0.0f;
66
67 /** Seed for deterministic procedural generation across clients. */
68 UPROPERTY()
69 int32 RandomSeed = 0;
70
71 /**
72 * Increments every time the simulation resets.
73 * Used to force clients (including late-joiners) to reset their local state and resync with the server.
74 */
75 UPROPERTY()
76 uint8 Generation = 0;
77};
78
79/**
80 * Dirty level for GPU texture synchronization.
81 */
82UENUM(BlueprintType)
83enum class EIVSmokeDirtyLevel : uint8
84{
85 /** Texture is up-to-date. */
86 Clean,
87
88 /** Voxel data changed, texture upload required. */
89 Dirty
90};
91
92/**
93 * Visualization modes for debugging.
94 */
95UENUM(BlueprintType)
96enum class EIVSmokeDebugViewMode : uint8
97{
98 /** Single uniform color. */
99 SolidColor,
100
101 /** Gradient based on generation order. */
102 Heatmap
103};
104
105/**
106 * Editor and runtime debug settings.
107 */
108USTRUCT(BlueprintType)
110{
111 GENERATED_BODY()
112
113 UPROPERTY(EditAnywhere, Category = "IVSmoke | Debug")
114 bool bDebugEnabled = true;
115
116 /** If true, smoke is rendered during editor preview. Disable to see only debug visualization without smoke. */
117 UPROPERTY(EditAnywhere, Category = "IVSmoke | Debug", meta = (EditCondition = "bDebugEnabled"))
118 bool bRenderSmokeInPreview = true;
119
120 UPROPERTY(EditAnywhere, Category = "IVSmoke | Debug", meta = (EditCondition = "bDebugEnabled"))
121 EIVSmokeDebugViewMode ViewMode = EIVSmokeDebugViewMode::SolidColor;
122
123 UPROPERTY(EditAnywhere, Category = "IVSmoke | Debug", meta = (EditCondition = "bDebugEnabled"))
124 bool bShowVolumeBounds = true;
125
126 UPROPERTY(EditAnywhere, Category = "IVSmoke | Debug", meta = (EditCondition = "bDebugEnabled"))
127 bool bShowVoxelMesh = false;
128
129 UPROPERTY(EditAnywhere, Category = "IVSmoke | Debug", meta = (EditCondition = "bDebugEnabled"))
130 bool bShowVoxelWireframe = true;
131
132 UPROPERTY(EditAnywhere, Category = "IVSmoke | Debug", meta = (EditCondition = "bDebugEnabled"))
133 bool bShowStatusText = true;
134
135 UPROPERTY(EditAnywhere, Category = "IVSmoke | Debug", meta = (EditCondition = "bDebugEnabled", UIMin = 0.0, UIMax = 1.0, ClampMin = 0.0))
136 FColor DebugWireframeColor = FColor(20, 20, 20);
137
138 UPROPERTY(EditAnywhere, Category = "IVSmoke | Debug", meta = (EditCondition = "bDebugEnabled", UIMin=0.0, UIMax=1.0))
139 float SliceHeight = 1.0f;
140
141 UPROPERTY(EditAnywhere, Category = "IVSmoke | Debug", meta = (EditCondition = "bDebugEnabled", ClampMin=0, ClampMax=100))
142 int32 VisibleStepCountPercent = 100;
143};
144
145/**
146 * The core volumetric actor that simulates dynamic smoke expansion using a deterministic voxel-based flood-fill algorithm.
147 *
148 * ## Overview
149 * This actor generates a 3D grid of voxels that expand outward from the center, navigating around obstacles
150 * defined by the collision settings. The simulation is deterministic, ensuring the same shape and timing
151 * across both Server and Clients without replicating individual voxel data.
152 *
153 * ## Simulation Lifecycle
154 * The simulation state machine progresses based on the sum of duration and fade settings:
155 * 1. Idle: Initial state.
156 * 2. Expansion: Spawns voxels. Ends after `ExpansionDuration + FadeInDuration`.
157 * 3. Sustain: Maintains the shape. Ends after `SustainDuration`.
158 * 4. Dissipation: Removes voxels. Ends after `DissipationDuration + FadeOutDuration`.
159 * 5. Finished: Simulation complete.
160 *
161 * ## Network & Execution
162 * The simulation logic executes deterministically on both the Server and Client.
163 * - The Server manages the authoritative state (State, Seed, StartTime) and replicates it to Clients.
164 * - Clients execute the exact same flood-fill algorithm locally based on the replicated Seed and Time.
165 */
166UCLASS()
167class IVSMOKE_API AIVSmokeVoxelVolume : public AActor
168{
169 GENERATED_BODY()
170
171 //~==============================================================================
172 // Actor Lifecycle
173#pragma region Lifecycle
174public:
176
177 virtual void Tick(float DeltaTime) override;
178 virtual bool ShouldTickIfViewportsOnly() const override;
179
180 virtual void OnConstruction(const FTransform& Transform) override;
181
182#if WITH_EDITOR
183 virtual void PostEditChangeProperty(struct FPropertyChangedEvent& PropertyChangedEvent) override;
184 virtual void PostEditMove(bool bFinished) override;
185 virtual bool CanEditChange(const FProperty* InProperty) const override;
186 virtual void EditorApplyRotation(const FRotator& DeltaRotation, bool bAltDown, bool bShiftDown, bool bCtrlDown) override;
187 virtual void EditorApplyScale(const FVector& DeltaScale, const FVector* PivotLocation, bool bAltDown, bool bShiftDown, bool bCtrlDown) override;
188 virtual void EditorApplyTranslation(const FVector& DeltaTranslation, bool bAltDown, bool bShiftDown, bool bCtrlDown) override;
189 virtual bool IsSelectable() const override;
190#endif
191
192protected:
193 virtual void BeginPlay() override;
194 virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
195 virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
196#pragma endregion
197
198 //~==============================================================================
199 // Actor Components
200#pragma region Components
201public:
202 /**
203 * Returns the component responsible for generating holes (negative space) in the smoke.
204 * Caches the result to avoid repeated lookups. Returns nullptr if the component is missing.
205 */
206 TObjectPtr<UIVSmokeHoleGeneratorComponent> GetHoleGeneratorComponent();
207
208 /**
209 * Returns the component responsible for handling physical interactions and collision queries.
210 * Caches the result to avoid repeated lookups. Returns nullptr if the component is missing.
211 */
212 TObjectPtr<UIVSmokeCollisionComponent> GetCollisionComponent();
213
214 /** Editor visualization sprite. Used as the RootComponent. */
215 UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "IVSmoke")
216 TObjectPtr<UBillboardComponent> BillboardComponent;
217
218private:
219 /**
220 * Component that handles dynamic hole generation.
221 * Reacts to physical interactions (e.g., projectiles, explosions) to carve out holes in the smoke,
222 */
223 UPROPERTY(EditAnywhere, Category = "IVSmoke")
224 TObjectPtr<UIVSmokeHoleGeneratorComponent> HoleGeneratorComponent;
225
226 /**
227 * Component that manages dynamic collision volumes.
228 * Generates blocking geometry based on active voxels, mainly designed to obstruct AI vision
229 * and prevent them from seeing through the thick smoke.
230 */
231 UPROPERTY(EditAnywhere, Category = "IVSmoke")
232 TObjectPtr<UIVSmokeCollisionComponent> CollisionComponent;
233
234#if WITH_EDITORONLY_DATA
235 /**
236 * InstancedStaticMeshComponent used exclusively for editor-time debug visualization.
237 * Renders individual voxels as meshes when `bShowVoxelMesh` is enabled.
238 */
239 UPROPERTY()
240 TObjectPtr<UInstancedStaticMeshComponent> DebugMeshComponent;
241#endif
242#pragma endregion
243
244 //~==============================================================================
245 // Actor Configuration
246#pragma region Configuation
247public:
248 /**
249 * Half-size of the voxel grid in index units.
250 * The actual grid resolution will be `(Extent * 2) - 1` per axis.
251 * @note Increasing this value exponentially increases memory usage. Keep it as low as possible.
252 */
253 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Config", meta = (ClampMin = "1", ClampMax = "16"))
254 FIntVector VolumeExtent = FIntVector(16, 16, 16);
255
256 /**
257 * Defines the relative aspect ratio of the smoke's expansion shape per axis.
258 * These values act as ratios, not absolute units.
259 * - Example: (1.0, 1.0, 1.0) creates a spherical shape.
260 * - Example: (2.0, 1.0, 1.0) creates an ellipsoid that stretches twice as far along the X-axis.
261 */
262 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Config", meta = (ClampMin = "0.1", UIMin = "0.1", UIMax = "5.0"))
263 FVector Radii = FVector(1.0f, 1.0f, 1.0f);
264
265 /**
266 * World space size of a single voxel in centimeters.
267 * @note Larger values cover more area with the same performance cost but reduce visual detail.
268 * Smaller values require more voxels (higher `VolumeExtent` or `MaxVoxelNum`) to cover the same area.
269 */
270 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Config", meta = (ClampMin = "1.0", UIMin = "10.0", UIMax = "100.0"))
271 float VoxelSize = 50.0f;
272
273 /**
274 * The hard limit on the number of active voxels.
275 * Simulation will stop spawning new voxels once this limit is reached.
276 * Use this to guarantee a fixed performance budget for this actor.
277 */
278 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Config", meta = (ClampMin = "1", UIMin = "100", UIMax = "10000"))
279 int32 MaxVoxelNum = 1000;
280
281 /** If true, the simulation starts automatically on BeginPlay. */
282 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Config")
283 bool bAutoStart = false;
284
285 /** If true, the actor is automatically destroyed when the simulation reaches the `Finished` state. */
286 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Config")
287 bool bDestroyOnFinish = false;
288
289 /** If true, the smoke stays in the `Sustain` phase indefinitely. */
290 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Config")
291 bool bIsInfinite = false;
292
293 /**
294 * Optional Data Asset to override visual properties such as Smoke Color, Absorption, and Density.
295 * If set, the visual settings in this preset take precedence over the actor's local settings.
296 * @see UIVSmokeSmokePreset
297 */
298 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Config")
299 TObjectPtr<UIVSmokeSmokePreset> SmokePresetOverride;
300
301#pragma endregion
302
303 //~==============================================================================
304 // Flood Fill Simulation
305#pragma region Simulation
306public:
307 /**
308 * Allocates memory for the voxel grid based on `VolumeExtent`.
309 * Automatically called on BeginPlay. Can be called manually to resize the grid at runtime,
310 */
311 UFUNCTION(BlueprintCallable, Category = "IVSmoke")
312 void Initialize();
313
314 /**
315 * Begins the simulation. (Server Only)
316 * Sets the state to `Expansion` and synchronizes the start time and random seed to clients.
317 * Has no effect if the simulation is already running.
318 */
319 UFUNCTION(Server, Reliable, BlueprintCallable, Category = "IVSmoke")
320 void StartSimulation();
321
322 /**
323 * Stops the simulation and triggers the dissipation phase. (Server Only)
324 *
325 * @param bImmediate If true, skips the dissipation phase and instantly transitions to `Finished`, clearing all voxels.
326 */
327 UFUNCTION(Server, Reliable, BlueprintCallable, Category = "IVSmoke")
328 void StopSimulation(bool bImmediate = false);
329
330 /**
331 * Resets the simulation state to `Idle` and clears all voxel data. (Server Only)
332 * Increments the `Generation` counter to force all clients to reset and resync.
333 */
334 UFUNCTION(Server, Reliable, BlueprintCallable, Category = "IVSmoke")
335 void ResetSimulation();
336
337 /**
338 * The duration (in seconds) of the active expansion phase where voxels are spawned.
339 * The actual Expansion state lasts for `ExpansionDuration + FadeInDuration`.
340 */
341 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Simulation", meta = (ClampMin = "0.0"))
342 float ExpansionDuration = 3.0f;
343
344 /**
345 * The duration (in seconds) the smoke maintains its shape after expansion.
346 * Ignored if `bIsInfinite` is true.
347 */
348 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Simulation", meta = (ClampMin = "0.0"))
349 float SustainDuration = 5.0f;
350
351 /**
352 * The duration (in seconds) of the voxel removal phase.
353 * The actual Dissipation state lasts for `DissipationDuration + FadeOutDuration`.
354 */
355 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Simulation", meta = (ClampMin = "0.0"))
356 float DissipationDuration = 2.0f;
357
358 /**
359 * Additional time added to the Expansion phase to allow for opacity fade-in.
360 */
361 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Simulation", meta = (ClampMin = "0.0"))
362 float FadeInDuration = 2.0f;
363
364 /**
365 * Additional time added to the Dissipation phase to allow for opacity fade-out.
366 */
367 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Simulation", meta = (ClampMin = "0.0"))
368 float FadeOutDuration = 2.0f;
369
370 /**
371 * Randomness added to the flood-fill pathfinding cost.
372 * Higher values create more irregular, jagged shapes instead of a perfect sphere.
373 */
374 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Simulation", meta = (ClampMin = "0.0", UIMin = "0.0", UIMax = "5000.0"))
375 float ExpansionNoise = 100.0f;
376
377 /**
378 * Randomness added to the voxel removal order.
379 * Higher values cause the smoke to break apart more randomly during dissipation.
380 */
381 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Simulation", meta = (ClampMin = "0.0", UIMin = "0.0", UIMax = "5000.0"))
382 float DissipationNoise = 100.0f;
383
384 /**
385 * Defines the normalized rate of voxel spawning over `ExpansionDuration`.
386 * - X-axis (Time): 0.0 to 1.0 (Normalized Duration)
387 * - Y-axis (Value): 0.0 to 1.0 (Fraction of `MaxVoxelNum` to spawn)
388 * @note The curve should be monotonically increasing.
389 */
390 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Simulation")
391 TObjectPtr<UCurveFloat> ExpansionCurve;
392
393 /**
394 * Defines the normalized rate of voxel survival over `DissipationDuration`.
395 * - X-axis (Time): 0.0 to 1.0 (Normalized Duration)
396 * - Y-axis (Value): 1.0 to 0.0 (Fraction of voxels remaining)
397 * @note The curve should be monotonically decreasing (start at 1.0, end at 0.0).
398 */
399 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Simulation")
400 TObjectPtr<UCurveFloat> DissipationCurve;
401
402 /**
403 * If true, voxels perform collision checks against the world before spawning.
404 * Disable this to allow smoke to pass through walls, significantly reducing CPU cost.
405 */
406 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Simulation")
407 bool bEnableSimulationCollision = true;
408
409 /** The collision channel used for obstacle detection during expansion. */
410 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "IVSmoke | Simulation", meta = (EditCondition = "bEnableSimulationCollision"))
411 TEnumAsByte<ECollisionChannel> VoxelCollisionChannel = ECC_WorldStatic;
412
413private:
414 /** Internal node structure for the Dijkstra-based flood fill algorithm. */
415 struct FIVSmokeVoxelNode
416 {
417 int32 Index;
418 int32 ParentIndex;
419 float Cost;
420 bool operator<(const FIVSmokeVoxelNode& Other) const
421 {
422 if (FMath::IsNearlyEqual(Cost, Other.Cost))
423 {
424 return Index < Other.Index;
425 }
426 return Cost < Other.Cost;
427 }
428 };
429
430 /**
431 * Helper to sample a curve or return linear alpha if no curve is provided.
432 *
433 * @param ElapsedTime Current time elapsed in the phase.
434 * @param Duration Total duration of the phase.
435 * @param Curve Optional curve to sample. If null, returns linear interpolation (ElapsedTime / Duration).
436 * @return Clamped float value between 0.0 and 1.0.
437 */
438 FORCEINLINE static float GetCurveValue(float ElapsedTime, float Duration, const UCurveFloat* Curve)
439 {
440 if (Duration <= KINDA_SMALL_NUMBER)
441 {
442 return 1.0f;
443 }
444
445 float Alpha = FMath::Clamp(ElapsedTime / Duration, 0.0f, 1.0f);
446
447 if (Curve)
448 {
449 return FMath::Clamp(Curve->GetFloatValue(Alpha), 0.0f, 1.0f);
450 }
451
452 return Alpha;
453 }
454
455 /** Handles network replication of the simulation state. */
456 UFUNCTION()
457 void OnRep_ServerState();
458
459 /**
460 * Main state machine handler.
461 * Transitions the local simulation logic to the new state (e.g., resets heaps, clears data).
462 *
463 * @param NewState The state to transition to.
464 */
465 void HandleStateTransition(EIVSmokeVoxelVolumeState NewState);
466
467 /** Resets all internal simulation arrays and counters to their initial state. */
468 void ClearSimulationData();
469
470 /**
471 * Checks if the line of sight between two voxel centers is blocked.
472 *
473 * @param World Pointer to the world context.
474 * @param BeginPos Start position of the trace.
475 * @param EndPos End position of the trace.
476 * @return True if a blocking hit occurs between the positions.
477 */
478 bool IsConnectionBlocked(const UWorld* World, const FVector& BeginPos, const FVector& EndPos) const;
479
480 /**
481 * Core logic for starting the simulation.
482 * Separated from the RPC to allow execution in both Editor-Preview and Networked-Server contexts.
483 */
484 void StartSimulationInternal();
485
486 /**
487 * Core logic for stopping or dissipating the smoke.
488 *
489 * @param bImmediate If true, skips the dissipation phase and instantly transitions to `Finished`, clearing all voxels.
490 */
491 void StopSimulationInternal(bool bImmediate = false);
492
493 /**
494 * Core logic for resetting the entire simulation state.
495 * Clears buffers, resets generation, and returns the actor to an Idle state.
496 */
497 void ResetSimulationInternal();
498
499 /**
500 * Simulates frames rapidly to catch up with the server's current state.
501 * Called on clients when they detect a `Generation` mismatch (late join or reset).
502 */
503 void FastForwardSimulation();
504
505 /** Per-frame update logic for the Expansion phase. */
506 void UpdateExpansion();
507
508 /** Per-frame update logic for the Sustain phase. */
509 void UpdateSustain();
510
511 /** Per-frame update logic for the Dissipation phase. */
512 void UpdateDissipation();
513
514 /**
515 * Pops nodes from the ExpansionHeap and spawns new voxels.
516 *
517 * @param SpawnNum Number of voxels to spawn this frame.
518 * @param StartSimTime Simulation time at the beginning of the frame.
519 * @param EndSimTime Simulation time at the end of the frame.
520 */
521 void ProcessExpansion(int32 SpawnNum, float StartSimTime, float EndSimTime);
522
523 /**
524 * Pops nodes from the DissipationHeap and removes existing voxels.
525 *
526 * @param RemoveNum Number of voxels to remove this frame.
527 * @param StartSimTime Simulation time at the beginning of the frame.
528 * @param EndSimTime Simulation time at the end of the frame.
529 */
530 void ProcessDissipation(int32 RemoveNum, float StartSimTime, float EndSimTime);
531
532 /**
533 * Sets the birth time for a voxel and marks it as active.
534 *
535 * @param Index Index of the voxel in the grid array.
536 * @param BirthTime The simulation time when this voxel was created.
537 */
538 void SetVoxelBirthTime(int32 Index, float BirthTime);
539
540 /**
541 * Sets the death time for a voxel and marks it as inactive.
542 *
543 * @param Index Index of the voxel in the grid array.
544 * @param DeathTime The simulation time when this voxel was removed.
545 */
546 void SetVoxelDeathTime(int32 Index, float DeathTime);
547
548 /**
549 * Calculates the weighted Euclidean distance between two voxels based on Radii.
550 * Used for Theta* cost estimation to approximate the actual distance.
551 *
552 * @param IndexA Index of the first voxel.
553 * @param IndexB Index of the second voxel.
554 * @param InvRadii Inverse of the radii vector for weighting the distance per axis.
555 * @return The weighted distance between the two voxels.
556 */
557 float CalculateWeightedDistance(int32 IndexA, int32 IndexB, const FVector& InvRadii) const;
558
559 /** Replicated state synchronized from the server. */
560 UPROPERTY(ReplicatedUsing = OnRep_ServerState)
561 FIVSmokeServerState ServerState;
562
563 /** Local copy of the state machine to detect transitions. */
564 EIVSmokeVoxelVolumeState LocalState = EIVSmokeVoxelVolumeState::Idle;
565
566 /** Tracks the generation number locally to detect server resets. */
567 uint8 LocalGeneration = 0;
568
569 /** RNG stream for deterministic procedural generation. */
570 FRandomStream RandomStream;
571
572 /** Current local simulation time relative to the phase start time. */
573 float SimTime = 0.0f;
574
575 /** True if memory has been allocated via Initialize(). */
576 bool bIsInitialized = false;
577
578 /** True if currently running the fast-forward catch-up logic. */
579 bool bIsFastForwarding = false;
580
581 /** World-space bounding box minimum of all active voxels. */
582 FVector VoxelWorldAABBMin = FVector(FLT_MAX, FLT_MAX, FLT_MAX);
583
584 /** World-space bounding box maximum of all active voxels. */
585 FVector VoxelWorldAABBMax = FVector(-FLT_MAX, -FLT_MAX, -FLT_MAX);
586
587 /** Timestamp when each voxel was spawned. */
588 TArray<float> VoxelBirthTimes;
589
590 /** Timestamp when each voxel was removed. */
591 TArray<float> VoxelDeathTimes;
592
593 /** Pathfinding cost for each voxel index (Dijkstra). */
594 TArray<float> VoxelCosts;
595
596 /**
597 * Bitmask buffer representing active voxels, packed for memory efficiency.
598 *
599 * ## Data Layout
600 * Each `uint64` element represents a single row of voxels along the X-axis at a specific (Y, Z) coordinate.
601 * - The X-coordinate maps directly to the bit index (0-63).
602 * - Array Index = `Z * GridResolution.Y + Y`
603 *
604 * @warning Hard Constraint: Since the X-axis is packed into a 64-bit integer, `GridResolution.X` cannot exceed 64.
605 * Consequently, `VolumeExtent.X` is effectively limited to roughly 32 (since Resolution = Extent * 2 - 1).
606 */
607 TArray<uint64> VoxelBits;
608
609 /** Priority queue for expansion (lowest cost first). */
610 TArray<FIVSmokeVoxelNode> ExpansionHeap;
611
612 /** Priority queue for dissipation (lowest cost + noise first). */
613 TArray<FIVSmokeVoxelNode> DissipationHeap;
614
615 /** List of indices of all currently active voxels. */
616 TArray<int32> GeneratedVoxelIndices;
617
618 TBitArray<> VoxelPenetrationFlags;
619
620#pragma endregion
621
622 //~==============================================================================
623 // Collision
624#pragma region Collision
625 /**
626 * Updates the collision geometry to match the current state of the voxel grid.
627 * Delegates the actual mesh/body generation to the `CollisionComponent`.
628 *
629 * @param bForce If true, forces a geometry rebuild even if the voxel data hasn't changed.
630 * Used during initialization or when applying a new preset.
631 */
632 void TryUpdateCollision(bool bForce = false);
633#pragma endregion
634
635 //~==============================================================================
636 // Data Access
637#pragma region DataAccess
638public:
639 /** Returns the current phase of the simulation state machine. */
640 FORCEINLINE EIVSmokeVoxelVolumeState GetCurrentState() const { return ServerState.State; }
641
642 /**
643 * Returns true if this volume should be rendered.
644 * Used by SceneViewExtension to filter active volumes without explicit registration.
645 */
646 bool ShouldRender() const;
647
648 /** Returns the raw array of timestamps indicating when each voxel was created (Server Time). */
649 FORCEINLINE const TArray<float>& GetVoxelBirthTimes() const { return VoxelBirthTimes; }
650
651 /** Returns the raw array of timestamps indicating when each voxel was removed (Server Time). */
652 FORCEINLINE const TArray<float>& GetVoxelDeathTimes() const { return VoxelDeathTimes; }
653
654 /** Returns the grid resolution (dimensions of the voxel grid). */
655 FORCEINLINE FIntVector GetGridResolution() const
656 {
657 FIntVector GridResolution;
658 GridResolution.X = FMath::Max(1, (VolumeExtent.X * 2) - 1);
659 GridResolution.Y = FMath::Max(1, (VolumeExtent.Y * 2) - 1);
660 GridResolution.Z = FMath::Max(1, (VolumeExtent.Z * 2) - 1);
661 return GridResolution;
662 }
663
664 /** Returns the center offset for grid-to-local coordinate conversion. */
665 FORCEINLINE FIntVector GetCenterOffset() const { return VolumeExtent - FIntVector(1, 1, 1); }
666
667 /** Returns the world-space size of each voxel. */
668 FORCEINLINE float GetVoxelSize() const { return VoxelSize; }
669
670 /** Returns the current dirty level for GPU buffer synchronization. */
671 FORCEINLINE EIVSmokeDirtyLevel GetDirtyLevel() const { return DirtyLevel; }
672
673 /** Returns true if voxel data has been modified since last GPU upload. */
674 FORCEINLINE bool IsVoxelDataDirty() const { return DirtyLevel != EIVSmokeDirtyLevel::Clean; }
675
676 /**
677 * Marks the voxel data as clean after a successful GPU upload.
678 * @note Should only be called by the `IVSmokeRenderer`.
679 */
680 FORCEINLINE void ClearVoxelDataDirty() { DirtyLevel = EIVSmokeDirtyLevel::Clean; }
681
682 /** Returns the current buffer size (for detecting resize). */
683 FORCEINLINE int32 GetVoxelBufferSize() const { return VoxelBirthTimes.Num(); }
684
685 /** Returns the number of active (non-zero density) voxels. */
686 FORCEINLINE int32 GetActiveVoxelNum() const { return ActiveVoxelNum; }
687
688 /** Returns the smoke preset override for this volume, or nullptr to use default. */
689 FORCEINLINE const UIVSmokeSmokePreset* GetSmokePresetOverride() const { return SmokePresetOverride; }
690
691 /** Returns the AABBMin of voxels. */
692 FORCEINLINE FVector GetVoxelWorldAABBMin() const { return VoxelWorldAABBMin - VoxelSize; }
693
694 /** Returns the AABBMax of voxels. */
695 FORCEINLINE FVector GetVoxelWorldAABBMax() const { return VoxelWorldAABBMax + VoxelSize; }
696
697 /**
698 * Checks if a voxel at the given linear index is currently active.
699 *
700 * @param Index Linear index of the voxel.
701 * @return True if the voxel is active (bit set).
702 */
703 FORCEINLINE bool IsVoxelActive(int32 Index) const
704 {
705 FIntVector GridPos = UIVSmokeGridLibrary::IndexToGrid(Index, GetGridResolution());
706 return IsVoxelActive(GridPos);
707 }
708
709 /**
710 * Checks if a voxel at the given grid coordinate is currently active.
711 *
712 * @param GridPos 3D grid coordinate of the voxel.
713 * @return True if the voxel is active.
714 */
715 FORCEINLINE bool IsVoxelActive(FIntVector GridPos) const
716 {
717 return UIVSmokeGridLibrary::IsVoxelBitSet(VoxelBits, GridPos, GetGridResolution());
718 }
719
720 /** Returns the RHI texture resource from the HoleGeneratorComponent, if available. */
721 FTextureRHIRef GetHoleTexture() const;
722
723 /**
724 * Returns the synchronized world time in seconds.
725 * Handles network time offsets to ensure clients see the simulation at the same progress as the server.
726 */
727 float GetSyncWorldTimeSeconds() const;
728
729private:
730 /** Internal counter for the number of active voxels. */
731 int32 ActiveVoxelNum = 0;
732
733 /** Tracks changes to voxel data for render thread synchronization. */
734 EIVSmokeDirtyLevel DirtyLevel = EIVSmokeDirtyLevel::Clean;
735#pragma endregion
736
737 //~==============================================================================
738 // Debug
739#pragma region Debug
740public:
741 /**
742 * Runs a preview of the simulation directly in the Editor viewport without starting a Play In Editor (PIE) session.
743 * Use this to quickly iterate on parameters like `ExpansionDuration`, `Noise`, and `Curves`.
744 * @note This resets the current simulation state.
745 */
746 UFUNCTION(CallInEditor, Category = "IVSmoke | Debug")
747 void StartPreviewSimulation();
748
749 /**
750 * Stops the current editor preview simulation and clears all generated voxel data.
751 * Returns the actor to an Idle state.
752 */
753 UFUNCTION(CallInEditor, Category = "IVSmoke | Debug")
754 void StopPreviewSimulation();
755
756 /** Configuration settings for visual debugging tools. */
757 UPROPERTY(EditAnywhere, Category = "IVSmoke | Debug")
759
760 /** Optional static mesh to use for voxel visualization when `bShowVoxelMesh` is enabled in settings. */
761 UPROPERTY(EditDefaultsOnly, Category = "IVSmoke | Debug", AdvancedDisplay)
762 TObjectPtr<UStaticMesh> DebugVoxelMesh;
763
764 /** Optional material to apply to the debug voxel mesh. */
765 UPROPERTY(EditDefaultsOnly, Category = "IVSmoke | Debug", AdvancedDisplay)
766 TObjectPtr<UMaterialInterface> DebugVoxelMaterial;
767
768private:
769 /** Main entry point for drawing all enabled debug visualizations per frame. */
770 void DrawDebugVisualization() const;
771
772 /** Draws voxel volume bounds. */
773 void DrawDebugVolumeBounds() const;
774
775 /** Draws lightweight wireframe cubes for active voxels. */
776 void DrawDebugVoxelWireframes() const;
777
778 /** Draws instanced static meshes for active voxels. Heavier performance cost. */
779 void DrawDebugVoxelMeshes() const;
780
781 /** Displays world-space text showing the current State, Voxel Count, and Simulation Time. */
782 void DrawDebugStatusText() const;
783
784 /** Calculates a CRC32 checksum of the current voxel state to verify deterministic sync between Server and Client. */
785 uint32 CalculateSimulationChecksum() const;
786
787 /** Internal flag to track if the actor is currently running an editor-only preview simulation. */
788 bool bIsEditorPreviewing = false;
789#pragma endregion
790};
FORCEINLINE const UIVSmokeSmokePreset * GetSmokePresetOverride() const
FORCEINLINE int32 GetVoxelBufferSize() const
FORCEINLINE FVector GetVoxelWorldAABBMax() const
FORCEINLINE bool IsVoxelActive(int32 Index) const
FORCEINLINE float GetVoxelSize() const
FORCEINLINE bool IsVoxelDataDirty() const
FORCEINLINE int32 GetActiveVoxelNum() const
FORCEINLINE bool IsVoxelActive(FIntVector GridPos) const
FORCEINLINE const TArray< float > & GetVoxelBirthTimes() const
FORCEINLINE FVector GetVoxelWorldAABBMin() const
FORCEINLINE EIVSmokeVoxelVolumeState GetCurrentState() const
FORCEINLINE FIntVector GetGridResolution() const
FORCEINLINE EIVSmokeDirtyLevel GetDirtyLevel() const
FORCEINLINE FIntVector GetCenterOffset() const
FORCEINLINE const TArray< float > & GetVoxelDeathTimes() const
FORCEINLINE void ClearVoxelDataDirty()
static FORCEINLINE FIntVector IndexToGrid(int32 Index, const FIntVector &Resolution)
static FORCEINLINE bool IsVoxelBitSet(const TArray< uint64 > &VoxelBitArray, const FIntVector &GridPos, const FIntVector &Resolution)
Component that generates hole texture for volumetric smoke. Provides public API for penetration and e...