翻译18 Realtime GI & LPPV & LOD

支持实时全局光照
用动画控制发光对GI的贡献
使用光照探针代理体LPPV
LOD组与GI结合
LOD之间的淡入淡出

从现在开始,这个系列教程将由Unity 2017.1.0f3来完成。它将不适用于Unity的旧版本,因为我们要使用一个新的着色器函数。

静态LOD组和实时全局光照混合后的完整预览

1 实时全局光照

  烘焙光照在静态物体上工作的非常好,对于动态几何体,由于有光照探针的缘故,烘焙光照这种方法也能工作的非常好。但是,烘焙光照不能处理动态光源。混合模式的光源可以通过一些实时的调节来消除,但调节的太多使得烘焙出来的间接光照不会改变。所以当你有一个户外场景的话,使用烘焙光照这种方法太阳的光照就不能有变化。太阳不能像在现实生活中一样在天空中移动,因为如果需要太阳在天空中移动的话,就需要逐渐变化的全局光照。所以场景必须一直不变。

  为了使间接光照能够在移动的太阳这样的情况发挥作用,Unity使用Enlighten系统来计算实时全局光照。除了在运行时计算光照和光照探针以外,它还采用烘焙间接光照一样的方式来工作。

  了解间接光需要知道光在静态表面之间如何反射。重点在于哪些表面可能会受到其他表面的影响,以及程度如何。弄清这些关系需要做很多的工作,不能实时完成。所以这个数据由编辑器处理并存储在运行时使用。然后 Enlighten系统会使用这个数据来计算实时光照贴图和探针数据。即使如此,只有低分辨率的光照贴图才可以在实时情况下运行。

1.1 启用全局光照

实时全局光照、烘焙全局光照都可以独立启用。你可以同时启用两个,或者启用其中的一个,或者两个都不启用。这两个选项都是通过“光照”窗口的“实时照光照”部分中的复选框启用。

实时全局光照和烘焙光照同时启用的状态

要实际查看实时全局光照,请将测试场景中的主光源的模式设置为实时模式。 由于我们没有其他光源,即使启用了烘焙光照也能有效地关闭。

主光源设置为实时模式

确保场景中的所有对象都使用我们的白色材质。 像上次一样,球体都是动态的,而其他的都是静态几何体。

只有动态对象能接收实时的全局光照

事实证明,只有动态对象会受益于实时全局光照。静态物体会变的暗一点。这是因为光照探针自动并入实时全局光照。而静态对象必须对实时的光照贴图进行采样,而这些光照贴图与烘焙的光照贴图不同。我们的着色器还不支持。

1.2 烘焙的全局光照

Unity在编辑模式下已经生成了实时的光照贴图,所以你可以随时查看实时的全局光照贴图。在编辑模式和播放模式之间进行切换的时候,这些贴图不会被保留,但是它们最终会得到相同的结果。你可以通过“光照”窗口的“对象贴图”选项来选择一个光照贴图静态对象对实时光照贴图进行检查。 选择“实时强度“可以可视化的查看实时光照贴图的数据。

实时光照贴图,屋顶被选中时候的状态

虽然实时光照贴图已经被烘焙出来,并且它们还可能显示正确,但我们的meta渲染通道实际上使用的是错误的坐标。实时全局光照具有自己的光照贴图坐标最终可能与静态光照贴图的坐标不同Unity会根据光照贴图和对象的设置来自动生成这些坐标。这些数据存储在第三套UV中。所以将这些数据添加到My Lightmapping中的VertexData里面。

struct VertexData {
    float4 vertex : POSITION;
    float2 uv : TEXCOORD0;
    float2 uv1 : TEXCOORD1;
    float2 uv2 : TEXCOORD2;
};

现在,MyLightmappingVertexProgram必须使用第二个或是第三个UV坐标,以及静态或动态光照贴图的大小和偏移量。 我们可以依靠UnityMetaVertexPosition函数来使用正确的数据。

Interpolators MyLightmappingVertexProgram (VertexData v) {
    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);
    i.pos = UnityMetaVertexPosition(
        v.vertex, v.uv1, v.uv2, unity_LightmapST, unity_DynamicLightmapST
    );

    i.uv.xy = TRANSFORM_TEX(v.uv, _MainTex);
    i.uv.zw = TRANSFORM_TEX(v.uv, _DetailTex);
    return i;
}

UnityMetaVertexPosition是什么样子的?

它除了通过unity_MetaVertexControl提供的标志来决定使用哪些坐标集和光照贴图之外,它还做了我们以前做的工作。

float4 UnityMetaVertexPosition (
    float4 vertex, float2 uv1, float2 uv2,
    float4 lightmapST, float4 dynlightmapST
) {
    if (unity_MetaVertexControl.x) {
        vertex.xy = uv1 * lightmapST.xy + lightmapST.zw;
        // OpenGL right now needs to actually use incoming vertex position,
        // so use it in a very dummy way
        vertex.z = vertex.z > 0 ? 1.0e-4f : 0.0f;
    }
    if (unity_MetaVertexControl.y) {
        vertex.xy = uv2 * dynlightmapST.xy + dynlightmapST.zw;
        // OpenGL right now needs to actually use incoming vertex position,
        // so use it in a very dummy way
        vertex.z = vertex.z > 0 ? 1.0e-4f : 0.0f;
    }
    return UnityObjectToClipPos(vertex);
}

请注意,meta渲染通道既用于烘焙光照贴图,也用于实时光照贴图所以当使用实时全局光照的时候,meta渲染通道也将被包含在构建中

1.3 对实时光照贴图进行采样

为了对实时光照贴图进行采样,我们还必须将第三个UV坐标添加到My Lightmapping中的VertexData里面。

struct VertexData {
    float4 vertex : POSITION;
    float3 normal : NORMAL;
    float4 tangent : TANGENT;
    float2 uv : TEXCOORD0;
    float2 uv1 : TEXCOORD1;
    float2 uv2 : TEXCOORD2;
};

当一张实时光照贴图被使用的时候,我们必须将这个光照贴图的坐标添加到我们的插值器中去。标准着色器在单个插值器中将两个光照贴图的坐标集合组合起来 - 与其他数据复用 - 但是我们可以为两者准备单独的插值器。当DYNAMICLIGHTMAP_ON关键字被定义的时候,我们知道有动态光照数据。它是multi_compile_fwdbase编译器指令的关键字列表的一部分。

struct Interpolators 
{
    …
    #if defined(DYNAMICLIGHTMAP_ON)
        float2 dynamicLightmapUV : TEXCOORD7;
    #endif
};

填充坐标就像对静态光照贴图的坐标所做的事情一样,除了动态光照图的缩放比例和偏移量的设置以外,这些可以通过unity_DynamicLightmapST变得可用。

Interpolators MyVertexProgram (VertexData v) {
    …
    #if defined(LIGHTMAP_ON) || ADDITIONAL_MASKED_DIRECTIONAL_SHADOWS
        i.lightmapUV = v.uv1 * unity_LightmapST.xy + unity_LightmapST.zw;
    #endif

    #if defined(DYNAMICLIGHTMAP_ON)
        i.dynamicLightmapUV = v.uv2 * unity_DynamicLightmapST.xy + unity_DynamicLightmapST.zw;
    #endif
    …
}

对实时光照贴图的采样是在我们的CreateIndirectLight函数中完成的。复制 #if defined(LIGHTMAP_ON) 代码块并进行一些更改。 首先,新的部分是基于DYNAMICLIGHTMAP_ON关键字的。 此外,它应该使用DecodeRealtimeLightmap而不是DecodeLightmap,这是因为实时光照贴图使用不同的颜色格式。而且因为这些数据可能被添加到烘焙光照中,不要立即分配给indirectLight.diffuse,而是使用最后添加的中间变量 最后,当不使用烘焙光照贴图和实时光照贴图的时候,我们只应该对球面谐波进行采样。

    #if defined(LIGHTMAP_ON)
        indirectLight.diffuse =    DecodeLightmap(UNITY_SAMPLE_TEX2D(unity_Lightmap, i.lightmapUV));
        #if defined(DIRLIGHTMAP_COMBINED)
            float4 lightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER(
                unity_LightmapInd, unity_Lightmap, i.lightmapUV
            );
            indirectLight.diffuse = DecodeDirectionalLightmap(
                indirectLight.diffuse, lightmapDirection, i.normal
            );
        #endif

        ApplySubtractiveLighting(i, indirectLight);
//    #else
//        indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
    #endif

    #if defined(DYNAMICLIGHTMAP_ON)
        float3 dynamicLightDiffuse = DecodeRealtimeLightmap(
            UNITY_SAMPLE_TEX2D(unity_DynamicLightmap, i.dynamicLightmapUV)
        );
        #if defined(DIRLIGHTMAP_COMBINED)
            float4 dynamicLightmapDirection = UNITY_SAMPLE_TEX2D_SAMPLER(
                unity_DynamicDirectionality, unity_DynamicLightmap,
                i.dynamicLightmapUV
            );
                   indirectLight.diffuse += DecodeDirectionalLightmap(
                       dynamicLightDiffuse, dynamicLightmapDirection, i.normal
                   );
        #else
            indirectLight.diffuse += dynamicLightDiffuse;
        #endif
    #endif

    #if !defined(LIGHTMAP_ON) && !defined(DYNAMICLIGHTMAP_ON)
        indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
    #endif

image

把实时全局光照应用于一切物体之上

现在我们的着色器使用的是实时光照贴图。最初,当使用Distance Shadowmask模式的时候,它的效果可能看起来与使用混合光源的烘焙光照的效果相同。当在播放模式下关闭光源的时候,差异就变得非常明显。

image

禁用混合光源以后,间接光照仍然被保留

禁用混合光源以后,其间接光照将保持不变。相比之下,实时光照的间接贡献就会消失,并重新出现 - 这是应该出现的情况。 不过,新情况的完全烘焙好可能需要一段时间。 Enlighten系统会逐步调整光照贴图和光照探针。 这种情况发生的速度取决于场景的复杂性和实时全局光照CPU质量设置。

realtime to static

切换实时光与实时GI

所有实时光源都对实时全局光照有贡献。 然而,它的典型用途是那些仅在主要方向上存在光线的光源,比如可以代表太阳,因为它在天空中移动。它适用于方向光源。点光源和聚光光源也能工作,但只是没有阴影。所以当使用带有阴影的点光源或聚光光源的时候,你可能会遇到不正确的间接光照结果

image image

没有影响的间接光源和实时的聚光光源

如果要从实时全局光照里面去掉一个实时光源,可以通过设置它的Indirect Multiplier将它的光强度设置为零。

1.4 自发光光源

实时全局光照也可以用于自发光的静态物体。这使得可以匹配实时间接光照来改变物体的自发光变得可能。让我们来试试看吧。向场景中添加一个静态球体,并赋予它一个使用我们着色器的材质,这个材质具有黑色的反照率和白色的自发光颜色。最初,我们只能看到通过静态光照贴图实现的自发光的间接效果。

image

用自发光球来烘焙全局光照

要将自发光光源烘焙到静态光照贴图中,我们必须在我们的着色器的GUI中设置材质的全局光照标志。因为我们总是将标志设置为BakedEmissive,光源最终将以烘焙好的光照贴图的形式出现。如果自发光光源是恒定的这个效果是很不错的,但这样就不允许我们做动画控制。

为了同时对自发光光源支持烘焙和实时光照,我们必须使其可配置化。我们可以通过向MyLightingShaderGUI中添加一个选项来做到这一点,使用的是MaterialEditor.LightmapEmissionProperty方法。这个方法的单个参数是属性的缩进级别。

    void DoEmission () {
        MaterialProperty map = FindProperty("_EmissionMap");
        Texture tex = map.textureValue;
        EditorGUI.BeginChangeCheck();
        editor.TexturePropertyWithHDRColor(
            MakeLabel(map, "Emission (RGB)"), map, FindProperty("_Emission"),
            emissionConfig, false
        );
        editor.LightmapEmissionProperty(2);
        if (EditorGUI.EndChangeCheck()) {
            if (tex != map.textureValue) {
                SetKeyword("_EMISSION_MAP", map.textureValue);
            }

            foreach (Material m in editor.targets) {
                m.globalIlluminationFlags =
                    MaterialGlobalIlluminationFlags.BakedEmissive;
            }
        }
    }

每次当自发光属性发生改变的时候,我们也必须停止覆盖这个标志位。其实真正要做的事情比这更复杂一点。其中一个标志选项是EmissiveIsBlack,这个表示表示的是自发光计算可以跳过。这个标志总是会针对新材质进行设置。要让间接自发光能够工作,我们必须保证这个标志不被设置,无论我们选择实时光照还是烘焙。我们可以通过总是屏蔽标志值的EmissiveIsBlack位来做到这一点。

foreach (Material m in editor.targets) {
    m.globalIlluminationFlags &= ~MaterialGlobalIlluminationFlags.EmissiveIsBlack;
}

image

image

带有自发光球的实时全局光照效果

烘焙全局光照和实时全局光照之间的视觉差异主要是因为实时光照贴图通常具有比烘焙全局光照更低的分辨率。所以当自发光不发生不变化的时候,你也可以使用烘焙全局光照,确保能够利用其更高的分辨率。

EmissiveIsBlack的目的是什么?

这是一个优化,使得计算可以跳过全局光照烘焙过程。然而,只有当自发光颜色确实是黑色的时候,它才依赖于标志。由于这个标志位由着色器的GUI进行设置,这是当材质在检视器里面进行编辑的时候确定的。或者至少,这是Unity的标准着色器的做法。因此,如果自发光颜色稍后被脚本或动画系统更改,则该标志位不会做相应的调整。这是许多人不理解为什么对自发光做动画不会影响到实时全局光照的原因。结果就是如果你想在运行时更改自发光颜色,那么就不要将自发光颜色设置为纯黑色。

我们没有使用这种方法,我们使用的是LightmapEmissionProperty,它还提供了对自发光完全关闭全局光照的选项。 所以这个选择对于用户来说是非常明确的,没有任何隐藏的行为。如果用户不要使用自发光? 那么只要确保它的全局光照被设置为None就可以了。

1.5 对自发光进行动画控制

用于自发光的实时全局光照只能用于静态对象。虽然物体是静态的,但其材质的自发光属性还是可以被动画化,并且将被全局光照系统所捕获到。让我们用一个在自发光颜色为白色和自发光颜色为黑色之间振荡的简单组件来尝试下这个事情。

using UnityEngine;

public class EmissiveOscillator : MonoBehaviour {
    Material emissiveMaterial;
    void Start () {
        emissiveMaterial = GetComponent<MeshRenderer>().material;
    }

    void Update () {
        Color c = Color.Lerp(
            Color.white, Color.black,
            Mathf.Sin(Time.time * Mathf.PI) * 0.5f + 0.5f
        );
        emissiveMaterial.SetColor("_Emission", c);
    }
}

将这个组件添加到我们的自发光球体。在播放模式下,自发光将会动画化,但间接光照不受影响。我们必须通知实时光照系统,它有工作要做。这可以通过调用适当网格渲染器的Renderer.UpdateGIMaterials方法来完成。

    MeshRenderer emissiveRenderer;
    Material emissiveMaterial;

    void Start () {
        emissiveRenderer = GetComponent<MeshRenderer>();
        emissiveMaterial = emissiveRenderer.material;
    }

    void Update () {
        …
        emissiveMaterial.SetColor("_Emission", c);
        emissiveRenderer.UpdateGIMaterials();
    }

realtimeGIAnimate

动画控制实时GI

调用UpdateGIMaterials方法会触发物体自发光的完整更新,并使用其meta渲染通道进行渲染。当自发光比纯色更复杂的时候,这是必要的,举个简单的例子来说,比如说我们使用纹理。如果一个纯色就足够了,那么我们可以通过使用渲染器和自发光颜色调用DynamicGI.SetEmissive方法来得到一个比较快捷的计算方式。这比使用meta渲染通道来渲染物体更快,所以在能够使用的时候可以利用这种方法。

//emissiveRenderer.UpdateGIMaterials();
DynamicGI.SetEmissive(emissiveRenderer, c);

2 光照探针

烘焙全局光照和实时全局光照都通过光照探针应用于动态对象。物体的位置用于对光探针数据进行插值,然后将其用于全局光照。这对于相当小的物体来说下效果很好,但对于较大的物体来说就太粗糙了。

举个简单的例子来是说,将做了比较大拉伸的立方体添加到测试场景,以便它可以受到不同的光照条件的影响。它应该使用我们的白色材质。由于它是一个动态立方体,所以最终使用一个点来确定它的全局光照贡献。让我们移动这个点的位置,使得这一点最终处于一个被遮蔽的位置,那么整个立方体就会变黑,这显然是错误的。为了使这一点非常明显,让我们使用一个烘焙主光源,所以所有光照都来自烘焙全局光照和实时全局光照的数据。

image

对于大型动态物体来说,光照效果不好

为了使光照探针器适用于这样的情况,我们可以使用光照探针代理体,或者简称为LPPV。这可以通过向着色器发送插值后的探针器数据网格而不是单个插值后的探针器数据来做到。这需要具有线性滤波的浮点数3D纹理,这就将这个方法限制到只能在现代显卡上使用。此外,还要确保在图形层设置中启用LPPV(光照探针代理体)支持。

image

启用了LPPV(光照探针代理体)支持

2.1 向物体中添加一个光照探针代理体

光照探针代理体可以以各种方式设置,最直接的方法是在作为使用光照探针代理体的物体的一个组件。你可以通过Component / Rendering / Light Probe Proxy Volume来添加它。

image

光照探针代理体组件

LPPV(光照探针代理体)通过在运行时在光照探针之间进行插值来工作,就好像它们是常规动态对象的网格一样。插值后得到的结果被缓存,刷新模式(Refresh Mode)控制在何时进行更新。默认值为“自动(Automatic)”,这意味着当动态全局光照更改和探针器组发生移动的时候会触发更新。包围盒模式(Bounding Box Mode)控制着代理体的定位。自动本地化(AutomaticLocal )意味着它会去匹配其附着的对象的包围盒。这些默认设置适用于我们的立方体,因此我们将保留这些设置。

要使我们的立方体实际使用LPPV(光照探针代理体),我们必须将其网格渲染器的光照探针(Light Probes)模式设置为使用光照探针代理体(Use ProxyVolume)。默认行为是使用对象本身的LPPV(光照探针代理体)组件,但也可以强制使用另一个代理体。

image

使用一个光照探针代理体而不是常规的探针器

自动分辨率模式(automaticresolution mode)对于我们的拉伸立方体不起作用。 因此,将“分辨率模式(Resolution Mode )”设置为“自定义(Custom )”,并确保立方体的角上有采样点,并沿着其长边有多个样本点。当你选中这个对象的时候,可以看到这些采样点。

image

image

自定义探针器分辨率以适应拉伸的立方体

2.2 对光照探针代理体进行采样

立方体已变黑,因为我们的着色器现在还不支持LPPV(光照探针代理体)采样。为了使其工作,我们必须在CreateIndirectLight函数内调整球面谐波代码。当使用LPPV(光照探针代理体)的时候,UNITY_LIGHT_PROBE_PROXY_VOLUME被定义为1。我们在这种情况下什么都不做,看看会发生什么。

#if !defined(LIGHTMAP_ON) && !defined(DYNAMICLIGHTMAP_ON)
    #if UNITY_LIGHT_PROBE_PROXY_VOLUME
    //...
    #else
        indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
    #endif
#endif

image

没有更多球面谐波的效果

得到的结果是所有的球面谐波被禁用,对于不使用LPPV(光照探针代理体)的动态对象也是如此。这是因为UNITY_LIGHT_PROBE_PROXY_VOLUME在项目范围内定义,而不是对每个对象实例进行定义。单个对象是否使用LPPV由UnityShaderVariables中定义的unity_ProbeVolumeParams的X分量指定。如果unity_ProbeVolumeParams的X分量设置为1,那么我们有一个LPPV(光照探针代理体),否则我们应该使用常规的球面谐波。

#if UNITY_LIGHT_PROBE_PROXY_VOLUME
    if (unity_ProbeVolumeParams.x == 1) {
        //...
    }
    else {
        indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
    }
#else
    indirectLight.diffuse += max(0, ShadeSH9(float4(i.normal, 1)));
#endif

要对光照探针代理体进行采样,我们可以使用SHEvalLinearL0L1_SampleProbeVolume函数而不是ShadeSH9。这个函数在UnityCG中进行定义,并且需要世界空间中的位置作为额外的参数。

if (unity_ProbeVolumeParams.x == 1) {
    indirectLight.diffuse = SHEvalLinearL0L1_SampleProbeVolume
    (
        float4(i.normal, 1), i.worldPos
    );
    indirectLight.diffuse = max(0, indirectLight.diffuse);
}

SHEvalLinearL0L1_SampleProbeVolume如何工作?

顾名思义,该函数仅包括前两个球面谐波带L0和L1。 Unity不使用LPPV(光照探针代理体)的第三个波带。所以我们得到较低质量的光照近似值,但是我们在多个世界空间中的样本之间进行插值,而不是使用单个点。下面是这个函数的代码。

half3 SHEvalLinearL0L1_SampleProbeVolume (half4 normal, float3 worldPos) {
    const float transformToLocal = unity_ProbeVolumeParams.y;
    const float texelSizeX = unity_ProbeVolumeParams.z;

    //The SH coefficients textures and probe occlusion
    // are packed into 1 atlas.
    //-------------------------
    //| ShR | ShG | ShB | Occ |
    //-------------------------

    float3 position = (transformToLocal == 1.0f) ?
        mul(unity_ProbeVolumeWorldToObject, float4(worldPos, 1.0)).xyz :
        worldPos;
    float3 texCoord = (position - unity_ProbeVolumeMin.xyz) *
        unity_ProbeVolumeSizeInv.xyz;
    texCoord.x = texCoord.x * 0.25f;

    // We need to compute proper X coordinate to sample. Clamp the
    // coordinate otherwize we'll have leaking between RGB coefficients
    float texCoordX =
        clamp(texCoord.x, 0.5f * texelSizeX, 0.25f - 0.5f * texelSizeX);

    // sampler state comes from SHr (all SH textures share the same sampler)
    texCoord.x = texCoordX;
    half4 SHAr = UNITY_SAMPLE_TEX3D_SAMPLER(
        unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord
    );
    texCoord.x = texCoordX + 0.25f;
    half4 SHAg = UNITY_SAMPLE_TEX3D_SAMPLER(
        unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord
    );
    texCoord.x = texCoordX + 0.5f;
    half4 SHAb = UNITY_SAMPLE_TEX3D_SAMPLER(
        unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord
    );
    // Linear + constant polynomial terms
    half3 x1;
    x1.r = dot(SHAr, normal);
    x1.g = dot(SHAg, normal);
    x1.b = dot(SHAb, normal);

    return x1;
}
View Code

image

采样后的LPPV(光照探针代理体)的效果,在伽马空间中的效果太暗

我们的着色器现在在需要的时候对LPPV(光照探针代理体)进行采样,但结果太暗了。至少在伽马颜色空间中工作就是这样的结果。这是因为球面谐波数据存储在线性空间中。因此,可能需要进行颜色的转换。

if (unity_ProbeVolumeParams.x == 1) {
    indirectLight.diffuse = SHEvalLinearL0L1_SampleProbeVolume(
        float4(i.normal, 1), i.worldPos
    );
    indirectLight.diffuse = max(0, indirectLight.diffuse);
    #if defined(UNITY_COLORSPACE_GAMMA)
               indirectLight.diffuse = LinearToGammaSpace(indirectLight.diffuse);
        #endif
}

image

采样后的LPPV(光照探针代理体)的效果,带有正确的颜色

3 LOD Groups

当一个对象最终只覆盖应用程序窗口的一小部分的时候,你不需要高度详细的网格来渲染它。你可以根据对象在视图中的大小使用不同的网格。这被称为细节层次,或简称LOD。Unity允许我们通过组件LOD组来实现这样的功能。

3.1 创建一个LOD层次结构

这个想法是你在各种不同的LOD等级使用同一网格的多个版本。最高级 - LOD 0 - 具有最多的顶点、子对象、动画、复杂的材质等。随后的级别逐渐变得更简单,更容易计算。在理想情况下,相邻的LOD等级被设计为使得当Unity从一个LOD等级切换到另一个LOD等级的时候,你不能轻易地辨别出它们之间的区别。否则突然有LOD等级变化的时候就会让人很晕。但是在研究这种技术的时候,我们会使用明显的不同的网格。

创建一个空的游戏对象并给它两个子对象。第一个子对象是标准球体,第二个子对象是标准立方体,其大小设置为0.75。 预期的结果看起来像是一个重叠的球体和立方体。

image image

球体和立方体作为一个对象

通过Component /Rendering / LOD Group将一个LOD组组件添加到父对象。你会得到一个具有默认设置的LOD组,它有三个LOD等级。 百分比是指由对象的包围盒覆盖的窗口的垂直部分。因此,当垂直尺寸下降到窗口高度的60%的时候,默认设置为切换到LOD 1,当垂直尺寸下降到窗口高度的30%的时候,默认设置为切换到LOD 2。当垂直尺寸下降到窗口高度的10%的时候,它根本不渲染。 你可以通过拖动LOD框的边来更改这些阈值。

image

组件LOD组

这些阈值由LOD偏移(LOD Bias)进行修改,LOD偏移(LOD Bias)可以在组件检视器里面查看并修改。目前使用的是质量设置为2的默认值,这意味着阈值被减半。也可以设置为最大LOD等级,这将导致跳过最高级别。

为了使其工作,你必须告诉组件每个LOD等级都会使用哪些对象。这是通过选择一个LOD块并将对象添加到其“渲染器”列表中完成的。你可以在场景中添加任何对象,但一定要确保添加其子对象到LOD块的“渲染器”列表。让LOD 0的“渲染器”使用球体,让LOD 1的“渲染器”使用立方体。我们将LOD 2的“渲染器”留空,所以我们只有两个LOD等级。如果需要的话,你可以通过右键单击上下文菜单删除并插入LOD等级。

image

让球这个子物体使用LOD 0等级

一旦配置了LOD等级,你可以通过移动相机来查看它们的效果。如果物体足够大的话,它将使用球体,否则的话它将使用立方体,或根本不会渲染。

LOD等级切换的效果如这个链接所示:https://gfycat.com/gifs/detail/ShyAffectionateFairyfly。

3.2 烘焙全局光照和LOD组

因为LOD组是如何渲染的取决于它的视图大小,所以它们自然是动态的。但是,你仍然可以使其成为静态。对整个对象层次结构执行此操作,因此也包括了根节点和它的两个子节点。然后设置主光源为烘焙光源,看看会发生什么。

image

使用烘焙光源得到的效果

看起来在烘焙静态光照贴图的时候使用的是LOD 0。 我们最终总是能够看到球体的阴影和间接光照的贡献,即使LOD组切换到一个立方体或是对自身做了剔除。但请注意,立方体也是使用了静态光照贴图。 所以它不使用光照探针,对吧? 转动光照探针组就能发现这一点。

image

没有光照探针时候的烘焙光照

禁用光探针组会使得立方体变得更暗。这意味着他们不再接受间接光照。 这是因为在烘焙过程中确定间接光照的时候使用的是LOD 0。为了找到其他LOD等级下的间接光照, Unity可以做到的最好程度是依靠烘焙光照探针。 因此,即使在运行时我们不需要光照探针,我们也需要光照探针来为我们的立方体计算间接光照。

3.3 实时全局光照和LOD组

当只使用实时全局光照的时候,方法是类似的,除了我们的立方体现在在运行时使用的是光照探针。你可以通过选择球体或立方体来验证这一点。选择立方体后,你可以看到小工具显示了哪些光照探针被使用。 球体不显示它们,因为它使用的是动态光照贴图。

image

LOD 1使用光照探针来计算实时全局光照

当烘焙全局光照和实时全局光照同时使用的时候,它会变得更加复杂。 在这种情况下,立方体应该对烘焙全局光照使用光照贴图,对实时全局光照使用光照探针。不幸的是,这是不可能的,这是因为光照贴图和和球面谐波不能同时使用。这是一个非此即彼的问题。因为光照贴图数据对于立方体来说是可用的,所以Unity最终会使用它。因此,立方体不受实时全局光照的影响。

image

仅对LOD 1等级使用烘焙光照,使用的是低强度的主光源

一个重要的细节是,烘焙的LOD等级和渲染的LOD等级是完全独立的。 他们不需要使用相同的设置。如果实时全局光照最终比烘焙全局光照更重要,你可以强制立方体使用光照探针,确保它对于光照贴图来说不是静态的,同时保持球体静止。

image

LOD 1强制使用光照探针

3.4 在不同的LOD等级切换的时候支持淡入淡出功能

LOD组这种方法的缺点是,当LOD等级发生变化的时候,它可以在视觉上很明显的表现出来。几何体会在视图中突然弹出、消失或改变形状。 这可以通过相邻LOD等级之间的淡入淡出来缓解,这通过将LOD组的渐变模式设置为淡入淡出来完成。还有另一种渐变模式,由Unity用于SpeedTree对象,我们不会使用这种模式。

当启用淡入淡出的时候,每个LOD等级都会显示一个淡入变换宽度(Fade Transition Width )字段,用于控制其块的哪个部分用于衰落。举个简单的例子来说,当设置为0.5的时候,一半LOD范围将用于淡出到下一级。或者,淡入淡出过程可以是有动画的,在这种情况下,在LOD等级之间的切换需要大约半秒钟。

image

带有0.5变换宽度的淡入淡出

当启用淡入淡出的时候,在LOD组之间进行转换的时候会同时渲染两个LOD等级。

3.5 支持淡入淡出

Unity的标准着色器在默认情况下是不支持淡入淡出的。如果想要支持支持淡入淡出的话,你必须复制标准着色器并为LOD_FADE_CROSSFADE关键字添加一个多编译指令。添加这条指令还有一个原因是为了在My First Lighting着色器里面支持淡入淡出功能。让我们将这条指令添加到除了meta渲染通道以外的所有渲染通道。

#pragma multi_compile _ LOD_FADE_CROSSFADE

我们将使用抖动来在LOD等级之间进行转换。这种方法适用于前向渲染和延迟渲染,也适用于有阴影的情况。

在创建半透明阴影的时候,我们已经使用了抖动这种方法。它需要片段的屏幕空间坐标,这迫使我们为顶点程序和片段程序使用不同的插值器结构。所以让我们复制My Lighting 中的Interpolators结构,将其重命名为InterpolatorsVertex。

struct InterpolatorsVertex {
    …
};
struct Interpolators {
    …
};

…
InterpolatorsVertex MyVertexProgram (VertexData v) {
    InterpolatorsVertex i;
    …
}

当我们必须进行淡入淡出处理的时候,片段程序的插值器里面必须包含vpos,否则我们可以使用同样的位置信息。

struct Interpolators {
    #if defined(LOD_FADE_CROSSFADE)
        UNITY_VPOS_TYPE vpos : VPOS;
    #else
        float4 pos : SV_POSITION;
    #endif
    …
};

我们可以在我们片段程序中开始的位置使用UnityApplyDitherCrossFade函数来执行淡入淡出操作。

FragmentOutput MyFragmentProgram (Interpolators i) {
    #if defined(LOD_FADE_CROSSFADE)
        UnityApplyDitherCrossFade(i.vpos);
    #endif
    …
}

UnityApplyDitherCrossFade是如何工作的?

这个函数在UnityCG中进行定义。它的方法类似于我们在《渲染12:半透明阴影》中使用的抖动方法,区别只是整个对象的抖动级别是均匀的。 因此,不需要混合抖动级别。 它使用存储在4×64大小的二维纹理中的16个抖动级别,而不是4×4×16大小的三维纹理。

FragmentOutput MyFragmentProgram (Interpolators i) {
    #if defined(LOD_FADE_CROSSFADE)
        UnityApplyDitherCrossFade(i.vpos);
    #endif

    …
}

unity_LODFade变量在UnityShaderVariables中进行定义。它的Y分量包含的是对象的渐变量,共有十六步。

image

通过抖动方法得到的淡入淡出几何体

淡入淡出现在可以在几何体上正常工作了。为了使其适用于阴影,我们必须调整My Shadows着色器。 首先,当我们进行淡入淡出处理的时候,必须使用vpos。其次,我们还必须在片段程序开始的位置使用UnityApplyDitherCrossFade函数。

struct Interpolators {
    #if SHADOWS_SEMITRANSPARENT || defined(LOD_FADE_CROSSFADE)
        UNITY_VPOS_TYPE vpos : VPOS;
    #else
        float4 positions : SV_POSITION;
    #endif
    …
};
…
float4 MyShadowFragmentProgram (Interpolators i) : SV_TARGET {
    #if defined(LOD_FADE_CROSSFADE)
        UnityApplyDitherCrossFade(i.vpos);
    #endif
    …
}

image lod fade

对几何体和阴影都做了淡入淡出处理

因为立方体和球体相互交叉,所以我们在对它们做淡入淡出处理的时候,得到一些奇怪的自阴影效果。这对于看到淡入淡出处理能在阴影上起作用是很方便的,但是当你为实际游戏创建LOD几何体的时候,需要注意这些瑕疵。

原文地址:https://www.cnblogs.com/baolong-chen/p/13034495.html