关于前端:threejs三维地图大屏项目分享

52次阅读

共计 8444 个字符,预计需要花费 22 分钟才能阅读完成。

这是最近公司的一个我的项目。客户的需要是基于总公司和子公司的数据,开发一个数据展现大屏。大屏两边都是一些图表展现数据,两头局部是一个三维中国地图,点击中国地图的某个省份,能够下钻到省份地图的展现。地图上,会做一些数据的标注,信息标牌。如下图所示:

本文将对一些技术原理进行分享。

2d 图表

2d 图表局部,次要通过 echart 图表进行开发,另外还会波及到一些 icon 文字的展现。这个局部置信大部分前端人员都晓得如何进行开发,可能须要的就是开发人员对于色彩,字体等有较好的敏感性,能够最大水平还原设计搞。

鉴于大家都比拟熟知,不再具体阐明。

三维地图的展现

对于两头的三维地图局部。咱们个别有几种形式来实现。

  1. 建模人员对地图局部进行建模
  2. 通过 json 数据生成三维模型
  3. 通过 svg 图片生产三维模型。

其中形式 1 能达到最好的成果,毕竟手动建模了,须要的成果都能够通过建模师智慧的双手进行调整。然而工作量相对来说较大,须要建设中国地图和各个省份的地图。所以咱们最终放弃了建模的这种思路。

通过 json 数据生成三维地图

首先要获取 json 数据。
通过 datav 能够获取中国地图的 json 数据,参考如下连贯
http://datav.aliyun.com/porta…

获取数据之后,通过解析 json 数据,而后通过 threejs 的 ExtrudeGeometry 生成地图模型。代码如下所示:

 let jsonData = await (await fetch(jsonUrl)).json();
  // console.log(jsonData);
  let map = new dt.Group();
  if (type && type === "world") {
    jsonData.features = jsonData.features.filter((ele) => ele.properties.name === "China"
    );
  }
  jsonData.features.forEach((elem, index) => {if (filter && filter(elem) == false) {return;}
    if (!elem.properties.name) {return;}
    // 定一个省份 3D 对象
    const province = new dt.Group();
    // 每个的 坐标 数组
    const coordinates = elem.geometry.coordinates;
    const color = COLOR_ARR[index % COLOR_ARR.length];
    // 循环坐标数组
    coordinates.forEach((multiPolygon, index) => {if (elem.properties.name == "海南省" && index > 0) {return;}
      if (elem.properties.name == "台湾省" && index > 0) {return;}
      if (elem.properties.name == "广东省" && index > 0) {return;}
      multiPolygon.forEach((polygon) => {const shape = new dt.Shape();

        let positions = [];
        for (let i = 0; i < polygon.length; i++) {let [x, y] = projection(polygon[i]);

          if (i === 0) {shape.moveTo(x, -y);
          }
          shape.lineTo(x, -y);

          positions.push(x, -y, 4);
        }

        const lineMaterial = new dt.LineBasicMaterial({color: "white",});
        const lineGeometry = new dt.LineXGeometry();
        // let attribute = new dt.BufferAttribute(new Float32Array(positions), 3);
        // lineGeometry.setAttribute("position", attribute);
        lineGeometry.setPositions(positions);

        const extrudeSettings = {
          depth: 4,
          bevelEnabled: false,
          bevelSegments: 5,
          bevelThickness: 0.1,
        };

        const geometry = new dt.ExtrudeGeometry(shape, extrudeSettings);
        // console.log("geometyr", geometry);
        const material = new dt.StandardMaterial({
          metalness: 1,
          // color: color,
          map: texture,
          transparent: true,
        });

        let material1 = new dt.StandardMaterial({
          // polygonOffset: true,
          // polygonOffsetFactor: 1,
          // polygonOffsetUnits: 1,
          metalness: 1,
          roughness: 1,
          color: color, //"#3abcbd",
        });

        material1 = createSideShaderMaterial(material1);

        const mesh = new dt.Mesh(geometry, [material, material1]);
        if (index % 2 === 0) {// mesh.scale.set(1, 1, 1.2);
        }

        mesh.castShadow = true;
        mesh.receiveShadow = true;
        mesh._color = color;
        mesh.properties = elem.properties;
        if (!type) {province.add(mesh);
        }

        const matLine = new dt.LineXMaterial({
          polygonOffset: true,
          polygonOffsetFactor: -1,
          polygonOffsetUnits: -1,
          color: type === "world" ? "#00BBF4" : 0xffffff,
          linewidth: type === "world" ? 3.0 : 0.25, // in pixels
          vertexColors: false,
          dashed: false,
        });
        matLine.resolution.set(graph.width, graph.height);
        line = new dt.LineX(lineGeometry, matLine);
        line.computeLineDistances();
        province.add(line);
      });
    });

    // 将 geo 的属性放到省份模型中
    province.properties = elem.properties;
    if (elem.properties.centorid) {const [x, y] = projection(elem.properties.centorid);
      province.properties._centroid = [x, y];
    }

    map.add(province);

中国地图的 json 数据,理论包含的是每个省份的数据。
上述代码生成中国地图以及省之间的轮廓线。
其中 projection 是投影函数,转换经纬度坐标未平面坐标,用的是 d3 这个库:

const projection = d3
  .geoMercator()
  .center([104.0, 37.5])
  .scale(80)
  .translate([0, 0]);

依照设计稿,还需生成整个中国地图的外轮廓。这种状况下,咱们先获取 world.json,而后只获取中国的局部,通过这个局部来生成轮廓线。

最终成果如下:

能够看出,通过 json 的形式生产地图,世界地图的 json 数据和中国地图的 json 数据,边缘的贴合度并不高,因而外边缘轮廓和地图块不能很好的交融在一块。

基于此,须要找新的计划。

通过 svg 数据生成三维地图

因为有设计师提供设计稿,所以设计师必定能够提供中国地图的轮廓数据,以及外部的每个省份的轮廓数据。拿到设计的 svg 后,对 svg 门路进行解析,而后通过 ExtrudeGeometry 生成地图块对下,通过 line 生成轮廓线。

 let childNodes = svg.childNodes;
  childNodes.forEach((child) => {readSVGPath(child, graph, group);
  });
  if (svg.tagName == "path") {const shape = getShapeBySvg(svg);
    // let shape = $d3g.transformSVGPath(pathStr);
    const extrudeSettings = {
      depth: 15,
      bevelEnabled: false,
      bevelSegments: 5,
      bevelThickness: 0.1,
    };

    const color = COLOR_ARR[parseInt(Math.random() * 3) % COLOR_ARR.length];
    const geometry = new dt.ExtrudeGeometry(shape, extrudeSettings);
    let center = new dt.Vec3();
    // console.log(geometry.getBoundingBox().getCenter(center));
    // geometry.translate(-center.x, -center.y, -center.z);
    geometry.scale(1, -1, -1);
    geometry.computeVertexNormals();
    // console.log("geometry", geometry);
    const material = new dt.StandardMaterial({
      metalness: 1,
      // color: color,
      // visible: false,
      map: window.texture,
    });

    let material1 = new dt.StandardMaterial({
      polygonOffset: true,
      polygonOffsetFactor: 1,
      polygonOffsetUnits: 1,
      metalness: 1,
      roughness: 1,
      color: color, //"#3abcbd",
    });

    material1 = createSideShaderMaterial(material1);

    const mesh = new dt.Mesh(geometry, [material, material1]);
    group.add(mesh);

其中解析 svg 门路的代码如下:

function getShapeBySvg(svg) {let pathStr = svg.getAttribute("d");
  let province = svg.getAttribute("province");
  let commonds = new svgpathdata.SVGPathData(pathStr).commands;

  const shape = new dt.Shape();
  let lastC, cmd, c;
  for (let i = 0; i < commonds.length; i++) {cmd = commonds[i];
    let relative = cmd.relative;

    if (relative) {c = copy(cmd);
      let x = cmd.x || 0;
      let y = cmd.y || 0;
      let lx = lastC.x || 0;
      let ly = lastC.y || 0;
      c.x = x + lx;
      c.y = y + ly;
      c.x1 = c.x1 + lx;
      c.x2 = c.x2 + lx;
      c.y1 = c.y1 + ly;
      c.y2 = c.y2 + ly;
    } else {c = cmd;}
    if (lastC) {
      let lx = lastC.x,
        ly = lastC.y;
      if (Math.hypot(lx - c.x, ly - c.y) < 0.2 &&
        province == "内蒙" &&
        [16, 32, 128, 64, 512, 4, 8].includes(c.type)
      ) {console.log(c.type);
        continue;
      }
    }
    if (c.type == 2) {shape.moveTo(c.x, c.y);
    } else if (c.type == 16) {shape.lineTo(c.x, c.y);
    } else if (c.type == 32) {shape.bezierCurveTo(c.x1, c.y1, c.x2, c.y2, c.x, c.y);
      // shape.lineTo(c.x, c.y);
    } else if (c.type == 128 || c.type == 64) {shape.quadraticCurveTo(c.x1 || c.x2, c.y1 || c.y2, c.x, c.y);
      // shape.lineTo(c.x, c.y);
    } else if (c.type == 512) {// shape.absellipse(c.x, c.y, c.rX, c.rY, 0, Math.PI * 2, true);
      shape.lineTo(c.x, c.y);
    } else if (c.type == 4) {
      c.y = lastC.y;
      shape.lineTo(c.x, lastC.y);
    } else if (c.type == 8) {
      c.x = lastC.x;
      shape.lineTo(lastC.x, c.y);
    } else if (c.type == 1) {// shape.closePath();
    } else {// console.log(c);
    }
    lastC = c;
  }
  return shape;
}

其中外面波及到绝对定位的概念,一个 cmd 的坐标是绝对于上一个坐标的,而不是相对定位。这就须要咱们在解析的时候,通过累加的形式获取相对定位坐标。

另外 cmd 的 type 次要包含:

  //   ARC: 512
  // CLOSE_PATH: 1
  // CURVE_TO: 32
  // DRAWING_COMMANDS: 1020
  // HORIZ_LINE_TO: 4
  // LINE_COMMANDS: 28
  // LINE_TO: 16
  // MOVE_TO: 2
  // QUAD_TO: 128
  // SMOOTH_CURVE_TO: 64
  // SMOOTH_QUAD_TO: 256
  // VERT_LINE_TO: 8

通过 Shape 的 moveTo,lineTo,bezierCurveTo,quadraticCurveTo 等等与之对应。
最终成果如下图:

能够看出轮廓线更加圆滑,外轮廓和地图块的贴合度更高。
这是咱们我的项目最终采纳的技术计划。

侧边突变成果

上述两种计划的效果图,能够看出侧边地图的侧面都有突变成果,这种是通过定制 threejs 的材质的 shader 来实现的。大抵代码如下:


function createSideShaderMaterial(material) {material.onBeforeCompile = function (shader, renderer) {// console.log(shader.fragmentShader);
    shader.vertexShader = shader.vertexShader.replace("void main() {",
      "varying vec4 vPosition;\nvoid main() {");
    shader.vertexShader = shader.vertexShader.replace(
      "#include <fog_vertex>",
      "#include <fog_vertex>\nvPosition=modelMatrix * vec4(transformed, 1.0);"
    );

    shader.fragmentShader = shader.fragmentShader.replace("void main() {",
      "varying vec4 vPosition;\nvoid main() {");

    shader.fragmentShader = shader.fragmentShader.replace(
      "#include <transmissionmap_fragment>",
      `
      #include <transmissionmap_fragment>
      float z = vPosition.z;
      float s = step(2.0,z);
      vec3 bottomColor =  vec3(.0,1.,1.0);
    
      diffuseColor.rgb = mix(bottomColor,diffuseColor.rgb,s);
      // float r =  abs(1.0 * (1.0 - s) + z  * (0.0  - s * 1.0) + s * 4.0) ;
      float r =  abs(z  * (1.0  - s * 2.0) + s * 4.0) ;
      diffuseColor.rgb *= pow(r, 0.5 + 2.0 * s);
      
      // float c = 
    `
    );
  };

  return material;
}

通过 material.onBeforeCompile 办法实现材质的动静更改,而后通过 z 坐标的高度进行色彩的突变差值运算。

三维地图的贴图

下面实现的成果,都是简略的色彩。没有贴图成果,而设计师提供的原型是有突变成果的:

这须要咱们的贴图来进行解决。然而贴图并不简略,波及到 uv 的 offset 和 repeat 的计算。通过计算整个中国地图的 boundingbox,通过 bongdingbox 的 size 和 min 值来设置 uv 的 offset 和 repeat,能够很好的对其贴图和模型,如下代码:

 let box = new dt.Box3();
 box.setFromObject(map);
 et size = new dt.Vec3(),
    center = new dt.Vec3();
console.log(box.getSize(size));
console.log(box.getCenter(center));
console.log(box);

texture.repeat.set(1 / size.x, 1 / size.y);
texture.offset.set(box.min.x / size.x, box.min.y / size.y);

通过这种形式,贴图能够很好的和模型对齐,最终成果和设计稿差异很小。

三维地图 icon 标注定位

图片上的图标定位数据是经纬度,所以须要把定位度转换为三维中的坐标。此处应用的是双线性差值。先获取模型左上,右上,左下,右下四个点的经纬度坐标和三维坐标,而后通过双线性差值,联合某个特定点的经纬度值 计算出三维坐标。这种形式必定不是最准确的,却是最简略的。如果对于定位的精确性要求不高,能够采纳这种形式。

icon 动画(APNG)

icon 的动画是通过 apng 的图片实现的。解析 apng 的每一帧,而后绘制到 canvas 下面,作为 sprite 的贴图,并一直刷新贴图的内容,实现了动效成果。无关 apng 的解析,网上有开源的 JavaScript 的解析包。读者能够自行进行钻研,上面是一个参考链接:

https://github.com/movableink…

其余

其余方面包含

  1. 点击省份下钻 技术实现就是暗藏其余省份模型,显示以后省份模型,并加载以后省份的点位数据。技术思路比较简单。
  2. 鼠标悬浮显示名称等信息 通过 div 实现信息标签,通过三维坐标转平面坐标的投影算法, 计算标签地位, 代码如下:

     getViewPosition(vector) {this.camera.updateMatrixWorld();
     var ret = new Vec3();
     // ret = this.projector.projectVector(vector, this._camera, ret);
     ret = vector.project(this.camera);
     ret.x = ret.x / 2 + 0.5;
     ret.y = -ret.y / 2 + 0.5;
     var point = {x: (this._canvas.width * ret.x) / this._pixelRatio,
       y: (this._canvas.height * ret.y) / this._pixelRatio,
       h: this._canvas.height,
     };
     return point;
      }

总结

下面分享的三维地图大屏。波及到的技术点并不少,包含次要如下技术点:

  • echart 应用
  • json 解析生成地图 projection 投影
  • svg 解析生成三维地图模型
  • 动静材质批改
  • 贴图的 offset 和 repeat 算法等
  • 经纬度定位,双线性差值
  • 三维的三维坐标转平面坐标的投影算法

最终多个技术的交融,做出了文章结尾的成果。

其中比拟难的是两头三维地图的生成和成果优化计划,如果有相似需要的读者能够参考。

如果你有好的教训,也欢送和我交换。关注公号“ITMan 彪叔”能够增加作者微信进行交换,及时收到更多有价值的文章。

正文完
 0