IVSmoke 1.0
Loading...
Searching...
No Matches
IVSmokeSceneViewExtension.cpp
1// Copyright (c) 2026, Team SDB. All rights reserved.
2
3#include "IVSmokeSceneViewExtension.h"
4#include "IVSmokeRenderer.h"
5#include "IVSmokeSettings.h"
6#include "IVSmokeShaders.h"
7#include "IVSmokeVoxelVolume.h"
8#include "PostProcess/PostProcessMaterialInputs.h"
9#include "ScreenPass.h"
10#include "RenderingThread.h"
11#include "EngineUtils.h"
12#include "GameFramework/GameStateBase.h"
13#include "PixelShaderUtils.h"
14
15#if WITH_EDITOR
16#include "Editor.h"
17#endif
18#include "SceneTexturesConfig.h"
19#include "SceneRenderTargetParameters.h"
20
21TSharedPtr<FIVSmokeSceneViewExtension, ESPMode::ThreadSafe> FIVSmokeSceneViewExtension::Instance;
22
23FIVSmokeSceneViewExtension::FIVSmokeSceneViewExtension(const FAutoRegister& AutoRegister)
24 : FSceneViewExtensionBase(AutoRegister)
25{
26}
27
29{
30 if (!Instance.IsValid())
31 {
32 Instance = FSceneViewExtensions::NewExtension<FIVSmokeSceneViewExtension>();
33 }
34}
35
37{
38 Instance.Reset();
39}
40
41void FIVSmokeSceneViewExtension::BeginRenderViewFamily(FSceneViewFamily& InViewFamily)
42{
43 // Called ONCE per ViewFamily on Game Thread
44 // Per-World architecture: Each World is processed independently
45 // Multiple ViewFamilies (Editor, PIE) can coexist without conflict
46 FIVSmokeRenderer& Renderer = FIVSmokeRenderer::Get();
47
48 // Get world from ViewFamily
49 UWorld* World = nullptr;
50 if (InViewFamily.Scene)
51 {
52 World = InViewFamily.Scene->GetWorld();
53 }
54
55 if (!World)
56 {
57 return;
58 }
59
60#if WITH_EDITOR
61 // Skip Editor world rendering when PIE is active
62 // When playing in editor, user focuses on PIE viewport - no need to render smoke in editor viewports
63 // This significantly improves performance by avoiding redundant ray marching in 4 editor viewports
64 if (GEditor && GEditor->IsPlayingSessionInEditor())
65 {
66 if (World->WorldType == EWorldType::Editor)
67 {
68 // Clear any stale cached render data for Editor world
69 ENQUEUE_RENDER_COMMAND(IVSmokeClearEditorRenderData)(
70 [&Renderer, World](FRHICommandListImmediate& RHICmdList)
71 {
73 }
74 );
75 return;
76 }
77 }
78#endif
79
80 // Sync server time if needed (per-world)
81 if (!Renderer.bIsServerTimeSynced(World))
82 {
83 if (AGameStateBase* GS = World->GetGameState())
84 {
85 float LocalTime = World->GetTimeSeconds();
86 float ServerTime = GS->GetServerWorldTimeSeconds();
87 Renderer.SetServerTimeOffset(World, ServerTime - LocalTime);
88 }
89 }
90
91 // Collect renderable volumes using TActorIterator (Pull-based pattern)
92 TArray<AIVSmokeVoxelVolume*> ValidVolumes;
93 for (TActorIterator<AIVSmokeVoxelVolume> It(World); It; ++It)
94 {
95 if (IsValid(*It) && It->ShouldRender())
96 {
97 ValidVolumes.Add(*It);
98 }
99 }
100
101 if (ValidVolumes.Num() == 0)
102 {
103 // Clear cached render data for this World to stop rendering
104 ENQUEUE_RENDER_COMMAND(IVSmokeClearRenderData)(
105 [&Renderer, World](FRHICommandListImmediate& RHICmdList)
106 {
108 }
109 );
110 return;
111 }
112
113 // Get camera position from first view for distance-based filtering
114 FVector CameraPosition = FVector::ZeroVector;
115 if (InViewFamily.Views.Num() > 0 && InViewFamily.Views[0])
116 {
117 CameraPosition = InViewFamily.Views[0]->ViewLocation;
118 }
119
120 // Prepare render data on Game Thread for this World
121 // PrepareRenderData includes frame deduplication (skips if already processed this frame)
122 FIVSmokePackedRenderData RenderData = Renderer.PrepareRenderData(World, ValidVolumes, CameraPosition);
123
124 // Transfer to Render Thread via command queue
125 ENQUEUE_RENDER_COMMAND(IVSmokeSetRenderData)(
126 [&Renderer, World, RenderData = MoveTemp(RenderData)](FRHICommandListImmediate& RHICmdList) mutable
127 {
128 Renderer.SetCachedRenderData(World, MoveTemp(RenderData));
129 }
130 );
131}
132
133bool FIVSmokeSceneViewExtension::IsActiveThisFrame_Internal(const FSceneViewExtensionContext& Context) const
134{
135 // Actual filtering happens in SubscribeToPostProcessingPass where we have access to FSceneView
136 return true;
137}
138
139void FIVSmokeSceneViewExtension::SubscribeToPostProcessingPass(
140 EPostProcessingPass Pass,
141 const FSceneView& InView,
142 FPostProcessingPassDelegateArray& InOutPassCallbacks,
143 bool bIsPassEnabled)
144{
145 // Skip non-primary views (Scene Capture, Reflection Capture, Planar Reflection, etc.)
146 // These views may have invalid projection matrices or extreme FOV values that cause GPU hangs
147 if (InView.bIsSceneCapture || InView.bIsReflectionCapture || InView.bIsPlanarReflection)
148 {
149 return;
150 }
151
152 // Always use AfterDOF pass - DOF applied to smoke, best balance of quality and compatibility
153 if (Pass == EPostProcessingPass::AfterDOF)
154 {
155 InOutPassCallbacks.Add(
156 FPostProcessingPassDelegate::CreateRaw(
157 this,
158 &FIVSmokeSceneViewExtension::Render_RenderThread
159 )
160 );
161 }
162}
163
164FScreenPassTexture FIVSmokeSceneViewExtension::Render_RenderThread(
165 FRDGBuilder& GraphBuilder,
166 const FSceneView& View,
167 const FPostProcessMaterialInputs& Inputs)
168{
169 return FIVSmokeRenderer::Get().Render(GraphBuilder, View, Inputs);
170}
171
172void FIVSmokeSceneViewExtension::PostRenderBasePassDeferred_RenderThread(
173 FRDGBuilder& GraphBuilder,
174 FSceneView& InView,
175 const FRenderTargetBindingSlots& RenderTargets,
176 TRDGUniformBufferRef<FSceneTextureUniformParameters> SceneTextures)
177{
178 // Skip non-primary views (Scene Capture, Reflection Capture, Planar Reflection, etc.)
179 // These views may have invalid projection matrices or extreme FOV values that cause GPU hangs
180 if (InView.bIsSceneCapture || InView.bIsReflectionCapture || InView.bIsPlanarReflection)
181 {
182 return;
183 }
184
185 const UIVSmokeSettings* Settings = UIVSmokeSettings::Get();
186 if (!Settings)
187 {
188 return;
189 }
190
191 // Pre-pass Pipeline: Ray March → Upscale → UpsampleFilter (→ Depth Write if enabled)
192 // Always runs when smoke rendering is enabled.
193 // Results are cached in View-based RDG cache for Post-process Visual/Composite passes.
194 if (Settings->bEnableSmokeRendering)
195 {
196 FIVSmokeRenderer::Get().RunPrePassPipeline(GraphBuilder, InView, RenderTargets, SceneTextures);
197 }
198}
199
200void FIVSmokeSceneViewExtension::PostRenderViewFamily_RenderThread(
201 FRDGBuilder& GraphBuilder,
202 FSceneViewFamily& InViewFamily)
203{
204 // Clear per-view data for this specific ViewFamily only
205 // Each ViewFamily has its own GraphBuilder, so only clear data belonging to this ViewFamily
206 // This prevents race conditions when multiple ViewFamilies render simultaneously (e.g., Editor split viewports)
207 FIVSmokeRenderer::Get().ClearViewDataForViewFamily(InViewFamily);
208}
void SetServerTimeOffset(UWorld *World, float InServerTimeOffset)
FScreenPassTexture Render(FRDGBuilder &GraphBuilder, const FSceneView &View, const FPostProcessMaterialInputs &Inputs)
void RunPrePassPipeline(FRDGBuilder &GraphBuilder, const FSceneView &View, const struct FRenderTargetBindingSlots &RenderTargets, TRDGUniformBufferRef< FSceneTextureUniformParameters > SceneTextures)
void SetCachedRenderData(UWorld *World, FIVSmokePackedRenderData &&InRenderData)
FIVSmokePackedRenderData PrepareRenderData(UWorld *World, const TArray< AIVSmokeVoxelVolume * > &InVolumes, const FVector &CameraPosition)
bool bIsServerTimeSynced(UWorld *World)
void ClearViewDataForViewFamily(const FSceneViewFamily &ViewFamily)
static const UIVSmokeSettings * Get()