在这篇文章中我们聊一聊 mapbox-gl 中我们如何基于 mapbox 内置的渲染流程构建自定义渲染
一、写在前面
为什么要做这件事情?
我们知道 mapbox-gl 面向开发者事实上早已提供了「customlayer」 接口来实现自定义渲染,比较出名的例如「threejs」、「deckgl」都有相关的案例;在过去我同样也是基于这个接口去扩展一些自定义的功能,常见的套路要么是基于原生 webgl 要么是基于一些 webgl 的封装库(例如 「regl」),当然不可避免的会遇到以下几个问题:
- 需要引入一个外部库(当然这个不一定是个问题)
- 浮点数精度问题,会造成大层级下图形抖动(Precision issues)
- mapbox2.0后无法与地形做深度融合
- Export some tool classes for third-party developers
- Add terrain rendering support for customlayer
- WIP: Custom layer draping support (for globe & terrain)
- 无法与 mapbox 最重要的 style概念结合
- 重复世界渲染
基于以上问题,最终还是回到了基于 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:
3. render模块
此模块位于源码 src/render 下,主要需要修改两个位置和新增两个个文件:
在src/render下新增 draw_tiff.js文件(命名风格保持和 mapbox 一致),文件内容可参考 src/source/draw_raster.js。
修改 src/source/painter.js,新增渲染类型 draw:
在src/render/program下新增 tiff_program.js文件(命名风格保持和 mapbox 一致),文件内容可参考 src/source/raster_program.js。
修改 src/render/program/program_uniforms.js,新增渲染类型 programUniforms:
4. shaders模块
此模块是上步渲染模块的核心内容,位于源码 src/shaders 下,新增顶点着色器src/shaders/tiff.vertex.glsl和片段着色器src/shaders/tiff.fragment.glsl,具体内容可以参考 src/shaders/raster.vertex.glsl,src/shaders/raster.fragment.glsl
并且我们需要修改src/shaders/shaders.js 新增我们的着色器预编译内容:
5. style模块
样式体系是 mapbox 的一个核心组成部分,其每个图层都有预定义的一套样式定义,在这里我们需要针对tiff 图层定义其关联的 paint属性。
我们需要在src/style/style_layer新增两个文件,其内容主要参考raster_style_layer_properties.js和raster_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中新增我们的自定义图层:
到这里我们需要修改的内容看似好像都已完毕,从数据源-图层-样式都已经完成,但是请不要忘了最重要的一个模块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);
});
});