# 屏幕空间伪 2D 流体 - 类 Control 效果

# 前言

流体这东西一直很让人着迷,在游戏特效中人们也经常用各种贴图来模拟魔法流动的效果,现在介绍一种做法可以实现伪流体解算同时消耗还低效果不错。

为什么说是伪流体解算呢,因为他跟传统的流体解算对比,他只走了一半流程,把最耗时的部分给去掉了,一般对于传统流体解算而言流程是这样的:

  1. 继承上一帧,设置发射源,一系列力的叠加这些
  2. 平流,就是用速度去搬运速度
  3. 算散度,解线性方程组算出压力,有很多种解法
  4. 用当前像素的速度叠加上压力算出新速度,然后平移密度得到最终图像

其中消耗最高的就是第三步,其他几步都是一些像素的位移花费不了多少时间,在解算压力上通常要迭代几十次才有好看的效果,也可以像我上一篇文章一样转化到频率空间去算,但总体来说消耗依然很高,对于视角效果而已,少了这一步只是少了因为向前空气的阻力逼迫你流体向后运动而出现的漩涡一样的东西。随着时间漩涡表现会很明显,从局部的小漩涡到大漩涡。但!对于特效而言我们并不需要烟雾存活很久,而且只需要他的一个小尾巴就足够了,看上去就有点魔法气息。

# 实现

所以我们修改上面步骤把,第三步去掉,因为第三步去掉了,所以第四步就没必要存在,直接简化成

  1. 继承上一帧,设置发射源,一系列力的叠加这些
  2. 平流,就是用速度去搬运速度和密度

当然,解算压力阶段可以保留,保持迭代少一点,这样消耗快一点,但实现后觉得效果不太明显还是占了大部分消耗,所以对于我而言还是去掉了

# 解算

为了模拟烟湍流的效果,直接采样一张 noise 图作为力叠加上去,当然在传统流体解算时候也会加上这个 trick。所以在第一步的时候我们力叠加就是重力(可以省略)、风力、湍流、vorticity(涡度)。然后我们用模板作为发射源,可以在场景随意调用。还有因为屏幕空间,你的解算是在屏幕空间上,所以采样时要像 TAA 还原上一帧屏幕空间位置。但即使是这样,还是会出现拖拽残影,这时候就当迷幻风格,像 control 效果一样 wwww。在这里我们加上点视觉效果,用 scenecolor 做一个色散处理写入另一张贴图供后续做流动色彩,色散处理简单用分通道做偏移,用 ue 的 cs 写法就是

第一步:

""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
void PreVelocity(uint2 DispatchThreadId : SV_DispatchThreadID)
{
//DispatchThreadId 像素序号
float2 ViewportUV = float2(DispatchThreadId+0.5) / GridSize.xy;//SvPositionToBufferUV(float4(DispatchThreadId+0.5,0,0) * DownScale);
float DeviceZ = LookupDeviceZ(ViewportUV);//这些函数在ScnenTextureCommon.ush里面,include就行
float2 ScreenPosition = ViewportUVToScreenPos(ViewportUV);
//转换到clip space
float4 ThisClip = float4(ScreenPosition, DeviceZ, 1);
float4 PrevClip = mul(ThisClip, View.ClipToPrevClip);//ue自带矩阵
//从clip space转化为NDC space
float2 PrevScreen = PrevClip.xy / PrevClip.w;
//移动向量
float2 BackN = ScreenPosition - PrevScreen;
float2 PreVelocity = float2(BackN.x, -BackN.y) *0.5;
float4 SimValue = SimGridSRV.SampleLevel(SimSampler,ViewportUV - PreVelocity,0);

float2 Velocity = SimValue.xy;
float Density = SimValue.z;
float2 Force = 0;

float noiseScale = .05 * NoiseFrequency;
float noiseIntensity = NoiseIntensity;
//Noise可以采样贴图也可以随便上网找函数或者,在ue Random.ush也有随机函数库
float noiseX = simplex3d(float3(DispatchThreadId.xy, Time * 0.1) * noiseScale);
float noiseY = simplex3d(float3(DispatchThreadId.xy, Time * 0.05) * noiseScale);
float3 noise = float3(noiseX,noiseY,1) ;
float4 NoiseTexture = InTexture.SampleLevel(InTextureSampler,ViewportUV * NoiseFrequency*2.f,0);

uint Stenlic = CalcSceneCustomStencil(DispatchThreadId / DownScale);
//source
if (Stenlic == 1u)
{
Density = 1;
Velocity = noise * NoiseIntensity;
//色散处理
float Dispersion = CalcSceneColor(ViewportUV - PreVelocity + float2(10,0) * View.ViewSizeAndInvSize.zw).r;
float Dispersion1 = CalcSceneColor(ViewportUV - PreVelocity ).g;
float Dispersion2 = CalcSceneColor(ViewportUV - PreVelocity - float2(10,0) * View.ViewSizeAndInvSize.zw).b;
//叠加上半透明pass提高视觉效果
float3 Translucency = TranslucencyTexture.SampleLevel(InTextureSampler,ViewportUV - PreVelocity,0).xyz;
FluidColorUAV[DispatchThreadId] = Translucency + max(float3(Dispersion,Dispersion1,Dispersion2), NoiseTexture.xyz*0.5);//
}

//Vorticity force
float2 Grad = GetGradient(SimGridSRV, int2(DispatchThreadId), dx);
float GradCurlLength = length(Grad);
if (GradCurlLength > 1e-5)
{
Force += cross(float3(Grad / GradCurlLength, 0), float3(0, 0, 1)).xy;
}

Force += float2(G,0.5 * -G) * Density * GravityScale * 0.025;

Force += noise.xy * noiseIntensity;
Velocity += Force * dt;

SimGridUAV[DispatchThreadId] = float4(Velocity.x, Velocity.y, Density, 1);
}

第二步:位移色彩和速度

""
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void Advection(uint2 DispatchThreadId : SV_DispatchThreadID, uint2 GroupThreadId)
{
//因为可能存在降分辨率,所以不用View.ViewSizeAndInvSize用自己的GridSize
float2 UV = float2(DispatchThreadId + 0.5) / GridSize.xy;//SvPositionToBufferUV(float4(DispatchThreadId+0.5,0,0) * DownScale);
float DeviceZ = LookupDeviceZ(UV);
float2 ScreenPosition = ViewportUVToScreenPos(UV);
float4 ThisClip = float4(ScreenPosition, DeviceZ, 1);
float4 PrevClip = mul(ThisClip, View.ClipToPrevClip);
float2 PrevScreen = PrevClip.xy / PrevClip.w;

float2 BackN = ScreenPosition - PrevScreen;
float2 PreVelocity = float2(BackN.x, -BackN.y) *0.5;

float4 SimValue = SimGridSRV.SampleLevel(SimSampler, UV, 0);//.Load(int3(DispatchThreadId, 0));
float2 ReadIndex = float2(DispatchThreadId + 0.5) - SimValue.xy * (dt / dx);
float4 Advection = SimGridSRV.SampleLevel(SimSampler, ReadIndex / GridSize.xy, 0);

float3 FluidColor = FluidColorUAV[ReadIndex - PreVelocity * GridSize.xy];

SimValue.x = clamp(Advection.x / (dt * VelocityDissipate + 1) - dt * 0.1 * VelocityDissipate, -1000, 1000);
SimValue.y = clamp(Advection.y / (dt * VelocityDissipate + 1) - dt * 0.1 * VelocityDissipate, -1000, 1000);
SimValue.z = clamp(Advection.z / (dt * DensityDissipate + 1) - dt * 0.1 * DensityDissipate, 0, 100);
SimGridUAV[DispatchThreadId] = SimValue;
FluidColorUAV[DispatchThreadId] = FluidColor;//float3(FluidColor.r,FluidColor1.g,FluidColor2.b);
}

图长这样

Untitled

然后为了增加视觉效果再加上一个模糊 pass,这个模糊 pass 可以降分辨率可以省很多,我是用分 pass 做 Bilateral filter (双边滤波) x 一次 y 一次,不解算压力图像看起来的确是比较 noise,最后直接输出到 PostProcess pass 前。

# 输出

对于输出这张图到屏幕上做混合,直接用 ViewExtension 输出,怎么用可以看这篇文章

最后代码是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205

class FPPPsychedelicPS : public FGlobalShader
{
public:
DECLARE_GLOBAL_SHADER(FPPPsychedelicPS);
SHADER_USE_PARAMETER_STRUCT(FPPPsychedelicPS, FGlobalShader);

static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
}

static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return true; //IsFeatureLevelSupported(Parameters.Platform, ERHIFeatureLevel::SM5);
}

BEGIN_SHADER_PARAMETER_STRUCT(FParameters,)
SHADER_PARAMETER_STRUCT_REF(FViewUniformShaderParameters, View)
SHADER_PARAMETER_RDG_TEXTURE(Texture2D, SimulationTexture)
SHADER_PARAMETER_RDG_TEXTURE(Texture2D, ColorTexture)
SHADER_PARAMETER_RDG_UNIFORM_BUFFER(FSceneTextureUniformParameters, SceneTexturesStruct)
SHADER_PARAMETER_STRUCT_INCLUDE(ShaderPrint::FShaderParameters, ShaderPrintParameters)
SHADER_PARAMETER_SAMPLER(SamplerState, SimulationTextureSampler)
SHADER_PARAMETER_SAMPLER(SamplerState, ColorTextureSampler)
RENDER_TARGET_BINDING_SLOTS()
END_SHADER_PARAMETER_STRUCT()
};

class FPPFluidCS : public FGlobalShader
{
public:
DECLARE_GLOBAL_SHADER(FPPFluidCS);
SHADER_USE_PARAMETER_STRUCT(FPPFluidCS, FGlobalShader);

BEGIN_SHADER_PARAMETER_STRUCT(FParameters,)
SHADER_PARAMETER_RDG_UNIFORM_BUFFER(FSceneTextureUniformParameters, SceneTexturesStruct)
SHADER_PARAMETER_STRUCT_INCLUDE(ShaderPrint::FShaderParameters, ShaderPrintUniformBuffer)
SHADER_PARAMETER_STRUCT_INCLUDE(FFluidParameter, FluidParameter)
SHADER_PARAMETER(int, AdvectionDensity)
SHADER_PARAMETER(int, IterationIndex)
SHADER_PARAMETER(int, bUseFFTPressure)
SHADER_PARAMETER(int, FluidShaderType)
SHADER_PARAMETER(int, DownScale)

SHADER_PARAMETER_RDG_TEXTURE(Texture2D, SimGridSRV)
SHADER_PARAMETER_RDG_TEXTURE(Texture2D,TranslucencyTexture)
SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D, SimGridUAV)
SHADER_PARAMETER_RDG_TEXTURE_UAV(RWTexture2D, FluidColorUAV)
END_SHADER_PARAMETER_STRUCT()

static bool ShouldCompilePermutation(const FGlobalShaderPermutationParameters& Parameters)
{
return true;
}

static void ModifyCompilationEnvironment(const FGlobalShaderPermutationParameters& Parameters, FShaderCompilerEnvironment& OutEnvironment)
{
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEX"), ThreadNumber.X);
OutEnvironment.SetDefine(TEXT("THREADGROUP_SIZEY"), ThreadNumber.Y);
OutEnvironment.SetDefine(TEXT("PREVELOCITY"), 0);
OutEnvironment.SetDefine(TEXT("ADVECTION"), 1);

OutEnvironment.SetDefine(TEXT("ITERATEPRESSURE"), 2);
OutEnvironment.SetDefine(TEXT("COMPUTEDIVERGENCE"), 3);

OutEnvironment.CompilerFlags.Add(CFLAG_AllowTypedUAVLoads);
}
};

IMPLEMENT_GLOBAL_SHADER(FPPFluidCS, "/Plugin/PhysicalSimulation/PPFluid/PPFluid.usf", "MainCS", SF_Compute);
IMPLEMENT_GLOBAL_SHADER(FPPPsychedelicPS, "/Plugin/PhysicalSimulation/PPFluid/PPFluidPixelShader.usf", "MainPS", SF_Pixel);
void FPsychedelicSolver::PrePostProcessPass_RenderThread(FRDGBuilder& GraphBuilder, const FSceneView& View, const FPostProcessingInputs& Inputs)
{
//会出现其他屏幕都画上的情况
if (View.UnconstrainedViewRect.Area() < 5000 || !SceneProxy->PlandFluidParameters->InTexture1)
{
return;
}
int DownScale = 1;
DECLARE_GPU_STAT(PlaneFluidSolver)
RDG_EVENT_SCOPE(GraphBuilder, "PlaneFluidSolver");
RDG_GPU_STAT_SCOPE(GraphBuilder, PlaneFluidSolver);

FIntPoint TextureSize = (*Inputs.SceneTextures)->SceneColorTexture->Desc.Extent / DownScale;
Frame++;
if (Frame == 1)
{
FPooledRenderTargetDesc RGBADesc(FPooledRenderTargetDesc::Create2DDesc(TextureSize, PF_FloatRGBA,
FClearValueBinding(), TexCreate_None, TexCreate_ShaderResource | TexCreate_RenderTargetable | TexCreate_UAV, false));

FPooledRenderTargetDesc FloatDesc(FPooledRenderTargetDesc::Create2DDesc(TextureSize, PF_FloatRGB,
FClearValueBinding(), TexCreate_None, TexCreate_ShaderResource | TexCreate_RenderTargetable | TexCreate_UAV, false));
GRenderTargetPool.FindFreeElement(GraphBuilder.RHICmdList, RGBADesc, SimulationTexturePool, TEXT("PsyChedelicSimulationTexture"));
GRenderTargetPool.FindFreeElement(GraphBuilder.RHICmdList, FloatDesc, PressureTexturePool, TEXT("PsyChedelicPressureTexture"));
AddClearUAVPass(GraphBuilder, GraphBuilder.CreateUAV(GraphBuilder.RegisterExternalTexture(SimulationTexturePool)), FLinearColor::Black);
AddClearUAVPass(GraphBuilder, GraphBuilder.CreateUAV(GraphBuilder.RegisterExternalTexture(PressureTexturePool)), FLinearColor::Black);
}

FRDGTextureRef SimulationTexture = GraphBuilder.RegisterExternalTexture(SimulationTexturePool);
FRDGTextureRef FluidColorTexture = GraphBuilder.RegisterExternalTexture(PressureTexturePool);
auto ShaderMap = GetGlobalShaderMap(View.FeatureLevel);

FRDGTextureDesc TempDesc = FRDGTextureDesc::Create2D(TextureSize, PF_FloatRGBA, FClearValueBinding::Black, TexCreate_ShaderResource | TexCreate_UAV | TexCreate_RenderTargetable);

FRDGTextureUAVRef FluidColorTextureUAV = GraphBuilder.CreateUAV(FluidColorTexture);
const FViewInfo& ViewInfo = static_cast<const FViewInfo&>(View);
FFluidParameter FluidParameter;

FluidParameter.DensityDissipate = SceneProxy->PlandFluidParameters->DensityDissipate;
FluidParameter.GravityScale = SceneProxy->PlandFluidParameters->GravityScale;
FluidParameter.NoiseFrequency = SceneProxy->PlandFluidParameters->NoiseFrequency;
FluidParameter.NoiseIntensity = SceneProxy->PlandFluidParameters->NoiseIntensity;
FluidParameter.VelocityDissipate = SceneProxy->PlandFluidParameters->VelocityDissipate;
FluidParameter.VorticityMult = SceneProxy->PlandFluidParameters->VorticityMult;

FluidParameter.UseFFT = true;

FluidParameter.SolverBaseParameter.dt = 0.06; //SceneProxy->World->GetDeltaSeconds() * 2;
FluidParameter.SolverBaseParameter.dx = *SceneProxy->Dx;
FluidParameter.SolverBaseParameter.Time = Frame;
FluidParameter.SolverBaseParameter.View = View.ViewUniformBuffer;
FluidParameter.SolverBaseParameter.GridSize = FVector3f(TextureSize, 1);
FluidParameter.SolverBaseParameter.WarpSampler = TStaticSamplerState<SF_Bilinear, AM_Wrap, AM_Wrap, AM_Wrap>::GetRHI();
FluidParameter.SimSampler = TStaticSamplerState<SF_Bilinear>::GetRHI();
FluidParameter.WorldVelocity = FVector3f(0); //Context->WorldVelocity;
FluidParameter.WorldPosition = FVector3f(0); //Context->WorldPosition;
FluidParameter.InTexture = SceneProxy->PlandFluidParameters->InTexture1->GetResource()->GetTextureRHI();
FluidParameter.InTextureSampler = TStaticSamplerState<SF_Bilinear, AM_Wrap, AM_Wrap, AM_Wrap>::GetRHI();
FRDGTextureRef PreVelocityTexture = GraphBuilder.CreateTexture(TempDesc,TEXT("PreVelocityTexture"));

//PreVelocitySolver
{
FPPFluidCS::FParameters* PassParameters = GraphBuilder.AllocParameters<FPPFluidCS::FParameters>();
PassParameters->FluidParameter = FluidParameter;
PassParameters->FluidShaderType = PreVel;
PassParameters->SimGridSRV = SimulationTexture;
PassParameters->TranslucencyTexture = Inputs.TranslucencyViewResourcesMap.Get(ETranslucencyPass::TPT_TranslucencyAfterDOF).ColorTexture.Target;
PassParameters->SimGridUAV = GraphBuilder.CreateUAV(PreVelocityTexture); //SimUAV;
PassParameters->FluidColorUAV = GraphBuilder.CreateUAV(FluidColorTexture);
PassParameters->SceneTexturesStruct = Inputs.SceneTextures;
PassParameters->DownScale = DownScale;

PassParameters->bUseFFTPressure = bUseFFTSolverPressure;

PassParameters->AdvectionDensity = false;
PassParameters->IterationIndex = 0;
TShaderMapRef<FPPFluidCS> ComputeShader(ShaderMap);

FComputeShaderUtils::AddPass(GraphBuilder,
RDG_EVENT_NAME("PreVelocitySolver"),
ERDGPassFlags::Compute,
ComputeShader,
PassParameters,
FComputeShaderUtils::GetGroupCount(FIntVector(TextureSize.X,TextureSize.Y, 1), ThreadNumber));
}

//Advection Velocity
{
FPPFluidCS::FParameters* AdvectionPassParameters = GraphBuilder.AllocParameters<FPPFluidCS::FParameters>();
//SetParameter(PassParameters, false, 0, SimUAV, PressureUAV, SimSRV, Advection);
AdvectionPassParameters->AdvectionDensity = false;
AdvectionPassParameters->IterationIndex = 0;
AdvectionPassParameters->FluidParameter = FluidParameter;
AdvectionPassParameters->FluidColorUAV = FluidColorTextureUAV;
AdvectionPassParameters->bUseFFTPressure = bUseFFTSolverPressure;
AdvectionPassParameters->SceneTexturesStruct = Inputs.SceneTextures;
AdvectionPassParameters->TranslucencyTexture = Inputs.TranslucencyViewResourcesMap.Get(ETranslucencyPass::TPT_TranslucencyAfterDOF).ColorTexture.Target;
AdvectionPassParameters->SimGridSRV = PreVelocityTexture;
AdvectionPassParameters->SimGridUAV = GraphBuilder.CreateUAV(SimulationTexture);
AdvectionPassParameters->FluidShaderType = Advection;
AdvectionPassParameters->DownScale = DownScale;
TShaderMapRef<FPPFluidCS> ComputeShader(ShaderMap);

FComputeShaderUtils::AddPass(GraphBuilder,
RDG_EVENT_NAME("AdvectionVelocity"),
ERDGPassFlags::Compute,
ComputeShader,
AdvectionPassParameters,
FComputeShaderUtils::GetGroupCount(FIntVector(TextureSize.X,TextureSize.Y, 1), ThreadNumber));
}
FRDGTextureRef BlurColorTexture = GraphBuilder.CreateTexture(FRDGTextureDesc::Create2D(TextureSize / 2,PF_FloatR11G11B10,FClearValueBinding::Black,TexCreate_ShaderResource | TexCreate_UAV),TEXT("BlurColorTexture"));
AddTextureBlurPass(GraphBuilder,ViewInfo,FluidColorTexture,BlurColorTexture,0.5);
//SimulationTexturePool = GraphBuilder.ConvertToExternalTexture(AdvectionDensityTexture);

TShaderMapRef<FPPPsychedelicPS> PixelShader(ShaderMap);
FPPPsychedelicPS::FParameters* PixelShaderParameters = GraphBuilder.AllocParameters<FPPPsychedelicPS::FParameters>();

PixelShaderParameters->View = View.ViewUniformBuffer;
PixelShaderParameters->RenderTargets[0] = FRenderTargetBinding((*Inputs.SceneTextures)->SceneColorTexture, ERenderTargetLoadAction::ENoAction);
PixelShaderParameters->SceneTexturesStruct = Inputs.SceneTextures;
PixelShaderParameters->SimulationTexture = SimulationTexture;
PixelShaderParameters->SimulationTextureSampler = TStaticSamplerState<SF_Bilinear>::GetRHI();
PixelShaderParameters->ColorTexture = BlurColorTexture;//SceneProxy->PlandFluidParameters->InTexture1->GetResource()->GetTextureRHI();
PixelShaderParameters->ColorTextureSampler = TStaticSamplerState<SF_Bilinear, AM_Wrap, AM_Wrap, AM_Wrap>::GetRHI();

ShaderPrint::SetParameters(GraphBuilder, ViewInfo.ShaderPrintData, PixelShaderParameters->ShaderPrintParameters);

FPixelShaderUtils::AddFullscreenPass(
GraphBuilder,
ShaderMap,
RDG_EVENT_NAME("PostProcessPsychedelic"),
PixelShader,
PixelShaderParameters,
View.UnconstrainedViewRect);
}