mapbox中的 Raster 图层渲染分析

近日mapbox-gl 在发布的 v3.0.0 中新增了新的 raster 特性,仅从日志似乎感觉并没有什么特别,但是实际上非常有趣。

下面就让我们仔细探索一下究竟有什么有趣的地方。

简介

此特性新增了 4 个 paint 属性:

  • raster-color:栅格着色,支持插值表达式。
  • raster-value:引入了栅格表达式,支持从栅格中提取像素值。
  • raster-color-mix: 当配置 raster-color 时,指定用于计算 raster-value 的 RGB 通道组合。 计算公式为 mix.r * src.r + mix.g * src.g + mix.b * src.b + mix.a 前三个分量分别指定图片的红、绿、蓝通道的混合。第四个分量是一个常量偏移,不会与栅格 alpha 值相乘。 数据源中的透明度将会应用于着色结果。默认值对应于 RGB 亮度。
  • raster-color-range:当配置 raster-color 时,指定着色的的色值范围。单位与通过raster-color-mix计算的值相对应。

下面我们看一下相关的应用。

示例

大致示例如下,核心代码来源于raster-color

vue 复制代码
<template>
  <div class="playground-content" ref="mapRef">
    <div class="gui-panel" ref="guiPanel"></div>
  </div>
</template>

<script setup lang="ts">
  import 'mapbox-gl/dist/mapbox-gl.css';
  import { onMounted, ref } from 'vue';
  import mapboxgl from 'https://esm.sh/mapbox-gl@3.0.1';
  import * as d3 from 'd3';
  import { Pane } from 'tweakpane';
  import { usePlayground } from '../../../.vitepress/hooks/usePlayground';
  import { config } from '../../../config';

  defineOptions({
    name: 'RasterValue',
  });

  const mapRef = ref<HTMLDivElement>();
  const guiPanel = ref<HTMLDivElement>();
  const emits = defineEmits(['mount']);

  let map;

  const { pause, resume } = usePlayground(
    () => {},
    () => {},
  );

  const mixes = {
    luminosity: [0.2126, 0.7152, 0.0722, 0],
    average: [1 / 3, 1 / 3, 1 / 3, 0],
    red: [1, 0, 0, 0],
    green: [0, 1, 0, 0],
    blue: [0, 0, 1, 0],
    'decode terrain-rgb': [256 * 256 * 256 * 0.1, 256 * 256 * 0.1, 256 * 0.1, -10000],
    'decode population log-density': [
      256 * 256 * 256 * 8.348993603394451e-7,
      256 * 256 * 8.348993603394451e-7,
      256 * 8.348993603394451e-7,
      -9,
    ],
  };

  const ranges = {
    '[0, 1]': [0, 1],
    '[0, 8848]': [0, 8848],
    '[-3, 4]': [-3, 4],
  };

  const quantize = (interpolator, opac = (x) => 1, n = 100) =>
    d3.quantize(interpolator, n).map((c: any, i) => {
      const col = d3.rgb(c);
      const t = i / (n - 1);
      return [t, `rgba(${col.r},${col.g},${col.b},${col.opacity * opac(t)})`];
    });

  const colorScales = {
    Viridis: quantize(d3.interpolateViridis),
    Greys: quantize((x) => d3.interpolateGreys(1 - x)),
    Turbo: quantize(d3.interpolateTurbo),
    Inferno: quantize(d3.interpolateInferno),
    Magma: quantize(d3.interpolateMagma),
    Plasma: quantize(d3.interpolatePlasma),
    Cividis: quantize(d3.interpolateCividis),
    Cool: quantize(d3.interpolateCool),
    Warm: quantize(d3.interpolateWarm),
    Cubehelix: quantize(d3.interpolateCubehelixDefault),
    RdPu: quantize((x) => d3.interpolateRdPu(1 - x)),
    YlGnBu: quantize((x) => d3.interpolateYlGnBu(1 - x)),
    Rainbow: quantize(d3.interpolateRainbow),
    Sinebow: quantize(d3.interpolateSinebow),
    RdBu: quantize(d3.interpolateRdBu),
    PopDensity: quantize(d3.interpolateMagma, (t) => 3 * t * t - 2 * t * t * t),
    Hypsometric: [
      [0.004, 'rgb(48, 167, 228)'],
      [0.005, 'rgb(57, 143, 83)'],
      [0.031, 'rgb(116, 166, 129)'],
      [0.055, 'rgb(178, 205, 174)'],
      [0.076, 'rgb(188, 195, 169)'],
      [0.108, 'rgb(221, 207, 153)'],
      [0.153, 'rgb(211, 174, 114)'],
      [0.205, 'rgb(207, 155, 103)'],
      [0.277, 'rgb(179, 120, 85)'],
      [0.375, 'rgb(227, 210, 197)'],
      [0.66, 'rgb(255, 255, 255)'],
    ],
    'Transparent white': [
      [0.0, 'rgba(255, 255, 255, 0)'],
      [1.0, 'rgba(255, 255, 255, 1)'],
    ],
    Radar: [
      [0.0, 'rgba(35,23,27,0)'],
      [0.0714, 'rgba(68, 188, 116, 0.1)'],
      [0.1428, 'rgba(30, 183, 66, 0.3)'],
      [0.2142, 'rgba(45, 193, 81, 0.5)'],
      [0.2857, 'rgba(38, 208, 43, 0.7)'],
      [0.3571, 'rgba(58, 237, 55, 0.9)'],
      [0.4285, 'rgb(143, 252, 95)'],
      [0.5, 'rgb(190, 251, 81)'],
      [0.5714, 'rgb(200, 235, 26)'],
      [0.6428, 'rgb(245,199,43)'],
      [0.7142, 'rgb(255,155,33)'],
      [0.7857, 'rgb(251,105,25)'],
      [0.8571, 'rgb(214,57,15)'],
      [0.9285, 'rgb(168,22,4)'],
      [1.0, 'rgb(183, 44, 204)'],
    ],
  };

  const tilesets = {
    'mapbox.terrain-rgb': 'mapbox.terrain-rgb',
    'mapbox.satellite': 'mapbox.satellite',
  };

  const presets = {
    Satellite: {
      tileset: "mapbox.satellite",
      rasterColor: "Cubehelix",
      rasterColorMix: "luminosity",
      rasterColorRange: "[0, 1]"
    },
    Terrain: {
      tileset: "mapbox.terrain-rgb",
      rasterColor: "Hypsometric",
      rasterColorMix: "decode terrain-rgb",
      rasterColorRange: "[0, 8848]",
      forcePngraw: true
    }
  };

  function GUIParams() {
    this.preset = 'Satellite';
    this.opacity = 1;
    Object.assign(this, presets[this.preset]);
  }

  const guiParams = new GUIParams();

  function setStyle(tileset) {
    map.setStyle({
      version: 8,
      sources: {
        mapbox: {
          type: 'raster',
          url: 'mapbox://mapbox.satellite',
          tileSize: 256,
        },
        raster: {
          type: 'raster',
          tiles: [`https://api.mapbox.com/v4/${tileset}/{z}/{x}/{y}@2x.webp`],
          maxzoom: 14,
          tileSize: 256,
        },
      },
      layers: [
        {
          id: 'background',
          type: 'background',
          paint: {
            'background-color': 'rgb(4,7,14)',
          },
        },
        {
          id: 'satellite',
          type: 'raster',
          source: 'mapbox',
          'source-layer': 'mapbox_satellite_full',
          paint: {
            'raster-saturation': -0.3,
            'raster-brightness-max': 0.5,
          },
        },
        {
          type: 'raster',
          source: 'raster',
          id: 'raster',
          paint: {
            'raster-opacity': guiParams.opacity,
          },
        },
      ],
    });
  }

  function initMap() {
    mapboxgl.accessToken = config.mbglToken;
    map = new mapboxgl.Map({
      container: mapRef.value,
      zoom: 0,
      center: [0, 0],
      style: {version: 8, layers: [], sources: {}},
      transformRequest (url, type) {
        if (type !== "Tile") return;
        const preset = presets[guiParams.preset];
        if (preset.forbid2x) url = url.replace('@2x', '');
        if (preset.forcePngraw) url = url.replace('.webp', '.pngraw');
        return {url};
      }
    });

    map.once('load', () => {
      const pane = new Pane({
        title: 'raster color',
        container: guiPanel.value,
      });

      const preset = pane.addBinding(guiParams, 'preset', {
        options: Object.keys(presets).reduce((prev: any, current: string) => ({
          ...prev,
          [current]: current,
        }), {} as any),
      });
      const tileset = pane.addBinding(guiParams, 'tileset', {
        options: Object.keys(tilesets).reduce((prev: any, current: string) => ({
          ...prev,
          [current]: current,
        }), {} as any)
      });
      const scale = pane.addBinding(guiParams, 'rasterColor', {
        options: Object.keys(colorScales).reduce((prev: any, current: string) => ({
          ...prev,
          [current]: current,
        }), {} as any)
      });
      const mix = pane.addBinding(guiParams, 'rasterColorMix', {
        options: Object.keys(mixes).reduce((prev: any, current: string) => ({
          ...prev,
          [current]: current,
        }), {} as any)
      });
      const range = pane.addBinding(guiParams, 'rasterColorRange', {
        options: Object.keys(ranges).reduce((prev: any, current: string) => ({
          ...prev,
          [current]: current,
        }), {} as any)
      });
      const opacity = pane.addBinding(guiParams, 'opacity', {
        min: 0,
        max: 1,
      });

      function setColorScale(scale) {
        const range = ranges[guiParams.rasterColorRange];
        const cs = colorScales[scale];
        map.setPaintProperty('raster', 'raster-color', [
          'interpolate',
          ['linear'],
          ['raster-value'],
          ...cs.map(([pos, col]) => [range[0] + (range[1] - range[0]) * pos, col]).flat(),
        ]);
      }

      function setColorMix(mix) {
        map.setPaintProperty('raster', 'raster-color-mix', mixes[mix]);
      }

      function setColorRange(range) {
        map.setPaintProperty('raster', 'raster-color-range', ranges[range]);
        setColorScale(guiParams.rasterColor);
      }

      function setTileset(range) {
        setStyle(guiParams.tileset);
        setColorMix(guiParams.rasterColorMix);
        setColorScale(guiParams.rasterColor);
        setColorRange(guiParams.rasterColorRange);
      }

      function setPreset(presetName) {
        const preset = presets[presetName];
        Object.assign(guiParams, preset);
        setTileset(guiParams.tileset);
      }

      mix.on('change', ({ value }) => setColorMix(value));
      scale.on('change', ({ value }) => setColorScale(value));
      range.on('change', ({ value }) => setColorRange(value));
      tileset.on('change', ({ value }) => setTileset(value));
      preset.on('change', ({ value }) => setPreset(value));

      opacity.on('change', ({ value }) => {
        map.setPaintProperty('raster', 'raster-opacity', value);
      });

      setTileset(guiParams.tileset);
    });

    emits('mount');
  }

  onMounted(() => {
    initMap();
  });

  defineExpose({
    pause,
    resume,
  });
</script>

<style></style>

应用场景

针对栅格自定义着色除了能让开发者更精准的控制叠加的栅格数据,还能通过这些配置去实现诸如 DEM 自定义着色,自定义数据源着色等场景。一个常见的应用场景是气象数据渲染,比如我们的温度数据渲染:

我们准备一张原始归一化后的栅格数据,栅格的每个格点的值压入方式为 (原始值 - 当前瓦片最大值) / ( 当前瓦片最大值 - 当前瓦片最小值 ) * 255,得到的图片如下:

然后向地图添加一个数据源:

js 复制代码
map.addSource('temp', {
  type: 'image',
  url: './temp-surface.jpeg',
  coordinates: [
    [-180, 85.051129],
    [180, 85.051129],
    [180, -85.051129],
    [-180, -85.051129],
  ],
  // 201.81008911132812,317.9674377441406
})

添加图层:

js 复制代码
map.addLayer({
  type: 'raster',
  source: 'temp',
  id: 'temp',
  paint: {
    'raster-opacity': 1,
    'raster-color': [
      'interpolate',
      ['linear'],
      ['raster-value'],
      ...tempInterpolateColor,
    ],
     // 相当于实现了 r * (max - min) + min
    'raster-color-mix': [(317.9674377441406 - 273.15) - (201.81008911132812 - 273.15), 0, 0, 201.81008911132812 - 273.15],
    'raster-color-range': [201.81008911132812 - 273.15, 317.9674377441406 - 273.15]
  },
})

最终渲染效果如下:

​ 这样我们就简单的实现了常规气象数据的渲染,而且可以走 mapbox 内部渲染流程贴球和地形。

​ 当然其使用场景不止如此,更多的需要我们结合自身业务,探寻不一样的场景。这样大部分的简单场景我们都不需要再使用自定义图层做渲染,仅仅使用官方 api 就能实现,而且也能方便的融合 mapbox 的三维场景。

实现原理

主要是通过 render 层 栅格绘制着色器 配合实现,主要新增了 raster color 的配置获取解析

以及新增的 uniformValues变量rasterColor.mix rasterColor.range

在片段着色器中先对传入的两个纹理进行插值 mix(color0, color1, u_fade_t),再根据对应的值从颜色图 u_color_ramp采样出相应的颜色,最终将原栅格的透明度应用到采样后的颜色。

相关推荐
前端小巷子25 分钟前
Web开发中的文件上传
前端·javascript·面试
翻滚吧键盘1 小时前
{{ }}和v-on:click
前端·vue.js
上单带刀不带妹1 小时前
手写 Vue 中虚拟 DOM 到真实 DOM 的完整过程
开发语言·前端·javascript·vue.js·前端框架
杨进军2 小时前
React 创建根节点 createRoot
前端·react.js·前端框架
ModyQyW2 小时前
用 AI 驱动 wot-design-uni 开发小程序
前端·uni-app
说码解字2 小时前
Kotlin lazy 委托的底层实现原理
前端
爱分享的程序员3 小时前
前端面试专栏-算法篇:18. 查找算法(二分查找、哈希查找)
前端·javascript·node.js
翻滚吧键盘3 小时前
vue 条件渲染(v-if v-else-if v-else v-show)
前端·javascript·vue.js
vim怎么退出3 小时前
万字长文带你了解微前端架构
前端·微服务·前端框架
你这个年龄怎么睡得着的3 小时前
为什么 JavaScript 中 'str' 不是对象,却能调用方法?
前端·javascript·面试