Ctrl+D为什么不能复制烘培效果
如果你们团队是从Unity 4版本一路用过来的,我相信你们的美术一定会说Unity 5的烘培系统太”坑爹”了。为什么呢?我们先从Unity 4的烘培聊起来。
一、Unity 4 与Unity 5的对比
Unity 4烘培场景后,如果物体不需要接收阴影,美术可以直接在Scene中用Ctrl+D进行GameObject的复制,不需要额外的再次烘培场景。为什么呢?我们随便烘培一下场景,然后用记事本一类的工具打开这个Scene如下图所示,Lightmap的烘培信息是记录在这里的。运行后Unity会根据这里的数据,把烘培信息载入给游戏对象。所以Ctrl+D复制出来的游戏对象是没有任何问题的。
或者还可以把烘培后的某个GameObject做成Prefab。Unity同样会把Lightmap的信息保存在Prefab里。用记事本一类的工具打开序列化的信息和上图是一样的。
我知道很多美术都在使用这个方法来复制游戏对象。但是这样做确实是有隐患的,当我Ctrl+D复制出来N个GameObject的时候,以后如果再次烘培这个场景,那么Scene里的Lightmap信息又会被重新刷新。
我们做一个简单的测试,如下图所示。我创建了个点光源,在Cube旁边,能看到立方体已经变成红色了。
接着,我Ctrl+D一份和它具有一样的烘培效果的Cube,移动了一下它的位置,如下图所示似乎也挺正常。
但是,此时隐患已经出现了。如果你再次烘培当前场景,此时这个物体的烘培效果已经和你当初复制的那个不一样了,因为是重新烘培,周围的光已经不同。Scene上记录的Lightmap信息也会更新,而且由于复制的物体多了烘培贴图可能也会变大。(可以确定的是和当初复制的时候肯定不一样了)
当然为了突出效果,上面的例子可能举得极端。回归实际的项目中, 通常树木、房子、石头、这些比较琐碎的东西,美术都喜欢Ctrl+D,因为相邻的物体,周围的光变化不小。所以实际上就算在烘培一下,效果也和之前差别不大。(但是烘培后,烘培贴图已经和之前的不一样了)。所以各位看官,尤其是吐槽Unity 5 烘培的美术朋友们,之前Ctrl+D的方便其实就是一个隐患。
接下来我们再来对比一下Unity 5,同样的场景我们把Scene用记事本一类的工具打开。此时已经找不到 m_LightmapIndex、 m_LightmapTilingOffset 这两个属性了,如此一来Ctrl+D是不可能复制烘培效果的。
再回到实际项目里,如果你的游戏场景比较小,烘培时间比较短,可以考虑Ctrl+D复制好以后,再统一另找时间进行烘培。但是如果你的项目场景比较大,比如有些大世界游戏,烘培一次就4个多小时,这种情况下简单复制一棵树或者一个房子还要等4个小时,显然太不科学!
二、复制烘培效果
首先我们知道场景里需要烘培的物体越多,那么对应占用的烘培贴图数量就会越多。所以解决这个问题之前,首先需要确认哪些贴图是需要复制的,哪些则不需要。需要复制的必须满足不带阴影的条件,比如 石头、草、地面的一些杂物等,不需要复制的就比较多了,总之需要产生烘培阴影的,都可以认为它不需要复制。上面说了这么多,下面再说说如何解决这个问题。
m_LightmapIndex、 m_LightmapTilingOffset 虽然不会序列化在场景或者Prefab上,但是我们可以在 Editor下或者运行时给MeshRenderer单独去赋值,或者可以做一个 Ctrl+Shift+D的快捷键来进行复制。
具体细节大家可以参考我之前写的一个例子 :https://bitbucket.org/xuanyusong/unity-duplicatelightmap
这个例子的原理就是Ctrl+Shift+D后,把当前选择的GameObject上所有的m_LightmapIndex、 m_LightmapTilingOffset取出来,克隆一份新的GameObject,然后完整地把m_LightmapIndex、 m_LightmapTilingOffset复制给新的。并且标记克隆出来的GameObject不会再参与下次的烘培,这样可以减少一些烘培贴图的数量。以后如果再次烘培,克隆出来的只会去取它克隆的那个GameObject身上的Renderer信息。
三、烘培与实时光混合使用
场景中策划希望某些物体使用实时光,而某些物体使用烘培光。比如主角使用实时光加实时阴影效果,而房子、或者山什么的都使用烘培光。如下图所示,不需要参与烘培的一定不要标记Lightmap Static 属性。(这种美术很有可能忘勾或者勾错,可以做个编辑器自动帮美术标记它)
接着标记灯光为混合模式,这样烘培后的物体就不会参与实时光的运算了。
运行时动态更新烘培贴图
很多人都反映在Unity 4上好好的,放到Unity 5上就不能在运行时更换烘培贴图了,其实Unity 5也是可以的,就是有点麻烦而已。
一、Unity 5的烘培信息
上面我们说了Unity 4的烘培信息(索引和偏移)保存在了场景或者Prefab里,然而Unity 5换地方了,如下图所示,场景烘培完后就更新这个LightingData二进制文件。(无法用记事本打开)
然而我们正常的第一反应都应该是:更换烘培贴图就是更新了这个LightingData文件。但是很遗憾的是它更新不了。Unity文档上说明,这个东西只是在Editor下用的。
二、Unity 5运行时更新
运行时更新烘培贴图分三种情况。
1、只换烘培贴图,不更换场景中的GameObject元素。
这种相对来说简单一点,通常游戏中有个白天场景,还有个夜晚场景,更新烘培贴图的时候就没必要单独做两个场景了。如下图所示,直接更换烘培贴图,直接修改LightmapSettings.Lightmaps即可。
2、需要换烘培贴图的时候,在运行中再添加GameObject元素,这种情况应该是最普遍的。
可能你是运行时Resources.Load然后Instantiate到游戏中了,这时候就要在运行时设置m_LightmapIndex、 m_LightmapTilingOffset 。比如美术的场景只做个大体框架,场景上的一些房子啊、树啊什么的是根据玩家行走的坐标动态创建出来的(走到哪里加载那些东西)。这些动态创建出来的物体是需要有烘培贴图的效果的。
对于这种需求,我建议美术单独拿出一个场景,来烘培运行时动态创建的Prefab,烘培完后工具自动绑定脚本,把对应的贴图、索引、offset都保存起来。每当这个Prefab实例化到游戏中的时候自动设置正确m_LightmapIndex、 m_LightmapTilingOffset 这两个数值即可。
3、大世界无缝地图 OpenWorld 加载烘培。
无缝大世界,就像魔兽世界那样,玩家在场景中行走,除了下副本以外,永远都不会Loading。Unity官方明确说明了,他们暂时不支持OpenWorld,但是并不是不能做。
我举个极端的例子,我们做了个N * N 的超大型场景,使用Unity自带的烘培器,烘培了100张烘培贴图。进场景以后肯定不能把这100张都载入游戏中吧?不然内存直接就挂了。理想的做法肯定是人物在世界中漫步行走,只加载附近的几张烘培贴图,释放不需要使用的烘培贴图。由于Unity没有真正的异步加载,为了避免一帧内处理的事情太多,所以尽可能地保证一帧加载的东西要少。
Renderer组件上确实是记录了Lightmap使用的序列,开始我尝试根据人物行走的坐标,取到将要出现在屏幕里或者离开屏幕里的Renderer,然后动态地加载与释放烘培贴图,保证内存里只有当前需要用的Lightmap烘培贴图。因此,在大地形的Lightmap烘焙时,大家可以尝试考虑以下方法:
如果是从零开始建立地形,那么可以直接使用Terrain Composer插件来进行制作,然后在Unity中进行烘焙,引擎会为每一块地形独自生成一个与其对应的Lightmap贴图;
如果事先已经做好地形,那么可直接在引擎中进行烘焙,烘焙后可尝试通过访问TerrainData来对地形进行切割,然后再根据地形的切分情况将其对应的Ligthmap进行切割;
对于地表物体(树、房子等模型),则可以通过Lightmap Parameters 中的 Baked Tag标签来将其进行分组,比如每块地形上的地表物体可以归结到一组,这样,这些地表物体在烘焙后,其烘焙信息会在同一张Lightmap中。
以上方式是针对Unity引擎的地形系统来进行烘焙,Unity的地形系统在使用上可能会带来一些Draw Call上的增加,对此,你也可以最后将Unity Terrain转换成Mesh网格来进行使用。
还有工具这块,如下图所示这样可以监听Lightmapping烘培结束后的事件。可以把对应Renderer需要烘培贴图的id、index、offset记录下来,序列化在特定的脚本中。
最后,欢迎大家给我提出宝贵的意见。也希望和大家一起探讨Unity开发技术。