本文节选自新书《GIS基础原理与技术实践》第7章。很多人以为三维建模只能靠 3ds Max 或 Blender,但在 GIS 中,我们完全可以从 DEM 出发,用代码手动生成带颜色、带纹理、甚至符合现代 glTF 标准的三维地形模型。本文带你一步步实现 PLY 白模、OBJ 纹理贴图、glTF 资产封装,揭开三维 GIS 的底层逻辑。
导言
在前三章中,笔者详细论述了矢量、栅格和地形相关的知识,这三种数据也是GIS中最为基本的三种地理空间数据。不过,这三种传统的地理空间数据通常被认为是二维的,其处理方法也大多数基于二维平面空间。随着越来越复杂的地理空间信息的需求,GIS也在逐渐向三维方向发展,三维GIS成为了地理信息系统科学中炙手可热的研究方向。将三维模型作为基本的地理空间数据的观点目前可能还未进入学校的经典教材,但已经有这个趋势。在本章中,笔者也总结了一些与GIS相关的三维模型的知识,希望给读者以参考。
7.1 初识三维模型
7.1.1 三维模型的数据载体
随着计算机图形技术的发展,我们或多或少都会见过或者听说过三维模型。笔者始终记得小时候第一次在电视上看到三维动画《变形金刚:超能勇士》的震撼感受;而现在我们已经可以在手机上玩三维游戏《王者荣耀》,实时操作造型精美的英雄模型了。这些东西的背后都离不开三维模型数据,他们往往是通过像Autodesk 3D Max这样的三维建模软件制作出来的。
如同栅格数据和矢量数据一样,三维模型数据也有形形色色的数据格式。这些不同的数据格式有时来源于不同的三维建模软件;一些机构或者组织出于标准化的目的,也会定义某种通用的三维模型数据格式。这些不同格式的数据文件构成了三维模型的数据载体。常用的三维数据格式如下表7.1所示:
名称全称特点PLYPolygon File Format描述最简单OBJWavefront .obj file经典通用性高3DS3D Studio广泛应用的经典格式MAX3D Studio Max3DMax专属格式glTFGraphics Language Transmission Format现代自由开放,适合OpenGL管线FBXFilmbox现代而全面的格式,游戏引擎中常用对这些三维数据格式我们可以做一个大致的认识,因为后面可能会直接用到:
- PLY是一种最简单的三维数据格式,一般用其存储不带纹理的模型数据,可通过文本和二进制两种形式来描述。
- OBJ是非常通用的三维模型数据格式,与PLY相比增加了对材质的描述,包括纹理信息。因此一个典型的OBJ格式的文件除了.obj文件,同时还会附带一个.mtl文件用于描述材质。而在材质文件中就可以指定纹理图片的地址。很长一段时间内由于其对三维模型文件的描述比较全面,通常用于不同三维建模软件的中转。
- 3DS和MAX属于著名三维建模软件Autodesk 3ds Max的专属的文件数据格式。理论上来说,3DS和MAX都属于商业三维数据格式,但是Autodesk 3ds Max在三维建模上的使用非常广泛,所以这两种数据格式也很常见。不同的是,3DS现在已经基本能够各种三维软件所识别,一些开源工具也能解析识别;Max则是Autodesk 3ds Max所专用,其他三维软件或者开源工具一般都不支持。Max还有一个笔者认为不太好的特点,就是版本迭代太快,低版本的Autodesk 3ds Max无法打开高版本的Max格式数据。
- glTF是一种自由开放,无专利限制的,适合传输和加载3D模型和场景的文件格式。相比较前面介绍三维数据格式来说,glTF诞生的时间较晚,可以采用了更为现代的图形技术来封装和组织这个格式。Khronos Group制定和维护了glTF数据格式的标准,同时由于其也是OpenGL接口标准的指定者和维护者,因此glTF特别适合OpenGL系列(OpenGL,OpenGL ES,WebGL)的图形渲染流水线所需要进行的处理。这个特点意味着glTF足够轻量化。目前glTF有1.0和2.0两个版本,其中glTF2.0已经成为了ISO国际标准。
- FBX同样也是Autodesk公司开发的一种通用三维数据格式。与glTF一样,FBX也是更为现代的三维数据格式,比如支持显示效果更为真实的PBR材质。FBX广泛应用于游戏开发领域,目前最火的三维游戏引擎Unity和Unreal都支持直接导入这种格式。Autodesk官方为FBX提供了开发包,支持解析和修改该格式数据文件。除此之外,也有一些开源第三方组件使用自己的方式兼容它。
综合来说,PLY、OBJ和3DS都属于比较早期的三维模型数据格式,受限于当年的图形技术的认知;而Max、glTF和FBX则设计得更为现代,文件组织结构更为合理,能提供更为强大的可视化效果。例如,现代三维模型数据格式已经不仅仅是像早期三维模型数据格式那样只包含模型数据本身,还会包括材质、动画、灯光甚至相机等,其描述的对象可以是整个三维场景。
7.1.2 从地形来认识三维模型(PLY格式)
如果没有三维图形的基础知识,上一小节的论述可能会让有的读者一头雾水。那么我们可以从GIS中的地形开始说起——在第6.3节中我们就已经使用过PLY格式的三维数据,将其表达成不规则三角网地形。但是,如图6.6所示的地形实在过于简陋,有没有办法给这个白模赋予着色信息,使其有更好的可视化效果呢?
一种最简单的可视化优化方案是,可以结合第6.5节中晕渲图的实现,创建一个带颜色信息的地形三维模型数据。如下例7.1所示:
[code]//例7.1 DEM数据转换PLY三维模型#include #include #include #include #include #include using namespace std;struct VertexProperty { double x; double y; double z; uint8_t red; uint8_t green; uint8_t blue;};size_t vertexCount;vector vertexData;size_t faceCount;vector indices;//颜色查找表using F_RGB = std::array;vector tableRGB(256);//生成渐变色void Gradient(F_RGB& start, F_RGB& end, vector& RGBList) { F_RGB d; for (int i = 0; i < 3; i++) { d = (end - start) / RGBList.size(); } for (size_t i = 0; i < RGBList.size(); i++) { for (int j = 0; j < 3; j++) { RGBList[j] = start[j] + d[j] * i; } }}//初始化颜色查找表void InitColorTable() { F_RGB blue({17, 60, 235}); //蓝色 F_RGB green({17, 235, 86}); //绿色 vector RGBList(60); Gradient(blue, green, RGBList); for (int i = 0; i < 60; i++) { tableRGB = RGBList; } F_RGB yellow({235, 173, 17}); //黄色 RGBList.clear(); RGBList.resize(60); Gradient(green, yellow, RGBList); for (int i = 0; i < 60; i++) { tableRGB[i + 60] = RGBList; } F_RGB red({235, 60, 17}); //红色 RGBList.clear(); RGBList.resize(60); Gradient(yellow, red, RGBList); for (int i = 0; i < 60; i++) { tableRGB[i + 120] = RGBList; } F_RGB white({235, 17, 235}); //紫色 RGBList.clear(); RGBList.resize(76); Gradient(red, white, RGBList); for (int i = 0; i < 76; i++) { tableRGB[i + 180] = RGBList; }}//根据高程选颜色inline int GetColorIndex(double z, double min_z, double max_z) { int temp = (int)floor((z - min_z) * 255 / (max_z - min_z) + 0.6); return temp;}void ReadDem() { string workDir = getenv("GISBasic"); string demPath = workDir + "/../Data/Model/dem.tif"; GDALDataset* dem = (GDALDataset*)GDALOpen(demPath.c_str(), GA_ReadOnly); if (!dem) { cout GetRasterYSize(); //坐标信息 double geoTransform[6] = {0}; dem->GetGeoTransform(geoTransform); double srcDx = geoTransform[1]; double srcDy = geoTransform[5]; double startX = geoTransform[0] + 0.5 * srcDx; double startY = geoTransform[3] + 0.5 * srcDy; double endX = startX + (srcDemWidth - 1) * srcDx; double endY = startY + (srcDemHeight - 1) * srcDy; size_t demBufNum = (size_t)srcDemWidth * srcDemHeight; vector srcDemBuf(demBufNum, 0); int depth = sizeof(float); dem->GetRasterBand(1)->RasterIO(GF_Read, 0, 0, srcDemWidth, srcDemHeight, srcDemBuf.data(), srcDemWidth, srcDemHeight, GDT_Float32, depth, srcDemWidth * depth); GDALClose(dem); double minZ = *(std::min_element(srcDemBuf.begin(), srcDemBuf.end())); double maxZ = *(std::max_element(srcDemBuf.begin(), srcDemBuf.end())); vertexCount = (size_t)srcDemWidth * srcDemHeight; vertexData.resize(vertexCount); for (int yi = 0; yi < srcDemHeight; yi++) { for (int xi = 0; xi < srcDemWidth; xi++) { size_t m = (size_t)srcDemWidth * yi + xi; vertexData[m].x = startX + xi * srcDx; vertexData[m].y = startY + yi * srcDy; vertexData[m].z = srcDemBuf[m]; int index = GetColorIndex(srcDemBuf[m], minZ, maxZ); vertexData[m].red = (uint8_t)(tableRGB[index][0] + 0.5); vertexData[m].green = (uint8_t)(tableRGB[index][1] + 0.5); vertexData[m].blue = (uint8_t)(tableRGB[index][2] + 0.5); } } faceCount = (size_t)(srcDemHeight - 1) * (srcDemWidth - 1) * 2; // indices.resize(faceCount); for (int yi = 0; yi < srcDemHeight - 1; yi++) { for (int xi = 0; xi < srcDemWidth - 1; xi++) { size_t m = (size_t)srcDemWidth * yi + xi; indices.push_back(m); indices.push_back(m + srcDemWidth); indices.push_back(m + srcDemWidth + 1); indices.push_back(m + srcDemWidth + 1); indices.push_back(m + 1); indices.push_back(m); } }}void WriteDemModel() { string workDir = getenv("GISBasic"); string demPath = workDir + "/../Data/Model/dst.ply"; ofstream outfile(demPath); if (!outfile) { printf("write file error %s\n", demPath.c_str()); return; } outfile |