Unity实时阴影的内部实现

发表于2018-01-23
评论0 4.8k浏览
Unity大中华区技术支持团队近年来为国内Unity用户提供了高质量的技术支持服务,本文将由该团队的技术支持工程师张陈渊,根据自己的丰富经验,为大家分享在Unity中实现实时阴影的方法。

光照在游戏作品中的重要地位早已不容忽视,阴影与光的实现技术一直在高效与真实之间左右权衡着。下面主要通过一个具体的问题探讨Unity实时阴影的内部实现。

两种阴影实现方式
Unity有两种阴影实现方式:烘培阴影与实时阴影


烘培是一种离线计算,它采用光线追踪算法来模拟现实世界中光的物理特性,如反射,折射及衰减,光无法到达的地方皆为阴影;实时阴影是一种更加精简的模拟,它忽略掉了光的众多物理特性,利用数学方法人为地去制造阴影。您可以通过下图更形象地了解到两种阴影之间的计算区别。

 
烘焙过程中,光线无法到达的地方成为阴影

实时阴影更像人为地绘制,将灯光角度无法看到的区域,绘制上阴影的颜色

烘培阴影是光线追踪算法的自然产物,准确无误,真实过渡。但由于其计算量巨大,阻碍了它在游戏中的实时运用。不能实时运用并不代表光线追踪不能应用到游戏中,实际上游戏中存在大量静止的物体,如场景中的地形,房屋等,在灯光不变的情况下,这些物体产生的阴影也是固定不变的。

由于这种需求普遍存在,Unity的烘焙系统随之诞生:通过离线计算,生成记录光照信息的Lightmap,渲染静态物体时采样Lightmap,进而产生较真实的阴影效果。对于动态物体而言,Unity使用Shadow Mapping技术,高效地产生实时阴影。由于篇幅限制,本文我们主要通过一个具体的问题探讨一下Unity实时阴影的内部实现。

实时阴影的内部实现
有企业级客户提到这样一个问题:半透明物体的阴影效果是如何实现的?

回答这个问题之前,有必要先了解Unity实现不透明物体阴影的基本原理。为了简化问题,假设场景中只有两个物体,石头A和地面B。


为了最快速地分析图像问题,我们列举一下渲染流程:首先,从摄像机角度生成A与B的深度值到Depth Buffer深度缓冲中。


接下来,从灯光视角生成A与B的深度到Shadowmap深度图,这里有四张深度图,可以认为是不同的精度。

3,4:近视角高精度;1,2:远视角低精度

然后进入整个流程的核心部分,利用上面两个步骤的数据生成Screen Space Shadow Mask纹理,基本方法为:通过深度缓冲可将屏幕空间的点还原到世界空间,然后将此世界空间坐标转换到灯光视角空间,投影之后的Z值再与对应深度图的值进行比较,如果大于深度图,说明此屏幕空间上的点是阴影。


最后一步,渲染A和B到Color Buffer颜色缓冲中,将世界空间的点转换到屏幕空间,采样Screen Space Shadow Mask纹理得出阴影,最后可根据自定义需求,与材质颜色进行相应的混合得出最终的阴影显示效果。


那么,现在开始进入正题了,之所以会有人问半透明物体的阴影如何产生,其根源在于:虽然半透明物体可以不写深度缓冲,但必须写深度图,那么半透明物体的深度值如何写入深度图呢?

众所周知,写深度这一操作,只有两种方式,写与不写:对于不透明的物体来说,深度一直写;对于半透明的物体来说,假设透明度是alpha,alpha=0不写,alpha=1写,那么alpha=0.5究竟写还是不写呢?

带着这个问题,让我们来看看Unity的标准Shader是如何处理的。


上边代码的红框决定深度是否写入一张3D纹理,如变量_DitherMaskLOD。设置变量vpos和alpha对3D纹理进行采样,vpos为正交或透视投影产生的结果,alpha为半透明物体对应的alpha值。由于vpos是变化的,即使alpha都为0.5,采样的结果也会不同,由此来决定此像素是否剔除。

制作这张3D抖动纹理时一定要保证alpha为0或1时采样的结果是固定,alpha大于0.5时,采样到1的概率偏大,alpha小于0.5时,采样到0的概率偏大。通过查看内存,此3D纹理可视化后是这样的:


讲到这里,这个问题的答案已呼之欲出:实时半透明物体阴影与不透明物体阴影在实现上并没有太多区别,只是在生成灯光视角的深度图上,半透明物体在Pixel Shader阶段会采样一张3D抖动纹理,来随机决定此像素是否输出,进而产生近似的半透明阴影效果。

这个算法有一个缺点,由于使用视角数据来采样抖动纹理,所以当视角变化时,阴影必然会有明显的抖动。


解决抖动的优化办法,留给大家去思考。


如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引