200+篇教程总入口,欢迎收藏:
放牛的星星:[教程汇总+持续更新]Unity从入门到入坟——收藏这一篇就够了zhuanlan.zhihu.com本文重点内容:
1、支持更多类型的灯光
2、包含实时的点光源和聚光灯
3、为点光源和聚光灯烘焙阴影
4、每个物体限制最多8个其他光源
这是有关创建自定义脚本渲染管道的系列教程的第九部分。它增加了对点光源和聚光灯的实时和烘焙支持,但还没有实时阴影。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。
本教程使用Unity 2019.2.21f1制作。
1 点光源
到目前为止,我们仅使用了定向光,因为这些光会影响所有事物并且具有无限范围。而其他光源类型则不同,不会假定它们无限远,因此它们具有位置并且强度会变化。这需要额外的工作来设置和呈现,这就是我们为此创建单独代码的原因。我们从点光源开始,点光源是无限小的点,可以均匀地向所有方向发光。
1.1 其他的灯光数据
与定向灯一样,我们只能支持有限数量的其他灯光。场景通常包含很多不定向的灯光,因为它们的有效范围有限。通常,对于任何给定的帧,所有其他光的子集都是可见的。因此,我们可以支持的最大值适用于单个帧,而不适用于整个场景。如果最终我们看到的可见光比最大数量更多,则将被忽略掉。Unity会根据重要性对可见光列表进行排序,因此只要可见光不发生变化,哪些灯被忽略就是一致的。但是,如果确实发生变化(由于相机移动或其他更改),则可能会导致明显的光过爆的情况。因此,我们不能使用太低的最大值。现在,让我们同时允许多达64个的其他光源,设置为Lighting中的另一个常量。
就像方向光一样,我们需要为其他类型的光发送光的数量和光颜色到GPU。而同时,我们还需要发送光的位置。添加着色器属性名称和向量数组字段来实现。
在SetupLights中,追踪其他光数量以及定向光数量。遍历可见光后,将所有数据发送到GPU。但是,如果我们最终得到零个其他光源,则无需发送数组。而且,现在只包含其他光源而没有定向光源也很有意义,因此我们也可以跳过发送定向光数组的操作。但不管是不是有光源,我们总是需要将光源数发送出去。
在着色器这边,定义另一个最大光照值和新的光照数据。
然后定义一个GetOtherLightCount函数,稍后我们将使用它。
1.2 设置点光源
在Lighting中创建一个SetupPointLight方法来设置点光源的颜色和位置。给它与SetupDirectionalLight相同的参数。颜色的设置也是相同的。位置的工作原理类似于方向光的方向,但我们需要本地到世界矩阵的最后一列而不是第三列。
现在,我们还需要调整SetupLights中的循环,以便区分方向光和点光源。一旦达到最大数量的方向光,我们不是像以前一样再结束循环。相反,我们会跳过方向光继续循环。对点光源执行相同的操作,同时要考虑其他光源的最大值。让我们使用switch语句对此进行编程。
1.3 着色
现在,着色器可以使用支持点光源所需的所有数据。要使用它,我们向Light添加一个GetOtherLight函数,其参数与GetDirectionalLight相同。这时,光线的方向会随每个片元而变化。我们通过将从表面位置到光线的光线归一化来找到它。因为目前不支持阴影,因此衰减为1。
要应用新的灯光,请在GetLighting中为方向光添加一个循环,然后为所有其他光添加一个循环。尽管循环是分开的,但我们需要为其迭代器变量使用不同的名称,否则在某些情况下,我们将获得着色器的编译器警告。所以我用j代替i作为第二个。
1.4 距离衰减
我们的点光源现在可以工作了,但是它们太亮了。现实中,随着光远离其源传播之后,它会散布开来,变得越来越不集中,因此光越远,亮度就越低。光线的强度是其中i 为配置强度,d 为距离。
这被称为平方反比定律。请注意,这意味着在小于1的距离处,强度会变为大于配置的强度。也就是说非常接近灯光的位置变得非常明亮。早先我们推断,最终使用的光色代表的是从正面照亮的完美白色漫射表面碎片反射时观察到的光量。对于方向光来说确实如此,但对于其他类型的光,它也专门用于与光之间距离为1的片元。
通过计算光的平方来应用距离衰减,并将其倒数用作衰减。为防止潜在的除0操作,请将平方距离的最小值设置为很小的正值。
1.5 光范围
尽管点光源强度现在会迅速衰减,但理论上它们的光仍然会影响所有对象,只是正常时候无法感知。漫反射很快变得不明显,而镜面反射在更远的距离仍然可见。
为了使渲染更真实,我们将使用最大照明范围参数,超过此范围我们将使照明强度强制为零。这是不符合现实的,但是这样设定之后,所有灯光无论距离多远都总是可视为可见。在增加范围的情况下,点光源包含在边界球中,边界球由其位置和范围定义。
我们不会突然切断球体边界处的光,而是通过应用距离衰减来平滑地将其淡出。Unity的Universal RP和lightmapper的使用
公式,r是光的范围,所以我们也会使用相同的函数。
我们可以将范围存储在Light Position的第四个分量位置。以减少着色器的工作量。直接存储,同样要避免除0操作。
然后在GetOtherLight中考虑距离衰减。
2 聚光灯
现在,我们来支持聚光灯。点光和聚光灯之间的区别在于,聚光灯的光被限制为圆锥形。实际上,它是一个点光源,该点光源被一个有孔的封闭球包围。孔的大小决定了光锥的大小。
2.1 方向
聚光灯具有方向和位置,因此向Lighting添加着色器属性名称和其他光源方向的数组。
在SetupLights中将新数据发送给GPU。
创建一个SetupSpotLight方法,该方法是SetupPointLight的副本,但它也存储光的方向。我们可以使用本地到世界矩阵的第三列的求反,类似于定向光。
然后在SetupLights循环中包括一个聚光灯的Case。
在着色器端,将新数据添加到Light中的缓冲区。
并在GetOtherLight中应用spot衰减。我们从简单地使用spot和光方向的饱和点积开始。这将使光衰减,使其在90°的光点角处达到零,然后照亮所有在灯光前方的物体。
2.2 角度
聚光灯有一个角度来控制其光锥的宽度。该角度是从中间开始测量的,因此90°的角度看起来像我们现在的角度。除此之外,还有一个单独的内角控制光何时开始衰减。Universal RP和lightmapper通过在饱和之前缩放并在点积中添加一些东西来完成此操作,然后对结果求平方。公式大概如下:
,其中d是点乘。
并且
则分别为它们的内角和外角弧度。
该函数也可以写成
,但是通过上面的方式分解,我们可以计算Lighting中的a和b,并通过一个新的点角度数组将它们发送到着色器中。定义数组及其属性名。
在SetupLights中将数组复制到GPU。
然后在SetupSpotLight中计算值,并将它们存储在spot angles数组的X和Y分量中。通过VisibleLight结构的spotAngle属性可以使用外角。但是,对于内角,我们首先需要通过其light属性检索Light游戏对象,该对象又具有innerSpotAngle属性。
为什么内角不存储在VisibleLight中?
可配置的内角是Unity的新增功能。VisibleLight结构可能没有它,因为它会更改其大小并需要重构Unity内部代码。
回到着色器,在Light中添加新的数组。
并在GetOtherLight中调整spot衰减。
最后,为确保点光源不受角度衰减计算的影响,请将其点角值设置为0和1。
2.3 配置内角角度
聚光灯始终具有可配置的外角,但是在引入Universal RP之前,不存在单独的内角。结果,默认的灯光检查器不会暴露内角参数。RP可以进一步修改灯光,因此可以覆盖灯光的默认检查器。这是通过创建扩展LightEditor的编辑器脚本并将其赋予CustomEditorForRenderPipeline属性来完成的。此属性的第一个参数必须是Light类型。第二个参数必须是我们要覆盖检查器的RP资产的类型。让我们创建一个这样的脚本,将其命名为CustomLightEditor,并将其放在Custom RP / Editor文件夹中。还给它提供CanEditMultipleObjects,以便它与选定的多个光源一起使用。
要替换检查器,我们需要重写OnInspectorGUI方法。但是我们将做最少的工作以暴露内角,因此我们首先调用base方法以正常绘制默认检查器。
之后,我们检查是否仅选择了聚光灯。可以通过一个方便的名为settings的子类属性来做到这一点,该属性提供对编辑器选择的序列化属性的访问。用它来检查我们没有多种不同的光源类型,并且类型是LightType.Spot。如果是的话,在设置上调用DrawInnerAndOuterSpotAngle以在默认检查器下方添加一个inner-outer spot angle滑块。然后,调用ApplyModifiedProperties以应用对该滑块所做的任何更改。
3 烘焙光和阴影
在本教程中,我们不会涵盖点光源和聚光灯的实时阴影,但是现在我们先支持烘焙这些光源类型。
3.1 全烘焙
完全烘焙点和聚光灯只需将其Mode设置为Baked即可。请注意,默认情况下,它们的Shadow Type设置为None,因此,如果要将它们与阴影一起烘焙,请将其更改为其他内容。
尽管现在已经足以烘焙这些光源,但事实证明它们在烘焙后太亮了。发生这种情况的原因是,默认情况下Unity使用不正确的光衰减,与传统RP的结果相匹配。
3.2 灯光代理
通过提供一个方法的委托,可以告诉Unity使用不同的衰减,该方法应在Unity在编辑器中执行光照映射之前被调用。为此,请将CustomRenderPipeline转换为局部类,并在其构造函数的末尾调用当前不存在的InitializeForEditor方法。
然后为它创建另一个特定于编辑器的局部类(就像CameraRenderer一样),该类为新方法定义了一个默认的模板。除了UnityEngine命名空间外,我们还需要使用Unity.Collections和UnityEngine.Experimental.GlobalIllumination。这将导致LightType发生类型冲突,因此请为其明确使用UnityEngine.LightType。
针对编辑器,我们需要重写光照贴图器以解决如何设置其光照数据。通过为它提供方法的委托来完成,该方法将数据从输入Light数组传输到NativeArray
我们还需要为每个光源配置一个LightDataGI结构,并将其添加到output中。我们需要为每种光源类型使用特殊的代码,因此需要在循环中使用switch语句。默认情况下,我们在灯光数据上调用带有灯光实例ID的InitNoBake,这指示Unity不烘焙灯光。
接下来,对于每种受支持的光源类型,我们需要构造一个专用的light结构,调用LightmapperUtils.Extract,以light和对该结构的引用作为参数,然后在光源数据上调用Init,并通过引用传递该结构。对方向光,点光源,聚光灯和区域光执行此操作。
因为我们尚不支持实时区域光,因此,如果存在,请强制将其light模式设置为烘焙。
现在只是我们必须包含的模板代码。所有这些的重点是,我们现在可以对所有的灯光设置光数据的falloff类型为 FalloffType.InverseSquared。
要让Unity调用我们的代码,请创建一个InitializeForEditor的编辑器版本,该编辑器以我们的委托作为参数来调用Lightmapping.SetDelegate。
当我们的管道被处理时,我们还需要清理并重置委托。这是通过重写Dispose方法,让其调用其基本实现以及Lightmapping.ResetDelegate来完成的。
不幸的是,Unity 2019.2光照贴图器不支持聚光灯的自定义内衰减角度。可以设置内spot角度,但它会被忽略。
光照贴图程序可以在更高版本的Unity中使用内Spot角度吗?
是的,从Unity 2019.3开始,AngularFalloffType存在,你可以执行以下操作:
3.3 阴影遮罩
通过将点光源和聚光灯的Mode设置为Mixed,也可以将它们的阴影烘焙到Mask中。就像方向光一样,每个光都有一个通道。但是因为它们的范围有限,所以只要它们不重叠,就有可能多个光源使用相同的通道。因此,Mask可以支持任意数量的光,但每个纹理像素最多只能支持四个。如果在尝试声明同一通道时多个光最终重叠,则最不重要的光将被强制为Baked模式,直到不再有冲突为止。
要将阴影遮罩用于点光源和聚光灯,请向Shadows添加ReserveOtherShadows方法。它的工作方式与ReserveDirectionalShadows相似,只是我们只关心阴影遮罩的模式,只需要配置阴影强度和Mask通道。
将阴影数据的着色器属性名称和数组添加到Lighting。
在SetupLights中将它发送给GPU。
并在SetupPointLight和SetupSpotLight中配置数据。
在着色器端,向阴影添加一个OtherShadowData结构和GetOtherShadowAttenuation函数。再次,我们使用与定向阴影相同的方法,只是我们只有强度和遮罩通道。如果强度为正,则我们总是调用GetBakedShadow,否则没有阴影。
在Light中,添加阴影数据并将其分解为GetOtherLight中的衰减。
4 逐物体的光源
当前,将对每个渲染的片元评估所有可见光。这对于方向光源很好,但是对于超出片元范围的其他光源则是不必要的工作。通常,每个点光或聚光灯只会影响所有片元的一小部分,因此,许多工作都是徒劳无功的,这可能会严重影响性能。为了支持许多性能良好的灯光,我们需要以某种方式减少每个片元评估的灯光数量。为此,有多种方法,其中最简单的方法是使用Unity的per-object光照索引。
这个想法是由Unity确定哪些灯光会影响哪些对象并将此信息发送到GPU。然后,我们可以在渲染每个对象时仅评估相关的灯光,而忽略其余的灯光。因此,灯光是基于每个对象而不是每个片元确定的。通常,这对于小型物体而言效果很好,但对于大型物体而言并不理想,因为如果光线仅影响物体的一小部分,则仍然需要对其整个表面进行评估。另外,可以影响每个物体的光线数量是有限制的,因此大型物体更容易缺少灯光。
由于per-object的光线指标不是理想的,可能会错过一些灯光,因此我们将其设为可选。这样,还可以轻松比较视觉效果和性能。
Unity的基于对象的灯光索引代码是不是中断过很多次?
是的,自Unity 2018以来,它已经被中断过了好几次,有时几个月了,它导致了很多错误。这是使其成为可选的另一个原因。
4.1 逐物体的灯光数据
向CameraRenderer.DrawVisibleGeometry添加一个布尔参数,以指示是否应使用lights-per-object模式。如果是,请为图形设置的每个对象数据启用PerObjectData.LightData和PerObjectData.LightIndices标志。
必须将相同的参数添加到Render,以便可以将其传递到DrawVisibleGeometry。
而且,我们还需要追踪和传递CustomRenderPipeline中的模式,就像其他布尔选项一样。
最后,将切换选项添加到CustomRenderPipelineAsset。
4.2 过滤灯光索引
Unity只是创建每个对象所有活动光源的列表,并按其重要性大致排序。此列表包括所有灯光,无论它们是否可见,当然包含方向灯光。我们需要清理这些列表,以便仅保留可见的非方向光的索引。我们在Lighting.SetupLights中执行此操作,因此向该方法中添加一个lights-per-object参数,并向Lighting.Setup添加该参数。
然后在camerarder . render中添加模式作为设置参数。
在Lighting.SetupLights中,在循环到可见光之前,请从剔除结果中检索光索引图。这是通过使用Allocator.Temp作为参数调用GetLightIndexMap来完成的,这为我们提供了一个临时的NativeArray
我们仅在使用lights per object时才需要检索此数据。由于NativeArray是一个结构,因此我们将其初始化为默认值,否则它将不分配任何内容。
我们只需要包含的点光源和聚光灯的索引,应该跳过所有其他类型的光源。通过将所有其他灯光的索引设置为-1来传达给Unity。我们还需要更改其余灯光的索引以匹配我们的索引。仅在我们检索Map时设置新索引。
我们还需要消除所有不可见光的索引。如果我们使用lights per object,请执行第二个循环,该循环在第一个循环之后继续进行。
完成后,我们必须通过在剔除结果上调用SetLightIndexMap将调整后的索引Map发送回Unity。此后不再需要IndexMap,因此我们应该通过在其上调用Dispose来对其进行释放。
最后,当使用lights per object时,我们将使用不同的着色器变体。通过适当地启用或禁用_LIGHTS_PER_OBJECT着色器关键字来决定。
4.3 使用索引
要使用灯光索引,请将相关的多编译编译指示添加到我们的Lit着色器的CustomLit的Pass中。
所需数据是UnityPerDraw缓冲区的一部分,由必须在unity_WorldTransformParams之后直接定义的两个real4值组成。首先是unity_LightData,它包含其Y分量中的灯光量。之后是unity_LightIndices,它是长度为2的数组。两个向量的每个通道都包含一个光索引,因此每个对象最多支持八个。
如果定义了_LIGHTS_PER_OBJECT,则对GetLighting中的其他灯光使用替代循环。在这种情况下,可以通过unity_LightData.y找到灯光量,并且必须从unity_LightIndices的适当元素和组件中检索灯光索引。可以通过将迭代器除以4并通过取模4得到正确的分量来获得正确的向量。
但是,尽管最多只有8个光索引可用,但是提供的光计数并未考虑此限制。因此,我们必须将循环明确地限制为八个迭代。
是否有缓冲方法不限于每个对象八个灯?
曾经有,但是该代码自Unity 2018.3起已被禁用,并且已从Universal RP中部分删除。死代码已经有一年多了,所以我不会再依赖它了。
请注意,启用Lights-per-object后,GPU实例化效率较低,因为灯光计数和索引列表匹配的对象才会分组。SRP批处理程序不受影响,因为每个对象仍然获得自己的优化后的DrawCall。
下一章,点光和聚光灯阴影。
本文翻译自 Jasper Flick的系列教程