基于Unreal引擎的大地形加载研究
UWA从去年开始进入Unreal引擎的学习,并且从去年底开始发表了一系列关于Unreal引擎使用方面的技术文章。但是,今天的这篇文章与以往的功能介绍不太一样,我们想通过一个实际的案例来让你对Unreal引擎产生更加深入的了解。
由于吃鸡游戏的火爆以及精品MMO游戏对于场景的需求越来越大,所以,我们这两个月使用Unreal引擎对大地形的动态加载功能进行了研究。从中,我们希望可以搞清楚以下两点:
-
Unreal引擎对于大场景动态加载的制作方法;
-
Unreal引擎在当今移动设备上的性能表现。
为了让我们的地形展示看起来更加“漂亮”一点,我们放弃了自己手动制作大地形场景的想法(我们自己做的地形实在拿不出手),而是使用了之前在Unity引擎中使用过的大地形场景模型。除了最终的渲染效果,我们几乎是1:1地在Unreal引擎还原了该场景环境,其在真机上的运行效果如视频所示。
https://v.qq.com/x/page/e06490ljasw.html
视频中左边是Unity引擎的渲染结果,右边是Unreal引擎的渲染结果。
本文将主要介绍我们通过Unreal引擎制作大地形的过程中遇到的问题,希望可以对正在或即将使用Unreal引擎开发大地形功能的研发团队有所帮助。本文主要包含四个部分:
-
Unreal引擎的资源准备;
-
Unreal引擎场景流式加载功能介绍;
-
Unreal引擎大场景动态加载制作介绍;
-
Unreal引擎在真机上的性能分析。
接下来,我们将分别介绍这四部分内容。
一、 Unreal引擎的资源准备
由于我们希望使用一个已有的案例资源来制作Unreal引擎的大地形加载,所以,我们首先需要解决的,就是如何将Unity引擎的资源转换到Unreal引擎中进行使用。在资源转换过程中,我们需要处理四种类型的资源:地形、模型、纹理和材质。接下来我们分别介绍这四种资源的转换方式。
1. 地形资源
在原始的大场景动态加载案例中,我们使用的是Unity引擎的Terrain系统。但由于Unity与Unreal引擎中地形的数据格式不一样,因此我们无法直接在Unreal引擎中使用Unity引擎的地形数据。因此,我们尝试通过地形的高度图来将Unity引擎Demo中的地形转换到Unreal引擎中,其转换方式如下图所示:
image我们首先对Unity引擎中的地形块导出高度图,然后将高度图导入Unreal引擎重建出相同的地形。高度图中的每一个像素记录的实际是地形块中每一个顶点在垂直方向上的高度。Unreal引擎中对于高度图的导入非常便利,如下图所示。在Unreal引擎高度图导入界面,我们只需在New Landscape界面中选择Import from File,并选中我们想要导入的高度图,再点击Import按钮即可导入高度图并重建地形。
image但是,由于Unity与Unreal引擎的坐标系的不同,从Unity引擎中导出的高度图直接放入Unreal引擎中会导致重建出的地形与原始Unity引擎中的地形相对X轴呈现镜像的关系,如下图所示:
image image上图中左图是Unity引擎坐标系和原始地形截图,右图是Unreal引擎坐标系和重建地形截图。对于这种情况,我们可以做一个简单的坐标变换来让其看起来一致。但是,我们发现在Unity引擎中导出高度图时勾选Flip Vertically选项,也可以达到同样的效果,如下图所示:
image image其中,左图是Unity引擎原始地形,右图是高度图Flip后在Unreal引擎中重建出的地形。
在制作Unreal引擎大地形Demo时,我们不仅希望地形的形状与Unity引擎Demo一致,并且希望地形参数设置也尽量与Unity保持一致。在Unreal引擎地形参数中,主要包含了三个参数:
-
Component
-
Section
-
Quad
这三个参数从上至下是包含关系,即:一块地形中可包含多个Component,一个Component可以包含多个Section,一个Section可以包含多个Quad。而Quad则是地形网格中最小的四边形。接下来我们分别介绍这三个参数的设置。
1.1 Component
在Unreal引擎的地形中,Component实际上是裁剪、渲染以及碰撞检测的基本单元。Unreal引擎在渲染地形时,一个Component要么被整体裁剪掉,要么被整体渲染出来并参与碰撞检测的计算。而其数量则是通过New Landscape窗口中的Number of Components参数进行设置,如下图所示:
image其中,左图显示了New Landscape窗口设置界面,红色方框显示了Number of Components参数。该参数的设置可以任意选择,例如:1x1,1xN或者NxN。而我们在设置该参数时主要是参考Unity引擎大地形Demo的设置。右图显示了Unity引擎中地形的截图。在Unity引擎中物体裁剪是以GameObject为单位,而我们在制作地形时是以一块子地形作为一个GameObject。因此,在Unity引擎中地形的裁剪是以一块子地形为独立的裁剪单元。为了与Unity引擎Demo的设置保持一致,我们将Unreal引擎的地形设置中Component设置为了1x1,即一块子地形只包含一个Component。这样就保证了Unreal引擎中地形的裁剪也是以一块子地形为基本单元。
1.2 Section
在Unreal引擎的地形中,Section实际上是LOD的基本单元。它的数量可以通过Sections Per Component进行设置,如下图所示:
image其中,一个Component中可以包含1(1x1)个或者4(2x2)个Section。该参数的设置我们也是参考了Unity引擎中地形的LOD。右图显示了Unity引擎中的一块地形网格截图。其中,右下角1/4大小的地形是比较密的网格,而其他3/4是比较稀疏的网格。因此,Unity引擎Demo中一个子地形块的LOD变化单元是1/4大小。所以,我们将Unreal引擎中的Section数量设置成2x2,即一个Component包含4个Section。这样可以保证地形的LOD变化粒度与Unity引擎地形基本一致。
1.3 Quad
在Unreal引擎的地形中,Quad决定了最后生成的地形网格的顶点数。其数量可通过Section Size参数进行设置,如下图所示:
image其中,左图是Unreal引擎地形参数设置界面,右图是Unity引擎中地形分辨率参数界面。由右图可知,Unity引擎Demo中一块地形的顶点数是65x65。而Unreal引擎地形的Section Size参数则不能任意设置,只有几个选项可选择。因此,我们选择了最为接近的分辨率,如左图红框中所示。最终,我们重建出的地形的顶点数为63x63,如左图中蓝色框所示。
但是,虽然分辨率很接近,但问题还是出现了。由于我们在Unreal引擎中使用65x65分辨率的高度图重建出了63x63顶点数的地形,导致Unreal引擎在重建地形时对高度图进行了重采样。而重采样的结果造成了我们将子地形块进行拼接时,在相邻地形块之间出现了裂缝,如下图所示:
image为了解决这一问题,我们对高度图进行了预处理。该预处理的目标有两个:
-
将高度图从65x65重采样成63x63;
-
保持相邻高度图的公共边的数值一致。
我们采用了如下的方法进行重采样:
image如上图所示,左图表示重采样之前的高度图,右图表示重采样之后的高度图。在计算中间像素时,我们从原始高度图中选取周围4个临近的像素进行插值,如上图绿色框所示。在计算到边缘像素时,我们仅仅使用边缘上的两个像素进行差值,如上图红色方框所示。由于原始高度图中相邻边上像素的数值是一样的,而差值的过程中又保证了相邻边的采样点仅仅来自于边缘像素,所以保持了重采样后的高度图中相邻边的像素值一致。通过这个过程,我们得到了63x63分辨率的高度图,并且保证了其相邻边上数值一致。因此,我们在Unreal引擎中重建出的地形就可拼接完好,如下图所示:
image其中,左图是原始案例中地形的网格截图,右图是Unreal引擎中重建出的地形拼接完成后的网格截图。
2. 模型资源
Unreal引擎中模型资源的导入非常便利,如下图所示:
image我们只需在Content Browser窗口下点击Import按钮,然后选择需要导入的模型,并点击导入设置窗口中的Import即可导入。Unreal引擎中支持各种不同格式的模型导入,例如:FBX,OBJ等。
3. 纹理资源
Unreal引擎中纹理资源的导入过程与模型资源类似。目前来说,Unreal引擎支持绝大多数的图像格式,并且官方推荐使用的是PSD格式。
4. 材质资源
Unreal引擎中材质资源通常是使用Blueprint节点图进行编辑的。因此,我们在Unreal引擎中创建出材质Blueprint,并添加相应的纹理以及设置好参数。然后,将材质设置到对应的模型中,如下图所示:
image其中,左图显示的是材质节点图,右图中红色方框显示的是将材质设置到相应模型中。
与模型材质不同的是,Unity引擎地形材质是通过将四张不同的纹理混合而成的。混合的参数则是通过一张Splat纹理中的RGBA四个通道分别指定,如下图所示:
image其中,左图显示了Unity引擎中四张混合纹理,右图显示了记录混合参数的Splat纹理。我们利用Unity引擎的TerrainData.alphamapTextures[0]将其导出。然后,我们在Unreal引擎的材质Blueprint中导入这些纹理,并编辑节点图进行混合,如下图所示:
image由此,我们准备好了Unreal引擎中所需要的资源,并重建出了地形。接下来,我们需要将Unreal场景中的模型进行摆放和复原。
由于Unity与Unreal引擎的坐标系不同,所以我们要对其进行转换,如下图所示:
image我们将原始场景中的所有GameObject的Transform信息制作成配置文件,然后通过一定的转换计算出Unreal引擎中Actor的坐标。由于Unity引擎中使用的长度单位是米,而Unreal引擎中使用的长度单位是厘米。因此,我们最终使用了如下的公式进行计算:
image其中,左边是位置信息的转换,右边是旋转信息的转换,下边是缩放信息的转换。通过转换过的配置文件,我们即可在Unreal引擎中创建出与原始场景几乎一致的场景。
二、Unreal引擎场景流式加载
在重建了场景之后,我们接下来需要考虑如何对其进行动态加载。Unreal引擎为开发者提供了便利的场景流式加载方案,即Streaming Level。在Unreal引擎中,场景关卡主要分为两种:Persistent Level和Sub Level。Persistent Level在游戏启动时自动加载,并且会一直存在、不能被卸载。而Sub Level则可被动态加载和卸载。Streaming Level功能则使得我们能够很方便地动态异步加载、卸载Sub Level。在制作大地形动态加载时,我们只需将动态加载的内容分别放到不同的Sub Level中,就可利用Streaming Level进行异步加载和卸载了。
Streaming Level中触发Sub Level加载和卸载的方式有两种:
-
Level Streaming Volume;
-
Scripted Level Streaming。
Level Streaming Volume方式是利用在场景中摆放一个立方体,当Player进入该立方体空间内时,触发与该立方体绑定的Sub Level的加载。当Player离开该立方体时触发与之绑定的Sub Level的卸载,如下图所示:
image其中,浅黄色的立方体方框即为Level Streaming Volume,深黄色矩形显示的是与之绑定的Sub Level中的一块地形。当玩家进出该浅黄色方框时,就会触发该地形块的加载和卸载。
Scripted Level Streaming则是提供了两个API:Load Stream Level和Unload Stream Level,如下图所示:
image利用这两个API可以在任何时刻对指定的Sub Level进行加载和卸载。
这两种触发方式各有优缺点。使用Level Streaming Volume进行触发时,修改加载策略便利,但是触发方式单一。使用Scripted Level Streaming进行加载时,触发方式灵活,但是需要自己管理加载策略。
在使用Unreal引擎提供的Streaming Level功能制作大地形加载比较便利。我们的制作过程如下图所示:
image我们在场景中创建出一系列的Level Streaming Volume,然后将其与对应的Sub Level进行绑定。在运行时,引擎便会自动根据玩家位置加载、卸载Sub Level。
https://v.qq.com/x/page/h06499i47ls.html
上述为通过Unreal引擎内置的Streaming Level功能来制作大场景的动态加载,同样,也可以通过代码来进行实现,逻辑与上述一致,其具体实现细节就不在这里细表了。
三、移动端性能分析
通过以上两章说明,相信大家已经知道如何通过Unreal引擎来实现一个大型场景的动态管理功能。在本章中,我们将就Unreal引擎的性能效率来进行检测。对此,我们通过与之前在Unity引擎上的场景案例来进行真机性能对比,以期让大家对Unreal引擎的性能效率能有一个更为直观的了解。
在原始案例中,我们通过Unity引擎使用两种方式来制作了大地形的动态加载功能。一种是自己控制任何一块地形、任何一个地表物体(花、草、树和石头等)的流式加载和卸载;另一种是使用Unity引擎提供的Scene Manager功能进行实现,通过LoadSceneAsync和UnloadSceneAsync来进行地形块的加载和卸载。我们的制作方式如下图所示:
image首先我们获取玩家的位置信息,然后根据玩家位置确定当前距离玩家最近的九个场景,最后利用Scene Manager API进行场景的加载和卸载。自己控制流式加载实现的过程也是类似进行管理。
但在性能对比时,我们更多的是使用自己实现的流式加载版本来进行对比,因为这是我们高度优化过的版本,其资源动态加载更为可控,从而性能较之SceneManager版本稍好一些,如下图所示。
image在比较Unreal与Unity引擎制作相同大场景加载的性能时,我们对两个引擎的渲染参数进行了相同的设置。在测试过程中,我们选用Unity引擎的版本是2017.2,选用Unreal引擎的版本是4.18。同时,我们设置渲染的管线都是Forward Path,都不开启HDR和AA,Light Map都使用了Non-Directional。由于Unity引擎的Demo使用了饱和度颜色映射的图像后处理,因此我们添加了Unreal引擎自带的图像后处理饱和度映射,如下图所示:
image其中,左图是Unity引擎的图像后处理设置界面,右图是Unreal引擎的图像后处理设置界面。我们分别从CPU性能、能耗和内存三个角度来进行分析。接下来我们分别进行介绍。
1. 性能比较
在性能比较中,我们比较了Unreal与Unity引擎Demo的FPS以及每帧耗时统计。在实验中,我们分别对比了Unity与Unreal引擎的PBR材质,以及Unity引擎Legacy和Unreal引擎PBR简化版的性能比较。这主要是因为在Unity 2017版本以后,地形系统的材质存在四种模式,如下图所示。所以,我们选择了Built In Standard和Built In Legacy Diffuse来进行测试。具体如下:在对比PBR材质时,Unity引擎中我们采用了Standard Shader进行渲染,Unreal引擎中我们使用了默认的参数设置。在对比Legacy和PBR简化版时,Unity引擎中我们采用了Legacy Diffuse材质,Unreal引擎中我们去掉了部分默认的PBR效果,例如:使用Full Rough,以及去掉Decal Response等。接下来我们将分别介绍它们的数据结果。
image1.1 Unity PBR vs Unreal PBR default FPS
我们在高、中、低不同机型上进行了测试,下图分别为红米2、小米5X和三星S6上的FPS统计图:
image image image其中,红色线条表示Unreal引擎的帧率统计,蓝色线条表示Unity引擎的帧率统计。从结果上看,在地形使用PBR材质时,Unreal引擎在红米2设备上帧率在15~25 FPS区间,在小米5X设备上帧率在40~55 FPS区间,而在三星S6设备上,Unreal引擎基本上可以达到满帧(60FPS)。同时,我们也可以看出,Unreal引擎在当前案例中的性能确实要领先于Unity引擎。
1.2 Unity Legacy vs Unreal PBR Simplified FPS
下图分别显示了红米2、小米5X以及三星S6上Unity Legacy Diffuse与Unreal PBR
简化版的FPS比较:
image image image由上图可知,在地形使用Legacy材质时,Unreal引擎和Unity引擎在移动设备上的性能均明显提升。其中,Unreal版本在红米2设备上帧率在30~45 FPS区间,在小米5X设备上帧率在45~60 FPS区间。同时,我们也可以看到,Unity版本虽然还有一些掉帧情况,但在高端设备上性能有了大幅提升。
除上述设备之外,我们一共在8台不同档次的移动设备上进行了比较,其具体的帧率统计如下表所示。
image由上图可知,在目前的高中低档设备中,Unreal版本(PBR简化版)的运行效率都相当优秀。除了红米Note2和魅族MX5两款机型外,Unreal引擎在本测试用例中的性能均高于Unity引擎。
****2. 能耗比较
在能耗测试中,我们采用Unity Legacy Diffuse与Unreal PBR简化版材质进行渲染,并且将帧率固定在30FPS,然后使用高通Trepn工具统计实时功率。下图分别显示了红米2、小米5X以及小米5S机型上的统计数据,单位为毫瓦(mW):
image image image由上图可知,在中、低端机型上Unreal引擎的能耗比Unity引擎稍高,而在高端机型(小米5S)上,Unreal引擎要比Unity引擎高出许多。
******3. ****内存比较**
在比较内存的试验中,我们采用Unity Legacy Diffuse与Unreal PBR简化版材质进行渲染,然后统计Android平台的PSS内存。下图显示了红米Note2机型上,Unity引擎自实现流式加载方式、SceneManager加载方式和Unreal引擎的内存统计,单位为MB:
image由上图可知,使用Unity引擎 Scene Manager加载方式(灰色)的PSS内存会在一开始阶段随时间逐步上升,最后稳定在一个固定值阶段。这是因为Scene Manager加载方式在Unload场景时只是删除了GameObject而没有释放相关的资源,所以其内存会越来越多,直到场景遍历完为止。如果要释放该部分资源,则需手动调用UnloadUnusedAssets接口才行。而在我们自实现的流式加载版本中,我们通过UnloadAsset API来手动控制相关资源的卸载,因此,可以做到在整个场景遍历时,内存都维持在较低且平稳的程度上。而Unreal Streaming Level模式则会自动释放资源,其资源卸载是由引擎自动管理,因此Unreal版本在整体运行过程中,PSS内存也较为一致。
下图显示了更多机型上的内存占用。我们采用了Unity自实现流式加载方式与Unreal引擎比较,单位为MB: image image由上图可知,Unreal引擎的PSS内存占用较之Unity引擎普遍较高。但其具体原因,我们暂时还无法给出解释,关于Unreal引擎底层的内存使用分析,是我们接下来计划重点研究的方向之一。
四、未来工作
以上则是我们近期对于Unreal引擎关于大地形动态加载的一些工作心得,相信读到这里的你,已经对开篇的两个问题——Unreal引擎中大地形的动态加载制作方法和在移动设备上的性能效率有了充分的答案。接下来,我们会在以下几个方面进行进一步的工作:
-
对Unreal引擎的内存使用进行深入研究,并对案例中的内存占用进行优化;
-
对Unreal引擎的CPU开销和能耗问题进行深入研究,并对案例中的性能进行优化;
-
将完善后的案例工程进行整理和开放,让更多的游戏开发者受益。
注:当我们第一次在真机上对Unreal的大地形版本进行测试时,完全被它的性能表现给震撼了。我们团队一直专注在游戏的性能优化方面,应该还算是有一定的技术积累,对于在何种设备上,何种复杂程度的游戏能跑出多大的性能数值还算是有些预判的。但当我们在红米2上运行我们自己做的Unreal版本时,心中的念头只有一个——“Unbelievable”!它展现出来的性能效果大大出乎我们的意料。也许,这就是Unreal引擎这二十年来的技术底蕴,它让我们对于Epic这个单词产生了更加深刻的理解。
但是,需要郑重说明的是,该篇文章虽然比较了Unreal引擎和Unity引擎在真机上的性能效率,但是这并不能说明两个引擎哪一个更好。引擎之间的比较是多维度的,并且是无法从一个案例中得出结论的。因此,我们希望读者不要产生误解。在我们眼里,二者都是当今世界上伟大的引擎。无论是Unity还是Unreal,亦或是其他引擎,只要是能推动中国游戏进步的就都是好引擎!
最后,感谢Unreal中国团队在我们案例制作和研究过程中提供的专业的技术支持!