1. 程式人生 > >空間資料視覺化之ArcLayer詳解

空間資料視覺化之ArcLayer詳解

deck-overlay中

首先使用d3中的scaleQuantile將資料進行分類,scaleQuantile方法是d3中的一種資料分類方法(https://www.cnblogs.com/kidsitcn/p/7182274.html)
https://raw.githubusercontent.com/uber-common/deck.gl-data/master/examples/arc/counties.json

 

 

_getArcs({data, selectedFeature}) {
    if (!data || !selectedFeature) {
      
return null; } const {flows, centroid} = selectedFeature.properties; const arcs = Object.keys(flows).map(toId => { const f = data[toId]; return { source: centroid, target: f.properties.centroid, value: flows[toId] }; }); const scale
= scaleQuantile() .domain(arcs.map(a => Math.abs(a.value))) .range(inFlowColors.map((c, i) => i)); arcs.forEach(a => { a.gain = Math.sign(a.value); a.quantile = scale(Math.abs(a.value)); }); return arcs; }

scaleQuantile是一種將連續的值轉化成離散的方法,最終離散成這幾種顏色分類

arc-layer中

這裡還是使用了例項化的方法,先新增一堆例項化變數:

initializeState() {
    const attributeManager = this.getAttributeManager();

    /* eslint-disable max-len */
    attributeManager.addInstanced({
      instancePositions: {
        size: 4,
        transition: true,
        accessor: ['getSourcePosition', 'getTargetPosition'],
        update: this.calculateInstancePositions
      },
      instanceSourceColors: {
        size: 4,
        type: GL.UNSIGNED_BYTE,
        transition: true,
        accessor: 'getSourceColor',
        update: this.calculateInstanceSourceColors
      },
      instanceTargetColors: {
        size: 4,
        type: GL.UNSIGNED_BYTE,
        transition: true,
        accessor: 'getTargetColor',
        update: this.calculateInstanceTargetColors
      }
    });
    /* eslint-enable max-len */
  }

然後是製作圖形,這裡使用50個點來模擬一條拋物線的效果

_getModel(gl) {
    let positions = [];
    const NUM_SEGMENTS = 50; // 利用50個點來模擬曲線
    /*
     *  (0, -1)-------------_(1, -1)
     *       |          _,-"  |
     *       o      _,-"      o
     *       |  _,-"          |
     *   (0, 1)"-------------(1, 1)
     */
    for (let i = 0; i < NUM_SEGMENTS; i++) { // 使用三角帶的方式來繪製三角形,同時這裡的-1和1也是為了在繪製寬度的時候確定法向量的偏移
      positions = positions.concat([i, -1, 0, i, 1, 0]);
    }

    const model = new Model(
      gl,
      Object.assign({}, this.getShaders(), {
        id: this.props.id,
        geometry: new Geometry({
          drawMode: GL.TRIANGLE_STRIP,
          attributes: {
            positions: new Float32Array(positions)
          }
        }),
        isInstanced: true,
        shaderCache: this.context.shaderCache // 快取著色器,我懷疑自己寫的hexagon偏慢也跟這個有關係
      })// 繪製物體,這裡是5.x的版本在新的版本中還要設定instanceCount引數,來控制繪製例項的數量
    );

    model.setUniforms({numSegments: NUM_SEGMENTS});

    return model;
  }

下面是計算一些例項變數,根據data的數量來控制,但是luma好像會預設給例項變數的陣列分配大小,實際的value中有一些多餘的空間,如果資料量小的話,可能繪製不出來;比如:data有22條線,按照如下計算,instancePositions可用的value就只有88個元素。

calculateInstancePositions(attribute) {
    const {data, getSourcePosition, getTargetPosition} = this.props;
    const {value, size} = attribute;
    let i = 0;
    for (const object of data) {
      const sourcePosition = getSourcePosition(object);
      const targetPosition = getTargetPosition(object);
      value[i + 0] = sourcePosition[0];
      value[i + 1] = sourcePosition[1];
      value[i + 2] = targetPosition[0];
      value[i + 3] = targetPosition[1];
      i += size;
    }
  }

  calculateInstancePositions64Low(attribute) {
    const {data, getSourcePosition, getTargetPosition} = this.props;
    const {value, size} = attribute;
    let i = 0;
    for (const object of data) {
      const sourcePosition = getSourcePosition(object);
      const targetPosition = getTargetPosition(object);
      value[i + 0] = fp64LowPart(sourcePosition[0]);
      value[i + 1] = fp64LowPart(sourcePosition[1]);
      value[i + 2] = fp64LowPart(targetPosition[0]);
      value[i + 3] = fp64LowPart(targetPosition[1]);
      i += size;
    }
  }

  calculateInstanceSourceColors(attribute) {
    const {data, getSourceColor} = this.props;
    const {value, size} = attribute;
    let i = 0;
    for (const object of data) {
      const color = getSourceColor(object);
      value[i + 0] = color[0];
      value[i + 1] = color[1];
      value[i + 2] = color[2];
      value[i + 3] = isNaN(color[3]) ? 255 : color[3];
      i += size;
    }
  }

  calculateInstanceTargetColors(attribute) {
    const {data, getTargetColor} = this.props;
    const {value, size} = attribute;
    let i = 0;
    for (const object of data) {
      const color = getTargetColor(object);
      value[i + 0] = color[0];
      value[i + 1] = color[1];
      value[i + 2] = color[2];
      value[i + 3] = isNaN(color[3]) ? 255 : color[3];
      i += size;
    }
  }

著色器程式碼

 

#define SHADER_NAME arc-layer-vertex-shader

attribute vec3 positions; // 幾何圖形的座標,同時這裡面也編碼了一些資訊,x代表線段索引,y可以代表偏移方向
// 本次可用的一些例項變數
attribute vec4 instanceSourceColors;// 起點的顏色
attribute vec4 instanceTargetColors; // 終點的顏色
attribute vec4 instancePositions; // 前兩個值記錄了起點經緯度,後兩個值記錄了終點經緯度
attribute vec3 instancePickingColors;

uniform float numSegments; // 拋物線的線段數量
uniform float strokeWidth; // 線寬度
uniform float opacity;

varying vec4 vColor;

// source和target是在3d空間中的單位,ratio代表本此線段在匯流排段數目的比值範圍在0~1,返回值時拋物線高度的平方
// 這裡的方式決定高度單位與source/target的單位保持一致
float paraboloid(vec2 source, vec2 target, float ratio) {

  vec2 x = mix(source, target, ratio); // 獲取該線段節點對應的直線位置
  vec2 center = mix(source, target, 0.5);// 取中心點,充分利用glsl內建函式,提升效能

  // 拋物線的公式應該是y * y = (source - center)^2 - (x - center)^2;
  float dSourceCenter = distance(source, center);
  float dXCenter = distance(x, center);
  return (dSourceCenter + dXCenter) * (dSourceCenter - dXCenter);
}

// 在螢幕空間中計算偏移值,最後在反算到裁切空間,也就是ndc空間
// offset_direction在position的y座標中記錄
// offset vector by strokeWidth pixels
// offset_direction is -1 (left) or 1 (right)
vec2 getExtrusionOffset(vec2 line_clipspace, float offset_direction) {
  // normalized direction of the line
  // ndc空間中的座標乘以螢幕寬高畫素,轉換成2維螢幕畫素;然後歸一化成單位向量
  vec2 dir_screenspace = normalize(line_clipspace * project_uViewportSize);
  // rotate by 90 degrees
  dir_screenspace = vec2(-dir_screenspace.y, dir_screenspace.x); // 求法線向量

  // 法向量乘以偏移方向乘以寬度一半獲取在螢幕空間中的偏移值
  vec2 offset_screenspace = dir_screenspace * offset_direction * strokeWidth / 2.0;
  // 將螢幕座標反算到ndc空間
  vec2 offset_clipspace = project_pixel_to_clipspace(offset_screenspace).xy;

  return offset_clipspace; // 返回ndc空間的偏移量
}

float getSegmentRatio(float index) { // 返回線段索引在匯流排段數目中的比值,轉換成0~1之間
  return smoothstep(0.0, 1.0, index / (numSegments - 1.0));
}

vec3 getPos(vec2 source, vec2 target, float segmentRatio) { // 獲取線段節點在三維空間中的位置
  float vertex_height = paraboloid(source, target, segmentRatio); // 獲取高度資訊

  return vec3(
    mix(source, target, segmentRatio), // 獲取節點的x/y座標
    sqrt(max(0.0, vertex_height))// 獲取節點的高度座標
  );
}

void main(void) {
  // 將insance中編碼的起終點的經緯度分別轉換成瓦片畫素單位
  vec2 source = project_position(instancePositions.xy);
  vec2 target = project_position(instancePositions.zw);

  float segmentIndex = positions.x;// 節點的線段索引
  float segmentRatio = getSegmentRatio(segmentIndex);
  // if it's the first point, use next - current as direction
  // otherwise use current - prev
  // 這裡處理方式比較巧妙,充分利用內建函式優勢;
  // step(edge, x) 作用如: x>=edge ? 1.0 : 0.0
  // 所以上面英文註釋所說,如果是起點就使用next-curr,其他的都是用curr - prev
  //float indexDir = mix(-1.0, 1.0, step(segmentIndex, 0.0));
  float indexDir = mix(-1.0, 1.0, (segmentIndex <= 0.0 ? 1.0 : 0.0));
  // 根據indexDir獲取下一段或者上一個線段節點的比值
  float nextSegmentRatio = getSegmentRatio(segmentIndex + indexDir);

  // 獲取兩個節點的3維世界座標並轉化成ndc座標
  vec3 currPos = getPos(source, target, segmentRatio);
  vec3 nextPos = getPos(source, target, nextSegmentRatio);
  vec4 curr = project_to_clipspace(vec4(currPos, 1.0));
  vec4 next = project_to_clipspace(vec4(nextPos, 1.0));

  // extrude
  // 進行線寬拉伸,獲取法線方向的偏移
  vec2 offset = getExtrusionOffset((next.xy - curr.xy) * indexDir, positions.y);
  gl_Position = curr + vec4(offset, 0.0, 0.0); // 獲取最終節點的ndc位置

  // 根據線段節點位置計算顏色插值
  vec4 color = mix(instanceSourceColors, instanceTargetColors, segmentRatio) / 255.;
  vColor = vec4(color.rgb, color.a * opacity);// 獲取最終顏色

  // Set color to be rendered to picking fbo (also used to check for selection highlight).
  picking_setPickingColor(instancePickingColors);
}