US3S6L7——透明度测试物体阴影实现

知识回顾

在游戏开发中,要让对象的某些部位完全透明而其他部位完全不透明,就会使用透明测试
这种透明需求往往不需要半透明效果,相对比较极端,只有看得见和看不见之分,比如树叶、草、栅栏等等

透明度测试基本原理:通过一个阈值来决定哪些像素应该被保留,哪些应该被丢弃

相关内容详见:US3S3L4——透明度测试

让透明度测试 Shader 投射阴影

  1. 复用之前实现的透明测试 Shader

    具体内容详见:US3S3L4——透明度测试

  2. 同样使用 FallBack​ 的形式投射阴影

    但是需要注意的是,这里 FallBack​ 的内容为:Transparent/Cutout/VertexLit
    该默认 Shader 中会把裁剪后的物体深度信息写入到 阴影映射纹理和摄像机深度图中

    注意:使用该默认 Shader 计算投射阴影时,需要使用 _Cutoff​ 属性 和 _Color​ 属性来进行相关计算
    因此我们必须保证我们的 Shader 当中有名字为 _Cutoff​ 的阈值属性 和 _Color​ 的漫反射颜色属性,否则无法得到正确阴影结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    Shader "TeachShader/Lesson69_AlphaTestShadow"
    {
    Properties
    {
    _MainTex("MainTex", 2D) = ""{} // 纹理贴图
    _Color("MainColor", Color) = (1, 1, 1, 1) // 漫反射颜色
    _SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) // 高光反射颜色
    _SpecularNum("SpecularNum", Range(0, 20)) = 15 // 光泽度
    _Cutoff("Curoff", Range(0, 1)) = 0 // 透明测试阈值
    }
    SubShader
    {
    Tags { "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}

    Pass { /*...*/ }

    Fallback "Transparent/Cutout/VertexLit"
    }
  3. 为了得到正确的阴影效果,需要将该物体的 Cast Shadows(投射阴影)属性设置为 Two Sided(双面)

    image

    这会强制让 Unity 计算阴影隐射纹理时计算所有面的深度信息。
    如果不设置,默认将物体渲染到阴影隐射纹理和摄像机深度图时只会考虑物体的正面
    背对光源的面不会参与计算,设置为双面后即可参与计算,得到正确的结果

    不开启 Two Sided 时的错误效果:

    image

    可以看到,立方体只有一部分面投射了阴影

这时,使用透明测试 Shader 的立方体就可以投射阴影了:

image

让透明度测试 Shader 接收阴影

和我们之前处理接收阴影的方式一样,主要分5步骤:

  1. 添加编译指令 #pragma multi_compile_fwdbase

    用于帮助我们编译所有变体,并且保证衰减相关光照变量能够正确赋值到对应的内置变量中

  2. 包含内置文件 #include "AutoLight.cginc"

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_fwdbase

    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    #include "AutoLight.cginc"
    // ...
    ENDCG
  3. v2f​ 结构体中声明阴影坐标宏:SHADOW_COORDS(n)

    n​ 为下一个可用的插值寄存器的索引值(v2f​ 结构体内部有几个 TEXCOORD​ 就填几)

    1
    2
    3
    4
    5
    6
    7
    8
    struct v2f
    {
    float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
    float2 uv: TEXCOORD0; // 纹理UV坐标
    float3 wNormal: NORMAL; // 世界空间下的法线
    float3 wPos: TEXCOORD1; // 世界空间下的顶点坐标
    SHADOW_COORDS(2) // 阴影坐标宏
    };
  4. 在顶点着色器中使用坐标转换宏:TRANSFER_SHADOW​

    TRANSFER_SHADOW()​ 内传入 v2f​ 结构体变量,其中 v2f​ 结构体内顶点坐标成员名必须是 pos

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    v2f vert (appdata_base v)
    {
    v2f data;
    data.pos = UnityObjectToClipPos(v.vertex); // 将模型空间下的法线转换到世界空间下
    data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 计算UV
    data.wNormal = UnityObjectToWorldNormal(v.normal); // 法线转换到世界空间
    data.wPos = mul(unity_ObjectToWorld, v.vertex); // 顶点转换到世界空间
    TRANSFER_SHADOW(data); // 阴影坐标转换宏

    return data;
    }
  5. 在顶点着色器中使用 Unity 光照衰减计算宏:UNITY_LIGHT_ATTENUATION​

    UNITY_LIGHT_ATTENUATION()​ 内分别传入衰减值变量名(宏内部自动声明,无需再自行声明),v2f​ 结构体变量,顶点世界坐标位置

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    fixed4 frag (v2f i) : SV_Target
    {
    fixed4 texColor = tex2D(_MainTex, i.uv); // 颜色纹理的颜色信息
    clip(texColor.a - _Cutoff); // A通道减去阈值传入到Clip函数内,若A值小于阈值就会被裁剪

    fixed3 albedo = texColor.rgb * _Color.rgb; // 反射率,即纹理颜色和漫反射材质颜色乘法叠加共同决定的颜色

    // 漫反射颜色
    float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); // 指向光源的方向
    fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(i.wNormal, lightDir));

    // 高光反射颜色
    float3 viewDir = normalize(UnityWorldSpaceViewDir(i.wPos)); // 视角方向
    float3 halfA = normalize(viewDir + lightDir); // 半角向量
    fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(i.wNormal, halfA)), _SpecularNum);

    // 衰减值计算
    UNITY_LIGHT_ATTENUATION(atten, i, i.wPos)

    // 最终颜色 = 环境光 * 反射颜色 + 漫反射颜色 + 高光反射颜色
    fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + (lambertColor + specularColor) * atten;

    return fixed4(color.rgb, 1);
    }

显示效果:

image

可见,现在的透明测试 Shader 可以投射和接收阴影了

完整 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
Shader "TeachShader/Lesson69_AlphaTestShadow"
{
Properties
{
_MainTex("MainTex", 2D) = ""{} // 纹理贴图
_Color("MainColor", Color) = (1, 1, 1, 1) // 漫反射颜色
_SpecularColor("SpecularColor", Color) = (1, 1, 1, 1) // 高光反射颜色
_SpecularNum("SpecularNum", Range(0, 20)) = 15 // 光泽度
_Cutoff("Curoff", Range(0, 1)) = 0 // 透明测试阈值
}
SubShader
{
Tags { "Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"}

Pass
{
Tags { "LightMode" = "ForwardBase" }
Cull Off // 关闭剔除,让模型双面都可以渲染

CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma multi_compile_fwdbase

#include "UnityCG.cginc"
#include "Lighting.cginc"
#include "AutoLight.cginc"

// 贴图纹理对应的映射成员
sampler2D _MainTex;
float4 _MainTex_ST;
// 漫反射颜色、高光反射颜色、光泽度
fixed4 _Color;
fixed4 _SpecularColor;
float _SpecularNum;
// 透明测试阈值
fixed _Cutoff;

struct v2f
{
float4 pos: SV_POSITION; // 裁剪空间下的顶点坐标
float2 uv: TEXCOORD0; // 纹理UV坐标
float3 wNormal: NORMAL; // 世界空间下的法线
float3 wPos: TEXCOORD1; // 世界空间下的顶点坐标
SHADOW_COORDS(2) // 阴影坐标宏
};

v2f vert (appdata_base v)
{
v2f data;
data.pos = UnityObjectToClipPos(v.vertex); // 将模型空间下的法线转换到世界空间下
data.uv = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; // 计算UV
data.wNormal = UnityObjectToWorldNormal(v.normal); // 法线转换到世界空间
data.wPos = mul(unity_ObjectToWorld, v.vertex); // 顶点转换到世界空间
TRANSFER_SHADOW(data); // 阴影坐标转换宏

return data;
}

fixed4 frag (v2f i) : SV_Target
{
fixed4 texColor = tex2D(_MainTex, i.uv); // 颜色纹理的颜色信息
clip(texColor.a - _Cutoff); // A通道减去阈值传入到Clip函数内,若A值小于阈值就会被裁剪

fixed3 albedo = texColor.rgb * _Color.rgb; // 反射率,即纹理颜色和漫反射材质颜色乘法叠加共同决定的颜色

// 漫反射颜色
float3 lightDir = normalize(_WorldSpaceLightPos0.xyz); // 指向光源的方向
fixed3 lambertColor = _LightColor0.rgb * albedo.rgb * max(0, dot(i.wNormal, lightDir));

// 高光反射颜色
float3 viewDir = normalize(UnityWorldSpaceViewDir(i.wPos)); // 视角方向
float3 halfA = normalize(viewDir + lightDir); // 半角向量
fixed3 specularColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(i.wNormal, halfA)), _SpecularNum);

// 衰减值计算
UNITY_LIGHT_ATTENUATION(atten, i, i.wPos)

// 最终颜色 = 环境光 * 反射颜色 + 漫反射颜色 + 高光反射颜色
fixed3 color = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo + (lambertColor + specularColor) * atten;

return fixed4(color.rgb, 1);
}
ENDCG
}
}

Fallback "Transparent/Cutout/VertexLit"
}