如何按照 mapbox 渲染流程构建你的自定义渲染

1,711 阅读6分钟

在这篇文章中我们聊一聊 mapbox-gl 中我们如何基于 mapbox 内置的渲染流程构建自定义渲染

一、写在前面

为什么要做这件事情?

我们知道 mapbox-gl 面向开发者事实上早已提供了「customlayer」 接口来实现自定义渲染,比较出名的例如「threejs」、「deckgl」都有相关的案例;在过去我同样也是基于这个接口去扩展一些自定义的功能,常见的套路要么是基于原生 webgl 要么是基于一些 webgl 的封装库(例如 「regl」),当然不可避免的会遇到以下几个问题:

  1. 需要引入一个外部库(当然这个不一定是个问题)
  2. 浮点数精度问题,会造成大层级下图形抖动(Precision issues
  3. mapbox2.0后无法与地形做深度融合
  1. 无法与 mapbox 最重要的 style概念结合
  2. 重复世界渲染

基于以上问题,最终还是回到了基于 mapbox-gl 源码做功能新增。当然这种方式有利有弊,好处是如果你完全是 mapbox 生态的用户那么对你的影响不会很大,不利的地方是这种方式会将你们技术(可视化)和 mapbox 强绑定,对未来变更框架或者迁移 webgl2 和 webgpu 暂时不是一个很好的选择。

二、技术细节

在这一章节我们会直接看代码,原理性的东西可能不会细说。

1. 调用形式

我们一般在使用 mapbox-gl 添加图层时一般主要涉及两个 API:

map.addSource(...args)
map.addLayer(...args)

同样的我们在构建自定义渲染图层时我们期望的调用方式如下(source 和 layer 的 type 为你想要定义的数据源类型和渲染类型):

map.addSource('tiff', {
  type: 'tiff',
  url: './temp.tif',
  projection: 'EPSG:3857',
});

map.addLayer({
  id: 'tiff',
  type: 'tiff',
  source: 'tiff',
  paint: {
    'raster-opacity': 0.5,
    'raster-fade-duration': 0,
  }
});

2. Source模块

此模块位于源码 src/source 下,主要需要修改一个位置和新增一个文件:

src/source下新增 tiff_source.js文件(命名风格保持和 mapbox 一致),文件内容可参考 src/source/image_source.js

修改 src/source/source.js,新增 sourceTypes:

image-20220912003959531

3. render模块

此模块位于源码 src/render 下,主要需要修改两个位置和新增两个个文件:

src/render下新增 draw_tiff.js文件(命名风格保持和 mapbox 一致),文件内容可参考 src/source/draw_raster.js

修改 src/source/painter.js,新增渲染类型 draw:

image-20220912004611175

src/render/program下新增 tiff_program.js文件(命名风格保持和 mapbox 一致),文件内容可参考 src/source/raster_program.js

修改 src/render/program/program_uniforms.js,新增渲染类型 programUniforms:

image-20220912004927698

4. shaders模块

此模块是上步渲染模块的核心内容,位于源码 src/shaders 下,新增顶点着色器src/shaders/tiff.vertex.glsl和片段着色器src/shaders/tiff.fragment.glsl,具体内容可以参考 src/shaders/raster.vertex.glslsrc/shaders/raster.fragment.glsl

并且我们需要修改src/shaders/shaders.js 新增我们的着色器预编译内容:

image-20220912005631847

5. style模块

样式体系是 mapbox 的一个核心组成部分,其每个图层都有预定义的一套样式定义,在这里我们需要针对tiff 图层定义其关联的 paint属性。

我们需要在src/style/style_layer新增两个文件,其内容主要参考raster_style_layer_properties.jsraster_style_layer.js

tiff_style_layer_properties.js:

import styleSpec from '../../style-spec/reference/latest';

import {
    Properties,
    DataConstantProperty,
    // ColorRampProperty
} from '../properties';

export type PaintProps = {|
    "raster-opacity": DataConstantProperty<number>,
    "raster-hue-rotate": DataConstantProperty<number>,
    "raster-brightness-min": DataConstantProperty<number>,
    "raster-brightness-max": DataConstantProperty<number>,
    "raster-saturation": DataConstantProperty<number>,
    "raster-contrast": DataConstantProperty<number>,
    "raster-resampling": DataConstantProperty<"linear" | "nearest">,
    "raster-fade-duration": DataConstantProperty<number>,
    "raster-display-range": DataConstantProperty<number>,
    // "raster-color": ColorRampProperty,
|};

const paint: Properties<PaintProps> = new Properties({
    "raster-opacity": new DataConstantProperty(styleSpec["paint_tiff"]["raster-opacity"]),
    "raster-hue-rotate": new DataConstantProperty(styleSpec["paint_tiff"]["raster-hue-rotate"]),
    "raster-brightness-min": new DataConstantProperty(styleSpec["paint_tiff"]["raster-brightness-min"]),
    "raster-brightness-max": new DataConstantProperty(styleSpec["paint_tiff"]["raster-brightness-max"]),
    "raster-saturation": new DataConstantProperty(styleSpec["paint_tiff"]["raster-saturation"]),
    "raster-contrast": new DataConstantProperty(styleSpec["paint_tiff"]["raster-contrast"]),
    "raster-resampling": new DataConstantProperty(styleSpec["paint_tiff"]["raster-resampling"]),
    "raster-fade-duration": new DataConstantProperty(styleSpec["paint_tiff"]["raster-fade-duration"]),
    "raster-display-range": new DataConstantProperty(styleSpec["paint_tiff"]["raster-display-range"]),
    // "raster-color": new ColorRampProperty(styleSpec["paint_tiff"]["raster-color"]),
});

// Note: without adding the explicit type annotation, Flow infers weaker types
// for these objects from their use in the constructor to StyleLayer, as
// {layout?: Properties<...>, paint: Properties<...>}
export default ({ paint }: $Exact<{
  paint: Properties<PaintProps>
}>);

tiff_style_layer.js:

import StyleLayer from '../style_layer';

import properties from './tiff_style_layer_properties';
import {Transitionable, Transitioning, PossiblyEvaluated} from '../properties';

import type {PaintProps} from './tiff_style_layer_properties';
import type {LayerSpecification} from '../../style-spec/types';

class TiffStyleLayer extends StyleLayer {
    _transitionablePaint: Transitionable<PaintProps>;
    _transitioningPaint: Transitioning<PaintProps>;
    paint: PossiblyEvaluated<PaintProps>;

    constructor(layer: LayerSpecification) {
        super(layer, properties);
    }
}

export default TiffStyleLayer;

然后我们需要在 src/style/create_style_layer.js中新增我们的自定义图层:

image-20220912010733437

到这里我们需要修改的内容看似好像都已完毕,从数据源-图层-样式都已经完成,但是请不要忘了最重要的一个模块src/style-spec

6. style-spec

首先我们可以先在src/style-spec/types.js新增 Tiff Source 的数据类型定义TiffSourceSpecification:

export type TiffSourceSpecification = {|
    "type": "tiff",
    "url": string | string[],
     ...
|}

然后我们需要在src/style-spec/reference/v8.json中的layer.type.values中的 raster下新增一个tiff配置:

"tiff": {
  "doc": "render tiff.",
  "sdk-support": {
    "basic functionality": {
      "js": "1.14.0"
    }
  }
},

新增 layout_tiff配置,内容和layout_raster保持一致。

新增paint_tiff配置:

{
    "raster-opacity": {
      "type": "number",
      "doc": "The opacity at which the image will be drawn.",
      "default": 1,
      "minimum": 0,
      "maximum": 1,
      "transition": true,
      "sdk-support": {
        "basic functionality": {
          "js": "1.14.0"
        }
      },
      "expression": {
        "interpolated": true,
        "parameters": [
          "zoom"
        ]
      },
      "property-type": "data-constant"
    },
    "raster-hue-rotate": {
      "type": "number",
      "default": 0,
      "period": 360,
      "transition": true,
      "units": "degrees",
      "doc": "Rotates hues around the color wheel.",
      "sdk-support": {
        "basic functionality": {
          "js": "1.14.0"
        }
      },
      "expression": {
        "interpolated": true,
        "parameters": [
          "zoom"
        ]
      },
      "property-type": "data-constant"
    },
    "raster-brightness-min": {
      "type": "number",
      "doc": "Increase or reduce the brightness of the image. The value is the minimum brightness.",
      "default": 0,
      "minimum": 0,
      "maximum": 1,
      "transition": true,
      "sdk-support": {
        "basic functionality": {
          "js": "1.14.0"
        }
      },
      "expression": {
        "interpolated": true,
        "parameters": [
          "zoom"
        ]
      },
      "property-type": "data-constant"
    },
    "raster-brightness-max": {
      "type": "number",
      "doc": "Increase or reduce the brightness of the image. The value is the maximum brightness.",
      "default": 1,
      "minimum": 0,
      "maximum": 1,
      "transition": true,
      "sdk-support": {
        "basic functionality": {
          "js": "1.14.0"
        }
      },
      "expression": {
        "interpolated": true,
        "parameters": [
          "zoom"
        ]
      },
      "property-type": "data-constant"
    },
    "raster-saturation": {
      "type": "number",
      "doc": "Increase or reduce the saturation of the image.",
      "default": 0,
      "minimum": -1,
      "maximum": 1,
      "transition": true,
      "sdk-support": {
        "basic functionality": {
          "js": "1.14.0"
        }
      },
      "expression": {
        "interpolated": true,
        "parameters": [
          "zoom"
        ]
      },
      "property-type": "data-constant"
    },
    "raster-contrast": {
      "type": "number",
      "doc": "Increase or reduce the contrast of the image.",
      "default": 0,
      "minimum": -1,
      "maximum": 1,
      "transition": true,
      "sdk-support": {
        "basic functionality": {
          "js": "1.14.0"
        }
      },
      "expression": {
        "interpolated": true,
        "parameters": [
          "zoom"
        ]
      },
      "property-type": "data-constant"
    },
    "raster-resampling": {
      "type": "enum",
      "doc": "The resampling/interpolation method to use for overscaling, also known as texture magnification filter",
      "values": {
        "linear": {
          "doc": "(Bi)linear filtering interpolates pixel values using the weighted average of the four closest original source pixels creating a smooth but blurry look when overscaled"
        },
        "nearest": {
          "doc": "Nearest neighbor filtering interpolates pixel values using the nearest original source pixel creating a sharp but pixelated look when overscaled"
        }
      },
      "default": "linear",
      "sdk-support": {
        "basic functionality": {
          "js": "1.14.0"
        }
      },
      "expression": {
        "interpolated": false,
        "parameters": [
          "zoom"
        ]
      },
      "property-type": "data-constant"
    },
    "raster-fade-duration": {
      "type": "number",
      "default": 300,
      "minimum": 0,
      "transition": false,
      "units": "milliseconds",
      "doc": "Fade duration when a new tile is added.",
      "sdk-support": {
        "basic functionality": {
          "js": "1.14.0"
        }
      },
      "expression": {
        "interpolated": true,
        "parameters": [
          "zoom"
        ]
      },
      "property-type": "data-constant"
    },
    "raster-display-range": {
      "type": "array",
      "value": "number",
      "length": 2,
      "default": [
        null,
        null
      ],
      "transition": false,
      "doc": "Fade duration when a new tile is added.",
      "sdk-support": {
        "basic functionality": {
          "js": "1.14.0"
        }
      },
      "expression": {
        "interpolated": true,
        "parameters": [
          "zoom"
        ]
      },
      "property-type": "data-constant"
    }
  }

以上就是需要修改的核心内容,当然中间还会涉及src/util/ajax.js中 tiff 数据的读取和重投影相关的内容,但是由于每个人实现方式可能不太一样,此处不做特殊说明。

三、可以实现什么

通过以上核心内容的修改我们就实现另一个简单的 Tiff 数据渲染图层,常规方式我们会通过以下方式调用:

map.on('load', function () {
  map.addSource('tiff', {
    type: 'tiff',
    url: './temp.tif',
    projection: 'EPSG:3857'
  });
  
  const colorArray = [[203, [115, 70, 105, 1]],
      [218, [202, 172, 195, 1]],
      [233, [162, 70, 145, 1]],
      [248, [143, 89, 169, 1]],
      [258, [157, 219, 217, 1]],
      [265, [106, 191, 181, 1]],
      [269, [100, 166, 189, 1]],
      [273.15, [93, 133, 198, 1]],
      [274, [68, 125, 99, 1]],
      [283, [128, 147, 24, 1]],
      [294, [243, 183, 4, 1]],
      [303, [232, 83, 25, 1]],
      [320, [71, 14, 0, 1]]];

  const colors = [];
  colorArray.map(item => colors.push(item[0] - 273.15, 'rgba(' + item[1].join(',') + ')'));

  map.addLayer({
    id: 'tiff',
    type: 'tiff',
    source: 'tiff',
    paint: {
      'raster-opacity': 0.5,
      'raster-fade-duration': 0,
      'tiff-color': [
          "interpolate",
          ["linear"],
          ["value"],
          ...colors,
      ]
    }
  });

  const source = map.getSource('tiff');

  map.on('click', (e) => {
    console.log(e.lngLat);
    const v = source.queryValue(e.lngLat.toArray());
    console.log(v);
  });
});

image-20220912013923849

四、其他