Unity Shader学习记录(七)

Unity Shader学习记录(七)

  阴影和光照总是密不可分的,就像明亮和阴暗本身就是两种亮度的对比那样。
  在计算机图形渲染中,阴影的产生遵循着类似现实中的阴影现象的原理,也就是寻找光线无法照到的地方将其认定为阴影并予以渲染。但和现实中不同的是,很多时候计算机实时渲染并不会真的去寻找每条光线能照射到什么地方,或者说至少不是每次渲染都寻找;有一种常用的被称为“阴影映射(ShadowMap)”的技术,它的原理是在渲染开始前先找出所有无法被光线照到的地方并且标记,在随后的渲染过程中便可以避开对这些区域进行光照渲染。
  其实现过程非常简单,想象一下将摄像机放到光源位置上,那么能看到的表面自然就是有光照的地方,而无法被看到的部分就是阴影区域了。而为了做到这一点,在Shader中就需要预留出至少一个Pass来标记阴影区域了,关于这个过程,Unity有一个特定的Pass用来检测阴影。


阴影投射

  Unity使用一个标签LightMode设置为ShadowCaster的Pass来专门处理阴影映射,需要注意的是如果当前Shader中没有这个Pass,则Unity会去Fallback的Shader中寻找,如果都找不到,则使用该Shader的所有物体无法投射阴影。
  传统的阴影映射纹理实现中,物体的顶点会被转换到光源空间下,然后使用坐标的XY分量对阴影映射纹理进行采样,得到的深度值与Z分量进行对比来判定当前顶点是否处于阴影中。但Unity使用了另一种采样技术,即“屏幕空间的阴影映射”技术,这项技术是预先使用阴影映射纹理和摄像机的深度映射合并计算得到屏幕空间的阴影图,这样一来也就是将阴影信息转换到了屏幕空间下,此后渲染物体时将顶点转换到屏幕空间再进行类似的采样处理即可。
  关于阴影的接收和投射总结如下

  • 如果想要接收来自其它物体的阴影,则必须在Shader中对阴影映射纹理进行采样,把采样结果和最后的光照结果相乘来产生阴影。
  • 如果想要向其它物体投射阴影,则必须将该物体加入到光源的阴影映射纹理中,也就是保证Shader里有一个LightMode为ShadowCaster的Pass即可。

  在Unity中要让物体可以接收或者投射阴影,首先要在物体的MeshRenderer组件上设置好CastShadows和ReceiveShadows参数,
  对于标签为ShadowCaster的Pass,Unity的自带Shader实现中就有实际代码,摘抄如下

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
}

  通过这样一些代码,物体的深度信息就被写入了渲染目标中,在这里这个渲染目标可以是光源的阴影映射纹理或者是摄像机的深度纹理。
  值得注意的是,计算阴影映射纹理时默认是剔除掉了背面的,这能适应大部分模型的情况;但如果有需要对于背向光源的物体进行阴影投射的话,那么必须手动打开物体阴影的双面渲染避免出错,只需要在Unity中设置物体MeshRenderer组件的CastShadows属性为TwoSides即可。


阴影接收

  如果要一个物体可以接收其它物体投射给它的阴影,那么就需要在在Shader中对阴影映射纹理进行采样并应用到自身的光照结果上。Untiy在这个方面为开发者提供了丰富的宏方便使用,这些宏可以在AutoLight.cginc文件中找到。
  接收阴影的代码如下

Pass {
    Tags {"LightMode"="ForwardBase"}

    CGPROGRAM
    #pragma vertex vert
    #pragma fragment frag
    #pragma multi_compile_fwdbase
    #include "UnityCG.cginc"
    #include "Lighting.cginc"
    #include "AutoLight.cginc"

    float4 _Color;
    float4 _Specular;
    float _Gloss;

    struct a2v {
        float4 vertex : POSITION;
        float3 normal : NORMAL;
    };

    struct v2f {
        float4 pos : SV_POSITION;
        float3 worldPos : TEXCOORD0;
        float3 worldNormal : TEXCOORD1;
        SHADOW_COORDS(2)
    };

    v2f vert(a2v v) {
        v2f o;
        o.pos = UnityObjectToClipPos(v.vertex);
        o.worldPos = mul(unity_ObjectToWorld, v.vertex);
        o.worldNormal = UnityObjectToWorldNormal(v.normal);
        TRANSFER_SHADOW(o);
        return o;
    }

    fixed4 frag(v2f i) : SV_TARGET {
        fixed shadow = SHADOW_ATTENUATION(i);
        fixed3 worldNormal = normalize(i.worldNormal);
        fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos));
        fixed3 viewDir = normalize(UnityWorldSpaceViewDir(i.worldPos));
        fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz;
        fixed3 diffuse = _LightColor0.rgb * _Color.rgb * saturate(dot(worldNormal, worldLightDir));
        fixed3 halfDir = normalize(worldLightDir + viewDir);
        fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(saturate(dot(worldNormal, halfDir)), _Gloss);
        return fixed4(ambient + (diffuse + specular) * shadow, 1.0);
    }

    ENDCG
}

  这是一个ForwardBase的Pass,对于其它的Additional的Pass也是一样的代码。注意到代码中出现了三个宏,SHADOW_COORDS(2),TRANSFER_SHADOW(o)和SHADOW_ATTENUATION(i);它们的具体代码实现在AutoLight中,功能包括定义阴影映射纹理采样的坐标,根据设置计算出坐标,使用坐标进行采样等。
  需要特别注意的是,这些宏的使用基于预定义的变量名称,包括a2f结构中的顶点坐标名称必须是vertex,顶点着色器的输入变量名必须为v,而v2f结构中的顶点位置必须命名为pos等。
  应用了这些代码之后,物体便可以在场景中接收其它物体投射的阴影了。


  Untiy提供的宏除了能方便快捷地处理阴影映射纹理之外,还可以统一管理光照衰减与阴影;在之前关于多光源光照Shader中提到过,Unity管理光照衰减的方法是光照纹理,除了平行光不衰减之外,一般的光源都会有自己的光照纹理,Shader通过采样来获取当前的光照强度。
  应用了阴影后,光照衰减也不能就此放弃不予考虑,那么两者都是采样,都是采样值乘以漫反射和高光,能不能统一到一起进行呢?答案当然是肯定的,但自行处理这样的情况并不轻松,因此Unity提供了内置宏一次性实现两者的采样和计算,这个宏的名字是UNITY_LIGHT_ATTENUATION。
  要使用这个宏,需要包含两个头文件

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

  之后按照前面的例子

struct v2f {
    float4 pos : SV_POSITION;
    float3 worldPos : TEXCOORD0;
    float3 worldNormal : TEXCOORD1;
    SHADOW_COORDS(2)
};

v2f vert(a2v v) {
    v2f o;
    ……
    TRANSFER_SHADOW(o);
    return o;
}

fixed4 frag(v2f i) : SV_TARGET {
    UNITY_LIGHT_ATTENUATION(atten, i, i.worldPos);
    ……
    return fixed4(ambient + (diffuse + specular) * atten, 1.0);
}

  这样一来,光照衰减和阴影就一次性计算完成了。
  如果去AutoLight文件中查阅这个宏的代码,可以发现Unity为它定义了许多版本,这是为了适应不同的光源,Cookie是否启用等不同的情况;使用这个宏最大的好处在于统一性,Unity为开发者考虑了多种情况,因此在Shader中不管是ForwardBase还是Additional,直接使用这个宏都没有问题。
  透明物体的阴影则是一个更加需要小心的情况,一般来说透明物体都需要预先进行透明度测试的计算,而Untiy内置的VertexLit这个Shader里的ShadowCaster并没有预先进行透明度测试,恰恰这个Shader是大部分情况下的Fallback会寻找到的地方,所以如果不注意的话,会出现有透明区域的物体产生了没有任何空洞的阴影的错误情况。
  这时只需要将Fallback改为Transparent/Cutout/VertexLit即可,这个Shader中考虑了透明度测试,生成的阴影映射纹理和深度图会是正确的。
  至于半透明物体,由于处理半透明物体时需要关闭深度写入,因此会影响到阴影的产生,而且想要正确处理半透明物体必须强制要求每个光源空间下对该物体进行从后往前的渲染,这不但非常复杂而且极其影响性能,因此Unity的内置Shader根本就不会让半透明物体产生阴影。
  当然了,开发者可以通过手动设置Fallback为不透明物体的Shader来强制给半透明物体渲染阴影,但效果是不正确的,因为处理阴影时物体被当成了不透明的;也可以通过自行编写所有光源的处理过程来实现所需的半透明阴影效果,但复杂度和效率就会成为不得不重视的问题。
  后面会解析UnityShader中和游戏运行时间相关的效果实现。

猜你喜欢

转载自blog.csdn.net/soul900524/article/details/79729187