US4L3——遮挡半透明效果

遮挡半透明效果

在游戏开发中,遮挡半透明效果就是物体被挡住的部分,也能呈现出一种半透明效果而被看到(具体效果可以自定义)
比如当角色在建筑物之间穿行时,被遮挡部分能够呈现出半透明效果而被我们看到。
遮挡半透明效果包含了两种显示效果,即挡住和没被挡住的部分,没被遮挡的部分正常显示,被遮挡的部分自定义显示(半透明、X光等等)

imageimage

遮挡半透明效果的基本原理

一句话描述它的基本原理:

两个 Pass​ 渲染对象,一个 Pass​ 用于渲染被遮挡部分,一个 Pass​ 用于渲染未遮挡模型。
被遮挡部分通过修改深度测试规则和关闭深度写入达到目的。

关键点:

  • 如何渲染遮挡部分
  • 如何渲染未遮挡部分

首先进行一个关于 深度测试 和 深度写入 的知识回顾,
我们可以设置 Pass​ 的深度测试规则,只有通过了深度测试,该 Pass​ 才会执行进行渲染

1
2
3
4
5
6
7
ZTest Less        //小于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
ZTest Greater //大于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
ZTest LEqual //小于等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
ZTest GEqual //大于等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
ZTest Equal //等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
ZTest NotEqual //不等于当前深度缓冲中的值,就通过测试,写入到深度缓冲中
ZTest Always //始终通过深度测试写入深度缓冲中

不设置的话,默认为 ZTest LEqual​ ,即小于等于当前深度缓冲中的值,就通过测试,
我们可以使用 ZWrite On​ 或 ZWrite Off​ 来决定是否将通过深度测试的值写入到缓冲区

因此,借助对深度测试与深度写入的修改,上文的两个关键点的实现方式如下:

  • 如何渲染遮挡部分

    在第一个 Pass​ 中,我们按照想要的遮挡效果去实现 Shader 即可(比如半透明或X射线效果)
    最关键的点,是需要修改该 Pass的深度测试规则为 ZTest Greater,并且关闭深度写入!

    • 修改深度测试的目的:只有自己的深度值大于深度缓冲中的值才渲染,相当于只有前方有遮挡时才渲染

    • 关闭深度写入的目的:为例防止被遮挡的部分后续的 Pass​ 通过测试

      如果前方有遮挡,又写入较大的深度值,执行第二个 Pass​ 时会通过深度写入,
      导致第二个 Pass​ 的内容通过深度测试,呈现出未被遮挡的错误效果!

      错误的效果如下:

      image

    1
    2
    3
    4
    5
    Pass
    {
    ZTest Greater
    ZWrite Off
    }

    Pass​ 应当实现的效果:

    fed30cdf524a9eeeeb80bcd3d71a7396

  • 如何渲染未遮挡部分

    在第二个 Pass​ 中,我们只需要正常按需求(是否受光照影响等)渲染模型即可,不需要做什么特别处理

自定义半透明效果

普通的半透明效果非常容易实现,只需要设置混合因子即可。而要实现类似 X 射线的效果,也就是边缘不透明,中间透明的效果
我们可以利用之前学习过的菲尼尔反射的公式来得到,详见:US3S8L6——菲涅尔反射

Schlick 菲涅耳近似等式为:

R(θ)=R0+(1R0)(1cos(θ))5R(θ)=R0+(1R0)(1VN)5\begin{align*} R(\theta) & = R_0 + (1 - R_0)(1-cos(\theta))^5 \\ \Rightarrow R(\theta) & = R_0 + (1 - R_0)(1-V \cdot N)^5 \end{align*}

其中:

  • R(θ)R(\theta) 表示入射角为 θ\theta 时的反射率
  • R0R_0 是垂直入射某介质时的反射率
  • VV 是视角方向单位向量(入射角)
  • NN 是顶点法线单位向量

image

将得到的入射角为 θ\theta 的反射率作为自定义颜色的 A 值,便可以得到类似的结果。

遮挡半透明效果的具体实现 —— 实现基础半透明效果

  1. 新建 Shader 取名 BaseOcclusionTransparent​ 删除无用代码

  2. 声明属性 映射属性

    • 主纹理
    • 透明度
    1
    2
    3
    4
    5
    6
    7
    8
    Properties
    {
    _Color("MainColor", Color) = (1, 1, 1, 1)
    _MainTex("MainTex", 2D) = ""{}
    _BumpMap("BumpMap", 2D) = ""{}
    _BumpScale("BumpScale", Range(0, 1)) = 1
    _CoveredAlpha("CoveredAlpha", Range(0, 1)) = 0.5
    }
  3. 复制一个 Pass

  4. 第一个 Pass

    1. 关键点 深度测试改为大于 关闭深度写入 传统透明混合因子

      • ZTest Greater

      • ZWrite Off

      • Blend SrcAlpha OneMinusSrcAlpha

        混合后的颜色 = (源颜色源Alpha) + (目标颜色 (1 − 源Alpha))

    2. 传统纹理采样实现,在片元着色器返回颜色中透明通道用透明度变量

    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
    Pass
    {
    ZTest Greater
    ZWrite Off
    Blend SrcAlpha OneMinusSrcAlpha

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"

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

    sampler2D _MainTex;
    float4 _MainTex_ST;
    fixed _CoveredAlpha;

    v2f vert (appdata_base v)
    {
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
    return o;
    }

    fixed4 frag (v2f i) : SV_Target
    {
    fixed4 color = tex2D(_MainTex, i.uv);
    return fixed4(color.rgb, _CoveredAlpha);
    }
    ENDCG
    }
  5. 第二个 Pass​,使用传统纹理采样实现

    这里直接复用标准漫反射的两个Pass,代码详见:US3S7L1——标准漫反射Shader

    1
    2
    3
    4
    5
    // 基础渲染通道 Base Pass
    UsePass "TeachShader/BumpedDiffuse/BasePass"

    // 附加渲染通道 Additional Pass
    UsePass "TeachShader/BumpedDiffuse/AdditionalPass"

完整 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
Shader "TeachShader/BaseOcclusionTransparent"
{
Properties
{
_Color("MainColor", Color) = (1, 1, 1, 1)
_MainTex("MainTex", 2D) = ""{}
_BumpMap("BumpMap", 2D) = ""{}
_BumpScale("BumpScale", Range(0, 1)) = 1
_CoveredAlpha("CoveredAlpha", Range(0, 1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Opaque" }

Pass
{
ZTest Greater
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha

CGPROGRAM
#pragma vertex vert
#pragma fragment frag

#include "UnityCG.cginc"

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

sampler2D _MainTex;
float4 _MainTex_ST;
fixed _CoveredAlpha;

v2f vert (appdata_base v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
return o;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 color = tex2D(_MainTex, i.uv);
return fixed4(color.rgb, _CoveredAlpha);
}
ENDCG
}

// 基础渲染通道 Base Pass
UsePass "TeachShader/BumpedDiffuse/BasePass"

// 附加渲染通道 Additional Pass
UsePass "TeachShader/BumpedDiffuse/AdditionalPass"
}
}

显示效果(透明度0.3):

image

可见,被遮挡部分呈现出了半透明效果,使得我们可以看到被遮挡部分的颜色

遮挡半透明效果的具体实现 —— 实现X射线半透明效果

  1. 新建 Shader 取名 XRayOcclusionTransparent​,删除无用代码

  2. 声明属性 映射属性

    • 主纹理
    • 自定义颜色
    • 菲涅尔反射率
    • 菲涅尔 n 次方
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Properties
    {
    _Color("MainColor", Color) = (1, 1, 1, 1)
    _MainTex("MainTex", 2D) = ""{}
    _BumpMap("BumpMap", 2D) = ""{}
    _BumpScale("BumpScale", Range(0, 1)) = 1
    _FresnelScale("FresnelScale", Range(0, 1)) = 1 // 菲涅尔反射垂直角度的反射率
    _FresnelPow("FresnelPow", Range(0, 5)) = 5 // 菲涅尔反射近似公式中的n次方
    _CoveredColor("CoveredColor", Color) = (1, 1, 1, 1) // 被遮挡部分显示的颜色
    }
  3. 复制一个 Pass

  4. 第一个 Pass

    1. 关键点 深度测试改为大于 关闭深度写入 传统透明混合因子

      • ZTest Greater

      • ZWrite Off

      • Blend SrcAlpha OneMinusSrcAlpha

        混合后的颜色 = (源颜色源Alpha) + (目标颜色 (1 − 源Alpha))

    2. 结构体中

      顶点、世界空间法线、世界空间视角方向

    3. 顶点着色器

      顶点变换、得到世界空间法线、世界空间视角方向

    4. 片元着色器

      利用菲涅尔公式带入自定义参数计算,将得到的值作为自定义颜色的透明通道值

    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
    // 被遮挡部分渲染通道 Covered Pass
    Pass
    {
    ZTest Greater
    ZWrite Off
    Blend SrcAlpha OneMinusSrcAlpha

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag

    #include "UnityCG.cginc"

    struct v2f
    {
    float4 pos : SV_POSITION; // 裁剪空间下顶点坐标
    float3 worldNormal: NORMAL; // 世界空间下的法线
    float3 worldViewDir: TEXCOORD0; // 世界空间下的视角方向
    };

    fixed _FresnelScale;
    fixed _FresnelPow;
    fixed4 _CoveredColor;

    v2f vert (appdata_base v)
    {
    v2f data;
    data.pos = UnityObjectToClipPos(v.vertex); // 顶点坐标转裁剪坐标
    data.worldNormal = UnityObjectToWorldNormal(v.normal); // 顶点法线转世界坐标
    fixed3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; // 顶点坐标装世界坐标
    data.worldViewDir = UnityWorldSpaceViewDir(worldPos); // 计算世界空间下视角所在方向

    return data;
    }

    fixed4 frag (v2f i) : SV_Target
    {
    // 根据schlick菲涅尔近似公式,计算菲涅尔反射率
    fixed fresnal =
    _FresnelScale + (1 - _FresnelScale) * pow((1 - dot(normalize(i.worldViewDir), normalize(i.worldNormal))), _FresnelPow);
    return fixed4(_CoveredColor.rgb, fresnal);
    }
    ENDCG
    }

  5. 第二个Pass,传统纹理采样实现

    这里直接复用标准漫反射的两个Pass,代码详见:US3S7L1——标准漫反射Shader

    1
    2
    3
    4
    5
    // 基础渲染通道 Base Pass
    UsePass "TeachShader/BumpedDiffuse/BasePass"

    // 附加渲染通道 Additional Pass
    UsePass "TeachShader/BumpedDiffuse/AdditionalPass"

显示效果(菲涅尔反射率为0.1,菲涅尔n次方为5):

image

可见,我们可以自定义被遮挡部分的渲染效果