Unity烘焙:光照贴图分场景烘焙与综合加载

前言:
文章的内容在于分享在Unity中如何对一整套模型分批烘焙,最后再综合加载的,以及加载后存在的问题。不会详细的讲解烘焙参数以及方法相关的知识,对这方面知识有需求的小伙伴可以自行查看其他资料。
最后再补充一点,这里只是分享分场景烘焙与综合加载的方法,至于在实际项目中是否实际可行,就需要小伙伴根据自己项目自行考虑啦。
如果我分享的内容存在问题也请大伙指正,我会及时修改,好了让我们开始正文吧。
部分借鉴:https://blog.csdn.net/Tel17610887670/article/details/113181709
该文章部分代码并未涉及,完全照做达不到最后的效果,部分内容据说需要下载示例工程。至少我木得积分,并没有下载。
正文:
首先介绍一下我的环境以及使用的素材给大伙一个参考:
1、Unity 3D版本: 2020.3.30f1c1
2、模型素材:Asset Store中的"Medieval Tavern Pack"(免费)
准备工作就已经结束了。
先来看一眼原始场景、整体烘焙后场景和分场景烘焙综合加载效果。文章的核心在于分享方案,所以请自动忽略这个效果不好的问题.....【实在是能力有限】



烘焙的相关知识这里不会展开讲解,因为这是一个比较大的内容,如果有需要可以单独开一篇来进行讲解。所以下面只会对关键面板进行截图展示并告诉大家修改某些参数,至于具体功能和其他参数这里不做过多赘述。
这里稍微说一下烘焙的方式,有基础小伙伴可以跳过。这里不对灯光和烘焙参数做过多的设置。
首先我们找到自己的模型文件,将所有用到的模型的“Generate Lightmap UVs”勾选,点击“Apply”为我们的模型生成光照UV,如果不做这一步,能会造成烘焙效果不正确,当然省略这一步会不会影响我们后续的操作,但是为了效果还是建议勾选。

将场景中所有模型勾选“Static”,否则烘焙后无法得到想要的数据。

将灯光的模式改为“Baked”,稍微调高光强和间接光强。

Lighting面板可以在顶部菜单栏"Window->Rendering->Lighting"打开。
然后在Lighting面板新生成一个“Lighting Settings”,勾选“Mixed Lighting”下的“Baked Global illumination”选项,并将“Lighting Mode”设置为“Shadowmask”.

这个地方建议大家选用较小的模型来测试,模型的大小直接决定我们的烘焙速度。将"Lightmapper" 改为“Progressive GPU(Preview)”可以增加速度(我的电脑能快5-10倍),如果场景不大,其他的选项可以不改。

最后点击“Generate Lighting”开始烘焙。顺便提一下,关闭“Auto Generate”。
这些数值不重要,可以照着填,也可以不改。

接下来,让我们看看场景烘焙完以后会的得到些什么。我这里是把场景分成了3份,3个场景分别烘焙的。所以会有3个文件夹。如果你没有分,就只会得到1个。这个地方解释一下,希望不要给读者造成困扰。


烘焙完成后,会在场景同级目录中生成一个同名文件夹,用来存放场景的烘焙数据类似光照贴图等。光照贴图会被加载到"Lighting"-“Baked Lightmaps”下。
再来看一下场景中的模型是如何利用这些光照贴图的。可以看到,烘焙后的“Mesh Renderer”组件下的“Lightmapping”多了一个“Baked Lightmap”,并且他还记录着光照贴图序号(Lightmap Index)和Tilling/Offset等,这就是我们想要的东西。




到这里相信已经有许多小伙伴已经知道我们最后应该怎么实现了,一定想迫不及待的自己试一下。
没错!就是你想的那个样子!!!
下面进入我们的代码环节,为了让小伙伴们方便的复现,所有的代码我都会完整的贴出来并且写好注释,争取让大伙都看的明白我每一步想要干什么,至于代码的优化问题,我相信小伙伴们都可以完成的,这里就以易读为主了。
代码部分
实现思路
1、 获取对象的 “Mesh Renderer”,并且保存“Lightmap Index”,“lightmapScaleOffset”(“Tilling”与“Offset”)。
2、往“综合场景”按照一定的顺序加载所有的光照贴图。
3、将第一步记录的数据在“综合场景”还原。
具体实现与详解
首先我们需要分割场景,并将分割后的模型分辨放入不同的场景,但是要保证各场景的灯光数值相同。



然后分别点击烘焙,得到三个上边讲过的烘焙数据文件夹。
先将场景的模型放到统一父物体下,并且做成预制体,放到Resources目录下,这很重要,否则后续的操作大概率会失败。

“LightMapData”存储每个对象的数据,会统一存入“SubSceneLightMapData”
[Serializable]
public class LightMapData
{
//Mesh Renderer组件
public MeshRenderer meshRenderer;
//引用的光照贴图在本场景的下标( MeshRenderer.lightmapIndex)
public int lightmapIndex;
//“Tilling”与“Offset”他们合并成一个Vector4返回(MeshRenderer.lightmapScaleOffset)
public Vector4 lightmapScaleOffset;
public LightMapData(MeshRenderer meshRenderer)
{
this.meshRenderer = meshRenderer;//略
lightmapIndex = meshRenderer.lightmapIndex;//略
lightmapScaleOffset = meshRenderer.lightmapScaleOffset;//略
}
}

“SubSceneLightMapData”记录场景中所有的光照贴图和各个物体的信息
public class SubSceneLightMapData : MonoBehaviour
{
//起始下标。假如当前是第二个场景,第一个场景加载了5张光照,那么起始下标就是5。在综合加载时使用
public int startIndex;
//本场景的所需要的光照贴图集合
public List<Texture2D> subLightMaps = new List<Texture2D>();
//本场景所有的模型对象以及光照信息
public List<LightMapData> lightMaps = new List<LightMapData>();
//在组件的菜单中增加一个选项,不理解的可以直接抄
[ContextMenu("执行函数")]
public void ExecuteAction()
{
LoadSubSceneLightMap();
GetLightMapData(transform);
//代码自动保存预制体,如果感觉波浪线不好看,可以不写手动保存
if (PrefabUtility.GetPrefabParent(gameObject) != null)
{
PrefabUtility.ReplacePrefab(gameObject, PrefabUtility.GetPrefabParent(gameObject), ReplacePrefabOptions.ConnectToPrefab);
}
}
//加载本场景的光照贴图
void LoadSubSceneLightMap()
{
foreach (var item in LightmapSettings.lightmaps)
{
subLightMaps.Add(item.lightmapColor);
}
}
//判断节点是否具有Mesh Renderer,保存相关数据,并判断当前节点是否具有子节点,如果有那么递归操作。
private void GetLightMapData(Transform node)
{
if (node.gameObject.isStatic)
{
var render = node.GetComponent<MeshRenderer>();
if (render != null)
{
lightMaps.Add(new LightMapData(render));
}
for (int i = 0; i < node.childCount; i++)
{
GetLightMapData(node.GetChild(i));
}
}
}
//综合加载时,将对应的光照贴图数据再还原回去
public void SetAllLightmapData()
{
if (lightMaps == null) return;
for (int i = 0; i < lightMaps.Count; i++)
{
//综合加载时,光照贴图的顺序要随着加载顺序发生变化
lightMaps[i].meshRenderer.lightmapIndex = lightMaps[i].lightmapIndex + startIndex;
lightMaps[i].meshRenderer.lightmapScaleOffset = lightMaps[i].lightmapScaleOffset;
}
}
}
挂载到最顶级对象上,执行函数。


一定记得保存预制体!虽然代码里有自动保存,但还是再说一遍!

最后一个脚本“SceneLightMapControl”,加载所有的预制体,光照截图,为每个子场景的预制体设置一个正确的顺序。
public class SceneLightMapControl : MonoBehaviour
{
//已经加载的光照贴图的数量,一会要赋值给SubSceneLightMapData使用
public int lightmapCount = 0;
//光照贴图总集合
public List<Texture2D> Templightmaps=new List<Texture2D>();
private void Start()
{
LoadSubPrefab();
}
public void LoadSubPrefab()
{
LightmapSettings.lightmapsMode = LightmapsMode.NonDirectional;
//为了方便加载我将预制体存放在Resources根目录下,分成了三份。
for (int i = 0; i < 3; i++)
{
GameObject go = Instantiate(Resources.Load<GameObject>("Part" + i));
go.GetComponent<SubSceneLightMapData>().startIndex = lightmapCount;
//获取子场景所有的光照贴图
foreach (var item in go.GetComponent<SubSceneLightMapData>().subLightMaps)
{
Templightmaps.Add(item);
}
go.GetComponent<SubSceneLightMapData>().SetAllLightmapData();
//更新起始下标
lightmapCount = Templightmaps.Count;
}
List<LightmapData> lightMapArray = new List<LightmapData>();
for (int i = 0; i < Templightmaps.Count; i++)
{
LightmapData lm = new LightmapData();
lm.lightmapColor = Templightmaps[i];
lightMapArray.Add(lm);
}
//将光照贴图重新赋值
LightmapSettings.lightmaps = lightMapArray.ToArray();
}
}

OK,下载将“SceneLightMapControl ”挂到场景中运行就可以看到效果了



现在我们的功能就已经全部实现了。
后话
如果最后加载出来的场景光影不正常如下图所示,请检查加载的贴图顺序是否正常。

另外这种分场景烘焙的方案存在一定的缺陷,比如连续的阴影存在缺陷


对于这种情况,只能请美术大佬想想办法给我们找补一下了。
但是更建议我们在拆分之前就选择不产生阴影的地方做分割或者不易被发现的地方分割。
好了今天的内容就到这里,有问题或者文章有错误请在评论区内留言,我会及时改正的!
我们下期再见!