深入了解Unity的光照贴图(一)

深入了解Unity的光照贴图(一)

前言

在开发手机游戏时,为了兼顾画面表现力和游戏运行效率,很多时候我们会选择使用光照贴图来作为场景的照明方案。Unity引擎为我们提供了功能强大的光照贴图功能,让美术人员可以在Unity编辑器里方便的烘焙光照贴图。

但是即便如此,在实际使用的过程中,还是会因为对Unity引擎的了解不足,导致遇到一些问题。常见的比如,希望在自己写的Shader里使用Unity烘焙的光照贴图,或者是希望用Prefab来保存游戏场景,而不使用Unity的Scene来保存场景,这时该怎么处理。又或者是在Windows上显示正常的光照贴图,切换到移动平台后出现亮度丢失的情况。

下面我会逐步介绍Unity光照贴图的相关知识,在过程中逐个解答以上的问题。

1. Unity的光照贴图和相关的数据

1.1 Unity的光照贴图

当Unity完成了光照贴图的烘焙时,按照不同的设置,最多会生成三种不同的光照贴图。其中以_light结尾的是光照贴图,以_dir结尾的是平行光的方向图,以_shadowmask结尾的是ShadowMask的阴影通道图。我们暂时先只关注_light结尾的光照图,另外两种暂时先不管。

1.2 Unity光照贴图的三种品质

在用Unity烘焙光照贴图时,可以选择高、中、低三种品质,这里我们来了解一下这个品质设置的具体作用。当选择High Quality时,生成的光照贴图格式是浮点型的HDR贴图,在Windows平台下是BC6H(Direct3D11支持的一种压缩格式)。当选择Normal Quality时,生成的光照贴图是RGBM编码的32位贴图,当选择Low Quality时,生成的是被称为DLDR的32位贴图。

因为在移动设备上不支持BC6H的压缩格式,需要用其他格式来压缩浮点型的HDR贴图,会导致光照贴图的容量变大。如果在Windows平台使用了BC6H格式的贴图,直接切换到ios或安卓平台后,贴图会直接被转换成pvrtc4或etc2的压缩格式,从而导致亮度的丢失。

为了避免这种情况,我们一般在烘焙光照贴图时,会选择Normal Quality这一档,对应生成的是RGBM编码格式的光照贴图。

1.3 RGBM编码格式的光照贴图

RGBM的编码方式其实并不复杂,我们可以看一下Unity内置管线里对应的Shader代码。

inline half3 DecodeLightmapRGBM (half4 data, half4 decodeInstructions)
{
    // If Linear mode is not supported we can skip exponent part
    #if defined(UNITY_COLORSPACE_GAMMA)
    # if defined(UNITY_FORCE_LINEAR_READ_FOR_RGBM)
        return (decodeInstructions.x * data.a) * sqrt(data.rgb);
    # else
        return (decodeInstructions.x * data.a) * data.rgb;
    # endif
    #else
        return (decodeInstructions.x * pow(data.a, decodeInstructions.y)) * data.rgb;
    #endif
}

这个函数里,我们可以把注意力放在中间的这行上,

return (decodeInstructions.x * data.a) * data.rgb;

这是Unity引擎在Gamma space下对RGBM格式光照贴图的解码方式,其中decodeInstructions对应的是unity_Lightmap_HDR这个4维向量。在百度查阅RGBM编解码后,我们可以知道这里的x分量表示的是Unity生成的HDR光照贴图里的最大光照范围。

1.4 其他相关的数据

首先来看一下Lighting面板,在这里我们能看到刚才烘焙后生成的所有光照贴图。查阅Unity文档,我们可以知道这些贴图是在UnityEngine.LightmapSettings.lightmaps里记录的。

再来看一下几个用到光照贴图的模型的Mesh Renderer,在这里我们能看到这个模型用到了哪一张光照贴图,以及对应的UV scale和offset。

这些就是除了光照贴图之外,Unity里记录的和光照贴图相关的数据了,后面我们会需要用到这些数据。

2. 用Prefab替代Scene来保存场景

某些情况下,我们会希望用Prefab来保存各种游戏场景,通过切换不同的Prefab来实现游戏内场景的切换。同时,也希望能使用Unity烘焙的光照贴图,在切换不同的Prefab时,能显示对应的光照贴图效果。

为此,我们需要在各个代表场景的Prefab里记录对应的光照贴图信息,最直接的方法就是通过绑定在Prefab上的Mono脚本来记录相关信息。

2.1 通过Mono脚本来记录光照贴图的信息

在通过上一节的分析后,我们知道需要记录以下信息:

  • 生成的所有光照贴图的Texture对象
  • 对于场景中所有使用光照贴图的模型,需要保存该模型用到的光照贴图索引,以及光照贴图UV的scale和offset
  • unity_Lightmap_HDR这个4维向量

在明确了需要保存的信息后,我们可以很容易的通过Mono脚本来获取并记录这些信息。首先,定义用来保存光照贴图信息的数据结构和对应的成员变量。

[System.Serializable]
struct RendererInfo
{
    public Renderer Renderer;
    public int LightmapIndex;
    public Vector4 LightmapScaleOffset;
}

[System.Serializable]
struct LightmapInfo
{
    public Texture2D Color;
    public Texture2D Dir;
    public Texture2D ShadowMask;
}

[SerializeField]
Vector4 _lightmapHDR;

[SerializeField]
LightmapInfo[] _lightmapInfos;

[SerializeField]
RendererInfo[] _rendererInfos;

然后,通过访问LightmapSettings来获取Unity生成的光照贴图,再通过遍历所有的Mesh Renderer,来记录光照贴图的索引和UV的scale和offset。

[ContextMenu("Save Lightmap Settings")]
public void SaveLightmapSettings()
{
    _lightmapInfos = null;
    _rendererInfos = null;

    if (LightmapSettings.lightmaps != null)
    {
        _lightmapInfos = LightmapSettings.lightmaps.Select(p => _CreateLightmapInfo(p)).ToArray();

        var renderers = GetComponentsInChildren<Renderer>();

        if (renderers != null)
        {
            _rendererInfos = renderers.Select(p => _CreateRendererInfo(p)).ToArray();
        }

        _lightmapHDR = Shader.GetGlobalColor("unity_Lightmap_HDR");
    }
}

LightmapInfo _CreateLightmapInfo(LightmapData lightmapData)
{
    var info = new LightmapInfo();

    info.Color = lightmapData.lightmapColor;
    info.Dir = lightmapData.lightmapDir;
    info.ShadowMask = lightmapData.shadowMask;

    return info;
}

RendererInfo _CreateRendererInfo(Renderer renderer)
{
    var info = new RendererInfo();

    info.Renderer = renderer;
    info.LightmapIndex = renderer.lightmapIndex;
    info.lightmapScaleOffset = renderer.lightmapScaleOffset;

    return info;
}

2.2 通过Mono脚本来应用记录在Prefab里的光照信息

这里假设我们是使用Unity的Standard Shader,当我们加载了一个Prefab,并希望能显示记录的光照贴图效果,我们需要将记录在Mono脚本里的这些信息重新写回到Unity的LightmapSettings和Mesh Renderer里。

LightmapData _CreateLightmapData(LightmapInfo lightmapInfo)
{
    var data = new LightmapData();

    data.lightmapColor = lightmapInfo.Color;
    data.lightmapDir = lightmapInfo.Dir;
    data.shadowMask = lightmapInfo.ShadowMask;

    return data;
}

[ContextMenu("Apply Lightmap Settings")]
public void ApplyLightmapSettings()
{
    if (_lightmapInfos == null || _lightmapInfos.Length == 0)
        return;

    LightmapSettings.lightmaps = _lightmapInfos.Select(p => _CreateLightmapData(p)).ToArray();

    Shader.SetGlobalColor("unity_Lightmap_HDR", _lightmapHDR);
}

[ContextMenu("Apply Lightmap Settings For Standard Shader")]
public void ApplyLightmapSettingsForStandardShader()
{
    ApplyLightmapSettings();

    if (_rendererInfos == null || _rendererInfos.Length == 0)
        return;

    Shader standard = Shader.Find("Standard");
    
    foreach (var info in _rendererInfos)
    {
        var renderer = info.Renderer;

        renderer.lightmapIndex = info.LightmapIndex;
        renderer.lightmapScaleOffset = info.lightmapScaleOffset;

        var count = renderer.sharedMaterials.Length;

        for (var i = 0; i < count; i++)
        {
            renderer.sharedMaterials[i].shader = standard;
        }
    }
}

通过运行脚本函数ApplyLightmapSettingsForStandardShader,我们可以将光照贴图的信息重新写回到LightmapSettings和Mesh Renderer里。

3. 在自己写的Shader里使用Unity的光照贴图

出于种种原因,在很多时候我们并不想直接使用Unity内置的Standard Shader,但同时我们又希望使用Unity的光照贴图。这种情况下,我们可以通过预处理的方法来修改场景模型的材质,使我们的Custom Shader能正常的显示Unity的光照贴图效果。

3.1 给Custom Shader设置正确的Lightmap

在弄清楚了怎么在Prefab里记录Unity生成的Lightmap以后,这个问题就变得非常简单了。

首先,我们可以遍历Prefab里记录的所有Renderer信息,获取到每个Renderer对应的LightmapIndex。用这个来索引记录的Lightmap信息数组,获取到对应的Lightmap对象,并将这个Texture对象直接传递给Custom Shader。

3.2 给Custom Shader设置Lightmap的scale和offset

在获取到Lightmap对象的同时,我们也可以获取到UV的scale和offset信息,同样一起传递给Custom Shader。

3.3 用正确的方式来采样Lightmap

除了Lightmap和UV,我们还需要将光照贴图的最大亮度也传给Shader,这样才能在Shader里正确解码RGBM格式的光照贴图。

这里我们只关注Gamma空间的采样方式,Linear空间的采样方式可以参考Unity内置的Shader代码。以下是PS里的公式:

BakedColor = LightmapColor.rgb * LightmapColor.a * _lightmap_HDR.x;

FinalColor = BakedColor * MainTexColor

这里的LightmapColor是采样光照贴图获取到的颜色,_lightmap_HDR是我们记录在Prefab里的4维向量,在Gamma空间下,我们只用到了x分量,也就是最大亮度范围。

因为相对简单,所以我就不给再给出详细的Shader代码了,相信大家都可以自己搞定。

4. 小结

到此,我们文章开头中提到的三个问题基本已经都有了答案,我们再做个简单的小结。我们先介绍了Unity光照贴图的三种不同格式,以及LightmapSettings和Mesh Renderer里记录的相关信息,再介绍了怎么用Mono脚本在Prefab里记录这些信息。最后我们介绍了怎么在自己编写的Shader里使用这些信息,以及怎么处理RGBM格式的光照贴图。这些便是本文的全部内容了。

希望本文能帮助到所有正在学习或使用Unity引擎的小伙伴们,如果大家喜欢,请积极给我点赞哦,在大家的鼓励下我会更有动力和大家一起分享我的一些经验心得。如果文章中有错漏的地方,也希望大家能够给我指出,我会及时更新的。谢谢大家。

发布于 2020-06-14 22:53