北京到上海,Three.js 旅行轨迹的可视化

开发 前端
最近从北京搬到了上海,开始了一段新的生活,算是人生中一个比较大的事件,于是特地用 Three.js 做了下可视化。

[[440408]]

本文转载自微信公众号「神光的编程秘籍」,作者神说要有光zxg。转载本文请联系神光的编程秘籍公众号。

最近从北京搬到了上海,开始了一段新的生活,算是人生中一个比较大的事件,于是特地用 Three.js 做了下可视化。

在这个地理信息相关的可视化的案例中,我们能学到地图怎么画、经纬度如何转成坐标值,这些是地理可视化的通用技术。

那我们就开始吧。

思路分析

Three.js 画立方体、画圆柱、画不规则图形我们都画过,但是如何画一个地图呢?

其实地图也是由线、由多边形构成的,有了数据我们就能画出来,缺少的只是数据。

地图信息的描述是一个通用需求,所以有相应的国际标准,就是 GeoJson,它是通过点、线、多边形来描述地理信息的。

通过指定点、线、多边形的类型、然后指定几个坐标位置,就可以描述出相应的形状。

geojson 的数据可以通过 geojson.io 这个网站做下预览。

比如中国地图的 geojson:

有了这个 json,只要用 Three.js 画出来就行,通过线和多边形两种方式。

但是还有一个问题,geojson 中记录的是经纬度信息,应该如何转成二维坐标来画呢?

这就涉及到了墨卡托转换,它就是做经纬度转二维坐标的事情。

这个转换也不用我们自己实现,可以用 d3 内置的墨卡托坐标转换函数来做。

这样,我们就用 Three.js 根据 geojson 来画出地图。

我们还要画一条北京到上海的曲线,这个用贝塞尔曲线画就行,知道两个端点的坐标,控制点放在中间的位置。

那怎么知道两个端点,也就是上海和北京的坐标呢?

这个可以用“百度坐标拾取系统”这个工具,点击地图的某个位置,就可以直接拿到那个位置的经纬度。然后我们做一次墨卡托转换,就拿到坐标了。

地图画出来了,旅行的曲线也画出来了,接下来调整下相机位置,从北京慢慢移动到上海就可以了。

思路理清了,我们来写下代码。

代码实现

我们要引入 d3,然后使用 d3 的墨卡托转换功能,

  1. const projection = d3.geoMercator() 
  2.     .center([116.412318,39.909843]) 
  3.     .translate([0, 0]); 

中间点的坐标就是北京的经纬度,就是我们通过“百度坐标拾取工具”那里拿到的。

北京和上海的坐标位置也可以把经纬度做墨卡托转换得到:

  1. let beijingPosition= projection([116.412318,39.909843]); 
  2. let shanghaiPosition = projection([121.495721,31.236797]); 

先不着急画旅行的曲线,先来画地图吧。

先加载 geojson:

  1. const loader = new THREE.FileLoader(); 
  2. loader.load('./data/china.json', (data) => { 
  3.     const jsondata = JSON.parse(data); 
  4.     generateGeometry(jsondata); 
  5. }) 

然后根据 json 的信息画地图。

遍历 geojson 的数据,把每个经纬度通过墨卡托转换变成坐标,然后分别用线和多边形画出来。

画多边形的时候遇到北京和上海用黄色,其他城市用蓝色。

  1. function generateGeometry(jsondata) { 
  2.   const map = new THREE.Group(); 
  3.      
  4.   jsondata.features.forEach((elem) => { 
  5.     const province = new THREE.Group(); 
  6.  
  7.     // 经纬度信息 
  8.     const coordinates = elem.geometry.coordinates; 
  9.     coordinates.forEach((multiPolygon) => { 
  10.       multiPolygon.forEach((polygon) => { 
  11.         // 画轮廓线 
  12.         const line = drawBoundary(polygon); 
  13.  
  14.         // 画多边形 
  15.         const provinceColor = ['北京市''上海市'].includes(elem.properties.name) ? 'yellow' : 'blue'
  16.         const mesh = drawExtrudeMesh(polygon, provinceColor); 
  17.  
  18.         province.add(line); 
  19.         province.add(mesh); 
  20.       }); 
  21.     }); 
  22.  
  23.     map.add(province); 
  24.   }) 
  25.  
  26.   scene.add(map); 

然后分别实现画轮廓线和画多边形:

轮廓线(Line)就是指定一系列顶点来构成几何体(Geometry),然后指定材质(Material)颜色为黄色:

  1. function drawBoundary(polygon) { 
  2.     const lineGeometry = new THREE.Geometry(); 
  3.  
  4.     for (let i = 0; i < polygon.length; i++) { 
  5.       const [x, y] = projection(polygon[i]); 
  6.       lineGeometry.vertices.push(new THREE.Vector3(x, -y, 0)); 
  7.     } 
  8.  
  9.     const lineMaterial = new THREE.LineBasicMaterial({  
  10.       color: 'yellow' 
  11.     }); 
  12.  
  13.     return new THREE.Line(lineGeometry, lineMaterial); 

现在的效果是这样的:

多边形是 ExtrudeGeometry,也就是可以先画出形状(shape),然后通过拉伸变成三维的。

  1. function drawExtrudeMesh(polygon, color) { 
  2.     const shape = new THREE.Shape(); 
  3.  
  4.     for (let i = 0; i < polygon.length; i++) { 
  5.       const [x, y] = projection(polygon[i]); 
  6.  
  7.       if (i === 0) { 
  8.         shape.moveTo(x, -y); 
  9.       } 
  10.  
  11.       shape.lineTo(x, -y); 
  12.     } 
  13.  
  14.     const geometry = new THREE.ExtrudeGeometry(shape, { 
  15.       depth: 0, 
  16.       bevelEnabled: false 
  17.     }); 
  18.  
  19.     const material = new THREE.MeshBasicMaterial({ 
  20.       color, 
  21.       transparent: true
  22.       opacity: 0.2, 
  23.     }) 
  24.  
  25.     return new THREE.Mesh(geometry, material); 

第一个点用 moveTo,后面的点用 lineTo,这样连成一个多边形,然后指定厚度为 0,指定侧面不需要多出一块斜面(bevel)。

这样,我们就给每个省都填充上了颜色,北京和上海是黄色,其余省是蓝色。

接下来,在北京和上海之间画一条贝塞尔曲线:

  1. const line = drawLine(beijingPosition, shanghaiPosition); 
  2. scene.add(line); 

贝塞尔曲线用 QuadraticBezierCurve3 来画,控制点指定中间位置的点。

  1. function drawLine(pos1, pos2) { 
  2.   const [x0, y0, z0] = [...pos1, 0]; 
  3.   const [x1, y1, z1] = [...pos2, 0]; 
  4.  
  5.   const geomentry = new THREE.Geometry(); 
  6.   geomentry.vertices = new THREE.QuadraticBezierCurve3( 
  7.       new THREE.Vector3(-x0, -y0, z0), 
  8.       new THREE.Vector3(-(x0 + x1) / 2, -(y0 + y1) / 2, -10), 
  9.       new THREE.Vector3(-x1, -y1, z1), 
  10.   ).getPoints(); 
  11.  
  12.   const material = new THREE.LineBasicMaterial({color: 'white'}); 
  13.  
  14.   const line = new THREE.Line(geomentry, material); 
  15.   line.rotation.y = Math.PI; 
  16.  
  17.   return line; 

这样,地图和旅行轨迹就都画完了:

当然,还有渲染器、相机、灯光的初始化代码:

渲染器:

  1. const renderer = new THREE.WebGLRenderer(); 
  2. renderer.setClearColor(0x000000); 
  3. renderer.setSize(window.innerWidth, window.innerHeight); 
  4. document.body.appendChild(renderer.domElement); 

渲染器设置背景颜色为黑色,画布大小为窗口大小。

灯光:

  1. let ambientLight = new THREE.AmbientLight(0xffffff); 
  2. scene.add(ambientLight); 

灯光用环境光,也就是每个方向的明暗都一样。

相机:

  1. const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); 
  2. camera.position.set(0, 0, 10); 
  3. camera.lookAt(scene.position); 

相机用透视相机,特点是近大远小,需要指定看的角度,宽高比,和远近的范围这样四个参数。

位置设置在 0 0 10 的位置,在这个位置去观察 0 0 0,就是北京上方的俯视图(我们做墨卡托转换的时候指定了北京为中心)。

修改了相机位置之后,看到的地图大了许多:

接下来就是一帧帧的渲染,在每帧渲染的时候移动下相机位置,这样就是从北京到上海的一个移动的效果:

  1. function render() { 
  2.     if(camera.position.x < shanghaiPosition[0]) { 
  3.         camera.position.x += 0.1; 
  4.     }   
  5.     if(camera.position.y > -shanghaiPosition[1]) { 
  6.         camera.position.y -= 0.2; 
  7.     } 
  8.     renderer.render(scene, camera); 
  9.     requestAnimationFrame(render); 

大功告成!我们来看下最终的效果吧:

代码上传到了 github: https://github.com/QuarkGluonPlasma/threejs-exercize

也在这里贴一份:

  1. <!DOCTYPE html> 
  2. <html lang="en"
  3.   <head> 
  4.     <meta charset="UTF-8" /> 
  5.     <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 
  6.     <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 
  7.     <title>map-travel</title> 
  8.     <style> 
  9.       html body { 
  10.         height: 100%; 
  11.         width: 100%; 
  12.         margin: 0; 
  13.         padding: 0; 
  14.         overflow: hidden; 
  15.       } 
  16.     </style> 
  17.   </head> 
  18.   <body> 
  19.     <script src="./js/three.js"></script> 
  20.     <script src="./js/d3.js"></script> 
  21.     <script> 
  22.       const scene = new THREE.Scene(); 
  23.  
  24.       const renderer = new THREE.WebGLRenderer(); 
  25.       renderer.setClearColor(0x000000); 
  26.       renderer.setSize(window.innerWidth, window.innerHeight); 
  27.       document.body.appendChild(renderer.domElement); 
  28.  
  29.       const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); 
  30.       camera.position.set(0, 0, 10); 
  31.       camera.lookAt(scene.position); 
  32.  
  33.       let ambientLight = new THREE.AmbientLight(0xffffff); 
  34.       scene.add(ambientLight); 
  35.  
  36.       function create() { 
  37.           const loader = new THREE.FileLoader(); 
  38.           loader.load('./data/china.json', (data) => { 
  39.             const jsondata = JSON.parse(data); 
  40.             generateGeometry(jsondata); 
  41.           }) 
  42.       } 
  43.  
  44.       const projection = d3.geoMercator() 
  45.             .center([116.412318,39.909843]) 
  46.             .translate([0, 0]); 
  47.  
  48.       let beijingPosition= projection([116.412318,39.909843]); 
  49.       let shanghaiPosition = projection([121.495721,31.236797]); 
  50.  
  51.       function drawBoundary(polygon) { 
  52.         const lineGeometry = new THREE.Geometry(); 
  53.  
  54.         for (let i = 0; i < polygon.length; i++) { 
  55.           const [x, y] = projection(polygon[i]); 
  56.           lineGeometry.vertices.push(new THREE.Vector3(x, -y, 0)); 
  57.         } 
  58.  
  59.         const lineMaterial = new THREE.LineBasicMaterial({  
  60.           color: 'yellow' 
  61.         }); 
  62.  
  63.         return new THREE.Line(lineGeometry, lineMaterial); 
  64.       } 
  65.  
  66.       function drawExtrudeMesh(polygon, color) { 
  67.         const shape = new THREE.Shape(); 
  68.  
  69.         for (let i = 0; i < polygon.length; i++) { 
  70.           const [x, y] = projection(polygon[i]); 
  71.  
  72.           if (i === 0) { 
  73.             shape.moveTo(x, -y); 
  74.           } 
  75.  
  76.           shape.lineTo(x, -y); 
  77.         } 
  78.  
  79.         const geometry = new THREE.ExtrudeGeometry(shape, { 
  80.           depth: 0, 
  81.           bevelEnabled: false 
  82.         }); 
  83.  
  84.         const material = new THREE.MeshBasicMaterial({ 
  85.           color, 
  86.           transparent: true
  87.           opacity: 0.2, 
  88.         }) 
  89.          
  90.         return new THREE.Mesh(geometry, material); 
  91.       } 
  92.  
  93.       function generateGeometry(jsondata) { 
  94.           const map = new THREE.Group(); 
  95.  
  96.           jsondata.features.forEach((elem) => { 
  97.             const province = new THREE.Group(); 
  98.  
  99.             const coordinates = elem.geometry.coordinates; 
  100.             coordinates.forEach((multiPolygon) => { 
  101.               multiPolygon.forEach((polygon) => { 
  102.                 const line = drawBoundary(polygon); 
  103.  
  104.                 const provinceColor = ['北京市''上海市'].includes(elem.properties.name) ? 'yellow' : 'blue'
  105.                 const mesh = drawExtrudeMesh(polygon, provinceColor); 
  106.                  
  107.                 province.add(line); 
  108.                 province.add(mesh); 
  109.               }); 
  110.             }); 
  111.  
  112.             map.add(province); 
  113.           }) 
  114.  
  115.           scene.add(map); 
  116.           const line = drawLine(beijingPosition, shanghaiPosition); 
  117.           scene.add(line); 
  118.  
  119.       } 
  120.  
  121.       function render() { 
  122.         if(camera.position.x < shanghaiPosition[0]) { 
  123.             camera.position.x += 0.1; 
  124.         }   
  125.         if(camera.position.y > -shanghaiPosition[1]) { 
  126.             camera.position.y -= 0.2; 
  127.         } 
  128.         renderer.render(scene, camera); 
  129.         requestAnimationFrame(render); 
  130.       } 
  131.  
  132.       function drawLine(pos1, pos2) { 
  133.           const [x0, y0, z0] = [...pos1, 0]; 
  134.           const [x1, y1, z1] = [...pos2, 0]; 
  135.  
  136.           const geomentry = new THREE.Geometry(); 
  137.           geomentry.vertices = new THREE.QuadraticBezierCurve3( 
  138.               new THREE.Vector3(-x0, -y0, z0), 
  139.               new THREE.Vector3(-(x0 + x1) / 2, -(y0 + y1) / 2, -10), 
  140.               new THREE.Vector3(-x1, -y1, z1), 
  141.           ).getPoints(); 
  142.  
  143.           const material = new THREE.LineBasicMaterial({color: 'white'}); 
  144.  
  145.           const line = new THREE.Line(geomentry, material); 
  146.           line.rotation.y = Math.PI; 
  147.  
  148.           return line; 
  149.       } 
  150.  
  151.       create(); 
  152.       render(); 
  153.     </script> 
  154.   </body> 
  155. </html> 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

总结

地图形状的表示是基于 geojson 的规范,它是由点、线、多边形等信息构成的。

用 Three.js 或者其他绘制方式来画地图只需要加载 geojson 的数据,然后通过线和多边型把每一部分画出来。

画之前还要把经纬度转成坐标,这需要用到墨卡托转换。

我们用 Three.js 画线是通过指定一系列顶点构成 Geometry,而画多边形是通过绘制一个形状,然后用 ExtrudeGeometry(挤压几何体) 拉伸成三维。墨卡托转换直接使用了 d3 的内置函数。旅行的效果是通过一帧帧的移动相机位置来实现的。

熟悉了 geojson 和墨卡托转换,就算是入门地理相关的可视化了。

你是否也想做一些和地理相关的可视化或者交互呢?不妨来尝试下吧。

 

责任编辑:武晓燕 来源: 神光的编程秘籍
相关推荐

2021-11-27 10:42:01

Three.js3D可视化AudioContex

2021-12-07 13:44:43

Three.js3D可视化3D 游戏

2019-11-29 09:30:37

Three.js3D前端

2017-05-08 11:41:37

WebGLThree.js

2021-11-22 06:14:45

Three.js3D 渲染花瓣雨

2020-03-11 14:39:26

数据可视化地图可视化地理信息

2021-04-23 16:40:49

Three.js前端代码

2022-07-15 13:09:33

Three.js前端

2022-01-16 19:23:25

Three.js粒子动画群星送福

2021-12-03 07:27:30

全景浏览Three.js

2022-08-26 09:15:58

Python可视化plotly

2009-04-21 14:26:41

可视化监控IT管理摩卡

2017-10-14 13:54:26

数据可视化数据信息可视化

2022-03-07 09:20:00

JavaScripThree.jsNFT

2022-07-08 10:39:09

Three.js元宇宙VR

2021-04-21 12:04:47

JS引擎流程

2017-11-27 11:59:40

Node.JSChrome调试程序

2015-08-20 10:06:36

可视化

2023-09-01 09:30:22

Three.js3D 图形库

2022-06-06 21:46:32

Kubernetes网络
点赞
收藏

51CTO技术栈公众号