unity-shader-模板测试-描边
unity-shader模板测试,描边效果,常用于 rpg 项目中 主角 被遮挡的情况,将被遮挡的部分的轮廓描边绘制出来,这样可以在任何情况都能知道 主角 在哪里。(还有另外一种就是使用 X光 效果,但这种效果不需要用到模板测试,所以这里用 描边效果 举栗子)
效果
思路
实现这种效果需要两个pass
- 正常绘制,即深度测试 LEqual,正常遮挡,同时往 模板缓冲区 写入一个 参考值 Ref=1(即使深度测试失败的值也要写入,保证 角色所有像素在模板缓冲区的值 都是 参考值 Ref=1)
- 只绘制被遮挡部分的描边
- 先把模型的 顶点 往 法线方向 偏移一个值(这个就是描边的宽度值,可以理解为将模型放大了),可以在观察空间 或者 世界空间、模型空间 偏移,只要和 法线 在同一空间下
- 对比 模板缓冲区 的参考值 Ref=1,因为第一个 pass 写入的参考值是 1,所以这个pass中就要不等1才让它通过,这样就能得到一个 差值区域,即描边的区域
- 深度测试,让被遮挡部分才让它通过,即 ZTest Greater,done!
shader代码
1 | Shader "Custom/Unlit-Texture-Outline" { |
使用 剔除Cull 的方式 描边
这种方式的描边不适合做遮挡部分描边,且不遮挡部分的效果也没有 模板测试 那种方式好,他的原理也是使用两个pass,一个pass正常渲染,剔除背面 (Cull Back),另外一个pass 也需要顶点外拉,然后 剔除正面(Cull Front),偏移深度。这种方式会在人体内也有描边,不像 模板测试 那种方式在人体完全没有描边。
效果
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// 这种方式的描边不适合做遮挡部分描边,且不遮挡部分的效果也没有 模板测试 那种方式好
Shader "ITS/test/testOutline_cull" {
Properties{
_MainTex("Base (RGB)", 2D) = "white" {}
_OutlineColor("Outline Color", Color) = (1,1,0,1)
_Outline("Outline width", Range(0.0, 0.5)) = 0.03
}
CGINCLUDE
struct appdata_t {
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
float3 normal : NORMAL;
};
struct v2f {
float4 vertex : SV_POSITION;
half2 texcoord : TEXCOORD0;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _Outline;
float4 _OutlineColor;
v2f vert(appdata_t v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}
v2f vert_outline(appdata_t v)
{
v2f o;
// 方式一,观察空间 下往法线偏移顶点
float4 viewPos = mul(UNITY_MATRIX_MV, v.vertex);
//float3 viewNorm = mul((float3x3)UNITY_MATRIX_IT_MV, v.normal);
float3 viewNorm = mul(v.normal, (float3x3)UNITY_MATRIX_T_MV);
float3 offset = normalize(viewNorm) * _Outline;
viewPos.xyz += offset;
o.vertex = mul(UNITY_MATRIX_P, viewPos);
//方式二,世界空间 下往法线偏移顶点
//float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
//float3 worldNormal = UnityObjectToWorldNormal(v.normal);
//float3 offset = normalize(worldNormal) * _Outline;
//worldPos.xyz += offset;
//o.vertex = mul(UNITY_MATRIX_VP, worldPos);
return o;
}
ENDCG
SubShader{
Tags{ "Queue" = "Transparent" "RenderType" = "Opaque" }
Pass{
ZTest LEqual
Cull Back
CGPROGRAM
fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.texcoord);
return col;
}
ENDCG
}
Pass{
// ZTest Greater
ZWrite Off
Cull Front
Offset 100,0
CGPROGRAM
half4 frag(v2f i) : COLOR
{
return _OutlineColor;
}
ENDCG
}
}
}
ps: 还有其他描边处理的方式,比如 后处理
可以参考 浅墨 大神 的 《Real-Time Rendering 3rd》提炼总结 里的 10.3 轮廓描边的渲染方法小结 这个章节的介绍
描边的几种方法
基于视点方向的描边
NdotV, 点乘结果接近于零,那么可以断定这个表面极大概率是侧向( Edge-on)的视线方向,而就将其视做轮廓边缘,进行描边
基于过程几何方法的描边
使用两个pass绘制, 先渲染正向表面( frontfaces),再渲染背向表面( backfaces), 有两种方式让背面可见
- z-bias : 使用 z 偏置( Biasing)或者其他技术来确保这些线条恰好位于正向表面之前
- shell method : 背面pass的顶点沿法线偏移 ( 膨胀一下 )
基于图像处理的描边
可以将其理解为一种后处理操作。通过寻找相邻 Z 缓冲数值的不连续性,就可以确定大多数轮廓线的位置. 优点是描边的线宽一致,缺点是需要额外的法线和深度信息,当然,由于近年来流行的延迟渲染框架,法线和深度本来就是G-Buffer的一部分,因此往往不需要额外绘制法线和深度的信息
基于轮廓边缘检测的描边
- sobel 等算法
混和轮廓描边
其实还有一种, 就是利用 模板测试. 也是使用两个pass, 一个正常绘制pass, 然后把绘制区域的模板值 (比如1) 写入到 模板缓冲区 中, 第二个pass先顶点沿法线偏移, 然后对比 模板缓冲区 的值是否为1, 是则不渲染. 这种方式的特显是正常绘制区域内不会有描边效果. 因此也不太适合用于 卡通渲染
ue4 中, 还是有个方式就是利用 custom stencil. 可以参考: ue4-shader-自定义模板CustomStencil - https://blog.csdn.net/yangxuan0261/article/details/90067581