第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/ 下的抽象,四种后端实现