看了下最近的 Mapbox GL JS 的更新日志,发现新增了立交桥的3D显示效果。根据车道级的高精地图数据,能够实现立交,高架,隧道,导流区等形式的效果。还是挺美观的。下面将从样式,数据,渲染等几个角度研究一下 Mapbox 的实现思路。
效果预览



数据的预览链接。截至本篇文章发表时,这个链接是能够正常预览的。不排除哪天就看不了了。测试数据分布在德国慕尼黑,不知道高精地图的功能是不是 Mapbox 为宝马定制的。
数据构成
从预览链接里能够看到高精地图效果的数据构成。数据依旧是基于 mvt 规范来提供的,暂时不确定这个 hd-roads 是否遵守了当前的 mvt 协议,也就是没有写入额外的数据。数据主要由以下几部分组成:
hd_road_centerlines
每一根车道的中心线,在渲染过程中没用,不包括应急车道,更适合用于在导航过程中显示行车路线。

hd_road_elevation
这个数据集有点奇怪,肯定不是直接用于渲染的,它既有面数据也有点数据,根据名字看应该承载了道路高度信息,具体有啥用后面再说。


hd_road_line
每一条车道两侧的边界线,也就是车道线,相邻的两个车道共用一条数据。应急车道的边界线也在其中。在渲染时可以是实线或者虚线。

hd_road_point
这个数据集中包含了多种数据,道路两侧的植物,道路指示符号等。


hd_road_polygon
这个数据集是面状数据,覆盖了上述的各种车道,应急车道,导流区等,可以简单的理解为,面状数据包含了所有与道路直接有关的数据,除了那些树木。

地图样式
直接看 debug 目录下的 3d-intersections.html 可以看到上面的示例。不过现在是要分析它的实现方式,所以还是简单一点好。我们可以从 test 目录入手,其中各个功能拆分的更细,便于研究。test 目录中可以看到这样的一个测试用例,这是其中的样式文件渲染出的效果:

这是对应的 style 文件(下面的内容不完整,无关的部分我删除了):
{
"version": 8,
"sources": {
"hd-roads": {
"type": "vector",
"tileSize": 512,
"maxzoom": 18,
"tiles": ["local://tiles/3d-intersections/{z}-{x}-{y}.mvt"]
},
"geojson": {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"zLevel": 1
},
"geometry": {
"type": "MultiPoint",
"coordinates": [[11.5406, 48.1763]]
}
}
]
}
}
},
"layers": [
{
"id": "fake-road-shade",
"type": "fill",
"source": "hd-roads",
"source-layer": "hd_road_polygon",
"filter": [
"all",
["match", ["get", "class"], ["road", "bridge"], true, false]
],
"paint": {
"fill-color": "rgb(214, 221, 219)"
}
},
{
"id": "road-base",
"type": "fill",
"source": "hd-roads",
"source-layer": "hd_road_polygon",
"filter": ["all", ["match", ["get", "class"], ["road"], true, false]],
"layout": {
"fill-elevation-reference": "hd-road-base"
},
"paint": {
"fill-color": [
"interpolate",
["linear"],
["zoom"],
16,
"hsl(212, 25%, 80%)",
18,
"hsl(212, 25%, 71%)"
]
}
},
{
"id": "road-base-bridge",
"type": "fill",
"source": "hd-roads",
"source-layer": "hd_road_polygon",
"filter": ["all", ["match", ["get", "class"], ["bridge"], true, false]],
"layout": {
"fill-elevation-reference": "hd-road-base"
},
"paint": {
"fill-color": [
"interpolate",
["linear"],
["zoom"],
16,
"hsl(212, 25%, 80%)",
18,
"hsl(212, 25%, 71%)"
]
}
},
{
"id": "road-hatched-area",
"type": "fill",
"source": "hd-roads",
"source-layer": "hd_road_polygon",
"filter": [
"all",
["match", ["get", "class"], ["hatched_area"], true, false]
],
"layout": {
"fill-elevation-reference": "hd-road-markup"
},
"paint": {
"fill-opacity": ["interpolate", ["linear"], ["zoom"], 15, 0, 16, 1],
"fill-pattern": [
"match",
["get", "color"],
["yellow"],
"hatched-pattern-yellow",
"hatched-pattern"
]
}
},
{
"id": "solid-lines",
"type": "line",
"source": "hd-roads",
"source-layer": "hd_road_line",
"filter": [
"all",
["match", ["get", "class"], ["lanes"], true, false],
[
"match",
["get", "line_type"],
["solid", "solid_half_arrow", "half_arrow_solid", "arrow_solid"],
true,
false
]
],
"layout": {
"line-elevation-reference": "hd-road-markup"
},
"paint": {
"line-color": [
"match",
["get", "color"],
["yellow"],
"hsl(54, 100%, 65%)",
"hsl(0, 0%, 96%)"
],
"line-width": [
"interpolate",
["exponential", 1.5],
["zoom"],
15,
0,
18,
1.5,
19,
3,
22,
10
]
}
},
{
"id": "double-lines",
"type": "line",
"source": "hd-roads",
"source-layer": "hd_road_line",
"slot": "",
"filter": [
"all",
["match", ["get", "class"], ["lanes"], true, false],
["match", ["get", "line_type"], ["double"], true, false]
],
"layout": {
"line-elevation-reference": "hd-road-markup"
},
"paint": {
"line-color": [
"match",
["get", "color"],
["yellow"],
"hsl(54, 100%, 65%)",
"hsl(0, 0%, 96%)"
],
"line-width": [
"interpolate",
["exponential", 1.5],
["zoom"],
15,
0,
18,
1.5,
19,
3,
22,
10
],
"line-gap-width": 2
}
},
{
"id": "dashed-lines",
"type": "line",
"source": "hd-roads",
"source-layer": "hd_road_line",
"filter": [
"all",
["match", ["get", "class"], ["lanes"], true, false],
[
"match",
["get", "line_type"],
[
"dashed",
"arrow_dashed",
"long_dashed",
"short_dash",
"solid_dashed"
],
true,
false
]
],
"layout": {
"line-elevation-reference": "hd-road-markup"
},
"paint": {
"line-color": [
"match",
["get", "color"],
["yellow"],
"hsl(54, 100%, 65%)",
"hsl(0, 0%, 96%)"
],
"line-width": [
"interpolate",
["exponential", 1.5],
["zoom"],
15,
0,
18,
1,
19,
3,
22,
6
],
"line-dasharray": [
"step",
["zoom"],
["literal", [14, 14]],
20,
["literal", [18, 18]]
]
}
},
{
"id": "circle",
"type": "circle",
"source": "geojson",
"layout": {
"circle-elevation-reference": "hd-road-markup"
},
"paint": {
"circle-radius": 40,
"circle-color": "green",
"circle-pitch-alignment": "map"
}
}
]
}
综合这份样式以及其他几份样式,可以发现,3D立交桥主要由以下几个图层组成:
fake-road-shade
将表示路面的 polygon 简单的呈现出来,达到阴影的效果,挺巧妙的。

road-base,road-base-bridge
灰色区域的路面都是 road-base,road-base-bridge。两者使用的数据都是 source 中表示路面的polygon。
road-base 贴在地面上,就是普通的二维多边形。road-base-bridge 是三维的,并且在 layout 配置中,指定 fill-elevation-reference 属性为 hd-road-base,这应该是实现三维效果的关键。


road-hatched-area
导流区,通过 fill-pattern 实现的,所以需要准备好条纹状的纹理,这样实现无法控制纹理的走向。

solid-lines,double-lines,dashed-lines
车道分界线,车道边缘线等。

实际上还有车道方向,隧道等要素,这些要素的表达和上面的图层基本类似,就不细说了。
代码实现解析
看完样式文件中的 source 和 layer 配置,最奇怪的应该就是 hd_road_elevation 数据集和 xxx-elevation-reference 样式了。
在 \src\data\bucket\fill_bucket.ts 文件中,可以看到 Bucket 读取 fill-elevation-reference 这个属性值作为 elevationMode。

对于 elevationMode 不为 none 的 source,需要根据 elevationFeatures 来创建几何信息:

对于一条数据,并不是所有的 elevationFeatures 都需要参与计算,在304行,代码根据 3d_elevation_id 来获取到对应的 tiledElevation 数据。


这时回过头来看上面的数据构成,hd_road_line 通过 3d_elevation_id 与 hd_road_elevation 数据关联,并从 hd_road_elevation 中获取到高度信息。


看一下某个 elevationFeatures 的数据,顶点数组中包含了点位坐标和高度:

在 \3d-style\elevation\elevation_feature.ts 文件中,可以看到代码通过线性插值的方式来为特定的点位计算高度值。

总结
自 Mapbox v2 起,地形渲染能力被正式引入,为地图叠加提供了三维地形支持。自此,点、线、面要素具备了立体表现能力。基于相似的渲染机制,Mapbox 现已支持高精地图的可视化展示。在此过程中,hd_road_elevation 数据源的构建至关重要,是实现高精道路形态还原的核心。
期待 Mapbox 在高精地图可视化领域的更多作品。

太棒了,但是探索了半天还是不知道如何应用。全网没有使用案例
数据很难生产。高精地图数据也不如普通的地图数据应用广泛。