unity-shader-模板测试-描边

unity-shader模板测试,描边效果,常用于 rpg 项目中 主角 被遮挡的情况,将被遮挡的部分的轮廓描边绘制出来,这样可以在任何情况都能知道 主角 在哪里。(还有另外一种就是使用 X光 效果,但这种效果不需要用到模板测试,所以这里用 描边效果 举栗子)


效果

这里写图片描述

思路

实现这种效果需要两个pass

  1. 正常绘制,即深度测试 LEqual,正常遮挡,同时往 模板缓冲区 写入一个 参考值 Ref=1(即使深度测试失败的值也要写入,保证 角色所有像素在模板缓冲区的值 都是 参考值 Ref=1)
  2. 只绘制被遮挡部分的描边
    1. 先把模型的 顶点 往 法线方向 偏移一个值(这个就是描边的宽度值,可以理解为将模型放大了),可以在观察空间 或者 世界空间、模型空间 偏移,只要和 法线 在同一空间下
    2. 对比 模板缓冲区 的参考值 Ref=1,因为第一个 pass 写入的参考值是 1,所以这个pass中就要不等1才让它通过,这样就能得到一个 差值区域,即描边的区域
    3. 深度测试,让被遮挡部分才让它通过,即 ZTest Greater,done!

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
Shader "Custom/Unlit-Texture-Outline" {
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
#include "UnityCG.cginc"
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{ // 正常绘制
Stencil
{
Ref 1
Comp Always
Pass Replace
ZFail Replace
}

ZTest LEqual
CGPROGRAM
#pragma vertex vert
#pragma fragment frag

fixed4 frag(v2f i) : SV_Target
{
fixed4 col = tex2D(_MainTex, i.texcoord);
return col;
}

ENDCG
}

Pass{ // 遮挡部分绘制描边
ZTest Greater
ZWrite Off
//Blend DstAlpha OneMinusDstAlpha

Stencil{
Ref 1
Comp NotEqual

}
CGPROGRAM
#pragma vertex vert_outline
#pragma fragment frag
half4 frag(v2f i) :COLOR
{
return _OutlineColor;
}
ENDCG
}
}
}

使用 剔除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
    #include "UnityCG.cginc"
    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
    #pragma vertex vert
    #pragma fragment frag

    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
    #pragma vertex vert_outline
    #pragma fragment frag
    half4 frag(v2f i) : COLOR
    {
    return _OutlineColor;
    }
    ENDCG
    }
    }
    }

ps: 还有其他描边处理的方式,比如 后处理

可以参考 浅墨 大神 的 《Real-Time Rendering 3rd》提炼总结 里的 10.3 轮廓描边的渲染方法小结 这个章节的介绍


描边的几种方法

  1. 基于视点方向的描边

    NdotV, 点乘结果接近于零,那么可以断定这个表面极大概率是侧向( Edge-on)的视线方向,而就将其视做轮廓边缘,进行描边

  2. 基于过程几何方法的描边

    使用两个pass绘制, 先渲染正向表面( frontfaces),再渲染背向表面( backfaces), 有两种方式让背面可见

    • z-bias : 使用 z 偏置( Biasing)或者其他技术来确保这些线条恰好位于正向表面之前
    • shell method : 背面pass的顶点沿法线偏移 ( 膨胀一下 )
  3. 基于图像处理的描边

    可以将其理解为一种后处理操作。通过寻找相邻 Z 缓冲数值的不连续性,就可以确定大多数轮廓线的位置. 优点是描边的线宽一致,缺点是需要额外的法线和深度信息,当然,由于近年来流行的延迟渲染框架,法线和深度本来就是G-Buffer的一部分,因此往往不需要额外绘制法线和深度的信息

  4. 基于轮廓边缘检测的描边

    1. sobel 等算法
  5. 混和轮廓描边

  6. 其实还有一种, 就是利用 模板测试. 也是使用两个pass, 一个正常绘制pass, 然后把绘制区域的模板值 (比如1) 写入到 模板缓冲区 中, 第二个pass先顶点沿法线偏移, 然后对比 模板缓冲区 的值是否为1, 是则不渲染. 这种方式的特显是正常绘制区域内不会有描边效果. 因此也不太适合用于 卡通渲染

  7. ue4 中, 还是有个方式就是利用 custom stencil. 可以参考: ue4-shader-自定义模板CustomStencil - https://blog.csdn.net/yangxuan0261/article/details/90067581