带你入门three.js——从0到1实现一个3d可视化地图(二)

简介: 可视化地图——three.js实现场景的搭建我先不管地图不地图的,地图的这些形状肯定是放置到场景中的。跟着我的脚步一步一步去搭建一个场景。场景的搭建就照相机,渲染器。我用一个map类来表示代码如下:class chinaMap { constructor() { this.init() } init() { // 第一步新建一个场景 this.scene = new THREE.Scene() this.setCamera() this.setRenderer() } // 新建透视相机

可视化地图——three.js实现


场景的搭建


我先不管地图不地图的,地图的这些形状肯定是放置到场景中的。跟着我的脚步一步一步去搭建一个场景。场景的搭建就照相机,渲染器。我用一个map类来表示代码如下:


class chinaMap {
    constructor() {
      this.init()
    }
    init() {
      // 第一步新建一个场景
      this.scene = new THREE.Scene()
      this.setCamera()
      this.setRenderer()
    }
    // 新建透视相机
    setCamera() {
      // 第二参数就是 长度和宽度比 默认采用浏览器  返回以像素为单位的窗口的内部宽度和高度
      this.camera = new THREE.PerspectiveCamera(
        75,
        window.innerWidth / window.innerHeight,
        0.1,
        1000
      )
    }
    // 设置渲染器
    setRenderer() {
      this.renderer = new THREE.WebGLRenderer()
      // 设置画布的大小
      this.renderer.setSize(window.innerWidth, window.innerHeight)
      //这里 其实就是canvas 画布  renderer.domElement
      document.body.appendChild(this.renderer.domElement)
    }
    // 设置环境光
    setLight() {
      this.ambientLight = new THREE.AmbientLight(0xffffff) // 环境光
      this.scene.add(ambientLight)
    }
  }


上面我做了一一的解释,现在场景有了,灯光也有了, 我们看下样子。

image.png

image.gifimage-20210703140701037.png


对场景黑乎乎的什么都没有, 接下来我们我随便随便加一个长方体并且调用renderer的render方法。代码如下:


init() {
  //第一步新建一个场景
  this.scene = new THREE.Scene()
  this.setCamera()
  this.setRenderer()
  const geometry = new THREE.BoxGeometry()
  const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
  const cube = new THREE.Mesh(geometry, material)
  this.scene.add(cube)
  this.render()
}
//render 方法 
render() {
  this.renderer.render(this.scene, this.camera)
}


按照上面👆去做你会页面还是明明都已经加了,为什么呢?


「默认情况下,当我们调用scene.add()的时候,物体将会被添加到(0,0,0)坐标。但将使得摄像机和立方体彼此在一起。为了防止这种情况的发生,我们只需要将摄像机稍微向外移动一些即可」


所以只要将照相机的位置z轴属性调整一下就可以到图片了


// 新建透视相机
  setCamera() {
    // 第二参数就是 长度和宽度比 默认采用浏览器  返回以像素为单位的窗口的内部宽度和高度
    this.camera = new THREE.PerspectiveCamera(
      75,
      window.innerWidth / window.innerHeight,
      0.1,
      1000
    )
    this.camera.position.z = 5
  }


图片如下:


image.png

image-20210703142305435.png


这时候有同学就会问,嗯搞半天不和canvas 2d 一样嘛,有什么区别?看不出立体的感觉?OK 接下来我就让这个立方体动起来。其实就是不停的去调用 我们render 函数。我们用reqestanimationframe。尽量还是不要用setInterval,有一个很简单的优化。


「requestAnimationFrame」有很多的优点。最重要的一点或许就是当用户切换到其它的标签页时,它会暂停,因此不会浪费用户宝贵的处理器资源,也不会损耗电池的使用寿命。


我这里做的让立方体的x,y 不断的+0.1。先看下代码:


render() {
  this.renderer.render(this.scene, this.camera)
}
animate() {
  requestAnimationFrame(this.animate.bind(this))
  this.cube.rotation.x += 0.01
  this.cube.rotation.y += 0.01
  this.render()
}


效果图如下:


640 (8).gif

立方体的旋转.gif


是不是有那个那个感觉了, 我是以最简单的立方体的旋转,带大家从头入门下three.js。如果看到这里觉得这里,觉得对你有帮助的话,希望你能给我点个赞👍哦,感谢各位老铁了!下面正式地图需求分析。


地图数据的获得


其实最重要的是获取地图数据, 大家可以了解下openStreetMap


这个是一个可供自由编辑的世界地图。OpenStreetMap允许你查看,编辑或者使用世界各地的地理数据来帮助你。


这里我自己把中国地图的数据json拷贝下来了,代码如下:


// 加载地图数据
loadMapData() {
  const loader = new THREE.FileLoader()
  loader.load('../json/china.json', (data) => {
    const jsondata = JSON.parse(JSON.stringify(data))
  })
}


我给大家先看下json 数据的格式


![image-20210703154646470](/Users/wangzhengfei/Library/Application

Support/typora-user-images/image-20210703154646470.png)


其实主要的是下面有个经纬度坐标, 其实这个才是我关心的,有了点才能生成线,最后才能生成平面。这里涉及到一个知识点, 「墨卡托投影转换」。 墨卡托投影转换可以把我们经纬度坐标转换成我们对应平面的2d坐标。大家对这个推导过程的感性的可以看下这篇文章:传送门

这里我直接用可视化框架——「d3」 它里面有自带的墨卡托投影转换。

// 墨卡托投影转换
  const projection = d3
    .geoMercator()
    .center([104.0, 37.5])
    .scale(80)
    .translate([0, 0])

由于中国有很多省,每个省都对应一个Object3d。


Object3d是three.js 所有的基类, 提供了一系列的属性和方法来对三维空间中的物体进行操纵。可以通过.add( object )方法来将对象进行组合,该方法将对象添加为子对象


我这里的整个中国是一个大的Object3d,每一个省是一个Object3d,省是挂在中国下的。然后中国这个Map挂在scene这个Object3d下。很明显,在three.js 是一个很典型的树形数据结构,我画了张图给大家看下。


image.pngimage-20210704115145494.png


Scence场景下挂了很多东西, 其中有一个就是Map, 整个地图, 然后每个省份, 每个省份又是由Mesh和lLine 组成的。


我们看下代码:

generateGeometry(jsondata) {
          // 初始化一个地图对象
          this.map = new THREE.Object3D()
          // 墨卡托投影转换
          const projection = d3
            .geoMercator()
            .center([104.0, 37.5])
            .scale(80)
            .translate([0, 0])
          jsondata.features.forEach((elem) => {
            // 定一个省份3D对象
            const province = new THREE.Object3D()
            this.map.add(province)
          })
          this.scene.add(this.map)
        }


看到这里我想你可能没有什么问题,我们整体框架定下来了,接下来我们进入核心环节


生成地图几何体


这里用到了 Three.shape() 和  THREE.ExtrudeGeometry()  为什么会用到这个呢? 我给大家解释下, 「首先每一个省份轮廓组成的下标是一个 2d坐标,但是我们要生成立方体,shape()  可以定义一个二维形状平面。它可以和ExtrudeGeometry一起使用,获取点,或者获取三角面。」


代码如下:


// 每个的 坐标 数组
    const coordinates = elem.geometry.coordinates
    // 循环坐标数组
    coordinates.forEach((multiPolygon) => {
      multiPolygon.forEach((polygon) => {
        const shape = new THREE.Shape()
        const lineMaterial = new THREE.LineBasicMaterial({
          color: 'white',
        })
        const lineGeometry = new THREE.Geometry()
        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)
          lineGeometry.vertices.push(new THREE.Vector3(x, -y, 4.01))
        }
        const extrudeSettings = {
          depth: 10,
          bevelEnabled: false,
        }
        const geometry = new THREE.ExtrudeGeometry(
          shape,
          extrudeSettings
        )
        const material = new THREE.MeshBasicMaterial({
          color: '#2defff',
          transparent: true,
          opacity: 0.6,
        })
        const material1 = new THREE.MeshBasicMaterial({
          color: '#3480C4',
          transparent: true,
          opacity: 0.5,
        })
        const mesh = new THREE.Mesh(geometry, [material, material1])
        const line = new THREE.Line(lineGeometry, lineMaterial)
        province.add(mesh)
        province.add(line)
      })
    })


遍历第一个点的的和canvas2d画图其实是一模一样的, 移动起点, 然后后面在划线, 画出轮廓。然后我们在这里可以设置拉伸的深度, 然后接下来就是设置材质了。


lineGeometry 其实 对应的是轮廓的边线。我们看下图片吧:


image.png


image-20210704142519856.png


相机辅助视图


为了方便调相机位置, 我增加了辅助视图, cameraHelper。 然后你回看下屏幕会出现一个十字架,然后我们就可以不断地调整相机的位置,让我们地地图处于画面的中央:

addHelper() {
  const helper = new THREE.CameraHelper(this.camera)
  this.scene.add(helper)
}

经过辅助的视图地不断调整:

image.png

哈哈哈哈,是不是有那个味道了。到这里我们的中国地图已经在画布的中央了就已经实现了。


增加交互控制器



现在地图是已经生成了,但是用户交互感比较差,这里我们引入three的OrbitControls  可以用鼠标在画面随意转动,就可以看到立方体的每一个部分了。但是这个方法不在three 的包里面, 得单独引入一个文件代码如下:


setController() {
  this.controller = new THREE.OrbitControls(
    this.camera,
    document.getElementById('canvas')
  )
}


我们看下效果:


640 (9).gif

轨道控制器.gif


射线追踪



但是对于我自己而言还是不满意, 我怎么知道的我点击的是哪一个省份呢,OK这时候就要引入我们three中非常重要的一个类了,Raycaster 。


这个类用于进行raycasting(光线投射)。光线投射用于进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)。


我们可以对canvas监听的onmouseMove 事件,然后 我们就可以知道当前移动的鼠标是选择的哪一个mesh。但是在这之前,我们先对每一个province这个对象上增加一个属性来表示他是哪一个省份的。


// 将省份的属性 加进来
province.properties = elem.properties


Ok, 我们可以引入射线追踪了带入如下:


setRaycaster() {
  this.raycaster = new THREE.Raycaster()
  this.mouse = new THREE.Vector2()
  const onMouseMove = (event) => {
    // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
    this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
    this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
  }
  window.addEventListener('mousemove', onMouseMove, false)
}
animate() {
  requestAnimationFrame(this.animate.bind(this))
  // 通过摄像机和鼠标位置更新射线
  this.raycaster.setFromCamera(this.mouse, this.camera)
  this.render()
}


由于我们不停地在在画布移动, 所以需要不停的的射线位置。现在有了射线, 那我们需要场景的所有东西去比较了,rayCaster 也提供了方法代码如下:


const intersects = this.raycaster.intersectObjects(
  this.scene.children, // 场景的
  true  // 若为true,则同时也会检测所有物体的后代。否则将只会检测对象本身的相交部分
)


这个intersects得到的交叉很多,但是呢我们只选择其中一个,那就是物体材质个数有两个的, 因为我们上面就是用对mesh用两个材质


const mesh = new THREE.Mesh(geometry, [material, material1])


所以过滤代码如下


animate() {
  requestAnimationFrame(this.animate.bind(this))
  // 通过摄像机和鼠标位置更新射线
  this.raycaster.setFromCamera(this.mouse, this.camera)
  // 算出射线 与当场景相交的对象有那些
  const intersects = this.raycaster.intersectObjects(
    this.scene.children,
    true
  )
  const find = intersects.find(
    (item) => item.object.material && item.object.material.length === 2
  )
  this.render()
}


我怎么知道我到底找到没,我们对找到的mesh将它的表面变成灰色,但是这样会导致一个问题,我们鼠标再一次移动的时候要把上一次的材质给他恢复过来。


代码如下:


animate() {
    requestAnimationFrame(this.animate.bind(this))
    // 通过摄像机和鼠标位置更新射线
    this.raycaster.setFromCamera(this.mouse, this.camera)
    // 算出射线 与当场景相交的对象有那些
    const intersects = this.raycaster.intersectObjects(
      this.scene.children,
      true
    )
    // 恢复上一次清空的
    if (this.lastPick) {
      this.lastPick.object.material[0].color.set('#2defff')
      this.lastPick.object.material[1].color.set('#3480C4')
    }
    this.lastPick = null
    this.lastPick = intersects.find(
      (item) => item.object.material && item.object.material.length === 2
    )
    if (this.lastPick) {
      this.lastPick.object.material[0].color.set(0xff0000)
      this.lastPick.object.material[1].color.set(0xff0000)
    }
    this.render()
  }


看下效果图:


640 (10).gif

鼠标pick.gif


增加tooltip


为了让交互更加完美,找到了同时在鼠标右下方显示个tooltip,那这个肯定是一个div默认是影藏的,然后根据鼠标的移动移动相应的位置。


第一步新建div


<div id="tooltip"></div>


第二步设置样式 默认是影藏的


#tooltip {
  position: absolute;
  z-index: 2;
  background: white;
  padding: 10px;
  border-radius: 2px;
  visibility: hidden;
}


第三步更改div的位置:


setRaycaster() {
    this.raycaster = new THREE.Raycaster()
    this.mouse = new THREE.Vector2()
    this.tooltip = document.getElementById('tooltip')
    const onMouseMove = (event) => {
      this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
      this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
      // 更改div位置
      this.tooltip.style.left = event.clientX + 2 + 'px'
      this.tooltip.style.top = event.clientY + 2 + 'px'
    }
    window.addEventListener('mousemove', onMouseMove, false)
  }


最后一步设置tooltip的名字:


showTip() {
    // 显示省份的信息
    if (this.lastPick) {
      const properties = this.lastPick.object.parent.properties
      this.tooltip.textContent = properties.name
      this.tooltip.style.visibility = 'visible'
    } else {
      this.tooltip.style.visibility = 'hidden'
    }
  }


到这里,整个3d可视化地球项目已经完成了, 我们一起看下效果吧。


640 (11).gif


最终效果.gif

相关文章
|
2月前
|
前端开发 机器人 API
前端大模型入门(一):用 js+langchain 构建基于 LLM 的应用
本文介绍了大语言模型(LLM)的HTTP API流式调用机制及其在前端的实现方法。通过流式调用,服务器可以逐步发送生成的文本内容,前端则实时处理并展示这些数据块,从而提升用户体验和实时性。文章详细讲解了如何使用`fetch`发起流式请求、处理响应流数据、逐步更新界面、处理中断和错误,以及优化用户交互。流式调用特别适用于聊天机器人、搜索建议等应用场景,能够显著减少用户的等待时间,增强交互性。
339 2
|
2月前
|
编解码 数据可视化 前端开发
如何使用 D3.js 创建一个交互式的地图可视化?
如何使用 D3.js 创建一个交互式的地图可视化?
|
21天前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
|
21天前
|
移动开发 前端开发 JavaScript
前端实训,刚入门,我用原生技术(H5、C3、JS、JQ)手写【网易游戏】页面特效
于辰在大学期间带领团队参考网易游戏官网的部分游戏页面,开发了一系列前端实训作品。项目包括首页、2021校园招聘页面和明日之后游戏页面,涉及多种特效实现,如动态图片切换和人物聚合效果。作品源码已上传至CSDN,视频效果可在CSDN预览。
29 0
前端实训,刚入门,我用原生技术(H5、C3、JS、JQ)手写【网易游戏】页面特效
|
29天前
|
监控 前端开发 JavaScript
React 静态网站生成工具 Next.js 入门指南
【10月更文挑战第20天】Next.js 是一个基于 React 的服务器端渲染框架,由 Vercel 开发。本文从基础概念出发,逐步探讨 Next.js 的常见问题、易错点及解决方法,并通过具体代码示例进行说明,帮助开发者快速构建高性能的 Web 应用。
70 10
|
27天前
|
数据采集 存储 JavaScript
如何使用Puppeteer和Node.js爬取大学招生数据:入门指南
本文介绍了如何使用Puppeteer和Node.js爬取大学招生数据,并通过代理IP提升爬取的稳定性和效率。Puppeteer作为一个强大的Node.js库,能够模拟真实浏览器访问,支持JavaScript渲染,适合复杂的爬取任务。文章详细讲解了安装Puppeteer、配置代理IP、实现爬虫代码的步骤,并提供了代码示例。此外,还给出了注意事项和优化建议,帮助读者高效地抓取和分析招生数据。
如何使用Puppeteer和Node.js爬取大学招生数据:入门指南
|
3月前
|
JavaScript 前端开发 小程序
一小时入门Vue.js前端开发
本文是作者关于Vue.js前端开发的快速入门教程,包括结果展示、参考链接、注意事项以及常见问题的解决方法。文章提供了Vue.js的基础使用介绍,如何安装和使用cnpm,以及如何解决命令行中遇到的一些常见问题。
一小时入门Vue.js前端开发
|
2月前
|
存储 JavaScript 前端开发
前端开发:Vue.js入门与实战
【10月更文挑战第9天】前端开发:Vue.js入门与实战
|
2月前
|
自然语言处理 JavaScript 前端开发
JavaScript高级——ES6基础入门
JavaScript高级——ES6基础入门
26 1
|
2月前
|
机器学习/深度学习 自然语言处理 前端开发
前端大模型入门:Transformer.js 和 Xenova-引领浏览器端的机器学习变革
除了调用API接口使用Transformer技术,你是否想过在浏览器中运行大模型?Xenova团队推出的Transformer.js,基于JavaScript,让开发者能在浏览器中本地加载和执行预训练模型,无需依赖服务器。该库利用WebAssembly和WebGPU技术,大幅提升性能,尤其适合隐私保护、离线应用和低延迟交互场景。无论是NLP任务还是实时文本生成,Transformer.js都提供了强大支持,成为构建浏览器AI应用的核心工具。
495 1