本文重点:
1、渲染到光照贴图,并从中采样 2、让烘焙光和法线一起正常生效 3、使用光探针组
(温馨提示:本系列知识是循序渐进的,推荐第一次阅读的同学从第一章看起)
这是关于渲染的系列教程的第16部分。上次,我们渲染了自己的延迟灯光。在这一部分中,我们转到灯光贴图上来。
本教程使用Unity 5.6.0制作。
(烘焙光)
1、光贴图
执行照明计算非常昂贵。延迟渲染使我们可以使用很多灯光,但是阴影仍然是一个限制因素。如果场景是动态的,那么我们将不可避免地执行这些计算。但是,如果光源和几何物体都不变,那么我们可以只计算一次光源并重复使用它。这样的话就可以在我们的场景中放置许多灯光,而又不必在运行时渲染它们。也可以使用区域光,但这些区域光同样不能用作实时照明。
预计算的灯光到底可以产生多少变化呢?在本教程中,我们会一路把所有内容烘焙到光照贴图中。以确保不会有任何动态光照。
为了进行光照贴图的效果展示,创建了一个简单的测试场景,该场景具有一个提供阴影的简单结构,以及在其周围和内部放置的一些球体。一切都使用默认的Unity材质。
(光照贴图测试场景)
1.1 烘焙光
开始进行光照映射之前,请将唯一的灯光对象的“Mode”更改为“Baked”,而不是“Realtime”。
(烘焙主方向光)
将主定向光转换为烘焙光后,它将不再包含在动态光照中。从动态对象的角度来看,就不存在光了。唯一剩下的就是环境照明,它仍然基于主光源。
(不再有直接照明)
要实际启用灯光映射,请在照明窗口的“Mixed Lighting”部分中启用“Baked Global Illumination”。然后将“Lighting Mode”设置为“Baked Indirect”。尽管是间接光的名称,但它也包括直接照明。它通常仅用于向场景添加间接光。另外,请确保已禁用“Realtime Global Illumination”,因为我们尚不支持。
(Baked indirect 模式)
1.2 静态几何体
场景的物件都应该是固定的,并且永远都不应该动。要将其传达给Unity,请将其标记为静态。你可以通过启用检查器窗口右上方的“Static”切换来实现。
灯也必须标记为静态吗?
不是必需的。灯光仅需设置为适当的模式。
有各种各样的子系统关心物体是否是静态的。静态还有一个下拉菜单,你可以使用它来微调哪些系统将对象视为静态。现在,我们只关心光照贴图,但是将所有内容完全静态化是最简单的。
(静态物体)
还可以通过其mesh renderer的检查器查看和编辑对象是否出于光照贴图的目的是静态的。
(只对光照贴图生效的静态物体)
现在所有对象都是静态的,它们将包含在光照贴图中。
(用烘焙光的场景)
请注意,与使用实时照明相比,光照贴图的结果亮度较弱。那是因为缺少镜面光,它只是包含漫射光。因为镜面光取决于视角,也就是取决于相机。通常,相机是可移动的,因此不能包含在光照贴图中。此限制意味着光照贴图可以用于微弱的灯光和暗淡的表面,但不适用于强直射的灯光或闪亮的表面。如果要使用镜面光,则必须使用实时照明。因此,通常最终会混合使用烘焙光和实时光。
为什么我没有烘焙光?
要确保在需要时实际生成并更新了光照贴图,请在光照窗口底部启用“Auto Generate”。否则,需要手动生成新的光照贴图。
(自动生成光照贴图)
1.3 光照贴图设置
照明窗口包含专门用于灯光映射设置的部分。在这里,你可以在质量,尺寸和烘焙时间之间进行权衡。也可以在Enlighten灯光映射引擎和Progressive灯光映射器之间切换。后者以增量方式生成光照贴图,优先考虑场景视图中可见的内容,这在编辑时很方便。我在本教程中使用了Enlighten。
(默认的光照贴图设置)
在执行其他任何操作之前,请将“Directional”设置为“Non-Direction”。稍后我们将讨论其他模式。
(非定向光照贴图)
烘焙的灯光存储在纹理中。你可以通过将照明窗口从“Scene”切换为“Global Maps”模式来查看它们。使用默认设置,我的测试场景可以轻松放入单个1024×1024的贴图中。
(光照贴图)
Unity的默认对象都具有配置为光照贴图的UV坐标。对于导入的网格,你可以提供自己的坐标,或者让Unity为你生成它们。烘焙后,可以在光照贴图中看到纹理展开。它们需要多少空间取决于场景中对象的大小和光照贴图分辨率设置。如果一张放不下,则Unity将创建额外的贴图。
(光照贴图的分辨率 会造成较大的差别)
哪个设置最好,取决于每个项目。你需要调整设置,直到达到良好的平衡。请注意,视觉质量还很大程度上取决于用于光照贴图的纹理展开的质量。缺失的纹理接缝会产生明显的失真。Unity的默认球体就是一个很好的例子。所以,它(球体)不适用于光照贴图。
1.4 间接光
虽然烘焙光意味着我们失去了镜面光,但我们获得了间接光。这是在到达我们的眼睛之前会从多个表面反弹的光。由于光线在拐角处反弹,因此本来会被阴影覆盖的区域仍会被照亮。间接光虽然无法实时计算,但可以在烘焙时加入反弹光。
为了清楚地看到实时照明与烘焙照明之间的差异,请将环境照明的强度设置为零。这消除了天空盒,因此所有光仅来自定向光。
(无环境照明,实时光VS光照贴图)
每次光子反弹,它都会失去部分能量,并被与其相互作用的材质融色。烘焙间接光时,Unity考虑到这一点。结果就是,物体会根据附近的物体进行上色。
(绿色的地板 实时光VS光照贴图)
自发光表面也会影响烘焙的光线。它们成为间接光源。
(自发光地表 实时光VS光照贴图)
间接光的特殊设置是环境光遮挡。这是指在角落和折痕中发生的间接光的阴影。这是一种人工增强,可以增强深度感。
(使用了环境遮挡)
环境光遮挡效果仅基于表面的隐藏程度。它不考虑光的实际来源。这并不总是有意义的,例如与发光表面结合使用。
(明显错误的环境光遮挡)
1.5 透明度
光照贴图最多可以处理半透明的表面。光线将通过它们,其颜色不会被它们过滤。
(半透明的顶)
cutout 材质也可以。
(cutout 顶)
但是,这仅在使用封闭表面时有效。当使用四边形等单面几何体时,不存在的一侧的光会损坏。当另一侧什么都没有时,这很好,但是在处理单面透明表面时会导致问题。
(四边形产生错误)
为了解决这个问题,你必须告诉灯光映射系统将这些表面视为透明。这可以通过自定义灯光映射设置来完成。可以通过Asset/ Create / Lightmap Parameters创建它们。这些资产允许你自定义每个对象的光照贴图计算。在这种情况下,我们只想表明我们正在处理透明对象。因此,启用“Transparent”。尽管它是“Precomputed Realtime GI ”部分的一部分,但它将影响所有烘焙的光。
(表示透明度)
要使用这些设置,请通过对象的网格渲染器检查器选择它们。你的资产名称将显示在“Lightmap Parameters”的下拉列表中。
(对透明四边形使用自定义参数)
将对象标记为透明还可以更改其对间接照明的作用。透明的物体使间接光通过,而不透明的物体则阻挡它。
2 使用光照贴图
现在我们知道了光照贴图的工作原理,我们可以将其支持添加到“My First Lighting Shader”中。此过程的第一步是对光照贴图进行采样。调整场景中的球体,使它们与我们的着色器一起使用白色材质。
(使用我们的白色材质的球体)
2.1 光照贴图着色器变体
当着色器应该使用光照贴图时,Unity将寻找与LIGHTMAP_ON关键字关联的变体。因此,我们必须为此关键字添加一个多编译指令。使用前向渲染路径时,仅在基本pass中对光照贴图进行采样。
使用光照贴图时,Unity将永远不会包含顶点光照。他们的关键字是互斥的。因此,我们不需要同时具有VERTEXLIGHT_ON和LIGHTMAP_ON的变体。
延迟渲染路径中也支持光照贴图,因此也应将关键字添加到延迟pass中。
2.2 光照贴图坐标
用于采样光照贴图的坐标存储在第二个纹理坐标通道uv1中。因此,将此通道添加到“My Lighting”中的VertexData。
光照贴图坐标也必须进行插值。因为它们与顶点灯互斥,所以两者都可以使用TEXCOORD6。
顶点数据中的坐标定义了用于光照贴图的网格的纹理展开。但这并没有告诉我们该展开的位置在光照图中的位置,也没有告诉我们其大小。我们必须缩放和偏移坐标才能得出最终的光照贴图坐标。这项工作类似于应用于常规纹理坐标的变换,只是该变换是特定于对象的,而不是特定于材质的。光照贴图纹理在UnityShaderVariables中定义为unity_Lightmap。
不幸的是,我们不能使用方便的TRANSFORM_TEX宏,因为它假定光照贴图转换定义为unity_Lightmap_ST,而实际上是unity_LightmapST。由于这种不一致,我们必须手动进行操作。
2.3 采样光照贴图
因为光照贴图数据被认为是间接光照,所以我们将在CreateIndirectLight函数中对其进行采样。当有光照贴图可用时,我们必须将它们用作间接光照的源,而不是球谐函数。
为什么要分配而不是添加到indirectLight.diffuse?
这表明光照贴图永远不会与顶点光照组合。
unity_Lightmap的确切形式取决于目标平台。它定义为UNITY_DECLARE_TEX2D(unity_Lightmap)。为了对其进行采样,我们将使用UNITY_SAMPLE_TEX2D宏而不是tex2D。我们稍后将说明原因。
(使用光照贴图的原始数据)
现在我们得到了间接照明,但看起来不对。那是因为光照图数据已被编码。颜色以RGBM格式存储或以半强度存储,以支持高强度光。UnityCG的DecodeLightmap函数负责为我们解码。
(使用解码后的光照数据)
3 创建光照贴图
虽然光照贴图似乎已经可以与我们的着色器一起使用,但这仅适用于我们简单的测试场景。当前,光照贴图器始终将我们的对象视为不透明和纯白色,即使它们并非如此。我们必须对着色器进行一些调整,甚至还要添加另一个pass来完全支持光照贴图。
从现在开始,对场景中的所有对象使用我们自己的着色器。默认材质将不再使用。
3.1 半透明阴影
光照贴图器不使用实时渲染管道,因此不使用着色器来完成其工作。当尝试使用半透明阴影时,这是最明显的。通过给它的色调的alpha分量设置为小于1的材质,使立方体顶面为半透明的。
(半透明的顶,错误的阴影)
光照贴图器仍将屋顶视为实心,这是不正确的。它使用材质的渲染类型来确定如何处理表面,这应该告诉我们我们的对象是半透明的。实际上,它确实知道屋顶是半透明的,只是将其视为完全不透明。发生这种情况是因为它使用_Color材质属性的alpha成分以及主纹理来设置不透明度。但是我们没有该属性,而是使用_Tint!
更糟糕的是,没有办法告诉灯光映射器要使用哪个属性。因此,要使光照贴图起作用,除了将_Tint的用法替换为_Color之外,我们别无选择。首先,更新我们的着色器的属性。
然后,为使着色器正常工作,我们还必须替换“My Lighting”中的相应变量。
同样处理“My Shadows”。
而且我们还必须调整MyLightingShaderGUI。
(半透明的立方体顶 正确的)
3.2 Cutout 阴影
Cutout 阴影也有类似的问题。光照贴图器希望将alpha截止值存储在_Cutoff属性中,但是我们正在使用_AlphaCutoff。结果,它使用默认截止值为1。
(cutout 顶面 不正确)
解决方案是再次采用Unity的命名约定。因此,替换该属性。
调整“My Lighting”以匹配新名称。
也更新“My Shadows”。
还有MyLightingShaderGUI。
(cutout 顶面 正确的)
3.3 增加meta pass
下一步是确保光照贴图器使用正确的表面反照率和发射率。现在,一切总是纯白色的。你可以通过将地板变绿来看到此情况。它应该导致绿色的间接光,但仍然是白色。
(绿色的地板 错误表现)
为了弄清楚对象的表面颜色,光照贴图器查找其光照模式设置为Meta的着色器通道。此过程仅由lightmapper使用,不包含在构建中。因此,让我们向着色器添加这样的pass。这是一个基本pass,不应使用剔除。将其代码放入新的My Lightmapping包含文件中。
现在我们需要确定反照率,镜面反射的颜色,平滑度和发射度。因此,将所需的变量和函数从“My Lighting”复制到“My Lightmapping”。为此,我们仅需要顶点位置和uv坐标。不使用法线和切线,但是需要顶点着色器中的光照贴图坐标。
我们可以按原样使用函数,但GetEmission除外。该函数仅在前向base pass 或延迟pass中使用时才起作用。在My Lightmapping中,我们可以简单地删除此限制。
这些函数仅在定义了适当的关键字后才起作用,因此请将其着色器功能添加到pass中。
3.4 顶点程序
顶点程序对于此过程很简单。转换位置并转换纹理坐标。
但是,我们实际上不是为照相机渲染,而是为光照贴图渲染。我们正在将颜色与光照贴图中展开的对象的纹理相关联。要执行此映射,必须使用光照贴图坐标而不是顶点位置,并进行适当的转换。
事实证明,要使它在所有机器上都可以使用,即使我们不使用顶点位置的Z坐标,也必须以某种方式使用它。Unity的着色器为此使用了一个虚拟值,因此我们将简单地做同样的事情。
3.5 片段程序
在片段程序中,我们必须输出反照率和自发光色。灯光映射器将通过执行两次pass来完成此操作,每个输出一次。为了方便,我们可以使用UnityMetaPass包含文件中定义的UnityMetaFragment函数。它具有UnityMetaInput结构作为参数,同时包含反照率和发射率。该函数将决定输出哪个以及如何对其进行编码。
UnityMetaInput也包含镜面反射颜色,即使它不存储在光照贴图中。它用于一些编辑器的可视化效果,这时我们将忽略它们。
UnityMetaFragment是什么样的?
unity_MetaFragmentControl变量包含一些标志,这些标志告诉函数是输出反照率颜色还是自发光颜色。也有用于编辑器可视化变体的代码,但我将其删除了,因为它与此处无关。
(间接光设置为0)
要获得自发光颜色,可以简单地使用GetEmission。要获得反照率,必须再次使用DiffuseAndSpecularFromMetallic。该函数具有用于镜面反射的颜色和反射率的输出参数,因此即使我们不在函数外使用它们,也必须提供这些参数。可以使用surfaceData.SpecularColor捕捉镜面颜色。
(间接光着色)
这适用于间接光,但是自发光可能尚未出现在光照贴图中。这是因为光照贴图器并不总是包含用于发光的pass。材质必须发出信号,表明它们具有发射光,有助于烘焙过程。这是通过Material.globalIlluminationFlags属性完成的。现在,让我们始终指示在编辑其发射时应该烘焙自发光。
3.6 粗糙的金属
现在,我们的着色器似乎可以正常工作,但是与标准着色器的结果不完全匹配。当使用平滑度非常低的有色金属时,这一点很明显。
(粗糙的绿色金属 标准 VS 我们的着色器)
这个想法是,非常粗糙的金属应该产生比我们目前的计算结果更多的间接光。标准着色器通过将部分镜面反射颜色添加到反照率来对此进行补偿。它使用UnityStandardBRDF中的SmoothnessToRoughness函数确定基于平滑度的粗糙度值,将其减半并将其用于缩放镜面反射颜色。
SmoothnessToRoughness计算什么?
转换值为1减去平滑度值的平方。从平滑度到粗糙度的平方映射最终会产生比线性转换更好的结果。
(调整了反照率)
4 定向光照贴图
光照贴图器仅使用几何图形的顶点数据,不考虑法线贴图。光照贴图分辨率太低,无法捕获典型法线贴图提供的细节。这意味着静态照明将是平坦的。当使用具有法线贴图的材质时,这一点变得非常明显。
(使用法线贴图 实时光VS光照贴图化)
4.1 定向
通过将“Directional Mode”改回“Directional”,可以使法线贴图在烘焙的光照下工作。
使用定向光照贴图时,Unity将创建两个贴图,而不只是一个。第一张图包含照常的照明信息,称为强度图。第二张地图称为方向图。它包含了大多数烘焙光所来自的方向。
(强度贴图和方向贴图)
当方向图可用时,我们可以使用它来对烘焙的光执行简单的漫反射着色。这使得可以应用法线贴图。请注意,只有一个光方向是已知的,因此阴影将是近似值。只要在强光下至少存在一个主导的光方向,结果就会看起来不错。
4.2 采样方向
当有方向性光照贴图可用时,Unity将寻找同时带有LIGHTMAP_ON和DIRLIGHTMAP_COMBINED关键字的着色器变体。无需手动为此添加多编译指令,我们可以在正向基本传递中使用#pragma multi_compile_fwdbase。它将处理所有的lightmapping关键字,以及VERTEXLIGHT_ON关键字。
我们可以对延迟的pass执行相同的操作,但是在这里我们必须使用#pragma multi_compile_prepassfinal指令。它照顾了光照映射和HDR关键字。
什么是 prepass final?
Unity 4使用了与更高版本不同的延迟渲染管道。在Unity 5中,这称为传统延迟照明。这种方法有更多的pass。Prepass final是那时的术语。除了引入新的指令外,#pragma multi_compile_prepassfinal还用于当前的延迟pass。
在CreateIndirectLight中检索烘焙光本身之后,直接需要烘焙光方向。方向图可通过unity_LightmapInd获得。
但是,这将导致编译错误。因为纹理变量实际上由两部分组成。有纹理资源,有采样器状态。采样器状态确定如何采样纹理,包括滤镜和钳位模式。通常,两个部分都是针对每个纹理定义的,但是并非所有平台都要求这样做。也可以将它们分开,这使我们可以为多个纹理定义单个采样器状态。
因为强度和方向图总是以相同的方式采样,所以Unity在可能的情况下使用单个采样器状态。这就是为什么在采样强度图时必须使用UNITY_SAMPLE_TEX2D宏的原因。方向图已定义为没有采样器。要对其进行采样,我们必须使用UNITY_SAMPLE_TEX2D_SAMPLER宏来明确告诉它要使用哪个采样器。
4.3 使用方向
要使用此方向,我们首先必须对其进行解码。然后,我们可以使用法线向量执行点积运算,以找到漫反射因子并将其应用于颜色。但是方向贴图实际上并不包含单位长度方向,它要更复杂一些。幸运的是,我们可以使用UnityCG的DecodeDirectionalLightmap函数解码方向性数据并为我们执行着色。
(使用定向 光照贴图)
DecodeDirectionalLightmap有什么作用?
实际上,它没有计算出正确的漫射照明因子。相反,它使用一半的Lambert代替。这种方法有效地将光线包裹在表面周围,从而照亮了阴影区域。这是必需的,因为烘焙的光本来不是来自单个方向。
代码注释中提到了镜面光照贴图。这些是支持镜面照明的光照贴图,但是需要更多的纹理,使用起来更昂贵,并且在大多数情况下效果不佳。自Unity 5.6起已将其删除。
5 光探针
光照贴图仅适用于静态对象,不适用于动态对象。结果,动态对象无法放入带有烘焙照明的场景中。当根本没有实时照明时,这是非常明显的。
(动态物体 显示异常)
为了更好地混合静态和动态对象,我们还必须以某种方式将烘焙的光照应用于动态对象。Unity为此提供了光探针。光探针是空间中的一个点,具有有关该位置的照明的信息。代替纹理,它使用球谐函数来存储此信息。如果可用,这些探针将用于动态对象,而不是全局环境数据。因此,我们要做的就是创建一些探针,等到烘焙完成,我们的着色器将自动使用它们。
5.1 创建一个光探针组
通过GameObject/ Light / Light Probe Group将一组光探测器添加到场景中。这将创建一个新的游戏对象,其中包含八个以立方体形式排列的探针。在为动态对象着色时将立即使用它们。
(新的光探针组)
在启用“Edit Light Probes ”模式后,可以通过其检查器编辑光探针组。启用后,你可以选择单个探针并在场景视图中移动它们,或通过检查器对其进行调整。可以像对待游戏对象一样操纵,复制和删除单个探针。
(光探针组的检视器)
你不必显式启用编辑模式。在场景视图中选择组,就可以开始编辑探针。要停止编辑它们,请取消选择该组。
5.2 防止光探针
光探针组将其包围的体积划分为四面体区域。四个探针定义了四面体的角。对这些探针进行插值,以确定动态对象所用的最终球谐函数,具体取决于其在四面体内部的位置。这意味着将动态对象视为单个点,因此它仅适用于相当小的对象。
编辑探针时,四面体会自动生成。你不需要了解它们的配置,但是它们的可视化可以帮助您查看探针的相对位置。
放置光探针只需调整一下,直到获得可接受的结果,就像操作光贴图设置一样。首先将要包含动态对象的区域包围起来。
(包裹区域)
然后根据照明条件的变化添加更多的探头。请勿将它们放置在静态几何体中,这一点至关重要。也不要将它们放在不透明的单面几何图形的错误一侧。
(放置更多的探针)
继续添加和移动探针,直到在所有区域都拥有合理的照明条件并且可接受它们之间的过渡为止
(调整探针)
可以通过移动动态对象来测试探针。选择动态对象时,还将显示当前影响它的探针。探针将显示其光照,而不仅仅是黄色的球体。你还可以查看用于动态对象的插值数据。
(移动动态物体穿过探针组)
下一章,介绍混合光照。