基于模板测试的透视描边效果

自定义模板缓冲显示被墙体遮挡的角色

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

Bilibili教程传送门————>小祥带你实现露娜探测箭穿墙透视高亮效果

实机

实现思路

通过实机画面可以看到,射出探测箭之后被扫描到的玩家轮廓像UI一样被高亮显示在画面上,相当于是在人物模型处加一个遮罩,后处理阶段对加了遮罩的区域进行指定效果的绘制,那么这种效果一般是用模板测试来做,通过写入模板值的方式指示哪些区域或者说像素需要进行后续处理。模板测试和深度测试都是GPU维护的缓冲区进行操作,性能非常好但是读写易用性就没那么好了,这次我们使用贴图自定义模板缓冲来实现这个效果。思路是所有被扫描到的人物模型对应的像素区域写入指定模板值到纹理,通过模板值来区分不同类型的人物的轮廓,后处理阶段以这张写入模板值的纹理作为遮罩进行绘制操作,比如在人物轮廓填充对应的高亮颜色然后添加外描边等等。

OverlayEffectRenderPass.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
public class OverlayEffectRenderPass : ScriptableRenderPass
{
private Material overlayOutlineMaterial;

private class PassData
{
public RendererListHandle rendererListHandle;
}

public OverlayEffectRenderPass(Material material)
{
overlayOutlineMaterial = material;
}

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

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

TextureHandle tempColor = UniversalRenderer.CreateRenderGraphTexture(renderGraph, desc, "_StencilTexture", true);

using (var builder = renderGraph.AddRasterRenderPass("Write Stencil", out PassData passData))
{
SortingCriteria sortingCriteria = cameraData.defaultOpaqueSortFlags;
RenderQueueRange renderQueueRange = RenderQueueRange.all;
FilteringSettings filteringSettings = new FilteringSettings(renderQueueRange, ~0);
DrawingSettings drawingSettings = RenderingUtils.CreateDrawingSettings(new ShaderTagId("WriteStencil"), renderingData, cameraData, lightData, sortingCriteria);
RendererListParams rendererListParams = new RendererListParams(renderingData.cullResults, drawingSettings, filteringSettings);

passData.rendererListHandle = renderGraph.CreateRendererList(rendererListParams);

builder.SetRenderAttachment(tempColor, 0);
builder.SetRenderAttachmentDepth(resourceData.activeDepthTexture);
builder.UseRendererList(passData.rendererListHandle);

builder.SetRenderFunc((PassData data, RasterGraphContext context) =>
{
context.cmd.ClearRenderTarget(RTClearFlags.DepthStencil, Color.clear, 1f, 0u);
context.cmd.DrawRendererList(data.rendererListHandle);
});

builder.SetGlobalTextureAfterPass(tempColor, Shader.PropertyToID("_StencilTexture"));
}

TextureHandle cameraColor = resourceData.activeColorTexture;
BlitMaterialParameters blitParams = new BlitMaterialParameters(tempColor, cameraColor, overlayOutlineMaterial, 0);
renderGraph.AddBlitPass(blitParams, "Overlay Outline");
resourceData.cameraColor = cameraColor;
}
}
  • 纹理只存模板值所以格式设置为R8_UInt单通道
  • 创建渲染队列,指定ShaderTagId为我们写入模板值的Pass对应的LightMode
  • 清空之前的深度测试和模板测试的值,提交渲染队列进行渲染
  • 因为使用了单通道纹理,所以绘制结束之后将写有模板值的纹理设置为全局纹理以供后处理的时候使用
  • 后续Blit调用后处理材质对画面进行处理

WriteStencil.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
Pass
{
Name "WriteStencil"
Tags { "LightMode" = "WriteStencil" }

ZTest LEqual
ZWrite Off
Cull Back
Blend Off

HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#pragma multi_compile_instancing

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

UNITY_INSTANCING_BUFFER_START(OverlayProps)
UNITY_DEFINE_INSTANCED_PROP(int, _OverlayID)
UNITY_INSTANCING_BUFFER_END(OverlayProps)

struct Attributes
{
float4 positionOS : POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

struct Varyings
{
float4 positionCS : SV_POSITION;
UNITY_VERTEX_INPUT_INSTANCE_ID
};

Varyings vert(Attributes input)
{
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
return output;
}

uint4 frag(Varyings input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input);
int overlayID = UNITY_ACCESS_INSTANCED_PROP(OverlayProps, _OverlayID);
return uint4(overlayID,0,0,0);
}
ENDHLSL
}
  • 这个Pass添加到需要透视高亮效果的模型对应的材质shader中
  • 模型可能会有重叠的时候,前面提交渲染队列之前清空了深度值,这里需要开启深度测试,保证写入的模板值是最前面模型的
  • 顶点着色器方法中正常进行顶点变换,片元着色器方法中只需要返回一个模板值即可

OverlayOutline.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
Shader "Custom/OverlayOutline"
{
Properties
{
[HDR]_FillColor ("Fill Color", Color) = (1,1,1,1)
[HDR]_OutlineColor ("Outline Color", Color) = (1,1,1,1)
[HDR]_OutlineColor2 ("Outline Color 2", Color) = (1,1,1,1)
}

SubShader
{
Pass
{
Name "OverlayOutline"
Tags { "LightMode" = "OverlayOutline" }

ZTest Always
ZWrite Off
Cull Back
Blend SrcAlpha OneMinusSrcAlpha

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

#pragma target 2.0
#pragma vertex Vert
#pragma fragment Frag

half4 _FillColor;
half4 _OutlineColor;
half4 _OutlineColor2;
Texture2D<uint> _StencilTexture;

bool IsEdge(uint2 p)
{
uint c = _StencilTexture.Load(int3(p, 0));
uint l = _StencilTexture.Load(int3(p + uint2(-1,0), 0));
uint r = _StencilTexture.Load(int3(p + uint2(1,0), 0));
uint u = _StencilTexture.Load(int3(p + uint2(0,-1), 0));
uint d = _StencilTexture.Load(int3(p + uint2(0,1), 0));
return (l != c) | (r != c) | (u != c) | (d != c);
}

half4 Frag(Varyings input) : SV_Target0
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);

float2 uv = input.texcoord.xy;
uint width, height;

_StencilTexture.GetDimensions(width, height);

uint2 pixelCoord = uint2(uv * float2(width, height));

uint mask = _StencilTexture.Load(int3(pixelCoord, 0)).r;

if(mask == 0) discard;

half4 outlineColor = _OutlineColor;
if(mask == 2) outlineColor = _OutlineColor2;

if(IsEdge(pixelCoord)) return outlineColor;
else return _FillColor;
}
ENDHLSL
}
}
FallBack Off
}
  • 声明创建的单通道纹理StencilTexture,这种纹理需要通过Load方法来进行采样,将uv坐标乘上纹理的宽高得到像素坐标进行采样得到前一步写入的模板值
  • 如果模板值为0说明不是需要处理的像素直接跳过即可
  • 通过写入的不同模板值选择对应的外描边颜色
  • 做一个简单的边缘检测,如果是边缘就返回外描边颜色,否则返回填充颜色

OverlayEffectController.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
public class OverlayEffectController : MonoBehaviour
{
public int overlayID;
private List<Material> materials;
private MaterialPropertyBlock materialPropertyBlock;

private void Start()
{
materialPropertyBlock = new MaterialPropertyBlock();
materialPropertyBlock.SetInt("_OverlayID", overlayID);
materials = new List<Material>();
foreach(var skinnedMeshRenderer in transform.GetComponentsInChildren<SkinnedMeshRenderer>())
{
materials.Add(skinnedMeshRenderer.sharedMaterial);
skinnedMeshRenderer.SetPropertyBlock(materialPropertyBlock);
}

SetShaderPassEnabled(false);
}

public void SetShaderPassEnabled(bool value)
{
foreach (var material in materials)
{
material.SetShaderPassEnabled("WriteStencil", value);
}
}
}
  • 通过MPB将不同角色对应的模板值传递到shader中
  • 调用SetShaderPassEnabled方法,在模型被扫描到的开启pass写入模板值,后处理的时候自然会将当前模型的高亮轮廓绘制出来
完结撒花~

pid:136697230


基于模板测试的透视描边效果
https://baifabaiquan.cn/2025/10/24/OverlayEffectThroughWalls/
作者
白发败犬
发布于
2025年10月24日
许可协议