在本文,笔者讲简单的讲一讲怎么在 Unity 中实现黑夜白天的一键切换。

演示:
场景中的灯效,除了直射光其他均是烘焙出来的。

色彩太丰富,实在不敢录太长时间
原理:
如果想要实现上面的这个黑夜白天切换效果,控制好以下几点就行了:
- 保证烘焙黑夜和白天光照贴图使用的是同一个场景(Ctrl+D挺好用)。
- 动态切换 黑夜和白天的 光照贴图设置(LightmapSettings)。
- 动态修改天空盒白天和黑夜对应的天空盒,或者像上面演示的那样:仅仅修改天空盒曝光值。
- 修改直射光的强度(如果有必要,可以修改直射光的角度)。
- 如果像演示中的镜面建筑,加了光照探针(ReflectionProbe)的需要指定对应的烘焙OK的贴图。
- 关于黑夜白天差异性的游戏对象:
- 不参与烘焙的,比如上面的效果晚上多加的 Lens 辉光,直接白天显示隐藏晚上显示即可。
- 参与烘焙的,可以直接使用显示 / 隐藏操作。
- 参与烘焙的,也可以存为预制体动态加载,但该方案要写点代码保证游戏对象上烘焙的光照信息能被加载出来。
示例:
using System.Linq;
using UnityEngine;
using DG.Tweening;
public class TestSwitchBakedLightMap : MonoBehaviour
{
[Header("Common Configuration and Component ")]
public ReflectionProbe reflectionProbe;
public Light directLight;
public GameObject effectLights; //晚上要打灯,白天不打灯
[Header("Configuration and Component For Day"), Space(10)]
public LightDataConfiguration dayConfig;
public Cubemap dayReflect;
public float daySkyboxExposure = 1f;
public float directLightDayIntensity = 1;
public float dayLightIntensityMultiplier = 1;
[Header("Configuration and Component For Night"), Space(10)]
public LightDataConfiguration nightConfig;
public Cubemap nightReflect;
public float nightSkyboxExposure = 0.2f;
public float directLightNightIntensity = 0.2f;
public float nightLightIntensityMultiplier = 0.3f;
// private fields
LightmapData[] day;
LightmapData[] night;
bool isDay = true;
private void Start()
{
night = nightConfig.Lightmaps
.Select(x => new LightmapData() { lightmapColor = x.lightmapColor, shadowMask = x.shadowMask })
.ToArray();
day = dayConfig.Lightmaps
.Select(x => new LightmapData() { lightmapColor = x.lightmapColor, shadowMask = x.shadowMask })
.ToArray();
RenderSettings.skybox.SetFloat("_Exposure", daySkyboxExposure);
reflectionProbe.customBakedTexture = dayReflect;
}
void OnGUI()
{
if (GUILayout.Button(isDay ? "To Night" : "To Day"))
{
SwithDayAndNight();
}
}
public void SwithDayAndNight()
{
if (!isDay)
{
// 切换为白天的配置
LightmapSettings.lightmaps = day;
directLight.DOIntensity(directLightDayIntensity, 0.5f);
DOTween.To(()=> RenderSettings.ambientIntensity, y=> { RenderSettings.ambientIntensity = y; },dayLightIntensityMultiplier,0.5f);
RenderSettings.skybox.DOFloat(daySkyboxExposure, "_Exposure", 0.5f);
effectLights.SetActive(false);
reflectionProbe.mode = UnityEngine.Rendering.ReflectionProbeMode.Custom;
reflectionProbe.customBakedTexture = dayReflect;
}
else
{
// 切换为晚上的配置
LightmapSettings.lightmaps = night;
directLight.DOIntensity(directLightNightIntensity, 0.5f);
DOTween.To(()=> RenderSettings.ambientIntensity, y=> { RenderSettings.ambientIntensity = y; },nightLightIntensityMultiplier,0.5f);
RenderSettings.skybox.DOFloat(nightSkyboxExposure, "_Exposure", 0.5f);
effectLights.SetActive(true);
reflectionProbe.customBakedTexture = nightReflect;
}
isDay = !isDay;
}
}
Tips:
- 仅仅是将原理中提到的几点找到对应的 API 通过代码动态修改罢了,仅供熟悉API.
- 使用了 DoTween 的非扩展方法形式的缓动,可以熟悉下。
- 使用了 DoTween 的扩展方法形式对天空盒(材质球)的曝光值进行了缓动。
- 使用了 ScriptableObject 分别保存的黑夜/白天的光照贴图设置信息,只需要 lightmapColor 和 shadowMask 两组信息,使用 ScriptableObject 在本示例中的好处可要好好体会体会哈~
- 演示中没有出现原理中第六条第三点提到的问题,所以示例代码没做演示。
扩展阅读:
Unity5.x场景优化之动态设置光照贴图lightmap - yuyingwin的专栏 - CSDN博客
↑怎么让动态加载的游戏对象实例化后自动加载光照贴图效果:↑[Unity3D]Lightmapping使用及动态加载lightmap方案 - bread's code - CSDN博客
↑更多关于光照贴图+光照探针的参考资料↑
转载请注明出处,谢谢!
LightDataConfigurationEditor
using Malee;
using System;
using UnityEngine;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor;
[CustomEditor(typeof(LightDataConfiguration))]
public class LightDataConfigurationEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
if (GUILayout.Button("StoreCurrentLightData"))
{
(target as LightDataConfiguration).StoreCurrentLightmaps();
EditorUtility.SetDirty(target);
}
}
}
#endif
/// <summary>
/// 抓取当前的灯光贴图
/// </summary>
[CreateAssetMenu(fileName = "LightDataConfiguration", menuName = "Configration/LightDataConfiguration")]
public class LightDataConfiguration : ScriptableObject
{
[Reorderable, SerializeField]
public LightMapInfoArray Lightmaps;
public string label = "Solid";
public bool forceOverride = false;
public void StoreCurrentLightmaps()
{
if (string.IsNullOrEmpty(label))
{
throw new InvalidOperationException("非法操作:未指定标签!");
}
LightMapInfo info = Lightmaps.Find(v => v.name == label);
if (null != info && !forceOverride)
{
throw new InvalidOperationException($"非法操作:已存在的条目 {label},强制覆盖请勾选 ForceOverride");
}
if (null == info)
{
info = new LightMapInfo { name = label, Lightmaps = new LightMapPairArray() };
Lightmaps.Add(info);
}
else
{
info.Lightmaps.Clear();
}
foreach (var item in LightmapSettings.lightmaps)
{
info.Lightmaps.Add(new LightMapPair { lightmapColor = item.lightmapColor, shadowMask = item.shadowMask });
}
Debug.Log($"数据装载完毕,共计:{LightmapSettings.lightmaps.Length}个");
}
public void Apply(string name)
{
LightMapInfo info = Lightmaps.Find(v => v.name == name);
if (null == info )
{
throw new InvalidOperationException($"非法操作:不存在此数据 {name}!");
}
LightmapSettings.lightmaps = info.Lightmaps
.Select(v=>new LightmapData { lightmapColor=v.lightmapColor, shadowMask = v.shadowMask })
.ToArray();
}
public void Apply(int index)
{
if (index<0||index>=Lightmaps.Count)
{
throw new InvalidOperationException($"非法操作:指定的索引超 {index} 下标!");
}
LightmapSettings.lightmaps = Lightmaps[index].Lightmaps
.Select(v => new LightmapData { lightmapColor = v.lightmapColor, shadowMask = v.shadowMask })
.ToArray();
}
[System.Serializable]
public class LightMapInfoArray : ReorderableArray<LightMapInfo> { }
[System.Serializable]
public class LightMapPairArray : ReorderableArray<LightMapPair> { }
[System.Serializable]
public class LightMapInfo
{
public string name;
[Reorderable, SerializeField]
public LightMapPairArray Lightmaps;
public void Clean()
{
Lightmaps.Clear();
name = string.Empty;
}
}
[System.Serializable]
public class LightMapPair
{
public Texture2D lightmapColor;
public Texture2D shadowMask;
}
}
tips: 使用了这个插件Unity-Reorderable-List