UnityEffects(1)之shadowMap(阴影映射)

来源:互联网 发布:阿里人工智能概念股 编辑:程序博客网 时间:2024/05/23 15:43

UnityEffects(1)之shadowMap(阴影映射)

效果图:


工程放在:https://github.com/aceyan/UnityEffects  使用unity5.4

场景是Scene/(1)ShadowMap

  shadowMap是一种常用的阴影技术,在各大引擎中被广泛使用,Unity中自带的阴影效果也是基于shadowMap的。

         为了理解这个常用技术,我们今天来造一个方形的轮子,手动在unity中实现一下shadowMap来模拟一个方向光的阴影。

一、原理:

阴影映射是一个双过程的技术:

1、  首先,场景以光源的位置为视点被渲染。每个渲染图像的像素的深度被记录在一个“深度纹理”中(这个纹理通常被称为阴影贴图)。

2、  然后,场景从眼睛的位置渲染,但是用标准的阴影纹理把阴影贴图从灯的位置投影到场景中。在每个像素,深度采样(从被投影的阴影贴图纹理)与片段到灯的距离进行比较。如果后者大,该像素就不是最靠近灯源的表面。这意味着这个片段是阴影,它在着色过程中不应该接受光照。

-------------------摘自《Cg教程_可编程实时图形权威指南

 

二、实现和细节

这一段话其实已经非常简明清晰的描述了shadowMap的实现原理(Cg教程是本好书^_^),那么接下来我们就把两个过程分解开来一步一步的在unity中实现:

1、以光源位置为视点渲染深度图

1)首先创建一个camera(这里叫做LightCamera)定义光的方向,我们要使用它来将我们需要产生阴影接受阴影的物体的深度渲染到一张纹理中(下面称作depthMap),因为是我要模拟的是平行光阴影,那么LightCamera使用正交投影。


 

那么问题来了!有了光的方向,这个相机的位置和视锥(后面称为光锥)相关的属性怎么确定呢?

 

举个栗子:试想一下,现在有一个场景一个plane上面有一个cube,现在我们要进行阴影映射的第二部,也就是从视点渲染这个场景,那么需要LightCamera渲染的深度图,查询当前要渲染像素对应的深度来完成渲染。如果这时LightCamera放歪了,没有渲染到这个部分的深度,那么岂不是渲染就出问题了?如果暴力的让LightCamera照射整个场景也不是一个好方案,效率不高,光锥过大还可能影响深度图的精度。

那么我们就需要一个方法来确定一个合适的光锥,可以参考一下两篇文章:

http://blog.csdn.net/zjull/article/details/11740505

http://www.klayge.org/2013/05/06/%E5%A4%A7%E8%8C%83%E5%9B%B4shadow-map%EF%BC%88%E4%B8%80%EF%BC%89%EF%BC%9Apssm/

第一篇文章中讲解了原理,而龚大的第二篇文章中则提供了一个有效的方法。

 

本示例中参考了龚大的方法:

1、  在LightCamera 的视空间中求视锥8顶点 

2、  将8个顶点变换到世界空间,将灯光相机设置在_mainCamera视锥中心

3、 求光view矩阵

4、把顶点从世界空间利用1中的矩阵变换到LightCamera  view空间

5、求包围盒 (由于光锥xy轴的对称性,这里求最大包围盒就好,不是严格意义的AABB)

5.5、 如果8个顶点在光锥view空间中的z<0,那么如果近裁平面 nearClipPlane=0,就可能出现应该被渲染depthmap的物体被光锥近裁面剪裁掉的情况,所以z < 0 的情况下要延光照负方向移动光源位置以避免这种情况

6、 根据包围盒确定投影矩阵 包围盒的最大z就是farClipPlane,Camera.orthographicSize由maxY决定 ,还要设置Camera.aspect=maxX /maxY

 

这样就得到了一个合适的光锥以及LightCamera的位置,在实例中,你可以旋转LightCamera光的方向,可以看到LightCamera 的视锥和位置也会自动改变,以确保将所以要产生或接受阴影的物体的深度被渲染。对应的代码在CreateDepthMap.cs的Update()中。

 

2)获得了合适的LightCamera设置,接下来要用它生成我们需要的depthMap。

将LightCamera 的depthTextureMode设置为DepthTextureMode.Depth,设置LightCamera的renderTarget为我们的depthMap,使用SetReplacementShader替换渲染来渲染场景,将"RenderType"="ShadowMap"的物体的深度从 _CameraDepthTexture中取出,EncodeFloatRGBA到我们的depthMap中,材质中"RenderType"="ShadowMap"的物体是我们需要产生阴影或接受阴影的物体,而RenderType为其他,比如"RenderType"="Opaque"的物体,我们并不希望它参与阴影的生成和投射,那么ReplacementShader也会有对应的"RenderType"="Opaque"的Subshader,直接将白色渲染到depthMap。表示这部分的深度离视点最远,不会受到阴影影响。对应代码在CreateDepthMap.cs的Start()中,而替换渲染的shader为DepthMap.shader。

 

那么问题又来了,如何从_CameraDepthTexture中取出物体的深度?

这里就要用到投影贴图技术。可以参考这篇文章:

http://blog.csdn.net/seamanj/article/details/10813271

也可以看《Cg教程_可编程实时图形权威指南》第9章9.3投影贴图(虽然有些年头,但这是本好书~)

换种说法就是使用投影仪的投影矩阵,将要渲染的顶点变换到投影仪的齐次剪裁空间x[0,w],y[0,w]。再映射到投影的纹理的uv空间x[0,1] y[0,1],取出对应的像素。

这里要特别注意的是在uv空间进行绘制时,纹理纵坐标的平台差异:http://docs.unity3d.com/Manual/SL-PlatformDifferences.html

 

Ps:示例用unity5.x开发,关于_CameraDepthTexture有一点需要注意:

Unity5更新日志:

DepthTexture在5.0之后不再使用ReplacementShader来生成,而是使用原shader 的ShadowCaster pass(LightMode=ShadowCaster)或者fallback一个。

官方文档:http://docs.unity3d.com/Manual/SL-CameraDepthTexture.html

 

2、结合上一步的深度图,以眼睛位置为视点渲染场景

建立一个MainCamera来渲染场景。为需要接受阴影的物体添加ShadowMap.cs脚本,并将其材质的shader选择为ShadowMap,例如这个plane:

 

 

ShadowMap.cs主要的功能是向材质传递LightCamera的 view projection矩阵,和上一步生成的depthMap,这样也要特别说明一下,projection矩阵我们使用GL.GetGPUProjectionMatrix来获取,这样获取到的矩阵是当前平台的相关矩阵,Camera.projectionMatrix 则是GL风格的投影矩阵。

ShadowMap.shader 使用LightCamera的 view projection矩阵将顶点变换到LightCamera 的齐次裁剪空间,顶点插值后在片段程序中将顶点映射到depthMap的纹理空间取出当前像素的深度值(同样要注意UV空间的平台差异),与当前像素的深度值比较(通过LightCamera 的 view projection矩阵投影,进行透视除法获得),如果当前像素的深度值比从depthMap中取出的大,那么说明前像素在阴影中。在比较深度大小的时候使用了一个偏移值_bias,关于原理可以参考:

https://www.zhihu.com/question/49090321

http://www.tairan.com/archives/5982/

http://blog.csdn.net/ronintao/article/details/51649664

当_bias为0时,会出现这样的情况(称作Shadow Acne):


但是使用_bias会引起peter panning现象影子脱离了物体)

还有一种方式可以避免Shadow Acne(也无法规避peter panning),使用Normal Bias,在渲染深度图的时候将物体延法线反方向偏移一些,这样就可以保证物体的表面不被“深度图中的自己”挡住,unity的文档也有一些相关参数的介绍:

https://docs.unity3d.com/Manual/ShadowOverview.html

https://docs.unity3d.com/ScriptReference/Light-shadowNormalBias.html


ps:齐次剪裁空间也有平台差异,经过透视除法后dx的是z在[0,1] ,Gl是在[-1,1],资料:http://docs.unity3d.com/Manual/SL-PlatformDifferences.html

而我们得depthMap中保存的深度是[0,1],所以我们在比较当前像素的深度值和depthMap中的深度值时,要根据平台将当前像素的深度值映射到[0,1]

 

扩展阅读:

Cascaded Shadow Mapping:

http://www.ogldev.org/www/tutorial49/tutorial49.html

 

 阴影的优化:

http://www.tairan.com/archives/5982/

 


update 2017.1.16

DepthMap2.shader可以替代DepthMap.shader

DepthMap2直接将物体的深度渲染到纹理中,而不依赖_CameraDepthTexture,这样就不会有硬件上的限制了。



1 0
原创粉丝点击