zt3ff3n 发表于 2022-5-3 10:54

Unity Shadows(二) cast Shadows

注意点:

在shadowCaster阶段shadow bias实现是增加z值 远离光源,使得shadowMap中存储的值变大;shadow normal bias 朝着法线反方向偏移(法线内推),同样远离光源
首先介绍几个关键宏

UNITY_REVERSED_Z :如果定义了UNITY_REVERSED_Z,那么在clip space中近剪裁面的z值为1,远剪裁面的z值为0。使用reversed-z技术的原因是为了弥补深度缓存的精度问题。由于z不是均匀分布的,离近剪裁面越近的地方,z的精度越高,而越远的地方,z的精度越低。而如果用浮点数来保存z的值,浮点数又在值靠近0时有较高的精度,靠近1时精度较差。因此,为了让其互补,就将z的取值进行reverse,这样靠近1的z值虽然浮点精度有限,但是离近剪裁面会更近;同样地,靠近0的z值虽然离近剪裁面较远,但是浮点精度更高。UNITY_NEAR_CLIP_VALUE 上面有提到dx中为0,opengl中为-1。
unity_LightShadowBias:它是一个四维向量,保存与阴影相关的参数。这个值与光源类型有关,先看平行光源的情况。当平行光源的bias值设置为0.05时,打开frame debugger定位到shadow caster阶段:



此时unity_LightShadowBias.x是一个比较小的负数,而不是简单的0.05。通过反复实验和猜测得出:
unity_LightShadowBias.x = -UNITY_MATRIX_P._33 * _Bias代入截图中的值计算,-0.026 * 0.05 = -0.0013刚刚好。
再看聚光灯的情况,同样当聚光灯的bias值设置为0.05时,打开frame debugger定位到shadow caster阶段:此时unity_LightShadowBias.x就是_Bias的值单纯取反得到。


最后看一下点光源:


这个就十分nice了,直接就是_Bias的值。
unity_LightShadowBias.z存储了与normal dias相关的参数。这个值还与shadow map的贴图尺寸有关。而且通过实践可以发现,这个值只在平行光源的情况下有效,其他光源都是0。
v2f结构定义V2F_SHADOW_CASTER

// Declare all data needed for shadow caster pass output (any shadow directions/depths/distances as needed),
// plus clip space position.
#define V2F_SHADOW_CASTER V2F_SHADOW_CASTER_NOPOS UNITY_POSITION(pos)顶点函数:TRANSFER_SHADOW_CASTER、TRANSFER_SHADOW_CASTER

if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
#define TRANSFER_SHADOW_CASTER_NOPOS(o,opos)
    //记录光源到顶点的位置向量
    o.vec = mul(unity_ObjectToWorld, v.vertex).xyz - _LightPositionRange.xyz;
    //顶点转换,不多说
    opos = UnityObjectToClipPos(v.vertex);
#else
#define TRANSFER_SHADOW_CASTER_NOPOS(o,opos)         
    opos = UnityClipSpaceShadowCasterPos(v.vertex, v.normal);
    opos = UnityApplyLinearShadowBias(opos);
#endifUnityClipSpaceShadowCasterPos源码

float4 UnityClipSpaceShadowCasterPos(float4 vertex, float3 normal)
{
    float4 wPos = mul(unity_ObjectToWorld, vertex);       
    if (unity_LightShadowBias.z != 0.0)
    {
      float3 wNormal = UnityObjectToWorldNormal(normal);
      float3 wLight = normalize(UnityWorldSpaceLightDir(wPos.xyz));
         // apply normal offset bias (inset position along the normal)
      // bias needs to be scaled by sine between normal and light direction
      // (Shadow Mapping Summary - Part 1)
      //
      // unity_LightShadowBias.z contains user-specified normal offset amount
      // scaled by world space texel size.

      float shadowCos = dot(wNormal, wLight);
      float shadowSine = sqrt(1-shadowCos*shadowCos);
      //上面两行呢,是为了求sin(灯光和法线夹角),因为1=sin^2+cos^2,所以sin=sqrt(1-con^2)
      //我们为什么要求sin,上一篇我们说过,我们的bias的偏移值要和法线以及法线和灯光的角度相关,
      //那么是什么关系呢?夹角越大,偏移越大。夹角为0偏移为0。所以sin函数的曲线完全符合。
      float normalBias = unity_LightShadowBias.z * shadowSine;       
      //下面就是朝着法线反方向偏移,现在我们是在做渲染深度图,在这里做了深度偏移
      wPos.xyz -= wNormal * normalBias;
    }
    //将偏移后的顶点,转换到投影空间       
    return mul(UNITY_MATRIX_VP, wPos);
}我们要求的normal bias就是CG的长度,它等于DI:
https://www.zhihu.com/equation?tex=+CG+%3D+DI+%3D+DH+%5Ccdot+sin%5Ctheta+%3D+%5Cdfrac%7B1%7D%7B2%7DAB+%5Ccdot+sin%5Ctheta+
AB其实是光源视锥体大小与shadowmap尺寸的比值,可以理解成是shadowmap的一个texel所能覆盖的视锥体区域。这里也可以看出,shadowmap的精度越高,覆盖的光源视锥体区域越小,引起shadow acne的可能性也越小。
https://www.zhihu.com/equation?tex=+AB+%3D+%5Cdfrac%7BfrustumSize%7D%7BshadowMapSize%7D+
从图中容易看出$\theta$其实就是光源与法线的夹角,所以:
https://www.zhihu.com/equation?tex=sin%5Ctheta+%3D+%5Csqrt%7B1+-+%28N+%5Ccdot+L%29%5E2%7D+
目前尚不清楚Unity是如何计算frustumSize的,但在某些情况下,这个frustumSize就是:
frustumSize = 2 / UNITY_MATRIX_P._11
也就得到:
https://www.zhihu.com/equation?tex=+CG+%3D+%5Cdfrac%7B1%7D%7BMatrixP_%7B11%7D+%5Ccdot+shadowMapSize%7D+%5Ccdot+sin%5Ctheta++
正弦前面这货就是unity_LightShadowBias.z(可能不准确)。
UnityApplyLinearShadowBias源码:

将UnityApplyLinearShadowBias函数得到的剪裁空间坐标的z值在做一定的增加,因为这个增加操作是在剪裁空间这样的齐次坐标下进行的,所以要对透视投影产生的z值进行补偿,使得阴影偏移值不会随着摄像机的距离的变化而变化,同时必须保证增加的z值不能超过剪裁空间的远近截面的z值。
// 光源相机
float4 UnityApplyLinearShadowBias(float4 clipPos)
{
#if !(defined(SHADOWS_CUBE) && defined(SHADOWS_CUBE_IN_DEPTH_TEX))
    #if defined(UNITY_REVERSED_Z)
//远离相机那么就要减小z值
      clipPos.z += max(-1, min(unity_LightShadowBias.x / clipPos.w, 0));
    #else
//远离相机那么就要增加值
      clipPos.z += saturate(unity_LightShadowBias.x/clipPos.w);
    #endif
#endif

//接下来的操作就是为了不让我们计算出z值越界,防止裁掉平行光的背面
#if defined(UNITY_REVERSED_Z)
//
    float clamped = min(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE);
#else
//
    float clamped = max(clipPos.z, clipPos.w*UNITY_NEAR_CLIP_VALUE);
#endif
    clipPos.z = lerp(clipPos.z, clamped, unity_LightShadowBias.y);
    return clipPos;
}1)unity_LightShadowBias.x / clipPos.w的处理,这就有点奇怪了,一般除w的操作,都是在fragement shader中出现的,这里还是vertex shader阶段,除w作用是什么呢?由于光源空间的投影可能有两种,正交投影和透视投影,因此让我们分别来看下。
首先是正交投影,平行光源就是正交投影。我们可以打开frame debugger确认一下:


把矩阵写成数学形式为:
https://www.zhihu.com/equation?tex=M+%3D++%5Cbegin%7Bbmatrix%7D++a+%26+0+%26+0+%26+0+%5C+%5C%5C+0+%26+b+%26+0+%26+0+%5C+%5C%5C+0+%26+0+%26+c+%26+d+%5C+%5C%5C+0+%26+0+%26+0+%26+1++%5Cend%7Bbmatrix%7D
显然这是一个正交投影矩阵。对于相机空间中的任一点,变换到齐次剪裁空间后的坐标为:
https://www.zhihu.com/equation?tex=P%27+%3D+MP%5ET+%3D+%5Cbegin%7Bbmatrix%7D++a+%26+0+%26+0+%26+0+%5C+%5C%5C+0+%26+b+%26+0+%26+0+%5C+%5C%5C+0+%26+0+%26+c+%26+d+%5C+%5C%5C+0+%26+0+%26+0+%26+1++%5Cend%7Bbmatrix%7D+%5Ccdot+%5Cbegin%7Bbmatrix%7D+x++%5C+y++%5C+z++%5C+1+%5Cend%7Bbmatrix%7D+%3D+%28ax%2C+by%2C+cz+%2B+d%2C+1%29%5ET
可以发现,对于正交投影,clipPos.w的值为1。所以除w这件事情对正交投影压根没有影响。那么,在先不考虑各种边界的情况下,就有:
clipPos.z += -UNITY_MATRIX_P._33 * _Bias;这里Unity需要乘上一个系数的原因就真相大白了。带有负号是因为RESERVE Z,这里不必考虑,_Bias这个值是暴露给使用者的参数,它的自身意义就是让物体在光源空间中往后偏移一个量,而无需考虑光源空间本身,换句话说就是物体在光源空间中延z方向往后偏移 (减去bias 所以是沿着-z方向) ,那么它在齐次剪裁空间中z的偏移量 是多少?这个很好计算

https://www.zhihu.com/equation?tex=P_1+%3D+%28x%2Cy%2Cz%2C1%29+%5C+%5C%5C+P_2+%3D+%28x%2Cy%2Cz%2B%5CDelta+z%2C+1%29+%5C+%5C%5C+P%27_1+%3D+%28ax%2Cby%2Ccz%2Bd%2C1%29+%5C+%5C%5C+P%27_2+%3D+%28ax%2Cby%2Cc%28z%2B%5CDelta+z%29+%2Bd%2C+1%29+%3D+P%27_1+%2B+%280%2C0%2Cc%5CDelta+z%2C+0%29++
也就是说,在齐次空间中,要让z的值偏移 https://www.zhihu.com/equation?tex=c%5CDelta+z 才行。这个c恰恰就是上面提到的UNITY_MATRIX_P._33!Unity选择直接参数传递而不是在shader中计算的原因,猜测是传递不必要的矩阵到GPU上是一种浪费,而且对GPU而言这本身就是个常量,没必要在每个顶点上都去算一遍。
再来看透视投影,聚光灯就是透视投影。我们可以打开frame debugger确认一下:



把矩阵写成数学形式为:
https://www.zhihu.com/equation?tex=+M+%3D+%5Cbegin%7Bbmatrix%7D++a+%26+0+%26+0+%26+0+%5C%5C%5C+0+%26+b+%26+0+%26+0+%5C+%5C%5C+0+%26+0+%26+c+%26+d+%5C+%5C%5C+0+%26+0+%26+-1+%26+0++%5Cend%7Bbmatrix%7D++
显然这是一个透视投影矩阵。对于相机空间中的任一点,变换到齐次剪裁空间后的坐标为:
https://www.zhihu.com/equation?tex=P%27+%3D+MP%5ET+%3D+%5Cbegin%7Bbmatrix%7D++a+%26+0+%26+0+%26+0+%5C%5C%5C++0+%26+b+%26+0+%26+0+%5C%5C%5C++0+%26+0+%26+c+%26+d+%5C+%5C%5C+0+%26+0+%26+-1+%26+0++%5Cend%7Bbmatrix%7D+%5Ccdot++%5Cbegin%7Bbmatrix%7D++x+++%5C%5C%5C+y++%5C%5C%5C+z++%5C%5C%5C+1+%5Cend%7Bbmatrix%7D+%3D+%28ax%2C+by%2C+cz+%2B+d%2C+-z%29%5ET+
同样,在先不考虑各种边界的情况下,就有:
clipPos.z += -_Bias / -viewPos.z;是因为相机空间是右手坐标系,相机看向的是z轴负方向,所以viewPos.z < 0,而在剪裁空间又是左手坐标系,需要对z轴取反。
类似正交投影,我们假设物体在光源空间中延z方向往后偏移,那么它在齐次剪裁空间中z的偏移量是多少?

https://www.zhihu.com/equation?tex=+P_1+%3D+%28x%2Cy%2Cz%2C1%29+%5C+%5C%5C+P_2+%3D+%28x%2Cy%2Cz%2B%5CDelta+z%2C+1%29+%5C+%5C%5C+P%27_1+%3D+%28ax%2Cby%2Ccz%2Bd%2C-z%29+%5C+%5C%5C+P%27_2+%3D+%28ax%2Cby%2Cc%28z%2B%5CDelta+z%29+%2Bd%2C+-%28z+%2B+%5CDelta+z%29%29++

好像没那么直观,让我们继续往下推导:

https://www.zhihu.com/equation?tex=P%27_2+%3D+%28%5Cdfrac%7Bax+%5Ccdot+%28-z%29%7D%7B-%28z+%2B+%5CDelta+z%29%7D%2C+%5Cdfrac%7Bby+%5Ccdot+%28-z%29%7D%7B-%28z+%2B+%5CDelta+z%29%7D%2C+%5Cdfrac%7B%28c%28z%2B%5CDelta+z%29+%2Bd%29+%5Ccdot+%28-z%29%7D%7B-%28z+%2B+%5CDelta+z%29%7D%2C+-z%29++

这回x和y都变了。但其实也是正常的,毕竟对于透视投影来说,将一个物体往z方向平移,投影的位置还和位移前相同,那么它的x,y方向也需要平移。不过,这里我们不需要考虑x和y的部分。继续放大z的计算部分往下看:

https://www.zhihu.com/equation?tex=%5CDelta+z%27+%3D+%5Cdfrac%7B%28c%28z%2B%5CDelta+z%29+%2Bd%29+%5Ccdot+%28-z%29%7D%7B-%28z+%2B+%5CDelta+z%29%7D+-+%28cz+%2Bd%29+%5C%5C%5C+%3D+%5Cdfrac%7B%28c%28z%2B%5CDelta+z%29+%2Bd%29+%5Ccdot+z+-+%28cz%2Bd%29%28z%2B%5CDelta+z%29%7D%7B%28z+%2B+%5CDelta+z%29%7D+%5C%5C%5C+%3D+%5Cdfrac%7B-d%5CDelta+z%7D%7Bz+%2B+%5CDelta+z%7D+

对于同一个光源空间来说,d其实是一个常数,为了计算方便可以直接拿掉,而和z本身相比,可以忽略不计,所以有:

https://www.zhihu.com/equation?tex=%5CDelta+z%27+%3D+%5Cdfrac%7B-d%5CDelta+z%7D%7Bz+%2B+%5CDelta+z%7D+%5Capprox+%5Cdfrac%7B-%5CDelta+z%7D%7Bz%7D+
这就和代码中的描述一致了。其实从直观上也好理解,这步操作是为了让物体在光源空间的不同位置往后偏移时,都能偏移相同的一个量,因为透视投影具有近大远小的性质,除z就是做了一个透视补偿。
再往下看代码,这里定义了一个clamped的分量,它在unity_LightShadowBias.y为1的时候生效。通过查阅资料可以知道,这个值只有在平行光源的情况为1,其他情况都为0。clamped所做的事情就是让clipPos.z不要超过近剪裁面,代表的是光源背面(近似)的点的z值。而平行光源是不存在光源背面这一概念的,理论上只要位于平行光源的光源空间内,就一定要跑一遍shadow caster。所以这么做的原因是为了防止裁掉平行光“背面”的点。
片元函数 SHADOW_CASTER_FRAGMENT

简单粗暴,只有一个宏:SHADOW_CASTER_FRAGMENT(i);平行光返回0,我们这是在投射阴影,所以只需要保存深度,不需要颜色。但是使用CubeMap来做阴影缺不一样,需要把z值写入cubemap中。UnityEncodeCubeShadowDepth把浮点数编码成RGBA形式存入图中。EncodeFloatRGBA一般用于2D纹理的浮点数压缩,自定义阴影可以使用这个。
#if defined(SHADOWS_CUBE) && !defined(SHADOWS_CUBE_IN_DEPTH_TEX)
   #define SHADOW_CASTER_FRAGMENT(i)
   //_LightPositionRange.w=1/range
   return UnityEncodeCubeShadowDepth ((length(i.vec) + unity_LightShadowBias.x) * _LightPositionRange.w);
#else
   #define SHADOW_CASTER_FRAGMENT(i) return 0;
#endif
页: [1]
查看完整版本: Unity Shadows(二) cast Shadows