Unity5.X中屏幕空间阴影投射技术(Screenspace ShadowMap)如何产生阴影图?
《Unity Shader 入门精要》第 197 页(9.4 节提到),使用 Screenspace ShadowMap 技术后,Unity 会根据相机的深度图和 ShadowMap 的深度作比较来生成阴影图。 我是作者,既然是读者提的问题还是要回答一下。书里在P197下面是这么写的:“如果摄像机的深度图中记录的表面深度大于**转换到**阴影映射纹理中的深度值,就说明……处于该光源的阴影中”,所以意思就和上面几位答主的意思一样,要先转换到同一个空间里再去比较深度值。
然后看到你的提问后,我就在github的issue【常见问题】对9.4节Unity的阴影的补充说明(重要) · Issue #49 · candycat1992/Unity_Shaders_Book里又补充了下,就直接粘贴过来啦
--------------------------------
今天知乎上有人问书里面提到的屏幕空间阴影技术是怎么实现的。这个书上在原理实现上讲得很简略,不过大家只要有耐心都可以在Unity里找到它的实现。这种屏幕空间阴影的实现是延迟渲染里面阴影的常见实现方法,网上也有一些文章介绍它们,例如这篇Tutorial - Deferred Rendering Shadow Mapping。
屏幕空间的阴影
延迟渲染中的光照计算绝大部分都是在屏幕空间里进行的,同样也包括阴影。这种屏幕空间的阴影实现主要有这么几个步骤:
首先得到从当前摄像机处观察到的深度纹理。在延迟渲染里这张深度图本来就有,如果是前向渲染的话就需要把场景整个渲染一遍,把深度渲染到深度图中。然后再从光源出发得到从该光源处观察到的深度纹理,也被称为这个光源的ShadowMap。然后在屏幕空间做一次阴影收集计算(Shadows Collector),这次计算会得到一张屏幕空间阴影纹理,也就是说这张图里面需要有阴影的部分已经显示在图上了。这个过程概括来说就是把每一个像素根据它在摄像机深度纹理中的深度值得到世界空间坐标,再把它的坐标从世界空间转换到光源空间中,和光源的ShadowMap里面的深度值对比,如果大于ShadowMap中的深度距离,那么就说明光源无法照到,在阴影内。最后,在正常渲染物体为它计算阴影的时候,只需要按照当前处理的fragment在屏幕空间中的位置对步骤3得到的屏幕空间阴影图采样就可以了。
上面这个过程在9.4.3节大致提到过。这里再补充一些实现细节问题,其实这个过程就和三张纹理的生成有关系:摄像机的深度纹理,光源的ShadowMap,以及靠前两者得到的屏幕空间阴影纹理。下面主要还是针对Unity里面的实现大概解释一下,希望有兴趣的还是要自己去trace下Unity的各个文件看看它的实现。
下面就是在Frame Debugger里看到的结果:
摄像机的深度纹理和光源的ShadowMap
这两张纹理是前期的准备工作。在Unity里在是前向渲染路径的情况下,这两张纹理主要都是靠有一个Shader中LightMode为ShadowCaster的Pass来完成的,这个实现细节在这个issue上面的答案中给了非常详细的解释。不再赘述。
上面那张ShadowMap有很多空白区域是因为开启了4层的Shadow Cascades,所以实际上渲染了四张ShadowMap。由于这个场景的FarPlane值比较大,而物体离摄像机都很近(距离在10以内),所以其他三张就啥也没渲染到。
屏幕空间阴影纹理
这张纹理也是靠内置的一个Shader渲染得到的。从Unity 5.4的Frame Debugger里看到的这个Pass的名称是Hidden/Internal-ScreenSpaceShadows(在DefaultResourcesExtra/Internal-ScreenSpaceShadows.shader)。这个Pass在不同的Unity版本里是不一样的,比如在Unity 5.3里面就是Internal-PrePassCollectShadows(在DefaultResources/Internal-PrePassCollectShadows.shader),看的时候还是要全局搜索确定下。
这个Shader挺长的就不放了,主要思路就是从摄像机的深度纹理里采样得到该fragment的深度值,然后利用矩阵变换计算得到该点对应的世界空间的世界坐标(利用CameraToWorld矩阵),然后再变换到光源空间下的坐标(利用WorldToShadow矩阵),最后拿这个坐标对光源的ShadowMap采样计算阴影。
物体的阴影
在前向渲染里面渲染每个物体的时候,会先计算fragment在屏幕空间的位置scrPos,然后再据此对屏幕空间阴影纹理采样即可。这个在Unity里面就是靠内置宏来完成的,比如SHADOW_COORDS、TRANSFER_SHADOW、UNITY_LIGHT_ATTENUATION那一套,这个也可以自己看到实现代码的。 书的作者乐乐女神已经给出了很详细很亲民的回答,但我还是想凑个热闹……
亲民的回答是不可能的了,硬核的回答倒是有(小声BB)
MaxwellGeng:GPU Driven Pipeline — 工具链与进阶渲染在这篇文章中,我是使用了比较基础的CommandBuffer的API实现了一个基本纯手动的Cascade Shadowmap,先说目的,为什么要使用Screen Space,原因是使用自定义的Deferred Shading Pass,所以深度图是直接随着GBuffer一起生成的,直接用深度图计算更方便。
深度图储存着投影空间下的z轴坐标,投影空间的坐标则是float4(screenUV * 2 - 1, depth, 1),图形学基础不必多说,再通过传入的VP矩阵的逆矩阵,也就是(projection * view).inverse,将深度转换到世界坐标,也就拥有了每个像素的世界坐标,shadowmap同理,最后比较两点距离灯光距离即可判断是否被遮挡,是否投影。
其他投影细节文章里讲的也比较清楚了。 需要换算到统一坐标系 我假设你理解了shadowmap的原理。
那么来看一下常规shadowmap跟screenspace的shadowmap有什么区别。
常规shadowmap的话,阴影的计算我们是在渲染每个模型的时候计算出来的,这个时候我们知道模型的位置信息,shadowmap和shadowmap的相机信息。根据这些信息我们可以计算出遮挡关系。
那么再来看一下屏幕空间(后处理postprocess)的阴影,因为是在屏幕空间,可以粗糙的理解为是处理一张2D图片的时候需要计算出阴影。但是的但是,在常规shadowmap的算法里面,是需要模型位置信息、shadowmap、shadowmap的相机信息。那么在屏幕空间中,shadowmap和shadowmap的相机信息都可以通过参数传递进去,跟之前没有任何差别。那么唯一缺少的是什么?模型位置信息,这个信息很关键,我们需要知道模型的位置数据,那么模型的位置数据从哪里来?简单的我们可以在渲染每个模型的时候把模型数据写入到rendertarget。但是一般不这么做,大家都是渲染正常视野的深度图,因为这个深度图用处很多,例如制作体积光,景深,软阴影等特效。这里尤其要注意,我们这里有了两张深度图,一个是灯光空间的深度图(shadowmap),一个是相机空间的深度图(Depthmap)。这两个都是深度图,只是用途不一样叫法不一样而已。
既然大家都在用深度图,那么就说明可以从深度图重建出模型的坐标体系(有很多需要重建3d场景得技术都是基于这个算法的,这也是为什么手机现在也有深度相机的功能原因)。深度图重建出坐标的算法就不细说了,手机打字不方便。但是你可以百度:深度图重建坐标
当重建出坐标之后,后面的流程就跟之前的一样了。 根据相机的深度图能计算到世界坐标,进而去算到光源方向的裁剪空间下的深度,用此深度与之前渲染的ShadowMap比较即可。
页:
[1]