手机三维地图显示引擎的设计与实现
2020-02-05顾茂强
顾茂强
(首钢工学院 计算机与媒体艺术学院 北京市 100041)
近年来,随着CPU 等硬件性能的大幅提升,以及无线网络模块、GPS、电子陀螺仪等配件的配置,再加上简单易用的手机操作系统iOS 和Android 的加持,智能触屏手机已成为较适合地图APP 的运行平台。由于手机的便携性,手机地图APP 日益频繁地出现于日常的活动中,包括打车、订餐、导航等,甚至不少司机将其用作主导航系统,而将车载导航系统用作手机电池电量不足或接听电话时的备份[1]。
手机地图APP 的常见的功能主要包括路径规划、方向引导、地图定位匹配、地图信息检索、地图渲染显示等,其中地图渲染显示功能为其他功能提供显示、交互支持,是手机地图APP 的核心部件[2]。而iOS 和Android 都提供了OpenGL ES 的接口,完全满足三维地图开发的要求。与二维地图相比,三维地图直观立体的可视化效果更符合人们的认知习惯和认知规律[3],能够满足用户在手机上日益增长的图形交互体验的需求。
随着三维渲染技术研究的不断发展,以及电子地图应用的不断深人,不少公司和学者正将三维技术应用于地图可视化。目前主流的手机地图App 如google maps、高德地图等,均采用了三维地图作为导航地图展示的主要方式[4]。OSM(OpenStreetMap)已经成为权威数据的的一个重要的替代来源,Brinkhoff Thomas 从OSM 要素中获取足够精度和性能的建成区和城市区域,并对结果的质量进行探讨[5];Brovelli Maria 和Zamboni Giorgio 通过地图匹配和相似性检查,从OSM 中检测出建筑物、计算出完整性指数[6]。架构对手机地图渲染显示功能来说也很重要,José M.Noguera 等人根据渲染操作发生在客户端还是发生在服务端,将地图渲染架构可以分为本地渲染架构、客户端渲染架构、服务端渲染架构、混合渲染架构,设计、开发了三维地图混合渲染架构,在移动客户端和远程服务器间自适应分配地图渲染任务[7];王亚美、鲁田在Android 手机上实现了二、三维地图的渲染功能,阐述二、三维地图渲染用到的缓冲机制、多线程机制、模型精简等关键技术[8]。
为了实现三维地图的手机端显示,本文基于iOS 或Android 系统上的OpenGL ES 2.0,归纳总结了手机上三维地图渲染引擎的开发流程,以OSM 为数据来源,以SDK 为主体设计且实现了手机平台的三维地图渲染引擎,并开发了相应的应用程序。该三维地图渲染引擎的SDK 增强了对地图的三维动态可视化描述,具有简单的开发接口,能简化客户端的开发流程,具有一定的实用价值和应用前景。
1 整体设计
一个完整的三维地图渲染引擎包括服务器端和客户端两个部分,如图1 所示。
图1:地图渲染的整体架构
图2:地图渲染矢量数据的逻辑模型
1.1 地图客户端
地图客户端划分为3个层次,即客户端OS层、SDK层和App层。其中地图渲染SDK 是地图客户端乃至整个地图渲染功能的核心,它适配主流的Android 与IOS,被APP 层调用,对UI 事件做出响应,调度管理地图矢量数据和样式数据,基于OpenGL ES渲染基础地图、3D 模型等要素。
地图渲染SDK 又可以划分为第三方库、渲染引擎、地图引擎和接口封装等几个子层。
第三方库子层为整个渲染SDK 提供基础函数功能,包括开源字体引擎库FreeType 库、跨平台(Windows, Mac,Linux)应用程序框架类库JUCE、PNG 文件读写的库libpng、嵌入式数据库sqlite3 等。
渲染引擎响应来自于APP UI 的touch 事件,为地图引擎提供地图元素渲染服务。其中,资源管理器实现对图片资源、3D 模型的本地文件到内存的加载、卸载以及网络下载等管理;字体管理器基于FreeType 开源软件库实现对字体文件的加载、更换以及字体图片的渲染等工作;相机模块保存了地图地理中心点GeoCenter 等状态信息,接受来自相机控制模块的控制并因此更新自己的状态参数及相关的坐标变换矩阵,提供坐标变换服务;相机控制器提供了包含了动画参数的平移、旋转、缩放、倾斜等接口,接受手势识别模块的调用;3D 对象渲染器实现对3D 对象的渲染。
地图引擎对地图矢量数据和地图样式数据进行管理,调用渲染引擎中的3D 对象渲染器的接口进行地图等对象的渲染。其中,矢量数据管理模块采用三级缓存机制对矢量瓦片数据进行管理并对数据的渲染前3D 建模准备(如建筑数据的柱状拔高、道路线的扩展成面等)等;样式数据管理模块主要用于实现对多个地图渲染样式(style)文件的加载、当前被使用的地图渲染样式(style)文件的设置和替换、样式元素的查询等操作;基础地图(BaseMap)渲染模块调用渲染引擎中的3D 对象渲染器的接口进行绿地、河流、道路、树木模型等对象的渲染。
MapView 类是一个C++封装类,它采用Facade(外观)模式,为地图渲染SDK 中的各类(或结构与方法)提供一个简明一致的界面,隐藏地图渲染SDK 的复杂性,使之更加容易使用。
而地图渲染SDK 是使用C++语言开发的,针对Android 上的APP 需要把MapView 封装成JNI 接口,针对iOS 操作系统上的APP 需要把MapView 封装成Objective-C 接口,接口封装子层主要是完成这个功能。
1.2 地图数据服务端
地图数据服务端的cache 中存放着地图渲染数据包括矢量瓦片数据、3D 模型数据等,对客户端的数据请求作出响应。
瓦片数据是由来自于OSM 的地图要素数据(如背景面、背景线、道路、楼块、3D 模型、以及文字标注等)经过处理转换得到的。在该处理转换的过程中,首先对mid/mif 格式的地图原始数据进行数据过滤等预处理,然后采用按比例尺分层、层内分块、层级数据复用的方式对地图进行组织,最后将块(即瓦片)内地图数据要素存储成符合OpenGL ES 图形API 需求的二进制数据格式。层级间数据复用的目的在于减少最终数据量。图2 即地图渲染矢量数据的逻辑模型。
除了少量3D 模型外不做大规模手工3D 建模,而是在客户端由地图矢量数据生成3D 模型,以缩短数据制作时间、保证地图绘制及开发的轻量级和高效。
本文采用业界常用的图2 所示。
2 关键技术
2.1 三级缓存机制和线程模型
地图渲染SDK 主要的工作流程是由客户端APP 的UI touch 事件操作发起的。该事件被地图渲染SDK 的手势识别模块转换成对相机控制模块的平移、旋转、缩放、倾斜等接口的调用,从而更新了相机的状态参数。地图渲染SDK 根据相机状态参数计算出屏幕范围对应的地理范围及其内的待渲染的瓦片ID 列表,从矢量数据管理模块的缓存中提取出相应的瓦片数据进行渲染。
图3:地图渲染SDK 的线程模型
图4:视图变换和投影变换
图5:两遍法渲染天空
图6:水系的渲染效果
图7:反走样纹理图及其alpha 分量分布
图8:建筑阴影的构建
为了提高客户端地图数据的处理效率,矢量数据管理模块采用了瓦片三级缓存机制。第三级为存储了所有地图数据瓦片远程地图数据服务器;第二级为移动终端本地sqlite 数据库,从服务器获取的地图瓦片数据首先缓存到本地sqlite 数据库,并采用LRU(最近最少使用)算法替换数据。第一级为内存,从本地sqlite 数据库获取的数据会缓存到内存中,最多缓存64 个瓦片,数据替换的算法也是LRU。提取瓦片数据时,矢量数据管理模块首先从速度最快的内存缓存中查找,查找不到的瓦片再读取较慢的本地sqlite 数据库;因为从远程服务端下载数据速度最慢,所以第三级缓存是提取瓦片数据的最后选择。
三级缓存机制的相关操作都是很耗费时间的操作。为避免UI界面和地图渲染的卡顿现象,提升整体处理性能,采用多线程模式,将手势事件处理、地图渲染、数据获取操作分别放入不同的线程,如图3 所示。
2.2 地图渲染SDK的坐标变换
地图渲染SDK 中,坐标变换前的地理坐标原点、视点(摄像头)均和OpenGL 的世界坐标系的原点重合;经过下列坐标变换后,最终变成绘制在二维屏幕上的场景:
(1)平移变换T:地图平移到指定的中心点GeoCenter(x,y,0);
(2)缩放变换S:地图缩放到指定的比例尺级别ZoomLevel;
(3)旋转变换Rz:绕z+ 轴逆时针旋转指定的方位角bearing,bearing 的取值范围为[0,360 度];
(4)旋转变换Rx:绕x+轴顺时针旋转指定的倒伏角pitch,pitch 的取值范围为[0,85 度],pitch 取0 度时,视线垂直于地图平面;
(5)视图变换V:将视点(摄像头)向z 轴正向移动一个视距sightDist 的距离,视线指向OpenGL 的世界坐标系的原点,视点的up 向量为(0,1,0),如图4 左图;
(6)地图透视投影变换P:投影矩阵P 采用类似于gluPerspective( fovy, aspect, zNear, zFar)类的轴对称透视投影函数来计算,其中fovy 取[45~60]度范围内的一个角度,各参数如图4 右图。
经过上述6 个步骤,将地图的地理坐标 (x,y,0)投影转换成屏幕坐标(xPixel,yPixel,0) ,即
3 三维地图的可视化实现
3.1 天空的渲染
在地图场景渲染天空,不仅使地图场景更加逼真,而且在大倒伏角度时能填补地图上方空缺、减少要渲染的地图矢量瓦片数量。地图天空的渲染方法有如下4 种[9]:方法1 是清除背景时用近似于天空颜色的淡蓝色,但是在三维地图场景中,这种方法渲染出来的天空因为缺乏正确的深度信息而无法遮挡天际线处的楼宇等立体模型,导致渲染出楼宇飘在天空的奇怪场景;方法2 是天空盒,它采用立方体贴图模拟的天空,这种方法较逼真,但在立方体的棱角处会产生明显的接缝;方法3 是天空穹,它用半球面模拟天空,但是渲染过程中使用过多的顶点,涉及过多的三角函数的计算,影响渲染效率;方法4 是曲面型天空模型,采用三角条带加纹理贴图的方式渲染,但是仍然涉及较多的计算。
本文采用更简单的两遍渲染的方法实现天空的渲染:
第一遍画2D 天空颜色信息:采用正射投影(ortho),打开颜色缓冲区、关闭深度检测,在屏幕视口的上部画出天空,如图5(a)所示;
图9:FPS 测试
第二遍画3D天空深度信息:在透视投影下,求出图5(a)中 A、B、C、D 四点的地理坐标,然后在关闭颜色缓冲区、开启深度检测的条件下,采用三角条带的方式渲染这4 点构成的四边形。
3.2 背景面的渲染
背景面形状包括草地、林地、水系(河流、湖泊等)等地图数据元素,采用无缝纹理平铺贴图的方式渲染的。通过对每个纹理坐标做某个方向向量的定时偏移,实现水系背景面水波纹动画的效果。
为了使得水系的渲染更加立体、逼真,突出水岸边的效果,水系以外的背景面渲染在z=0 的地平面上,而水系则渲染地平面以下的z=-Z(Z>0)平面,水系的岸边渲染树立的平面表示岸边,如图6 所示。
3.3 道路的渲染
GLES 对于三角形类型的绘制方式(如三角形条带GL_TRIANGLE_STRIP)的支持很好,三角形上纹理贴图的支持更是独到。
在矢量瓦片中,道路数据是以无宽度的折线形式提供的;而在地图SDK 中,宽度可控的道路是采用三角形条带GL_TRIANGLE_STRIP 的方式渲染的。首先对道路折线进行曲面细分,方法是沿着折线路径获取每个点的法线,并在每侧向外扩展一半的线宽,以得到三角形条带;然后设置三角形条带每个顶点的纹理坐标,把纹理图片延展到适当的顶点位置上;最后在启用混合的情况下,采用GL_TRIANGLE_STRIP 绘制方式对道路进行绘制。
道路采用两遍渲染。第一遍依然是用较大的宽度渲染出整体上圆润的道路轮廓线,方法是通过设置纹理坐标把如图7 所示的圆形反走样纹理延展到三角形条带适当的顶点位置上,使得道路轮廓线的线帽和拐点都是圆头;第二遍用略小的宽度渲染道路的车道线。
3.4 建筑阴影的渲染方法
在三维电子地图中,建筑要素及其阴影的存在,使得整个三维地图的场景真实感得到增强。
矢量瓦片中只存储了建筑底面多边形和高度信息。地图渲染SDK 是对建筑的底面进行柱状拔高建模后渲染的。
传统的阴影实现方法包括平面阴影、阴影映射[10][11]、阴影体[12]、光线跟踪等。但是阴影映射、光线跟踪两种方法的实现较为复杂,光线跟踪技术的渲染效率较差;文献[13]提出一种建筑阴影渲染方法,但是也比较繁杂。而平面阴影生成方法较简单、性能好,本文进一步简化了平面阴影技术,结合FBO 技术,用于实现太阳平行光下的建筑阴影的渲染,更加简单方便,能满足手机渲染的性能和效果要求。
以图8 为例,已知建筑底面由点B0、B1、B2、B3、B4 构成,太阳的平行光向量在地面上的投影向量为V,影子长度为shdLen,则顶面在地面上的投影相当于底面在地面上沿着V 的方向偏移shdLen 的距离,即顶面每个点在地面上的投影Si(i=1,2,...,4)坐标可以通过下面的公式计算得到:
ShdLen 的大小这里简单地指定为楼高的某个百分比,受到当前时辰的制约;V 向量则受到当前时辰和楼位置纬度的制约。
显然,图8 中阴影的三角条带坐标序列为B0、S0、B1、S1...B4、S4,可以采用三角条带方式将其直接画到地面上。但是这种方法只能渲染各个建筑的阴影相互间没有重合相交的情形,这在地图渲染中是不现实的。为解决这个问题,本文采用了FBO,因此建筑阴影的具体渲染步骤是先关闭GL_BLEND 和GL_DEPTH_TEST、通过GL 的三角条带渲染方式以灰色渲染到离屏渲染的FBO 中,然后打开GL_BLEND 和GL_DEPTH_TEST,将FBO 纹理渲染到屏幕上。
图10:16 级和18 级三维地图显示效果示例
4 性能测试
地图渲染SDK 适配了当前主流的iOS 和Android 系统。地图数据服务器端测试用的地图数据是由来源于OSM 的印尼雅加达范围内容的数据经过分层分块处理的数据。地图渲染SDK 及相应App 主要在PC 机上用VC2010 进行模拟开发;PC 上基本开发调试完成后,将代码整合移植到MAC 机上的iOS 版本导航App 中,并利用Mac 上的Xcode 环境进行性能评测和相关bug 的修正;最后在AndroidStudio 环境下微调适配Android 版本的导航App。
地图渲染SDK 性能的定量测试分析是在MacBook 上的Xcode环境下连接iPhone6 真机进行的。测试方法是一边打开Xcode 的instruments 工具,一边对三维地图画面进行匀速浏览测试,具体做法是在手机地图上先后分别用常速、较快的速度匀速左右滑动屏幕,接着用常速和和较快的速度匀速旋转屏幕。图9 是Xcode 的instruments 工具的测试结果,从中看出程序启动后稳定运行时的FPS 大于20。
从测试操作时的感觉来看,除了16 比例尺,在其他大于16 级的比例尺下的常速浏览地图时感觉不到明显的卡顿现象;而快速浏览地图时在所有大于17级的比例尺下都感觉不到明显的卡顿现象。
从测试结果来看,16 和17 级比例尺下地图浏览时就有卡顿现象,是因为这级比例尺下加载的地图数据较多,需要显示的三维立体地图要素超过了CPU、GPU 的负载能力。而在比例尺大于17 级的情况下,尽管有不少立体地图要素要显示,由于地图可显示地理范围迅速变小,加载的地图数据也少了,因此显示负荷小了很多,显示速度明显变快。如图10 左图和右图分别是16 级和18 级三维地图显示效果示例,明显可见16 级地图范围更广,显示的地物更多。
从显示效果和性能的定性、定量测试结果来看,地图渲染SDK显示效果良好,性能也基本能满足手机导航地图渲染的要求。
5 结论
本文根据Android 和iOS 平台上OpenGL ES 程序开发的特点,将三维渲染技术应用于地图显示表达,对三维地图引擎的客户端架构和服务端数据模型进行了设计,描述了三级缓存机制和线程模型等关键技术,详细介绍了客户端SDK 的三维地图可视化实现方法,并对客户端SDK 进行性能测试分析。经实验验证,整个三维地图渲染引擎显示效果和性能良好,完成了建设目标。本文设计的三维地图渲染引擎具有一定普适性和使用价值。