1 阴影原理

光源照射到不透明物体上,会向该物体的后面投射阴影,如果阴影区域存在其他物体,这些物体不被光源照射的部分就需要渲染阴影。因此,我们可以将阴影的生成抽象出 2 个流程:物体投射阴影、物体接收阴影。

unity影子可以模糊吗_#pragma

1.1 阴影相关开关

1)开启 Light 组件渲染阴影

unity影子可以模糊吗_d3_02

  • No Shadows:不渲染阴影
  • Hard Shadows:硬阴影(阴影边缘较清晰)
  • Soft Shadows:软阴影(阴影边缘较模糊)

2)开启投射阴影 / 接收阴影

unity影子可以模糊吗_#pragma_03

  • Cast Shadows:投射阴影,取值有 Off(关闭投射)、On(开启投射)、Two Sided(双面都可以投射阴影)、Shadows Only(只投射阴影,但物体不可见)
  • Receive Shadows:接收阴影

注意:投射阴影和接收阴影开关可以独立控制,不相互依赖,因此,一个物体可以只投射阴影而不接收阴影,也可以只接收阴影而不投射阴影。

1.2 物体投射阴影

1)阴影映射纹理

物体的 MeshRenderer 组件开启了 Cast Shadows 后,Unity 会把该物体加入到光源的阴影映射纹理(Shadow Map)的计算中。阴影映射纹理是一张深度图,它记录了从光源位置能看到的顶点的位置和深度,即将相机放在光源位置渲染的深度纹理。

unity影子可以模糊吗_unity影子可以模糊吗_04

2)屏幕空间的阴影映射纹理

Unity 5 中,Unity 使用了不同于这种传统的阴影采样技术,即屏幕空间的阴影映射技术(Screenspace Shadow Map)。其原理是:先生成相机的深度纹理和灯光的阴影映射纹理,对于场景中任意一点,以其深度纹理的 x、y 值和阴影映射纹理的 z 值作为该点的屏幕空间阴影映射纹理。注意:Unity 并没有在所有平台上都使用了这种技术并,需要显卡支持 MRT,有些移动平台就不支持该特性。

3)ShadowCaster Pass

正常的渲染流程会调用 LightMode 标签为 ForwardBase 和 Additional 的 Pass,而深度映射纹理是一张深度图,不需要渲染顶点颜色,为节省性能,Unity 设计了 LightMode 为 ShadowCaster 的 Pass 专门用于渲染阴影纹理。如果当前 Shader 文件中没有 LightMode 为 ShadowCaster 的 Pass,就去 Fallback 指定的 Shader 中继续寻找,如果仍没有找到,就继续去 Fallback 里寻找,直到找到了 ShadowCaster Pass 或没有 Fallback,如果最后找到了,Unity 会使用该 Pass 更新光源的阴影映射纹理。

4)渲染阴影映射纹理

对于阴影映射纹理的渲染,Unity 封装得较好,用户只需要在 Shader 的 Fallback 中指定 Diffuse、Specular 或 VertexLit 等,如下:

Fallback "Specular"

内置的 Specular 中没有 ShadowCaster Pass,其 Fallback 为 VertexLit(Shader 见:Unity Editor 安装目录下的 Editor\Data\built-in-shaders-xxx\Shaders\DefaultResourcesExtra\Normal-VertexLit.shader 文件,built-in-shaders),VertexLit 中有 ShadowCaster Pass,如下:

// Pass to render object as a shadow caster
Pass {
    Name "ShadowCaster"
    Tags { "LightMode" = "ShadowCaster" }
 
    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_shadowcaster
    #include "UnityCG.cginc"
 
    struct v2f {
        V2F_SHADOW_CASTER;
    };
 
    v2f vert(appdata_base v) {
        v2f o;
        TRANSFER_SHADOW_CASTER_NORMALOFFSET(o)
        return o;
    }
 
    float4 frag(v2f i) : SV_Target {
        SHADOW_CASTER_FRAGMENT(i)
    }
    ENDCG
}

1.3 物体接收阴影

对于模型的任意顶点,使用该点对应的阴影灰度值(可以从屏幕空间的阴影映射纹理中获取)乘以光照纹理,得到的就是该点最终要显示的纹理。阴影灰度值的计算如下:

struct v2f {
    float4 pos : SV_POSITION; // 裁剪空间顶点坐标
    float3 normal: Normal; // 世界空间顶点法线向量
    float3 worldPos : TEXCOORD0; // 世界空间顶点坐标
    SHADOW_COORDS(1) // 声明一个用于阴影纹理采样的uv坐标, 入参是下一个可用的插值寄存器的索引(此处下一个可用插值寄存器是TEXCOORD1, 其索引为1)
};
 
v2f vert(a2v v) {
    v2f o;
    ...
    TRANSFER_SHADOW(o); // 计算阴影纹理uv坐标
    return o;
}
 
fixed4 frag(v2f i) : SV_Target {
    ...
    // fixed shadow = SHADOW_ATTENUATION(i); // 计算阴影灰度
    // return fixed4(ambient + (diffuse + specular) * shadow, 1);
    UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); // 计算衰减因子和阴影灰度的乘积
    return fixed4(ambient + (diffuse + specular) * atten, 1);
}

说明:上述宏的定义在 AutoLight.cginc 文件中;这些宏中使用了上下文中部分变量进行相关计算(如:v2f 中的 pos),因此 pos 的名称不能改变。

2 阴影应用

Shadow.Shader

Shader "MyShader/Shadow" {
    Properties {
        _ModelColor ("Model Color", Color) = (1, 1, 1, 1) // 模型颜色
        _Specular ("Specular Color", Color) = (1, 1, 1, 1) // 镜面反射颜色
        _Gloss ("Gloss", Range(8.0, 256)) = 20 // 镜面反射光泽度
    }
 
    SubShader {
        Tags { "RenderType"="Opaque" }
 
        Pass { 
            Tags { "LightMode"="ForwardBase" }
        
            CGPROGRAM
            #pragma multi_compile_fwdbase
            #pragma vertex vert
            #pragma fragment frag
            
            #include "UnityCG.cginc"
            #include "Lighting.cginc"
            #include "AutoLight.cginc"
            
            fixed4 _ModelColor; // 模型颜色
            fixed4 _Specular; // 镜面反射颜色
            float _Gloss; // 镜面反射光泽度
            
            struct a2v {
                float4 vertex : POSITION; // 模型空间顶点坐标
                float3 normal: NORMAL; // 模型空间顶点法线向量
            };
            
            struct v2f {
                float4 pos : SV_POSITION; // 裁剪空间顶点坐标
                float3 normal : Normal; // 世界空间顶点法线向量
                float3 worldPos : TEXCOORD0; // 世界空间顶点坐标
                SHADOW_COORDS(1)
            };
 
            v2f vert(a2v v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex); // 模型空间顶点坐标变换到裁剪空间, 等价于: mul(UNITY_MATRIX_MVP, v.vertex)
                o.normal = UnityObjectToWorldNormal(v.normal); // 将模型空间法线向量变换到世界空间
                o.worldPos = mul(unity_ObjectToWorld, v.vertex); // 将模型空间顶点坐标变换到世界空间
                TRANSFER_SHADOW(o);
                return o;
            }
 
            fixed4 frag(v2f i) : SV_Target {
                fixed3 normal = normalize(i.normal); // 世界空间法线向量
                fixed3 lightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); // 世界空间灯光向量
                fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos)); // 世界空间观察向量
                fixed3 halfDir = normalize(lightDir + viewDir); // 半向量
                fixed3 albedo = _ModelColor; // 模型自身颜色
                fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT * albedo; // 环境光
                fixed3 diffuse = _LightColor0 * albedo * max(0, dot(normal, lightDir)); // 漫反射光
                fixed3 specular = _LightColor0 * _Specular * pow(max(0, dot(normal, halfDir)), _Gloss); // 镜面反射光(Blinn Phong光照模型)
                // fixed shadow = SHADOW_ATTENUATION(i); // 计算阴影灰度
                // return fixed4(ambient + (diffuse + specular) * shadow, 1);
                UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos); // 计算衰减因子和阴影灰度的乘积
                return fixed4(ambient + (diffuse + specular) * atten, 1);
            }
            
            ENDCG
        }
    }
 
    FallBack "Specular"
}

运行效果:

unity影子可以模糊吗_d3_05

3 帧调试器查看阴影绘制过程

通过 Window → Analysis → Frame Debug 打开帧调试器,如下:

unity影子可以模糊吗_unity影子可以模糊吗_06

渲染对象主要有:Camera DepthTexture(相机深度纹理)、ShadowMap(阴影映射纹理)、Screenspace ShadowMap(屏幕空间阴影映射纹理)、TempBuffer(帧缓冲区)。

1)Camera DepthTexture

unity影子可以模糊吗_unity影子可以模糊吗_07

2)ShadowMap

unity影子可以模糊吗_灰度_08

3)Screenspace ShadowMap

unity影子可以模糊吗_灰度_09

4)TempBuffer

unity影子可以模糊吗_#pragma_10