这篇实现来的有点墨迹,前先后后折腾零碎的时间折腾了半个月才才实现一个基本的shadow map流程,只能说是对原理理解更深入一些,但离实际应用估计还须要作不少优化。这篇文章大体分析下shadow map的基本原理、Unity中实现ShadowMap阴影方式以及一些有用的参考。php
基本的shadow Map 原理, 参考 "Unity基础(5) Shadow Map 概述". 其基本步骤以下:html
Unity 获取深度纹理的方式能够参考以前的日记:Unity Shader 基础(3) 获取深度纹理 , 笔记中给出了三种获取Unity深度纹理的方式。 若是采用自定义的方式来获取深度,能够考虑使用EncodeFloatRGBA对深度进行编码。另外,能够经过增长多个subshader实现对不一样RenderType 阴影的支持。git
SubShader { Tags { "RenderType"="Opaque" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct v2f { float4 vertex : SV_POSITION; float2 depth: TEXCOORD0; }; v2f vert (appdata_base v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.depth = o.vertex.zw ; return o; } fixed4 frag (v2f i) : SV_Target { float depth = i.depth.x/i.depth.y ; return EncodeFloatRGBA(depth) ; } ENDCG } }
1. 类型
shadow Map的相机会根据光源的不一样有所差别,直线光使用平行投影比较合适,点光源和聚光灯带有位置信息,适合使用透视投影, 这篇文章以平行光和平行投影为例来实现。对于平行投影相机而言,主要关于方向、近平面、远平面、视场大小。github
1. 建立算法
以光源为父节点建立相机,设置投影方式以及 RenderTexture对象。其方向与父节点保持一致。windows
2. 视场匹配app
阴影实现中shadow map占用的空间是最大的,合适的相机视场设置能够在一样资源下得到更好的效果、更高的精度。在Common Techniques to Improve Shadow Depth Maps一文中给出相机参数适应场景的两种方式:FIt to scene和 FIt to view. 对于Fit to Scene,其实现流程:性能
Fit to Scene方式计算整个场景的AABB来摄像 Shadow Map采集相机参数,但若是场景相机视场比较小的状况下,好比FPS游戏中角色,这种方式就不是很合适。对于这种状况,Fit to VIEW 更合适。优化
判断是否为阴影须要比较场景中物体深度与Shadow Map中深度值,这个过程须要确保两者在一个空间中。深度采集保存在shadow map贴图中的数值是NDC空间数值,因此渲染物体时会将物体从世界坐标转换到Shadow Map相机空间下,而后经过投影计算转换到NDC坐标,也就是原理图中的\(z_b\) 。投影矩阵参数能够传递到shader'中进行,以下:ui
//perspective matrix void GetLightProjectMatrix(Camera camera) { Matrix4x4 worldToView = camera.worldToCameraMatrix; Matrix4x4 projection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, false); Matrix4x4 lightProjecionMatrix = projection * worldToView; Shader.SetGlobalMatrix ("_LightProjection", lightProjecionMatrix); }
pixel shadow 中 计算NDC坐标:
fixed4 object_frag (v2f i) : SV_Target { //计算NDC坐标 fixed4 ndcpos = mul(_LightProjection , i.worldPos); ndcpos.xyz = ndcpos.xyz / ndcpos.w ; //从[-1,1]转换到[0,1] float3 uvpos = ndcpos * 0.5 + 0.5 ; ... ... }
经过比较场景物体转换到shadow map相机NDC空间深度\(z_b\)与shadow map贴图中深度值\(z_a\)便可判断顶点是否在阴影区域。以原理图为例,若是 \(z_b\)大于\(z_a\), 顶点是在遮挡物体以后,处于阴影区域。须要注意的是对shadow map 纹理采样坐标须要将场景物体顶点在shadow map相机NDC空间下的坐标转换到[0,1]的范围。下面的代码没有结合光照:
fixed4 object_frag (v2f i) : SV_Target { //计算NDC坐标 fixed4 ndcpos = mul(_LightProjection , i.worldPos); ndcpos.xyz = ndcpos.xyz / ndcpos.w ; //从[-1,1]转换到[0,1] float3 uvpos = ndcpos * 0.5 + 0.5 ; float depth = DecodeFloatRGBA(tex2D(_LightDepthTex, uvpos.xy)); if(ndcpos.z < depth ){return 1;} else{return 0;} }
深度纹理分辨率的关系,会存在场景中多个顶点对深度纹理同一个点进行采样来判断是否为处于阴影的状况,再加上不一样计算方式的精度问题就会产生图上Shadow acne的状况,具体能够参考:https://www.zhihu.com/question/49090321 ,描述的比较详细。
最简单的作法是对场景深度或者贴图深度作稍微的调整,也就是 shadow bias,
shadow bias的作法简单粗暴,若是偏移过大就会出现 Peter Panning的状况,形成阴影和物体分割开的状况。
更好的纠正作法是基于物体与光照方向的夹角,也就是Slope-Scale Depth Bias,这种方式的提出主要是基于物体表面和光照的夹角越大, Perspective Aliasing的状况越严重,也就越容易出现Shadow Acne,以下图因此。若是采用统一的shadow bais就会出现物体表面一部分区域存再Peter Panning 一部分区域还存在shadow acne。
更好的办法是根据这个slope进行计算bias,其计算公式以下,\(miniBais + maxBais * SlopeScale\) , 其中\(SlopeScale\)能够理解为光线方向与表面法线方向夹角的tan值(也便是水平方向为1的状况下,不一样角度对应的矫正量)。
float GetShadowBias(float3 lightDir , float3 normal , float maxBias , float baseBias) { float cos_val = saturate(dot(lightDir, normal)); float sin_val = sqrt(1 - cos_val*cos_val); // sin(acos(L·N)) float tan_val = sin_val / cos_val; // tan(acos(L·N)) float bias = baseBias + clamp(tan_val,0 , maxBias) ; return bias ; }
不过Bias数值是个有点感性的数据,也能够采用其余方式,只要考虑到这个slopescale就行,好比:
// dot product returns cosine between N and L in [-1, 1] range // then map the value to [0, 1], invert and use as offset float offsetMod = 1.0 - clamp(dot(N, L), 0, 1) float offset = minOffset + maxSlopeOffset * offsetMod; // another method to calculate offset // gives very large offset for surfaces parallel to light rays float offsetMod2 = tan(acos(dot(N, L))) float offset2 = minOffset + clamp(offsetMod2, 0, maxSlopeOffset);
解决完shadow acne后,放大阴影边缘就会看到这种锯齿现象,其主要缘由还在于shadow map的分辨率。物体多个点会采集深度纹理同一个点进行阴影计算。这个问题通常能够经过滤波紧进行处理,好比多重采样。
Pencentage close Filtering(PCF),最简单的一种处理方式,当前点是否为阴影区域须要考虑周围顶点的状况,处理中须要对当前点周围几个像素进行采集,并且这个采集单位越大PCF的效果会越好,固然性能也越差。如今的GPU通常支持2*2的PCF滤波, 也就是Unity设置中的Hard Shadow 。
//PCF滤波 float PercentCloaerFilter(float2 xy , float sceneDepth , float bias) { float shadow = 0.0; float2 texelSize = float2(_TexturePixelWidth,_TexturePixelHeight); texelSize = 1 / texelSize; for(int x = -_FilterSize; x <= _FilterSize; ++x) { for(int y = -_FilterSize; y <= _FilterSize; ++y) { float2 uv_offset = float2(x , y) * texelSize; float depth = DecodeFloatRGBA(tex2D(_LightDepthTex, xy + uv_offset)); shadow += (sceneDepth - bias > depth ? 1.0 : 0.0); } } float total = (_FilterSize * 2 + 1) * (_FilterSize * 2 + 1); shadow /= total; return shadow; }
改进算法
Shadow Map Antialiasing 对PCF作了一些改进,能够更快的执行。Improvements for shadow mapping in OpenGL and GLSL 结合PCF和泊松滤波处理,使用PCF相对少的采样数,就能够得到很好的效果OpenGl Tutorial 16 : Shadow mapping也采用了相似的方式。相似的算法还有不少,不一一列举。
pixels close to the near plane are closer together and require a higher shadow map resolution. Perspective shadow maps (PSMs) and light space perspective shadow maps (LSPSMs) attempt to address perspective aliasing by skewing the light's projection matrix in order to place more texels near the eye where they are needed. Cascaded shadow maps (CSMs) are the most popular technique for dealing with perspective aliasing.
参考:Cascaded Shadow Maps , 具体实现能够参考:http://blog.csdn.net/ronintao/article/details/51649664
Unity 5.4版本以后阴影的基本原理相似,可是处理方式有点差别,具体能够查看: Screem space shadow map
阴影的处理有不少方式,有本专著《实时阴影技术》对阴影处理作了不少介绍,翻了下果断放弃了,老是得到一个效果好、性能好的阴影效果仍是须要费点时间。
工程下载:https://github.com/carlosCn/Unity-ShadowMap-Test.git
挺赞的一篇文章:
Unity移动端动态阴影总结
Unity Shadow Map实现
Unity基础(5) Shadow Map 概述
OpenGL Shadow Mapping
OpenGl Tutorial 16 : Shadow mapping
Shadow Map Wiki
Shadow Acne知乎
Common Techniques to Improve Shadow Depth Maps
Cascaded Shadow Maps
Percentage Closer Filtering
Variance Shadow Map Papper
Shadow Mapping Summary
Improvements for shadow mapping in OpenGL and GLSL
Screem space shadow map
Unity移动端动态阴影总结