Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【常见问题】Unity阴影和深度纹理的补充说明(重要) #49

Open
candycat1992 opened this issue Sep 22, 2016 · 7 comments

Comments

@candycat1992
Copy link
Owner

candycat1992 commented Sep 22, 2016

在9.4节,书里提到Unity会在一些平台上使用屏幕空间的阴影映射技术,并在后面解释了如何让物体投射和接收阴影。这里有一些内容需要补充。

  • 截止到Unity 5.4,当项目工程的目标平台是Mobile的时候,就不会使用屏幕空间的阴影映射技术,即使用原始的Shadows Map方法。在代码里,Unity会定义内置宏UNITY_NO_SCREENSPACE_SHADOWS来控制。而当项目工程的目标平台是支持屏幕空间阴影的话,例如PC, Mac & Linux Standalone平台(其他平台还没有验证过……有验证过的欢迎补充)时,会开启屏幕空间的阴影映射技术。正如书中P201下面所写的那样,由于宏UNITY_NO_SCREENSPACE_SHADOWS的定义与否,会生成不同的代码。读者可以通过帧调试器(Frame Debugger)来分辨当前是否使用了屏幕空间的阴影映射技术:

    2016-09-22 21 14 39 2016-09-22 21 32 51
    左边是PC平台,使用了屏幕空间的阴影映射技术,可以看到渲染事件明显较多,除了需要生成光源的阴影映射纹理,还增加了生成摄像机的深度纹理和生成屏幕空间阴影纹理的渲染事件。而右边是Mobile平台,使用的是传统的Shadow Map,只需要为光源生成阴影映射纹理。
    P.S.:左边在生成光源的阴影映射纹理时也显然比右边多了很多渲染事件,这是因为PC平台上为了效果和性能使用了Shadow Cascades技术,在上图里面4个cascades,而Mobile平台上Shadow Cascades是禁用的。

使用的是屏幕空间的阴影映射技术还是传统的Shadow Map也会影响阴影效果。

屏幕空间的阴影映射技术

此时LightMode为ShadowCaster的Pass会同时影响阴影投射和阴影接收的效果。具体来说,不管是希望物体能够向其他物体投射阴影还是自己接收来自其他物体的阴影,都需要定义LightMode为ShadowCaster的Pass。

因为对于投射阴影来说,需要使用LightMode为ShadowCaster的Pass来生成该光源的阴影映射纹理,这样如果它距离光源更近的话就会记录到阴影映射纹理中;对于接收阴影来说,也需要使用这个Pass来生成屏幕空间的深度纹理,从而可以在计算屏幕空间的阴影纹理时,可以据此来判断该点是否在阴影中。简单来说,因为阴影纹理是一张在正常渲染物体前就生成好的,因此**无论是阴影投射还是阴影接收,都需要定义LightMode为ShadowCaster的Pass**。

需要注意的是,是否会渲染到生成深度纹理不仅仅和LightMode为ShadowCaster的Pass相关,也和渲染队列有关。正如Unity文档中所说:

Note that only “opaque” objects (that which have their materials and shaders setup to use render queue <= 2500) are rendered into the depth texture.

因此,如果想要确保一个物体会被渲染到深度纹理中,我们需要保证它使用的shader:包含一个LightMode为ShadowCaster的Pass,并且它的渲染队列<= 2500。

综上,我们给出和屏幕空间的阴影相关的几个变量的使用规则:

渲染到Depth Texture 渲染到Shadowmap Receive Shadows的开关 Cast Shadows的开关
有一个LightMode为ShadowCaster的Pass,渲染队列<=2500 有一个LightMode为ShadowCaster的Pass,且打开了Cast Shadows 控制利用SHADOW_ATTENUATION等宏采样的结果,但不会影响Depth Texture和Shadowmap的渲染,换句话说它仅仅是不显示出来物体上的阴影而已 控制该物体是否会加入到Shadowmap的渲染

我们给出在屏幕空间的阴影映射技术下,要实现各个效果所需要的变量设置:

效果 Depth Texture Shadowmap Receive Shadows Cast Shadows 解释
接收阴影,且投射阴影 Y Y Y Y 标准情况,但如果是半透明物体的话会出现图1那样的情况,地板有穿帮。这是因为在渲染屏幕空间的阴影图时,由于在Depth Texture上地板区域的深度值被半透明物体覆盖,因此在计算阴影时计算的是半透明物体位置对应的阴影,而这部分区域是不在阴影内的
接收阴影,但不投射阴影 Y N Y N 为了不让物体渲染到Shadowmap就需要在物体面板关闭Cast Shadows,但同样对于半透明物体来说有明显穿帮,如图2所示,这是因为在渲染屏幕空间的阴影图时,通过Shadowmap判断地板区域没有物体挡住它,因此地板上有一块明显的非阴影区域
不接收/挡住阴影,但投射阴影 N Y - Y Receive Shadows只是控制宏采样的结果,不会影响是否渲染到Depth Texture,因此是否打开都不重要。这种情况下物体也不会显示任何来自其它物体的阴影,如图3所示

transparent_shadows

对于Unity 5的Standard Shader的实现,如果我们把它的Rendering Mode设置成Transparent,它的阴影处理方式是和上表中的第三种,即不接收/挡住阴影,但投射阴影,是相同的。除此之外,为了实现半透明阴影的效果,Standard Shader会配合使用Dither的方法来伪造效果。具体可参见issue

传统的Shadow Map

由于不会提前渲染Depth Texture,此时LightMode为ShadowCaster的Pass仅会影响阴影投射的效果。对于需要接收阴影的物体来说,由于它们会在普通的Pass中进行空间转换和判断是否在光源阴影内的计算,而不是提前生成的信息,因此它不需要定义LightMode为ShadowCaster的Pass也可以正确接收到其他物体的阴影。

为了完整性,我们给出给出和Shadow Map阴影相关的几个变量的使用规则:

渲染到Shadowmap Receive Shadows的开关 Cast Shadows的开关
有一个LightMode为ShadowCaster的Pass,且打开了Cast Shadows 控制利用SHADOW_ATTENUATION等宏采样的结果,但不会影响Shadowmap的渲染,换句话说它仅仅是不显示出来物体上的阴影而已 控制该物体是否会加入到Shadowmap的渲染

我们给出在Shadow Map阴影映射技术下,要实现各个效果所需要的变量设置:

效果 Shadowmap Receive Shadows Cast Shadows 解释
接收阴影,且投射阴影 Y Y Y 标准情况,如果是半透明物体的话会出现图1那样的情况。注意到这与屏幕空间阴影的效果是不同的,如图4
接收阴影,但不投射阴影 N Y N 为了不让物体渲染到Shadowmap就需要在物体面板关闭Cast Shadows。注意到这与屏幕空间阴影的效果也是不同的,如图5
不接收/挡住阴影,但投射阴影 Y N Y Receive Shadows控制宏采样的结果,因此我们只需要关闭Receive Shadows即可,如图6所示

transparent_shadowmap


这一点需要在书中9.4.2节第二部分”让物体接收阴影“中补充。

@candycat1992 candycat1992 changed the title 【常见问题】对9.4节Unity的阴影的补充说明 【常见问题】对9.4节Unity的阴影的补充说明(重要) Oct 21, 2016
@candycat1992
Copy link
Owner Author

今天知乎上有人问书里面提到的屏幕空间阴影技术是怎么实现的。这个书上在原理实现上讲得很简略,不过大家只要有耐心都可以在Unity里找到它的实现。这种屏幕空间阴影的实现是延迟渲染里面阴影的常见实现方法,网上也有一些文章介绍它们,例如这篇Tutorial - Deferred Rendering Shadow Mapping

屏幕空间的阴影

延迟渲染中的光照计算绝大部分都是在屏幕空间里进行的,同样也包括阴影。这种屏幕空间的阴影实现主要有这么几个步骤:

  1. 首先得到从当前摄像机处观察到的深度纹理。在延迟渲染里这张深度图本来就有,如果是前向渲染的话就需要把场景整个渲染一遍,把深度渲染到深度图中。
  2. 然后再从光源出发得到从该光源处观察到的深度纹理,也被称为这个光源的ShadowMap
  3. 然后在屏幕空间做一次阴影收集计算(Shadows Collector),这次计算会得到一张屏幕空间阴影纹理,也就是说这张图里面需要有阴影的部分已经显示在图上了。这个过程概括来说就是把每一个像素根据它在摄像机深度纹理中的深度值得到世界空间坐标,再把它的坐标从世界空间转换到光源空间中,和光源的ShadowMap里面的深度值对比,如果大于ShadowMap中的深度距离,那么就说明光源无法照到,在阴影内。
  4. 最后,在正常渲染物体为它计算阴影的时候,只需要按照当前处理的fragment在屏幕空间中的位置对步骤3得到的屏幕空间阴影图采样就可以了。

上面这个过程在9.4.3节大致提到过。这里再补充一些实现细节问题,其实这个过程就和三张纹理的生成有关系:摄像机的深度纹理,光源的ShadowMap,以及靠前两者得到的屏幕空间阴影纹理。下面主要还是针对Unity里面的实现大概解释一下,希望有兴趣的还是要自己去trace下Unity的各个文件看看它的实现。

下面就是在Frame Debugger里看到的结果:
2016-11-20 19 52 35

摄像机的深度纹理和光源的ShadowMap

2016-11-20 20 02 55 2016-11-20 20 27 08

这两张纹理是前期的准备工作。在Unity里在是前向渲染路径的情况下,这两张纹理主要都是靠有一个Shader中LightMode为ShadowCaster的Pass来完成的,这个实现细节在这个issue上面的答案中给了非常详细的解释。不再赘述。

上面那张ShadowMap有很多空白区域是因为开启了4层的Shadow Cascades,所以实际上渲染了四张ShadowMap。由于这个场景的FarPlane值比较大,而物体离摄像机都很近(距离在10以内),所以其他三张就啥也没渲染到。

屏幕空间阴影纹理

2016-11-20 20 03 25

这张纹理也是靠内置的一个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那一套,这个也可以自己看到实现代码的。

@candycat1992 candycat1992 changed the title 【常见问题】对9.4节Unity的阴影的补充说明(重要) 【常见问题】Unity阴影和深度纹理的补充说明(重要) Dec 2, 2016
@candycat1992
Copy link
Owner Author

屏幕后处理使用的深度和法线纹理

在书里的第13章讲了一些使用摄像机深度和法线纹理的特效。这里需要纠正和补充一些关于什么时候以及什么条件下会渲染到这两张纹理(深度纹理以及深度+法线纹理)的内容。

纹理类型 前提 哪些物体会渲染到该纹理 解释
_CameraDepthTexture 在脚本里开启了DepthTextureMode.Depth模式,或者其他需求也需要渲染摄像机的深度纹理(例如如果当前已经开启了屏幕空间的阴影) 有一个LightMode为ShadowCaster的Pass,渲染队列<=2500 _CameraDepthTexture的渲染其实并没有使用到Shader Replacement,而是像上面阴影里解释的那样,是直接在渲染队列<=2500的物体的Shader里找LightMode为ShadowCaster的Pass,因此不需要设置"RenderType"="Opaque"。如果使用了屏幕空间阴影的话,它其实也是渲染阴影时摄像机的深度纹理,即是共用纹理的
_CameraDepthNormalsTexture 在脚本里开启了DepthTextureMode.DepthNormals 物体的"RenderType"="Opaque",对渲染队列、有无LightMode为ShadowCaster的Pass没有要求 _CameraDepthNormalsTexture的渲染机制不同于_CameraDepthTexture,它需要使用Shader Replacement来使用一个全新的Shader(在Unity 5.3中是builtin_shaders-5.3.xxx/DefaultResources/Camera-DepthNormalTexture.shader,在Unity 5.4中是builtin_shaders-5.4.xxx/DefaultResourcesExtra/Internal-DepthNormalsTexture.shader)来渲染所有RenderType为Opaque的物体,因此必须设置"RenderType"="Opaque"才可以保证物体被渲染到了_CameraDepthNormalsTexture中

@chenyong2github
Copy link

chenyong2github commented Jul 12, 2017

Unity版本5.5.1
Hidden/Internal-ScreenSpaceShadows 里的
struct v2f {
...
// View space ray, for perspective case
float3 ray : TEXCOORD1;
}
vert里o.ray = v.normal;为什么就能得到view空间下的法线呢?Unity背后做了什么事情?

@haolly
Copy link

haolly commented Oct 27, 2017

哇, 乐乐女神真是细心,刚看到13.1 节的时候感觉说的不清不楚,于是上issues 来看看,没想到就真的有补充说明 👍

@Matrix64
Copy link

你好!感谢你编写这本书让我入门了UnityShader!
不过我在学习到接收阴影到时候发现,即使只有一个ForwardBass Pass,在编译的时候也必须要加上#pragma multi_compile_fwdbase才能正确显示阴影。我使用Frame Debug查看的时候发现,即使在Shadow.CollectShadows阶段已经处理得到正确的阴影,没有上面一句话也不能正确显示。
想请教一下这句话其实是影响到了哪些内容呢?

@candycat1992
Copy link
Owner Author

你好,对于所有的ForwardBass Pass来说,只要你想让它能够有正确的光照,都需要设置好LightMode,添加#pragma multi_compile_fwdbase。multi_compile_fwdbase会为这个shader自动生成相应的shader variants,这些variants会负责处理各种光照keywords下的光照计算,比如要不要接受阴影等。

@GuoLei1990
Copy link

GuoLei1990 commented Jan 15, 2020

你好女神,简单看了Unity ShadowMap的实现,有一点疑问,在前向渲染下屏幕空间阴影相对于传统比较是否真的有提升呢,直接使用传统的方式实现cascades是否比屏幕空间性能高呢,?
屏幕空间实现:
1.为了得到cameraDepthTex,增加了很多DC(增加)
2.为了得到屏幕空间阴影纹理,又全屏合成了一次,虽然是只有一次,但FS是全屏的(增加)
3.渲染物体的时候简单的采样屏幕空间阴影纹理即可(计算阴影直接采样-节省),有OverDraw问题

传统方式实现cascades:
1.渲染物体得时候直接计算阴影,阴影部分计算开销同屏幕空间实现的第2步,有OverDraw问题

那是否除非OverDraw非常多的时候(传统方式OverDraw计算阴影的代码开销更大一些)屏幕空间阴影才有优势呢,是否实际情况很难多到可以抵消屏幕空间1、2步的开销呢?还是Unity只是为了让前向和延迟的阴影方案保持保持一致性,也可能是我忽略了哪里。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants