代码部分为Unity实现屏幕空间的实时阴影
学习教程来自:【技术美术百人计划】图形 4.3 实时阴影介绍

笔记

1. 基于图片的实时阴影技术

主流方法之一,把阴影生成为一张图片

1.1 平面投影阴影


将阴影投影在一个平面上。
缺点:必须是平面,产生阴影的物体必须介于光和平面之间
为了解决上一个方法的缺点之一(只能在平面上产生阴影)。
步骤简述:为物体多设置一个相机产生阴影纹理,与被阴影覆盖的表面的纹理进行混合得到阴影效果,在Unity中使用Projector组件实现

1.2 阴影映射(Shadow Map)

概念:从光源的位置和角度获取的深度图
核心思想:对比Shadow Map和摄像机视角的深度图,片元在Shadow Map中的值小于后者时,产生阴影


1.3 屏幕空间阴影映射

Unity中的阴影映射实现(即屏幕空间的阴影映射)


步骤:

  1. 屏幕空间的深度贴图
  2. 光源方向的Shadow Map


  3. 屏幕空间下进行对1、2的结果进行计算得到屏幕空间的阴影纹理

  4. 绘制3中的结果

2. 阴影映射的优化

2.1 自阴影问题


也叫Z-Fighting、阴影瑕疵、阴影粉刺(Surface Acne),由于阴影贴图分辨率(其分辨率低,但是相机得到的深度图分辨率高)、离散采样、数值精度等问题产生的错误的自阴影
解决办法:

  1. 深度偏移(Depth Bias):设置一个差值的阈值,减少阴影的产生。太大会导致Peter Panning(阴影与投影者脱节)
  2. 法线偏移(Normal Bias):上一条中的方向为视角方向,本方法在法线方向上偏移
    补充:偏移单位为纹素(1/分辨率),只在阴影深度测试时使用,不影响其他效果

    2.2 走样问题

    由于采样产生

    2.2.1 透视走样


Shadow Map本身大小均匀,但透视投影完成后采样变得不均匀,由此产生了走样(距离观察者近的元素产生走样)
解决:

  1. 在Shadow Map生成时进行透视投影,以保持均匀性的一致
  2. 级联阴影映射(Unity的解决办法):划分视锥体,得到相同大小的Shadow Map(近处的质量更高)


我猜这也是为什么上边1.3中的Shadow Map有4个

2.2.2 重采样

采样贴图时产生的误差
解决:滤波(PCF滤波),对滤波核的每一个采样点,对比中间的值后划分为blocked和visible这2种状态,输出shadow=visible/(visible+blocked)。实现方式有很多种(不同的采样个数、不同的滤波函数)


作业

1. 总结实时阴影系统的优化方案

内容来自以上笔记:

方法名称 方法 解决的问题
深度偏移 使用一个偏移值来避免深度比较时产生的误差 自阴影
法线偏移 沿法线方向进行偏移 自阴影
透视投影 在Shadow Map生成时进行透视投影,以保持均匀性的一致 透视走样
级联阴影映射 从近到远划分视锥体,得到相同大小的Shadow Map 透视走样
PCF滤波 对阴影贴图滤波,得到shadow值 重采样

2. 自己实现阴影系统

Tips:可以把模型的背面渲染出来作为阴影的一个Pass,来优化和避免一些问题 //TODO:
做这个是四处看代码,左抄点,又抄点,混一起成了。
参考来源:
Unity的实时阴影-ShadowMap实现原理:粘了不好使(盲猜shadowmap的矩阵转换不对),但学到大概知道要这么2个shader,一个脚本
Unity基础6 Shadow Map 阴影实现:对着上边的代码看了一下每一步大概的实现
Unity实时阴影实现——Shadow Mapping:然后看到这篇,比较接近能直接来拿粘贴的程度了,但还差点,大概是在update里调用一下函数,先调哪个后调那个不太确定。
上一条作者的github:完结,里边有一个老一点版本的shadowmap实现(对应PPT,1个shadowMap的Shader,1个屏幕空间深度的Shader,1个比较这2个深度图并计算阴影的Shader),现在对着上边的文章改成在接收阴影的材质shader中比较深度计算阴影,完成实现(2个shader,1个计算shadowMap,1个计算阴影)
总结一下实现过程,好像和上边PPT讲的不太一样:

  1. 从光源方向创建相机,渲染深度图的得到shadowMap
  2. 保存一个变换矩阵_gWorldToShadow,将阴影接收者的世界空间坐标转换到shadowMap对应的空间中(和PPT中不同,这里在shadowMap对应的空间下做collector计算,而不是将shadowMap转换到屏幕空间)
  3. 转换后的坐标,xy值作为UV采样shadowmap得到sampleDepth,z作为深度depth(这些值都经过了一些处理达到0-1),这里的depth并不是渲染深度得到的,而是一个位置信息,如果大于shadowMap采样的值,证明被挡住了,应该有阴影产生
  4. 用sampleDepth和depth计算得到阴影(sampleDepth可以for循环多采样几次计算模糊)
    比较关键的地方就是把接收者的顶点位置转换到shadowMap对应的空间下(xy值作为UV采样shadowmap得到sampleDepth,z作为深度depth)
v2f vert (appdata_full v) 
{
    v2f o;
    o.pos = UnityObjectToClipPos (v.vertex);
    float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
    o.shadowCoord = mul(_gWorldToShadow, worldPos);//转换到shadowMap对应的空间

    return o; 
}

fixed4 frag (v2f i) : COLOR0 
{            
    // shadow
    i.shadowCoord.xy = i.shadowCoord.xy/i.shadowCoord.w;
    float2 uv = i.shadowCoord.xy;
    uv = uv*0.5 + 0.5; //(-1, 1)-->(0, 1)

    float depth = i.shadowCoord.z / i.shadowCoord.w;
    #if defined (SHADER_TARGET_GLSL)
    depth = depth*0.5 + 0.5; //(-1, 1)-->(0, 1)
    #elif defined (UNITY_REVERSED_Z)
    depth = 1 - depth;       //(1, 0)-->(0, 1)
    #endif

    // sample depth texture
    // 模糊前
    //float4 col = tex2D(_gShadowMapTexture, uv);
    //float sampleDepth = DecodeFloatRGBA(col);
    //float shadow = sampleDepth < depth ? _gShadowStrength : 1;
    // 模糊后
    float shadow = PCFSample(depth, uv);

    return shadow;
}
这个是shadowMap,不过是EncodeFloatRGBA之后的

上图:


刚开始有个小问题:之前建模的钢铁侠面罩模型只有一个面,没有封口,就是背面是透的,结果来到这里正面面对阳光的时候,竟然没有阴影,开了framedebug,原来是cull front去掉了正面。如图



注释掉好了,看来这个就是开头说的直接用背面作为渲染阴影的Pass了,这样做的好处猜测应该是,反正shadowMap空间下正面和背面一定会相互遮挡,不如只考虑一个面,而背面的复杂度应该又比正面低一点,所以这样是效率比较高的


最后是边缘模糊PCF Soft Shadow(Percentage Closer Filtering),代码也来自上边知乎大佬的帖子,原理就是对ShadowMap围绕着中心点采样了9次,每次都和中心点的depth比较后累加,再除9

float PCFSample(float depth, float2 uv)
{
    float shadow = 0.0;
    for (int x = -1; x <= 1; ++x)
    {
        for (int y = -1; y <= 1; ++y)
        {
            float4 col = tex2D(_gShadowMapTexture, uv + float2(x, y) * _gShadowMapTexture_TexelSize.xy);
            float sampleDepth = DecodeFloatRGBA(col);
            shadow += sampleDepth < depth ? _gShadowStrength : 1;//每一个采样点都与深度值相比较,累加
        }
    }
    return shadow /= 9;
}
模糊前

模糊后

远点效果看着还行

最后,这些代码也上传git了,感兴趣的可以拉下来看看(本篇内容在/Scene/4300)
git地址