Skip to content

ShadowPCF

zilch edited this page May 7, 2021 · 1 revision

[七之上] 软阴影的PCF实现

在先前的篇章中只实现了硬阴影,接下来准备着手实现软阴影。实现软阴影的有很多种,例如PCF(Percent Closer Filter)、VSM(Variance Shadow Map)、ESM(Exponential Shadow Map)。这几种算法的简介,在知乎有篇不错的文章如下:

游戏中的阴影(一):基础

本章计划使用PCF来实现SRP中的软阴影。

由于本文将要使用的PCF算法介绍起来比较复杂,因此独立成篇。Git版本地址如下:

阴影的PCF采样优化算法 - Git版本

如果Git版本的图片或者公式显示异常的话,也可以看知乎专栏版本:

阴影的PCF采样优化算法 - 知乎版本

接下来默认大家已经阅读完以上算法的详细理论内容,这里将侧重于SRP中代码的实现。

1. 增加配置项

本章将会实现多个不同kernel size的PCF软阴影,因此首先在ShadowSetting中加入如下枚举:

public enum ShadowAAType{
    None = 0,
    PCF1,
    PCF3Fast,
    PCF3,
    PCF5,
}

用以切换管线采取的Shadow AA策略。对应的管线配置界面上会出现如下的可选项:

对应的在ShadowCasterPass中将相关配置同步到Shader中:

private void ConfigShadowAAParams(CommandBuffer commandBuffer,ShadowSetting setting){
    var shadowAA = setting.shadowAAType;
    var isPCFEnabled = ShadowUtils.IsPCFEnabled(shadowAA);
    Utils.SetGlobalShaderKeyword(commandBuffer,ShaderKeywords.ShadowPCF,isPCFEnabled);
    if(isPCFEnabled){
        commandBuffer.SetGlobalVector(ShaderProperties.ShadowAAParams,new Vector4((int)shadowAA,0,0,0));
    }
}

相应的,在Shader中,我们有:

  • 使用shader keyword: X_SHADOW_PCF 来控制PCF的开关
  • 使用_ShadowAAParams.x来表明当前使用的ShadowAAType

2. Shader实现

增加ShadowTentFilter.hlsl文件,里面实现了以上文章中提到的几种PCF,对应如下:

  • ShadowAAType.PCF1对应SampleShadowPCF函数,执行一次采样,覆盖4个像素.
  • ShadowAAType.PCF3Fast对应SampleShadowPCF3x3_4Tap_Fast函数,执行4次采样,覆盖9个像素。4次采样点的uv只作固定偏移,但效果一般般。
  • ShadowAAType.PCF3对应SampleShadowPCF3x3_4Tap函数,执行4次采样,覆盖12个像素。按照连续TentFilter卷积计算出来的权重来得到4次采样的uv。有较好的渐变效果。
  • ShadowAAType.PCF5对应SampleShadowPCF5x5_9Tap函数,算法同ShadowAAType.PCF3。

每种PCF采样函数的详细实现代码可参考源文件。

综合之后,阴影强度采样函数如下:

///采样阴影强度,返回区间[0,1]
float SampleShadowStrength(float3 uvd){
    #if X_SHADOW_PCF
        float atten = 0;
        if(_ShadowAAParams.x == 1){
            atten = SampleShadowPCF(uvd);
        }else if(_ShadowAAParams.x == 2){
            atten = SampleShadowPCF3x3_4Tap_Fast(uvd);
        }else if(_ShadowAAParams.x == 3){
            atten = SampleShadowPCF3x3_4Tap(uvd);
        }else if(_ShadowAAParams.x == 4){
            atten = SampleShadowPCF5x5_9Tap(uvd);
        }else{
            atten = SampleShadowPCF(uvd);
        }
        return 1 - atten;
    #else
        float depth = UNITY_SAMPLE_TEX2D(_XMainShadowMap,uvd.xy);
        #if UNITY_REVERSED_Z
        //depth > z
        return step(uvd.z,depth);
        #else   
        return step(depth,uvd.z);
        #endif

    #endif
}

3. 跨平台要注意的点

本项目仅作学习之用,因此没有考虑平台兼容性。实际上,在一些老式的平台上,例如Android OpenGLES 2.0,对Hardware的PCF支持的不是很好。

所以好几年前Unity的发过一个声明:

停止在android OpenGL ES 2.0上支持“native shadow maps”   阴影映射可以通过以下两种方式实现:一种是使用”本地GPU支持“从阴影贴图(shadowmap)中采样后直接返回”阴影值“,也可能使用硬件的PCF过滤,)另一种是”手动“进行(从阴影贴图中获取深度信息,与像素深度做比较来决定是否在阴影范围内)。   第一种形式通常被喜欢更好一些,尤其因为很多GPUs提供“免费的”2*2 PCF filtering过滤. 在大多数平台式上,我们预先知道哪些阴影模式(shadow modes)受到支持,但是Android OpenGLES 2.0是奇怪的,因为一些设备支持“本地阴影贴图(native shadow maps)”,但是一些别的设备不支持。这就意味着所有shadow阴影相关的着色器,如在Android ES 2.0上,我们都要编译成两种着色器,以涵盖这两种情况。   然而,我们看见发现数据显示在Android上支持EXT_shadow_sampler的特别少(所有设备中有1-2%)。因此我们认为直接移除对它的支持是可以的。我们会一直认为把Android ES 2.0看成“在阴影上执行手动深度比较(manualdepth comparison for shadows)” 的平台。

同时,URP中使用了很多宏定义来照顾多平台之间的兼容性,可以在URP Package的ShaderLibrary/API文件夹中进行相关参考

Clone this wiki locally