Unity可编程渲染管线系列(八)全局光照(直接光照)
目录 |
1 灯光贴图 1.1 设置场景 1.2 烘焙光 1.3 采样光照贴图 1.4 透明表面 1.5 合并直接光和间接光 1.6 只有烘焙光 1.7 元通道 2 自发光 2.1 自发光颜色 2.2 间接自发光 3 光探针 3.1 采样探针 3.2 放置光探针 3.3 光探针代理体积 4 实时全局光照 4.1 渲染实时全局光照 4.2 采样动态光照贴图 4.3 实时间接自发光 4.4 透明度 5 点和聚光灯 5.1 实时 全局光照 5.2 烘焙全局光照 |
本文重点:
1、烘焙和采样光照贴图
2、展示间接光
3、创建自发光材质
4、通过探针和LVVPs采样灯光
5、支持预计算的实时全局光照
这是涵盖Unity的可编写脚本的渲染管线的教程系列的第八部分。关于支持静态和动态全局照明。
本教程是CatLikeCoding系列的一部分,原文地址见文章底部。“原创”标识意为原创翻译而非原创教程。
本教程使用Unity 2018.3.0f2制作。
(光线在角落和物体之外找到了出路)
修正:
如果你之前照着教程做了以前的教程,有两个错误必须修复。首先,仅渲染级联阴影时,还必须设置正方形阴影距离。
其次,环境探针混合插值器包含错误的代码。
1 灯光贴图
实时照明仅处理直射光。只有直接暴露在光线下的表面才被其照亮。缺少的是间接光,它是由光从一个表面传播到另一个表面并最终到达相机而引起的。这也称为全局照明。我们可以通过将其烘焙到光照贴图中来在Unity中添加它。Rendering 16,Static Lighting教程涵盖了Unity中烘焙光的基础知识,但仅适用于使用Enlighten光照贴图器的旧管线。
1.1 设置场景
最容易看到的是,场景中只有一个定向光,而没有间接光。所有阴影区域将在黑色附近。
(只有实时光的场景)
一些非常大的阴影使这一点更加明显。
(大阴影)
我们仍然可以看到阴影内的对象,因为镜面环境反射已添加到直接照明中。如果没有使用反射探头,那么我们将看到反射的天空盒是明亮的。通过将天空盒的强度系数(Intensity Multiplier)降低到零来消除天空盒的影响。这将使所有阴影区域都完全变黑。
(黑色的环境)
1.2 烘焙光
通过在场景照明设置中的“Mixed Lighting”下启用“Baked Global Illumination”,并将其“Lighting Mode”选择为“Baked Indirect”,可以完成烘焙间接照明。尽管我们还看不到它,但这将使Unity烘烤照明。
(烘焙间接光)
我将使用默认的Lightmapping设置,并进行一些更改。默认设置是使用渐进式光照贴图器,我将继续使用。因为我的场景很小,所以将 Lightmap Resolution 从10增加到20。禁用 Compress Lightmaps 以获得最佳质量,跳过贴图压缩步骤。另外,将“Directional Mode”更改为“Non-Directional”,因为这仅在使用法线贴图时才有意义,而我们没有。
(光照贴图设置)
烘焙的灯光是静态的,因此在运行模式下无法更改。只有标记为静态光照贴图的游戏对象才会烘焙其间接光照贡献。将所有几何标记为完全静态是最快的。
(静态物体)
烘焙时,Unity可能会报错说UV重叠。当对象的UV展开在光照贴图中变得太小而导致光信息重叠时,可能会发生这种情况。你可以通过在光照贴图中调整对象的比例来调整和缩放对象的比例。另外,对于诸如默认球体之类的对象,启用“Stitch Seams”会改善烘焙的光。
(Scale in lightmap 和 seam stitching)
最后,要烘焙主光的贡献,请将其模式设置为“Mixed”。这意味着它将用于实时照明,而其间接照明也将被烘焙。
烘焙完成后,你可以通过“ Lighting ”窗口的“ Baked Lightmaps”选项卡检查贴图。最终可以得到多个贴图,具体取决于贴图的大小以及烘焙所有静态几何体需要多少空间。
(两张光照贴图)
1.3 采样光照贴图
要对光照贴图进行采样,我们需要指示Unity使这些贴图可用于我们的着色器,并将光照贴图的UV坐标包括在顶点数据中。这是通过在MyPipeline.Render中启用RendererConfiguration.PerObjectLightmaps标志来完成的,就像我们启用反射探针一样。
现在,当渲染带有光照贴图的对象时,Unity将提供所需的数据,还将为LIGHTMAP_ON关键字选择一个着色器变体。因此,我们必须为其着色器添加一个多编译指令。
可通过unity_Lightmap及其随附的采样器状态使用光照贴图,因此请将其添加到Lit.hlsl。
光照贴图坐标是通过第二个UV通道提供的,因此请添加到VertexInput。
我们也必须将它们添加到VertexOutput中,但这仅在使用光照贴图时才需要。
光照贴图也具有比例和偏移量,但是它们并不完全适用于贴图。取而代之的是,它们被用来告诉物体的UV展开在光照贴图中的位置。作为UnityPerDraw缓冲区的一部分,它定义为unity_LightmapST。因为它与TRANSFORM_TEX期望的命名约定不匹配,所以如果需要,我们必须在LitPassVertex中自己转换坐标。
可以对使用光照贴图的对象进行实例化吗?
每次绘制都会设置unity_LightmapST,但是如果启用了实例化,则当我们包含UnityInstancing时,它会被宏定义所取代。因此,GPU实例化可与光照贴图一起使用,但仅适用于最终从同一光照贴图采样的对象。
请注意,静态批处理将替代实例化,但仅在播放模式下有效。当对象标记为静态批处理并且在播放器设置中启用了静态批处理时,会发生这种情况。
让我们创建一个单独的SampleLightmap函数,在给定一些UV坐标的情况下对光照图进行采样。在其中,我们会将调用转发到Core EntityLighting文件中定义的SampleSingleLightmap函数。我们必须为其提供贴图,采样器状态和坐标。前两个需要通过TEXTURE2D_PARAM宏传递。
不应该是TEXTURE2D_ARGS吗?
这会更好,但是现在我们在Unity 2018.3中使用的实验版本中,才以相反的方式定义了宏。它们已在未来的版本中切换了。
SampleSingleLightmap需要更多参数。接下来是UV坐标的比例偏移转换。但是我们已经在顶点程序中做到了,所以在这里我们将提供身份转换。
然后是一个布尔值,指示是否需要对光照图中的数据进行解码。这取决于目标平台。如果Unity使用完整的HDR光照贴图,则没有必要,在定义UNITY_LIGHTMAP_FULL_HDR时就是这种情况。
最后,我们需要提供解码指令,以将照明带入正确的范围。为此,我们需要使用float4(LIGHTMAP_HDR_MULTIPLIER,LIGHTMAP_HDR_EXPONENT,0.0,0.0)。
对光照贴图进行采样,因为我们想添加全局照明。因此,让我们为此创建一个GlobalIllumination函数,该函数负责细节。给它一个VertexOutput参数,这意味着它需要在该结构之后定义。如果有光照贴图,请对其进行采样,否则返回零。
在LitPassFragment的末尾调用此函数,首先替换所有其他照明,以便我们可以单独查看它。
(只有全局光照)
1.4 透明表面
结果看起来应该看起来很柔和,但是在透明表面附近可能会出现不连续现象,尤其是对于淡入淡出材质。渐进式光照贴图器使用材质的渲染队列来检测透明度,并依赖_Cutoff着色器属性来裁剪材质。这样可以有效果,但背面暴露却有麻烦。当正面和背面重叠时,双面几何也会造成麻烦,我们自己生成的双面几何就是这种情况。
(透明失真)
问题在于,光照贴图仅适用于正面。背面不能包含数据。渲染背面是可行的,但最终它们将使用正面的光数据。出现失真的原因是,采样时光照贴图器会击中背面,从而不会产生有效的光照信息。你可以通过将自定义的Lightmap Parameters 分配给以失真结尾的对象,并降低“Backface Tolerance”阈值来减轻此问题,从而使光照贴图器接受更多丢失的数据并将其平滑。
(耐光贴图)
1.5 合并直接光和间接光
现在我们知道全局照明有效,将其添加到直接光中。由于间接光仅是漫反射的,因此请将其与表面的漫反射属性相乘。
(直接光的全局光照)
和不带全局照明相比,结果没有达到预期的亮度。但是,现在的场景比以前明亮得多。那是因为天空盒是全局照明的因素。我们只添加单向光的间接光,以便通过将环境光的强度降低到零来更好地检查它。
(黑色的环境)
1.6 只有烘焙光
也可以将我们的灯光模式设置为“Baked”。这意味着它不再是实时照明。而是将其直接和间接光都烘焙到光照图中。在我们的情况下,我们最终得到一个没有任何实时照明的场景。它还消除了所有镜面照明并柔化了阴影。
(完全使用烘焙光)
1.7 元通道
要烘焙光线,光照贴图器必须知道场景中对象的表面属性。它通过使用特殊的meta pass渲染它们来检索它们。我们的着色器没有这样的通道,而Unity使用了默认的meta传递。但是,默认设置并不适用于我们的着色器。因此,我们将创建自己的。在其光模式设置为Meta的情况下添加通道,而无需剔除,并将其代码保存在单独的Meta.hlsl文件中。
Meta.hlsl可以作为Lit.hlsl的修剪版本开始。我们只需要unity_MatrixVP矩阵,unity_LightmapST,主纹理和非实例材质属性。没有对象到世界的转换,因此直接从对象空间转到剪辑空间。最初,使片段程序返回零。
就像在采样光照贴图时一样,在渲染光照数据时,unity_LightmapST用于获取贴图的正确区域。在这种情况下,我们必须调整输入位置的XY坐标。另外,还使用了一个技巧来使OpenGL渲染正常工作,因为显然在未调整Z位置时它会失败。
我们将需要初始化光照表面,但是我们只有颜色,金属和光滑度信息。向Lighting.hlsl添加一个便捷的GetLitSurfaceMeta函数,该函数将所有其他值设置为零。
在元片段程序中检索表面数据。
现在,我们可以适当的访问表面反照率,必须将其在RGB通道中输出,而A通道设置为1。但是,可以通过unity_OneOverOutputBoost提供的指数以及定义最大亮度的unity_MaxOutputValue来调整其强度。通过PositivePow函数应用它以获得最终颜色,并将其钳位在零和最大值之间。
现在,我们输出用于光照贴图的反照率,但是meta pass也用于生成其他数据。通过unity_MetaFragmentControl中定义的布尔标志可以知道请求的数据。如果设置了它的第一个组件,那么我们应该输出反照率。否则,我们将输出零。
现在,我们得到的结果与默认的元传递相同。但是,默认的元通道还会将镜面反射颜色的一半乘以粗糙度。其背后的想法是高度镜面但粗糙的材质也会通过一些间接光。默认阴影可以做到这一点,但是希望平滑度值存储在_Smoothness之外的其他位置。因此,我们必须自己做。
观察差异的最佳方法是使用平滑度为零的白色金属球体,然后仅渲染间接光。
(没有和有元通道)
2 自发光
除了反射或吸收然后再发光之外,对象还可以自行发光。这就是真实光源的工作方式,但是在渲染时并未考虑在内。要创建发光材质,只需将颜色添加到计算的照明中即可。
2.1 自发光颜色
向我们的着色器添加_EmissionColor属性,默认情况下设置为黑色。由于发出的光可能具有任意强度,因此可以通过将HDR属性应用到颜色来将其标记为高动态范围。
(自发光颜色)
将发射颜色添加到InstancedMaterialProperties中。在这种情况下,通过应用ColorUsage属性并将其作为第二个参数,将其标记为HDR。它的第一个参数指示是否应显示Alpha通道,此处不是这种情况。
(逐物体 自发光颜色)
将发射颜色作为另一个实例属性添加到Lit.hlsl。然后将其添加到LitPassFragment末尾的片段颜色。
(直接自发光,一些白色、绿色和红色)
2.2 间接自发光
发射颜色使对象自身的表面变亮,但不会影响其他表面,因为它不是灯光。我们能做的最好的事情就是在渲染光照贴图时将其考虑在内,从而有效地将其转化为烘焙的光照。
光照贴图器还使用meta传递来收集从表面发出的光。在这种情况下,将设置unity_MetaFragmentControl的第二个组件标志。在这种情况下,输出发光颜色,并将alpha设置为1。
这还不足以使发射光影响其他表面。默认情况下,光照贴图器不收集来自对象的发射光,因为它需要做更多的工作。必须为每种材质启用它。为了实现这一点,我们将通过在LitShaderGUI.OnGUI的编辑器上调用LightmapEmissionPropertry,将全局照明属性添加到着色器GUI中。让我们将其放在用于阴影投射的切换开关下方。
(烘焙自发光)
仅将其设置为Baked是不够的,因为Unity使用了另一种优化方法。如果材质的发射最终变成黑色,则也会被跳过。这是通过设置材质的globalIlluminationFlags的MaterialGlobalIlluminationFlags.EmissiveIsBlack标志来指示的。但是,此标志不会自动调整。我们必须自己做。
当全局照明属性更改时,我们将简单地删除该标志,而不是清除。这意味着,所有使用设置为烘焙全局照明的材质的对象都将发出发射光。因此,我们仅应在需要时使用此类材质。
(烘焙间接自发光)
3 光探针
光照贴图只能与静态几何体结合使用。它们不能用于动态对象,也不适用于许多小对象。但是,将光照贴图对象和非光照贴图对象组合在一起并不能很好地工作,因为差异在视觉上是显而易见的。为了说明这一点,我使所有非发光的白色球体成为动态的。
(除了自发光的白色球 其他是动态的)
将灯设置为完全烘焙时,差异会更大。在这种情况下,动态对象根本不会获得任何照明,而是全黑的。
(全烘焙)
当无法使用光照贴图时,我们可以依靠光探头。光探头是特定点的照明样本,编码为球谐函数。“渲染5,多重光源”中说明了球谐函数的工作原理。
3.1 采样探针
就像光照贴图数据一样,必须将光照探针信息传递给着色器。在这种情况下,我们必须使用RendererConfiguration.PerObjectLightProbe标志启用它。
着色器中的球谐函数系数通过UnityPerDraw缓冲区中的七个float4向量提供。创建一个具有法线向量参数的SampleLightProbes函数,该函数将系数放入数组中,并将其与法线一起传递给也在EntityLighting中定义的SampleSH9函数。返回结果之前,请确保结果不是负数。
可以实例化使用光探针的对象吗?
与光照贴图一样,UnityInstancing将覆盖系数,因此在合适的情况下,实例化可以正常工作。为此,请确保在包含UnityInstancing之后定义了SampleLightProbes函数。
如果不使用光照贴图,则将表面的参数添加到GlobalIllumination函数,并使其返回SampleLightProbes的结果,而不是零。
然后在LitPassFragment中添加所需的参数。
3.2 放置光探针
现在,动态对象使用光照探针,但是目前仅将环境光照存储在其中,我们将其设置为黑色。要通过光探测器提供烘焙的光,我们必须通过GameObject/ Light / Light Probe Group将光探测器组添加到场景中。这将创建一个包含八个探针的组,你需要对其进行编辑以适合场景,如渲染16,静态照明中所述。
(光探针组)
添加光探针组后,动态对象将拾取间接照明。Unity对附近的光探测器进行插值,以得出每个对象在本地原点处的探测器值。这意味着,当动态对象位于光探针组中时,无法对其进行实例化。可以覆盖每个对象用于插值的位置,因此你可以让附近的对象使用相同的探测数据,仍然可以对它们进行实例化。
3.3 光探针代理体积
由于光探测器数据基于对象的本地原点,因此它仅适用于相对较小的对象。为了说明这一点,我在场景中添加了一个细长的动态立方体。它应该经受不同的烘焙光水平,但最终会均匀照明。
(大型动态物体)
对于这样的对象,只有对一个以上的探针进行采样,我们才能获得合理的结果。我们可以通过使用轻探针代理卷(简称为LPV)来实现该目标,可以通过“Component/ Rendering / Light Probe Proxy Volume”将其添加到对象中,如“渲染18,实时GI,探针卷,LOD组”中所述。
(LPPV组件,设置为2X2X16)
要启用LPPV使用,必须将对象的“Light Probes”模式设置为“Use Proxy Volume”。
(使用代理体积)
我们还必须指示Unity将必要的数据发送到GPU,使用RendererConfiguration.PerObjectLightProbeProxyVolume标志。
LPPV配置放在UnityProbeVolume缓冲区中,其中包含一些参数,转换矩阵和大小调整数据。探针体积数据存储在浮点3D纹理中,我们可以将其定义为TEXTURE3D_FLOAT(unity_ProbeVolumeSH),并附带采样器状态。
在SampleLightProbes中,检查是否设置了unity_ProbeVolumeParams的第一个组件。如果是这样,我们必须采样LPPV而不是常规探针。通过从EntityLighting调用SampleProbeVolumeSH4,以纹理,表面位置和法线,变换矩阵,第二个和第三个参数值以及大小调整配置作为参数,可以完成此操作。
(大型动态物体 使用LPPV)
使用LPPV的对象可以被实例化吗?
是的,如果他们使用相同的LPPV,则可以通过设置他们的 Proxy Volume Override 并使用其他游戏对象的LPPV来完成。
4 实时全局光照
烘焙灯的缺点是在播放模式下不能改变。如“渲染18,实时GI,探针体积,LOD组”中所述,Unity可以预先计算全局照明关系,而仍可以在播放模式下调整光强度和方向。这是通过在“Lighting ”窗口中的“Realtime Lighting”下启用“Realtime Global Illumination”来完成的。在禁用烘焙照明的同时,将照明的“Mode ”设置为“Realtime”。
(只有实时全局光照)
Unity将使用Enlighten引擎预先计算传播间接光所需的所有数据,然后存储该信息以在以后完成烘焙过程。这使得可以在播放时更新全局照明。只有光探针才能拾取实时全局照明。静态对象使用动态光照贴图代替。
(通过光探针实时全局照明。)
4.1 渲染实时全局光照
渲染表面信息以进行实时光照映射也可以通过meta pass完成。但是实时光照贴图的分辨率会低得多,并且UV展开可能会有所不同。因此,我们需要不同的UV坐标和变换,可通过第三个顶点UV通道和unity_DynamicLightmapST进行使用。
烘焙和实时光照贴图都需要相同的输出,因此唯一不同的是我们必须使用哪个坐标。这是通过unity_MetaVertexControl指示的,它的第一个标志设置为烘焙,第二个标志设置为实时。
4.2 采样动态光照贴图
现在,我们可以在Lit.hlsl中对动态光照贴图进行采样,其工作方式类似于烘焙的光照贴图,但是要通过unity_DynamicLightmap纹理和关联的采样器状态进行采样。创建一个SampleDynamicLightmap函数,该函数是SampleLightmap的副本,但它使用其他纹理并且从不对其进行编码。
当需要采样动态光照贴图时,Unity将选择设置了DYNAMICLIGHTMAP_ON关键字的着色器变体,因此请为其添加多编译指令。
添加所需的UV坐标和变换,就像烘焙光照贴图一样。
在GlobalIllumination中,对动态光照贴图进行采样(如果可用)。烘焙光照贴图和实时光照贴图都可以同时使用,因此请在这种情况下添加它们。
现在,静态对象还可以接收实时全局照明。
(采样动态光照贴图)
4.3 实时间接自发光
我们的静态自发光对象也可以使其发光作用于实时全局照明,而不是只被烘焙。
(实时间接自发光)
这样做的好处是,可以调整发光颜色并使其在运行模式下影响间接光,就像调整主光的颜色和方向会影响它一样。为了证明这一点,请在InstancedMaterialProperties中添加一个脉冲发射频率配置选项。如果它大于零,则在运行模式下,使用余弦在其原始值和黑色之间振荡发射颜色。
(调整发光,但是没有改变间接光)
仅更改发射颜色不会自动更新全局照明。我们必须告诉Unity照明情况已更改,这可以通过在更改的对象的MeshRenderer组件上调用UpdateGIMaterials来完成。
这将触发对象的meta pass渲染,这仅在更改统一颜色时就显得过大了。在这种情况下,我们可以通过使用渲染器和颜色调用DynamicGI.SetEmissive来直接设置统一的颜色,这将大大加快计算速度。
(变化的全局光)
4.4 透明度
Unity使用Enlighten生成实时全局照明,默认情况下,该照明不适用于透明表面。
(透明度太暗)
现在,我们必须通过启用“Is Transparent”的自定义光照贴图参数,将对象明确标记为透明。
(靠近透明地方太亮)
透明的表面不会阻挡光线,但它们仍然完全有助于间接的光累积。结果是全局照明在透明表面附近变得太强。使对象完全透明时,这变得非常明显。
(全透明,没有光照变化)
我们可以通过在元通道中将不透明度计入反照率和发射颜色中来对此进行补偿。
因为动态光照贴图的分辨率非常低,所以纹理不会对最终结果产生太大影响,但是均匀的不透明度值会影响效??果。
(照明中要考虑透明度)
请注意,即使Unity重新创建了预先计算的数据,它也不总是拾取这些更改。如果认为旧的光照结果仍然有效,则可以将其缓存在编辑模式下。我发现切换播放模式以显示差异最可靠。
5 点和聚光灯
现在,我们仅使用了定向光。让我们检查一下所有内容是否也适用于点光源和聚光灯。
(实时光 点光源和聚光灯)
5.1 实时 全局光照
启用实时全局照明后,两盏灯均按预期方式烘焙,但结果不正确。在计算阴影的间接照明时,即使不考虑阴影。它们也太亮了。
(不产生阴影的间接光照)
事实证明,这些灯不支持阴影实时间接照明,Unity在其检查器中提到了这一点。动态全局照明主要是为了支持间接的太阳光而添加的,以允许昼夜循环与全局照明相结合。这只需要完全支持定向灯。
(不支持)
5.2 烘焙全局光照
Mixed 和 fully-baked 的点光源和聚光灯不会遇到上述问题,并且可以使用,除非它们仍然显得太亮。
(只烘焙,太亮了)
光的贡献太大,因为Unity假设使用了默认管线的传统光衰减,而我们使用的是物理正确的平方反比衰减。对于定向光,这不是问题,因为它们没有衰减。为了解决这个问题,MyPipeline必须告诉Unity光照贴图时要使用哪个衰减函数。为此,我们需要使用Unity.Collections和UnityEngine.Experimental.GlobalIllumination中的类型。但是,使用后者会导致LightType发生类型冲突,因此应将其明确用作UnityEngine.LightType。
我们必须重写光照映射器如何设置其光照数据。通过为它提供方法的委托来完成,该方法将数据从输入Light数组传输到输出LightDataGI数组。委托的类型为Lightmapping.RequestLightsDelegate,使用lambda表达式定义该方法,因为我们在其他任何地方都不需要它。
该委托将仅在编辑器中使用,因此我们可以使其编译为有条件的。
遍历所有光源,适当地配置LightDataGI结构,将其衰减设置为FalloffType.InverseSquared,然后将其复制到输出数组。
即使我们不需要更改衰减以外的任何默认行为,也必须显式配置每种光源类型。我们可以使用LightmapperUtils.Extract方法将适当的值放入特定于光源的结构中,然后通过其Init方法将其复制到光源数据中。如果我们最终得到一个未知的灯光类型,那么我们将使用灯光的实例标识符调用InitNoBake。
通过将其传递给构造函数方法末尾的Lightmapping.SetDelegate方法,使用此委托覆盖默认行为。处理流水线对象时,还必须还原为默认行为,方法是通过重写其Dispose方法来调用基本实现,然后调用Lightmapping.ResetDelegate。
(正确的烘焙)
下一节,介绍烘焙阴影。
欢迎扫描二维码,查看更多精彩内容。点击 阅读原文 可以跳转原教程。
本文翻译自 Jasper Flick的系列教程
原文地址:
https://catlikecoding.com/unity/tutorials