经纬志
首页 / MapLibre / 教程

3D 与地形

先看结论

MapLibre 不是三维引擎,它的三维能力集中在三件事:用高程数据抬起地表、 用晕渲表现坡向、用拉伸把面变成柱体。三者都建立在倾斜视角(pitch)之上。

最后验证 2026-11-03 · MapLibre GL JS 4.x · 更新 2026-10-29 · 引用本文

先有倾角,再谈三维

默认视角是正俯视,地形起伏看不出来。把 pitch 调到 45 到 60 度, 地形的立体感才出现;bearing 决定朝向,两者一起构成相机姿态。

var map = new maplibregl.Map({
  container: 'map',
  style: 'https://demotiles.maplibre.org/style.json',
  center: [103.0, 30.0],
  zoom: 10,
  pitch: 55,
  bearing: -20,
  maxPitch: 75,
  hash: true            // 把视角写进 URL,便于分享与复现
});

map.addControl(new maplibregl.NavigationControl({
  visualizePitch: true
}), 'top-right');

maxPitch 默认 60,最大可到 85。角度越大,远处地面被压缩得越厉害, 瓦片在屏幕上被拉长,纹理细节也会变糊。展示地形时 50 到 65 度是常见区间。

地形:raster-dem 加 terrain

setTerrain 只是声明用哪个源抬升地表。真正的解码工作由源的 encoding 完成,配错编码时地表会变成随机坑洼。

map.on('load', function () {
  map.addSource('dem', {
    type: 'raster-dem',
    tiles: [
      'https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png'
    ],
    encoding: 'terrarium',
    tileSize: 256,
    maxzoom: 14
  });

  map.setTerrain({
    source: 'dem',
    exaggeration: 1.5      // 垂直放大倍数
  });

  map.addLayer({
    id: 'hillshade',
    type: 'hillshade',
    source: 'dem',
    paint: {
      'hillshade-exaggeration': 0.4,
      'hillshade-shadow-color': '#2b2b2b',
      'hillshade-highlight-color': '#ffffff'
    }
  });
});

exaggeration 大于 1 会放大高差,让山更陡。真实地形用 1.0 到 1.2, 做视觉强调时 1.5 到 2 都合理,但超过 2 后道路与建筑会明显漂浮。

地形与图层顺序无关。setTerrain 会改写整个渲染流程, 矢量图层默认贴地渲染。需要悬浮在地面之上的要素(航线、空域边界), 用 fill-extrusion 的 base 抬升到目标高度, 普通线图层做不到离地绘制。

建筑体块:fill-extrusion

拉伸图层把面的高度解释为米。高度可以来自属性,也可以固定。 没有 height 字段时,整片建筑会变成同一高度的方块。

map.addLayer({
  id: 'buildings-3d',
  type: 'fill-extrusion',
  source: 'base',
  'source-layer': 'building',
  minzoom: 13,
  paint: {
    'fill-extrusion-height': ['coalesce', ['get', 'height'], 8],
    'fill-extrusion-base': ['coalesce', ['get', 'min_height'], 0],
    'fill-extrusion-color': ['interpolate', ['linear'], ['get', 'height'],
      0,   '#d8dee4',
      20,  '#aeb9c4',
      80,  '#7f8d9b'
    ],
    'fill-extrusion-opacity': 0.9,
    'fill-extrusion-vertical-gradient': true
  }
});

fill-extrusion-vertical-gradient 让侧面有上下明暗渐变, 体块边界更清楚。关掉后大片建筑会糊成一块纯色。

光与天空

三维图层的明暗由 light 决定。写在内联样式里最直接, 也可用 map.setLight() 运行时调整;天空与雾效则由 map.setSky() 单独设置。

map.setLight({
  anchor: 'viewport',          // 光源相对视口,转动地图时明暗不变
  position: [1.2, 210, 30],    // 球面坐标:径向、方位角、极角
  color: '#ffffff',
  intensity: 0.45
});

map.setSky({
  'sky-color': '#8fb8de',
  'horizon-color': '#e8eef4',
  'fog-color': '#dfe6ee',
  'sky-horizon-blend': 0.8
});

anchor: 'viewport' 是默认值,光固定来自屏幕方向; 改成 'map' 后光跟着地图坐标走,转动时建筑明暗会变化。

能力依赖关键属性
地表起伏raster-dem 源setTerrain 的 source 与 exaggeration
坡面晕渲同一个 raster-dem 源hillshade-exaggeration、阴影与高光色
建筑体块含高度字段的面数据fill-extrusion-height、fill-extrusion-base
明暗无light 的 anchor、position、intensity
天空与雾无sky 的 sky-color、horizon-color、fog-color

注意事项

倾斜后注记压在建筑里

拉伸图层与符号图层的绘制顺序决定遮挡关系。建筑放在注记之前, 体块会挡住文字;想要文字浮在上面,把 symbol 图层排在 buildings-3d 之后,并给文字加 text-allow-overlap 与描边,避免被地面颜色吃掉。

地形下的相机高度

开启地形后,map.getCenter() 仍返回平面坐标,海拔不参与计算。 需要贴合地面时读 map.queryTerrainElevation(lngLat), 它在地形未加载完成时返回 null。

移动端关闭地形

地形会额外请求一套高程瓦片,并让每帧的顶点计算量翻倍。 低端设备上按 map.getCanvas 所在窗口的宽度或 navigator.hardwareConcurrency 判断,直接跳过 setTerrain, 保留晕渲即可获得大部分视觉收益。

引用本文

经纬志(2026)。《3D 与地形》。https://www.jingweizhi.com/maplibre/tutorial/terrain-3d.html(访问日期 2026-11-03)。