找回密码
 立即注册
查看: 478|回复: 0

Custom SRP 学习杂记3-LOD和反射、点光源和聚光灯

[复制链接]
发表于 2022-3-9 21:33 | 显示全部楼层 |阅读模式
Catlike Coding CustomSRP部分的练习笔记,记录了工程思路、知识点和一些注意事项。跟随的中文翻译版本。英文原教程页面这里
LOD和反射



  • LOD
    LOD(level of detail)技术是一种常用的提升渲染效率的手段,它的原理是根据物体的包围盒的高度所占当前屏幕的高度百分比来调用不同精度的模型。当一个物体离摄像机很远时,使用精细度低的模型,对象靠近相机时,使用精细度高的模型。缺点是需要准备几种不同精细的模型。
    启用CrossFade时,两个LOD级别的对象会同时渲染出来,然后着色器以某种方法混合,Unity通常使用屏幕空间抖动或混合来实现Cross Fade。
    下面在CustomLit和ShadowCasterPass中添加LOD_FADE_CROSSFADE指令。此指令表示正在过渡的过程中,淡出的过渡因子从1然后减到了0。
    要混合两个LOD级别,可以使用裁剪,要用类似半透明阴影的方法,由于我们需要对表面和阴影进行裁剪,因此定义ClipLOD的裁剪方法,使用
    表面在裁剪空间的位置和过度因子作为参数,利用过渡因子减去抖动值来裁剪,抖动值取决于在什么方向上进行渐变。

  • 反射
    我们要实现镜面反射增加场景真实感,这个特性对于金属物体很重要,我们需要通过调整材质的金属度和光滑度实现反射。

  • 间接BRDF
    我们定义一个IndirectBRDF方法获取BRDF的间接照明,它有四个参数,分别是表面、BRDF、全局照明中获得的漫反射和镜面反射颜色。
float3 IndirectBRDF(Surface s, BRDF b, float3 diffuse, float3 specular);
间接BRDF:现在开始支持镜面反射全局照明。通过全局照明中的镜面反射颜色乘BRDF中的镜面反射颜色得到镜面反射照明,但表面的粗糙度会散射镜面反射,所以最终反射到人眼的镜面反射是减弱的。将镜面反射除以表面粗糙度的平方加1,这对高粗糙度的表面可以使镜面反射强度减半。最后将镜面反射加上漫反射照明得到基于BRDF的间接照明。
    采样环境的CubeMap
    镜面反射反映了环境,默认情况下是天空盒,它是立方体纹理,在GI中声明这个纹理。并定义采样方法。通过SAMPLE_TEXTURECUBE_LOD对立方体纹理进行采样,它需要3D纹理坐标uvw和纹理mipmap等级。uvw可以通过负的视角方向和法线方向得到反射方向,mipmap设置为最高级0也就是对全分辨率的cubemap采样。为GI增加镜面反射属性,通过采样cubemap来获得环境的镜面反射。将正确的镜面反射颜色传递给IndirectBRDF中。在drawSettings中,传入反射探针的标志。使用PerceptualRoughnessToMipmapLevel(PerceptualRoughness)方法为采样方法计算正确的mip等级。
  • 菲涅尔反射
    菲涅尔反射:实时渲染中用菲涅尔反射来根据视角的方向控制反射程度。这里使用了Schlick近似的变种,将表面光滑度和反射率加在一起得到菲涅尔颜色。将法线和视线的点乘的四次方作为菲涅尔强度,将菲涅尔颜色和表面镜面反射颜色根据菲涅尔强度插值,得到表面菲涅尔项亮度。
float fresnel = saturate(surface.smoothness+surface.metallic);float fresnelStrength = Pow4(1.0-saturate(dot(surface.normal, surface.viewDirection)));float3 reflection = specular*lerp(brdf.specular, fresnel, fresnelStrength);

  • 反射探针
    通过GameObject->Light->Reflection Probe创建反射探针,通过Importance和Box Size控制每个探针影响哪个区域。
    反射探针:会动态地产生周围环境的贴图,产生环境映射的效果。通过渲染立方体贴图,获取周围环境,他会渲染六次场景,每个面渲染一次。默认情况下该类型设置为Baked烘焙模式,它会在编辑阶段生成一个储存了探针周围环境景象的立方体纹理,然后对场景中标记为Reflection ProbeStatic的游戏对象进行取景烘焙。烘焙完成后立方体纹理不会发生变换,所以不会受到物体实时位置变化的影响。当然也可以设置为Realtime,在运行时动态更新立方体纹理,但是这样做非常耗费性能。
    解码探针:在UnityPerDraw缓冲区中声明float4 unity_SpecCube0_HDR;来提供解码立方体纹理数据的设置。在GI中调用DecodeHDREnvironment(environment, unity_SpecCube0_HDR)通过原始的环境数据和环境设置作为参数,获得正确的环境反射颜色。
点光源和聚光灯


点光源是一个无限小的点,照亮范围是一个球体,光的亮度随光源距离增大而变小,超过照亮范围时,亮度为0,光源亮度和距离成平方反比关系。
    增加其他光源类型的数据。定义支持非定向光源的最大值。其他光源也需要数量、位置、颜色数据,并在脚本中检测,如果有其他光源,则为GPU发送这些数据。按照方向光的步骤,遍历灯光,保存非方向光的数据,发送至GPU,并在GPU获取,在着色过程中增加非方向光的着色结果。光照随距离衰减:公式,i为光照强度,d为光照距离。这样做理论上光照还是在影响着所有物体,尽管它通常无法明显感知出来。我们要限制光照的最大范围,超过这个范围光照强度设置为0。点光源包围在球中,球体由光源位置和范围决定,且球体边界的光照不应突然消失,而应该通过距离衰减平滑过渡。Unity的URP烘焙系统使用这个公式来定义光照强度距离衰减曲线,r是光照范围。我们在点光源的setup方法中,把光照范围的平方的倒数存储在光源位置的w分量中,以减少着色器的计算量。
  • 聚光灯光源:聚光灯比点光源多了方向属性,方向属性是由localToWorldMatrix第3列取负值取得。
    聚光角度:聚光灯通过聚光角度控制光锥的宽度,还有个单独的内角控制光线何时衰减,URP和lightmapper通过在saturate之前对点积结果缩放和添加,然后对结果平方做到这点,d是点积结果,ri和ro分别是内角和外角弧度。总公式为


  • 烘焙光照和阴影
    将点光源和聚光灯的Mode属性改为Baked/Mixed再进行烘焙即可。但是发现烘焙后光照比较亮,这时因为Unity默认使用了错误的灯光衰减,和旧版的渲染管线相匹配。
    灯光委托。告诉Unity使用不同的衰减,通过在Unity编辑器中执行光照烘焙之前提供一个委托方法。在CustomRenderPipeline类中声明partial void InitializeForEditor() 方法。仅在编辑器下,需要重写lightmapper设置光照数据,通过提供一个委托方法,传入一个Light数组,最后输出一个NativeArray<LightDataGI>,结构委托类型是Lightmapping.RequestLightsDelegate,使用lambda表达式定义这个方法,因为其他地方不需要它。为每个光配置一个LightDataGI结构,添加到输出中。为每个光源类型使用不同的处理代码。默认情况下调用LightDataGI的InitNoBake方法,传入光源的实例ID,表示不烘焙光照。接下来根据不同的光源类型,创建专业的光源结构,调用LightmapperUtils.Extract(Light l, ref Experimental.GlobalIllumination) )方法,参数是光源和光源引用结构,从光源中提取数据到光源引用中。然后调用LightDataGI的Init方法。然后对所有灯光数据的衰减类型设置为FalloffType.InverseSquared。然后让Unity调用这个委托,实现InitializeForEditor()方法,调用Lightmapping.SetDelegate()方法,将委托作为参数,将光源列表转换为传递给烘焙后端的LightDataGI结构列表。当管线被处理时,还需要清理和重置委托,需要重写Dispose(bool disposing)方法,先调用管线基类的Dispose(bool disposing)方法清理,然后调用Lightmapping.ResetDelegate来重置委托。此时,再烘焙一次场景,获得了正确的光照衰减。


  • 阴影蒙版
    每个光源使用一个通道,就像方向光。但由于它们范围有限,所以只要不重叠,就能使多个光源使用同一通道。因此理论上ShadowMask可以支持任意数量的光,但每个纹素最多只能支持四个。如果多个光源发生通道使用冲突,那么不重要的灯光会设置为Baked,直到没有冲突。
    将Mixed Lighting Mode设置为ShadowMask后,同时光源的Mode为Mixed时,也能将阴影烘焙到ShadowMask中。保存非方向光阴影数据时,判断这两项,如果符合就返回阴影强度和Mask通道。保存非方向光的阴影数据,并发送到GPU在GPU接收非方向光的数据,并使用和方向光阴影蒙版一样的方法,计算阴影蒙版的阴影衰减。综合阴影衰减和非方向光的距离衰减,最终得到光线的衰减。


  • 逐对象光源
    目前所有可见光都会渲染对象的每一个片元,然而非定向光源只会影响物体表面的一小部分片元,因此会有计算浪费,为了支持更多光源,我们需要减少每个片元的评估光源数量,那就是使用Unity的逐对象光源索引。
    Unity会确定哪些灯光会影响哪些对象,并将此信息发送给GPU。在渲染每个对象时,只评估相关灯光而忽略其他对象。因此灯光时根据每个对象而不是片元确定的。每个对象会受到多少个灯光影响是有限制的,因此大对象会更容易缺少照明。
    将逐对象光源配置为管线的可配置选项。在CameraRenderer中判断逐对象光源的配置,为DrawSettings中添加逐对象光源。清除光源索引。Unity会为每个对象创建一个活跃光源列表,列表中包含了场景内所有存在的光源(无论是否可见),并且包含方向光,然后根据光源的重要性进行排序。我们要清理这些光源列表,只保留可见的非方向光索引。在设置光线时,如果使用逐对象光源,则从CullingResults中拿到活跃的光源索引列表。在循环可见光时,对于点光源和聚光灯,保存它们在非方向光中的索引,并传入到光源索引列表中,方向光的索引设置为-1。消除所有不可见光的索引,然后使用cullingResults将光源索引列表发送回Unity,调用光源索引变量的Dispose方法释放。最后添加是否启用逐对象光源功能的关键字,启用逐对象光源关键字。使用光源索引。在shader中声明关键字。在UnityPerDraw缓冲区中声明2个相关属性,real4 unity_LightData和real4 unity_LightIndices[2]。其中前者的Y分量包含了灯光数量,后者两个分量都包含光源索引,所以每个对象最多支持8个光源逐对象索引。在着色函数中,遍历非定向光源时,判断逐对象光源关键字,遍历逐对象光源灯光数量,并获取光源id,计算光照计算着色。
点光源和聚光灯阴影


依然使用阴影图集的方式支持点光源和聚光灯阴影,使用方法和方向光大致相同。
    聚光灯阴影的阴影混合
    获得非定向光的阴影衰减。判断非定向光的阴影强度和全局阴影强度是否不为正,则只考虑阴影蒙版的情况。否则则计算实时光照下非定向光的阴影衰减。将阴影的距离过渡发送给GPU,阴影的距离过渡数据应该是被所有光源所用的,不应该在方向光的处理中才会发送给GPU,因为如果出现没有方向光只有非方向光的情况,阴影的距离过渡数据就不会发送给GPU了。修改全局阴影数据的阴影强度赋值方式。原来是:当遍历到超出级联数量个级联等级时,标记为超出阴影范围,将阴影强度设为0。现在是:当阴影级联等级大于0(表示有方向光),且级联等级超过最大级联等级时,才将阴影强度设为0。保证了在没有方向光的极端情况下,全局阴影强度不会被设为0。

    非定向光的实时阴影。
    为非方向光设置单独的阴影图集,记录最大的非方向光数量和已投影的非方向光数量。
  • 只会为可见光列表中的光源渲染阴影图集。同时只会为开启了阴影的光源渲染阴影。
    也就是说,可见光都会参与光照计算(不管是光照贴图、实时光照、逐对象光源),但是可见光&&开启阴影才能渲染阴影(不管是实时阴影、阴影蒙版)非定向光的阴影图集。单独在管线配置中设置非定向光阴影图集,不需要设置级联,只需要考虑图集大小和滤波模式。设置非定向光的PCF等级关键字。设置非定向光阴影图集ID和阴影转换矩阵。将非定向光阴影图集大小和定向光阴影图集大小一起发送到GPU。



  • 渲染聚光灯阴影
    聚光灯也会受到阴影渗漏的影响,由于透视投影的纹素大小是不固定的,所以阴影渗漏也是不固定的,离光源越远渗漏越大。
    纹素大小随着与灯光平面的距离呈线性增加,灯光平面将世界分散在光线的前面或后面,因此我们可以计算纹素大小,从而计算距离1处的法线偏差,并发送到着色器,在哪里将其缩放到适当的大小。世界空间中,距离光平面为1的阴影图块大小是聚光弧度半角的切线的两倍。
    距离为1时的世界空间纹素大小等于2除以投影比例,为此我们使用其矩阵的左上角值,可以像方向光一样用它来计算法线偏差,不同之处在于因为没有多个级联,我们可以立即将光的法线偏移考虑进去。
    我们对方向光的阴影配置了级联包围球,确保不会在适合的阴影图块之外采样,但不能对非定向光阴影使用相同的方法。聚光灯的阴影图块与圆锥体紧密相连,因此法线偏差和滤波尺寸会将采样推到圆锥体边界或之外,导致边缘附近有错误的图块产生的阴影。最简单的方法是手动clamp采样让其限制在图块范围内,就像每个图块都是各自单独的纹理一样。这依然会拉伸边缘附近的阴影,但不会引入无效阴影。

  • 渲染点光源的阴影
    采样点光源的阴影:
    与聚光灯类似,但不同的是,点光源不限于圆锥体。点光源生成的阴影,其阴影深度贴图存储在一个立方体纹理中,然后单独渲染立方体的六个面阴影。可以将点光源是为6个灯光,它会占据阴影图集的6个图块,这意味着我们目前可以支持最多两个点光源的实时阴影,它会占据最大值16个图块中的12个,如果少于6个图块空间,则点光源是无法实时渲染阴影的。
    采样阴影:点光源的阴影贴图储存在一个正方体纹理中,然后在shader中进行采样,但是现在我们将其6个面的纹理分别存储在图集中,因此我们无法使用标准的立方体纹理采样获取阴影数据。我们要自己确定合适的面来进行采样,所以需要知道是否在处理点光源,以及物体表面到光源的方向
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 17:20 , Processed in 0.092109 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表