场景外描边冲击波后处理效果

类似绝区零布盯的场景外描边冲击波后处理效果

Github源码传送门————>ShockwaveOutline

Bilibili教程传送门————>小祥带你实现绝区零布盯效果

实机

整体思路

从游戏内画面可以看到,布盯效果从相机位置出发不断向外四周扩散,影响到的模型边缘似乎被柔和的橙色灯光照亮了。提取关键词,向四周扩散就是类似于冲击波,模型边缘的话我们想到外描边,柔和光照我们想到自发光bloom产生类似效果。那么一一对应游戏中的实现,控制冲击波影响范围需要计算冲击波开始位置到被影响像素的距离,外描边需要深度和法线综合进行边缘检测产生,然后开启bloom后处理让外描边自发光柔和。

ShockwaveOutlineRendererFeature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ShockwaveOutlineRendererFeature : ScriptableRendererFeature
{
public ShockwaveOutlineRenderPass shockwaveOutlineRenderPass;

public bool activeEffect = false;
public Material shockwaveOutlineMaterial;

public override void Create()
{
shockwaveOutlineRenderPass = new ShockwaveOutlineRenderPass(shockwaveOutlineMaterial);
shockwaveOutlineRenderPass.renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
}

public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
if (activeEffect)
{
renderer.EnqueuePass(shockwaveOutlineRenderPass);
}
}
}
  • RenderPassEvent.AfterRenderingTransparents 因为外描边自发光需要参与bloom效果,所以放在透明物体之后后处理之前进行渲染
  • activeEffect 外置一个参数控制是否执行这个Pass

ShockwaveOutlineRenderPass

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
public class ShockwaveOutlineRenderPass : ScriptableRenderPass
{
private Material shockwaveOutlineMaterial;

public ShockwaveOutlineRenderPass(Material shockwaveOutlineMat)
{
shockwaveOutlineMaterial = shockwaveOutlineMat;
}

public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData)
{
UniversalCameraData cameraData = frameData.Get<UniversalCameraData>();
UniversalResourceData resourceData = frameData.Get<UniversalResourceData>();

var desc = cameraData.cameraTargetDescriptor;
desc.depthStencilFormat = GraphicsFormat.None;
desc.msaaSamples = 1;
desc.graphicsFormat = GraphicsFormat.R16G16B16A16_SFloat;

TextureHandle cameraColor = resourceData.activeColorTexture;
TextureHandle tempColor = UniversalRenderer.CreateRenderGraphTexture(renderGraph, desc, "_ShockwaveOutline", true);

BlitMaterialParameters blitParams = new BlitMaterialParameters(cameraColor, tempColor, shockwaveOutlineMaterial, 0);
renderGraph.AddBlitPass(blitParams, "Shockwave Outline");
resourceData.cameraColor = tempColor;
}
}
  • 常规全屏效果处理流程 activeColorTexture 作为源通过材质处理后赋值给 cameraColor

ShockwaveOutline.shader

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
Shader "Custom/ShockwaveOutline"
{
Properties
{
_DepthSensitivity("Depth Sensitivity", Float) = 100.0
_NormalSensitivity("Normal Sensitivity", Float) = 100.0
_EdgeThresholdMin("Edge Threshold Min", Range(0,1)) = 0.85
_EdgeThresholdMax("Edge Threshold Max", Range(0,1)) = 0.95
_ShockWaveWidth("Shock Wave Width", Float) = 3.0
[HDR]_OutlineColor("Outline Color", Color) = (10, 4, 0.5, 1)

[HideInInspector]
_ShockWaveRadius("Shock Wave Radius", Float) = 0.0
[HideInInspector]
_StartPosition("Start Position", Vector) = (0, 0, 0, 0)
}

SubShader
{
Tags{ "RenderPipeline" = "UniversalPipeline" "IgnoreProjector" = "True"}

Pass
{
Name "ShockwaveOutline"
ZTest Always
ZWrite Off
Cull Off

HLSLPROGRAM

#pragma vertex Vert
#pragma fragment Fragment

#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/Runtime/Utilities/Blit.hlsl"

float _DepthSensitivity;
float _NormalSensitivity;
float _EdgeThresholdMin;
float _EdgeThresholdMax;
float _ShockWaveWidth;
half4 _OutlineColor;
float _ShockWaveRadius;
float3 _StartPosition;

TEXTURE2D(_CameraDepthTexture);
TEXTURE2D(_CameraNormalsTexture);

float SampleDepth(float2 uv)
{
float rawDepth = SAMPLE_TEXTURE2D(_CameraDepthTexture, sampler_PointClamp, uv).r;
#if UNITY_REVERSED_Z
return rawDepth;
#else
return rawDepth * 2.0 - 1.0;
#endif
}

float3 SampleNormal(float2 uv)
{
float4 packedNormal = SAMPLE_TEXTURE2D(_CameraNormalsTexture, sampler_PointClamp, uv);
return normalize(packedNormal.xyz * 2.0 - 1.0);
}

float DepthEdge(float2 uv)
{
float2 texelSize = 1.0 / _ScreenParams.xy;

float d = SampleDepth(uv);
float dR = SampleDepth(uv + float2(texelSize.x, 0));
float dL = SampleDepth(uv - float2(texelSize.x, 0));
float dU = SampleDepth(uv + float2(0, texelSize.y));
float dD = SampleDepth(uv - float2(0, texelSize.y));

float dx = abs(dR - dL);
float dy = abs(dU - dD);
float edge = sqrt(dx * dx + dy * dy);
return saturate(edge * _DepthSensitivity);
}

float NormalEdge(float2 uv)
{
float2 texelSize = 1.0 / _ScreenParams.xy;

float3 n = SampleNormal(uv);
float3 nR = SampleNormal(uv + float2(texelSize.x, 0));
float3 nL = SampleNormal(uv - float2(texelSize.x, 0));
float3 nU = SampleNormal(uv + float2(0, texelSize.y));
float3 nD = SampleNormal(uv - float2(0, texelSize.y));

float3 dn = (nR - nL) + (nU - nD);
float edge = length(dn);
return saturate(edge * _NormalSensitivity);
}

half4 Fragment(Varyings input) : SV_Target
{
float2 uv = input.texcoord.xy;

float edgeDepth = DepthEdge(uv);
float edgeNormal = NormalEdge(uv);
float edge = saturate(edgeDepth + edgeNormal);
float edgeMask = smoothstep(_EdgeThresholdMin, _EdgeThresholdMax, edge);

float depth = SampleDepth(uv);
float3 worldPosition = ComputeWorldSpacePosition(uv, depth, unity_MatrixInvVP);
float dist = distance(worldPosition.xz, _StartPosition.xz);
float shockMask = step(_ShockWaveRadius, dist) * step(dist, _ShockWaveRadius + _ShockWaveWidth);

float mask = edgeMask * shockMask;

half4 color = SAMPLE_TEXTURE2D_X_LOD(_BlitTexture, sampler_LinearRepeat, uv, _BlitMipLevel);

return lerp(color, _OutlineColor, mask);
}
ENDHLSL
}
}
}
  • 分别从深度贴图(_CameraDepthTexture)和法线贴图(_CameraNormalsTexture)采样深度值和法线向量
  • 边缘检测的精度不需要太高这里使用五点采样
    • 深度是标量,计算纵横两个方向的梯度,计算这个二维梯度向量的长度作为当前像素的深度梯度
    • 法线是向量,直接合成两个方向的法线向量,使用 length() 方法获得其长度值作为法线梯度即可
  • 计算得到的深度边缘值和法线边缘值相加作为边缘值,smoothstep() 使得描边更柔和,计算得到edgeMask
  • ComputeWorldSpacePosition() 反向计算出像素在世界空间的坐标,计算出和起始点的距离(这里使用xz分量计算使得冲击波以圆柱体向外扩散,要球体效果的话使用xyz分量计算距离),然后判断当前像素是否在冲击波范围内,计算得到shockMask
  • 结合两个mask值作为插值系数,插值采样到的屏幕像素颜色和外描边颜色(_OutlineColor)作为最终颜色输出(要在Bloom中产生效果,外描边颜色设置为HDR色彩)

完结撒花~

pid:132137905


场景外描边冲击波后处理效果
https://baifabaiquan.cn/2025/07/21/ShockwaveOutline/
作者
白发败犬
发布于
2025年7月21日
许可协议