第08章 - 样式解析与图层渲染
一份 style JSON 变成屏幕像素,要经过三段处理:解析(JSON → C++ 对象)、差异与分组(新旧对象对比,决定重建范围)、渲染编排(图层 → bucket → drawable → GPU pass),分别落在 src/mln/style/、src/mln/renderer/ 与 src/mln/shaders/ 下。命名空间为 mln(旧版本写作 mbgl)。
8.1 解析:Parser 要求 version 8
8.1.1 入口
样式解析入口在 src/mln/style/parser.hpp:
namespace style {
using StyleParseResult = std::exception_ptr;
class Parser {
public:
StyleParseResult parse(const std::string&);
std::vector<Sprite> sprites;
std::string glyphURL;
std::shared_ptr<FontFaces> fontFaces;
std::vector<std::unique_ptr<Source>> sources;
std::vector<std::unique_ptr<Layer>> layers;
TransitionOptions transition{ {util::DEFAULT_TRANSITION_DURATION} };
Light light;
GlobalStateMap globalStateDefaults;
std::string name;
LatLng latLng;
double zoom = 0;
double bearing = 0;
double pitch = 0;
double roll = 0;
};
}
注意返回类型是 std::exception_ptr 而不是抛异常——解析失败通过返回值报告,调用方决定是记日志还是整个样式加载失败。这是移动端崩溃率敏感度决定的:一个字段写错不应该杀进程。
8.1.2 版本校验只有一条规则
parser.cpp 里对 version 的处理非常直接:
if (document.HasMember("version")) {
const int version = versionValue.IsNumber() ? versionValue.GetInt() : 0;
if (version != 8) {
// log warning:
// "... version 8; using an outdated style will cause rendering errors"
}
}
只认 8。GL Style Spec 的 7 版与 8 版差异很大(表达式语法、sprite 结构),引擎不做兼容转换。所以”样式加载了但什么都没画出来”时,第一件事是确认 "version": 8 存在且是数字而不是字符串。
8.1.3 每个字段独立报错
Parser::parseLayers 对每个图层单独 conversion::convert<...>(value, error, id),失败时只 Log::Warning(Event::ParseStyle, error.message) 并跳过该层——所以”某个图层不显示”要看 ParseStyle 日志,而不是等整体报错。
8.1.4 图层类型的落点
每种图层在源码里对应三个文件,以 fill 为例:
src/mln/style/layers/fill_layer.cpp // JSON → Impl 的转换入口
src/mln/style/layers/fill_layer_impl.cpp/.hpp // 不可变数据 Impl
src/mln/style/layers/fill_layer_properties.cpp/.hpp // evaluated/paint/layout 属性
全部图层类型(src/mln/style/layers/ 下)与对应的渲染层(src/mln/renderer/layers/ 下)如下:
| style 图层 | style 目录的文件前缀 | renderer 目录的渲染类 |
|---|---|---|
| background | background_layer |
RenderBackgroundLayer |
| circle | circle_layer |
RenderCircleLayer |
| fill | fill_layer |
RenderFillLayer |
| fill-extrusion | fill_extrusion_layer |
RenderFillExtrusionLayer |
| heatmap | heatmap_layer |
RenderHeatmapLayer |
| hillshade | hillshade_layer |
RenderHillshadeLayer |
| line | line_layer |
RenderLineLayer |
| raster | raster_layer |
RenderRasterLayer |
| symbol | symbol_layer |
RenderSymbolLayer |
| color-relief | color_relief_layer |
RenderColorReliefLayer |
| location-indicator | location_indicator_layer |
RenderLocationIndicatorLayer |
| custom | custom_layer |
RenderCustomLayer |
| plugin | plugin_style_layer |
RenderPluginStyleLayer |
color-relief 与 location-indicator 是 MapLibre 自己的扩展(栅格着色、设备定位指示器),不在标准 spec 图层类型里。
8.2 差异计算:改一行样式触发什么
样式重载时不可能全量重建——那会导致整张地图闪烁。渲染编排持有上一帧的三份不可变列表:
Immutable<std::vector<Immutable<style::Image::Impl>>> imageImpls;
Immutable<std::vector<Immutable<style::Source::Impl>>> sourceImpls;
Immutable<std::vector<Immutable<style::Layer::Impl>>> layerImpls;
新样式到达后,src/mln/renderer/style_diff.hpp 提供三个纯函数做对比:
ImageDifference diffImages (const Immutable<std::vector<ImmutableImage>>&, ...);
SourceDifference diffSources(const Immutable<std::vector<ImmutableSource>>&, ...);
LayerDifference diffLayers (const Immutable<std::vector<ImmutableLayer>>&, ...);
每次 diff 返回 added / removed / changed 三个 map。渲染侧据此决定:
| 变化 | 处理 | 用户可见后果 |
|---|---|---|
| 图层 added | 创建 RenderLayer,为当前可见瓦片创建 bucket |
立即出现 |
| 图层 removed | 销毁 RenderLayer 与其 drawable |
立即消失 |
| paint 属性 changed | 只重算颜色/透明度,不重排 bucket | 渐变过渡,不闪烁 |
| layout 属性 changed | 标记 relayout,重建 bucket | 可能短暂空白后重排 |
区分 paint 与 layout 是性能关键。源码里有一个专门的判断:hasLayoutDifference(const LayerDifference&, const std::string& layerID)。改 fill-color 走快路径,改 fill-sort-key 走重排路径——业务上”按条件换颜色”很便宜,”按条件换排序”很贵。
8.2.1 图层分组:group_by_layout
同一 source、同一 source-layer、同一 zoom 范围、同一 filter 的多个图层,可以共享一次瓦片解析结果。src/mln/renderer/layer_group 之外的 group_by_layout.hpp 定义了归并键 LayoutGroupKey:typeInfo、source、sourceLayer、minZoom/maxZoom、visibility、filterExpression 与图层 Impl 指针。groupLayers() 返回 GroupMap,凡键相同的图层共享一次瓦片解析——这就是”十个 fill 图层指向同一 source-layer 只解析一次”的原因,也解释了”source 拆太碎会让首屏变慢”。
8.3 渲染编排:RenderOrchestrator
src/mln/renderer/render_orchestrator.hpp 的 RenderOrchestrator 是每帧的指挥者。它的职责从接口就能读出来:
std::unique_ptr<RenderTree> createRenderTree(const std::shared_ptr<UpdateParameters>&,
gfx::DynamicTextureAtlasPtr);
void update(gfx::ShaderRegistry&, gfx::Context&, const TransformState&,
const std::shared_ptr<UpdateParameters>&, const PaintParameters&, const RenderTree&);
void processChanges();
void updateLayers(gfx::ShaderRegistry&, gfx::Context&, const TransformState&,
const std::shared_ptr<UpdateParameters>&,
const PaintParameters& paintParameters, const RenderTree&);
一帧的典型顺序是:
1. processChanges() 把上一帧积累的 ChangeRequest 应用掉(图层增删改、tile 增减)
2. createRenderTree() 按当前图层顺序生成 RenderItem 列表
3. update() 对每个 RenderLayer 调用 update()
4. render(paintParams) 按 RenderPass 逐层执行 drawable 的 render()
RenderTree(render_tree.hpp)的元素是 RenderItem,实现类 LayerRenderItem 持有 RenderLayer&、RenderSource* 与图层索引,按索引排序——这就是”后定义的图层画在上面”的实现位置。
8.3.1 三个 render pass
src/mln/renderer/render_pass.hpp 定义了一个位掩码枚举:
enum class RenderPass : uint8_t {
None = 0,
Opaque = 1 << 0,
Translucent = 1 << 1,
Pass3D = 1 << 2,
};
绘制顺序固定为 Opaque → Translucent → Pass3D。这个顺序不是可配置的,它决定了:
- 不透明图层(fill、background、raster 的默认情况)先画,减少 overdraw;
- 半透明图层(带
fill-opacity < 1、带渐变的线、带 opacity 的 raster)后画,保证混色正确; - 3D(fill-extrusion)最后画,需要深度缓冲与模板缓冲。
一个 RenderItem 可能同时支持多个 pass(hasRenderPass(RenderPass)),编排器会分别在对应 pass 里渲染它。这也解释了半透明与不透明 fill 的”层级错乱”:它们在不同 pass 里,中间插入了其他图层的不透明绘制。
8.3.2 PaintParameters 里几个关键常量
src/mln/renderer/paint_parameters.hpp 是每帧每 pass 的绘制上下文,几个静态常量值得记住:
static constexpr int numSublayers = 3;
static constexpr float depthEpsilon = 1.0f / (1 << 16);
static constexpr int maxStencilValue = 255;
numSublayers = 3:同一图层内部可分三个子层;depthEpsilon:深度偏移量,用来避免同一平面上前后两个 drawable 的 z-fighting;maxStencilValue = 255:模板缓冲最多同时裁剪 255 个瓦片区域,超过会退化。
模板缓冲的使用点是 renderTileClippingMasks()。开了 clip 属性的图层需要”只在某个瓦片范围内绘制”,实现就是给每个瓦片分配一个 stencil id,绘制时限定 stencil 值。maxStencilValue 是硬上限,也是”为什么超多瓦片同时开裁剪会看到图形泄漏”的原因。
8.4 Bucket:瓦片解析的产物
每个瓦片被解析后产生一组 Bucket,每个 bucket 对应一个图层布局组。bucket 的位置:
src/mln/renderer/buckets/fill_bucket.cpp
src/mln/renderer/buckets/line_bucket.cpp
src/mln/renderer/buckets/symbol_bucket.cpp
src/mln/renderer/buckets/raster_bucket.cpp
...
bucket 里放的是已经预处理过的顶点与索引:线的斜接、填充的三角剖分、圆的分段,都在解析阶段算完。渲染阶段只做 uniform 更新与 draw call。
两个参数控制这个阶段的时机与质量(src/mln/renderer/bucket_parameters.hpp 与 src/mln/renderer/bucket.hpp):
| 参数 | 含义 | 影响 |
|---|---|---|
tileSize |
瓦片边长,默认 512(util::tileSize_D) |
影响几何精度与 overzoom 层级 |
pixelRatio |
设备像素比 | 影响 line-width、图标尺寸的换算 |
overscaling |
当前瓦片的放大倍数 | 用于修正线宽与虚线间隔,避免 overzoom 后线变粗 |
overscaling 最容易忽略:z14 数据在 z16 放大显示时,线的地理宽度不变,但屏幕像素宽度翻四倍。bucket 生成时不除以 overscaling,overzoom 区域的线会粗一圈。
8.5 LayerTweaker:属性如何进 shader
属性求值完成后,需要写进 shader 的 uniform。这一层在 src/mln/renderer/layer_tweaker.hpp 与各图层的 *_layer_tweaker.cpp:
src/mln/renderer/layers/background_layer_tweaker.cpp
src/mln/renderer/layers/circle_layer_tweaker.cpp
src/mln/renderer/layers/fill_layer_tweaker.cpp
src/mln/renderer/layers/line_layer_tweaker.cpp
src/mln/renderer/layers/raster_layer_tweaker.cpp
src/mln/renderer/layers/symbol_layer_tweaker.cpp
src/mln/renderer/layers/heatmap_layer_tweaker.cpp
...
每个 tweaker 在 drawable 的 uniform 缓冲里写值。以 fill 为例,典型 uniform 是 u_color、u_opacity、u_pattern_to_tile;以 line 为例是 u_color、u_width、u_dash_array、u_gap_width。
属性在进入 tweaker 前还要过一次过渡(transition):src/mln/renderer/transition_parameters.hpp 与 src/mln/style/transition_options.hpp 一起实现”样式变化时颜色/透明度在 300 ms 内插值”。默认时长来自 include/mln/util/constants.hpp:
constexpr Duration DEFAULT_TRANSITION_DURATION = Milliseconds(300);
constexpr UnitBezier DEFAULT_TRANSITION_EASE = {0, 0, 0.25, 1};
即”300 ms、ease-out”。把 transition 设为 false 或 duration: 0 就从渐变变瞬变。
8.6 Shader:从 GLSL 到可执行程序
仓库根目录的 shaders/ 放的是唯一的 shader 源:
shaders/background.vertex.glsl shaders/background.fragment.glsl
shaders/fill.vertex.glsl shaders/fill.fragment.glsl
shaders/line.vertex.glsl shaders/line.fragment.glsl
shaders/symbol_icon.vertex.glsl shaders/symbol_icon.fragment.glsl
shaders/symbol_sdf.vertex.glsl shaders/symbol_sdf.fragment.glsl
shaders/_prelude.vertex.glsl shaders/_prelude.fragment.glsl
shaders/manifest.json
shaders/generate_shader_code.mjs
构建时 generate_shader_code.mjs 读 manifest.json,把成对的 vertex/fragment 拼成 C++ 头文件,输出到两个目录:
include/mln/shaders/gl/<name>.hpp # shader 源码字符串
src/mln/shaders/gl/*.cpp # program 类
生成的文件带 // Generated code, do not modify this file! 头注释,改了会在下次构建被覆盖;要改 shader 就改 shaders/*.glsl。
几类 shader 与图层 API 的对应关系:
| shader | 使用者 | 特点 |
|---|---|---|
background |
RenderBackgroundLayer |
最简单,一个全屏 quad + 颜色或 pattern |
fill / fill_pattern / fill_outline |
RenderFillLayer |
三角剖分后的多边形 |
line / line_sdf / line_pattern / line_gradient |
RenderLineLayer |
线宽在 shader 里扩展 |
circle |
RenderCircleLayer |
点精灵距离场画圆 |
symbol_icon / symbol_sdf / symbol_text_and_icon |
RenderSymbolLayer |
图标与文字共用一套 quad |
collision_box / collision_circle |
碰撞调试 | 打开 MapDebugOptions 后可见 |
raster / heatmap / hillshade |
对应渲染层 | 纹理采样 + 混合 |
shader 的注册与查找走 ShaderRegistry(include/mln/gfx/shader_registry.hpp):
class ShaderRegistry {
public:
[[nodiscard]] ShaderGroup& getLegacyGroup() noexcept;
[[nodiscard]] virtual bool isShaderGroup(const std::string&) const noexcept;
[[nodiscard]] virtual const ShaderGroupPtr getShaderGroup(const std::string&) const noexcept;
[[nodiscard]] virtual bool replaceShader(ShaderGroupPtr&&, const std::string&) noexcept;
[[nodiscard]] virtual bool registerShaderGroup(ShaderGroupPtr&&, const std::string&) noexcept;
};
“legacy group” 是给插件与旧着色器留的兜底组;新代码应该用命名 group。运行时可以通过 registerShaderGroup 注入自定义 shader,这是实现自定义图层渲染的正规入口。
8.7 四个后端
src/mln/gfx/ 是后端抽象的公共层;具体后端在 src/mln/{gl,vulkan,mtl,webgpu}/(命名来自目录):
| 后端 | 目录 | 宏 |
|---|---|---|
| OpenGL / GLES | src/mln/gl/ |
MLN_RENDER_BACKEND_OPENGL |
| Metal | src/mln/mtl/ |
MLN_RENDER_BACKEND_METAL |
| Vulkan | src/mln/vulkan/ |
MLN_RENDER_BACKEND_VULKAN |
| WebGPU | src/mln/webgpu/ |
MLN_RENDER_BACKEND_WEBGPU |
条件编译宏在 paint_parameters.hpp 里就能看到用法,例如 #if MLN_RENDER_BACKEND_OPENGL 包围着模板缓冲可用性的检测代码。
跨后端接口都在 include/mln/gfx/(drawable.hpp、shader_group.hpp、renderer_backend.hpp 等),写自定义图层只依赖这层抽象。
8.8 排错清单
| 现象 | 根因 | 定位方法 |
|---|---|---|
整张地图空白,日志有 ParseStyle 警告 |
style 不是 version 8,或根对象不是 JSON object | 检查 "version": 8 与最外层结构 |
| 单个图层不显示,其他正常 | 该图层某个 paint/layout 字段类型错误,被单独丢弃 | 在日志里找 Event::ParseStyle 的 message |
| 改样式后整图闪一下 | 改动被判定为 layout change,触发了 bucket 重建 | 只改 paint 属性;layout 属性拆成独立图层 |
| 颜色变化没有过渡 | transition 被设为 0 或 false |
移除该层的 transition 覆盖,用默认 300 ms |
| overzoom 区域线变粗 | bucket 生成时未按 overscaling 修正 |
检查自定义 bucket 里 line-width 的计算 |
| 自定义 shader 不生效 | 写错 group 名,或改的是生成文件 | 改 shaders/*.glsl 并重新跑生成脚本;用 registerShaderGroup 注册 |
| 半透明图层遮挡关系异常 | 跨 render pass,中间插入了其他图层 | 接受 pass 顺序,或把半透明拆成独立样式层 |
| 图标不显示 | sprite 未加载完,areSpritesLoaded() 为 false |
等样式加载完成后再操作 |
8.9 生产建议
样式里别写无效版本。 解析器对未知字段宽容(warning 继续),对 version 不宽容;CI 加一步 style 校验能省掉大量线上排查。
paint 与 layout 分开管理。运行时会频繁改的(高亮色、透明度)只放在 paint 属性里;会改图层顺序或 filter 的操作都是 layout 级,代价高一个量级。
图层数量影响的是每帧遍历成本,不是解析成本。createRenderTree 每帧都会遍历全部图层,几百个图层在低端机上会有可测量的开销。合并同 source-layer 的图层是主要优化手段(见 8.2.1 的 groupLayers)。
自定义渲染优先用 plugin 图层。 RenderPluginStyleLayer 与 ShaderRegistry::registerShaderGroup 是官方扩展点;直接改生成文件会在升级时丢失。
8.10 本章小结
| 要点 | 事实(main 分支) |
|---|---|
| 解析入口 | style::Parser::parse(const std::string&),返回 StyleParseResult(exception_ptr) |
| 版本 | 只接受 version: 8,不等时发 warning 继续解析 |
| 图层类型 | 13 种标准类型 + color-relief / location-indicator 扩展 |
| 差异计算 | src/mln/renderer/style_diff.hpp 的 diffLayers/diffSources/diffImages |
| 分组 | group_by_layout.hpp 的 groupLayers(),键为 LayoutGroupKey |
| 每帧编排 | RenderOrchestrator::processChanges/createRenderTree/updateLayers |
| 绘制顺序 | RenderPass:Opaque → Translucent → Pass3D |
| 过渡默认 | 300 ms,DEFAULT_TRANSITION_EASE = {0, 0, 0.25, 1} |
| Shader 源 | shaders/*.glsl + manifest.json,由 generate_shader_code.mjs 生成到 include/mln/shaders 与 src/mln/shaders |
| 后端接口 | include/mln/gfx/ 下的抽象,四种后端实现 |