Unity5.6大规模地形资源创建方法
在很多仿真和游戏应用中都需要大规模地形,这样会使3d环境似乎“无限大”,增加用户的真实感,比如飞行模拟游戏。那么在unity中如何实现大规模地形场景呢?官方中文论坛中有一个帖子讲得比较在点,帖子地址在。总结起来有三点:地形分块,动态加载和卸载,内存优化。其中前两点是最基本的。
最近一直在研究这个问题,也查了好多资料,博客、论坛,甚至一些科研论文,还并没有发现一套完整的解决方案(也许asset store上有,不过并没有找到免费的)。于是决定在这里将研究过程中的一些思路和实施方法分享给诸位unity开发者,我会根据研究的进度不定时地更新这个系列。欢迎各位一起讨论!
我采用worldmachine来绘制地形,并分块导出heightmap和texture。worldmachine的使用暂时并没有深入研究,而分块导出的方法在wm手册上描述的比较详细。这一部分以后会单独更一篇,现在我们暂且不论。那么有了heightmap和texture如何创建unity地形,或者说是terrain object呢?这里介绍两种方法:
1. 在editor中手动创建
这部分很多博客都有介绍,简单介绍,有几个关键地方提一下。
创建terrain对象,它自动包含一个terrain组件,包含了所有属性设置。
点击这个import raw...按钮,导入你的高度图,我从wm中导出的heightmap资源采用.r16格式(16位raw),选择后弹出这个对话框:
unity会自动识别heightmap的一些信息。我的平台选择windows(mac有什么不同呢?)。terrain size设置包括地形大小和高度,注意这个y值是你希望的地形高度中最大值和最小值之间的差值,它决定了你的地形的整体高度。我一开始对这个值的存在有一点疑惑,直到方法2的实现才明白其中的细节(先按下不表)。另外一个最重要的地方,一定要勾选flip vertically,为什么呢?wm三维空间中采用右手系(y轴向上),而unity采用左手系(y轴也向上),在二者x轴正向相同的情况下,z轴恰好反转。如果没有勾选这个选项,你的texture显然就没法用了。设置完毕后,效果如下:
加上贴图也很简单,不过要将texture尺寸设置为和地形一样的大小,最终效果如下:
如此,一块地形就做好了,可以将它做成prefab方便在场景中调用。不过这种方法的问题是,我的大地形可能要分许多块,那我总不能每一块都这样手工做一遍吧...不管工作量是否允许,面对这种重复工作我们都应该尽量去找到自动化实现的方法!因此就有了方法2。
2. 通过editor扩展批量生产
不过说到底这个方法还是看的别人的,在unity community wiki这个社区发现的一篇好帖,上链接。看完发现这个帖子是在unity3版本时创建的,似乎现在这个社区都没有什么人光顾了。
unity允许对editor功能进行扩展,方法是在editor文件夹下创建脚本,使用的api大多在unityeditor命名空间中,这样我们可以用脚本代替一些手动工作。它的意义在于脚本和一些配置文档(比如txt,xml格式)作为静态的存在,可以保存我们的编辑指令、设置、操作等,方便完成对多个不同对象的重复操作,简单来说就是说就是“自动化”地“批量生产”。
现在,我们要编写editor script来实现方法1中的工作。那么首先我们要查询script api文档,搞明白地形创建的流程。地形的核心在于terraindata,每一次我们在hiearchy视图中创建一个terrain对象时,unity会自动为它创建一个terraindata类型的对象,如图那个灰色的东西就是。
我们的heghtmap和texture都要交付于这个类型来实现。这里,我们先只介绍heightmap是如何加载进terraindata对象的。先上代码:
using system.collections; using system.collections.generic; using unityengine; using unityeditor; using system.io; using system; public class importterrain : monobehaviour { [menuitem("assetdatabase/importterrain")] static void generateterrain() { string terraindatapath = "assets/512test/td.asset"; //路径 //创建terraindata对象 terraindata terraindata = (terraindata)assetdatabase.loadassetatpath( terraindatapath, typeof(terraindata)); if (!terraindata) { terraindata = new terraindata(); assetdatabase.createasset(terraindata, terraindatapath); } //地形设置 vector3 terrainsize = new vector3(512, 250, 512); //地形的大小,最高、最低点的差值 terraindata.size = terrainsize; terraindata.heightmapresolution = 513; float[,] height = new float[512, 512]; //地形高度数组 //读取高度文件 fileinfo hmfile = new fileinfo(@"assets/512test/output_x0_y0.r16"); filestream hmfs = hmfile.openread(); const int bytesize = 512 * 512 * 2; byte[] hmb = new byte[bytesize]; int result = hmfs.read(hmb, 0, bytesize); //读取.r16字节流 hmfs.close(); //赋值 int i = 0; for (int x = 0; x < 512; x++) { for (int y = 0; y < 512; y++) { //注意两点: //windows字节序 //右手系转换到左手系 height[511-x, y] = (hmb[i++] + hmb[i++] * 256.0f) / 65535.0f; } } terraindata.setheights(0, 0, height); //一切都是为了这个方法... } }
这个脚本在editor中添加了一个importterrain按钮,点击这个按钮会自动创建一个名为hd的terraindata资源,有了这个资源我们可以在场景中使用脚本很方便地创建terrain对象。这个功能实现的重点在于:如何从.r16格式的高度图文件中提取我们需要的terraindata.setheights方法的参数——一个二维float型数组。
首先,如何解析.r16文件?从worldmachine中导出的.r16文件时一种二进制文件,存储了所有高度的字节,每个高度都有16位精度(而raw格式是8位)。比如从wm中导出一张分辨率为512*512的heightmap,格式存储为.r16,这样每个高度2byte,共512*512*2byte=512kb,查看一下.r16文件大小果然是这样。那么使用.net平台的库函数可以很方便地将这些字节读取到内存中,存储为一个字节数组。
其次,如何解析这个字节数组?显然每两个相邻字节表示一个高度值,在windows中字节序是低8位在前,高8位在后存储(ios正好相反)。从帖子中的代码来看,.r16是按照行优先存储的,并且是以左上角为坐标原点。还记得我们使用editor中import heightmap按钮时,弹出界面有一个flipp y(翻转y轴)的选项吧?没错,我们的代码要考虑左手系与右手系的不同。最后一点,地形高度数组的元素全部是介于0~1之间的float型数值。实际高度值还由最大高度和最小高度之间的差值决定,这个值在terraindata.size中设置。我们可以发现这里的实现,与方法1中高度图对话框的设置之间具有明显的对应关系,也能够解释方法1中我们留下的一些疑问。
总结一下,这个脚本只是我参照原贴中javascript脚本,用c#写的测试脚本,存在着许多问题。比如,只实现了高度图的导入,并没有texture;代码可能比较丑陋可靠性完全没有考虑;并没有结合配置文件来实现所谓的“批量生产”...就当是一个初步研究成果吧,后续我会在项目中完善剩余的功能,编写完整的代码模块。也希望继续分享后续的unity研究,欢迎各位开发者参与讨论!
参考文献(部分文中也有):
1.
2. 方法2的原文链接
4. msdn
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 使用第二层路由跟踪进行排错