人工智能

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

时间:2010-12-5 17:23:32  作者:域名   来源:人工智能  查看:  评论:0
内容摘要:本文转载自微信公众号「神光的编程秘籍」,作者神说要有光zxg。转载本文请联系神光的编程秘籍公众号。最近从北京搬到了上海,开始了一段新的生活,算是人生中一个比较大的事件,于是特地用 Three.js 做

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

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

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

那我们就开始吧。到上

思路分析

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

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

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

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

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

比如中国地图的 geojson:

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

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

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

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

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

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

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

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

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

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

代码实现

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

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

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

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

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

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

先加载 geojson:

const loader = new THREE.FileLoader(); loader.load(./data/china.json, (data) => {      const jsondata = JSON.parse(data);     generateGeometry(jsondata); }) 

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

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

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

function generateGeometry(jsondata) {    const map = new THREE.Group();   jsondata.features.forEach((elem) => {      const province = new THREE.Group();     // 经纬度信息     const coordinates = elem.geometry.coordinates;     coordinates.forEach((multiPolygon) => {        multiPolygon.forEach((polygon) => {          // 画轮廓线         const line = drawBoundary(polygon);         // 画多边形         const provinceColor = [北京市, 上海市].includes(elem.properties.name) ? yellow : blue;         const mesh = drawExtrudeMesh(polygon, provinceColor);         province.add(line);         province.add(mesh);       });     });     map.add(province);   })   scene.add(map); } 

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

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

function drawBoundary(polygon) {      const lineGeometry = new THREE.Geometry();     for (let i = 0; i < polygon.length; i++) {        const [x, y] = projection(polygon[i]);       lineGeometry.vertices.push(new THREE.Vector3(x, -y, 0));     }     const lineMaterial = new THREE.LineBasicMaterial({         color: yellow     });     return new THREE.Line(lineGeometry, lineMaterial); } 

现在的效果是这样的:

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

function drawExtrudeMesh(polygon, color) {      const shape = new THREE.Shape();     for (let i = 0; i < polygon.length; i++) {        const [x, y] = projection(polygon[i]);       if (i === 0) {          shape.moveTo(x, -y);       }       shape.lineTo(x, -y);     }     const geometry = new THREE.ExtrudeGeometry(shape, {        depth: 0,       bevelEnabled: false     });     const material = new THREE.MeshBasicMaterial({        color,       transparent: true,       opacity: 0.2,     })     return new THREE.Mesh(geometry, material); } 

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

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

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

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

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

function drawLine(pos1, pos2) {    const [x0, y0, z0] = [...pos1, 0];   const [x1, y1, z1] = [...pos2, 0];   const geomentry = new THREE.Geometry();   geomentry.vertices = new THREE.QuadraticBezierCurve3(       new THREE.Vector3(-x0, -y0, z0),       new THREE.Vector3(-(x0 + x1) / 2, -(y0 + y1) / 2, -10),       new THREE.Vector3(-x1, -y1, z1),   ).getPoints();   const material = new THREE.LineBasicMaterial({ color: white});   const line = new THREE.Line(geomentry, material);   line.rotation.y = Math.PI;   return line; } 

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

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

渲染器:

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

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

灯光:

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

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

相机:

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

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

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

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

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

function render() {      if(camera.position.x < shanghaiPosition[0]) {          camera.position.x += 0.1;     }       if(camera.position.y > -shanghaiPosition[1]) {          camera.position.y -= 0.2;     }     renderer.render(scene, camera);     requestAnimationFrame(render); } 

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

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

也在这里贴一份:

<!DOCTYPE html> <html lang="en">   <head>     <meta charset="UTF-8" />     <meta http-equiv="X-UA-Compatible" content="IE=edge" />     <meta name="viewport" content="width=device-width, initial-scale=1.0" />     <title>map-travel</title>     <style>       html body {          height: 100%;         width: 100%;         margin: 0;         padding: 0;         overflow: hidden;       }     </style>   </head>   <body>     <script src="./js/three.js"></script>     <script src="./js/d3.js"></script>     <script>       const scene = new THREE.Scene();       const renderer = new THREE.WebGLRenderer();       renderer.setClearColor(0x000000);       renderer.setSize(window.innerWidth, window.innerHeight);       document.body.appendChild(renderer.domElement);       const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);       camera.position.set(0, 0, 10);       camera.lookAt(scene.position);       let ambientLight = new THREE.AmbientLight(0xffffff);       scene.add(ambientLight);       function create() {            const loader = new THREE.FileLoader();           loader.load(./data/china.json, (data) => {              const jsondata = JSON.parse(data);             generateGeometry(jsondata);           })       }       const projection = d3.geoMercator()             .center([116.412318,39.909843])             .translate([0, 0]);       let beijingPosition= projection([116.412318,39.909843]);       let shanghaiPosition = projection([121.495721,31.236797]);       function drawBoundary(polygon) {          const lineGeometry = new THREE.Geometry();         for (let i = 0; i < polygon.length; i++) {            const [x, y] = projection(polygon[i]);           lineGeometry.vertices.push(new THREE.Vector3(x, -y, 0));         }         const lineMaterial = new THREE.LineBasicMaterial({             color: yellow         });         return new THREE.Line(lineGeometry, lineMaterial);       }       function drawExtrudeMesh(polygon, color) {          const shape = new THREE.Shape();         for (let i = 0; i < polygon.length; i++) {            const [x, y] = projection(polygon[i]);           if (i === 0) {              shape.moveTo(x, -y);           }           shape.lineTo(x, -y);         }         const geometry = new THREE.ExtrudeGeometry(shape, {            depth: 0,           bevelEnabled: false         });         const material = new THREE.MeshBasicMaterial({            color,           transparent: true,           opacity: 0.2,         })         return new THREE.Mesh(geometry, material);       }       function generateGeometry(jsondata) {            const map = new THREE.Group();           jsondata.features.forEach((elem) => {              const province = new THREE.Group();             const coordinates = elem.geometry.coordinates;             coordinates.forEach((multiPolygon) => {                multiPolygon.forEach((polygon) => {                  const line = drawBoundary(polygon);                 const provinceColor = [北京市, 上海市].includes(elem.properties.name) ? yellow : blue;                 const mesh = drawExtrudeMesh(polygon, provinceColor);                 province.add(line);                 province.add(mesh);               });             });             map.add(province);           })           scene.add(map);           const line = drawLine(beijingPosition, shanghaiPosition);           scene.add(line);       }       function render() {          if(camera.position.x < shanghaiPosition[0]) {              camera.position.x += 0.1;         }           if(camera.position.y > -shanghaiPosition[1]) {              camera.position.y -= 0.2;         }         renderer.render(scene, camera);         requestAnimationFrame(render);       }       function drawLine(pos1, pos2) {            const [x0, y0, z0] = [...pos1, 0];           const [x1, y1, z1] = [...pos2, 0];           const geomentry = new THREE.Geometry();           geomentry.vertices = new THREE.QuadraticBezierCurve3(               new THREE.Vector3(-x0, -y0, z0),               new THREE.Vector3(-(x0 + x1) / 2, -(y0 + y1) / 2, -10),               new THREE.Vector3(-x1, -y1, z1),           ).getPoints();           const material = new THREE.LineBasicMaterial({ color: white});           const line = new THREE.Line(geomentry, material);           line.rotation.y = Math.PI;           return line;       }       create();       render();     </script>   </body> </html> 

总结

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

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

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

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

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

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

copyright © 2025 powered by 益强资讯全景  滇ICP备2023006006号-31sitemap