赞
踩
我们可以先考虑真实生活中阴影是如何产生的。当一个光源发射的一条光线遇到一个不透明物体时,这条光线就不可以再继续照亮其他物体(这里不考虑光线反射)。因此,这个物体就会向它旁边的物体投射阴影,那些阴影区域的产生是因为光线无法到达这些区域。
在实时渲染中,我们最常使用的是一种名为 Shadow Map 的技术。这种技术理解起来非常简它会首先把摄像机的位置放在与光源重合的位置上,那么场景中该光源的阴影区域就是那些像机看不到的地方。而Unitv 就是使用的这种技术。
在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity 就会为该光源计算它的阴影映射纹理(shadowmap)。这张阴影映射纹理本质上也是一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息。
那么,在计算阴影映射纹理时,我们如何判定距离它最近的表面位置呢?一种方法是,先把摄像机放置到光源的位置上,然后按正常的染流程,即调用 Base Pass和Additional Pass 来更新深度信息,得到阴影映射纹理。但这种方法会对性能造成一定的浪费,因为我们实际上仅仅需要深度信息而已,而 Base Pass和Additional Pass 中往往涉及很多复杂的光照模型计算。因此,Unity选择使用一个额外的 Pass来专门更新光源的阴影映射纹理,这个Pass 就是 LightMode 标签被设置为ShadowCaster的Pass。这个Pass 的染目标不是缓存而是阴影映射纹理(或深度纹理)。Unity 首先把摄像机放置到光源的位置上,然后调用该 Pass,通过对顶点变换后得到光源空间下的位置,并据此来输出深度信息到阴影映射纹理中。因此,当开启了光源的阴影效果后,底层渲染引擎首先会在当前渲染物体的Unity Shader 中找到 LightMode为ShadowCaster 的 Pass,如果没有,它就会在 Fallback 指定的 Unity Shader 中继续寻找,如果仍然没有找到,该物体就无法向其他物体投射阴影(但它仍然可以接收来自其他物体的阴影)。当找到了一个 LightMode为ShadowCaster的Pass后,Unity会使用该Pass来更新光源的阴影映射纹理。
在前向渲染路径中,如果场景中最重要的平行光开启了阴影,Unity 就会为该光源计算它的阴影映射纹理(shadowmap)。这张阴影映射纹理本质上也是一张深度图,它记录了从该光源的位置出发、能看到的场景中距离它最近的表面位置(深度信息)。
当使用了屏幕空间的阴影映射技术时,Unity 首先会通过调用 LightMode为 ShadowCaster的Pass 来得到可投射阴影的光源的阴影映射纹理以及摄像机的深度纹理。然后,根据光源的阴影映射纹理和摄像机的深度纹理来得到屏幕空间的阴影图。如果摄像机的深度图中记录的表面深度大于转换到阴影映射纹理中的深度值,就说明该表面虽然是可见的,但是却处于该光源的阴影中。通过这样的方式,阴影图就包含了屏幕空间中所有有阴影的区域。如果我们想要一个物体接收来自其他物体的阴影,只需要在 Shader 中对阴影图进行采样。由于阴影图是屏幕空间下的,因此,我们首先需要把表面坐标从模型空间变换到屏幕空间中,然后使用这个坐标对阴影图进行采样即可。总结一下,一个物体接收来自其他物体的阴影,以及它向其他物体投射阴影是两个过程。
如果我们想要一个物体接收来自其他物体的阴影,就必须在 Shader 中对阴影映射纹理(包括屏幕空间的阴影图) 进行采样,把采样结果和最后的光照结果相乘来产生阴影效果。
如果我们想要一个物体向其他物体投射阴影,就必须把该物体加入到光源的阴影映射纹理的计算中,从而让其他物体在对阴影映射纹理采样时可以得到该物体的相关信息。
在 Unity中,这个过程是通过为该物体执行 LightMode为ShadowCaster的 Pass来实现的。如果使用了屏幕空间的投影映射技术,Unity 还会使用这个Pass 产生一张摄像机的深度纹理在下面的章节中,我们会学习如何在Unity中实现上面两个过程。
物体开启受阴影照射/投射阴影
但正方体仍然可以向下面的平面投射阴影。一些读者可能会有疑问:“之前不是说Unity要使用LightMode为ShadowCaster的Pass来渲染阴影映射纹理和深度图吗?但是Chapter9-ForwardRendering中并没有这样一个Pass啊。”没错,我们在Chapter9-ForwardRendering的SubShader 只定义了两个Pass—ABasePass,一个AdditionalPass。那么为什么它还可以投射阴影呢?实际上,秘密就在于Chapter9-ForwardRendering中的Fallback语义,在Chapter9-ForwardRendering中,我们为它的Fallback 指定了一个用于回调 Unity Shader,即内置的Specular。虽然 Specular 本身也没有包含这样一个Pass,但是由于它的Fallback 调用了 VertexLit,它会继续回调,并最终回调到内置的 VertexLit.我们可以在 Unity 内置的着色器里找到它: builtin-shaders-xxx->DefaultResourcesExtra-NormalVertexLitshader。打开它,我们就可以看到“传说中”的 LightMode为 ShadowCaster的Pass了。
Shader "MyShader/9-ShadowShader" { Properties { _Diffuse ("Diffuse", Color) = (1, 1, 1, 1) _Specular ("Specular", Color) = (1, 1, 1, 1) _Gloss ("Gloss", Range(8.0, 256)) = 20 } SubShader { Tags { "RenderType"="Opaque" } Pass { // 前向渲染标签 Tags { "LightMode"="ForwardBase" } CGPROGRAM // Shader 中使用光照衰减等光照变量可以被正确赋值 #pragma multi_compile_fwdbase #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" //引入阴影计算的文件 #include "AutoLight.cginc" fixed4 _Diffuse; fixed4 _Specular; float _Gloss; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; //输出结构体的宏 SHADOW_COORDS(2) }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; //顶点着色器的宏预设 TRANSFER_SHADOW(o); return o; } fixed4 frag(v2f i) : SV_Target { fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz; fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir)); fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); fixed3 halfDir = normalize(worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss); fixed atten = 1.0; //片元着色器的宏预设 fixed shadow= SHADOW_ATTENUATION(i); return fixed4(ambient + (diffuse + specular) * atten * shadow, 1.0); } ENDCG } Pass { // Pass for other pixel lights Tags { "LightMode"="ForwardAdd" } Blend One One CGPROGRAM // Apparently need to add this declaration #pragma multi_compile_fwdadd #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" #include "AutoLight.cginc" fixed4 _Diffuse; fixed4 _Specular; float _Gloss; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; return o; } fixed4 frag(v2f i) : SV_Target { fixed3 worldNormal = normalize(i.worldNormal); #ifdef USING_DIRECTIONAL_LIGHT fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz); #else fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz - i.worldPos.xyz); #endif fixed3 diffuse = _LightColor0.rgb * _Diffuse.rgb * max(0, dot(worldNormal, worldLightDir)); fixed3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos.xyz); fixed3 halfDir = normalize(worldLightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(worldNormal, halfDir)), _Gloss); #ifdef USING_DIRECTIONAL_LIGHT fixed atten = 1.0; #else #if defined (POINT) float3 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)).xyz; fixed atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL; #elif defined (SPOT) float4 lightCoord = mul(unity_WorldToLight, float4(i.worldPos, 1)); fixed atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL; #else fixed atten = 1.0; #endif #endif return fixed4((diffuse + specular) * atten, 1.0); } ENDCG } } FallBack "Specular" }
![在这里插入图片描述](https://img-blog.csdnimg.cn/e14963c8bc87477aa376314bf53c343e.png
绘制该场景共需要花费 20 个染事件。这些染事件可以分为4个部分:
UpdateDepthTexture,即更新摄像机的深度纹理;
RenderShadowmap,即渲染得到平行光的阴影映射纹理:
CollectShadows,即根据深度纹理和阴影映射纹理得到屏幕空间的阴影图;
最后绘制渲染结果。
Unity Shader 的前向染路径中计算光照衰减-在Base Pass 中,平行光的衰减因子总是等于 1,而在Additional Pass 中,我们需要判断该 Pass处里的光源类型,再使用内置变量和宏计算衰减因子。实际上,光照衰减和阴影对物体最终的渲染吉果的影响本质上是相同的一一我们都是把光照衰减因子和阴影值及光照结果相乘得到最终的渲染结果。那么,是不是可以有一个方法可以同时计算两个信息呢?好消息是,Unity 在 Shader 里是供了这样的功能,这主要是通过内置的UNITY LIGHTATTENUATION宏来实现的。
UNITY LIGHT ATTENUATION是Unity 内置的用于计算光照衰减和阴影的宏,我们可以在内置的AutoLightcginc 里找到它的相关声明。它接受3 个参数,它会将光照衰减和阴影值相乘后的结果存储到第一个参数中。注意到,我们并没有在代码中声明第一个参数atten,这是因为UNITY_LIGHTATTENUATION会帮我们声明这个变量。
它的第二个参数是结构体v2f,这个参数会传递给9.4.2节中使用的SHADOWATTENUATION,用来计算阴影值。而第三个参数是世界空间的坐标,正如我们在 9.3 节中看到的一样,这个参数会用于计算光源空间下的坐标,再对光照衰减纹理采样来得到光照衰减。
我们强烈建议读者查阅AutoLightcginc 中UNITY LIGHTATTENUATION的声明,读者可以发现,Unity 针对不同光源类型、是否启用cookie等不同情况声明了多个版本的UNITY LIGHTATTENUATION。这些不同版本的声明是保证我们可以通过这样一个简单的代码来得到正确结果的关键。
想要在 Unity 里让物体能够向其他物体投射阴影,一定要在它使用的Unity Shader 中提供一个 LightMode为 ShadowCaster的 Pass。在前面的例子中,我们使用内置的VertexLit 中提供的ShadowCaster 来投射阴影。VertexLit 中的 ShadowCaster 实现很简单,它会正常渲染整个物体,然后把深度结果输出到一张深度图或阴影映射纹理中。读者可以在内置文件中找到相关的文件。对于大多数不透明物体来说,把 Fallback 设为 VertexLit 就可以得到正确的阴影。但对于透明物体来说,我们就需要小心处理它的阴影。透明物体的实现通常会使用透明度测试或透明度混合,我们需要小心设置这些物体的 Fallback。
Shader "MyShader/9-AlphaTestWithShadow" { Properties { _Color ("Color Tint", Color) = (1, 1, 1, 1) _MainTex ("Main Tex", 2D) = "white" {} //透明度测试条件 _Cutoff("Alpha Cutoff", Range(0,1) ) = 0.5 } SubShader { //渲染队列透明度测试 Tags {"Queue"="AlphaTest" "IgnoreProjector"="True" "RenderType"="TransparentCutout"} Pass { Tags { "LightMode"="ForwardBase" } Cull Off CGPROGRAM #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" #include "AutoLight.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; fixed _Cutoff; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float3 worldNormal : TEXCOORD0; float3 worldPos : TEXCOORD1; float2 uv : TEXCOORD2; //注意到,由于我们已经占用了3个插值寄存器(使用TEXCOORDO、TEXCOORD1TEXCOORD2修饰的变量),因此 SHADOW_COORDS中传入的参数是3,这意味着,阴影纹坐标将占用第四个插值寄存器TEXCOORD3。 SHADOW_COORDS(3) }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.worldNormal = UnityObjectToWorldNormal(v.normal); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; o.uv = TRANSFORM_TEX(v.texcoord, _MainTex); TRANSFER_SHADOW(o); return o; } fixed4 frag(v2f i) : SV_Target { //归一化向量 fixed3 worldNormal = normalize(i.worldNormal); fixed3 worldLightDir = normalize(UnityWorldSpaceLightDir(i.worldPos)); fixed4 texColor = tex2D(_MainTex, i.uv); // 判断texColor.a - _Cutoff是否为负数,是则舍弃输出 clip (texColor.a - _Cutoff); // 等同于 // if ((texColor.a - _Cutoff) < 0.0) { // discard; // } fixed3 albedo = texColor.rgb * _Color.rgb; //片元环境光 fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; //漫反射光 fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(worldNormal, worldLightDir)); UNITY_LIGHT_ATTENUATION(atten,i,i.worldPos); return fixed4(ambient + diffuse * atten, 1.0); } ENDCG } } // FallBack "Transparent/Cutout/VertexLit" }
与透明度测试的物体相比,想要为使用透明度混合的物体添加阴影是一件比较复杂的事情。事实上,所有内置的透明度混合的Unity Shader,如Transparent/VertexLit等,都没有包含阴影投射的Pass。这意味着,这些半透明物体不会参与深度图和阴影映射纹理的计算,也就是说,它们不会向其他物体投射阴影,同样它们也不会接收来自其他物体的阴影。
Unity 会这样处理半透明物体是有它的原因的。由于透明度混合需要关团深度与人,由此带来的问题也影响了阴影的生成。总体来说,要想为这些半透明物体产生正确的阴影,需要在每个光源空间下仍然严格按照从后往前的顺序进行渲染,这会让阴影处理变得非常复杂,而且也会影响性能。因此,在 Unity 中,所有内置的半透明 Shader 是不会产生任何阴影效果的。
Shader "Unity Shaders Book/Common/Bumped Specular" { Properties { _Color ("Color Tint", Color) = (1, 1, 1, 1) _MainTex ("Main Tex", 2D) = "white" {} _BumpMap ("Normal Map", 2D) = "bump" {} _Specular ("Specular Color", Color) = (1, 1, 1, 1) _Gloss ("Gloss", Range(8.0, 256)) = 20 } SubShader { Tags { "RenderType"="Opaque" "Queue"="Geometry"} 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 _Color; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; fixed4 _Specular; float _Gloss; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float4 uv : TEXCOORD0; float4 TtoW0 : TEXCOORD1; float4 TtoW1 : TEXCOORD2; float4 TtoW2 : TEXCOORD3; SHADOW_COORDS(4) }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; TANGENT_SPACE_ROTATION; float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x); o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y); o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z); TRANSFER_SHADOW(o); return o; } fixed4 frag(v2f i) : SV_Target { float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w); fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos)); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos)); fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw)); bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump))); fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb; fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.xyz * albedo; fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir)); fixed3 halfDir = normalize(lightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss); UNITY_LIGHT_ATTENUATION(atten, i, worldPos); return fixed4(ambient + (diffuse + specular) * atten, 1.0); } ENDCG } Pass { Tags { "LightMode"="ForwardAdd" } Blend One One CGPROGRAM #pragma multi_compile_fwdadd // Use the line below to add shadows for point and spot lights // #pragma multi_compile_fwdadd_fullshadows #pragma vertex vert #pragma fragment frag #include "Lighting.cginc" #include "AutoLight.cginc" fixed4 _Color; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _BumpMap; float4 _BumpMap_ST; float _BumpScale; fixed4 _Specular; float _Gloss; struct a2v { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; float4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; float4 uv : TEXCOORD0; float4 TtoW0 : TEXCOORD1; float4 TtoW1 : TEXCOORD2; float4 TtoW2 : TEXCOORD3; SHADOW_COORDS(4) }; v2f vert(a2v v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv.xy = v.texcoord.xy * _MainTex_ST.xy + _MainTex_ST.zw; o.uv.zw = v.texcoord.xy * _BumpMap_ST.xy + _BumpMap_ST.zw; float3 worldPos = mul(unity_ObjectToWorld, v.vertex).xyz; fixed3 worldNormal = UnityObjectToWorldNormal(v.normal); fixed3 worldTangent = UnityObjectToWorldDir(v.tangent.xyz); fixed3 worldBinormal = cross(worldNormal, worldTangent) * v.tangent.w; o.TtoW0 = float4(worldTangent.x, worldBinormal.x, worldNormal.x, worldPos.x); o.TtoW1 = float4(worldTangent.y, worldBinormal.y, worldNormal.y, worldPos.y); o.TtoW2 = float4(worldTangent.z, worldBinormal.z, worldNormal.z, worldPos.z); TRANSFER_SHADOW(o); return o; } fixed4 frag(v2f i) : SV_Target { float3 worldPos = float3(i.TtoW0.w, i.TtoW1.w, i.TtoW2.w); fixed3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos)); fixed3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos)); fixed3 bump = UnpackNormal(tex2D(_BumpMap, i.uv.zw)); bump = normalize(half3(dot(i.TtoW0.xyz, bump), dot(i.TtoW1.xyz, bump), dot(i.TtoW2.xyz, bump))); fixed3 albedo = tex2D(_MainTex, i.uv.xy).rgb * _Color.rgb; fixed3 diffuse = _LightColor0.rgb * albedo * max(0, dot(bump, lightDir)); fixed3 halfDir = normalize(lightDir + viewDir); fixed3 specular = _LightColor0.rgb * _Specular.rgb * pow(max(0, dot(bump, halfDir)), _Gloss); UNITY_LIGHT_ATTENUATION(atten, i, worldPos); return fixed4((diffuse + specular) * atten, 1.0); } ENDCG } } FallBack "Specular" }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。