【Unity URP】模拟破晓传说"Atmos Shader"

Posted by FlowingCrescent on 2021-06-12
Estimated Reading Time 8 Minutes
Words 1.5k In Total
Viewed Times

Tales of系列最新作Tale of Arise终于要在今年发售,而其大幅进化的画面是最让玩家所期待的部分之一。
官方也非常懂人心地特地拿出一个噱头来宣传,即Atmos Shader。
image.png
在这种商业宣传中,我们技术人员根本无法了解“Atmos Shader”到底是什么,是一种特殊的光照算法还是后处理?总之意味不明。

原理

不过我们可以在万代南梦宫于CEDEC2019的技术分享上略窥一二,当时它还没有什么Atmos Shader这样的“高端”名,就是暂名Filter,那么很显然,它就是一个后处理效果。
image.png

在技术分享PPT中所展现的效果:
image.png
image.png

显然它这两张图之间相隔绝不止一个后处理……我们稍微感受一下水彩的效果就行了。
那么来看看原理,官方说是SNN+锐化+法线描边
image.png
关于SNN,可以参考ShaderToy的一个实现:
https://www.shadertoy.com/view/MlyfWd
说得直白一些的话,就是水彩化的后处理,但是实际用起来会发现它把物体的边界模糊太多,几乎没法单独使用。
因此就增加了一个锐化,以突出物体的边界,增加一些细节。
锐化则可以参考:https://www.shadertoy.com/view/XlycDV

实现

那么代码实现都有的Copy了,实现起来就比较简单了。
我们使用Unity URP的RenderFeature来实现一个多Pass后处理效果。

RenderFeature:

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
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class AtmosShader : ScriptableRendererFeature
{
[System.Serializable]
public class Setting
{
public RenderPassEvent passEvent = RenderPassEvent.AfterRenderingTransparents;
public Material SNN_Mat;
[Range(0, 20)]
public int half_width = 5;

public Material SharpenMat;
[Range(0f, 1f)]
public float shaprenStrenth = 0.5f;
}
public Setting setting = new Setting();
class CustomRenderPass : ScriptableRenderPass
{
Camera camera;
public int SNNColorID = 0;
public int SharpenColorID = 0;
public Setting setting;
public RenderTargetIdentifier source;
public CustomRenderPass(Setting set)
{
setting = set;
camera = Camera.main;
}

public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{
SNNColorID = Shader.PropertyToID("_SNNLayer");
SharpenColorID = Shader.PropertyToID("_SharpenLayer");
setting.SNN_Mat.SetInt("_HalfWidth", setting.half_width);
setting.SharpenMat.SetFloat("_SharpenStrength", setting.shaprenStrenth);
}


public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
CommandBuffer cmd = CommandBufferPool.Get("SNNRendering");
RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor;
int width = opaqueDesc.width;
int height = opaqueDesc.height;
cmd.GetTemporaryRT(SNNColorID, width, height, 0, FilterMode.Bilinear, opaqueDesc.colorFormat);
cmd.GetTemporaryRT(SharpenColorID, width, height, 0, FilterMode.Bilinear, opaqueDesc.colorFormat);
cmd.Blit(source, SNNColorID, setting.SNN_Mat);

cmd.Blit(SNNColorID, SharpenColorID, setting.SharpenMat);

cmd.Blit(SharpenColorID, source);
context.ExecuteCommandBuffer(cmd);
cmd.ReleaseTemporaryRT(SNNColorID);
}

/// Cleanup any allocated resources that were created during the execution of this render pass.
public override void FrameCleanup(CommandBuffer cmd)
{

}


}

CustomRenderPass m_ScriptablePass;

public override void Create()
{
m_ScriptablePass = new CustomRenderPass(setting);

// Configures where the render pass should be injected.
m_ScriptablePass.renderPassEvent = setting.passEvent;
}

// Here you can inject one or multiple render passes in the renderer.
// This method is called when setting up the renderer once per-camera.
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
m_ScriptablePass.source = renderer.cameraColorTarget;
if (setting.SNN_Mat != null)
renderer.EnqueuePass(m_ScriptablePass);
}


}



SNN:

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
Shader "Custom/SNN Filter"
{
Properties
{
_MainTex ("Main Texture", 2D) = "white" { }
_HalfWidth ("Sampler Count", int) = 5
}
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalRenderPipeline" }

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

CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
int _HalfWidth;
float _SharpenStrength;
CBUFFER_END
ENDHLSL

Pass
{
Name "Example"
Tags { "LightMode" = "UniversalForward" }

HLSLPROGRAM

#pragma vertex vert
#pragma fragment frag

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

struct v2f
{
float4 positionCS: SV_POSITION;
float2 uv: TEXCOORD0;
};

TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);

v2f vert(a2v v)
{
v2f o;

//VertexPositionInputs positionInputs = GetVertexPositionInputs(input.positionOS.xyz);
//o.positionCS = positionInputs.positionCS;
// Or this :
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);

return o;
}

//Copy from https://www.shadertoy.com/view/MlyfWd
// Calculate color distance
float CalcDistance(float3 c0, float3 c1)
{
float3 sub = c0 - c1;
return dot(sub, sub);
}

// Symmetric Nearest Neighbor
float3 CalcSNN(float2 fragCoord)
{
float2 src_size = _ScaledScreenParams.xy;
float2 inv_src_size = 1.0f / src_size;
float2 uv = fragCoord;

float3 c0 = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv).rgb;

float4 sum = float4(0.0f, 0.0f, 0.0f, 0.0f);

for (int i = 0; i <= _HalfWidth; ++ i)
{
float3 c1 = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(+i, 0) * inv_src_size).rgb;
float3 c2 = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(-i, 0) * inv_src_size).rgb;

float d1 = CalcDistance(c1, c0);
float d2 = CalcDistance(c2, c0);
if (d1 < d2)
{
sum.rgb += c1;
}
else
{
sum.rgb += c2;
}
sum.a += 1.0f;
}
for (int j = 1; j <= _HalfWidth; ++ j)
{
for (int i = -_HalfWidth; i <= _HalfWidth; ++ i)
{
float3 c1 = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(+i, +j) * inv_src_size).rgb;
float3 c2 = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(-i, -j) * inv_src_size).rgb;

float d1 = CalcDistance(c1, c0);
float d2 = CalcDistance(c2, c0);
if(d1 < d2)
{
sum.rgb += c1;
}
else
{
sum.rgb += c2;
}
sum.a += 1.0f;
}
}
return sum.rgb / sum.a;
}



half4 frag(v2f i): SV_Target
{
//half4 baseMap = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
//return baseMap;
float3 result = CalcSNN(i.uv);
//result = sharpenFilter(i.uv, _SharpenStrength);


return float4(result, 1);
}
ENDHLSL

}
}
}

锐化:

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
Shader "Custom/Sharpen Filter"
{
Properties
{
_MainTex ("Main Texture", 2D) = "white" { }
_HalfWidth ("Sampler Count", int) = 5
}
SubShader
{
Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalRenderPipeline" }

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

CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
int _HalfWidth;
float _SharpenStrength;
CBUFFER_END
ENDHLSL

Pass
{
Name "Example"
Tags { "LightMode" = "UniversalForward" }

HLSLPROGRAM

#pragma vertex vert
#pragma fragment frag

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

struct v2f
{
float4 positionCS: SV_POSITION;
float2 uv: TEXCOORD0;
};

TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);

v2f vert(a2v v)
{
v2f o;

//VertexPositionInputs positionInputs = GetVertexPositionInputs(input.positionOS.xyz);
//o.positionCS = positionInputs.positionCS;
// Or this :
o.positionCS = TransformObjectToHClip(v.positionOS.xyz);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);

return o;
}

//Copy from https://www.shadertoy.com/view/XlycDV
float3 texSample(float x, float y, float2 uv)
{
float2 src_size = _ScaledScreenParams.xy;
float2 inv_src_size = 1.0f / src_size;
return SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + float2(x, y) * inv_src_size).rgb;
}

float3 sharpenFilter(in float2 fragCoord, float strength)
{
float3 f = texSample(-1, -1, fragCoord) * - 1. +
texSample(0, -1, fragCoord) * - 1. +
texSample(1, -1, fragCoord) * - 1. +
texSample(-1, 0, fragCoord) * - 1. +
texSample(0, 0, fragCoord) * 9. +
texSample(1, 0, fragCoord) * - 1. +
texSample(-1, 1, fragCoord) * - 1. +
texSample(0, 1, fragCoord) * - 1. +
texSample(1, 1, fragCoord) * - 1.
;
return lerp(texSample(0, 0, fragCoord), f, strength);
}

half4 frag(v2f i): SV_Target
{
//half4 baseMap = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
//return baseMap;
//float3 result = CalcSNN(i.uv);
float3 result = sharpenFilter(i.uv, _SharpenStrength);


return float4(result, 1);
}
ENDHLSL

}
}
}

虽然尝试加过法线描边,但是感觉效果不太好,而且锐化也已经产生了类似描边的效果。

image.png
image.png
image.png
image.png

未实现的部分

image.png
其中提到了一个Blend Weight,其遵从Bilateral性质,但说实话并没有懂
以及该后处理的强度随深度变化,还把SNN拆成了水平和垂直两个部分,最后只需要7+7,共14次采样
总之,他们根据项目需求对算法进行了一些魔改,最终达成了理想的开销并获得目标效果。


感谢您阅读完本文,若您认为本文对您有所帮助,可以将其分享给其他人;若您发现文章对您的利益产生侵害,请联系作者进行删除。