翻译16 Static Lighting-烘焙光照

1 光照贴图-Lightingmapping

实时光照计算的开销很是昂贵。根据翻译13,延迟渲染容许程序员使用的光源能够多于Forward渲染,但阴影的开销仍然对性能有一个限制。若是咱们的场景是动态的,那么没有办法来避免执行这些计算。可是若是光源和几何物体位置都是不变的,那么咱们能够只计算一次光照并重复使用。这使得咱们能够在场景中放置许多光源,而没必要在运行的时候再渲染它们。这种方法也可使用那些不能用做实时光源的区域光源(area lighting)。程序员

在本教程中,会将全部内容都放在光照贴图中,因此根本不会有任何动态光照。算法

为了尝试光照贴图,我建立了一个简单的测试场景,它具备一个简单的结构,能够提供阴影,还有一些放置在其内部的球体。一切物体都使用默认的Unity材质。app

image

针对光照贴图的一个测试场景dom

1.1 烘焙光源-Baked Lights

要开始使用光照贴图,将惟一的光源对象的模式改成“Baked(烘焙)”而不是“Realtime(实时)”。编辑器

image

使用烘焙模式的主方向光源ide

将主方向光源变成烘培光源后,就不被归入动态光照计算。从动态对象的角度来看,光源是不存在的。 惟一仍然不变的是环境光照,它仍然是基于主方向光源的。函数

image

没有直接光照的效果性能

要实际启用光照贴图,请在lighting窗口的“混合光照(Mixed Lighting)”中打开“烘培全局光照(BakedGlobal Illumination)”。 而后将光照模式设置为“烘培间接光照(BakedIndirect)”。 尽管它的名字说的是烘培间接光照,可是它也包括了直接光照。 它一般用于向场景添加间接光照。另外,确保实时全局光照(Realtime Global Illumination)被禁用,由于咱们尚未支持到这一点。测试

image

烘培间接光照模式this

1.2 静态几何体

场景的对象都应该是固定的:它们位置永远不会移动。要将这一个信息传达给Unity,请将这些对象标记为静态。你能够经过启用检视器窗口右上角的“静态”切换键来作到这一点。

光源也必须被标记为静态吗?不需。光源只须要设置为适当的模式。

有各类子系统关心物体是不是静态的。“静态(static)”还有一个下拉菜单,你可使用它来微调哪些系统会将这个对象视为静态的。如今咱们只关心光照贴图,但最简单的作法是使一切都彻底是静态的。

image

静态标签设定

一个物体对于光照贴图来讲是不是静态的,也能够经过其网格渲染器的检视器来进行查看和编辑。

image

对于光照贴图来讲是静态的物体

如今,全部的物体都是静态的,它们将被包含在光照贴图的处理过程当中。

image

使用烘焙光照的场景

必须注意使用光照贴图获得的结果不如使用实时照明获得的结果亮度那么高。这是由于缺失了镜面高光,只剩下了漫反射光照。镜面高光取决于视角,所以取决于相机的角度。正是因为相机是移动的,所以它不能包含在光照贴图中。(使用场景推荐)这种限制意味着光照贴图能够用于微弱的光线和暗淡的表面,但不能用于强直射光或有光泽的表面。若是你想要镜面高光,你将不得不使用实时光源。因此你常常会使用烘烤光源和实时光源的混合。

为何没有当即获得烘焙光源?
为了确保在须要的时候光照贴图能够实际生成和更新,请在光照窗口的底部启用“自动生成(Auto Generate)”。 不然,你必须手动生成新的光照贴图。

image

自动烘焙

1.3 光照贴图设置-Lightingmapping Setting

光照烘焙窗口包含专门用于光照贴图设置的部分。在这里,你能够在质量尺寸烘烤时间之间取得平衡。你还能够在光照贴图烘焙算法引擎EnlightenProgressive lightmapper之间进行切换。后者会增量地生成光照贴图,优先考虑场景视图中可见的内容,这在编辑的时候很方便。本教程中使用的是Enlighten光照贴图引擎。

image

默认的光照贴图设置

在作任何事情以前,请将“DirectionalMode“设置为”Non-Direction“。 稍后咱们会处理其余模式。

image

使用“Non-directional”模式的光照贴图

烘烤的光照存储在纹理中。 你能够经过将光照窗口从“场景(Scene)“切换到”全局地图(Global Maps)“模式来进行查看。 使用默认设置,个人测试场景很容易与一张1024×1024贴图相匹配。

image

获得光照贴图

Unity自带的Objects物体都有用于光照贴图的UV坐标。对于手动导入的模型,能够本身提供UV坐标,也可让Unity生成。烘烤后能够在光照贴图中看到展开的纹理。它们须要多少空间取决于场景中物体的大小和光照贴图的分辨率设置。 若是质量要求高分辨率太大,一张贴图涨不下,Unity会建立额外的贴图存储,直至完成。

image image

光照贴图的分辨率的不一样会带来很大的差别

对于每一个项目来讲,最佳设置都是不一样。 你必须不断的调整烘焙参数,直到达成很好的效果及平衡。须要注意的是,视觉质量也很大程度上取决于用于光照贴图的纹理展开的质量。不存在纹理接缝可能会产生明显的瑕疵。Unity的默认球体就是一个很好的例子。它不适用于光照贴图。

1.4 间接光源

烘焙光照会失去镜面高光,只能得到的是间接光照,它是在到达人眼以前会在多个表面反射的光。烘焙光会在拐角周围区域反射,那些原本会被遮挡的区域仍然会被照亮。咱们不能实时计算镜面高光这个信息(本节1.2有说明),可是咱们能够在烘焙的时候包括反射光。

要清楚地看到实时光照和烘培光照之间的差别:将环境光照的强度设置为零,去掉天空盒的影响,全部的光都只是来自方向光。比对

image image

没有环境光照,realtime vs. lightmapped

每次光子反射的时候,它都会失去一些能量,它会被一些须要的材质采样着色。Unity在烘焙间接光照的时候,物体会根据附近的颜色进行着色。

image image

绿色的地面,realtime vs. lightmapped

自发光表面也会影响烘焙光照。它们会成为间接光源。

image image

自发光的地面,realtime vs. lightmapped

间接光照的一个特殊设置是AO环境遮挡:这是指在角落和转折中发生的间接光照形成的阴影。这是一种人为的提高,能够加强深度方面的视觉。

image

image

使用环境遮挡的效果

环境遮挡效果彻底基于物体表面。它不考虑光线实际来自哪里。烘焙时并不老是正确,举个简单的例子:当与自发光表面组合的时候就会产生一些错误的结果。

image

显然是错误的环境遮挡效果

1.5 透明度-Transparency

光照贴图在必定程度上能够处理半透明表面。 光将经过它们,尽管光的颜色不会被它们所过滤。

image

半透明的屋顶

镂空材质也能够在光照贴图中正常工做。

image

镂空的屋顶

但 这仅在使用封闭曲面的时候有效。当使用像是quad这样的单面几何,光线将在不存在的一面损坏。当另一面没有任何东西的时候,这是很好的,可是当使用单面透明表面的时候会致使问题

image

四边形上有一个错误

为了处理这个问题,必须告诉光照贴图系统将这些表面视为透明的。 这能够经过自定义光照贴图设置完成

一、经过Asset / Create / Lightmap参数来建立这些数据。这些资源容许你自定义每一个对象的光照贴图计算。在这种状况下,咱们只想代表咱们正在处理一个透明的对象。因此启用“它是透明的(Is Transparent)“。 下面它是一个全局做用预计算实时全局光照(Precomputed Realtime GI)部分中的一部分,它会影响全部烘烤光照。

image

指示这是透明的

二、单独设置:经过物体的网格渲染器检视器来选择它们。你的资源名字将显示在Lightmap参数的下拉列表中。

image image

为透明四边形使用自定义参数

将物体标记为透明也会改变它对间接光照的贡献。透明物体让间接光经过,而不透明物体则会阻挡间接光。

2 使用光照贴图

如今咱们知道光照贴图是如何工做的,咱们能够为Shader着色器添加对光照贴图的支持。第一步是对光照贴图进行采样。调整场景中的球体,以便咱们的着色器使用白色材质。

image

使用咱们的白色材质的球体

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_ONLIGHTMAP_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、uv一、uv二、uv3;C#中是UV、UV二、UV三、UV4

image

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;
}
为何indirectLight.diffuse的值被赋值而不是加起来?光照贴图历来没有与顶点光源组合起来。

unity_Lightmap的确切形式取决于目标平台。 它被定义为UNITY_DECLARE_TEX2D(unity_Lightmap)。要对它进行采样,咱们将使用UNITY_SAMPLE_TEX2D宏而不是tex2D。这是根据不一样平台决定。

indirectLight.diffuse = UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV);

image

使用原始光照图数据的效果

咱们如今获得了烘焙的间接光照,但效果看起来不对。这是由于光照贴图数据已被编码。颜色以RGBM格式或是半强度格式进行存储,以支持高强度的光。UnityCG的DecodeLightmap函数负责为咱们解码。

indirectLight.diffuse = DecodeLightmap
(
    UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV)
);

image

使用解码后光照图数据的效果


3 建立光照贴图

目前,光照贴图会将场景对象老是视为不透明和纯白色的物体。咱们必须对咱们的着色器进行一些调整,添加一个渲染通道来彻底支持光照贴图。

从如今开始,对场景中的全部对象使用咱们本身的着色器。也再也不使用默认的材质。

3.1 半透明的阴影-Semitransparent Shadow

光照贴图不使用实时渲染管道,所以现有自写的shader不能支持。 当尝试使用半透明阴影的时候,这是最明显的。经过设置屋顶立方体材质的色调alpha份量小于1来赋予屋顶立方体半透明度。

image

半透明的屋顶,效果不正确

光照贴图仍然把屋顶当作是实心物体,这是不正确的。它使用材质的渲染类型来肯定如何处理表面,这应该告诉光照贴图咱们的对象是半透明的。事实上,它确实知道屋顶是半透明的,它只是把它看做是彻底不透明的而已。这是由于采用Unity的命名约定_Color材质属性的alpha组件以及主纹理来设置不透明度

用_Color替换_Tint。

Properties
{
//    _Tint ("Tint", Color) = (1, 1, 1, 1)
    _Color ("Tint", Color) = (1, 1, 1, 1)
    …
}

而后,为了保证咱们的着色器的功能,咱们还必须在shader文件、cg文件替换,并且咱们还要调整GUI拓展。

image

半透明的屋顶,正确的效果

3.2 镂空部分的阴影-Cutout Shadow

镂空部分的阴影也有相似的问题。光照贴图程序指望透明度的阈值存储在_Cutoff属性中,可是咱们使用的是_AlphaCutoff。 所以,它使用默认阈值1。

image

镂空的屋顶,效果不正确

解决方案是再次采用Unity的命名约定_Cutoff材质属性。因此替换shader、cg文件、GUI拓展。

image

镂空的屋顶,正确的效果

3.3 添加一个Meta渲染通道-Add Meta Pass

渲染光照贴图正确的表面反照率和自发光

image

绿色的地板,效果不正确

要采样物体的表面颜色,光照贴图程序会将它的光照模式设置为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_PASSDEFERRED_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;
}
View Code

image

间接光照设置为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);
}

image

间接光照着色的效果

但自发光光照可能尚未出如今光照贴图中。这是由于光照贴图程序并不老是包含一个自发光光照的渲染通道。材质必须代表它们具备自发光光照属性,以对烘焙过程作出贡献。这是经过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如今看起来能够正常工做了,但它与标准着色器的结果不彻底匹配。 当使用平滑度很是低的有色金属的时候,物体表面不太明亮

image image

粗糙的绿色金属,standard vs. our

标准着色器经过将反射率的一部分加到镜面高光颜色进行补偿(高亮)。它使用UnityStandardBRDF.cgincSmoothnessToRoughness函数来肯定基于平滑度的粗糙度值,将其缩小一半,并使用它来缩放镜面高光颜色。

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);
}

image

调整反照率后的效果

4 方向光照贴图-Directinal Lightmap

光照贴图程序只使用物体的顶点数据,不考虑物体的法线贴图。光照贴图的分辨率过低,没法捕获由典型法线贴图提供的细节。这意味着静态光照将是平坦的。当使用具备法线贴图的材质的时候,这变得很是明显。

image image

使用了法线贴图,standard vs. our

当从实时光照切换到烘焙光时,法线贴图的影响几乎彻底消失。这是由于它要求环境反射才能看到它们。

4.1 方向性-Directionality

经过将“DirectionalMode”改回“Directional”,可让法线贴图与烘焙光照一块儿工做。

image

再次启用定向光照贴图

当使用方向光照贴图的时候,Unity将建立两个贴图。第一张贴图包含一般的光照信息,称为强度图。 第二张贴图被称为方向图。 它包含大部分烘烤光来自的方向。

image

强度图和方向图

当方向图可用的时候,用它来对烘焙光进行简单的漫反射阴影计算。这使得它可用于法线贴图之上。注意,只有一个光方向是已知的,因此阴影将是一个近似。至少有一个主方向光照的时候,结果就会很好。

4.2 对方向进行采样

当方向光照贴图可用的时候,Unity将使用LIGHTMAP_ONDIRLIGHTMAP_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 使用方向贴图

要使用方向:一、解码 二、对法向量执行点积,找到漫反射因子并将其应用于颜色。

可是方向贴图并无包含单位长度的方向,而是比单位长度的方向会大一些。 幸运的是可使用UnityCG的DecodeDirectionLightmap函数来解码方向数据。

float4 lightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER
(
    unity_LightmapInd, unity_Lightmap, i.lightmapUV
);

indirectLight.diffuse = DecodeDirectionalLightmap
(
    indirectLight.diffuse, lightmapDirection, i.normal
);

image

使用带有方向的光照贴图的效果

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

光照贴图仅适用于静态对象,而不适用于动态对象。 所以,动态对象不适合带有烘烤光照的场景。当没有实时光源的时候,这是很是明显的。

image

为了更好地混合静态和动态对象,咱们必须以某种方式将烘焙的光照应用于动态对象。为了解决这个问题,Unity有光照探针。 光照探针是对空间中的一个点包含该位置的光照信息。 它是用球面谐波来存储这些信息而不是用纹理。 若是可用的话,这些光照探针将用于动态对象,而不是全局环境数据。因此咱们要作的就是建立一些探针,等到烘焙的时候,咱们的着色器就会自动使用它们。

5.1 建立光照探针组

经过GameObject / Light /Light Probe Group将一组光探测器添加到场景中。 这将建立一个新的游戏对象,在立方体的形状中共有八个光探测器。 它们将在渲染动态对象的时候当即使用。

image

一个新的光探测器组

经过检视器,能够在启用“编辑探针”模式后编辑光探测器组。

image

5.2 放置光照探针

光照探针组将其包围的体积分红四个区域。四个探测器定义了四面体的角。 这些探测器被进行插值以肯定用于动态物体的最终球谐函数,这取决于其在四面体内的位置。这意味着动态对象被视为一个单一的点,所以这种方法只对至关小的对象有效。在编辑探测器的时候,会自动生成四面体。 你不须要知道他们的配置,但它们的可视化信息能够帮助你查看探测器的相对位置。放置光照探针须要你去调整他们的位置,直到你获得一个你能够接受的结果,就像光照贴图的设置同样。首先封装将要包含动态对象的区域。

image

封装区域

而后根据光照条件如何变化来添加更多的探针。你没必要将它们放置在静态几何中。 也不要把它们放在不透明的单面几何体错误的那一面。

image

放置更多的探测器

继续添加和移动探测器,直到你在全部区域都有了合理的光照条件,而且在它们之间发生的转换是能够接受的。

image

调整探测器的位置

能够经过移动动态对象来测试探针。当选择一个动态对象的时候,也会显示当前正在发挥做用的探针。探针将显示其光照,而不只仅是黄色球体。你还能够看到用于动态对象的内插数据。

lightProbeMove

移动动态对象

经过不一样的光照探头,物体的明暗变化明显。