在unity中,我们可以使用unity自带的地形系统创建一个超大的地形场景,并且可以利用地形图层,创建出富有真实感的地表材质。但是当我们需要更改地形的渲染方式的时候,比如需要风格化渲染时,使用unity自带的地形系统就会很麻烦。因此,我尝试在unity中使用mesh的方式实现了一个简易的地形系统,这样地形的渲染就和场景中其他网格物体的渲染没有什么区别了,可以很方便地实现各种效果。
以下我将分步简要描述实现思路与过程,目录如下:
1、 将world machine中创建的地形导入到unity的mesh中;
2、 处理地形数据,按照四叉树层级分块;
3、 根据摄像机位置动态地组合分块,达到动态地形LOD的效果;
以下是实现的具体过程,个人编码水平比较低,希望大家多多提出意见。
一、导入地形数据
在unity自带的地形系统中,我们可以通过一张高度图的方式将地形数据从地形生成软件(World Machine,Gaea等)导入到unity引擎中,这张高度图通常是原始图片数据格式(.raw)。.raw格式的图片保存了最原始、无损的信息,而我们常用的png、jpg图片采用了一些压缩算法来减小图片的大小,但是会损失一些数据,因此在程序化地形工作流中,我们常常使用.raw来保存地形信息。
但是.raw格式的图片并不方便,windows本身是无法查看这类图片的,而C#也需要配置一些环境,并且也比较复杂。因此我选择不使用.raw图片来存储地形数据,转而直接使用模型文件.obj格式。
在World Machine中,我们可以直接导出.obj文件格式,方式如下:
其中Triangulation项表示导出模型的精度,我们选择Full Resolution Mesh,创建完整的模型。第三项坐标系统,我们选择X=east Z=north,以匹配unity的坐标系统。其他导出选项可以根据需要来选择。
然后我们就获得了地形网格模型,是常见的obj文件,本质上是一个,分别用建模软件和VS code查看如下
可以看到三角形网格和以字符文本形式保存的模型信息。在.obj文件中,v关键字表示顶点,后面三个值分别表示该顶点的x、y、z位置坐标。
现在我们获得了地形的完整的模型文件,在使用它之前需要先将它按照四叉树分割成小块,具体如下图所示:
对于本例而言,地形大小为4096*4096,因此采用六级LOD,最终LOD0有1024块,所有LOD分块一共1365块,我们先从obj文件中提取顶点数据,再根据每一块的LOD级别与覆盖范围分别建立网格。
因此,我们先用C++写一个小程序来将顶点信息提取出来,存入到一个文本文件中,方便后续建立网格时使用。
观察顶点数据,发现顶点数据按照行-列的方式排列,这方便了后续的操作。因此,为了提高文件读取的效率,可以将x轴与z轴的数据抛弃,通过在第一行添加一个常数来表示x、z平面上顶点的间隔距离,并且添加第二个常数来表示地形高度的乘数,增强了我们对网格的控制。
在完善之后,这一部分的C++代码如下:
#include <iostream> #include <fstream> #include <sstream> #include <cstring> #include <string> using namespace std; struct vec3{ int x; float y; int z; }; vec3 vertices[16785409]; int main() { fstream newtxt; newtxt.open(R"(D:\WorldMachine\World Machine 4016\World Machine 4016 Professional\World Machine Documents\Desert\TerrainData.txt)", ios::out); fstream myObj; myObj.open(R"(D:\WorldMachine\World Machine 4016\World Machine 4016 Professional\World Machine Documents\Desert\height.obj)", ios::in); if(!myObj.is_open()) std::cerr<<"cannot open the file"; // 输出先导数据 float xz_bias = 5; float y_bias = 1; newtxt << xz_bias << " " << y_bias << endl; // 全局 char buffer[1024] = {0};// 读取文件行缓存 int mesh_num = 0; // 网格(物体)计数编号 int count_num = 0; // 行数计数编号 int cnt_vert = 0; while (myObj.getline(buffer,sizeof(buffer))){ count_num ++; stringstream ss(buffer); // 一行的字符串流操作 if (ss.str().empty()) // 空行 continue; string flag; // 行首关键字 ss >> flag; if (flag == "#"){ cout << "第 " << count_num << " 行: " << "这是一行注释, " << "注释的内容是:" << ss.str() << endl; } else if (flag == "mtllib"){ continue; } else if (flag == "v"){ // 顶点 float a, b, c; ss >> a >> b >> c; vertices[cnt_vert].x = (cnt_vert % 4097); vertices[cnt_vert].y = b; vertices[cnt_vert].z = (cnt_vert / 4097); newtxt << b << endl; cnt_vert ++; } else cout << ss.str() << endl; memset(buffer, 0, sizeof(buffer)); } myObj.close(); cout << "读取文件完成力" << endl; return 0; }
在拿到处理后的顶点数据后,我们打开unity,新建一个脚本。
首先创建如下的成员变量:
// 需要读取的文件路径 public string datapath; private float xzbias = 0, ybias = 0; // 格点间隔距离与高度乘数 public int Size = 4097; // 地形的规模(正方形边长,要求是2的幂+1) private int LEN; // 常数,表示完整顶点组的长度,等于Size^2 private int len; // 常数,表示单片顶点组的长度,等于129*129 private Vector3[] CompVert; // 完整的地形顶点数据,在开始时从文本文件读取数据到该顶点组,在构建mesh时按需从该顶点组读取数据 private Vector2[] CompUV1; // CompVert对应的第一套UV(地表贴图纹理uv) private Vector2[] CompUV2; // CompVert对应的第二套UV(遮罩图贴图splat map uv) private Mesh LodMesh; // 结果的网格,在每次四叉树遍历过程中即时更新该mesh并保存至本地,因此不需要开数组 private Vector3[] vert; // 结果的顶点数据 private Vector2[] uv1; private Vector2[] uv2; private int[] tri; // 结果的三角形数据(不需要在每次遍历中更新) private struct quadTreeNodeInfo // 地形瓦片 四叉树节点 信息 { public Vector2 begin_Pos; // 开始顶点在完整顶点组中的第几行、第几列 public int interval; // 该地形瓦片的连续顶点在完整顶点组中对应顶点相差数量(LOD0对应该值为1,LOD2对应该值为4) public int LodLeval; // 该地形瓦片的lod等级 public Vector2 Center; // 该地形瓦片的xz平面几何中心位置 } private quadTreeNodeInfo[] qTree; // 四叉树
标签:int,private,地形,unity,World,顶点,四叉树 From: https://www.cnblogs.com/yaocenji/p/17290337.html