Posted in

Mapbox 3D 立交实现方式解析

看了下最近的 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 在高精地图可视化领域的更多作品。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注