IVSmoke 1.0
Loading...
Searching...
No Matches
IVSmokeRenderer.cpp
1// Copyright (c) 2026, Team SDB. All rights reserved.
2
3#include "IVSmokeRenderer.h"
4#include "IVSmoke.h"
5#include "IVSmokePostProcessPass.h"
6#include "IVSmokeSettings.h"
7#include "IVSmokeShaders.h"
8#include "IVSmokeSmokePreset.h"
9#include "IVSmokeVoxelVolume.h"
10#include "IVSmokeCSMRenderer.h"
11#include "IVSmokeVSMProcessor.h"
12#include "IVSmokeRayMarchPipeline.h"
13#include "Engine/TextureRenderTargetVolume.h"
14#include "PostProcess/PostProcessMaterialInputs.h"
15#include "SceneRenderTargetParameters.h"
16#include "IVSmokeHoleGeneratorComponent.h"
17#include "RenderGraphUtils.h"
18#include "Engine/DirectionalLight.h"
19#include "Components/DirectionalLightComponent.h"
20#include "EngineUtils.h"
21#include "Kismet/GameplayStatics.h"
22#include "Components/SceneComponent.h"
23#include "RHI.h"
24#include "Engine/TextureRenderTarget2D.h"
25#include "Materials/MaterialRenderProxy.h"
26#include "MaterialShader.h"
27#include "MaterialShaderType.h"
28#include "IVSmokeVisualMaterialPreset.h"
29#include "PixelShaderUtils.h"
30#include "FXRenderingUtils.h" // For UE::FXRenderingUtils::GetRawViewRectUnsafe
31
32#if !UE_SERVER
33FIVSmokeRenderer& FIVSmokeRenderer::Get()
34{
35 static FIVSmokeRenderer Instance;
36 return Instance;
37}
38
39FIVSmokeRenderer::FIVSmokeRenderer() = default;
40
41FIVSmokeRenderer::~FIVSmokeRenderer()
42{
43 // Destructor defined here where FIVSmokeCSMRenderer and FIVSmokeVSMProcessor are complete types
44 Shutdown();
45}
46
47//~==============================================================================
48// Lifecycle
49
51{
52 if (IsRunningCommandlet())
53 {
54 return;
55 }
56
57 if (NoiseVolume)
58 {
59 return; // Already initialized
60 }
61
62 CreateNoiseVolume();
63
64 UE_LOG(LogIVSmoke, Log, TEXT("[FIVSmokeRenderer::Initialize] Renderer initialized. Global settings loaded from UIVSmokeSettings."));
65}
66
68{
69 // Flush GPU commands before cleaning up any resources
70 FlushRenderingCommands();
71
72 if (IsValid(NoiseVolume))
73 {
74 NoiseVolume->RemoveFromRoot();
75 NoiseVolume = nullptr;
76 }
77
78 // Clear per-view data (RDG textures are only valid within frame, so just clear the map)
79 ViewDataMap.Empty();
80
81 // Cleanup all World data
82 {
83 FScopeLock Lock(&WorldDataMutex);
84
85 for (auto& Pair : WorldDataMap)
86 {
87 if (Pair.Value.IsValid())
88 {
89 Pair.Value->CleanupCSM();
90 }
91 }
92 WorldDataMap.Empty();
93
94 // Note: Delegate handles are now invalid (Worlds are destroyed), no need to remove them
95 WorldEndPlayHandles.Empty();
96 }
97}
98
99//~==============================================================================
100// FPerWorldData Implementation
101
102FPerWorldData::FPerWorldData() = default;
103
104FPerWorldData::~FPerWorldData()
105{
106 // TUniquePtr destructor requires complete type - defined here where types are complete
107 CleanupCSM();
108}
109
111{
112 if (CSMRenderer)
113 {
114 CSMRenderer->Shutdown();
115 CSMRenderer.Reset();
116 }
117 VSMProcessor.Reset();
120}
121
122//~==============================================================================
123// Per-World Data Management
124
125TSharedPtr<FPerWorldData> FIVSmokeRenderer::GetOrCreateWorldData(UWorld* World)
126{
127 if (!World)
128 {
129 return nullptr;
130 }
131
132 FScopeLock Lock(&WorldDataMutex);
133
134 TObjectKey<UWorld> WorldKey(World);
135
136 // Return existing data if found
137 if (TSharedPtr<FPerWorldData>* Found = WorldDataMap.Find(WorldKey))
138 {
139 return *Found;
140 }
141
142 // Create new per-world data
143 TSharedPtr<FPerWorldData> NewData = MakeShared<FPerWorldData>();
144 WorldDataMap.Add(WorldKey, NewData);
145
146 // Register World destruction delegate for automatic cleanup
147 // Use FWorldDelegates::OnWorldCleanup (called when world is being cleaned up)
148 FDelegateHandle Handle = FWorldDelegates::OnWorldCleanup.AddLambda(
149 [this, WorldKey](UWorld* CleaningWorld, bool bSessionEnded, bool bCleanupResources)
150 {
151 if (TObjectKey<UWorld>(CleaningWorld) == WorldKey)
152 {
153 CleanupWorldData(CleaningWorld);
154 }
155 }
156 );
157 WorldEndPlayHandles.Add(WorldKey, Handle);
158
159 UE_LOG(LogIVSmoke, Log, TEXT("[FIVSmokeRenderer::GetOrCreateWorldData] New world registered: %s"), *World->GetName());
160
161 return NewData;
162}
163
164TSharedPtr<FPerWorldData> FIVSmokeRenderer::GetWorldData(UWorld* World)
165{
166 if (!World)
167 {
168 return nullptr;
169 }
170
171 FScopeLock Lock(&WorldDataMutex);
172
173 TObjectKey<UWorld> WorldKey(World);
174 TSharedPtr<FPerWorldData>* Found = WorldDataMap.Find(WorldKey);
175
176 return Found ? *Found : nullptr;
177}
178
180{
181 if (!World)
182 {
183 return;
184 }
185
186 UE_LOG(LogIVSmoke, Log, TEXT("[FIVSmokeRenderer::CleanupWorldData] Cleaning up world: %s"), *World->GetName());
187
188 // CRITICAL: Wait for GPU to finish using any resources from this World
189 // This prevents DXGI_ERROR_DEVICE_HUNG when textures are destroyed while in use
190 FlushRenderingCommands();
191
192 FScopeLock Lock(&WorldDataMutex);
193
194 TObjectKey<UWorld> WorldKey(World);
195
196 // Remove delegate handle
197 if (FDelegateHandle* Handle = WorldEndPlayHandles.Find(WorldKey))
198 {
199 FWorldDelegates::OnWorldCleanup.Remove(*Handle);
200 WorldEndPlayHandles.Remove(WorldKey);
201 }
202
203 // Cleanup CSM and remove world data
204 if (TSharedPtr<FPerWorldData>* Found = WorldDataMap.Find(WorldKey))
205 {
206 if (Found->IsValid())
207 {
208 (*Found)->CleanupCSM();
209 }
210 }
211 WorldDataMap.Remove(WorldKey);
212}
213
215{
216 TSharedPtr<FPerWorldData> WorldData = GetWorldData(World);
217 return WorldData.IsValid() ? WorldData->bServerTimeSynced : false;
218}
219
220void FIVSmokeRenderer::SetServerTimeOffset(UWorld* World, float InServerTimeOffset)
221{
222 TSharedPtr<FPerWorldData> WorldData = GetOrCreateWorldData(World);
223 if (WorldData.IsValid())
224 {
225 WorldData->bServerTimeSynced = true;
226 WorldData->ServerTimeOffset = InServerTimeOffset;
227 }
228}
229
231{
232 if (!World)
233 {
234 return;
235 }
236
237 FScopeLock Lock(&WorldDataMutex);
238
239 TObjectKey<UWorld> WorldKey(World);
240 TSharedPtr<FPerWorldData>* Found = WorldDataMap.Find(WorldKey);
241
242 if (Found && Found->IsValid())
243 {
244 (*Found)->CachedRenderData = MoveTemp(InRenderData);
245 }
246}
247FIntVector FIVSmokeRenderer::GetAtlasTexCount(const FIntVector& TexSize, const int32 TexCount, const int32 TexturePackInterval, const int32 TexturePackMaxSize) const
248{
249 int QuotientX = TexturePackMaxSize / (TexSize.X + TexturePackInterval);
250 int QuotientY = TexturePackMaxSize / (TexSize.Y + TexturePackInterval);
251 int QuotientZ = TexturePackMaxSize / (TexSize.Z + TexturePackInterval);
252
253 FIntVector AtlasTexCount = FIntVector(1, 1, 1);
254 if (QuotientX < TexCount)
255 {
256 AtlasTexCount.X = QuotientX;
257 }
258 else
259 {
260 AtlasTexCount.X = TexCount;
261 }
262
263 int CurTexCount = TexCount / QuotientX + (TexCount % QuotientX == 0 ? 0 : 1);
264 if (QuotientY < CurTexCount)
265 {
266 AtlasTexCount.Y = QuotientY;
267 }
268 else
269 {
270 AtlasTexCount.Y = CurTexCount;
271 }
272
273 CurTexCount = CurTexCount / QuotientY + (CurTexCount % QuotientY == 0 ? 0 : 1);
274 if (QuotientZ < CurTexCount)
275 {
276 // Warning: atlas size full
277 AtlasTexCount.Z = QuotientZ;
278 }
279 else
280 {
281 AtlasTexCount.Z = CurTexCount;
282 }
283 return AtlasTexCount;
284}
285
286void FIVSmokeRenderer::InitializeCSM(TSharedPtr<FPerWorldData> WorldData, UWorld* World)
287{
288 if (!World || !WorldData.IsValid())
289 {
290 return;
291 }
292
293 const UIVSmokeSettings* Settings = UIVSmokeSettings::Get();
294 if (!Settings || !Settings->IsExternalShadowingEnabled())
295 {
296 return;
297 }
298
299 // Create CSM renderer if not exists
300 if (!WorldData->CSMRenderer)
301 {
302 WorldData->CSMRenderer = MakeUnique<FIVSmokeCSMRenderer>();
303 }
304
305 // Initialize with settings
306 if (!WorldData->CSMRenderer->IsInitialized())
307 {
308 WorldData->CSMRenderer->Initialize(
309 World,
310 Settings->GetEffectiveNumCascades(),
313 );
314 }
315
316 // Create VSM processor if VSM is enabled
317 if (Settings->bEnableVSM && !WorldData->VSMProcessor)
318 {
319 WorldData->VSMProcessor = MakeUnique<FIVSmokeVSMProcessor>();
320 }
321}
322
323bool FIVSmokeRenderer::GetMainDirectionalLight(UWorld* World, FVector& OutDirection, FLinearColor& OutColor, float& OutIntensity)
324{
325 if (!World)
326 {
327 return false;
328 }
329
330 UDirectionalLightComponent* BestLight = nullptr;
331 int32 BestIndex = INT_MAX;
332
333 // Find the atmosphere sun light with lowest index (0 = sun, 1 = moon)
334 for (TActorIterator<ADirectionalLight> It(World); It; ++It)
335 {
336 UDirectionalLightComponent* LightComp = Cast<UDirectionalLightComponent>(It->GetLightComponent());
337 if (LightComp && LightComp->IsUsedAsAtmosphereSunLight())
338 {
339 int32 Index = LightComp->GetAtmosphereSunLightIndex();
340 if (Index < BestIndex)
341 {
342 BestIndex = Index;
343 BestLight = LightComp;
344 }
345 }
346 }
347
348 // Fallback: first DirectionalLight found
349 if (!BestLight)
350 {
351 for (TActorIterator<ADirectionalLight> It(World); It; ++It)
352 {
353 BestLight = Cast<UDirectionalLightComponent>(It->GetLightComponent());
354 if (BestLight)
355 {
356 break;
357 }
358 }
359 }
360
361 if (BestLight)
362 {
363 // Negate: Shader expects direction TOWARD the light, not FROM the light
364 OutDirection = -BestLight->GetComponentRotation().Vector();
365 OutColor = BestLight->GetLightColor();
366 OutIntensity = BestLight->Intensity;
367 return true;
368 }
369
370 return false;
371}
372
373void FIVSmokeRenderer::CreateNoiseVolume()
374{
375 constexpr int32 TexSize = FIVSmokeNoiseConfig::TexSize;
376
377 // Create volume texture
378 NoiseVolume = NewObject<UTextureRenderTargetVolume>();
379 NoiseVolume->AddToRoot(); // Prevent GC
380 NoiseVolume->Init(TexSize, TexSize, TexSize, EPixelFormat::PF_R16F);
381 NoiseVolume->bCanCreateUAV = true;
382 NoiseVolume->ClearColor = FLinearColor::Black;
383 NoiseVolume->SRGB = false;
384 NoiseVolume->UpdateResourceImmediate(true);
385
386 // Cache noise volume size for stats
387 CachedNoiseVolumeSize = CalculateImageBytes(TexSize, TexSize, TexSize, PF_R16F);
388
389 // Run compute shader to generate noise
390 FTextureRenderTargetResource* RenderTargetResource = NoiseVolume->GameThread_GetRenderTargetResource();
391 if (!RenderTargetResource)
392 {
393 UE_LOG(LogIVSmoke, Error, TEXT("[FIVSmokeRenderer::CreateNoiseVolume] Failed to get render target resource"));
394 return;
395 }
396
397 ENQUEUE_RENDER_COMMAND(IVSmokeGenerateNoise)(
398 [RenderTargetResource](FRHICommandListImmediate& RHICmdList)
399 {
400 FRDGBuilder GraphBuilder(RHICmdList);
401 FRDGTextureRef NoiseTexture = GraphBuilder.RegisterExternalTexture(
402 CreateRenderTarget(RenderTargetResource->TextureRHI, TEXT("IVSmokeNoiseVolume"))
403 );
404
405 FRDGTextureUAVRef OutputUAV = GraphBuilder.CreateUAV(NoiseTexture);
406
407 auto* Parameters = GraphBuilder.AllocParameters<FIVSmokeNoiseGeneratorGlobalCS::FParameters>();
408 Parameters->RWNoiseTex = OutputUAV;
409 Parameters->TexSize = FUintVector3(FIVSmokeNoiseConfig::TexSize, FIVSmokeNoiseConfig::TexSize, FIVSmokeNoiseConfig::TexSize);
410 Parameters->Octaves = FIVSmokeNoiseConfig::Octaves;
411 Parameters->Wrap = FIVSmokeNoiseConfig::Wrap;
412 Parameters->AxisCellCount = FIVSmokeNoiseConfig::AxisCellCount;
413 Parameters->Amplitude = FIVSmokeNoiseConfig::Amplitude;
414 Parameters->CellSize = FIVSmokeNoiseConfig::CellSize;
415 Parameters->Seed = FIVSmokeNoiseConfig::Seed;
416
417 TShaderMapRef<FIVSmokeNoiseGeneratorGlobalCS> ComputeShader(GetGlobalShaderMap(GMaxRHIFeatureLevel));
418
419 FIntVector GroupCount(
420 FMath::DivideAndRoundUp(FIVSmokeNoiseConfig::TexSize, 8),
421 FMath::DivideAndRoundUp(FIVSmokeNoiseConfig::TexSize, 8),
422 FMath::DivideAndRoundUp(FIVSmokeNoiseConfig::TexSize, 8)
423 );
424
425 GraphBuilder.AddPass(
426 RDG_EVENT_NAME("IVSmokeNoiseGeneration"),
427 Parameters,
428 ERDGPassFlags::Compute,
429 [Parameters, ComputeShader, GroupCount](FRHIComputeCommandList& RHICmdList)
430 {
431 FComputeShaderUtils::Dispatch(RHICmdList, ComputeShader, *Parameters, GroupCount);
432 }
433 );
434 GraphBuilder.Execute();
435 }
436 );
437}
438
439const UIVSmokeSmokePreset* FIVSmokeRenderer::GetEffectivePreset(const AIVSmokeVoxelVolume* Volume) const
440{
441 // Check for volume-specific override first
442 if (Volume)
443 {
444 const UIVSmokeSmokePreset* Override = Volume->GetSmokePresetOverride();
445 if (Override)
446 {
447 return Override;
448 }
449 }
450
451 // Fall back to CDO (Class Default Object) for default appearance values
452 return GetDefault<UIVSmokeSmokePreset>();
453}
454
455//~==============================================================================
456// Thread-Safe Render Data Preparation
457
458FIVSmokePackedRenderData FIVSmokeRenderer::PrepareRenderData(UWorld* World, const TArray<AIVSmokeVoxelVolume*>& InVolumes, const FVector& CameraPosition)
459{
460 // Must be called on Game Thread
461 check(IsInGameThread());
462
464
465 if (!World || InVolumes.Num() == 0)
466 {
467 return Result;
468 }
469
470 // Lazy initialization on first render
471 if (!IsInitialized())
472 {
473 Initialize();
474 }
475
476 // Get or create per-world data
477 TSharedPtr<FPerWorldData> WorldData = GetOrCreateWorldData(World);
478 if (!WorldData.IsValid())
479 {
480 return Result;
481 }
482
483 // Frame deduplication: Skip if already prepared this frame
484 // This prevents redundant processing when multiple ViewFamilies render the same World
485 const uint32 CurrentFrameNumber = GFrameNumber;
486 if (WorldData->LastPreparedFrameNumber == CurrentFrameNumber)
487 {
488 // Already prepared this frame - return Game Thread cached data
489 // Note: Use GameThreadCachedRenderData, not CachedRenderData (which is for Render Thread)
490 return WorldData->GameThreadCachedRenderData;
491 }
492 WorldData->LastPreparedFrameNumber = CurrentFrameNumber;
493
494 // Filter volumes if exceeding maximum supported count
495 TArray<AIVSmokeVoxelVolume*> FilteredVolumes;
496 if (InVolumes.Num() > MaxSupportedVolumes)
497 {
498 UE_LOG(LogIVSmoke, Warning,
499 TEXT("[FIVSmokeRenderer::PrepareRenderData] Volume count (%d) exceeds maximum (%d). "
500 "Farthest volumes from camera will be excluded."),
501 InVolumes.Num(), MaxSupportedVolumes
502 );
503
504 // Copy and sort by distance from camera (closest first)
505 FilteredVolumes = InVolumes;
506 FilteredVolumes.Sort([&CameraPosition](const AIVSmokeVoxelVolume& A, const AIVSmokeVoxelVolume& B)
507 {
508 const float DistA = FVector::DistSquared(CameraPosition, A.GetActorLocation());
509 const float DistB = FVector::DistSquared(CameraPosition, B.GetActorLocation());
510 return DistA < DistB;
511 });
512
513 // Keep only the closest MaxSupportedVolumes
514 FilteredVolumes.SetNum(MaxSupportedVolumes);
515 }
516
517 const TArray<AIVSmokeVoxelVolume*>& VolumesToProcess = (FilteredVolumes.Num() > 0) ? FilteredVolumes : InVolumes;
518
519 // Count valid volumes and get resolution info
520 // Note: All volumes in VolumesToProcess passed ShouldRender() which now checks bIsInitialized,
521 // so they are guaranteed to have properly allocated buffers.
522 int32 ValidVolumeCount = 0;
523 for (AIVSmokeVoxelVolume* Vol : VolumesToProcess)
524 {
525 if (IsValid(Vol))
526 {
527 if (ValidVolumeCount == 0)
528 {
529 Result.VoxelResolution = Vol->GetGridResolution();
530 }
531 ValidVolumeCount++;
532 }
533 }
534
535 Result.VolumeCount = ValidVolumeCount;
536
537 // Early return if no valid volumes
538 if (Result.VolumeCount == 0)
539 {
540 return Result;
541 }
542
543 Result.VolumeDataArray.Reserve(Result.VolumeCount);
544 Result.HoleTextures.Reserve(Result.VolumeCount);
545 Result.HoleTextureSizes.Reserve(Result.VolumeCount);
546
547 // Get hole resolution from first valid volume with HoleGenerator
548 for (AIVSmokeVoxelVolume* Volume : VolumesToProcess)
549 {
550 if (IsValid(Volume))
551 {
553 {
554 if (FTextureRHIRef HoleTex = HoleComp->GetHoleTextureRHI())
555 {
556 Result.HoleResolution = HoleTex->GetSizeXYZ();
557 break;
558 }
559 }
560 }
561 }
562
563 // Fallback for hole resolution
564 if (Result.HoleResolution == FIntVector::ZeroValue)
565 {
566 Result.HoleResolution = FIntVector(64, 64, 64);
567 }
568
569 // Calculate packed buffer sizes
570 const int32 TexturePackInterval = 4;
571 TArray<float> VoxelIntervalData;
572 VoxelIntervalData.Init(0, Result.VoxelResolution.X * Result.VoxelResolution.Y * TexturePackInterval);
573
574 FIntVector VoxelAtlasResolution = FIntVector(
575 Result.VoxelResolution.X,
576 Result.VoxelResolution.Y,
577 Result.VoxelResolution.Z * Result.VolumeCount + TexturePackInterval * (Result.VolumeCount - 1)
578 );
579 int32 TotalVoxelSize = VoxelAtlasResolution.X * VoxelAtlasResolution.Y * VoxelAtlasResolution.Z;
580 Result.PackedVoxelBirthTimes.Reserve(TotalVoxelSize);
581 Result.PackedVoxelDeathTimes.Reserve(TotalVoxelSize);
582
583 // Expected voxel count based on resolution (for defensive validation)
584 const int32 ExpectedVoxelCount = Result.VoxelResolution.X * Result.VoxelResolution.Y * Result.VoxelResolution.Z;
585
586 // Collect data from all volumes (Game Thread - safe to access)
587 int32 ValidVolumeIndex = 0;
588 for (int32 i = 0; i < VolumesToProcess.Num(); ++i)
589 {
590 AIVSmokeVoxelVolume* Volume = VolumesToProcess[i];
591 if (!IsValid(Volume))
592 {
593 continue;
594 }
595
596 //~==========================================================================
597 // Copy VoxelArray data (Game Thread safe)
598 const TArray<float>& VoxelBirthTimes = Volume->GetVoxelBirthTimes();
599 const TArray<float>& VoxelDeathTimes = Volume->GetVoxelDeathTimes();
600
601 // Defensive check: ShouldRender() guarantees bIsInitialized, so buffer sizes should match
602 // Using ensure() to catch bugs in debug builds without skipping in release
603 if (!ensure(VoxelBirthTimes.Num() == ExpectedVoxelCount && VoxelDeathTimes.Num() == ExpectedVoxelCount))
604 {
605 UE_LOG(LogIVSmoke, Error, TEXT("[PrepareRenderData] %s: Buffer size mismatch - this should never happen if ShouldRender() is correct"), *Volume->GetName());
606 continue;
607 }
608
609 Result.PackedVoxelBirthTimes.Append(VoxelBirthTimes);
610 Result.PackedVoxelDeathTimes.Append(VoxelDeathTimes);
611
612 // Add interval padding between volumes (not after the last valid volume)
613 if (ValidVolumeIndex < Result.VolumeCount - 1)
614 {
615 Result.PackedVoxelBirthTimes.Append(VoxelIntervalData);
616 Result.PackedVoxelDeathTimes.Append(VoxelIntervalData);
617 }
618
619 //~==========================================================================
620 // Hole Texture reference (RHI resources are thread-safe)
622 if (IsValid(HoleComp))
623 {
624 FTextureRHIRef HoleTex = HoleComp->GetHoleTextureRHI();
625 Result.HoleTextures.Add(HoleTex);
626 if (HoleTex)
627 {
628 Result.HoleTextureSizes.Add(HoleTex->GetSizeXYZ());
629 }
630 else
631 {
632 Result.HoleTextureSizes.Add(FIntVector::ZeroValue);
633 }
634 }
635 else
636 {
637 Result.HoleTextures.Add(nullptr);
638 Result.HoleTextureSizes.Add(FIntVector::ZeroValue);
639 }
640
641 //~==========================================================================
642 // Build GPU metadata
643 const FIntVector GridRes = Volume->GetGridResolution();
644 const FIntVector CenterOff = Volume->GetCenterOffset();
645 const float VoxelSz = Volume->GetVoxelSize();
646 const FTransform VolumeTransform = Volume->GetActorTransform();
647
648 // Calculate AABB
649 FVector HalfExtent = FVector(CenterOff) * VoxelSz;
650 FVector LocalMin = -HalfExtent;
651 FVector LocalMax = FVector(GridRes - CenterOff - FIntVector(1, 1, 1)) * VoxelSz;
652 FBox LocalBox(LocalMin, LocalMax);
653 FBox WorldBox = LocalBox.TransformBy(VolumeTransform);
654
655 // Get preset data
656 const UIVSmokeSmokePreset* Preset = GetEffectivePreset(Volume);
657
658 // Build GPU data struct
659 FIVSmokeVolumeGPUData GPUData;
660 FMemory::Memzero(&GPUData, sizeof(GPUData));
661
662 GPUData.VoxelSize = VoxelSz;
663 // Use ValidVolumeIndex for buffer offset (not loop index i) to handle skipped invalid volumes
664 GPUData.VoxelBufferOffset = Result.VoxelResolution.X * Result.VoxelResolution.Y *
665 (Result.VoxelResolution.Z + TexturePackInterval) * ValidVolumeIndex;
666 GPUData.GridResolution = FIntVector3(GridRes.X, GridRes.Y, GridRes.Z);
667 GPUData.VoxelCount = VoxelBirthTimes.Num();
668 GPUData.CenterOffset = FVector3f(CenterOff.X, CenterOff.Y, CenterOff.Z);
669 GPUData.VolumeWorldAABBMin = FVector3f(WorldBox.Min);
670 GPUData.VolumeWorldAABBMax = FVector3f(WorldBox.Max);
671
672 // VoxelWorldAABB: Use actual voxel bounds if valid, otherwise fallback to VolumeWorldAABB
673 // Initial state (FLT_MAX/-FLT_MAX) indicates no voxels have been created yet
674 FVector VoxelAABBMin = Volume->GetVoxelWorldAABBMin();
675 FVector VoxelAABBMax = Volume->GetVoxelWorldAABBMax();
676 const bool bVoxelAABBValid = (VoxelAABBMin.X < VoxelAABBMax.X) &&
677 (VoxelAABBMin.Y < VoxelAABBMax.Y) &&
678 (VoxelAABBMin.Z < VoxelAABBMax.Z);
679 if (bVoxelAABBValid)
680 {
681 GPUData.VoxelWorldAABBMin = FVector3f(VoxelAABBMin);
682 GPUData.VoxelWorldAABBMax = FVector3f(VoxelAABBMax);
683 }
684 else
685 {
686 // Fallback to volume bounds to prevent NaN in shader
687 GPUData.VoxelWorldAABBMin = GPUData.VolumeWorldAABBMin;
688 GPUData.VoxelWorldAABBMax = GPUData.VolumeWorldAABBMax;
689 }
690 GPUData.FadeInDuration = Volume->FadeInDuration;
691 GPUData.FadeOutDuration = Volume->FadeOutDuration;
692
693 if (Preset)
694 {
695 GPUData.SmokeColor = FVector3f(Preset->SmokeColor.R, Preset->SmokeColor.G, Preset->SmokeColor.B);
696 GPUData.Absorption = Preset->SmokeAbsorption;
697 GPUData.DensityScale = Preset->VolumeDensity;
698 }
699 else
700 {
701 GPUData.SmokeColor = FVector3f(0.8f, 0.8f, 0.8f);
702 GPUData.Absorption = 0.1f;
703 GPUData.DensityScale = 1.0f;
704 }
705
706 Result.VolumeDataArray.Add(GPUData);
707 ValidVolumeIndex++;
708 }
709
710 // Verify: ValidVolumeIndex should match pre-counted VolumeCount
711 check(ValidVolumeIndex == Result.VolumeCount);
712
713 //~==========================================================================
714 // Copy global settings parameters
715 const UIVSmokeSettings* Settings = UIVSmokeSettings::Get();
716
717 if (Settings)
718 {
719 // Ray marching
720 Result.MaxSteps = Settings->GetEffectiveMaxSteps();
721
722 // Appearance
723 Result.GlobalAbsorption = 0.1f; // Default, per-volume absorption from preset
724 Result.SmokeSize = Settings->SmokeSize;
725 Result.SmokeDensityFalloff = Settings->SmokeDensityFalloff;
726 Result.WindDirection = Settings->WindDirection;
727 Result.VolumeRangeOffset = Settings->VolumeRangeOffset;
728 Result.VolumeEdgeNoiseFadeOffset = Settings->VolumeEdgeNoiseFadeOffset;
729 Result.VolumeEdgeFadeSharpness = Settings->VolumeEdgeFadeSharpness;
730
731 // Scattering
732 Result.bEnableScattering = Settings->bEnableScattering;
733 Result.ScatterScale = Settings->ScatterScale;
734 Result.ScatteringAnisotropy = Settings->ScatteringAnisotropy;
735
736 //Rendering
737 UIVSmokeVisualMaterialPreset* VisualMaterialPreset = Settings->GetVisualMaterialPreset();
738 if (VisualMaterialPreset)
739 {
740 Result.SmokeVisualMaterial = VisualMaterialPreset->SmokeVisualMaterial.Get();
741 Result.UpSampleFilterType = (int)VisualMaterialPreset->UpSampleFilterType;
742 Result.SharpenStrength = VisualMaterialPreset->SharpenStrength;
743 Result.BlurStrength = VisualMaterialPreset->BlurStrength;
744 }
745
746 // Note: World is passed as parameter, used for light detection and shadow capture
747
748 // Light Direction and Color
749 // Priority: Settings Override > World DirectionalLight > Default
750 if (Settings->bOverrideLightDirection)
751 {
752 Result.LightDirection = Settings->LightDirectionOverride.GetSafeNormal();
753 Result.LightIntensity = 1.0f; // Override assumes full intensity
754 }
755 else
756 {
757 FVector AutoLightDir;
758 FLinearColor AutoLightColor;
759 float AutoLightIntensity;
760
761 if (GetMainDirectionalLight(World, AutoLightDir, AutoLightColor, AutoLightIntensity))
762 {
763 Result.LightDirection = AutoLightDir;
764 Result.LightIntensity = AutoLightIntensity;
765
766 // Also use auto light color if not overridden
767 if (!Settings->bOverrideLightColor)
768 {
769 Result.LightColor = AutoLightColor;
770 }
771 }
772 else
773 {
774 // No directional light found - dark environment
775 Result.LightDirection = FVector(0.0f, 0.0f, -1.0f);
776 Result.LightIntensity = 0.0f;
777 Result.LightColor = FLinearColor::Black;
778 }
779 }
780
781 if (Settings->bOverrideLightColor)
782 {
783 Result.LightColor = Settings->LightColorOverride;
784 }
785
786 // Self-shadowing
787 Result.bEnableSelfShadowing = Settings->IsSelfShadowingEnabled();
788 Result.LightMarchingSteps = Settings->GetEffectiveLightMarchingSteps();
789 Result.LightMarchingDistance = Settings->LightMarchingDistance;
790 Result.LightMarchingExpFactor = Settings->LightMarchingExpFactor;
791 Result.ShadowAmbient = Settings->ShadowAmbient;
792
793 // External shadowing (CSM - Cascaded Shadow Maps)
794 // Note: CSM is always used when external shadowing is enabled. NumCascades > 0 indicates active.
795 Result.ShadowDepthBias = Settings->ShadowDepthBias;
796 Result.ExternalShadowAmbient = Settings->ExternalShadowAmbient;
797
798 // VSM settings
799 Result.bEnableVSM = Settings->bEnableVSM;
800 Result.VSMMinVariance = Settings->VSMMinVariance;
801 Result.VSMLightBleedingReduction = Settings->VSMLightBleedingReduction;
802 Result.CascadeBlendRange = Settings->CascadeBlendRange;
803
804 // Skip shadow capture if we're already inside a shadow capture render pass (prevents infinite recursion)
805 if (Settings->IsExternalShadowingEnabled() && VolumesToProcess.Num() > 0 && !bIsCapturingShadow)
806 {
807 // Per-frame guard: Only update once per actual engine frame
808 // PrepareRenderData can be called multiple times per frame (multiple views)
809 const bool bAlreadyUpdatedThisFrame = (WorldData->LastCSMUpdateFrameNumber == CurrentFrameNumber);
810
811 if (!bAlreadyUpdatedThisFrame)
812 {
813 WorldData->LastCSMUpdateFrameNumber = CurrentFrameNumber;
814
815 // Initialize CSM if needed
816 InitializeCSM(WorldData, World);
817
818 if (WorldData->CSMRenderer && WorldData->CSMRenderer->IsInitialized())
819 {
820 // Set re-entry guard (safety measure)
821 bIsCapturingShadow = true;
822
823 // Get camera position from first volume's world (or use centroid of volumes)
824 FVector CSMCameraPosition = FVector::ZeroVector;
825 FVector CSMCameraForward = FVector(1.0f, 0.0f, 0.0f);
826
827 // Try to get player camera position
828 if (APlayerController* PC = World->GetFirstPlayerController())
829 {
830 if (APlayerCameraManager* CameraManager = PC->PlayerCameraManager)
831 {
832 CSMCameraPosition = CameraManager->GetCameraLocation();
833 CSMCameraForward = CameraManager->GetCameraRotation().Vector();
834 }
835 }
836
837 // Update CSM with current frame (includes synchronous capture)
838 WorldData->CSMRenderer->Update(
839 CSMCameraPosition,
840 CSMCameraForward,
841 Result.LightDirection,
842 CurrentFrameNumber
843 );
844
845 bIsCapturingShadow = false;
846 }
847 }
848
849 // Populate CSM data for shader (even if not updated this frame)
850 // Synchronous capture: VP matrix and depth texture are from the SAME Update() call
851 if (WorldData->CSMRenderer && WorldData->CSMRenderer->IsInitialized() && WorldData->CSMRenderer->HasValidShadowData())
852 {
853 Result.NumCascades = WorldData->CSMRenderer->GetNumCascades();
854
855 // Get split distances
856 Result.CSMSplitDistances = WorldData->CSMRenderer->GetSplitDistances();
857
858 // Get textures, matrices, and light camera data for each cascade
859 Result.CSMDepthTextures.SetNum(Result.NumCascades);
860 Result.CSMVSMTextures.SetNum(Result.NumCascades);
861 Result.CSMViewProjectionMatrices.SetNum(Result.NumCascades);
862 Result.CSMLightCameraPositions.SetNum(Result.NumCascades);
863 Result.CSMLightCameraForwards.SetNum(Result.NumCascades);
864
865 for (int32 i = 0; i < Result.NumCascades; i++)
866 {
867 const FIVSmokeCascadeData& Cascade = WorldData->CSMRenderer->GetCascade(i);
868 // Synchronous capture: VP matrix and texture are from the SAME frame
869 Result.CSMViewProjectionMatrices[i] = Cascade.ViewProjectionMatrix;
870 Result.CSMDepthTextures[i] = WorldData->CSMRenderer->GetDepthTexture(i);
871 Result.CSMVSMTextures[i] = WorldData->CSMRenderer->GetVSMTexture(i);
872 Result.CSMLightCameraPositions[i] = Cascade.LightCameraPosition;
873 Result.CSMLightCameraForwards[i] = Cascade.LightCameraForward;
874 }
875
876 // Store the main camera position for consistent use in shader
877 Result.CSMMainCameraPosition = WorldData->CSMRenderer->GetMainCameraPosition();
878 }
879 }
880 }
881
882 Result.bIsValid = Result.VolumeDataArray.Num() && Result.PackedVoxelBirthTimes.Num() > 0 && Result.PackedVoxelDeathTimes.Num() > 0;
883
884 if (VolumesToProcess.Num() > 0 && IsValid(VolumesToProcess[0]))
885 {
886 Result.GameTime = VolumesToProcess[0]->GetSyncWorldTimeSeconds();
887 }
888 else
889 {
890 Result.GameTime = 0.0f;
891 }
892
893 // Cache for Game Thread (used when multiple ViewFamilies query same frame)
894 WorldData->GameThreadCachedRenderData = Result;
895
896 return Result;
897}
898
899//~==============================================================================
900// Rendering
901
902FScreenPassTexture FIVSmokeRenderer::Render(
903 FRDGBuilder& GraphBuilder,
904 const FSceneView& View,
905 const FPostProcessMaterialInputs& Inputs)
906{
907 // Get scene color from inputs FIRST - needed for passthrough
908 FScreenPassTextureSlice SceneColorSlice = Inputs.GetInput(EPostProcessMaterialInput::SceneColor);
909 if (!SceneColorSlice.IsValid())
910 {
911 return FScreenPassTexture();
912 }
913
914 FScreenPassTexture SceneColor(SceneColorSlice);
915
916 // Get settings with null check
917 const UIVSmokeSettings* Settings = UIVSmokeSettings::Get();
918 if (!Settings)
919 {
920 return SceneColor;
921 }
922
923 // Check if rendering is enabled - passthrough if disabled
924 if (!Settings->bEnableSmokeRendering)
925 {
926 return SceneColor;
927 }
928
929 // Extract World from View
930 UWorld* World = nullptr;
931 if (View.Family && View.Family->Scene)
932 {
933 World = View.Family->Scene->GetWorld();
934 }
935
936 if (!World)
937 {
938 return SceneColor;
939 }
940
941 // Get per-world data
942 TSharedPtr<FPerWorldData> WorldData = GetWorldData(World);
943 if (!WorldData.IsValid())
944 {
945 return SceneColor;
946 }
947
948 // Get cached render data for this World
949 // Use copy instead of MoveTemp - multiple views in same frame share the same data
950 FIVSmokePackedRenderData RenderData;
951 {
952 FScopeLock Lock(&WorldDataMutex);
953 RenderData = WorldData->CachedRenderData; // Copy - don't consume, other views may need it
954 }
955
956 // Early out if no valid render data - avoid unnecessary texture allocations
957 if (!RenderData.bIsValid)
958 {
959 return SceneColor;
960 }
961
962 FScreenPassRenderTarget Output = Inputs.OverrideOutput;
963
964 if (!Output.IsValid())
965 {
966 Output = FScreenPassRenderTarget(
967 SceneColor.Texture,
968 SceneColor.ViewRect,
969 ERenderTargetLoadAction::ELoad
970 );
971 }
972
973 // Use ViewRect size consistently for all passes
974 const FIntPoint ViewportSize = SceneColor.ViewRect.Size();
975 const FIntPoint ViewRectMin = SceneColor.ViewRect.Min;
976
977 // Update stats (1-second interval)
978 UpdateStatsIfNeeded(RenderData, ViewportSize);
979
980 //~==========================================================================
981 // Get smoke textures from View-based RDG cache
982 // Pre-pass (PostRenderBasePassDeferred) runs BEFORE Post-process, so cache should be valid
983 // Cache is keyed by ViewState to ensure correct View matching
984
985 if (!View.State)
986 {
987 // View.State is null - cannot use as cache key (would cause incorrect sharing)
988 UE_LOG(LogIVSmoke, Verbose, TEXT("[FIVSmokeRenderer::Render] View.State is null, skipping smoke rendering"));
989 return SceneColor;
990 }
991
992 // Thread-safe ViewData access (protect against viewport switching races)
993 FRDGTextureRef SmokeTex;
994 FRDGTextureRef SmokeLocalPosAlphaFull;
995 FRDGTextureRef SmokeWorldPosDepthFull;
996 FIntPoint EffectiveViewportSize;
997 {
998 FScopeLock Lock(&ViewDataMutex);
999 FPerViewData* ViewData = ViewDataMap.Find(View.State);
1000 if (!ViewData || !ViewData->bIsValid)
1001 {
1002 // Per-view data not available for this View - passthrough without smoke rendering
1003 // This can happen when: Scene Capture (filtered out), first frame, etc.
1004 UE_LOG(LogIVSmoke, Warning, TEXT("[FIVSmokeRenderer::Render] ViewData miss: ViewState=%p, ViewFamily=%p, FamilyFrame=%u, ViewDataMapSize=%d"),
1005 View.State, View.Family, View.Family ? View.Family->FrameNumber : 0, ViewDataMap.Num());
1006 return SceneColor;
1007 }
1008
1009
1010 // Copy RDG texture references (safe to use outside mutex - RDG manages lifetime)
1011 SmokeTex = ViewData->SmokeTex;
1012 SmokeLocalPosAlphaFull = ViewData->LocalPosAlphaTex;
1013 SmokeWorldPosDepthFull = ViewData->WorldPosDepthTex;
1014 EffectiveViewportSize = ViewData->ViewportSize;
1015 }
1016
1017 FIntVector SceneSize = SceneColor.Texture->Desc.GetSize();
1018 FInt32Point SceneViewRectSize = SceneColor.ViewRect.Size();
1019 RDG_EVENT_SCOPE(GraphBuilder, "IVSmoke_PostProcess_VisualComposite");
1020
1021 //~==========================================================================
1022 // Visual Pass
1023 // Note: Use EffectiveViewportSize to match smoke texture dimensions (may differ from SceneColor.ViewRect when using Pre-pass cache)
1024 if (RenderData.SmokeVisualMaterial)
1025 {
1026 FRDGTextureRef SmokeVisualTex = AddSmokeVisualPass(GraphBuilder, RenderData, View, SmokeTex, SmokeLocalPosAlphaFull, SmokeWorldPosDepthFull, SceneColor.Texture, EffectiveViewportSize);
1027 return FScreenPassTexture(SmokeVisualTex);
1028 }
1029 else
1030 {
1031 AddCompositePass(GraphBuilder, RenderData, View, SceneColor.Texture, SmokeTex, Output, EffectiveViewportSize);
1032 return FScreenPassTexture(Output);
1033 }
1034}
1035
1036//~==============================================================================
1037// Composite Pass Functions
1038
1039void FIVSmokeRenderer::AddCompositePass(
1040 FRDGBuilder& GraphBuilder,
1041 const FIVSmokePackedRenderData& RenderData,
1042 const FSceneView& View,
1043 FRDGTextureRef SceneTex,
1044 FRDGTextureRef SmokeTex,
1045 const FScreenPassRenderTarget& Output,
1046 const FIntPoint& ViewportSize)
1047{
1048 FGlobalShaderMap* ShaderMap = GetGlobalShaderMap(View.FeatureLevel);
1049 TShaderMapRef<FIVSmokeCompositePS> PixelShader(ShaderMap);
1050
1051 auto* Parameters = GraphBuilder.AllocParameters<FIVSmokeCompositePS::FParameters>();
1052 Parameters->SceneTex = SceneTex;
1053 Parameters->SmokeTex = SmokeTex;
1054 Parameters->LinearClamp_Sampler = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
1055 Parameters->ViewportSize = FVector2f(ViewportSize);
1056 Parameters->ViewRectMin = FVector2f(Output.ViewRect.Min);
1057 Parameters->RenderTargets[0] = Output.GetRenderTargetBinding();
1058
1059 FIVSmokePostProcessPass::AddPixelShaderPass<FIVSmokeCompositePS>(GraphBuilder, ShaderMap, PixelShader, Parameters, Output);
1060}
1061
1062
1063
1064//~==============================================================================
1065// Copy Pass (Progressive Upscaling)
1066
1067FRDGTextureRef FIVSmokeRenderer::AddCopyPass(
1068 FRDGBuilder& GraphBuilder,
1069 const FSceneView& View,
1070 FRDGTextureRef SourceTex,
1071 const FIntPoint& DestSize,
1072 const TCHAR* TexName)
1073{
1074 // Create destination texture at specified size
1075 FRDGTextureRef DestTex = FIVSmokePostProcessPass::CreateOutputTexture(
1076 GraphBuilder,
1077 SourceTex,
1078 TexName,
1079 PF_FloatRGBA,
1080 DestSize,
1081 TexCreate_RenderTargetable | TexCreate_ShaderResource
1082 );
1083
1084 // Perform copy
1085 AddCopyPass(GraphBuilder, View, SourceTex, DestTex);
1086
1087 return DestTex;
1088}
1089
1090void FIVSmokeRenderer::AddCopyPass(
1091 FRDGBuilder& GraphBuilder,
1092 const FSceneView& View,
1093 FRDGTextureRef SourceTex,
1094 FRDGTextureRef DestTex)
1095{
1096 FGlobalShaderMap* ShaderMap = GetGlobalShaderMap(View.FeatureLevel);
1097 TShaderMapRef<FIVSmokeCopyPS> CopyShader(ShaderMap);
1098
1099 const FIntPoint DestSize = DestTex->Desc.Extent;
1100
1101 auto* Parameters = GraphBuilder.AllocParameters<FIVSmokeCopyPS::FParameters>();
1102 Parameters->MainTex = SourceTex;
1103 Parameters->LinearRepeat_Sampler = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
1104 Parameters->ViewportSize = FVector2f(DestSize);
1105 Parameters->RenderTargets[0] = FRenderTargetBinding(DestTex, ERenderTargetLoadAction::ENoAction);
1106
1107 FScreenPassRenderTarget Output(
1108 DestTex,
1109 FIntRect(0, 0, DestSize.X, DestSize.Y),
1110 ERenderTargetLoadAction::ENoAction
1111 );
1112
1113 FIVSmokePostProcessPass::AddPixelShaderPass<FIVSmokeCopyPS>(GraphBuilder, ShaderMap, CopyShader, Parameters, Output);
1114}
1115
1116//~==============================================================================
1117// Upsample Filtering Pass
1118FRDGTextureRef FIVSmokeRenderer::AddUpsampleFilterPass(
1119 FRDGBuilder& GraphBuilder,
1120 const FIVSmokePackedRenderData& RenderData,
1121 const FSceneView& View,
1122 FRDGTextureRef SceneTex,
1123 FRDGTextureRef SmokeAlbedo,
1124 FRDGTextureRef SmokeLocalPosAlpha,
1125 const FIntPoint& TexSize,
1126 const FIntPoint& ViewRectMin)
1127{
1128 FRDGTextureRef SmokeTex = FIVSmokePostProcessPass::CreateOutputTexture(
1129 GraphBuilder,
1130 SmokeAlbedo,
1131 TEXT("IVSmokeUpsampleFilterTex"),
1132 PF_FloatRGBA,
1133 TexSize
1134 );
1135
1136 FGlobalShaderMap* ShaderMap = GetGlobalShaderMap(View.FeatureLevel);
1137 TShaderMapRef<FIVSmokeUpsampleFilterPS> PixelShader(ShaderMap);
1138 auto* Parameters = GraphBuilder.AllocParameters<FIVSmokeUpsampleFilterPS::FParameters>();
1139 Parameters->SceneTex = SceneTex;
1140 Parameters->SmokeAlbedoTex = SmokeAlbedo;
1141 Parameters->SmokeLocalPosAlphaTex = SmokeLocalPosAlpha;
1142 Parameters->LinearClamp_Sampler = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
1143 Parameters->UpSampleFilterType = RenderData.UpSampleFilterType;
1144 Parameters->SharpenStrength = RenderData.SharpenStrength;
1145 Parameters->BlurStrength = RenderData.BlurStrength;
1146 Parameters->ViewportSize = TexSize;
1147 Parameters->ViewRectMin = FVector2f(ViewRectMin);
1148 Parameters->RenderTargets[0] = FRenderTargetBinding(SmokeTex, ERenderTargetLoadAction::ENoAction);
1149 FScreenPassRenderTarget Output(
1150 SmokeTex,
1151 FIntRect(0, 0, TexSize.X, TexSize.Y),
1152 ERenderTargetLoadAction::ENoAction
1153 );
1154 FIVSmokePostProcessPass::AddPixelShaderPass<FIVSmokeUpsampleFilterPS>(GraphBuilder, ShaderMap, PixelShader, Parameters, Output);
1155
1156 return SmokeTex;
1157}
1158
1159//~==============================================================================
1160// Smoke Visual Pass
1161FRDGTextureRef FIVSmokeRenderer::AddSmokeVisualPass(FRDGBuilder& GraphBuilder, const FIVSmokePackedRenderData& RenderData, const FSceneView& View, FRDGTextureRef SmokeTex, FRDGTextureRef SmokeLocalPosAlphaTex, FRDGTextureRef SmokeWorldPosDepthTex, FRDGTextureRef SceneTex, const FIntPoint& TexSize)
1162{
1163 UMaterialInterface* SmokeVisualMat = RenderData.SmokeVisualMaterial;
1164
1165 if (SmokeVisualMat == nullptr)
1166 {
1167 // UE_LOG(LogIVSmoke, Display, TEXT("SmokeVisualMaterial is none"));
1168 return SmokeTex;
1169 }
1170
1171 FPostProcessMaterialInputs PostProcessInputs;
1172
1173 // SmokeTex → PostProcessInput0
1174 PostProcessInputs.SetInput(GraphBuilder, EPostProcessMaterialInput::SceneColor, FScreenPassTexture(SmokeTex));
1175
1176 // SmokeLocalPosAlphaTex → PostProcessInput1
1177 PostProcessInputs.SetInput(GraphBuilder, EPostProcessMaterialInput::SeparateTranslucency, FScreenPassTexture(SmokeLocalPosAlphaTex));
1178
1179 // SceneTexture -> PostProcessInput 3
1180 PostProcessInputs.SetInput(GraphBuilder, EPostProcessMaterialInput::PostTonemapHDRColor, FScreenPassTexture(SceneTex));
1181
1182 // SmokeWorldPosDepthTex → PostProcessInput4
1183 PostProcessInputs.SetInput(GraphBuilder, EPostProcessMaterialInput::Velocity, FScreenPassTexture(SmokeWorldPosDepthTex));
1184
1185 PostProcessInputs.SceneTextures = GetSceneTextureShaderParameters(View);
1186
1187 //FRDGTextureRef OutputTex = GraphBuilder.CreateTexture(
1188 // FRDGTextureDesc::Create2D(TexSize, PF_FloatRGBA, FClearValueBinding::Black,
1189 // TexCreate_RenderTargetable | TexCreate_ShaderResource),
1190 // TEXT("IVSmokeVisualTex")
1191 //);
1192 FRDGTextureRef OutputTexture = FIVSmokePostProcessPass::CreateOutputTexture(
1193 GraphBuilder,
1194 SceneTex,
1195 TEXT("IVSmokeVisualTex"),
1196 PF_FloatRGBA,
1197 TexSize,
1198 TexCreate_RenderTargetable | TexCreate_ShaderResource
1199 );
1200 PostProcessInputs.OverrideOutput = FScreenPassRenderTarget(OutputTexture, ERenderTargetLoadAction::ENoAction);
1201
1202 AddPostProcessMaterialPass(GraphBuilder, View, PostProcessInputs, SmokeVisualMat);
1203
1204 return OutputTexture;
1205}
1206
1207
1208//~==============================================================================
1209// Multi-Volume Ray March Pass (Occupancy-Based Three-Pass Pipeline)
1210
1211void FIVSmokeRenderer::AddMultiVolumeRayMarchPass(
1212 FRDGBuilder& GraphBuilder,
1213 const FSceneView& View,
1214 const FIVSmokePackedRenderData& RenderData,
1215 FRDGTextureRef SmokeAlbedoTex,
1216 FRDGTextureRef SmokeLocalPosAlphaTex,
1217 FRDGTextureRef SmokeWorldPosDepthTex,
1218 const FIntPoint& TexSize,
1219 const FIntPoint& ViewportSize,
1220 const FIntPoint& ViewRectMin,
1221 FRDGTextureRef SceneDepthForDependency)
1222{
1223 const int32 VolumeCount = RenderData.VolumeCount;
1224
1225 if (VolumeCount == 0 || !NoiseVolume || !RenderData.bIsValid)
1226 {
1227 return;
1228 }
1229
1230 // Extract World from View to access per-world data (for VSMProcessor)
1231 UWorld* World = nullptr;
1232 if (View.Family && View.Family->Scene)
1233 {
1234 World = View.Family->Scene->GetWorld();
1235 }
1236 TSharedPtr<FPerWorldData> WorldData = World ? GetWorldData(World) : nullptr;
1237
1238 // Get global settings
1239 const UIVSmokeSettings* Settings = UIVSmokeSettings::Get();
1240
1241 //~==========================================================================
1242 // Phase 0: Setup common resources (same as standard ray march)
1243
1244 const int32 TexturePackInterval = 4;
1245 const int32 TexturePackMaxSize = 2048;
1246 const FIntVector VoxelResolution = RenderData.VoxelResolution;
1247 const FIntVector HoleResolution = RenderData.HoleResolution;
1248
1249 if (VoxelResolution.X <= 0 || VoxelResolution.Y <= 0 || VoxelResolution.Z <= 0)
1250 {
1251 UE_LOG(LogIVSmoke, Error, TEXT("[AddMultiVolumeRayMarchPass] Invalid VoxelResolution, aborting"));
1252 return;
1253 }
1254
1255 const FIntVector VoxelAtlasCount = GetAtlasTexCount(VoxelResolution, VolumeCount, TexturePackInterval, TexturePackMaxSize);
1256 const FIntVector HoleAtlasCount = GetAtlasTexCount(HoleResolution, VolumeCount, TexturePackInterval, TexturePackMaxSize);
1257
1258 // Validate atlas count to prevent negative resolution
1259 if (VoxelAtlasCount.X <= 0 || VoxelAtlasCount.Y <= 0 || VoxelAtlasCount.Z <= 0)
1260 {
1261 UE_LOG(LogIVSmoke, Error, TEXT("[AddMultiVolumeRayMarchPass] Invalid VoxelAtlasCount (%d,%d,%d), likely VolumeCount=0, aborting"),
1262 VoxelAtlasCount.X, VoxelAtlasCount.Y, VoxelAtlasCount.Z);
1263 return;
1264 }
1265
1266 // Voxel Atlas: 3D packing
1267 const FIntVector VoxelAtlasResolution = FIntVector(
1268 VoxelResolution.X * VoxelAtlasCount.X + TexturePackInterval * (VoxelAtlasCount.X - 1),
1269 VoxelResolution.Y * VoxelAtlasCount.Y + TexturePackInterval * (VoxelAtlasCount.Y - 1),
1270 VoxelResolution.Z * VoxelAtlasCount.Z + TexturePackInterval * (VoxelAtlasCount.Z - 1)
1271 );
1272 const FIntVector VoxelAtlasFXAAResolution = VoxelAtlasResolution * 1;
1273
1274 // Hole Atlas: 3D packing
1275 const FIntVector HoleAtlasResolution = FIntVector(
1276 HoleResolution.X * HoleAtlasCount.X + TexturePackInterval * (HoleAtlasCount.X - 1),
1277 HoleResolution.Y * HoleAtlasCount.Y + TexturePackInterval * (HoleAtlasCount.Y - 1),
1278 HoleResolution.Z * HoleAtlasCount.Z + TexturePackInterval * (HoleAtlasCount.Z - 1)
1279 );
1280
1281 // Create atlas textures
1282 FRDGTextureDesc VoxelAtlasDesc = FRDGTextureDesc::Create3D(
1283 VoxelAtlasResolution,
1284 PF_R32_FLOAT,
1285 FClearValueBinding::None,
1286 TexCreate_ShaderResource | TexCreate_UAV
1287 );
1288 FRDGTextureRef PackedVoxelAtlas = GraphBuilder.CreateTexture(VoxelAtlasDesc, TEXT("IVSmoke_PackedVoxelAtlas"));
1289
1290 FRDGTextureDesc VoxelAtlasFXAAResDesc = FRDGTextureDesc::Create3D(
1291 VoxelAtlasFXAAResolution,
1292 PF_R32_FLOAT,
1293 FClearValueBinding::None,
1294 TexCreate_ShaderResource | TexCreate_UAV
1295 );
1296 FRDGTextureRef PackedVoxelAtlasFXAA = GraphBuilder.CreateTexture(VoxelAtlasFXAAResDesc, TEXT("IVSmoke_PackedVoxelAtlasFXAA"));
1297
1298 // Clear Voxel Atlas textures - RDG may reuse textures from pool with stale data
1299 AddClearUAVPass(GraphBuilder, GraphBuilder.CreateUAV(PackedVoxelAtlas), 0.0f);
1300 AddClearUAVPass(GraphBuilder, GraphBuilder.CreateUAV(PackedVoxelAtlasFXAA), 0.0f);
1301
1302 FRDGTextureDesc HoleAtlasDesc = FRDGTextureDesc::Create3D(
1303 HoleAtlasResolution,
1304 PF_FloatRGBA,
1305 FClearValueBinding::None,
1306 TexCreate_ShaderResource | TexCreate_UAV
1307 );
1308 FRDGTextureRef PackedHoleAtlas = GraphBuilder.CreateTexture(HoleAtlasDesc, TEXT("IVSmoke_PackedHoleAtlas"));
1309
1310 // Clear Hole Atlas with alpha = 1 (so density is not zeroed when HoleTexture is missing)
1311 AddClearUAVPass(GraphBuilder, GraphBuilder.CreateUAV(PackedHoleAtlas), FLinearColor(0.0f, 0.0f, 0.0f, 1.0f));
1312
1313 // Copy Hole Textures to Atlas
1314 FRHICopyTextureInfo HoleCpyInfo;
1315 HoleCpyInfo.Size = HoleResolution;
1316 HoleCpyInfo.SourcePosition = FIntVector::ZeroValue;
1317
1318 for (int z = 0; z < HoleAtlasCount.Z; ++z)
1319 {
1320 for (int y = 0; y < HoleAtlasCount.Y; ++y)
1321 {
1322 for (int x = 0; x < HoleAtlasCount.X; ++x)
1323 {
1324 int i = x + HoleAtlasCount.X * y + z * HoleAtlasCount.X * HoleAtlasCount.Y;
1325
1326 if (i >= RenderData.HoleTextures.Num())
1327 {
1328 break;
1329 }
1330
1331 FTextureRHIRef SourceRHI = RenderData.HoleTextures[i];
1332 if (!SourceRHI)
1333 {
1334 continue;
1335 }
1336
1337 FRDGTextureRef SourceTexture = GraphBuilder.RegisterExternalTexture(
1338 CreateRenderTarget(SourceRHI, TEXT("IVSmoke_CopyHoleSource"))
1339 );
1340
1341 HoleCpyInfo.DestPosition.X = x * (HoleResolution.X + TexturePackInterval);
1342 HoleCpyInfo.DestPosition.Y = y * (HoleResolution.Y + TexturePackInterval);
1343 HoleCpyInfo.DestPosition.Z = z * (HoleResolution.Z + TexturePackInterval);
1344 AddCopyTexturePass(GraphBuilder, SourceTexture, PackedHoleAtlas, HoleCpyInfo);
1345 }
1346 }
1347 }
1348
1349 // Validate buffer data before creating GPU buffers
1350 if (RenderData.PackedVoxelBirthTimes.Num() == 0 || RenderData.VolumeDataArray.Num() == 0)
1351 {
1352 UE_LOG(LogIVSmoke, Warning, TEXT("[AddMultiVolumeRayMarchPass] Empty render data (BirthTimes=%d, VolumeCount=%d), skipping"),
1353 RenderData.PackedVoxelBirthTimes.Num(), RenderData.VolumeDataArray.Num());
1354 return;
1355 }
1356
1357 // Create GPU buffers
1358 FGlobalShaderMap* ShaderMap = GetGlobalShaderMap(View.FeatureLevel);
1359
1360 FRDGBufferDesc BirthBufferDesc = FRDGBufferDesc::CreateStructuredDesc(sizeof(float), RenderData.PackedVoxelBirthTimes.Num());
1361 FRDGBufferRef BirthBuffer = GraphBuilder.CreateBuffer(BirthBufferDesc, TEXT("IVSmoke_PackedBirthBuffer"));
1362 GraphBuilder.QueueBufferUpload(BirthBuffer, RenderData.PackedVoxelBirthTimes.GetData(), RenderData.PackedVoxelBirthTimes.Num() * sizeof(float));
1363
1364 FRDGBufferDesc DeathBufferDesc = FRDGBufferDesc::CreateStructuredDesc(sizeof(float), RenderData.PackedVoxelDeathTimes.Num());
1365 FRDGBufferRef DeathBuffer = GraphBuilder.CreateBuffer(DeathBufferDesc, TEXT("IVSmoke_PackedDeathBuffer"));
1366 GraphBuilder.QueueBufferUpload(DeathBuffer, RenderData.PackedVoxelDeathTimes.GetData(), RenderData.PackedVoxelDeathTimes.Num() * sizeof(float));
1367
1368 FRDGBufferDesc VolumeBufferDesc = FRDGBufferDesc::CreateStructuredDesc(sizeof(FIVSmokeVolumeGPUData), RenderData.VolumeDataArray.Num());
1369 FRDGBufferRef VolumeBuffer = GraphBuilder.CreateBuffer(VolumeBufferDesc, TEXT("IVSmokeVolumeDataBuffer"));
1370 GraphBuilder.QueueBufferUpload(VolumeBuffer, RenderData.VolumeDataArray.GetData(), RenderData.VolumeDataArray.Num() * sizeof(FIVSmokeVolumeGPUData));
1371
1372 // StructuredToTexture Pass
1373 TShaderMapRef<FIVSmokeStructuredToTextureCS> StructuredCopyShader(ShaderMap);
1374 auto* StructuredCopyParams = GraphBuilder.AllocParameters<FIVSmokeStructuredToTextureCS::FParameters>();
1375 StructuredCopyParams->Desti = GraphBuilder.CreateUAV(PackedVoxelAtlas);
1376 StructuredCopyParams->BirthTimes = GraphBuilder.CreateSRV(BirthBuffer);
1377 StructuredCopyParams->DeathTimes = GraphBuilder.CreateSRV(DeathBuffer);
1378 StructuredCopyParams->VolumeDataBuffer = GraphBuilder.CreateSRV(VolumeBuffer);
1379 StructuredCopyParams->TexSize = VoxelAtlasResolution;
1380 StructuredCopyParams->VoxelResolution = RenderData.VoxelResolution;
1381 StructuredCopyParams->PackedInterval = TexturePackInterval;
1382 StructuredCopyParams->VoxelAtlasCount = VoxelAtlasCount;
1383 StructuredCopyParams->GameTime = RenderData.GameTime;
1384 StructuredCopyParams->VolumeCount = VolumeCount;
1385
1387 GraphBuilder,
1388 ShaderMap,
1389 StructuredCopyShader,
1390 StructuredCopyParams,
1391 VoxelAtlasResolution
1392 );
1393
1394 // Voxel FXAA Pass
1395 TShaderMapRef<FIVSmokeVoxelFXAACS> VoxelFXAAShader(ShaderMap);
1396 auto* VoxelFXAAParams = GraphBuilder.AllocParameters<FIVSmokeVoxelFXAACS::FParameters>();
1397
1398 VoxelFXAAParams->Desti = GraphBuilder.CreateUAV(PackedVoxelAtlasFXAA);
1399 VoxelFXAAParams->Source = GraphBuilder.CreateSRV(PackedVoxelAtlas);
1400 VoxelFXAAParams->LinearBorder_Sampler = TStaticSamplerState<SF_Bilinear, AM_Border, AM_Border, AM_Border>::GetRHI();
1401 VoxelFXAAParams->TexSize = VoxelAtlasFXAAResolution;
1402 VoxelFXAAParams->FXAASpanMax = Settings->FXAASpanMax;
1403 VoxelFXAAParams->FXAARange = Settings->FXAARange;
1404 VoxelFXAAParams->FXAASharpness = Settings->FXAASharpness;
1405
1407 GraphBuilder,
1408 ShaderMap,
1409 VoxelFXAAShader,
1410 VoxelFXAAParams,
1411 VoxelAtlasFXAAResolution
1412 );
1413
1414 //~==========================================================================
1415 // Phase 1: Create Occupancy Resources
1416
1417 const FIntPoint TileCount = IVSmokeOccupancy::ComputeTileCount(ViewportSize);
1418 const uint32 StepSliceCount = IVSmokeOccupancy::ComputeStepSliceCount(RenderData.MaxSteps);
1419
1420 FIVSmokeOccupancyResources OccResources = IVSmokeOccupancy::CreateOccupancyResources(
1421 GraphBuilder,
1422 TileCount,
1423 StepSliceCount
1424 );
1425
1426 // Calculate GlobalAABB from all volumes
1427 FVector3f GlobalAABBMin(1e10f, 1e10f, 1e10f);
1428 FVector3f GlobalAABBMax(-1e10f, -1e10f, -1e10f);
1429 for (const FIVSmokeVolumeGPUData& VolData : RenderData.VolumeDataArray)
1430 {
1431 GlobalAABBMin = FVector3f::Min(GlobalAABBMin, VolData.VolumeWorldAABBMin);
1432 GlobalAABBMax = FVector3f::Max(GlobalAABBMax, VolData.VolumeWorldAABBMax);
1433 }
1434
1435 // MaxRayDistance: Maximum distance from camera to farthest GlobalAABB corner.
1436 // This ensures all volumes are rendered regardless of camera distance.
1437 // TODO: Consider adding configurable max render distance with distance-based fade.
1438 const FVector3f CameraPos(View.ViewLocation);
1439 float MaxRayDistance = 0.0f;
1440 for (int32 i = 0; i < 8; i++)
1441 {
1442 FVector3f Corner(
1443 (i & 1) ? GlobalAABBMax.X : GlobalAABBMin.X,
1444 (i & 2) ? GlobalAABBMax.Y : GlobalAABBMin.Y,
1445 (i & 4) ? GlobalAABBMax.Z : GlobalAABBMin.Z
1446 );
1447 MaxRayDistance = FMath::Max(MaxRayDistance, FVector3f::Dist(CameraPos, Corner));
1448 }
1449 MaxRayDistance = FMath::Clamp(MaxRayDistance, 10000.0f, 1000000.0f);
1450
1451 // MinStepSize from settings (minimum world units per step, TotalVolumeLength computed per-tile in shader)
1452 const float MinStepSize = Settings->GetEffectiveMinStepSize();
1453
1454 // Validate critical values
1455 if (TileCount.X <= 0 || TileCount.Y <= 0)
1456 {
1457 UE_LOG(LogIVSmoke, Error, TEXT("[AddMultiVolumeRayMarchPass] Invalid TileCount: %dx%d, aborting"), TileCount.X, TileCount.Y);
1458 return;
1459 }
1460 if (StepSliceCount == 0)
1461 {
1462 UE_LOG(LogIVSmoke, Error, TEXT("[AddMultiVolumeRayMarchPass] StepSliceCount is 0, aborting"));
1463 return;
1464 }
1465 if (!FMath::IsFinite(MaxRayDistance) || MaxRayDistance <= 0.0f)
1466 {
1467 UE_LOG(LogIVSmoke, Error, TEXT("[AddMultiVolumeRayMarchPass] Invalid MaxRayDistance: %.1f, aborting"), MaxRayDistance);
1468 return;
1469 }
1470
1471 //~==========================================================================
1472 // Phase 2: Pass 0 - Tile Setup
1473
1474 IVSmokeOccupancy::AddTileSetupPass(
1475 GraphBuilder,
1476 View,
1477 VolumeBuffer,
1478 RenderData.VolumeDataArray.Num(),
1479 OccResources.TileDataBuffer,
1480 TileCount,
1481 StepSliceCount,
1482 MaxRayDistance,
1483 ViewportSize,
1484 ViewRectMin
1485 );
1486
1487 //~==========================================================================
1488 // Phase 3: Pass 1 - Occupancy Build
1489
1490 IVSmokeOccupancy::AddOccupancyBuildPass(
1491 GraphBuilder,
1492 View,
1493 OccResources.TileDataBuffer,
1494 VolumeBuffer,
1495 RenderData.VolumeDataArray.Num(),
1496 OccResources.ViewOccupancy,
1497 OccResources.LightOccupancy,
1498 TileCount,
1499 StepSliceCount,
1500 FVector3f(RenderData.LightDirection),
1501 RenderData.LightMarchingDistance > 0.0f ? RenderData.LightMarchingDistance : MaxRayDistance,
1502 ViewportSize
1503 );
1504
1505 //~==========================================================================
1506 // Phase 4: Pass 2 - Ray March with Occupancy
1507
1508 TShaderMapRef<FIVSmokeMultiVolumeRayMarchCS> ComputeShader(ShaderMap);
1509 auto* Parameters = GraphBuilder.AllocParameters<FIVSmokeMultiVolumeRayMarchCS::FParameters>();
1510
1511 // Output (Dual Render Target)
1512 Parameters->SmokeAlbedoTex = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(SmokeAlbedoTex));
1513 Parameters->SmokeLocalPosAlphaTex = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(SmokeLocalPosAlphaTex));
1514 Parameters->SmokeWorldPosDepthTex = GraphBuilder.CreateUAV(FRDGTextureUAVDesc(SmokeWorldPosDepthTex));
1515
1516 // Occupancy inputs
1517 Parameters->TileDataBuffer = GraphBuilder.CreateSRV(OccResources.TileDataBuffer);
1518 Parameters->ViewOccupancy = GraphBuilder.CreateSRV(OccResources.ViewOccupancy);
1519 Parameters->LightOccupancy = GraphBuilder.CreateSRV(OccResources.LightOccupancy);
1520
1521 // Tile configuration
1522 Parameters->TileCount = TileCount;
1523 Parameters->StepSliceCount = StepSliceCount;
1524 Parameters->StepDivisor = FIVSmokeOccupancyConfig::StepDivisor;
1525
1526 // Noise Volume
1527 FTextureRHIRef TextureRHI = NoiseVolume->GetRenderTargetResource()->GetRenderTargetTexture();
1528 FRDGTextureRef NoiseVolumeRDG = GraphBuilder.RegisterExternalTexture(
1529 CreateRenderTarget(TextureRHI, TEXT("IVSmokeNoiseVolume"))
1530 );
1531 Parameters->NoiseVolume = NoiseVolumeRDG;
1532 Parameters->NoiseUVMul = FIVSmokeNoiseConfig::NoiseUVMul;
1533
1534 // Sampler
1535 Parameters->LinearBorder_Sampler = TStaticSamplerState<SF_Trilinear, AM_Border, AM_Border, AM_Border>::GetRHI();
1536 Parameters->LinearRepeat_Sampler = TStaticSamplerState<SF_Trilinear, AM_Wrap, AM_Wrap, AM_Wrap>::GetRHI();
1537
1538 // Time
1539 //Parameters->ElapsedTime = View.Family->Time.GetRealTimeSeconds();
1540 // Use per-world ServerTimeOffset for animation sync
1541 const float WorldServerTimeOffset = WorldData.IsValid() ? WorldData->ServerTimeOffset : 0.f;
1542 Parameters->ElapsedTime = View.Family->Time.GetRealTimeSeconds() + WorldServerTimeOffset;
1543
1544 // Viewport
1545 Parameters->TexSize = FIntPoint(TexSize.X, TexSize.Y);
1546 Parameters->ViewportSize = FVector2f(ViewportSize);
1547 Parameters->ViewRectMin = FVector2f(ViewRectMin);
1548
1549 // Camera
1550 const FViewMatrices& ViewMatrices = View.ViewMatrices;
1551 Parameters->CameraPosition = FVector3f(ViewMatrices.GetViewOrigin());
1552 Parameters->CameraForward = FVector3f(View.GetViewDirection());
1553 Parameters->CameraRight = FVector3f(View.GetViewRight());
1554 Parameters->CameraUp = FVector3f(View.GetViewUp());
1555
1556 const FMatrix& ProjMatrix = ViewMatrices.GetProjectionMatrix();
1557 float ProjM11 = ProjMatrix.M[1][1];
1558
1559 // Validate projection matrix to prevent inf/NaN
1560 if (FMath::IsNearlyZero(ProjM11, 1e-6f) || !FMath::IsFinite(ProjM11))
1561 {
1562 UE_LOG(LogIVSmoke, Error, TEXT("[RayMarch] Invalid ProjMatrix.M[1][1] = %f (likely orthographic viewport), skipping"), ProjM11);
1563 return; // Skip rendering for orthographic/invalid views
1564 }
1565
1566 Parameters->TanHalfFOV = 1.0f / ProjM11;
1567 Parameters->AspectRatio = (float)ViewportSize.X / (float)ViewportSize.Y;
1568
1569 // Ray Marching
1570 Parameters->MaxSteps = RenderData.MaxSteps;
1571 Parameters->MinStepSize = MinStepSize;
1572
1573 // Volume Data Buffer
1574 Parameters->VolumeDataBuffer = GraphBuilder.CreateSRV(VolumeBuffer);
1575 Parameters->NumActiveVolumes = RenderData.VolumeDataArray.Num();
1576
1577 // Packed Textures
1578 Parameters->PackedInterval = TexturePackInterval;
1579 Parameters->PackedVoxelAtlas = GraphBuilder.CreateSRV(PackedVoxelAtlasFXAA);
1580 Parameters->VoxelTexSize = VoxelResolution;
1581 Parameters->PackedVoxelTexSize = VoxelAtlasResolution;
1582 Parameters->VoxelAtlasCount = VoxelAtlasCount;
1583 Parameters->PackedHoleAtlas = GraphBuilder.CreateSRV(PackedHoleAtlas);
1584 Parameters->HoleTexSize = HoleResolution;
1585 Parameters->PackedHoleTexSize = HoleAtlasResolution;
1586 Parameters->HoleAtlasCount = HoleAtlasCount;
1587
1588 // Scene Textures
1589 Parameters->SceneTexturesStruct = GetSceneTextureShaderParameters(View).SceneTextures;
1590 Parameters->InvDeviceZToWorldZTransform = FVector4f(View.InvDeviceZToWorldZTransform);
1591
1592 // Explicit SceneDepth dependency for RDG ordering
1593 // When SceneDepthForDependency is provided (from Pre-pass), we use explicit texture binding
1594 // to ensure RDG sees the dependency and orders Ray March BEFORE Depth Write
1595 if (SceneDepthForDependency)
1596 {
1597 Parameters->SceneDepthTexture_RDGDependency = SceneDepthForDependency;
1598 Parameters->bUseExplicitSceneDepth = 1; // Pre-pass mode: use explicit texture
1599 }
1600 else
1601 {
1602 // Post-process mode: Create a dummy 1x1 texture to satisfy parameter validation
1603 // The shader will use SceneTexturesStruct.SceneDepthTexture instead (bUseExplicitSceneDepth=0)
1604 FRDGTextureDesc DummyDesc = FRDGTextureDesc::Create2D(
1605 FIntPoint(1, 1),
1606 PF_R32_FLOAT,
1607 FClearValueBinding::Black,
1608 TexCreate_ShaderResource
1609 );
1610 Parameters->SceneDepthTexture_RDGDependency = GraphBuilder.CreateTexture(DummyDesc, TEXT("IVSmoke_DummyDepth"));
1611 Parameters->bUseExplicitSceneDepth = 0; // Post-process mode: use uniform buffer (no RDG conflict)
1612 }
1613
1614 // View (for BlueNoise access)
1615 Parameters->View = View.ViewUniformBuffer;
1616
1617 // Global Smoke Parameters
1618 Parameters->GlobalAbsorption = RenderData.GlobalAbsorption;
1619 Parameters->SmokeSize = RenderData.SmokeSize;
1620 Parameters->WindDirection = FVector3f(RenderData.WindDirection);
1621 Parameters->VolumeRangeOffset = RenderData.VolumeRangeOffset;
1622 Parameters->VolumeEdgeNoiseFadeOffset = RenderData.VolumeEdgeNoiseFadeOffset;
1623 Parameters->VolumeEdgeFadeSharpness = RenderData.VolumeEdgeFadeSharpness;
1624
1625 // Rayleigh Scattering
1626 Parameters->LightDirection = FVector3f(RenderData.LightDirection);
1627 Parameters->LightColor = FVector3f(RenderData.LightColor.R, RenderData.LightColor.G, RenderData.LightColor.B);
1628 Parameters->ScatterScale = RenderData.bEnableScattering ? (RenderData.ScatterScale * RenderData.LightIntensity) : 0.0f;
1629 Parameters->ScatteringAnisotropy = RenderData.ScatteringAnisotropy;
1630
1631 // Self-Shadowing
1632 Parameters->LightMarchingSteps = RenderData.bEnableSelfShadowing ? RenderData.LightMarchingSteps : 0;
1633 Parameters->LightMarchingDistance = RenderData.LightMarchingDistance;
1634 Parameters->LightMarchingExpFactor = RenderData.LightMarchingExpFactor;
1635 Parameters->ShadowAmbient = RenderData.ShadowAmbient;
1636
1637 // Global AABB for per-pixel light march distance calculation
1638 Parameters->GlobalAABBMin = GlobalAABBMin;
1639 Parameters->GlobalAABBMax = GlobalAABBMax;
1640
1641 // External Shadowing (CSM)
1642 Parameters->ShadowDepthBias = RenderData.ShadowDepthBias;
1643 Parameters->ExternalShadowAmbient = RenderData.ExternalShadowAmbient;
1644 Parameters->NumCascades = RenderData.NumCascades;
1645 Parameters->CascadeBlendRange = RenderData.CascadeBlendRange;
1646 Parameters->CSMCameraPosition = FVector3f(ViewMatrices.GetViewOrigin());
1647 Parameters->bEnableVSM = RenderData.bEnableVSM ? 1 : 0;
1648 Parameters->VSMMinVariance = RenderData.VSMMinVariance;
1649 Parameters->VSMLightBleedingReduction = RenderData.VSMLightBleedingReduction;
1650
1651 // CSM cascade data
1652 for (int32 i = 0; i < 8; i++)
1653 {
1654 if (i < RenderData.NumCascades && i < RenderData.CSMViewProjectionMatrices.Num())
1655 {
1656 Parameters->CSMViewProjectionMatrices[i] = FMatrix44f(RenderData.CSMViewProjectionMatrices[i]);
1657 Parameters->CSMLightCameraPositions[i] = FVector4f(
1658 FVector3f(RenderData.CSMLightCameraPositions[i]),
1659 0.0f
1660 );
1661 Parameters->CSMLightCameraForwards[i] = FVector4f(
1662 FVector3f(RenderData.CSMLightCameraForwards[i]),
1663 0.0f
1664 );
1665 }
1666 else
1667 {
1668 Parameters->CSMViewProjectionMatrices[i] = FMatrix44f::Identity;
1669 Parameters->CSMLightCameraPositions[i] = FVector4f(0.0f, 0.0f, 0.0f, 0.0f);
1670 Parameters->CSMLightCameraForwards[i] = FVector4f(0.0f, 0.0f, -1.0f, 0.0f);
1671 }
1672 }
1673
1674 // Split distances
1675 {
1676 float SplitDists[8];
1677 for (int32 i = 0; i < 8; i++)
1678 {
1679 SplitDists[i] = (i < RenderData.CSMSplitDistances.Num()) ? RenderData.CSMSplitDistances[i] : 100000.0f;
1680 }
1681 Parameters->CSMSplitDistances[0] = FVector4f(SplitDists[0], SplitDists[1], SplitDists[2], SplitDists[3]);
1682 Parameters->CSMSplitDistances[1] = FVector4f(SplitDists[4], SplitDists[5], SplitDists[6], SplitDists[7]);
1683 }
1684
1685 // CSM texture arrays
1686 // NOTE: RegisterExternalTexture automatically handles duplicate registration within the same GraphBuilder.
1687 // Each ViewFamily has its own GraphBuilder, so we simply register textures every time.
1688 if (RenderData.NumCascades > 0)
1689 {
1690 const int32 CascadeCount = RenderData.NumCascades;
1691 const FIntPoint CascadeResolution = RenderData.CSMDepthTextures.Num() > 0 && RenderData.CSMDepthTextures[0].IsValid()
1692 ? FIntPoint(RenderData.CSMDepthTextures[0]->GetSizeXYZ().X, RenderData.CSMDepthTextures[0]->GetSizeXYZ().Y)
1693 : FIntPoint(512, 512);
1694
1695 // Create array textures for shader binding
1696 FRDGTextureDesc DepthArrayDesc = FRDGTextureDesc::Create2DArray(
1697 CascadeResolution,
1698 PF_R32_FLOAT,
1699 FClearValueBinding(FLinearColor(1.0f, 0.0f, 0.0f, 0.0f)),
1700 TexCreate_ShaderResource | TexCreate_UAV,
1701 CascadeCount
1702 );
1703 FRDGTextureRef CSMDepthArray = GraphBuilder.CreateTexture(DepthArrayDesc, TEXT("IVSmokeCSMDepthArray"));
1704
1705 FRDGTextureDesc VSMArrayDesc = FRDGTextureDesc::Create2DArray(
1706 CascadeResolution,
1707 PF_G32R32F,
1708 FClearValueBinding(FLinearColor(1.0f, 1.0f, 0.0f, 0.0f)),
1709 TexCreate_ShaderResource | TexCreate_UAV,
1710 CascadeCount
1711 );
1712 FRDGTextureRef CSMVSMArray = GraphBuilder.CreateTexture(VSMArrayDesc, TEXT("IVSmokeCSMVSMArray"));
1713
1714 AddClearUAVPass(GraphBuilder, GraphBuilder.CreateUAV(CSMDepthArray), FVector4f(1.0f, 0.0f, 0.0f, 0.0f));
1715 AddClearUAVPass(GraphBuilder, GraphBuilder.CreateUAV(CSMVSMArray), FVector4f(1.0f, 1.0f, 0.0f, 0.0f));
1716
1717 // VSM processing: once per frame per World (results stored in persistent RHI textures)
1718 const uint32 CurrentRenderFrameNumber = View.Family->FrameNumber;
1719 const int32 VSMBlurRadius = Settings ? Settings->VSMBlurRadius : 2;
1720 FIVSmokeVSMProcessor* VSMProcessorPtr = WorldData.IsValid() ? WorldData->VSMProcessor.Get() : nullptr;
1721 const bool bNeedVSMProcessing = RenderData.bEnableVSM && VSMProcessorPtr &&
1722 WorldData.IsValid() && WorldData->LastVSMProcessFrameNumber != CurrentRenderFrameNumber;
1723
1724 for (int32 i = 0; i < CascadeCount; i++)
1725 {
1726 if (i < RenderData.CSMDepthTextures.Num() && RenderData.CSMDepthTextures[i].IsValid())
1727 {
1728 // Register external texture (RDG handles duplicates within same GraphBuilder)
1729 FRDGTextureRef SourceDepth = GraphBuilder.RegisterExternalTexture(
1730 CreateRenderTarget(RenderData.CSMDepthTextures[i], TEXT("IVSmokeCSMDepthSource"))
1731 );
1732
1733 FRHICopyTextureInfo DepthCopyInfo;
1734 DepthCopyInfo.Size = FIntVector(CascadeResolution.X, CascadeResolution.Y, 1);
1735 DepthCopyInfo.SourcePosition = FIntVector::ZeroValue;
1736 DepthCopyInfo.DestPosition = FIntVector::ZeroValue;
1737 DepthCopyInfo.DestSliceIndex = i;
1738 DepthCopyInfo.NumSlices = 1;
1739 AddCopyTexturePass(GraphBuilder, SourceDepth, CSMDepthArray, DepthCopyInfo);
1740
1741 if (RenderData.bEnableVSM && i < RenderData.CSMVSMTextures.Num() && RenderData.CSMVSMTextures[i].IsValid())
1742 {
1743 FRDGTextureRef VSMTexture = GraphBuilder.RegisterExternalTexture(
1744 CreateRenderTarget(RenderData.CSMVSMTextures[i], TEXT("IVSmokeCSMVSMSource"))
1745 );
1746
1747 // Process VSM only once per frame (results persist in RHI texture)
1748 if (bNeedVSMProcessing && VSMProcessorPtr)
1749 {
1750 VSMProcessorPtr->Process(GraphBuilder, SourceDepth, VSMTexture, VSMBlurRadius);
1751 }
1752
1753 FRHICopyTextureInfo VSMCopyInfo;
1754 VSMCopyInfo.Size = FIntVector(CascadeResolution.X, CascadeResolution.Y, 1);
1755 VSMCopyInfo.SourcePosition = FIntVector::ZeroValue;
1756 VSMCopyInfo.DestPosition = FIntVector::ZeroValue;
1757 VSMCopyInfo.DestSliceIndex = i;
1758 VSMCopyInfo.NumSlices = 1;
1759 AddCopyTexturePass(GraphBuilder, VSMTexture, CSMVSMArray, VSMCopyInfo);
1760 }
1761 }
1762 }
1763
1764 // Mark VSM as processed for this frame
1765 if (bNeedVSMProcessing && WorldData.IsValid())
1766 {
1767 WorldData->LastVSMProcessFrameNumber = CurrentRenderFrameNumber;
1768 }
1769
1770 Parameters->CSMDepthTextureArray = CSMDepthArray;
1771 Parameters->CSMVSMTextureArray = CSMVSMArray;
1772 }
1773 else
1774 {
1775 FRDGTextureDesc DummyDepthArrayDesc = FRDGTextureDesc::Create2DArray(
1776 FIntPoint(1, 1),
1777 PF_R32_FLOAT,
1778 FClearValueBinding(FLinearColor(1.0f, 0.0f, 0.0f, 0.0f)),
1779 TexCreate_ShaderResource | TexCreate_UAV,
1780 1
1781 );
1782 FRDGTextureRef DummyDepthArray = GraphBuilder.CreateTexture(DummyDepthArrayDesc, TEXT("IVSmokeCSMDepthArrayDummy"));
1783 AddClearUAVPass(GraphBuilder, GraphBuilder.CreateUAV(DummyDepthArray), FVector4f(1.0f, 0.0f, 0.0f, 0.0f));
1784
1785 FRDGTextureDesc DummyVSMArrayDesc = FRDGTextureDesc::Create2DArray(
1786 FIntPoint(1, 1),
1787 PF_G32R32F,
1788 FClearValueBinding(FLinearColor(1.0f, 1.0f, 0.0f, 0.0f)),
1789 TexCreate_ShaderResource | TexCreate_UAV,
1790 1
1791 );
1792 FRDGTextureRef DummyVSMArray = GraphBuilder.CreateTexture(DummyVSMArrayDesc, TEXT("IVSmokeCSMVSMArrayDummy"));
1793 AddClearUAVPass(GraphBuilder, GraphBuilder.CreateUAV(DummyVSMArray), FVector4f(1.0f, 1.0f, 0.0f, 0.0f));
1794
1795 Parameters->CSMDepthTextureArray = DummyDepthArray;
1796 Parameters->CSMVSMTextureArray = DummyVSMArray;
1797 }
1798 Parameters->CSMSampler = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
1799
1800 // Temporal
1801 Parameters->FrameNumber = View.Family->FrameNumber;
1802 Parameters->JitterIntensity = 1.0f;
1803
1804 // Dispatch
1806 GraphBuilder,
1807 ShaderMap,
1808 ComputeShader,
1809 Parameters,
1810 FIntVector(TexSize.X, TexSize.Y, 1)
1811 );
1812}
1813
1814//~==============================================================================
1815// Stats Tracking
1816
1817void FIVSmokeRenderer::UpdateStatsIfNeeded(const FIVSmokePackedRenderData& RenderData, const FIntPoint& ViewportSize)
1818{
1819 const double CurrentTime = FPlatformTime::Seconds();
1820 if (CurrentTime - LastStatUpdateTime < 1.0)
1821 {
1822 return;
1823 }
1824 LastStatUpdateTime = CurrentTime;
1825
1826 // Calculate per-frame texture size
1827 CachedPerFrameSize = CalculatePerFrameTextureSize(
1828 ViewportSize,
1829 RenderData.VolumeCount,
1830 RenderData.VoxelResolution,
1831 RenderData.HoleResolution
1832 );
1833
1834 // Calculate CSM size using CalcTextureMemorySizeEnum (sum across all worlds)
1835 CachedCSMSize = 0;
1836 {
1837 FScopeLock Lock(&WorldDataMutex);
1838 for (const auto& Pair : WorldDataMap)
1839 {
1840 if (Pair.Value.IsValid() && Pair.Value->CSMRenderer && Pair.Value->CSMRenderer->IsInitialized())
1841 {
1842 const TArray<FIVSmokeCascadeData>& Cascades = Pair.Value->CSMRenderer->GetCascades();
1843 for (const FIVSmokeCascadeData& Cascade : Cascades)
1844 {
1845 if (Cascade.DepthRT)
1846 {
1847 CachedCSMSize += Cascade.DepthRT->CalcTextureMemorySizeEnum(TMC_ResidentMips);
1848 }
1849 if (Cascade.VSMRT)
1850 {
1851 CachedCSMSize += Cascade.VSMRT->CalcTextureMemorySizeEnum(TMC_ResidentMips);
1852 }
1853 }
1854 }
1855 }
1856 }
1857
1858 // Update all stats
1859 UpdateAllStats();
1860}
1861
1862int64 FIVSmokeRenderer::CalculatePerFrameTextureSize(
1863 const FIntPoint& ViewportSize,
1864 int32 VolumeCount,
1865 const FIntVector& VoxelResolution,
1866 const FIntVector& HoleResolution
1867) const
1868{
1869 if (VolumeCount == 0)
1870 {
1871 return 0;
1872 }
1873
1874 int64 TotalSize = 0;
1875
1876 // Half-resolution Smoke Albedo + Mask (PF_FloatRGBA)
1877 const FIntPoint HalfSize(FMath::Max(1, ViewportSize.X / 2), FMath::Max(1, ViewportSize.Y / 2));
1878 TotalSize += CalculateImageBytes(HalfSize.X, HalfSize.Y, 1, PF_FloatRGBA) * 2;
1879
1880 // Voxel Atlas: Use existing GetAtlasTexCount logic constants
1881 const int32 TexturePackInterval = 4;
1882 const int32 TexturePackMaxSize = 2048;
1883
1884 FIntVector VoxelAtlasCount = GetAtlasTexCount(VoxelResolution, VolumeCount, TexturePackInterval, TexturePackMaxSize);
1885 FIntVector VoxelAtlasResolution(
1886 VoxelResolution.X * VoxelAtlasCount.X + TexturePackInterval * (VoxelAtlasCount.X - 1),
1887 VoxelResolution.Y * VoxelAtlasCount.Y + TexturePackInterval * (VoxelAtlasCount.Y - 1),
1888 VoxelResolution.Z * VoxelAtlasCount.Z + TexturePackInterval * (VoxelAtlasCount.Z - 1)
1889 );
1890 // PackedVoxelAtlas (PF_R32_FLOAT) + PackedVoxelAtlasFXAA (PF_R32_FLOAT)
1891 TotalSize += CalculateImageBytes(VoxelAtlasResolution.X, VoxelAtlasResolution.Y, VoxelAtlasResolution.Z, PF_R32_FLOAT) * 2;
1892
1893 // Hole Atlas (PF_FloatRGBA)
1894 FIntVector HoleAtlasCount = GetAtlasTexCount(HoleResolution, VolumeCount, TexturePackInterval, TexturePackMaxSize);
1895 FIntVector HoleAtlasResolution(
1896 HoleResolution.X * HoleAtlasCount.X + TexturePackInterval * (HoleAtlasCount.X - 1),
1897 HoleResolution.Y * HoleAtlasCount.Y + TexturePackInterval * (HoleAtlasCount.Y - 1),
1898 HoleResolution.Z * HoleAtlasCount.Z + TexturePackInterval * (HoleAtlasCount.Z - 1)
1899 );
1900 TotalSize += CalculateImageBytes(HoleAtlasResolution.X, HoleAtlasResolution.Y, HoleAtlasResolution.Z, PF_FloatRGBA);
1901
1902 // Occupancy textures (View + Light): Use FIVSmokeOccupancyConfig constants
1903 const UIVSmokeSettings* Settings = UIVSmokeSettings::Get();
1904 if (Settings)
1905 {
1906 const FIntPoint TileCount(
1908 (ViewportSize.Y + FIVSmokeOccupancyConfig::TileSizeY - 1) / FIVSmokeOccupancyConfig::TileSizeY
1909 );
1911 // uint4 = 16 bytes per texel, 2 textures (View + Light)
1912 TotalSize += CalculateImageBytes(TileCount.X, TileCount.Y, StepSliceCount, PF_R32G32B32A32_UINT) * 2;
1913 }
1914
1915 return TotalSize;
1916}
1917
1918void FIVSmokeRenderer::UpdateAllStats()
1919{
1920 // Memory stats
1921 SET_MEMORY_STAT(STAT_IVSmoke_NoiseVolume, CachedNoiseVolumeSize);
1922 SET_MEMORY_STAT(STAT_IVSmoke_CSMShadowMaps, CachedCSMSize);
1923 SET_MEMORY_STAT(STAT_IVSmoke_PerFrameTextures, CachedPerFrameSize);
1924 SET_MEMORY_STAT(STAT_IVSmoke_TotalVRAM, CachedNoiseVolumeSize + CachedCSMSize + CachedPerFrameSize);
1925}
1926
1927//~==============================================================================
1928// Pre-Pass Pipeline (Ray March + Upscale + UpsampleFilter + Depth Write)
1929
1930bool FIVSmokeRenderer::IsPrimaryGameView(const FSceneView& View)
1931{
1932 // Filter out auxiliary views that should not render smoke
1933 // This is a policy decision, not a heuristic
1934 if (View.bIsSceneCapture || View.bIsPlanarReflection || View.bIsReflectionCapture)
1935 {
1936 return false;
1937 }
1938 return true;
1939}
1940
1942 FRDGBuilder& GraphBuilder,
1943 const FSceneView& View,
1944 const FRenderTargetBindingSlots& RenderTargets,
1945 TRDGUniformBufferRef<FSceneTextureUniformParameters> SceneTextures)
1946{
1947 // Get settings
1948 const UIVSmokeSettings* Settings = UIVSmokeSettings::Get();
1949 if (!Settings || !Settings->bEnableSmokeRendering)
1950 {
1951 return;
1952 }
1953
1954 // Filter out non-primary views (Scene Capture, Planar Reflection, etc.)
1955 // This is a policy decision for performance - smoke only renders in main game view
1956 if (!IsPrimaryGameView(View))
1957 {
1958 return;
1959 }
1960
1961 // View.State null check - required for cache key
1962 if (!View.State)
1963 {
1964 UE_LOG(LogIVSmoke, Verbose, TEXT("[FIVSmokeRenderer::RunPrePassPipeline] View.State is null, skipping Pre-pass"));
1965 return;
1966 }
1967
1968 // Extract World from View
1969 UWorld* World = nullptr;
1970 if (View.Family && View.Family->Scene)
1971 {
1972 World = View.Family->Scene->GetWorld();
1973 }
1974
1975 if (!World)
1976 {
1977 return;
1978 }
1979
1980 // Get per-world data
1981 TSharedPtr<FPerWorldData> WorldData = GetWorldData(World);
1982 if (!WorldData.IsValid())
1983 {
1984 return;
1985 }
1986
1987 // Get cached render data for this World
1988 FIVSmokePackedRenderData RenderData;
1989 {
1990 FScopeLock Lock(&WorldDataMutex);
1991 RenderData = WorldData->CachedRenderData;
1992 }
1993
1994 // Early out if no valid render data
1995 if (!RenderData.bIsValid || RenderData.VolumeCount == 0)
1996 {
1997 return;
1998 }
1999
2000 // Ensure renderer is initialized
2001 if (!NoiseVolume)
2002 {
2003 return;
2004 }
2005
2006 // Get the scaled ViewRect using public API
2007 // This matches SceneColor.ViewRect in Post-process (scaled by Primary Screen Percentage)
2008 const FIntRect ScaledViewRect = UE::FXRenderingUtils::GetRawViewRectUnsafe(View);
2009
2010 RDG_EVENT_SCOPE(GraphBuilder, "IVSmoke_PrePassPipeline");
2011
2012 // Use scaled ViewRect - matches Post-process SceneColor.ViewRect
2013 const FIntRect PrePassViewRect = ScaledViewRect;
2014 const FIntPoint ViewportSize = PrePassViewRect.Size();
2015 const FIntPoint ViewRectMin = PrePassViewRect.Min;
2016
2017 // Validate viewport size
2018 if (ViewportSize.X <= 0 || ViewportSize.Y <= 0)
2019 {
2020 UE_LOG(LogIVSmoke, Error, TEXT("[RunPrePassPipeline] Invalid ViewportSize: %dx%d, skipping"), ViewportSize.X, ViewportSize.Y);
2021 return;
2022 }
2023
2024 //~==========================================================================
2025 // Get or create per-view data (thread-safe)
2026 // RDG textures are valid within the same GraphBuilder (same ViewFamily's frame)
2027 FPerViewData* ViewDataPtr = nullptr;
2028 {
2029 FScopeLock Lock(&ViewDataMutex);
2030 ViewDataPtr = &ViewDataMap.FindOrAdd(View.State);
2031 }
2032 FPerViewData& ViewData = *ViewDataPtr;
2033
2034 //~==========================================================================
2035 // Resolution Setup
2036 const FIntPoint HalfSize = FIntPoint(
2037 FMath::Max(1, ViewportSize.X / 2),
2038 FMath::Max(1, ViewportSize.Y / 2)
2039 );
2040
2041 //~==========================================================================
2042 // Create textures at full resolution for cache
2043 FRDGTextureDesc FullResDesc = FRDGTextureDesc::Create2D(
2044 ViewportSize, PF_FloatRGBA, FClearValueBinding::Black,
2045 TexCreate_RenderTargetable | TexCreate_ShaderResource | TexCreate_UAV
2046 );
2047
2048 ViewData.SmokeTex = GraphBuilder.CreateTexture(FullResDesc, TEXT("IVSmoke_SmokeTex"));
2049 ViewData.LocalPosAlphaTex = GraphBuilder.CreateTexture(FullResDesc, TEXT("IVSmoke_LocalPosAlphaTex"));
2050 ViewData.WorldPosDepthTex = GraphBuilder.CreateTexture(FullResDesc, TEXT("IVSmoke_WorldPosDepthTex"));
2051
2052 //~==========================================================================
2053 // Create temporary textures at 1/2 resolution
2054 FRDGTextureDesc HalfResDesc = FRDGTextureDesc::Create2D(
2055 HalfSize, PF_FloatRGBA, FClearValueBinding::Black,
2056 TexCreate_RenderTargetable | TexCreate_ShaderResource | TexCreate_UAV
2057 );
2058
2059 FRDGTextureRef SmokeAlbedoHalf = GraphBuilder.CreateTexture(HalfResDesc, TEXT("IVSmokeAlbedoTex_Half_PrePass"));
2060 FRDGTextureRef SmokeLocalPosAlphaHalf = GraphBuilder.CreateTexture(HalfResDesc, TEXT("IVSmokeLocalPosAlphaTex_Half_PrePass"));
2061 FRDGTextureRef SmokeWorldPosDepthHalf = GraphBuilder.CreateTexture(HalfResDesc, TEXT("IVSmokeWorldPosDepthTex_Half_PrePass"));
2062
2063 //~==========================================================================
2064 // Ray March Pass (1/2 Resolution)
2065 // Pass SceneDepth explicitly to create RDG dependency
2066 FRDGTextureRef SceneDepthTexture = RenderTargets.DepthStencil.GetTexture();
2067 AddMultiVolumeRayMarchPass(
2068 GraphBuilder, View, RenderData,
2069 SmokeAlbedoHalf, SmokeLocalPosAlphaHalf, SmokeWorldPosDepthHalf,
2070 HalfSize, ViewportSize, ViewRectMin,
2071 SceneDepthTexture // Explicit RDG dependency
2072 );
2073
2074 //~==========================================================================
2075 // Upscaling (1/2 to Full) - Copy to cache textures
2076 FRDGTextureRef SmokeAlbedoFull = AddCopyPass(
2077 GraphBuilder, View, SmokeAlbedoHalf, ViewportSize, TEXT("IVSmokeAlbedoTex_Full_PrePass")
2078 );
2079 AddCopyPass(GraphBuilder, View, SmokeLocalPosAlphaHalf, ViewData.LocalPosAlphaTex);
2080 AddCopyPass(GraphBuilder, View, SmokeWorldPosDepthHalf, ViewData.WorldPosDepthTex);
2081
2082 //~==========================================================================
2083 // Upsample Filter Pass
2084 FRDGTextureRef FilteredSmokeTex = AddUpsampleFilterPass(
2085 GraphBuilder, RenderData, View,
2086 SmokeAlbedoFull, // Dummy for SceneTex (not used in output)
2087 SmokeAlbedoFull, ViewData.LocalPosAlphaTex,
2088 ViewportSize, ViewRectMin
2089 );
2090
2091 // Copy filtered result to cached SmokeTex
2092 AddCopyPass(GraphBuilder, View, FilteredSmokeTex, ViewData.SmokeTex);
2093
2094 //~==========================================================================
2095 // Depth Write Pass (optional)
2096 if (Settings->bEnableDepthWrite)
2097 {
2098 ExecuteDepthWrite(
2099 GraphBuilder, View, RenderTargets,
2100 ViewData.WorldPosDepthTex, ViewData.LocalPosAlphaTex,
2101 ViewportSize, ViewRectMin
2102 );
2103 }
2104
2105 //~==========================================================================
2106 // Mark ViewData as Valid
2107 ViewData.ViewportSize = ViewportSize;
2108 ViewData.ViewRectMin = ViewRectMin;
2109 ViewData.bIsValid = true;
2110}
2111
2112void FIVSmokeRenderer::ExecuteDepthWrite(
2113 FRDGBuilder& GraphBuilder,
2114 const FSceneView& View,
2115 const FRenderTargetBindingSlots& RenderTargets,
2116 FRDGTextureRef SmokeWorldPosDepthTex,
2117 FRDGTextureRef SmokeLocalPosAlphaTex,
2118 const FIntPoint& ViewportSize,
2119 const FIntPoint& ViewRectMin)
2120{
2121 RDG_EVENT_SCOPE(GraphBuilder, "IVSmoke_DepthWrite");
2122
2123 // Get the depth stencil target
2124 FRDGTextureRef DepthTexture = RenderTargets.DepthStencil.GetTexture();
2125 if (!DepthTexture)
2126 {
2127 return;
2128 }
2129
2130 const UIVSmokeSettings* Settings = UIVSmokeSettings::Get();
2131
2132 // Get shader
2133 FGlobalShaderMap* ShaderMap = GetGlobalShaderMap(View.FeatureLevel);
2134 TShaderMapRef<FIVSmokeDepthWritePS> PixelShader(ShaderMap);
2135
2136 // Setup parameters
2137 FIVSmokeDepthWritePS::FParameters* Parameters = GraphBuilder.AllocParameters<FIVSmokeDepthWritePS::FParameters>();
2138
2139 Parameters->SmokeWorldPosDepthTex = SmokeWorldPosDepthTex;
2140 Parameters->SmokeLocalPosAlphaTex = SmokeLocalPosAlphaTex;
2141 Parameters->LinearClampSampler = TStaticSamplerState<SF_Bilinear, AM_Clamp, AM_Clamp, AM_Clamp>::GetRHI();
2142
2143 // Camera parameters
2144 Parameters->CameraForward = FVector3f(View.GetViewDirection());
2145 Parameters->CameraOrigin = FVector3f(View.ViewMatrices.GetViewOrigin());
2146
2147 // ViewRect parameters for UV calculation
2148 Parameters->ViewportSize = FVector2f(ViewportSize);
2149 Parameters->ViewRectMin = FVector2f(ViewRectMin);
2150
2151 // Depth bias from settings
2152 Parameters->DepthBias = Settings ? Settings->DepthWriteBias : 0.0f;
2153
2154 // Alpha threshold from settings (default 0.99 for nearly opaque pixels)
2155 Parameters->AlphaThreshold = Settings ? Settings->DepthWriteAlphaThreshold : 0.99f;
2156
2157 // Projection matrix parameters for manual depth conversion
2158 // ConvertToDeviceZ formula: DeviceZ = ViewToClip[2][2] + ViewToClip[3][2] / LinearZ
2159 const FMatrix& ViewToClip = View.ViewMatrices.GetProjectionMatrix();
2160 Parameters->ViewToClip_22 = ViewToClip.M[2][2];
2161 Parameters->ViewToClip_32 = ViewToClip.M[3][2];
2162
2163 // Bind depth for writing
2164 Parameters->RenderTargets.DepthStencil = FDepthStencilBinding(
2165 DepthTexture,
2166 ERenderTargetLoadAction::ELoad,
2167 ERenderTargetLoadAction::ELoad,
2168 FExclusiveDepthStencil::DepthWrite_StencilNop
2169 );
2170
2171 // Get ViewRect for fullscreen pass
2172 FIntRect ViewRect = View.UnscaledViewRect;
2173
2174 // Add fullscreen pass with depth write
2175 // BlendState = nullptr (no color output), RasterizerState = nullptr (default)
2176 // DepthStencilState: Enable depth write, always pass
2177 FPixelShaderUtils::AddFullscreenPass(
2178 GraphBuilder,
2179 ShaderMap,
2180 RDG_EVENT_NAME("IVSmoke_DepthWritePS"),
2181 PixelShader,
2182 Parameters,
2183 ViewRect,
2184 nullptr,
2185 nullptr,
2186 TStaticDepthStencilState<true, CF_Always>::GetRHI()
2187 );
2188}
2189
2190#endif
FORCEINLINE const UIVSmokeSmokePreset * GetSmokePresetOverride() const
FORCEINLINE FVector GetVoxelWorldAABBMax() const
FORCEINLINE float GetVoxelSize() const
TObjectPtr< UIVSmokeHoleGeneratorComponent > GetHoleGeneratorComponent()
FORCEINLINE const TArray< float > & GetVoxelBirthTimes() const
FORCEINLINE FVector GetVoxelWorldAABBMin() const
FORCEINLINE FIntVector GetGridResolution() const
FORCEINLINE FIntVector GetCenterOffset() const
FORCEINLINE const TArray< float > & GetVoxelDeathTimes() const
static void AddComputeShaderPass(FRDGBuilder &GraphBuilder, FGlobalShaderMap *ShaderMap, TShaderMapRef< TShaderClass > ComputeShader, typename TShaderClass::FParameters *Parameters, const FIntVector &TotalThreadSize)
static FRDGTextureRef CreateOutputTexture(FRDGBuilder &GraphBuilder, FRDGTextureRef SourceTexture, const TCHAR *DebugName=, EPixelFormat OverrideFormat=PF_Unknown, FIntPoint OverrideExtent=FIntPoint::ZeroValue, ETextureCreateFlags Flags=ETextureCreateFlags::UAV)
void SetServerTimeOffset(UWorld *World, float InServerTimeOffset)
TSharedPtr< FPerWorldData > GetWorldData(UWorld *World)
static constexpr int32 MaxSupportedVolumes
FScreenPassTexture Render(FRDGBuilder &GraphBuilder, const FSceneView &View, const FPostProcessMaterialInputs &Inputs)
TSharedPtr< FPerWorldData > GetOrCreateWorldData(UWorld *World)
void RunPrePassPipeline(FRDGBuilder &GraphBuilder, const FSceneView &View, const struct FRenderTargetBindingSlots &RenderTargets, TRDGUniformBufferRef< FSceneTextureUniformParameters > SceneTextures)
void SetCachedRenderData(UWorld *World, FIVSmokePackedRenderData &&InRenderData)
bool IsInitialized() const
FIVSmokePackedRenderData PrepareRenderData(UWorld *World, const TArray< AIVSmokeVoxelVolume * > &InVolumes, const FVector &CameraPosition)
bool bIsServerTimeSynced(UWorld *World)
void CleanupWorldData(UWorld *World)
void Process(FRDGBuilder &GraphBuilder, FRDGTextureRef DepthTexture, FRDGTextureRef VSMTexture, int32 BlurRadius)
Component that generates hole texture for volumetric smoke. Provides public API for penetration and e...
int32 GetEffectiveNumCascades() const
int32 GetEffectiveCascadeResolution() const
static const UIVSmokeSettings * Get()
bool IsSelfShadowingEnabled() const
float GetEffectiveMinStepSize() const
int32 GetEffectiveMaxSteps() const
FLinearColor LightColorOverride
float GetEffectiveShadowMaxDistance() const
bool IsExternalShadowingEnabled() const
FVector LightDirectionOverride
int32 GetEffectiveLightMarchingSteps() const
TObjectPtr< UMaterialInterface > SmokeVisualMaterial
EIVSmokeUpSampleFilterType UpSampleFilterType
static constexpr float NoiseUVMul
static constexpr uint32 TileSizeX
static constexpr uint32 StepDivisor
TArray< float > PackedVoxelBirthTimes
TArray< float > PackedVoxelDeathTimes
TArray< FIVSmokeVolumeGPUData > VolumeDataArray
TArray< FTextureRHIRef > HoleTextures
TArray< FTextureRHIRef > CSMDepthTextures
UMaterialInterface * SmokeVisualMaterial
uint32 LastCSMUpdateFrameNumber
TUniquePtr< FIVSmokeVSMProcessor > VSMProcessor
TUniquePtr< FIVSmokeCSMRenderer > CSMRenderer
uint32 LastVSMProcessFrameNumber