前言

前几天在网上看到一位大神的 Unity Shader-热空气扭曲效果 文章,感觉应该是个常见的效果,所以准备在URP里实现一下,正好再次深入使用一下URP,期间也遇到了一些匪夷所思的坑,也会在文章中说明。

原文中的全屏扭曲和基于GrabPass的方式都省略不谈,这里来用URP实现一下基于后处理的热空气扭曲。

环境

URP版本:7.3.1

Unity版本:2019.4.8f1

正文

原文中的实现核心思路是在需要扭曲的地方摆放一个面片,然后将这个面片渲染到一张RenderTexture上作为Mask,后处理的时候以Mask为基准决定ColorTexture哪些地方需要扭曲,然后对一张Noise图进行采样,对目标像素做偏移,达到扭曲的效果。

仔细分析后发现其实就一个难点,就是如何在URP下将物体渲染到一个RenderTexture上。

恰巧前阵子 研究战争迷雾 的时候看到了这篇文章:流朔 -【Unity URP】以Render Feature实现卡通渲染中的刘海投影 ,其中就有将物体渲染到RenderTexture的相关操作,这样一来就没有问题了,开搞。

首先创建一个RenderFeature命名为RenderMaskFeature,用于将指定层级物体渲染到一张RenderTexture上,核心代码就是这RenderPass中的这两个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{
//获取一个ID,这也是我们之后在Shader中用到的Buffer名
int temp = Shader.PropertyToID("_MaskSoildColor");
m_SoildColorID = temp;
//一般都可以使用比较低的分辨率来做扭曲效果,因为扭曲效果本身就不太追求画质了
RenderTextureDescriptor desc = new RenderTextureDescriptor(128,128);
cmd.GetTemporaryRT(temp, desc);
//将这个RT设置为Render Target
ConfigureTarget(temp);
//将RT清空为黑
ConfigureClear(ClearFlag.All, Color.black);
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
var drawMask = CreateDrawingSettings(m_ShaderTag, ref renderingData,
renderingData.cameraData.defaultOpaqueSortFlags);
drawMask.overrideMaterial = m_Setting.Material;
drawMask.overrideMaterialPassIndex = 0;
context.DrawRenderers(renderingData.cullResults, ref drawMask, ref m_FilteringSettings);
}

其中有几个注意点

首先是降采样,因为扭曲效果本身不会用到太精细的分辨率,所以可以使用很低的分辨率进行采样(当然如果这个扭曲效果要求比较高,比如刀光的扭曲,那么可能就得用屏幕分辨率的1/4,具体情况具体分析),比如我这里使用了128*128的RenderTexture,效果也说得过去(效果见文末)

image-20210907004036559

其次m_ShaderTag类型为ShaderTagId,对应一个Pass中的Tag的LightMode关键字,其实这一点从URP自带的RenderObject的Shader Pass字段ToolTip不难看出

Controls which shader passes to use when rendering objects. The name given here must match the LightMode tag in a shader pass.

最后是overrideMaterial,表示此批次绘制会使用的材质,然后是overrideMaterialPassIndex,表示会用此材质的第几个Pass进行渲染,这个值默认为0,到这里其实就可以发觉,overrideMaterialPassIndex是有可能与我们的m_ShaderTag发生冲突的,我这边实验的结果是,以overrideMaterialPassIndex为准(例如m_ShaderTag的Pass Index为0,但是overrideMaterialPassIndex为1,那么就会使用Pass Index为1的Pass进行渲染)。

然后我就开始遇到坑了,我这里的m_ShaderTag设置和刘海投影那篇文章一样,都是ShaderTagId(“UniversalForward”),当我更换物体材质的时候,打开FrameDebug却发现绘制Mask这一步骤凭空消失了

image-20210907004058549

???这是什么情况,我只是更换了物体的材质,没有更换Render Feature的材质,为什么Render Feature直接失效了?

先来复盘一下场景,物体使用的材质Shader是Universal Render Pipeline/Lit,其中正好有一个Tags{“LightMode” = “UniversalForward”}的Pass,但是我更换了Universal Render Pipeline/Unlit后Render Feature就失效了,思来想去只能是因为Universal Render Pipeline/Unlit中没有LightMode为UniversalForward的Pass,看了下Shader果真如此。

这就有点震惊了,我设置了overrideMaterial,在这个Render Feature中我都没有用到物体自身材质的Pass,那么物体的材质改变就不应该导致Render Feature的失效。随便翻了翻URP的源码也没有看出个所以然来,索性再多试验几种情况,总结一下吧:

  • 如果不设置override material的话,就会默认使用物体身上的材质上的目标Pass进行渲染,但是缺点是如果物体的材质没有达成SRP Batch的条件,就无法进行SRP Batch,SRP Batcher相关内容参见:关于静态批处理/动态批处理/GPU Instancing /SRP Batcher的详细剖析
  • 如果设置了override marerial的话,就会使用override marerial的目标Pass进行渲染,这种方式可以忽略任何情况(除非你材质本身无法进行SRP Batch)直接一个SRP Batch搞定
  • 对于overrideMaterialPassIndex和ShaderTagId冲突的情况,以overrideMaterialPassIndex为准
  • 对于更换材质会导致写入Mask Texture失效的情况,是因为,不知道为什么他似乎有个潜规则,就是你的物体身上的材质,必须有一个Pass,它的Tag和你设置的shaderTagId相同,不然就会失效

然后创建另一个RenderFeature,命名为AirDistortionFeature,用于进行后处理操作,核心代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
CommandBuffer cmd = CommandBufferPool.Get(m_ProfilerTag);

RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor;
cmd.GetTemporaryRT(m_TemporaryColorTexture.id, opaqueDesc);

Blit(cmd, source, m_TemporaryColorTexture.Identifier(), blitMaterial);
Blit(cmd, m_TemporaryColorTexture.Identifier(), source);

cmd.ReleaseTemporaryRT(m_TemporaryColorTexture.id);

context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}

然后是空气扭曲的后处理Shader,除了改用了HLSL,内容基本和原文一致

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
Shader "URP_Practise/Air-distortion"
{
SubShader
{
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

TEXTURE2D(_CameraColorTexture);
SAMPLER(sampler_CameraColorTexture);
TEXTURE2D(_NoiseTex);
SAMPLER(sampler_NoiseTex);
TEXTURE2D(_MaskSoildColor);
SAMPLER(sampler_MaskSoildColor);

uniform float _DistortTimeFactor;
uniform float _DistortStrength;

struct Attributes
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};

struct Varyings
{
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
};

Varyings vert(Attributes input)
{
Varyings output;

VertexPositionInputs vertexInput = GetVertexPositionInputs(input.positionOS.xyz);
output.vertex = vertexInput.positionCS;
output.uv = input.uv;

return output;
}

float4 frag(Varyings input) : SV_Target
{
//根据时间改变采样噪声图获得随机的输出
float4 noise = SAMPLE_TEXTURE2D(_NoiseTex, sampler_CameraColorTexture,
input.uv - _Time.xy * _DistortTimeFactor);
//以随机的输出*控制系数得到偏移值
float2 offset = noise.xy * _DistortStrength;
//采样Mask图获得权重信息
float4 factor = SAMPLE_TEXTURE2D(_MaskSoildColor, sampler_NoiseTex, input.uv);
//像素采样时偏移offset,用Mask权重进行修改
float2 uv = offset * factor.r + input.uv;
return SAMPLE_TEXTURE2D(_CameraColorTexture, sampler_CameraColorTexture, uv);
}
ENDHLSL
}
}
FallBack off
}

然后在RenderFeature中添加我们的RenderMaskFeature和AirDistortionFeature即可,至于噪声图的选择,使用一般水面的噪声图效果应该就挺好了了,我这里随便找了一张噪声

image-20210907004119600

效果如图所示:(可以看到即使场景有非常多的空气扭曲效果,但渲染耗时和单个空气扭曲基本一致)

v2-70e2b20f2ed506a772e75c6e961ee762_b

项目地址

https://github.com/wqaetly/URP_Practise

引用

Unity Shader-热空气扭曲效果

研究战争迷雾

流朔 -【Unity URP】以Render Feature实现卡通渲染中的刘海投影

关于静态批处理/动态批处理/GPU Instancing /SRP Batcher的详细剖析