自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

北京到上海,Three.js 旅行軌跡的可視化

開發(fā) 前端
最近從北京搬到了上海,開始了一段新的生活,算是人生中一個(gè)比較大的事件,于是特地用 Three.js 做了下可視化。

[[440408]]

本文轉(zhuǎn)載自微信公眾號(hào)「神光的編程秘籍」,作者神說要有光zxg。轉(zhuǎn)載本文請(qǐng)聯(lián)系神光的編程秘籍公眾號(hào)。

最近從北京搬到了上海,開始了一段新的生活,算是人生中一個(gè)比較大的事件,于是特地用 Three.js 做了下可視化。

在這個(gè)地理信息相關(guān)的可視化的案例中,我們能學(xué)到地圖怎么畫、經(jīng)緯度如何轉(zhuǎn)成坐標(biāo)值,這些是地理可視化的通用技術(shù)。

那我們就開始吧。

思路分析

Three.js 畫立方體、畫圓柱、畫不規(guī)則圖形我們都畫過,但是如何畫一個(gè)地圖呢?

其實(shí)地圖也是由線、由多邊形構(gòu)成的,有了數(shù)據(jù)我們就能畫出來,缺少的只是數(shù)據(jù)。

地圖信息的描述是一個(gè)通用需求,所以有相應(yīng)的國際標(biāo)準(zhǔn),就是 GeoJson,它是通過點(diǎn)、線、多邊形來描述地理信息的。

通過指定點(diǎn)、線、多邊形的類型、然后指定幾個(gè)坐標(biāo)位置,就可以描述出相應(yīng)的形狀。

geojson 的數(shù)據(jù)可以通過 geojson.io 這個(gè)網(wǎng)站做下預(yù)覽。

比如中國地圖的 geojson:

有了這個(gè) json,只要用 Three.js 畫出來就行,通過線和多邊形兩種方式。

但是還有一個(gè)問題,geojson 中記錄的是經(jīng)緯度信息,應(yīng)該如何轉(zhuǎn)成二維坐標(biāo)來畫呢?

這就涉及到了墨卡托轉(zhuǎn)換,它就是做經(jīng)緯度轉(zhuǎn)二維坐標(biāo)的事情。

這個(gè)轉(zhuǎn)換也不用我們自己實(shí)現(xiàn),可以用 d3 內(nèi)置的墨卡托坐標(biāo)轉(zhuǎn)換函數(shù)來做。

這樣,我們就用 Three.js 根據(jù) geojson 來畫出地圖。

我們還要畫一條北京到上海的曲線,這個(gè)用貝塞爾曲線畫就行,知道兩個(gè)端點(diǎn)的坐標(biāo),控制點(diǎn)放在中間的位置。

那怎么知道兩個(gè)端點(diǎn),也就是上海和北京的坐標(biāo)呢?

這個(gè)可以用“百度坐標(biāo)拾取系統(tǒng)”這個(gè)工具,點(diǎn)擊地圖的某個(gè)位置,就可以直接拿到那個(gè)位置的經(jīng)緯度。然后我們做一次墨卡托轉(zhuǎn)換,就拿到坐標(biāo)了。

地圖畫出來了,旅行的曲線也畫出來了,接下來調(diào)整下相機(jī)位置,從北京慢慢移動(dòng)到上海就可以了。

思路理清了,我們來寫下代碼。

代碼實(shí)現(xiàn)

我們要引入 d3,然后使用 d3 的墨卡托轉(zhuǎn)換功能,

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

中間點(diǎn)的坐標(biāo)就是北京的經(jīng)緯度,就是我們通過“百度坐標(biāo)拾取工具”那里拿到的。

北京和上海的坐標(biāo)位置也可以把經(jīng)緯度做墨卡托轉(zhuǎn)換得到:

  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. }) 

然后根據(jù) json 的信息畫地圖。

遍歷 geojson 的數(shù)據(jù),把每個(gè)經(jīng)緯度通過墨卡托轉(zhuǎn)換變成坐標(biāo),然后分別用線和多邊形畫出來。

畫多邊形的時(shí)候遇到北京和上海用黃色,其他城市用藍(lán)色。

  1. function generateGeometry(jsondata) { 
  2.   const map = new THREE.Group(); 
  3.      
  4.   jsondata.features.forEach((elem) => { 
  5.     const province = new THREE.Group(); 
  6.  
  7.     // 經(jīng)緯度信息 
  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); 

然后分別實(shí)現(xiàn)畫輪廓線和畫多邊形:

輪廓線(Line)就是指定一系列頂點(diǎn)來構(gòu)成幾何體(Geometry),然后指定材質(zhì)(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); 

現(xiàn)在的效果是這樣的:

多邊形是 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); 

第一個(gè)點(diǎn)用 moveTo,后面的點(diǎn)用 lineTo,這樣連成一個(gè)多邊形,然后指定厚度為 0,指定側(cè)面不需要多出一塊斜面(bevel)。

這樣,我們就給每個(gè)省都填充上了顏色,北京和上海是黃色,其余省是藍(lán)色。

接下來,在北京和上海之間畫一條貝塞爾曲線:

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

貝塞爾曲線用 QuadraticBezierCurve3 來畫,控制點(diǎn)指定中間位置的點(diǎn)。

  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; 

這樣,地圖和旅行軌跡就都畫完了:

當(dāng)然,還有渲染器、相機(jī)、燈光的初始化代碼:

渲染器:

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

渲染器設(shè)置背景顏色為黑色,畫布大小為窗口大小。

燈光:

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

燈光用環(huán)境光,也就是每個(gè)方向的明暗都一樣。

相機(jī):

  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); 

相機(jī)用透視相機(jī),特點(diǎn)是近大遠(yuǎn)小,需要指定看的角度,寬高比,和遠(yuǎn)近的范圍這樣四個(gè)參數(shù)。

位置設(shè)置在 0 0 10 的位置,在這個(gè)位置去觀察 0 0 0,就是北京上方的俯視圖(我們做墨卡托轉(zhuǎn)換的時(shí)候指定了北京為中心)。

修改了相機(jī)位置之后,看到的地圖大了許多:

接下來就是一幀幀的渲染,在每幀渲染的時(shí)候移動(dòng)下相機(jī)位置,這樣就是從北京到上海的一個(gè)移動(dòng)的效果:

  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> 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

總結(jié)

地圖形狀的表示是基于 geojson 的規(guī)范,它是由點(diǎn)、線、多邊形等信息構(gòu)成的。

用 Three.js 或者其他繪制方式來畫地圖只需要加載 geojson 的數(shù)據(jù),然后通過線和多邊型把每一部分畫出來。

畫之前還要把經(jīng)緯度轉(zhuǎn)成坐標(biāo),這需要用到墨卡托轉(zhuǎn)換。

我們用 Three.js 畫線是通過指定一系列頂點(diǎn)構(gòu)成 Geometry,而畫多邊形是通過繪制一個(gè)形狀,然后用 ExtrudeGeometry(擠壓幾何體) 拉伸成三維。墨卡托轉(zhuǎn)換直接使用了 d3 的內(nèi)置函數(shù)。旅行的效果是通過一幀幀的移動(dòng)相機(jī)位置來實(shí)現(xiàn)的。

熟悉了 geojson 和墨卡托轉(zhuǎn)換,就算是入門地理相關(guān)的可視化了。

你是否也想做一些和地理相關(guān)的可視化或者交互呢?不妨來嘗試下吧。

 

責(zé)任編輯:武曉燕 來源: 神光的編程秘籍
相關(guān)推薦

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 渲染花瓣雨

2024-07-18 06:58:36

2020-03-11 14:39:26

數(shù)據(jù)可視化地圖可視化地理信息

2021-04-23 16:40:49

Three.js前端代碼

2022-07-15 13:09:33

Three.js前端

2021-12-03 07:27:30

全景瀏覽Three.js

2022-01-16 19:23:25

Three.js粒子動(dòng)畫群星送福

2025-03-13 10:54:18

2022-03-07 09:20:00

JavaScripThree.jsNFT

2017-10-14 13:54:26

數(shù)據(jù)可視化數(shù)據(jù)信息可視化

2022-08-26 09:15:58

Python可視化plotly

2022-07-08 10:39:09

Three.js元宇宙VR

2009-04-21 14:26:41

可視化監(jiān)控IT管理摩卡

2021-04-21 12:04:47

JS引擎流程

2017-11-27 11:59:40

Node.JSChrome調(diào)試程序

2015-08-20 10:06:36

可視化
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)