☰
Mapbox全球3D建筑加载:矢量瓦片、fill-extrusion与性能优化
2026/10/27 6:42:36 网站建设 项目流程

mapbox 加载全球 3D 建筑这件事,拆开看就三样东西:一份带高度字段的矢量瓦片、一个fill-extrusion图层、一次关于视觉和性能的长时间拉锯。它能让一张平面地图在几十毫秒内"长"出楼宇轮廓,让航线规划、城市分析、活动场地预览这类场景一下子有了空间感。很多人第一次搜这个标题,是想要一段能跑的代码;但真正写完上线之后才发现,难点从来不在那几行addLayer,而在全球尺度下怎么让瓦片按需加载、怎么让低楼层不糊成一片、怎么让手机不卡成幻灯片。这篇内容我会按一个做过若干地理可视化项目的从业者视角,把数据来源、图层参数、光照地形、性能降级、排查套路一层层摊开讲。零基础能照着抄,做过一两个项目的也能从里面挑到几条以前没注意的细节。

1. 先把"全球 3D 建筑"这件事拆开看

1.1 标题背后其实是三件独立的事

很多人一上手就去翻fill-extrusion的文档,翻完发现还是加载不出建筑,问题往往出在另外两层。我把这件事拆成数据层、渲染层、视角层三段来看,任何一段没打通,画面都不会对。

数据层负责回答"哪里有一栋楼、多高、底面在哪里"。全球范围的建筑轮廓不可能塞进一个 GeoJSON,它必须是切片后的矢量瓦片,浏览器按当前视口和缩放级别去要对应的 z/x/y 分片。瓦片里每个建筑是一个 Polygon,属性里带height、min_height这类字段,渲染层才有东西可拉伸。

渲染层负责把二维多边形变成三维棱柱。核心就是fill-extrusion这个图层类型,它以多边形为底面,向上拉伸到fill-extrusion-height指定的米数,底面从fill-extrusion-base开始。它不生成真正的网格模型,没有窗户、没有屋顶细节,本质上是一堆挤出的棱柱体,所以渲染成本比真实三维模型低一个数量级。

视角层决定"看起来像不像 3D"。默认俯视角度下,再高的楼也只是一个个色块。必须把pitch拉到 45 度以上,配合bearing旋转和光照方向,人眼才能从轮廓的侧面明暗里读出体积感。这一段最容易被忽略,也是新手做出来"感觉不对"的最常见原因。

把这三层分清楚,后面每一步该查什么文档、该调哪个参数就不会乱。

1.2 三条可行路线,先选对再动手

全球 3D 建筑不是一个方案,而是至少三条路线,成本、可控性、视觉上限完全不同。选错了路线,后面所有优化都是白费力气。

路线数据来源代码量可定制性适用场景
A. 官方底图图层内置矢量瓦片很少中快速出效果、通用底图
B. 新版本样式配置内置 3D 对象极少低只想要"有楼"的展示页
C. 自备数据GeoJSON / 自建瓦片多高特定园区、自建数据、精模

路线 A 是最常见的做法:在已有的底图样式之上,从composite源里再取一次building图层,用fill-extrusion重新渲染一遍。它的好处是零数据成本,全球覆盖,样式随便换;坏处是高度字段的完整度和准确度由数据方决定,你只能接受。

路线 B 依赖较新的地图样式规范,底图本身就带了三维对象,通过配置项开关和切换光照预设就能出效果。代码最少,但基本没法改颜色、没法只显示某一类建筑,做展示页够用,做业务系统就不够。

路线 C 适合"建筑是主角"的场景:某个园区、某片改造区、某个赛事场地。数据可能是测绘给的 Shapefile,也可能是建模方给的带高度字段的 GeoJSON。这时能不能跑顺,全看你有没有把数据切片,几万栋楼直接塞一个 GeoJSON 进浏览器,卡死是必然的。

我一般的判断标准是:如果建筑只是底图的装饰,选 A;如果建筑是页面的第一视觉主体,选 C;B 只在原型阶段用。后面主要围绕 A 和 C 展开,因为这两条覆盖了绝大多数真实需求。

2. 官方矢量瓦片路线的关键细节

2.1 building 图层里到底有哪些字段

用路线 A 时,最容易踩的坑是"以为字段名是固定的"。不同样式版本、不同数据版本,source-layer的名字和属性都可能变。稳妥的做法不是背字段,而是现场探查。

底图样式加载完成后,用querySourceFeatures把当前视口里的建筑要素捞出来,打印一下属性键,比翻半天文档快得多:

map.on('sourcedata', (e) => { if (e.sourceId === 'composite' && e.isSourceLoaded) { const feats = map.querySourceFeatures('composite', { sourceLayer: 'building' }); if (feats.length) { console.log('字段列表:', Object.keys(feats[0].properties)); console.log('样例:', feats[0].properties); } } });

探查结果通常会看到这么几个关键字段:height是建筑高度(米),min_height是底面离地高度(米),有些数据里还有type(住宅、商业、公共设施之类)和extrude(是否应该被拉伸)。名字看着简单,陷阱在于**height经常缺失**,或者是个字符串,或者是0。如果直接['get', 'height']拿去当高度表达式,缺字段的建筑轻则变成贴地的一张纸,重则整层渲染报错。

我的处理习惯是永远做兜底:用to-number带默认值,缺高度的建筑给一个 3 到 5 米的矮房子,视觉上不突兀,也不会在地图上留下空洞。

const heightExpr = ['to-number', ['get', 'height'], 3]; const baseExpr = ['to-number', ['get', 'min_height'], 0];

这里有个细节值得说:为什么底面要单独取min_height而不是固定 0?因为高架、天桥、悬挑结构、跨层连廊这类要素,底面本来就不在地面上。全部从 0 拉起,视觉上会和地面糊成一块,看起来像"整片地被填实了"。把min_height用上,悬空结构才会真正悬空。

2.2 fill-extrusion 的几个参数,决定成败

fill-extrusion的绘制属性不多,但每一个都直接影响观感,值得逐个说清楚为什么要这么设。

fill-extrusion-height是拉伸高度,单位米。它接受表达式,所以我更推荐写成随缩放变化的插值,而不是固定值。原因很实际:在 zoom 15 刚进入建筑可见级别时,如果高度瞬间满值,满屏高楼会"啪"地弹出来,观感非常差。用 15 到 15.05 这段极小跨度做一个从 0 到目标高度的过渡,视觉上就是楼宇"生长"出来,几乎感知不到延迟。

fill-extrusion-base是底面高度,同上用min_height兜底。注意一个硬性限制:base必须小于height,否则这个要素会被直接丢弃,什么都不画。数据里如果存在min_height大于height的脏数据,你会在排查时抓狂,因为它不报错,就是不显示。

fill-extrusion-opacity控制透明度。很多人喜欢调到 0.6 做半透明,觉得很"科技感"。但半透明会触发深度排序问题:同一个瓦片里前后的楼叠加时,颜色会出现奇怪的深浅断层,尤其在俯角很低的时候特别明显。我的建议是只要不是刻意要透视效果,就保持 1.0,让渲染管线走不透明路径,性能和观感都更稳。

fill-extrusion-color是颜色,可以接表达式按高度或类型渐变,下一节细说。

fill-extrusion-vertical-gradient默认是开着的,作用是在棱柱侧面做自上而下的轻微明暗渐变,模拟天光。关掉它,楼体会变成一块死板的纯色,看起来像贴纸。这个开关我一般不碰,保持默认。

把这些参数放在一起,一段可以基本照抄的图层配置大概长这样:

map.addLayer({ id: '3d-buildings', source: 'composite', 'source-layer': 'building', type: 'fill-extrusion', minzoom: 14, paint: { 'fill-extrusion-color': '#c8ccd1', 'fill-extrusion-height': [ 'interpolate', ['linear'], ['zoom'], 14, 0, 14.1, ['to-number', ['get', 'height'], 3] ], 'fill-extrusion-base': [ 'interpolate', ['linear'], ['zoom'], 14, 0, 14.1, ['to-number', ['get', 'min_height'], 0] ], 'fill-extrusion-opacity': 1.0 } });

2.3 光照与地形:3D 感从哪来

建筑有了体积,不代表有立体感。立体感的来源是明暗对比,而明暗由光照方向和视角共同决定。

地图的光照配置一般包含光源方位、极角、强度几个量。极角接近 0 意味着光从正上方打下来,楼顶亮、楼身一样亮,完全看不出高差;把极角压到 30 到 40 度附近,让光斜着来,楼的一侧亮一侧暗,体积感立刻就出来了。至于强度,我不建议拉满,全球尺度下大面积高对比会显得脏,0.3 到 0.5 之间比较耐看。

map.setLight({ anchor: 'viewport', color: '#ffffff', intensity: 0.4, position: [1.15, 210, 35] });

anchor: 'viewport'这一项值得单独提一句。它让光源跟随视角旋转,而不是固定在地理方位上。好处是你转到哪个方向看,建筑都有稳定的明暗关系;坏处是背离真实太阳方位,做日照分析时不能用这套。做通用展示我用 viewport,做有真实时间概念的场景就换成地理锚定,自己算太阳角度。

地形是另一个容易被高估的东西。给地图挂上地形数据源,山谷和丘陵确实会起伏,建筑也会跟着贴合地面,视觉上很高级。但代价是渲染开销明显上升,而且在山区,建筑底面和地形的贴合精度不够时,会出现楼被山体"啃掉一角"或者地基悬空的情况。我的做法是:平原城市不开地形,山区或海岸线场景才开,且地形夸张系数控制在 1.2 到 1.5,再高就失真了。

map.addSource('dem', { type: 'raster-dem', url: 'mapbox://mapbox.mapbox-terrain-dem-v1', tileSize: 512, maxzoom: 14 }); map.setTerrain({ source: 'dem', exaggeration: 1.3 });

3. 从零跑通的完整实操

3.1 初始化:别小看这几个参数

初始化阶段有几个参数,当时不改后面要返工,属于典型的"五分钟省事、五小时填坑"。

第一是pitch和maxPitch。初始俯角给到 45 到 55 度之间,用户打开就能看到立体效果,不用自己去拖。maxPitch我一般设到 70 到 85,太接近 90 度会出现地平线撕裂和天空缺失的观感问题。

第二是renderWorldCopies。默认地图在低缩放级别会横向复制多份世界,看起来更"连续",代价是渲染量翻倍。做全球视图时我会评估一下:如果主要业务在国内某个区域,直接关掉,省下的都是白赚的;如果真要做全球对比,就保留,但在低缩放级别把建筑层设成不可见。

第三是antialias。开抗锯齿边缘会漂亮一些,但 MSAA 在移动端很贵。我的默认选择是关,靠样式调色和适度模糊来掩盖锯齿,只在桌面端展示页才打开。

const map = new mapboxgl.Map({ container: 'map', style: 'mapbox://styles/mapbox/streets-v12', center: [116.4, 39.9], zoom: 15, pitch: 50, bearing: -20, maxPitch: 80, antialias: false, renderWorldCopies: false, attributionControl: true });

注意style这一项:不同版本的样式,building图层的字段和可见性策略可能不一样。选一个稳定的版本并在项目里锁死,别跟着官方样式自动升级,否则某天早上你会发现建筑全没了,然后花一上午找原因。

3.2 加层:一段可以直接抄的配置

图层的添加时机必须等样式加载完。map.on('load')里加,或者至少style.load之后。加晚了会报"source 不存在",加早了样式还没解析完,同样报错。

完整的添加流程我会写成三层判断:先确认源存在,再确认目标图层不存在(避免热更新时重复添加),最后才加。

map.on('load', () => { const style = map.getStyle(); if (!style.sources.composite) { console.warn('当前样式没有 composite 源,无法使用内置建筑数据'); return; } if (map.getLayer('3d-buildings')) { map.removeLayer('3d-buildings'); } map.addLayer({ id: '3d-buildings', source: 'composite', 'source-layer': 'building', type: 'fill-extrusion', minzoom: 13.5, paint: { 'fill-extrusion-color': '#c8ccd1', 'fill-extrusion-height': [ 'interpolate', ['linear'], ['zoom'], 13.5, 0, 14.2, ['to-number', ['get', 'height'], 3] ], 'fill-extrusion-base': [ 'interpolate', ['linear'], ['zoom'], 13.5, 0, 14.2, ['to-number', ['get', 'min_height'], 0] ], 'fill-extrusion-opacity': 1 } }); });

minzoom这个值我前面写 14,这里写 13.5,是想说明它不是一个固定答案。它的取值取决于两件事:你的底图在这个缩放级别有没有建筑数据,以及你希望用户在多大的视野下开始看到城市天际线。判断方法很土但有效:手动把地图缩放到某个级别,看建筑是不是"有内容但不拥挤",那个级别就是你的答案。

还有一个细节:如果底图样式本身已经渲染了建筑(很多样式在 zoom 15 后会画二维建筑色块),你再叠一层 3D 会出现两层打架、颜色不对的情况。稳妥做法是找到样式里的原有建筑图层,把它隐藏掉:

map.getStyle().layers.forEach((layer) => { if (layer.type === 'fill' && layer['source-layer'] === 'building') { if (map.getLayer(layer.id)) map.setLayoutProperty(layer.id, 'visibility', 'none'); } });

3.3 高度着色:让城市有层次

纯灰色的建筑群,远看是一块平整的水泥板。用高度做颜色渐变,城市的层次感立刻就出来了,这也是一眼能看出"做过"和"没做过"的差别。

表达式的写法要注意一点:插值的输入必须是数字,null会直接让整层报错。所以我一定会在外面套一层兜底:

'fill-extrusion-color': [ 'interpolate', ['linear'], ['to-number', ['get', 'height'], 3], 3, '#e8eaed', 15, '#d4d8dd', 40, '#b9c0c8', 80, '#9aa4b0', 150, '#7d8998' ];

这段配色是从浅到深,低矮建筑浅、高层建筑深,视觉上会形成一种"城市中心的塔楼更沉稳"的秩序感。反过来用浅到亮也可以,取决于你的底图是深色还是浅色。深色底图配高亮渐变更出效果,浅色底图配低饱和灰阶更耐看。

再进一步,可以按建筑类型分别配色。前提是数据里有可用的类型字段,探查的时候留意一下有没有类似type、class的属性:

'fill-extrusion-color': [ 'match', ['get', 'type'], 'residential', '#dfe6e9', 'commercial', '#a29bfe', 'public', '#74b9ff', '#c8ccd1' ];

用match而不是一堆case,写起来短,渲染效率也更好。但要注意:类型字段的取值是数据方定义的,可能长得完全不是上面这些词,别照抄,先探查再写。

3.4 自备数据:GeoJSON 与瓦片两条路

当建筑是业务主角时,就得接自己的数据。按数据量分两条路走。

几百到两三千栋,直接上 GeoJSON 完全够用。数据要求不复杂:每个要素是 Polygon 或 MultiPolygon,坐标是 WGS84 的经纬度顺序,属性里带高度字段。加进地图就是一个geojson源加一个fill-extrusion图层,高度直接从属性里取:

map.addSource('my-buildings', { type: 'geojson', data: './data/buildings.geojson' }); map.addLayer({ id: 'my-buildings-3d', type: 'fill-extrusion', source: 'my-buildings', paint: { 'fill-extrusion-color': '#8fa3bf', 'fill-extrusion-height': ['to-number', ['get', 'height'], 6], 'fill-extrusion-base': ['to-number', ['get', 'base_height'], 0], 'fill-extrusion-opacity': 1 } });

这条路的两个高频错误:坐标顺序写反(把纬度放前面),地图上会跑到南极附近;环的方向不统一,某些要素会渲染成空洞。前者用控制台打印第一个坐标就能确认,后者可以用现成的几何校验工具在入库前统一处理,比在浏览器里查快得多。

上万栋就不要再喂 GeoJSON 了。浏览器解析几万个多边形加上逐帧拉伸,内存和帧率都会崩。这时要么自己切成矢量瓦片,按缩放级别分层,要么直接上三维瓦片方案,把模型分块按需加载。切瓦片的工具有不少,核心思路一样:把数据按网格切开,低层级合并简化、高层级保留细节,最后让地图按需请求。这套流程第一次做会花掉半天到一天,但做完之后数据量再翻十倍也不用改架构。

4. 全球尺度下的性能取舍

4.1 用缩放分级控制渲染量

"全球"这两个字是性能的最大敌人。全球范围的建筑瓦片合起来是海量数据,但任何时刻屏幕上的像素是有限的,所以真正要做的不是"全部加载",而是"让不该出现的永远不出现"。

核心手段就是缩放分级。建筑层在低缩放级别不显示,用户缩放到街区级别才开始出现。这不只是视觉设计,更是性能设计:全球视图下地图只需要渲染几十上百个瓦片,一旦进入城市级别,视口里最多也就几十个瓦片,负载是恒定的。

我的分级习惯大致是这样:

  • zoom 0 到 10:建筑层完全不可见,只跑底图和矢量路网
  • zoom 10 到 13.5:可以显示一些低精度的体量块,或者干脆继续隐藏
  • zoom 13.5 到 15:建筑层渐入,高度做插值生长
  • zoom 15 以上:完整建筑,同时考虑加细节层(楼顶、地下结构)
map.setLayoutProperty('3d-buildings', 'visibility', 'none'); map.on('zoom', () => { const z = map.getZoom(); map.setLayoutProperty( '3d-buildings', 'visibility', z >= 13.5 ? 'visible' : 'none' ); });

用zoom事件逐帧判断会有一点开销,更省的做法是在zoomend里判断,或者在moveend里做一次性切换。频繁切换可见性也有成本,因为切回来要重新生成瓦片,所以在边界值附近留一点缓冲,别让用户在地图边缘反复横跳。

4.2 渲染开销的几个大头

知道钱花在哪,才谈得上省。全球 3D 建筑的开销主要集中在四个地方,按影响从大到小排。

一是瓦片请求与解析。每个新进入视口的瓦片都要请求、解码、解析成几何数据。低缩放级别横扫地图时,这个开销比渲染本身还大。能做的就是控制图层可见范围、控制最大瓦片缓存,别让无用的瓦片长期占内存。

二是几何数量。每栋楼的轮廓边数不同,一个复杂的老城区可能有几千个顶点。全球尺度下同时存在的顶面数量相当可观。可行的减负手段是在低缩放级别用简化数据,只在高层级请求完整几何。

三是光照与阴影计算。启用地形、开启高光强、开抗锯齿,都会让每个像素的计算量上升。移动端上这些叠加起来就是灾难。

四是透明度排序。前面提过,半透明棱柱需要排序,元素一多就变成 CPU 瓶颈。这一项最容易被忽略,也最容易通过"把透明度改回 1"解决。

我做过一个对比,同一台设备、同一片城区、同样的视角,只改三个开关,帧率能差出一倍多。所以优化的顺序应该是:先关透明度,再降光照强度,再考虑关地形,最后才去动数据。

4.3 移动端与低端设备的降级套路

做全球尺度的页面,用户里有相当比例是手机。手机端的 GPU 和内存都紧张,得有一套自动降级逻辑,而不是让用户自己关开关。

我的做法是分三级。第一级看设备像素比,超过 2 的一律按 2 渲染,没必要为 3 倍屏浪费四倍的填充率。第二级看运行时帧率,连续若干帧低于阈值就自动把建筑层隐藏或者降级成二维色块。第三级给用户一个手动开关,愿意看效果的自己开,不愿意的默认关。

const dpr = Math.min(window.devicePixelRatio || 1, 2); const map = new mapboxgl.Map({ container: 'map', style: 'mapbox://styles/mapbox/streets-v12', pixelRatio: dpr, pitch: 45, antialias: false }); let slowFrames = 0; map.on('render', () => { const t = performance.now(); if (map._lastFrameTime && t - map._lastFrameTime > 50) slowFrames++; else slowFrames = Math.max(0, slowFrames - 1); map._lastFrameTime = t; if (slowFrames > 30 && map.getLayer('3d-buildings')) { map.setLayoutProperty('3d-buildings', 'visibility', 'none'); } });

这段只是思路示意,真实项目里帧率采样要做得更稳,别被偶发的加载卡顿误判。但方向是对的:宁可少看一点效果,也不能让用户对着转不动的页面。

5. 常见问题排查实录

5.1 建筑是"纸片人":高度丢失

这是最高频的问题,症状是建筑层加上了,地图上也确实有东西,但所有楼都是平的,俯角一拉就什么都没了。

原因基本锁定在三处。第一,height字段根本不存在,你的表达式取到null,拉伸高度为 0。第二,字段存在但是字符串,比如"24.5",某些表达式能容错,某些不能。第三,你加了filter,把有高度的建筑全过滤掉了。

排查顺序我一般这样走:先把filter整个删掉,看是否恢复正常;再打印一个有高度建筑的属性,确认字段名和类型;最后把表达式改成固定值80试一下,如果能立起来,就说明是取值问题,不是渲染问题。

// 临时调试:把高度写死,验证渲染链路 map.setPaintProperty('3d-buildings', 'fill-extrusion-height', 60);

用这个土办法能在一分钟内区分"数据问题"和"渲染问题",比逐行读代码快得多。

5.2 建筑发黑、发灰、发脏

颜色不对通常不是颜色参数写错了,而是光照方向和视角的组合出了问题。

如果整片建筑都是黑的,先看光照极角是不是接近 0,或者光源强度是不是被设成了负数。如果只有在某些旋转角度下发黑,那是anchor设成了地理锚定,光从背面打过来,转个方向就好。如果建筑表面有明显的斜向条纹或者一半亮一半暗分界生硬,那是垂直渐变和光照方向打架,可以把强度调低一点。

还有一种"脏"是配色问题。低饱和的灰色在浅色底图上容易显得像蒙了层灰,尤其是大量矮楼挤在一起的时候。解决办法不是提亮,而是拉开层次:给不同高度段设置足够大的色差,别在 20 到 30 米这个区间里塞四五个相近的灰色。

5.3 掉帧与白屏

掉帧好理解,白屏才吓人。我遇到过两次白屏,原因完全不同。

第一次是图层加在了错误的时机。样式还在加载中就addLayer,整个渲染管线直接崩掉,控制台一堆"source not found",页面一片白。修法是把所有加层逻辑塞进map.on('load')或map.on('style.load'),并且在加之前确认源存在。

第二次是数据里有非法几何。某批自建数据里有自相交的多边形,浏览器在三角化时直接卡死,表现是页面加载到一半没了响应。这类问题在浏览器里很难查,我的做法是在数据入库阶段就跑一遍几何校验,把自相交、重复点、环方向错误的要素全部修掉再上传。前期多花二十分钟,后期省两天。

还有一种"假白屏"是网络问题。瓦片请求 401 或者超时,地图背景色透出来,看起来像白屏。打开网络面板看一眼请求状态就清楚了。

5.4 建筑和地形打架

开了地形之后,山区的建筑经常出问题:楼底悬空、楼被山体切掉一半、楼陷进地里。

根因是建筑数据的底面高度按海拔 0 计算的,而地形把地面抬高了。fill-extrusion-base设的是相对地面的偏移,不是绝对海拔,所以大部分情况下地图会自动处理。真正出问题的是那些数据本身带min_height的要素,两个高度叠加之后超过了山体高度,看起来就悬空了。

处理手段有限,我的经验是:山区场景要么不开地形,要么接受一定程度的视觉误差,别在这上面死磕。真要精确,就得走三维瓦片路线,让模型自带地形贴合信息,那是另一个工程量级的事。

5.5 问题速查表

把上面这些整理成一张表,出问题时按症状对号入座,比从头读代码快。

症状最可能原因快速验证处理方式
建筑全平高度字段缺失或为字符串高度改固定值试渲染表达式加to-number兜底
部分建筑不显示base大于height检查该要素两个字段数据侧修正或在表达式里取最小值
整层不可见图层加在样式加载前看控制台报错移到load回调内
颜色发黑光照极角过小或强度为负恢复默认光照极角调到 30 到 40 度
帧率骤降半透明导致排序开销透明度改 1 测试保持不透明
低缩放也加载建筑minzoom缺失看图层配置设minzoom并做显隐控制
移动端白屏几何数据非法换小数据集测试入库前做几何校验
山区建筑悬空地形与min_height叠加关地形对比山区不开地形或接受误差

6. 几个踩过才知道的细节

6.1 数据侧的隐性坑

做这条路线的项目,最后卡住你的往往不是代码,是数据。

第一个坑是高度字段的单位不统一。有的数据用米,有的用英尺,有的用楼层数乘以一个系数。全球数据混在一起时,会出现一片区域楼高得离谱、另一片区域楼矮得看不见。我的做法是抽样比对:挑十几个有代表性的位置,用卫星图或者街景目测一下,看渲染出来的高度是否合理,不合理就整批乘一个换算系数。

第二个坑是建筑轮廓的精度差异极大。核心城区的轮廓精细到能看出裙楼和塔楼,郊区就是几个粗多边形。混在一起渲染,视觉上会很不协调。这一点没什么好办法,只能接受,或者在低缩放级别用统一的简化色块覆盖掉差异。

第三个坑是覆盖密度不均。有些区域建筑数据很全,有些区域几乎是空白,缩放到那里会突然出现大片"平地",让人怀疑地图坏了。这个在页面上最好有心理准备,别把它当成 bug 去查。

6.2 上线前我会过一遍的清单

每次这类项目上线前,我都会按同一份清单走一遍,基本都是踩过的坑换来的。

  • 初始化时pitch给了非零值,用户打开就有立体感
  • 建筑层设了minzoom,低缩放级别不加载
  • 高度和底面表达式都有兜底值,缺字段不会导致整层消失
  • 样式原有的二维建筑层已经隐藏,不会和 3D 层打架
  • 透明度是 1,没有为了好看牺牲排序性能
  • 光照强度在 0.3 到 0.5 之间,没有过曝
  • 移动端有像素比限制和降级逻辑
  • 自建数据做过几何校验,没有自相交和重复点
  • 控制台没有任何 source 或 layer 相关的警告
  • 在 4G 网络下从打开到建筑出现,时间在可接受范围内

这份清单看起来琐碎,但每一条背后都是一次具体的翻车。真正把全球 3D 建筑跑顺,靠的不是某个神奇参数,而是把这些看似不起眼的细节一个个按死。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询