Catlike Coding CustomSRP部分的练习笔记,记录了工程思路、知识点和一些注意事项。跟随的中文翻译版本。英文原教程页面这里
4. 方向光阴影
阴影的实现:1.渲染阴影贴图 2.采样阴影贴图
- 渲染阴影:使用Shadow Map实现场景阴影。在渲染场景前,先使用LightMode为ShadowCaster的Pass,通过光源对场景取景,将光源可见场景中最近的片元深度信息保存到ShadowMap中,真实渲染过程中,将渲染的片元转换到光源相机空间计算深度值,判断在不在阴影范围内。
- 渲染阴影前,进行一些属性配置,比如渲染阴影的最大距离和阴影贴图的大小。如果把摄像机看到的物体全部进行阴影绘制,那么性能开销大,阴影贴图也需要很大尺寸。所以设置最大距离100,阴影贴图的大小设置一组枚举。把阴影设置看作是管线的设置之一,在PipelineAsset中设置。作为渲染参数传入CameraRenderer,在相机裁剪中配置阴影距离,将相机远平面和最大阴影距离中的小值作为最大阴影距离,在灯光中设置阴影距离。
- 使用阴影类配置阴影。在遍历可见光时,保存可见光的阴影数据。当可投影光源数量小于上限&&光源开启阴影&&阴影强度大于0&&场景中有一个阴影投射物受到了光源影响,满足以上条件时,将可见光记录进投影光源列表中。
这里使用了bool cullingResults.GetShadowCasterBounds(int index, out Bounds b)
方法判断光源是否影响了阴影投射对象。如果光源影响了场景中至少一个阴影投射对象,则为true,out bounds返回封装了可见阴影投射物的包围盒。注意,此处index是光线在场景中的索引,不应该仅仅是方向光的索引,在未来还会增加非方向光的阴影,非方向光源也会应用到这里的索引。 - 渲染阴影。创建渲染纹理,并指定纹理类型是ShadowMap,记得要在相机渲染完后释放临时渲染纹理。指定渲染纹理存储在RenderTarget中,否则会存储到帧缓冲中。调整阴影渲染时机,在渲染场景前渲染阴影。
渲染阴影时,循环遍历所有光线,为所有保存在投影光源列表中的光线渲染阴影。要为单个光线渲染阴影,首先要创建一个ShadowDrawingSettings(cullingResults, lightIndex)
实例,用来创建阴影对象,它需要剔除结果和可见光索引作为构造参数。阴影图本质也是一张深度图,它记录了从光源出发,能看到的场景中距离它最近的表面位置(深度信息)。但是方向光没有真实位置,我们要找出与光源方向相匹配的视图和投影矩阵,并给我们一个裁剪空间立方体,这个立方体与包含光源阴影的摄影机的可见区域重叠,这些数据的获取可以直接调用cullingResults中的方法cullingResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(..., out Matrix4x4 viewMatrix, out Matrix4x4 projMatrix, out ShadowSplitData splitData
获取光源空间的视图变换和投影变换矩阵,以及阴影剔除信息。为渲染过程设置视图和投影变换矩阵,然后执行context.DrawShadows(ref shadowSettings)
方法渲染投射阴影。 - 编写ShadowCasterPass。因为
context.DrawShadows
方法渲染阴影只会渲染Shader中带有ShadowCasterPass的物体,我们需要为Shader编写ShadowCasterPass。因为这个Pass只写入深度数据,所以为Pass添加ColorMask 0表示不写入任何颜色数据。我们只需要得到顶点在裁剪空间的位置和裁剪的基础颜色,片元函数的作用只是裁剪不满足阈值的片元。 - 多光源阴影投射。为每个平行光都在阴影图集中分配一个独立的图块,当灯光数小于1,图块不拆分,大于一就拆分成4块。为每个平行光计算各自的图块在阴影图集上的坐标偏移量。为每个光线渲染ShadowMap时,我们通过
buffer.SetViewport(Rect(x,y,width,height))
调整视口的位置来将渲染位置放在图集的相应图块上。
- 采样阴影
- 保存阴影转换矩阵。我们要将片元转换到光照空间,找到对应的纹理坐标,所以要将每个光源的阴影转换视图和投影矩阵保存下来,将其发送到GPU。
- 将阴影贴图图块相关信息也要保存并发送给GPU,如阴影图块偏移、图块拆分数量、视图投影矩阵等。将阴影变换矩阵的视口映射到阴影纹理图集空间,并找到正确tile上。首先判断当前图形API,如果使用了反向z-buffer将矩阵的z反转,由于深度缓冲精度有限,能表示的深度数量有限,反转能更好地利用这些位(储存为unsigned形式)OpenGL中0是0深度,1是最大深度,Dx中0是0深度,-1是最大深度。
- 获取阴影数据。在保存方向光阴影时记录阴影强度和阴影图块的偏移(光源id)。将所有的方向光阴影数据发送到GPU。在shader中接收所有的方向光阴影数据。
- 正式采样阴影图集。创建shadows.hlsl专门对阴影图集采样。阴影图集不是常规纹理,所以使用TEXTURE2D_SHADOW宏定义阴影图集。我们将使用一个特殊的SAMPLER_CMP宏来定义采样器状态,这个宏定义了一种不同的方式来采样阴影贴图,因为常规的双线性过滤对深度数据没有意义。我们对阴影贴图进行采样的方式就是比较深度值,因此我们可以定义一个显式的采样器状态,将单个分量与指定的比较值进行比较,而不是依赖Unity推导的渲染纹理状态。通过唯一的名字,我们可以内联定义一个采样器状态,这里我们使用名字是sampler_linear_clamp_compare,同时为其定义了一个简写的SHADOW_SAMPLER宏。
- 通过SAMPLE_TEXTURE2D_SHADOW宏对阴影图及采样,它和其他采样器一样,需要阴影图集、采样器和片元在纹理空间的位置。
- 注意:采样阴影图集的结果是根据有多少光到达表面决定的,它是[0,1]区间的值,通常叫做阴影衰减因子,如果片元完全被阴影覆盖就是0,如果没有任何阴影覆盖就是1,之间表示被部分阴影遮挡。还要考虑,当灯光的阴影强度为0时,阴影衰减值永远是1,也就是没有阴影覆盖,此时就与阴影采样无关了。所以最终结果是阴影采样被灯光阴影强度插值的结果。
- 获取灯光阴影衰减。为灯光新增阴影衰减属性。获取CPU传来的阴影强度和图块位置。将shadow.hlsl计算得到的阴影衰减传递给灯光。
- 计算灯光时,将灯光的阴影衰减添加到入射光强度中。
注意:此时的阴影会有很强的自阴影,需要后续优化。
- 级联阴影
这时的阴影贴图通常会有透视走样的问题。透视走样指越靠近相机,边缘的锯齿化就会越严重,因为阴影贴图的分辨率是固定的,同样大小的阴影所对应的阴影贴图中的纹素大小也是固定的(阴影贴图使用正交投影,因此阴影贴图中的每个纹素都有固定的世界空间大小)。如果使用透视相机,其效果是近大远小,在渲染时,阴影越靠近相机,越容易出现多个片元从阴影贴图中的同一纹素进行采样的情况。这几个片元是同一阴影值,从而产生锯齿边。提高分辨率可以缓解问题,但无法解决。(本质原因:光源照射方向与法线方向不一致,导致光源到表面的深度会有锯齿状波动。)
(和MIPMAP的原因一样)
- 设置级联。在阴影设置中设置级联数量和每层级联对应的级联比例。
- 渲染级联。每个级联都需要自己的阴影转换矩阵,所以需要调整阴影转换矩阵的大小。为每个方向光保存多个连续的阴影图块作为级联阴影。在渲染每个光源阴影时,循环遍历级联索引,获得每个级联阴影的视图投影矩阵和阴影剔除数据。
- 级联包围球。为子视截体构建投影矩阵时,要在生成的阴影贴图中(阴影贴图空间),并尽可能减少当前不在视野内的无关区域。也就是说,要尽可能计算出与子视截体紧密贴合的投影矩阵,投影矩阵用正交投影(相当于一个长方体AABB包围盒),投影空间是一个能包住子视截体的包围盒。但因为在渲染时,摄像机的位置和朝向等属性会及时改变,每个层级的子视截体也在不停变化,正交投影包围盒也在变化,这样就可能导致前后两帧包围盒发生突变,进而使生成的阴影贴图分辨率突变,产生阴影抖动问题。解决方法是把包围盒改成包围球。包围球的缺点是视截体外的空间也被包含,会在剔除区域以外也看到阴影。因为每个方向光的方向和球无关,所以每个方向光都可以使用同一个包围球,所以我们需要保存级联等级的数量个包围球数据即可。因此:我们需要知道这些球体应该从哪个级联中采样,因此在shader中定义级联数量、包围球数组(vec4,xyz位置,w半径)
- 采样级联。将级联数量、级联包围球数据、级联阴影转换矩阵在shader中接收。新建ShadowData数据,保存级联索引。计算级联等级,根据表面位置和级联包围球球心距离和级联包围球半径的大小关系,找到相应的级联等级。
- 级联剔除。在计算级联等级时,当级联等级超过最大的级联等级时,说明超过阴影范围了,要剔除。在ShadowData新增阴影强度数据,当超过阴影范围时,阴影强度设置为0。同样,当片元深度超过最大阴影深度时,也要进行阴影剔除。所以在shader中接收CPU传来的最大阴影深度,在片元函数中保存表面在视图空间中的深度,当表面深度大于最大阴影深度时,将阴影强度同样设置为0.
- 阴影过渡。使用公式获得过渡阶段的阴影强度,以淡化阴影。为表面的深度,是最大阴影距离,为阴影过渡范围。在shadowsetting中引入阴影过渡范围,并将阴影过渡范围和最大阴影距离一起发送给GPU。
- 级联过渡。与阴影过度类似,在最后一个级联边缘对阴影进行平滑过渡。使用公式:, 表面深度,包围球半径,过渡参数。
- 阴影质量
目前实现的阴影有严重的自阴影现象,阴影渗漏(shadow acne)。产生阴影渗漏的主因是阴影贴图分辨率问题,如果分辨率比较小,导致场景中多个片元在计算阴影的时候对应上了同一个纹素,导致判断该点有没有阴影时出现了问题。假设光线方向与表面法线方向不一致,那么就会导致对表面上四个片元来说对应同一阴影纹素,都要与此阴影纹素对比表面深度,由于光线方向与表面法线方向不一致,而自然会有两个片元深度比该纹素浅,不被阴影遮挡,而两个片元深度比纹素深,从而被遮挡。提高阴影分辨率可以减小阴影渗漏现象,但不能解决。
- 调整阴影偏差Bias。计算片元深度时,减去一个偏差值,使原来比纹素深度大的两个片元也能深度小于阴影纹素,去掉了自阴影。问题:难以定量地针对当前被照明物体的表面凹凸程度设置准确的偏差值。Unity使用的是基于物体斜度比例的深度偏差值。大部分改善对阴影深度贴图采样误差的算法,核心思想是分析待绘制场景中各部分内容对采样误差的影响程度。unity默认设置(1.0 1.0),自测(1.0,2.0)好用。深度偏差会带来影物漂移,我们不用
- 级联数据。自阴影的大小与世界空间的阴影纹素有关,不同级联的纹素大小不一样,所以需要向GPU发送更多数据。
- 法线偏差:在采样阴影时,使表面法线方向偏移一些,然后对表面的一点进行采样,如果距离足够远就可以避免阴影渗漏。这会让阴影位置发生稍微改变,比如边缘不齐或假阴影,但不会导致影物漂移。做法:沿表面法线稍微移动表面位置。如果只考虑一个维度,那么移动距离等于世界空间中一个纹素大小即可。纹素大小计算:包围球直径/阴影贴图尺寸。法线偏移计算:法线*纹素大小,然后加上表面位置即为偏移后的表面位置。
- 可调节的偏差:光源组件上有bias和normalBias两个属性,将bias当作斜度比例偏差值也就是
buffer.SetGlobalDepthBias
第二个参数,然后把normalBias也参与到计算好的法线偏差中,使法线偏差可以自由调节。需要注意,创建新方向光都要根据实际情况调节,否则会出现严重的影物漂移。
总结最终的自阴影的解决方法:
- 斜度比例偏差。通过设置
buffer.SetGlobalDepthBias(0f, light.slopeScaleBias);
来获取光源的斜度偏差数据,设置斜度偏差。渲染完毕阴影后要将深度偏差复原。 - 法线偏差。计算纹素大小,并使顶点位置沿法线偏移一个纹素大小,灯光属性里还有一个法线偏差参数,作为法线偏差距离的缩放参数。
float3 normalBias = surface.normal * (dirShadowData.normalBias * _CascadeData[shadowData.cascadeIndex].y);
法线偏差距离 = 表面法线光线法线偏差缩放参数纹素大小。
计算表面在纹理空间的位置时,把表面世界空间位置加上法线偏差距离,得到偏移后的表面位置,用偏移后的表面位置参与纹理空间的位置的计算。
- 阴影平坠
阴影平坠:导致阴影渗漏的另一个潜在问题是Unity应用了阴影平坠(shadow pancaking)技术,那么可以剔除不希望看到的阴影。Idea是:渲染方向光阴影时,通过剪裁光照空间,给该空间设定阴影视锥体近裁剪平面,只有在近平面以内的物体才能投射阴影,且阴影视锥体近裁剪平面会尽可能向前移动(排除了不可见的阴影投射物),意在减少光照空间范围,从而提高阴影贴图精度,减少阴影渗漏。但是问题在于:对于穿过阴影视锥体裁剪平面的物体,会带来瑕疵,产生不正确的阴影。Unity通过调整QualitySettings中的ShadowNearPlaneOffset避免发生这个问题。
- 在ShadowCasterPass中,通过在顶点函数中将顶点位置限制在阴影视锥体近平面内以解决问题。阴影视锥体内的顶点,如果超出了视锥体近平面,那么就让它深度贴在近平面上。以防止近平面漏光。
- 这对在近平面两侧的阴影投射非常有效,但是对于大型物体来说只影响了部分顶点,而与近平面有穿插的投影会变形。我们通过把视锥体近平面往后拉一下来缓解这个问题,为方向光定义近裁剪平面偏移属性。
- 百分比切近滤波(PCF)
阴影锯齿问题:锯齿原因在于,判断片元是否在阴影内,进行深度测试时,要把该片元从当前摄像机的观察空间转移到光源空间,转换矩阵不同,且阴影贴图分辨率不大,导致观察空间多个片元对应一个纹素。最直接的方法就是提高阴影贴图分辨率,但内存占用大,而且无法解决问题。实际开发中通过适当的分辨率+区域采样的方法改善锯齿现象。因为阴影贴图的纹素存储的不是颜色信息而是深度信息,对深度取均值会产生不正确的结果,所以锯齿不能通过局部均值方式消除。百分比切近滤波(Percentage close filtering,PCF)方法是对阴影比较测试后的值进行滤波,可以使生成的阴影边缘平滑柔和。
片元着色器中,将当前片元先转换到光源空间,然后经过通过投影和视口变换到阴影深度贴图空间,假设变换后深度为z,贴图坐标为(u,v),对应纹素坐标为z0.如果不使用PCF,那么就会根据z和z0的大小判断片元在阴影中是全黑还是不黑。PCF对(u,v)处周围纹素也采样获取深度值,如果周围纹素深度值小于当前片元深度值,表示更靠前,不在阴影中,在卷积核中置0,否则置1,然后求此卷积核的平均值,作为柔滑边缘的系数。
PCF步骤:
- CustomLit Pass中添加滤波关键字
- 阴影设置中设置滤波设置
- 根据滤波设置,激活对应的关键字
- 传递阴影图集大小和纹素大小
- 引用ShadowSamplingTent.hlsl定义采样方法和采样点数量。
- 调用采样方法使用大尺寸的PCF会使阴影变平滑,但会出现阴影渗漏现象(原因是PCF超采样又使一个纹素对应多个片元了)。需要使法线偏差匹配滤波模式的尺寸。
烘焙光照
场景中的间接光照信息通过烘焙获取,静态物体的间接光照信息烘焙到光照贴图、用于照明动态物体的信息是烘焙到光照探针。间接光照是全局光照的一部分,光线通过环境或自发光物体表面照射而来。
- 烘焙静态光照
- 设置场景的光照设置。将LightSetting中MixedLighting中选择Baked Indirect光照模式,勾选Baked Global Illumination。将需要贡献间接光照的物体勾选Mesh Renderer组件上的Contribute Global Illumination,该对象就能够作为光线反射的对象,提供间接照明。
- 采样烘焙光照。定义GI.hlsl,定义struct GI和内部的漫反射颜色。间接光照的来源是不固定的,因此只能用于漫反射。而镜面反射通常通过反射探针实现。
- 光照贴图的UV坐标:需要首先由unity将其发送到着色器中,需要管线对每个烘焙了光照信息的物体都这样做。drawingSetting中可以设置PerObjectdata. Lightmaps。litshader中添加LIGHTMAP_ON的编译指令。光照贴图是顶点数据的一部分,在顶点和片元结构体中使用宏定义它。光照贴图的UV坐标由Unity为每个Mesh生成,将Mesh平铺展开,像纹理一样且不重叠不拉伸不旋转,以便将其映射到纹理坐标,然后使物体均匀且不重叠地按照缩放和偏移放置在这个贴图中,就像BaseUV中应用缩放和偏移一样。在UnityPerDraw中定义光照贴图的UV转换属性,动态st是为了避免兼容问题而导致SRP批处理中断。将光照贴图UV应用UV ST偏移。
- 采样光照贴图。使用
SampleSingleLightmap
方法对光照贴图采样,SampleSingleLightmap的参数,第一个是TEXTURE2D_ARGS宏,需要将光照贴图和采样器作为参数,第二个是UV坐标,第三个是UV缩放和偏移,第四个是bool值表示是否压缩了光照贴图,通过UNITY_LIGHTMAP_FULL_HDR来判断,最后参数包含了解码指令的float4类型变量
SampleSingleLightmap(TEXTURE2D_ARGS(unity_Lightmap, samplerunity_Lightmap),lightMapUV, float4(1.0,1.0,0.0,0.0),
#if defined(UNITY_LIGHTMAP_FULL_HDR)
false,
#else
true,
#endif
float4(LIGHTMAP_HDR_MULTIPLIER, LIGHTMAP_HDR_EXPONENT,0.0,0.0));
- 光照探针
光照贴图无法作用在非静态物体上,运动的场景和物体就显得不协调,所以我们使用光照探针模拟光照贴图效果。
原理是:在某一光照探针的所在位置点上对光照信息进行采样,然后从该光照探针相邻的其他光照探针的位置上对光照信息进行采样,把这些采样得到的光照信息插值运算,可以得出光照探针之间某个位置的光照信息。运行期间这些插值的速度很快,可以得到实时渲染的要求。
从实现的角度来说,光照探针对照亮在3D空间中的某个指定点的光照信息在运行前的预计算阶段进行采样,然后把这些信息通过球谐函数编码打包储存。游戏运行时,着色器程序可以把这些光照信息编码快速地重建出原始光照效果。光照探针同光照贴图一样储存场景中的照明信息,不同之处在于光照贴图存储的是光线照射到场景物体表面的照明信息,而光照探针则存储的是穿过场景中空白空间的光线信息。光照探针之间的连线表示在空间中光线的传递路径。光照探针的限制是:如果要处理光照的高频信息,球谐函数的阶数就要增大,提升阶数性能耗费增加,因此Unity使用3阶球谐函数,忽略光的高频信息。最简单的光照探针布局方式是将光照探针排列成一个规则的3D网格央视,这样的设置方法简单高效,但会消耗大量内存,因为光照探针本质上是个球形的且记录了当前采样点周围环境的纹理图像。如果一片区域的照明信息都差不多那也没必要使用大量光照探针,光照探针通常用于照明效果突然改变的场合。
球谐函数部分请看GAMES202 IBL部分。
- 采样光照探针。光照探针的插值数据需要逐对象地传给GPU,drawsettings中设置。在UnityPerDraw缓冲区中定义7个float4类型的变量来接受光照探针数据,它们是代表红色、绿色和蓝色的多项式组件,命名为unity_SH([AB][rgb])|C。在GI文件中对光照探针进行采样光照探针,如果对象正在使用光照贴图则直接返回0,否则返回max(0,SampleSH9()),该方法用于采样光照探针信息,需要光照探针数据和表面法线数据作为传参。
- 光照探针代理体(LPPV)
光照探针代理体(LPPV):在3D空间的位置点上,因为有且只有一个球面表达式用于描述光照,所以光照探针照明还不适合用于描述光线穿过一个很大物体的情况,因为这种情况光照会发生很多的变动,从而无法精准地模拟。光照探针适合小物体,它的照明是基于一个点因此不适用于大物体。另一个限制是,因为球谐函数是在一个球面上对光照信息进行编码,所以对于有大型的平坦的表面的物体或者有凹面的物体,光照探针照明技术也不适用。如果想在大物体上用光照探针照明,则需使用光照探针代理体(Light Probe Proxy Volume)组件。LPPV是“解决无法直接用光探针技术处理大型动态游戏对象问题”的组件。
- 给物体挂载LPPV,并配置
- 采样LPPV,用drawingsettings将数据发送给GPU,UnityPerDraw定义LPPV的四个属性 。
- 判断是否使用了LPPV或光照探针,如果使用了,则必须通过SampleProbeVolumeSH4方法对LPPV采样。
SampleProbeVolumeSH4(TEXTURE3D_ARGS(unity_ProbeVolumeSH, samplerunity_ProbeVolumeSH), s.position, s.normal,
unity_ProbeVolumeWorldToObject, unity_ProbeVolumeParams.y, unity_ProbeVolumeParams.z, unity_ProbeVolumeMin.xyz,
unity_ProbeVolumeSizeInv.xyz);
3.判断并采样对LPPV进行采样需要对代理体的空间进行转换,以及一些其他的计算,例如代理体纹理采样和球谐函数应用等,这种情况下使用L1球谐函数可能结果会不太准确,但可能因单个物体表面而异。
- Meta Pass处理漫反射
因为间接漫反射光照会从表面反射出来,因此还应该受到这些表面的漫反射率的影响。Unity使用一个特殊的Meta Pass来确定烘焙时从表面反射的光照,然后提供给烘焙系统,从而间接计算光照。目前没有定义该Pass,Unity默认表面为白色。
- 添加Meta Pass,Pass的Light Mode设置为Meta,关闭剔除功能。
- 像采样光照图一样,使用光照贴图的UV坐标。不同的是,我们将UV赋值给对象空间顶点位置的XY分量,Z分量限制到[0,FLT_MIN]区间,FLT_MIN是最小正浮点数。然后使用TransformWorldToHClip方法将顶点转换到裁剪空间中。(就是需要这么做)
- Meta Pass通过定义一个bool4类型的标记向量unity_MetaFragmentControl进行通信,如果标记了x分量,则需要漫反射率。这已经足够给反射光照着色,但是Unity的Meta Pass还通过加上粗糙度乘一半的镜面反射来提升效果。这是因为考虑到高镜面但是粗糙的材质也可以传递一些间接光照。最后通过PositivePow扩大反射光照,将其限制到unity_MaxOutputValue.
- 在着色函数中计算全局光照的漫反射,应用到表面漫反射项上,得到间接光照的漫反射。
- 自发光表面
自发光表面:有些表面可以发出光,尽管场景中没有任何照明,因为它不是真正的光源,所以不能影响其他表面,但是可以参与烘焙光照贴图的计算中,从而照明周围静态物体。
- 给shader增加自发光纹理和自发光颜色两个属性。
- 烘焙自发光。在Meta Pass中判断,如果unity_MetaFragmentControl标记了y分量,则返回自发光颜色。但是现在自发光物体还不能参与烘焙光照的计算中,也不能照亮其他物体,自发光是通过单独的Pass烘焙的,需要对每个材质进行烘焙自发光的配置才行。
阴影蒙版
- 烘焙阴影
使用光照贴图的好处是可以不限于阴影的最大距离,烘焙的阴影在最大距离之外也不会被剔除。通常情况下,我们可以设置在阴影最大距离之外使用实时阴影,超过范围则使用烘焙阴影。
ShadowMask照明模式:阴影蒙版是一种纹理,它和与之搭配使用的光照贴图纹理使用相同的UV采样坐标和纹理分辨率。ShadowMask的每一个纹素存储着它对应场景位置点上面最多4个光源(因为目前的GPU架构,一个纹素最多只支持RGBA 4个颜色通道)在该位置的遮挡信息,即记录这一点有多少光源能照到。
将LightMode改为ShadowMask照明模式,此模式下Unity会先计算从静止的物体投射到其他静止物体上的阴影。多出来的混合模式光源会转用烘焙式光照计算,具体哪个光源由烘焙光源计算由引擎决定,每个光照探针最多存储4个光源的遮挡信息,如果4个以上的光源发出的光线相交,则其余混合模式光源则会自动改为使用烘焙模式,并且这些光源信息是提前计算好的。因为混合模式光源的阴影蒙版在运行时有所保留,所以运动的游戏对象所投射的阴影可以与预先计算并存储在阴影蒙版中的阴影正确合成,不会导致重复投影问题。
在ShadowMask照明模式里,间接照明效果和阴影衰减都储存在光照贴图中,阴影被储存在额外的ShadowMask中,当只有主光源时,所有被照亮的物体都会作为红色出现在ShadowMask中,红色是因为阴影信息存储在纹理的Red通道中,贴图可以存储四个光照的阴影,它有四个通道。
在ShadowMask模式下,静止的物体向静止的物体投射阴影是不受shadow distance限制的,只有运动的物体向静止的物体投影才受限制,且要在最大阴影距离内才生效,此部分阴影通过阴影贴图实现。同时运动的游戏对象也可以从静止的游戏对象处接受阴影投射,这部分阴影是通过光照探针实现的。一般的ShadowMask纹理比实时阴影的阴影贴图纹理分辨率低。unity会自动对静态和动态游戏对象产生的重叠阴影进行组合,因为控制静态物体的光照与阴影蒙版和控制动态物体的光照和阴影贴图将会被编码为遮蔽信息。
- LightMode中设置Shadow Mask,在Project Settings->Quality->Shadows中Shadowmask Mode设置为Distance Shadowmask模型。
- 使用阴影蒙版。设置shader中,Shadow Mask相关关键字,使渲染管线知道Shadow Mask的存在。
- 定义是否使用Shadow Mask的标记,如果对于场景中的光线,符合Shadow Mask条件,则打开Shadow Mask标记并且将关键字设置为true。
- 让着色器知道是否使用shadowmask,如果使用则得到烘焙的阴影数据。shadow中需要ShadowMask数据,ShadowMask也是ShadowData的数据之一。同时,GI中也需要知道ShadowMask数据。
- GI中,如果开启了Distance Shadowmask模式,则采样shadowmask。
- 在GetLighting中,获取GI中的ShadowMask。
7.drawingsettings中发送逐对象shadowmask数据
- 遮挡探针
遮挡探针:阴影蒙版已经被正确的应用到了静态的光照贴图对象,但是动态对象还没有阴影蒙版数据,因为它们使用光照探针而不是光照贴图。unity将阴影蒙版数据烘焙到光照探针中,称之为遮挡探针。可以通过向缓冲区中添加unity_ProbesOcclusion向量访问。
- UnityPerdraw缓冲区中定义数据
- 没有光照贴图的情况下,返回探针数据
- drawingsettings中发送探针数据。对于探针来说,shadow mask没有使用的通道被设置为白色,所以动态物体处于完全照明时为白色,处在完全阴影中为青色。
- 此时还无法使用UnityInstancing,只有在定义SHADOWS_SHADOWMASK时,遮挡数据才能自动获得实例,所以定义SHADOWS_SHADOWMASK。
- LPPV也可以和shadowmask配合使用,drawingsettings加入LPPV与遮挡探针的配合。在shadowmask采样时需要判断LPPV是否开启,开启则采样LPPV遮挡
- 混合阴影
混合阴影:有了烘焙的阴影和实时阴影,接下来将烘焙阴影与实时阴影混合。超过阴影最大距离使用烘焙阴影,在阴影距离内使用实时阴影。
- GetDirectionalShadowAttenuation方法获得阴影衰减,可以获得烘焙与实时阴影,因此将烘焙阴影与实时阴影的代码分开。
- 分别获取烘焙阴影衰减和实时阴影衰减。
- 判断如果定义了shadowmask的distance属性,则用烘焙阴影衰减替代实时阴影衰减。
- 阴影过渡:根据深度来从实时阴影过渡到烘焙阴影。且根据全局阴影强度在二者之间插值。使用单独对cullingResults.GetShadowCasterBounds(index, out Bounds b)的判断,确定光源是否使用阴影遮罩,即使没有阴影投射(也就是说超过了最大阴影距离),也要返回光源阴影强度的负值,这样可以在shader中通过是否为负来判断是否超过最大阴影距离,从而在超过最大阴影距离的位置,使用烘焙阴影。
- 支持shadowmask模式。其工作原理与Distance shadowmask相同,但distance shadowmask在不超过阴影距离内,使用阴影贴图的实时阴影,在超过阴影距离外,使用烘焙阴影(静态的用mask,动态的用探针)。shadow mask模式在所有的静态物体只使用烘焙阴影,动态对象只使用阴影贴图。
- 多光源烘焙阴影:shadowmask纹理共有4个通道,最多支持4个mixed光源,烘焙时,最重要的方向光的阴影信息存储在R通道中,第二个光源阴影信息保存在G通道,目前的着色器只使用了R通道的阴影信息,需要将光源通道索引发送到GPU来调整,但我们不能以来灯光的顺序,因为灯光随时可变