Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >threejs三维地图大屏项目分享

threejs三维地图大屏项目分享

作者头像
用户3158888
发布于 2022-11-23 08:37:43
发布于 2022-11-23 08:37:43
3.9K04
代码可运行
举报
运行总次数:4
代码可运行

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

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

2d图表

2d图表部分,主要通过echart图表进行开发,另外还会涉及到一些icon 文字的展示。 这个部分相信大部分前端人员都知道如何进行开发,可能需要的就是开发人员对于颜色,字体等有较好的敏感性,可以最大程度还原设计搞。

鉴于大家都比较熟知,不再详细说明。

三维地图的展示

对于中间的三维地图部分。 我们一般有几种方式来实现。

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

其中方式1能达到最好的效果,毕竟手动建模了,需要的效果都可以通过建模师智慧的双手进行调整。但是工作量相对来说较大,需要建立中国地图和各个省份的地图。 所以我们最终放弃了建模的这种思路。

通过json数据生成三维地图

首先要获取json数据。 通过datav可以获取中国地图的json数据

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

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 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这个库:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
const projection = d3
  .geoMercator()
  .center([104.0, 37.5])
  .scale(80)
  .translate([0, 0]);

按照设计稿,还需生成整个中国地图的外轮廓。这种情况下,我们先获取world.json,然后只获取中国的部分,通过这个部分来生成轮廓线。

最终效果如下:

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

基于此,需要找新的方案。

通过svg数据生成三维地图

由于有设计师提供设计稿,所以设计师肯定可以提供中国地图的轮廓数据,以及内部的每个省份的轮廓数据。拿到设计的svg后,对svg路径进行解析,然后通过ExtrudeGeometry生成地图块对下,通过line生成轮廓线。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 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路径的代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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主要包括:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  //   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来实现的。大致代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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,可以很好的对其贴图和模型,如下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 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...

其他

其他方面包括

点击省份下钻 技术实现就是隐藏其他省份模型,显示当前省份模型,并加载当前省份的点位数据。技术思路比较简单。

鼠标悬浮显示名称等信息 通过div实现信息标签,通过三维坐标转平面坐标的投影算法,计算标签位置,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 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算法等
  • 经纬度定位,双线性差值
  • 三维的三维坐标转平面坐标的投影算法

最终多个技术的融合,做出了文章开头的效果。

其中比较难的是中间三维地图的生成和效果优化方案,如果有类似需求的读者可以参考。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-11-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【愚公系列】2023年08月 Three.js专题-几何体
几何体是指由点、线、面所构成的空间实体。其中,点、线、面是几何体的基本元素,几何体包括球体、立方体、圆锥体、圆柱体、棱柱、棱锥、棱台等。这些几何体都有自己的特定形状和特征,可以应用于各种数学、物理和工程领域。
愚公搬代码
2025/05/28
610
【愚公系列】2023年08月 Three.js专题-几何体
基于threejs实现中国地图轮廓动画
目前项目的中国地图是echarts画的,现在这想再次基础上增加一个中国地图描边动画。
星宇大前端
2022/05/06
3.3K0
基于threejs实现中国地图轮廓动画
Three.js实战—中国地图
这里涉及到一个知识点,墨卡托投影转换。墨卡托投影转换可以把我们经纬度坐标转换成我们对应平面的2d坐标。具体原理
阶钟
2024/12/03
1.9K0
Three.js实战—中国地图
three.js 绘制3d地图
这篇郭先生就来说说使用three.js几何体制作3D地图。在线案例点击3D中国地图
郭先生的博客
2020/08/31
11.5K0
three.js 绘制3d地图
北京到上海,Three.js 旅行轨迹的可视化
最近从北京搬到了上海,开始了一段新的生活,算是人生中一个比较大的事件,于是特地用 Three.js 做了下可视化。
神说要有光zxg
2021/12/17
1.8K0
北京到上海,Three.js 旅行轨迹的可视化
threejs地球、星空、世界轮廓绘制、飞线、坐标涟漪 、旋转动画(上篇)
github仓库地址:https://github.com/RainManGO/3d-earth
星宇大前端
2022/03/09
12.3K0
threejs地球、星空、世界轮廓绘制、飞线、坐标涟漪 、旋转动画(上篇)
我是如何用 Three.js 在三维世界建房子的(详细教程)
这两天用 Three.js 画了一个 3D 的房子,放了一个床进去,可以用鼠标和键盘控制移动,有种 3D 游戏的即视感。
神说要有光zxg
2021/12/10
5.4K0
我是如何用 Three.js 在三维世界建房子的(详细教程)
Threejs项目实战之四:实现地图雷达效果
最近事情比较多,今晚难得有空,就抽空完成了一个使用Threejs实现地图雷达扫描效果的程序,下面说下代码实现的原理及核心代码,老规矩,先看下效果图
九仞山
2023/12/30
1K0
Threejs项目实战之四:实现地图雷达效果
用Three.js构建三维世界的房子
最近在学习Three.js,无奈不知道从哪里下手,查阅大部分资料都是先介绍渲染器(Renderer)、场景(Scene)、照相机(Camera),其实这些概念确实需要了解,如果不给你立体模型,你始终是无法理解的。网上看了一个大佬(神说要有光)的教程,感觉算是一只脚已经入了门,接下来我们通过这篇文章,从造物主的视角开始创建一个房子。我们先看下最终效果。
青年码农
2022/12/13
2K0
用Three.js构建三维世界的房子
Threejs进阶之十八:使用ExtrudeGeometry从二维图形创建三维几何体
上一节我们介绍了Threejs中二维图形相关的类,这一节我们来聊一聊如何通过创建的二维图形来生成三维图形
九仞山
2023/10/14
2.1K0
Threejs进阶之十八:使用ExtrudeGeometry从二维图形创建三维几何体
如何1人5天开发完3D数据可视化大屏,超炫酷 【二】
与地球的实现方法不同,平面地图依赖geojson进行绘制。有什么样的geojson,绘制什么样的地图块。
winty
2020/11/23
1.9K0
如何1人5天开发完3D数据可视化大屏,超炫酷 【二】
电力布局三维编辑器功能设计
最近和一家公司在谈一个项目合作,他们公司主要是做电力相关的。 项目背景大概是这样的: 国家电网对电网资产需要做到数字化管理,对现有变压器台区内的电表箱电能表做可视化数字孪生管理。 由于涉及到的台区非常多,所以客户希望开发的不是单个项目,而是可以实现项目的3D编辑器,使得电网的台区经理使用编辑器编辑出所负责的变压器台区的设备关系场景及数据状态展示。
用户3158888
2023/04/28
5030
电力布局三维编辑器功能设计
threejs三维模型添加文字标签,及添加文字的方式介绍
上次在文章ThreeJS中三维世界坐标转换成二维屏幕坐标介绍了三维二维坐标的转换方法,今天结合一个用例具体说下用法。
程序你好
2021/07/23
23.3K0
threejs三维模型添加文字标签,及添加文字的方式介绍
教你如何用Three.js创造一个三维太阳系
笔者认为Three.js是一个伟大的框架,为什么这样说,因为它可以让我们轻易创造三维世界,甚至好像笔者写这遍教程,可以创造一个太阳系,在这个三维世界里你就是创世主。哈哈!好像说得有点夸!!
lizhenwen
2021/07/18
2.8K2
教你如何用Three.js创造一个三维太阳系
Threejs进阶之十七:Threejs中的Path、Shape和ShapeGeometry类
在实际的应用中,有时候需要我们根据一个二维图形拉伸为三维图形的情况,这就需要我们对Threejs中提供的二维图形相关的类有一个深入的了解,这一节我们就深入的聊一聊Threejs中的Path、Shape和ShapeGeometry类
九仞山
2023/10/14
2.1K0
Threejs进阶之十七:Threejs中的Path、Shape和ShapeGeometry类
webgl(threejs)生成房间楼层
在很多数字孪生项目中,都会涉及到楼层的建模。楼层的建模由于结构繁多,如果都是建模师进行手动建模,工作量会比较大。而楼层本身的结构,可以抽象成可以通过路径构造的对象(这和之前的文章提及的的管路以及道路类似),这方便我们通过代码的方式来生成房间楼层。
用户3158888
2022/09/23
1.7K0
webgl(threejs)生成房间楼层
Threejs进阶之十一:使用FontLoader和TextGeometry创建三维文字
在Threejs中我们可以通过FontLoader和TextGeometry结合使用来创建三维文字,FontLoader用于加载JSON格式的字体,FontLoader返回值是表示字体的Shape类型的数组;TextGeometry用于将文本生成为单一的几何体。下面我们先来了解下这两个类
九仞山
2023/05/03
3.9K0
Threejs进阶之十一:使用FontLoader和TextGeometry创建三维文字
three.js 着色器材质之变量(一)
上一篇说顶点着色器和片元着色器的皮毛,这篇郭先生说一说着色器变量,通过变量可以设置材质。先看看今天要做的如下图。在线案例请点击红绿灯。
郭先生的博客
2020/08/31
1.6K0
three.js 着色器材质之变量(一)
用 Three.js 画一个哆啦A梦的时光机
在 three.js 里以向右的方向为 x 轴,向上的方向为 y 轴,向前的方向为 z 轴:
神说要有光zxg
2023/08/28
5860
用 Three.js 画一个哆啦A梦的时光机
(Vue3结合ThreeJS开发3D)在线三维CAD中绘制窗户模型
本文使用mxcad3d在网页中创建一个简单的三维窗户模型,mxcad3d提供了丰富的三维建模功能和便捷的API,使得创建各种三维模型变得简单方便,最终效果如下图:
梦想云图网页CAD
2024/10/30
1880
(Vue3结合ThreeJS开发3D)在线三维CAD中绘制窗户模型
推荐阅读
相关推荐
【愚公系列】2023年08月 Three.js专题-几何体
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验