1 光照贴图-Lightingmapping
实时光照计算的开销非常昂贵。根据翻译13,延迟渲染允许程序员使用的光源可以多于Forward渲染,但阴影的开销仍然对性能有一个限制。如果我们的场景是动态的,那么没有办法来避免执行这些计算。但是如果光源和几何物体位置都是不变的,那么我们可以只计算一次光照并重复使用。这使得我们可以在场景中放置许多光源,而不必在运行的时候再渲染它们。这种方法也可以使用那些不能用作实时光源的区域光源(area lighting)。
在本教程中,会将所有内容都放在光照贴图中,所以根本不会有任何动态光照。
为了尝试光照贴图,我创建了一个简单的测试场景,它具有一个简单的结构,可以提供阴影,还有一些放置在其内部的球体。一切物体都使用默认的Unity材质。
针对光照贴图的一个测试场景
1.1 烘焙光源-Baked Lights
要开始使用光照贴图,将唯一的光源对象的模式改为“Baked(烘焙)”而不是“Realtime(实时)”。
使用烘焙模式的主方向光源
将主方向光源变成烘培光源后,就不被纳入动态光照计算。从动态对象的角度来看,光源是不存在的。 唯一仍然不变的是环境光照,它仍然是基于主方向光源的。
没有直接光照的效果
要实际启用光照贴图,请在lighting窗口的“混合光照(Mixed Lighting)”中打开“烘培全局光照(BakedGlobal Illumination)”。 然后将光照模式设置为“烘培间接光照(BakedIndirect)”。 尽管它的名字说的是烘培间接光照,但是它也包括了直接光照。 它通常用于向场景添加间接光照。另外,确保实时全局光照(Realtime Global Illumination)被禁用,因为我们还没有支持到这一点。
烘培间接光照模式
1.2 静态几何体
场景的对象都应该是固定的:它们位置永远不会移动。要将这一个信息传达给Unity,请将这些对象标记为静态。你可以通过启用检视器窗口右上角的“静态”切换键来做到这一点。
光源也必须被标记为静态吗?不需。光源只需要设置为适当的模式。
有各种子系统关心物体是否是静态的。“静态(static)”还有一个下拉菜单,你可以使用它来微调哪些系统会将这个对象视为静态的。现在我们只关心光照贴图,但最简单的做法是使一切都完全是静态的。
静态标签设定
一个物体对于光照贴图来说是否是静态的,也可以通过其网格渲染器的检视器来进行查看和编辑。
对于光照贴图来说是静态的物体
现在,所有的物体都是静态的,它们将被包含在光照贴图的处理过程中。
使用烘焙光照的场景
必须注意,使用光照贴图得到的结果不如使用实时照明得到的结果亮度那么高。这是因为缺失了镜面高光,只剩下了漫反射光照。镜面高光取决于视角,因此取决于相机的角度。正是由于相机是移动的,因此它不能包含在光照贴图中。(使用场景推荐)这种限制意味着光照贴图可以用于微弱的光线和暗淡的表面,但不能用于强直射光或有光泽的表面。如果你想要镜面高光,你将不得不使用实时光源。所以你经常会使用烘烤光源和实时光源的混合。
为什么没有立即得到烘焙光源? 为了确保在需要的时候光照贴图可以实际生成和更新,请在光照窗口的底部启用“自动生成(Auto Generate)”。 否则,你必须手动生成新的光照贴图。
自动烘焙
1.3 光照贴图设置-Lightingmapping Setting
光照烘焙窗口包含专门用于光照贴图设置的部分。在这里,你可以在质量、尺寸和烘烤时间之间取得平衡。你还可以在光照贴图烘焙算法引擎:Enlighten和Progressive lightmapper之间进行切换。后者会增量地生成光照贴图,优先考虑场景视图中可见的内容,这在编辑的时候很方便。本教程中使用的是Enlighten光照贴图引擎。
默认的光照贴图设置
在做任何事情之前,请将“DirectionalMode“设置为”Non-Direction“。 稍后我们会处理其他模式。
使用“Non-directional”模式的光照贴图
烘烤的光照存储在纹理中。 你可以通过将光照窗口从“场景(Scene)“切换到”全局地图(Global Maps)“模式来进行查看。 使用默认设置,我的测试场景很容易与一张1024×1024贴图相匹配。
得到光照贴图
Unity自带的Objects物体都有用于光照贴图的UV坐标。对于手动导入的模型,可以自己提供UV坐标,也可以让Unity生成。烘烤后可以在光照贴图中看到展开的纹理。它们需要多少空间取决于场景中物体的大小和光照贴图的分辨率设置。 如果质量要求高分辨率太大,一张贴图涨不下,Unity会创建额外的贴图存储,直至完成。
光照贴图的分辨率的不同会带来很大的差异
对于每个项目来说,最佳设置都是不同。 你必须不断的调整烘焙参数,直到达成很好的效果及平衡。需要注意的是,视觉质量也很大程度上取决于用于光照贴图的纹理展开的质量。不存在纹理接缝可能会产生明显的瑕疵。Unity的默认球体就是一个很好的例子。它不适用于光照贴图。
1.4 间接光源
烘焙光照会失去镜面高光,只能获得的是间接光照,它是在到达人眼之前会在多个表面反射的光。烘焙光会在拐角周围区域反射,那些本来会被遮挡的区域仍然会被照亮。我们不能实时计算镜面高光这个信息(本节1.2有说明),但是我们可以在烘焙的时候包括反射光。
要清楚地看到实时光照和烘培光照之间的差异:将环境光照的强度设置为零,去掉天空盒的影响,所有的光都只是来自方向光。比对
没有环境光照,realtime vs. lightmapped
每次光子反射的时候,它都会失去一些能量,它会被一些需要的材质采样着色。Unity在烘焙间接光照的时候,物体会根据附近的颜色进行着色。
绿色的地面,realtime vs. lightmapped
自发光表面也会影响烘焙光照。它们会成为间接光源。
自发光的地面,realtime vs. lightmapped
间接光照的一个特殊设置是AO环境遮挡:这是指在角落和转折中发生的间接光照造成的阴影。这是一种人为的提升,可以增强深度方面的视觉。
使用环境遮挡的效果
环境遮挡效果完全基于物体表面。它不考虑光线实际来自哪里。烘焙时并不总是正确,举个简单的例子:当与自发光表面组合的时候就会产生一些错误的结果。
显然是错误的环境遮挡效果
1.5 透明度-Transparency
光照贴图在一定程度上可以处理半透明表面。 光将通过它们,尽管光的颜色不会被它们所过滤。
半透明的屋顶
镂空材质也可以在光照贴图中正常工作。
镂空的屋顶
但 这仅在使用封闭曲面的时候有效。当使用像是quad这样的单面几何,光线将在不存在的一面损坏。当另外一面没有任何东西的时候,这是很好的,但是当使用单面透明表面的时候会导致问题。
四边形上有一个错误
为了处理这个问题,必须告诉光照贴图系统将这些表面视为透明的。 这可以通过自定义光照贴图设置完成
1、通过Asset / Create / Lightmap参数来创建这些数据。这些资源允许你自定义每个对象的光照贴图计算。在这种情况下,我们只想表明我们正在处理一个透明的对象。所以启用“它是透明的(Is Transparent)“。 下面它是一个全局作用预计算实时全局光照(Precomputed Realtime GI)部分中的一部分,它会影响所有烘烤光照。
指示这是透明的
2、单独设置:通过物体的网格渲染器检视器来选择它们。你的资源名字将显示在Lightmap参数的下拉列表中。
为透明四边形使用自定义参数
将物体标记为透明也会改变它对间接光照的贡献。透明物体让间接光通过,而不透明物体则会阻挡间接光。
2 使用光照贴图
现在我们知道光照贴图是如何工作的,我们可以为Shader着色器添加对光照贴图的支持。第一步是对光照贴图进行采样。调整场景中的球体,以便我们的着色器使用白色材质。
使用我们的白色材质的球体
2.1 光照贴图的着色器变体
当一个着色器被认为应该使用光照贴图的时候,Unity会寻找与LIGHTMAP_ON关键字关联的变体。 所以我们必须为这个关键字添加一个多编译指令。 当使用forward-render-path的时候,仅在base-pass中采样光照贴图。
#pragma multi_compile _ SHADOWS_SCREEN #pragma multi_compile _ VERTEXLIGHT_ON #pragma multi_compile _ LIGHTMAP_ON #pragma multi_compile_fog
当使用光照贴图的时候,Unity不会包含顶点光源。他们的关键字是相互排斥的。所以我们不需要一个会同时使用VERTEXLIGHT_ON和LIGHTMAP_ON的变体。(互斥)
#pragma multi_compile _ SHADOWS_SCREEN //#pragma multi_compile _ VERTEXLIGHT_ON //#pragma multi_compile _ LIGHTMAP_ON #pragma multi_compile _ LIGHTMAP_ON VERTEXLIGHT_ON #pragma multi_compile_fog
延迟渲染路径中也支持光照贴图,因此也可以将这个关键字添加到延迟渲染通道中。
#pragma multi_compile _ UNITY_HDR_ON #pragma multi_compile _ LIGHTMAP_ON
2.2 光照贴图的坐标
用于采样光照贴图的坐标存储在TEXCOORD1。 所以将此通道添加到shader中的VertexData结构体中。Unity给出了uv使用说明表:Shader中是uv0、uv1、uv2、uv3;C#中是UV、UV2、UV3、UV4
struct VertexData { float4 vertex : POSITION; float3 normal : NORMAL; float4 tangent : TANGENT; float2 uv : TEXCOORD0; float2 uv1 : TEXCOORD1; };
光照贴图坐标也必须进行插值。因为它们与顶点光源互斥,所以都可以使用TEXCOORD6。
struct Interpolators { … #if defined(VERTEXLIGHT_ON) float3 vertexLightColor : TEXCOORD6; #endif #if defined(LIGHTMAP_ON) float2 lightmapUV : TEXCOORD6; #endif };
来自模型顶点数据的坐标定义了用于光照贴图的纹理展开(第二套uv)。但是它并没有告诉我们这个展开位置在哪里,展开尺寸大小。我们必须缩放和偏移坐标才能得到最终的光照贴图坐标。这种方法类似于常规纹理坐标的转换,除了转换是特定于对象的,而这里的方法是特定于材质的。在UnityShaderVariables中将光照贴图的纹理定义为unity_Lightmap。
Interpolators MyVertexProgram (VertexData v)
{
…
i.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
i.uv.zw = TRANSFORM_TEX(v.uv, _DetailTex);
#if defined(LIGHTMAP_ON)
i.lightmapUV = TRANSFORM_TEX(v.uv1, unity_Lightmap);
#endif
…
}
不幸的是,我们不能使用方便的TRANSFORM_TEX宏,因为它假定光照贴图的变换被被定义为unity_Lightmap_ST,而实际上是被定义为unity_LightmapST。由于这种不一致,我们必须手动进行这个变换。
i.lightmapUV = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw;
2.3 对光照贴图进行采样-Sampling Lightmap
因为光照贴图的数据被认为是间接光照,我们将在CreateIndirectLight函数中进行采样。当光照贴图可用的时候,必须将它们用作间接光而不是球面谐波。
UnityIndirect CreateIndirectLight (Interpolators i, float3 viewDir) { … #if defined(VERTEXLIGHT_ON) indirectLight.diffuse = i.vertexLightColor; #endif #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS) #if defined(LIGHTMAP_ON) indirectLight.diffuse = 0; #else indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1))); #endif float3 reflectionDir = reflect(-viewDir, i.normal); … #endif return indirectLight; }
unity_Lightmap的确切形式取决于目标平台。 它被定义为UNITY_DECLARE_TEX2D(unity_Lightmap)。要对它进行采样,我们将使用UNITY_SAMPLE_TEX2D宏而不是tex2D。这是根据不同平台决定。
indirectLight.diffuse = UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV);
使用原始光照图数据的效果
我们现在得到了烘焙的间接光照,但效果看起来不对。这是因为光照贴图数据已被编码。颜色以RGBM格式或是半强度格式进行存储,以支持高强度的光。UnityCG的DecodeLightmap函数负责为我们解码。
indirectLight.diffuse = DecodeLightmap ( UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV) );
使用解码后光照图数据的效果
3 创建光照贴图
目前,光照贴图会将场景对象总是视为不透明和纯白色的物体。我们必须对我们的着色器进行一些调整,添加一个渲染通道来完全支持光照贴图。
从现在开始,对场景中的所有对象使用我们自己的着色器。也不再使用默认的材质。
3.1 半透明的阴影-Semitransparent Shadow
光照贴图不使用实时渲染管道,因此现有自写的shader不能支持。 当尝试使用半透明阴影的时候,这是最明显的。通过设置屋顶立方体材质的色调alpha分量小于1来赋予屋顶立方体半透明度。
半透明的屋顶,效果不正确
光照贴图仍然把屋顶看成是实心物体,这是不正确的。它使用材质的渲染类型来确定如何处理表面,这应该告诉光照贴图我们的对象是半透明的。事实上,它确实知道屋顶是半透明的,它只是把它看作是完全不透明的而已。这是因为它采用Unity的命名约定_Color材质属性的alpha组件以及主纹理来设置不透明度。
用_Color替换_Tint。
Properties { // _Tint ("Tint", Color) = (1, 1, 1, 1) _Color ("Tint", Color) = (1, 1, 1, 1) … }
然后,为了保证我们的着色器的功能,我们还必须在shader文件、cg文件替换,而且我们还要调整GUI拓展。
半透明的屋顶,正确的效果
3.2 镂空部分的阴影-Cutout Shadow
镂空部分的阴影也有类似的问题。光照贴图程序期望透明度的阈值存储在_Cutoff属性中,但是我们使用的是_AlphaCutoff。 因此,它使用默认阈值1。
镂空的屋顶,效果不正确
解决方案是再次采用Unity的命名约定_Cutoff材质属性。所以替换shader、cg文件、GUI拓展。
镂空的屋顶,正确的效果
3.3 添加一个Meta渲染通道-Add Meta Pass
渲染光照贴图正确的表面反照率和自发光。
绿色的地板,效果不正确
要采样物体的表面颜色,光照贴图程序会将它的光照模式设置为Meta来寻找一个着色器渲染通道。这个渲染通道仅由光照贴图程序使用,不使用剔除。所以让我们在我们的着色器上添加一个渲染通道。
Pass { Tags { "LightMode" = "Meta" } Cull Off CGPROGRAM #pragma vertex MyLightmappingVertexProgram #pragma fragment MyLightmappingFragmentProgram #include "My Lightmapping.cginc" ENDCG }
现在我们需要确定反照率、镜面高光颜色、平滑度、自发光。只需要顶点的位置和uv坐标,以及需要vertexProgram中的光照贴图坐标。不使用法线和切线。
#if !defined(MY_LIGHTMAPPING_INCLUDED) #define MY_LIGHTMAPPING_INCLUDED #include "UnityPBSLighting.cginc" float4 _Color; sampler2D _MainTex, _DetailTex, _DetailMask; float4 _MainTex_ST, _DetailTex_ST; sampler2D _MetallicMap; float _Metallic; float _Smoothness; sampler2D _EmissionMap; float3 _Emission; struct VertexData { float4 vertex : POSITION; float2 uv : TEXCOORD0; float2 uv1 : TEXCOORD1; }; struct Interpolators { float4 pos : SV_POSITION; float4 uv : TEXCOORD0; }; float GetDetailMask (Interpolators i) { … } float3 GetAlbedo (Interpolators i) { … } float GetMetallic (Interpolators i) { … } float GetSmoothness (Interpolators i) { … } float3 GetEmission (Interpolators i) { … } #endif
GetEmission函数去除FORWARD_BASE_PASS和DEFERRED_PASS限制。
float3 GetEmission (Interpolators i) { // #if defined(FORWARD_BASE_PASS) || defined(DEFERRED_PASS) #if defined(_EMISSION_MAP) return tex2D(_EmissionMap, i.uv.xy) * _Emission; #else return _Emission; #endif // #else // return 0; // #endif }
这些函数只有在定义了适当的关键字时才会起作用,因此可以在渲染通道中为其添加着色功能。
#pragma vertex MyLightmappingVertexProgram
#pragma fragment MyLightmappingFragmentProgram
#pragma shader_feature _METALLIC_MAP
#pragma shader_feature _ _SMOOTHNESS_ALBEDO _SMOOTHNESS_METALLIC
#pragma shader_feature _EMISSION_MAP
#pragma shader_feature _DETAIL_MASK
#pragma shader_feature _DETAIL_ALBEDO_MAP
#include "My Lightmapping.cginc"
3.4 顶点程序-Vertex Program
这个pass的vertex 程序很简单。只是转换位置、转换纹理坐标。
Interpolators MyLightmappingVertexProgram (VertexData v) {
Interpolators i;
i.pos = UnityObjectToClipPos(v.vertex);
i.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
i.uv.zw = TRANSFORM_TEX(v.uv, _DetailTex);
return i;
}
计算2.2提到的映射偏移,我们必须使用光照贴图uv坐标而不是顶点位置,然后进行适当的转换把纹理uv坐标作为模型顶点的屏幕位置,模型的UV映射必须要正确:纹理上的每个点必须映射为模型上的唯一点。
Interpolators i; v.vertex.xy = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw; v.vertex.z = 0; i.pos = UnityObjectToClipPos(v.vertex);
v.vertex.z = 0,不是所有机器上都能支持,顶点位置的Z坐标必须以某种方式使用,即使我们不使用它也是如此。Unity的着色器为此使用虚拟值,所以我们将简单地做同样的事情。
Interpolators i; v.vertex.xy = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw; v.vertex.z = v.vertex.z > 0 ? 0.0001 : 0; i.pos = UnityObjectToClipPos(v.vertex);
3.5 片段程序-Fragment Program
在片段程序中,计算输出反照率和自发光颜色。光照贴图程序将通过执行两次渲染来做到这一点,每次执行有一个输出。为了使这个过程更容易,我们可以使用UnityMetaPass.cginc文件中定义的UnityMetaFragment函数。它使用UnityMetaInput结构作为参数,其中包含反照率和自发光颜色。 该函数将决定要输出反照率和自发光颜色中的哪一个以及如何编码输出结果。
UnityMetaInput也包含镜面高光颜色,即使它不存储在光照贴图中。它用于一些编辑器可视化,我们先忽略它。
#include "UnityPBSLighting.cginc" #include "UnityMetaPass.cginc" … float4 MyLightmappingFragmentProgram (Interpolators i) : SV_TARGET { UnityMetaInput surfaceData; surfaceData.Emission = 0; surfaceData.Albedo = 0; surfaceData.SpecularColor = 0; return UnityMetaFragment(surfaceData); }
UnityMetaFragment是什么样子的?
//unity_MetaFragmentControl变量包含一个标记,这个标记会告诉函数是否输出反照率或是自发光颜色。还有一段有关 //编辑器可视化变体的代码,但是我把它删掉了,因为与这里的内容不相关。 half4 UnityMetaFragment (UnityMetaInput IN) { half4 res = 0; if (unity_MetaFragmentControl.x) { res = half4(IN.Albedo,1); // d3d9 shader compiler doesn't like NaNs and infinity. unity_OneOverOutputBoost = saturate(unity_OneOverOutputBoost); // Apply Albedo Boost from LightmapSettings. res.rgb = clamp( pow(res.rgb, unity_OneOverOutputBoost), 0, unity_MaxOutputValue ); } if (unity_MetaFragmentControl.y) { half3 emission; if (unity_UseLinearSpace) emission = IN.Emission; else emission = GammaToLinearSpace (IN.Emission); res = UnityEncodeRGBM(emission, EMISSIVE_RGBM_SCALE); } return res; }
间接光照设置为0的效果
要获得自发光颜色,我们可以简单的使用GetEmission函数。要获得反照率,我们必须再次使用DiffuseAndSpecularFromMetallic函数。 该函数具有镜面高光颜色和反射率作为输出参数,即使我们现在不使用它们,我们也必须提供这些参数。我们可以使用surfaceData.SpecularColor来捕获镜面高光颜色。
float4 MyLightmappingFragmentProgram (Interpolators i) : SV_TARGET
{
UnityMetaInput surfaceData;
surfaceData.Emission = GetEmission(i);
float oneMinusReflectivity;
surfaceData.Albedo = DiffuseAndSpecularFromMetallic
( GetAlbedo(i), GetMetallic(i), surfaceData.SpecularColor, oneMinusReflectivity ); //surfaceData.SpecularColor = 0; return UnityMetaFragment(surfaceData); }
间接光照着色的效果
但自发光光照可能还没有出现在光照贴图中。这是因为光照贴图程序并不总是包含一个自发光光照的渲染通道。材质必须表明它们具有自发光光照属性,以对烘焙过程做出贡献。这是通过Material.globalIlluminationFlags属性完成的。扩展GUI设置:当自发光光照编辑的时候,它应该被烘焙进光照贴图。
void DoEmission () { … if (EditorGUI.EndChangeCheck()) { if (tex != map.textureValue) { SetKeyword("_EMISSION_MAP", map.textureValue); } foreach (Material m in editor.targets) { m.globalIlluminationFlags = MaterialGlobalIlluminationFlags.BakedEmissive; } } }
3.6 粗糙的金属-Rough Metals
我们的shader现在看起来可以正常工作了,但它与标准着色器的结果不完全匹配。 当使用平滑度非常低的有色金属的时候,物体表面不太明亮。
粗糙的绿色金属,standard vs. our
标准着色器通过将反射率的一部分加到镜面高光颜色进行补偿(高亮)。它使用UnityStandardBRDF.cginc的SmoothnessToRoughness函数来确定基于平滑度的粗糙度值,将其缩小一半,并使用它来缩放镜面高光颜色。
float roughness = SmoothnessToRoughness(GetSmoothness(i)) * 0.5; surfaceData.Albedo += surfaceData.SpecularColor * roughness; return UnityMetaFragment(surfaceData);
SmoothnessToRoughness计算了什么东西?
//转换:减去平滑度值,然后平方。 从平滑度到粗糙度的平方映射最终会产生比仅仅做线性转换更好的结果。 // Smoothness is the user facing name // it should be perceptualSmoothness // but we don't want the user to have to deal with this name half SmoothnessToRoughness(half smoothness) { return(1 - smoothness) * (1 - smoothness); }
调整反照率后的效果
4 方向光照贴图-Directinal Lightmap
光照贴图程序只使用物体的顶点数据,不考虑物体的法线贴图。光照贴图的分辨率太低,无法捕获由典型法线贴图提供的细节。这意味着静态光照将是平坦的。当使用具有法线贴图的材质的时候,这变得非常明显。
使用了法线贴图,standard vs. our
当从实时光照切换到烘焙光时,法线贴图的影响几乎完全消失。这是因为它要求环境反射才能看到它们。
4.1 方向性-Directionality
通过将“DirectionalMode”改回“Directional”,可以让法线贴图与烘焙光照一起工作。
再次启用定向光照贴图
当使用方向光照贴图的时候,Unity将创建两个贴图。第一张贴图包含通常的光照信息,称为强度图。 第二张贴图被称为方向图。 它包含大部分烘烤光来自的方向。
强度图和方向图
当方向图可用的时候,用它来对烘焙光进行简单的漫反射阴影计算。这使得它可用于法线贴图之上。注意,只有一个光方向是已知的,所以阴影将是一个近似。至少有一个主方向光照的时候,结果就会很好。
4.2 对方向进行采样
当方向光照贴图可用的时候,Unity将使用LIGHTMAP_ON和DIRLIGHTMAP_COMBINED关键字查找着色器变体。我们可以在forward-base-pass通道中使用#pragma multi_compile_fwdbase,而不是为手动添加多编译指令。它会负责解决所有的光照贴图关键字,以及VERTEXLIGHT_ON关键字。
//#pragma multi_compile _ SHADOWS_SCREEN //#pragma multi_compile _ LIGHTMAP_ON VERTEXLIGHT_ON #pragma multi_compile_fwdbase #pragma multi_compile_fog
我们可以为deferred-pass必须使用#pragma multi_compile_prepassfinal指令。 它解决了光照贴图和高动态光照渲染的关键字。
//#pragma multi_compile _ UNITY_HDR_ON //#pragma multi_compile _ LIGHTMAP_ON #pragma multi_compile_prepassfinal
prepassfinal是什么东西?
Unity 4使用了一种与以后的版本不同的延迟渲染管线。 在Unity 5中,它被称为传统延迟光照。 这种方法有更多的渲染通道。Prepass决定是当时的术语。不需要引入新的指令,#pragma multi_compile_prepassfinal也用于当前的延迟渲染通道。
在CreateIndirectLight函数中,在检索烘焙光源本身后,需要直接获得烘焙光的方向。方向贴图可以通过unity_LightmapInd获得。
#if defined(LIGHTMAP_ON) indirectLight.diffuse = DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV)); #if defined(DIRLIGHTMAP_COMBINED) float4 lightmapDirection = UNITY_SAMPLE_TEX2D ( unity_LightmapInd, i.lightmapUV
); #endif #else indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1))); #endif
但是,这将导致编译错误。这是因为一个纹理变量实际上由两部分组成。 有纹理资源,还有采样器状态。采样器状态决定纹理的采样方式,包括滤波器和截取模式。 通常,每个纹理都定义了这两个部分,但这并不是所有平台都需要的。 也可以将这两个部分分开,这允许我们为多个纹理定义单个采样器状态。
因为强度和方向贴图总是以相同的方式进行采样,所以在可能的情况下,Unity使用单个采样器状态。 这就是为什么我们在采样强度贴图的时候必须使用UNITY_SAMPLE_TEX2D宏。方向贴图已经定义,没有采样器。 要对其进行采样,我们必须使用UNITY_SAMPLE_TEX2D_SAMPLER宏来明确地告诉它要使用哪个采样器。
float4 lightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER
(
unity_LightmapInd, unity_Lightmap, i.lightmapUV
);
4.3 使用方向贴图
要使用方向:1、解码 2、对法向量执行点积,找到漫反射因子并将其应用于颜色。
但是方向贴图并没有包含单位长度的方向,而是比单位长度的方向会大一些。 幸运的是可以使用UnityCG的DecodeDirectionLightmap函数来解码方向数据。
float4 lightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER
(
unity_LightmapInd, unity_Lightmap, i.lightmapUV
);
indirectLight.diffuse = DecodeDirectionalLightmap
( indirectLight.diffuse, lightmapDirection, i.normal );
使用带有方向的光照贴图的效果
DecodeDirectionLightmap内部做了什么?
DecodeDirectionLightmap实际上并不计算正确的漫射照明因子。 相反,它使用的是半Lambert。 这种方法可以有效地将光照射在表面周围,照亮阴影的区域会更多。这么做是有必要的,这是因为烘烤的光照不是来自于单个方向。
inline half3 DecodeDirectionalLightmap ( half3 color, fixed4 dirTex, half3 normalWorld ) { // In directional (non-specular) mode Enlighten bakes dominant light // direction in a way, that using it for half Lambert and then dividing // by a "rebalancing coefficient" gives a result close to plain diffuse // response lightmaps, but normalmapped. // Note that dir is not unit length on purpose. Its length is // "directionality", like for the directional specular lightmaps. half halfLambert = dot(normalWorld, dirTex.xyz - 0.5) + 0.5; return color * halfLambert / max(1e-4h, dirTex.w); }
代码的注释中提到镜面高光。 这些是支持镜面高光的光照贴图,但需要更多的纹理,使用起来也更昂贵,并且在大多数情况下没有产生良好的效果。自Unity 5.6起,它们已被删除了。
5 光照探针-Light Probes
光照贴图仅适用于静态对象,而不适用于动态对象。 因此,动态对象不适合带有烘烤光照的场景。当没有实时光源的时候,这是非常明显的。
为了更好地混合静态和动态对象,我们必须以某种方式将烘焙的光照应用于动态对象。为了解决这个问题,Unity有光照探针。 光照探针是对空间中的一个点包含该位置的光照信息。 它是用球面谐波来存储这些信息而不是用纹理。 如果可用的话,这些光照探针将用于动态对象,而不是全局环境数据。所以我们要做的就是创建一些探针,等到烘焙的时候,我们的着色器就会自动使用它们。
5.1 创建光照探针组
通过GameObject / Light /Light Probe Group将一组光探测器添加到场景中。 这将创建一个新的游戏对象,在立方体的形状中共有八个光探测器。 它们将在渲染动态对象的时候立即使用。
一个新的光探测器组
通过检视器,可以在启用“编辑探针”模式后编辑光探测器组。
5.2 放置光照探针
光照探针组将其包围的体积分成四个区域。四个探测器定义了四面体的角。 这些探测器被进行插值以确定用于动态物体的最终球谐函数,这取决于其在四面体内的位置。这意味着动态对象被视为一个单一的点,因此这种方法只对相当小的对象有效。在编辑探测器的时候,会自动生成四面体。 你不需要知道他们的配置,但它们的可视化信息可以帮助你查看探测器的相对位置。放置光照探针需要你去调整他们的位置,直到你得到一个你可以接受的结果,就像光照贴图的设置一样。首先封装将要包含动态对象的区域。
封装区域
然后根据光照条件如何变化来添加更多的探针。你不必将它们放置在静态几何中。 也不要把它们放在不透明的单面几何体错误的那一面。
放置更多的探测器
继续添加和移动探测器,直到你在所有区域都有了合理的光照条件,并且在它们之间发生的转换是可以接受的。
调整探测器的位置
可以通过移动动态对象来测试探针。当选择一个动态对象的时候,也会显示当前正在发挥作用的探针。探针将显示其光照,而不仅仅是黄色球体。你还可以看到用于动态对象的内插数据。
移动动态对象
通过不同的光照探头,物体的明暗变化明显。