最近因为工作需要,开始接触了一些三维数据格式。项目是基于Cesium.js进行的开发,三维场景免不了使用到地形数据,Cesium官方以及CesiumLab都提供了将DEM数据转换成quantized-mesh地形瓦片的生成工具。但是由这些工具生成的地形瓦片不太好控制(比如说我想更改其中部分三角网的高程),因此参考了官方文档写了一个三维瓦片生成的小工具,其中碰到了一点小坑,这篇文章在官方文档的基础上加入了一些个人理解,留作记录。
1. 数据组织形式
首先需要对数据组织形式有个清晰的认知,这边我先提前处理好了一个DEM,将其处理成地形瓦片的结果如下图所示,由于我在做数据处理的时候设置了最大生成层数为8,因此可以看到最上层是0-8九个文件夹以及layer.json,meta.json,这两个文件比较简单放在文章最后说,点开任意一个文件夹,下层依然是文件夹,再进入某个文件夹终于看到terrain数据,这里文件的组织形式其实就是官方文档的简单多分辨率四叉树网格金字塔(simple multi-resolution quadtree pyramid of meshes)。金子塔遵从TMS(Tile Map Service)切片规则,简单来说就是将全球的经纬度范围(-180~180,-90~90)先划分成两个格网(-180 deg, -90 deg) - (0 deg, 90 deg) ,(0 deg, -90 deg) - (180 deg, 90 deg),即图中名称为0的文件夹下的文件,然后每个格网内四叉树递归划分......回到terrain数据格式上,理论情况下层级为2的文件夹下应该是0~7,8个文件夹(2^3),每个文件夹下应该有0~3四个terrain文件,图中则是DEM所在范围的格网。
2. 瓦片格式分析
每个瓦片其实就是dem在这个瓦片范围内的点以及瓦片的顶点构建的三角网。
2.1 瓦片头信息
struct QuantizedMeshHeader { // 瓦片中心点的地心坐标 double CenterX; double CenterY; double CenterZ; // 这块瓦片的最低和最高的高程 float MinimumHeight; float MaximumHeight; // 瓦片的球体包围盒信息,其中xyz与瓦片中心点的地心坐标一致,radius为包围球半径 double BoundingSphereCenterX; double BoundingSphereCenterY; double BoundingSphereCenterZ; double BoundingSphereRadius; // 地平线遮挡点 // 更多信息:http://cesiumjs.org/2013/04/25/Horizon-culling/ for more information. double HorizonOcclusionPointX; double HorizonOcclusionPointY; double HorizonOcclusionPointZ; };
讯享网
讯享网double HorizonOcclusionPoint[3] = { centerX / , centerY / , centerZ / . };
2.2 瓦片顶点信息
struct VertexData { unsigned int vertexCount; // 顶点个数 unsigned short u[vertexCount]; // 顶点横坐标 unsigned short v[vertexCount]; // 顶点纵坐标 unsigned short height[vertexCount]; // 顶点高程值 };
紧跟着头信息的是顶点信息,首先以一个无符号整型存储顶点的个数,然后是三个短整型数组存储顶点坐标,为了方便存储和数据传输,这里使用了zig-zag编码方法对数据进行压缩,根据官方文档给出了顶点坐标的解码方法:
讯享网int zigzagDecode(unsigned short value) { return (value >> 1) ^ (-(value & 1)); } int u = 0; int v = 0; int h = 0; int divideSum = 32767; for (unsigned int i = 0; i < vertexCount[0]; i++ ) { u += zigzagDecode(uvh[i]); v += zigzagDecode(uvh[vertexCount[0] + i]); h += zigzagDecode(uvh[vertexCount[0] * 2 + i]); double lon = minx + double(u) / divideSum * (maxx - minx); double lat = miny + double(v) / divideSum * (maxy - miny); double hei = height[0] + double(h) / divideSum * (height[1] - height[0]); }
double vectorX[vertexCount];double vectorY[vertexCount];double vectorZ[vertexCount];
讯享网unsigned short zizagEncode(short value) { return (value >> 15) ^ (value << 1); } short* uList = new short[vertexCount]; short* vList = new short[vertexCount]; short* hList = new short[vertexCount]; for (unsigned int i = 0; i < vertexCount; i++) { uList[i] = 32767 * (vectorX[i] - minx) / (maxx - minx); vList[i] = 32767 * (vectorY[i] - miny) / (maxy - miny); hList[i] = 32767 * (vectorZ[i] - minHeight) / (maxHeight - minHeight); } for (unsigned int i = vertexCount -1; i > 0; i--) { uList[i] = uList[i] - uList[i - 1]; vList[i] = vList[i] - vList[i - 1]; hList[i] = hList[i] - hList[i - 1]; } unsigned short* uvh = new unsigned short[vertexCount * 3]; for (unsigned int i = 0; i < vertexCount; i++) { uvh[i] = zizagEncode(uList[i]); uvh[vertexCount + i] = zizagEncode(vList[i]); uvh[vertexCount * 2 + i] = zizagEncode(hList[i]); }
顶点之后是用来强制字节对齐的占格符,用来确保IndexData16为2字节对齐和IndexData32为4字节对齐。在二进制文件中,经常会出现强制对齐的现象,本人是这样理解的:前面的变量类型有double,float,int,short,其中double,float,int一定是2或者4的整数倍,short的数组,一定是2的整数倍,但是不一定是4的整数倍,这个对齐的意思就是当定点数大于65536时,为了使数据对齐,就需要填充空格使得前面的字节总数一定是对应数据类型的整数倍。这个地方在解析的时候需要注意,如果漏掉了会出现解析不正确Cesium无法正常加载的情况。
然后便是三角面的顶点索引数据,也就是顶点坐标在顶点数组中的位置(下标)。数据三个一组,指定顶点如何链接在一起成三角形。如果tile具有超过65536个顶点,则tile使用IndexData32结构对索引进行编码。否则,它使用IndexData16结构。
struct IndexData16 { unsigned int triangleCount; // 三角形个数 unsigned short indices[triangleCount * 3]; // 三角形顶点索引 } struct IndexData32 { unsigned int triangleCount; unsigned int indices[triangleCount * 3]; }
讯享网int highest = 0; for (var i = 0; i < indices.length; ++i) { int code = indices[i]; indices[i] = highest - code; if (code == 0) { ++highest; } }
三角面索引后是边缘顶点索引信息,这些信息记录下来主要是为了瓦片避免边缘贴合度不够产生的裂缝问题。边缘顶点由于相邻两个点的差值很大不适合做zigzag压缩编码因此直接存储了原始顶点索引id,根据顶点是否大于65536,定义的两种数据结构如下:
struct EdgeIndices16 { unsigned int westVertexCount; unsigned short westIndices[westVertexCount]; unsigned int southVertexCount; unsigned short southIndices[southVertexCount]; unsigned int eastVertexCount; unsigned short eastIndices[eastVertexCount]; unsigned int northVertexCount; unsigned short northIndices[northVertexCount]; } struct EdgeIndices32 { unsigned int westVertexCount; unsigned int westIndices[westVertexCount]; unsigned int southVertexCount; unsigned int southIndices[southVertexCount]; unsigned int eastVertexCount; unsigned int eastIndices[eastVertexCount]; unsigned int northVertexCount; unsigned int northIndices[northVertexCount]; }
紧跟着边缘顶点的是瓦片的扩展信息,虽然说是扩展信息但是如果想要自行做地形切片,这部分基本上必须使用,特别是Meta元信息,因此非常有必要掌握。每个扩展信息按照头信息-主体信息的数据格式进行存储,其中扩展头信息的数据结构如下:
讯享网struct ExtensionHeader { unsigned char extensionId; unsigned int extensionLength; }
struct Metadata { unsigned int jsonLength; char json[jsonLength]; }
{
"available": [
[
{
"endX": 3,
"endY": 1,
"startX": 3,
"startY": 1
}
],
[
{
"endX": 6,
"endY": 2,
"startX": 6,
"startY": 2
}
],
[
{
"endX": 12,
"endY": 5,
"startX": 12,
"startY": 5
}
],
[
{
"endX": 25,
"endY": 10,
"startX": 25,
"startY": 10
}
]
...
]
}
3. json文件分析
首先是layer.json,文件内容如下,大部分内容都还比较直观,其中available即瓦片的范围和第0层的meta信息比较类似,这里也就不再展开了,这个validbounds即dem的范围。minzoom和maxzoom为缩放层级,这个怎么改都不会影像文件的解析和显示。meta.json的文件内容就更简单了,这边也贴个截图, 其中Bounds和latlonBounds都是DEM的经纬度范围。
总结:

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/13683.html