> 技术文档 > UnityShader学习笔记——阴影_unity shader 阴影

UnityShader学习笔记——阴影_unity shader 阴影

——内容源自唐老狮的shader课程

目录

1.规则

2.Shadow Mapping(阴影贴图)技术

2.1.基本原理

2.2.unity中如何应用Shadow Mapping技术

3.Screen Space Shadow Mapping技术

3.1.是什么

3.2.基本原理

4.unity中如何实现阴影

4.1.理论

4.2.接收

4.2.1.思路

4.2.2.流程

4.3.3.关键宏(需包含AutoLight.cginc)

4.3.投射

4.3.1.关键宏

4.4.面板上的设置

4.5.投射与接收

5.阴影与多种光源

6.透明度测试与阴影

7.透明度混合与阴影

8.如有疏漏,还请指出


1.规则

        阴影区域的产生是因为光线无法达到


2.Shadow Mapping(阴影贴图)技术

2.1.基本原理

        将摄像机的位置放在与光源重合的位置上,那么场景中关于这个光源的阴影区域就是摄像机看不到的地方。本质上也是基于“阴影区域的产生是因为光线无法达到”

        而在unity中,它会生成一张深度图(阴影映射纹理),一般存于显存中。该图记录了从光源位置出发,能看到的场景中距离它最近的表面的位置(一般记录其深度信息,值在01之间,0最近)

        一般情况下,点光源用透视投影,平行光用正交投影

2.2.unity中如何应用Shadow Mapping技术

        有了阴影映射纹理后,我们需要在Pass中将顶点信息变换到光源空间下,得到顶点在光源空间下的位置,然后使用xy分量对阴影映射纹理进行采样,得到阴影映射纹理中该位置对的深度信息。如果取出的深度值小于该顶点的深度值(也就是说该点被遮挡),那么说明该点在阴影之中


3.Screen Space Shadow Mapping技术

3.1.是什么

        即:屏幕空间阴影映射技术,是一种基于Shadow Mapping技术的拓展和改进

        并不是所有设备都支持SSSM技术,当然,unity会自己判断,如果不支持,就会使用Shadow Mapping技术

3.2.基本原理

        在Shadow Mapping的基础上多生成一张屏幕空间深度图,该图中记录了从摄像机视角看到的每个像素的深度值。其同样会为每个光源生成对应的阴影映射纹理

        二者都有之后,我们需要对屏幕空间下的像素坐标进行坐标转换,令其变换光源空间下,然后再光源空间下比较每个像素的深度值(通过对屏幕空间深度图取样获取)和阴影映射纹理中的值,如果当前像素的深度值大于光源深度图中的值,那么说明该像素在阴影中

        当屏幕空间中的像素位置变换导光源空间下时,可能不在光源空间的可视范围内(毕竟屏幕空间和光源空间看的不一样),这时我们无需进行比较,该像素不用进行阴影处理

       

        简而言之:

        1.基于光源位置生成 阴影映射纹理

        2.基于渲染游戏画面得到 屏幕空间深度图

        3.将屏幕像素变换到 光源空间下

        4.对屏幕空间深度图和阴影映射纹理采样,比较深度值,决定最后的阴影处理效果


4.unity中如何实现阴影

4.1.理论

        unity会调用LightMode为ShadowCaster(阴影反射器)的Pass通道,来生成对应的阴影映射纹理,然后在后续阴影计算中使用。阴影映射纹理无需自己手动计算,untiy中有宏帮助我们计算,调用即可。

        如果Shader中没有该Pass,则会在Fallback指定的shader中找,如果还没就找,那就不会投射阴影,但仍可以接收阴影。

        而对于支持SSSM的设备,虽然其还需要屏幕空间深度图,但它通常由摄像机在渲染过程中自动生成,并存储在摄像机的深度纹理中,我们计算时,从光源的阴影映射纹理以及屏幕空间深度图中进行采样即可。当然,unity也为这个提供了宏,直接调用以及。

4.2.接收

4.2.1.思路

        在Shader中对阴影映射纹理进行采样并比较,并把比较结果与漫反射颜色与高光反射颜色之和相乘

4.2.2.流程

        1.在顶点着色器中进行顶点坐标转换,将顶点坐标转换为阴影映射纹理坐标

        2.在片元着色器用该坐标对阴影映射纹理采样,通过将得到的深度值与坐标深度值比较以计算出阴影衰减值

        3.用阴影衰减值参与计算,即与漫反射颜色与高光反射颜色之和相乘

4.3.3.关键宏(需包含AutoLight.cginc)

        1.SHADOW_COORDS(n):阴影坐标宏,在v2f结构体中使用,本质上声明了一个用于对阴影映射纹理进行采样的坐标变量_ShadowCoord。n为下一个纹理坐标的索引

        2.TRANSFER_SHADOW:转移阴影宏,在顶点着色器中调用,传入对应的v2f结构体变量,其在内部会判断该使用哪种阴影映射技术,最终的目的是将顶点进行坐标转换并存储在_ShadowCoord中。

           需要注意的是:这里的v2f结构体中,顶点命名必须为pos,而传入顶点着色器中的结构体,其顶点命名必须为vertex

        3.SHADOW_ATTENUATION:阴影衰减宏,在片元着色器中调用,传入v2f结构体变量,其会在内部对阴影映射纹理进行采样并进行比较,以计算出一个fixed3的阴影衰减量

//仅为对应的Pass Pass { Tags { \"LightMode\" = \"ForwardBase\" } CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdbase #include \"Lighting.cginc\" #include \"UnityCG.cginc\" #include \"AutoLight.cginc\" fixed4 _DiffuseColor; fixed4 _SpecularColor; float _Gloss; struct v2f { float4 pos : SV_POSITION; float3 wNormal : NORMAL; float3 wPos : TEXCOORD0; //阴影坐标宏,用于存储阴影纹理坐标。无需分号 SHADOW_COORDS(1) }; v2f vert(appdata_full v) { v2f data; data.pos = UnityObjectToClipPos(v.vertex); data.wNormal = UnityObjectToWorldNormal(v.normal); float4 wPos = mul(unity_ObjectToWorld, v.vertex); data.wPos = wPos.xyz; TRANSFER_SHADOW(data); return data; } fixed4 frag(v2f f) : SV_TARGET { float3 wLightDir = normalize(_WorldSpaceLightPos0); float3 wViewDir = normalize(_WorldSpaceCameraPos - f.wPos).xyz; float3 halfAngle = normalize(wLightDir + wViewDir); fixed3 lambertColor = _LightColor0.rgb * _DiffuseColor.rgb * max(0, dot(f.wNormal, wLightDir)); fixed3 blinn_PhongColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(f.wNormal, halfAngle)), _Gloss); fixed3 shadow = SHADOW_ATTENUATION(f); fixed3 finalColor = UNITY_LIGHTMODEL_AMBIENT.rgb + (lambertColor + blinn_PhongColor) * shadow; return fixed4(finalColor, 1); } ENDCG }

 
 

仅接收阴影

4.3.投射

        必须由LightMode被设置为ShadowCaster的Pass通道

4.3.1.关键宏

        1.V2F_SHADOW_CASTER:顶点到片元着色器投射结构体数据宏,它定义了一些标准的成员变量,这些变量用于在阴影投射路径中传递顶点数据到片元着色器

        2.TRANSFER_SHADOW_CASTER_NORMALOFFSET:阴影投射器法线偏移宏,用于在顶点着色器中计算和传递阴影投射所需的变量。主要做了:将模型空间的顶点位置转换到裁剪空间;考虑法线偏移,以减轻阴影失真问题,尤其是在处理白阴影时;考虑法线的投影空间位置,用以后续计算

        3.SHADOW_CASTER_FRAGMENT:阴影投射片元宏,将深度值写入到阴影映射纹理中。

//仅为投射阴影的Pass Pass { Tags { \"LightMode\" = \"ShadowCaster\" } CGPROGRAM #include \"UnityCG.cginc\" #include \"Lighting.cginc\" #pragma vertex vert #pragma fragment frag // 该编译指令告诉unity编译器生成多个变体 // 可以保证着色器能够在所有可能的阴影投射模式下正确渲染 #pragma multi_compile_shadowcaster struct v2f { V2F_SHADOW_CASTER; }; v2f vert(appdata_full v) { v2f data; //这个宏会自动使用v的数据 TRANSFER_SHADOW_CASTER_NORMALOFFSET(data); return data; } fixed4 frag(v2f f) : SV_TARGET { SHADOW_CASTER_FRAGMENT(f); return fixed4(1, 1, 1, 1); } ENDCG }
仅投射阴影

4.4.面板上的设置

        1.保证光源能够生成阴影映射纹理:光源组件上设置ShadowType,只要不设置程NoShadow就行

        2.保证光源能救收其他物体的阴影:网格渲染器上勾选ReceiveShadows

        3.保证物体向其他物体投射阴影:网格渲染器上设置CastShadows

4.5.投射与接收

        要让俩合一块,分别实现它们的代码然后放到一个SubShader中就行,不过由于阴影投射的代码较为通用,一般不会自己写,而是使用Fallback


5.阴影与多种光源

        将这两者结合只需要将多种光源的pass和阴影的pass结合即可,即在基础通道里正常渲染高光和漫反射,在附加通道中同时渲染阴影和多种光源

        

//附加通道 Pass { Tags { \"LightMode\" = \"ForwardAdd\" } Blend One One CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fwdadd_fullshadows #include \"Lighting.cginc\" #include \"UnityCG.cginc\" #include \"AutoLight.cginc\" fixed4 _DiffuseColor; fixed4 _SpecularColor; float _Gloss; struct v2f { float4 pos : SV_POSITION; float3 wNormal : NORMAL; float3 wPos : TEXCOORD0; SHADOW_COORDS(2) }; v2f vert(appdata_full v) { v2f data; data.pos = UnityObjectToClipPos(v.vertex); data.wNormal = UnityObjectToWorldNormal(v.normal); float3 wPos = mul(unity_ObjectToWorld, v.vertex).xyz; data.wPos = wPos; TRANSFER_SHADOW(data); return data; } fixed4 frag(v2f f) : SV_TARGET { float3 wNormal = normalize(f.wNormal); float3 wLightDir = float3(0, 0, 0); #if defined(_DIRECTIONAL_LIGHT) wLightDir = normalize(_WorldSpaceLightPos0); #else wLightDir = normalize(_WorldSpaceLightPos0.xyz - f.wPos); #endif float3 wViewDir = normalize(_WorldSpaceCameraPos - f.wPos).xyz; float3 halfAngle = normalize(wLightDir + wViewDir); fixed3 lambertColor = _LightColor0.rgb * _DiffuseColor.rgb * max(0, dot(wNormal, wLightDir)); fixed3 blinn_PhongColor = _LightColor0.rgb * _SpecularColor.rgb * pow(max(0, dot(wNormal, halfAngle)), _Gloss); //#ifdef USING_DIRECTIONAL_LIGHT // float atten = 1; //#else // #if defined(POINT) // float3 lightCoord = mul(unity_WorldToLight, float4(f.wPos, 1)).xyz; // float atten = tex2D(_LightTexture0, dot(lightCoord, lightCoord).xx).UNITY_ATTEN_CHANNEL; // #elif defined(SPOT) // float4 lightCoord = mul(unity_WorldToLight, float4(f.wPos, 1)); // float atten = (lightCoord.z > 0) * tex2D(_LightTexture0, lightCoord.xy / lightCoord.w + 0.5).w * tex2D(_LightTextureB0, dot(lightCoord, lightCoord).rr).UNITY_ATTEN_CHANNEL; // #else // float atten = 1; // #endif //#endif UNITY_LIGHT_ATTENUATION(atten, f, f.wPos); fixed3 finalColor = (lambertColor + blinn_PhongColor) * atten; return fixed4(finalColor, 1); } ENDCG }

        代码中被注释的代码就对光源类型判断的另一种形式,而之所以将其注释,是因为UNITY_LIGHT_ATTENUATION宏可以同时处理光源和阴影的衰减,进而省去对光源和阴影衰减值的单独计算。

        第一个参数为衰减值的变量,一个名字即可,宏内部会去声明它。第二个参数为v2f结构体。第三个参数为v2f结构体中顶点的世界坐标。

        当然,为了能投射阴影,还需要阴影投射的Pass,可以自己写,也可以使用Fallback \"Specular\"(不一定非得是这个)

阴影与多种光源

6.透明度测试与阴影

        把透明度测试和阴影的代码拼一块就行,UNITY_LIGHT_ATTENUATION在基础通道中也能用,就是只能接收阴影,不能产生多种光源的效果。

透明度测试与阴影

 


7.透明度混合与阴影

        由于透明度混合会关闭深度写入,而阴影处理需要深度值,所以unity从性能方面考虑(半透明物体的阴影效果相对复杂),所有的内置半透明Shaderr都不会产生阴影效果。

        简而言之:unity不会直接为透明度混合Shader处理阴影

        但是可以强制开启,如将Fallback设置为一个非透明Shader,如VertexLit等。当然,并不真实。

透明度混合与阴影

 


8.如有疏漏,还请指出