【前端】three.js指南

饭宝  论坛元老 | 2024-12-1 11:15:38 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 1037|帖子 1037|积分 3111

概述

Three.js是基于原生WebGL封装运行的三维引擎,在全部WebGL引擎中,Three.js是国内文资料最多、使用最广泛的三维引擎。
既然Threejs是一款WebGL三维引擎,那么它可以用来做什么想必你肯定很关心。所以接下来内容会展示大量基于Threejs引擎或Threejs雷同引擎开发的Web3D应用,以便大家相识。
three.js-master目次结构


  1. three.js-master
  2. └───build——src目录下各个代码模块打包后的结果
  3.     │───three.js——开发的时候.html文件中要引入的threejs引擎库,和引入jquery一样,可以辅助浏览器调试
  4.     │───three.min.js——three.js压缩后的结构文件体积更小,可以部署项目的时候在.html中引入。
  5.     │
  6. └───docs——Three.js API文档文件
  7.     │───index.html——打开该文件可以实现离线查看threejs API文档
  8.     │
  9. └───editor——Three.js的可视化编辑器,可以编辑3D场景
  10.     │───index.html——打开应用程序
  11.     │
  12. └───docs——Three.js API文档文件
  13.     │───index.html——打开该文件可以实现离线查看threejs API文档
  14.     │
  15. └───examples——里面有大量的threejs案例,平时可以通过代码编辑全局查找某个API、方法或属性来定位到一个案例
  16.     │
  17. └───src——Three.js引擎的各个模块,可以通过阅读源码深度理解threejs引擎
  18.     │───index.html——打开该文件可以实现离线查看threejs API文档
  19.     │
  20. └───utils——一些辅助工具
  21.     │───\utils\exporters\blender——blender导出threejs文件的插件
复制代码
html文件引入three.js引擎
在.html文件中引入three.js就像引入别的.js文件一样直接引入即可。
  1. <!--相对地址加载-->
  2. <script src="./three.js"></script>
复制代码
我已经把three.js文件上传到了我的博客服务器, 可以通过下面的URL地点去加载
  1. <!--http绝对地址远程加载-->
  2. <script src="http://www.yanhuangxueyuan.com/3D/example/three.js"></script>
  3. <!-- 压缩版本 -->
  4. <script src="http://www.yanhuangxueyuan.com/3D/example/three.min.js"></script>
复制代码
.html文件中引入threejs文件之后,可以通过浏览器控制台F12验证是否乐成引入,在.html文件引入three.js后可以通过THREE访问全部的API。
  1. // 如果返回的不是未定义,说明threejs成功引入
  2. console.log('打印场景API',THREE.Scene);
复制代码
Threejs 的基本要素

场景

「场景」:是一个三维空间,全部物品的容器,可以把场景想象成一个空房间,接下来我们会往房间里放要呈现的物体、相机、光源等。
用代码表示就是如下:
  1. const scene = new THREE.Scene();
复制代码
你就把他想象成一个房间,然后你可以往内里去添加一些物体,加一个正方体哈,加矩形,什么都可以。其实three.js 整个之间的关系是一个 「树形结构」。
相机

「相机」:Threejs必须要往场景中添加一个相机,相机用来确定位置、方向、角度,相机看到的内容就是我们最总在屏幕上看到的内容。在程序运行过程中,可以调整相机的位置、方向和角度。
three.js 中的相机分为两种一种是正交相机 和透视相机 ,接下来我给大家逐一先容,但是理解照相机的环境下,你要先理解一个概念——视椎体
透视相机

视锥体是摄像机可见的空间,看上去像截掉顶部的金字塔。视锥体由6个裁剪面围成,构成视锥体的4个侧面称为上左下右面,分别对应屏幕的四个界限。为了防止物体离摄像机过近,设置近切面,同时为了防止物体离摄像机太远而不可见,设置远切面。

oc 就是照相机的位置, 近平面、和远平面图中已经标注。从图中可以看出,棱台构成的6个面之内的东西,是可以被看到的。 影响透视照相机的大小因素:


  • 摄像机视锥体垂直视野角度 也就是图中的「a」
  • 摄像机视锥体近端面 也就是图中的 「near plane」
  • 摄像机视锥体远端面 也就是图中的「far plane」
  • 摄像机视锥体长宽比 「表示输出图像的宽和高之比」
对应的three 中的照相机:
  1. const camera = new THREE.PerspectiveCamera( 45, width / height, 1, 1000 );
复制代码
​ 透视相机最大的特点:就是符合我们人眼观察事物的特点, 近大远小。
近大远小的背后的实现原理就是相机会有一个投影矩阵: 投影矩阵的做的事情很简单,就是把视椎体转换成一个正方体。 所以远截面的点就要缩小, 近距离的反而放大。

正交相机

正交相机的特点就是视椎体的是一个立方体
在这种投影模式下,无论物体距离相机距离远或者近,在终极渲染的图片中物体的大小都保持不变。
这对于渲染2D场景或者UI元素是非常有用的。如图:

three中代码如下:
  1. const camera = new THREE.OrthographicCamera( width / - 2, width / 2, height / 2, height / - 2, 1, 1000 );
复制代码
网格

在计算机的世界里,一条弧线是由有限个点构成的有限条线段连接得到的。当线段数量越多,长度就越短,当到达你无法察觉这是线段时,一条平滑的弧线就出现了。 计算机的三维模型也是雷同的。只不过线段变成了平面,普遍用三角形构成的网格来形貌。我们把这种模型称之为 Mesh 模型。
一条弧线由多条线段得到,线段的数量越多,越靠近弧线。 不懂的小同伴,可以看下我的这篇文章:口试官问我会canvas? 我可以绘制一个烟花 动画内里「贝塞尔曲线可以是用一段段小线段去拟合起来的」 。
three.js 背后全部的图形在举行渲染之前, 都会举行三角化, 然后交给webgl 去渲染。
Threejs提供了一些常见的几何外形,有三维的也有二维的,三维的比如长方体、球体等,二维的比如长方形圆形等,假如默认提供的外形不能满足需求,也可以自定义通过定义顶点和顶点之间的连线绘制自定义几何外形,更复杂的模型还可以用建模软件建模后导入。
2d


3d


有了外形,可能渲染出来的图形没有美丽的样子,这时间材质就出来了。 构成的mesh其实是有两个部分:
材质(Material)+几何体(Geometry)就是一个 mesh,Threejs提供了集中比较有代表性的材质,常用的用漫反射、镜面反射两种材质,还可以引入外部图片,贴到物体外貌,成为纹理贴图。大家有爱好可以自己去试一下。如图:

灯光

假如没有光,摄像机看不到任何东西,因此必要往场景添加光源,为了跟真实世界更加靠近,Threejs支持模拟不同光源,显现不同光照效果,有点光源、平行光、聚光灯、环境光等。
AmbientLight(环境光)

环境光会均匀的照亮场景中的全部物体,环境光不能用来投射阴影,因为它没有方向。
  1. const light = new THREE.AmbientLight( 0x404040 ); // soft white light
复制代码
平行光(DirectionalLight)

平行光是沿着特定方向发射的光。这种光的体现像是无限远,从它发出的光线都是平行的。常常用平行光来模拟太阳光 的效果; 太阳足够远,因此我们可以认为太阳的位置是无限远,所以我们认为从太阳发出的光线也都是平行的。
  1. const directionalLight = new THREE.DirectionalLight( 0xffffff, 0.5 );
复制代码
点光源(PointLight)

从一个点向各个方向发射的光源。一个常见的例子是模拟一个灯泡发出的光。
  1. const light = new THREE.PointLight( 0xff0000, 1, 100 );
复制代码
聚光灯(SpotLight)

光线从一个点沿一个方向射出,随着光线照射的变远,光线圆锥体的尺寸也渐渐增大。
  1. const spotLight = new THREE.SpotLight( 0xffffff );
复制代码
还有一些其他的灯光,感爱好的小同伴可以自行去three.js 官网检察。
渲染器

渲染器就是去渲染你场景中灯光、相机、网格哇。
  1. let renderer = new THREE.WebGLRenderer({
  2.     antialias: true, // true/false表示是否开启反锯齿
  3.     alpha: true, // true/false 表示是否可以设置背景色透明
  4.     precision: 'highp', // highp/mediump/lowp 表示着色精度选择
  5.     premultipliedAlpha: false, // true/false 表示是否可以设置像素深度(用来度量图像的分率)
  6.     preserveDrawingBuffer: true, // true/false 表示是否保存绘图缓冲
  7.     maxLights: 3, // 最大灯光数
  8.     stencil: false // false/true 表示是否使用模板字体或图案
复制代码
three.js大体的一些要素我都先容过了,接下面就进入在正题了,three.js 怎样实现一个可视化地图呢?
Threejs 的实现

场景的搭建

我先不管地图不地图的,地图的这些外形肯定是放置加入景中的。跟着我的脚步一步一步去搭建一个场景。场景的搭建就照相机,渲染器。我用一个map类来表示代码如下:
  1. class chinaMap {
  2.     constructor() {
  3.       this.init()
  4.     }
  5.     init() {
  6.       // 第一步新建一个场景
  7.       this.scene = new THREE.Scene()
  8.       this.setCamera()
  9.       this.setRenderer()
  10.     }
  11.     // 新建透视相机
  12.     setCamera() {
  13.       // 第二参数就是 长度和宽度比 默认采用浏览器  返回以像素为单位的窗口的内部宽度和高度
  14.       this.camera = new THREE.PerspectiveCamera(
  15.         75,
  16.         window.innerWidth / window.innerHeight,
  17.         0.1,
  18.         1000
  19.       )
  20.     }
  21.     // 设置渲染器
  22.     setRenderer() {
  23.       this.renderer = new THREE.WebGLRenderer()
  24.       // 设置画布的大小
  25.       this.renderer.setSize(window.innerWidth, window.innerHeight)
  26.       //这里 其实就是canvas 画布  renderer.domElement
  27.       document.body.appendChild(this.renderer.domElement)
  28.     }
  29.    
  30.     // 设置环境光
  31.     setLight() {
  32.       this.ambientLight = new THREE.AmbientLight(0xffffff) // 环境光
  33.       this.scene.add(ambientLight)
  34.     }
  35.   }
复制代码
上面我做了逐一的表明,现在场景有了,灯光也有了, 我们看下样子。

对场景黑乎乎的什么都没有, 接下来我们我随便随便加一个长方体而且调用renderer的render方法。代码如下:
  1. init() {
  2.   //第一步新建一个场景
  3.   this.scene = new THREE.Scene()
  4.   this.setCamera()
  5.   this.setRenderer()
  6.   const geometry = new THREE.BoxGeometry()
  7.   const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 })
  8.   const cube = new THREE.Mesh(geometry, material)
  9.   this.scene.add(cube)
  10.   this.render()
  11. }
  12. //render 方法
  13. render() {
  14.   this.renderer.render(this.scene, this.camera)
  15. }
复制代码
按照上面 去做你会页面还是明明都已经加了,为什么呢?
「默认环境下,当我们调用scene.add()的时间,物体将会被添加到(0,0,0)坐标。但将使得摄像机和立方体彼此在一起。为了防止这种环境的发生,我们只必要将摄像机轻微向外移动一些即可」
所以只要将照相机的位置z轴属性调整一下就可以到图片了
  1. // 新建透视相机
  2.   setCamera() {
  3.     // 第二参数就是 长度和宽度比 默认采用浏览器  返回以像素为单位的窗口的内部宽度和高度
  4.     this.camera = new THREE.PerspectiveCamera(
  5.       75,
  6.       window.innerWidth / window.innerHeight,
  7.       0.1,
  8.       1000
  9.     )
  10.     this.camera.position.z = 5
  11.   }
复制代码
图片如下:

这时间有同学就会问,嗯搞半天不和canvas 2d 一样嘛,有什么区别? 看不出立体的感觉?
OK 接下来我就让这个立方体动起来。 其实就是不绝的去调用 我们render 函数。 我们用reqestanimationframe。只管还是不要用setInterval,有一个很简单的优化。
「requestAnimationFrame」有很多的优点。最重要的一点大概就是当用户切换到别的的标签页时,它会暂停,因此不会浪费用户宝贵的处理器资源,也不会损耗电池的使用寿命。
我这里做的让立方体的x,y 不停的+0.1。 先看下代码:
  1. render() {
  2.   this.renderer.render(this.scene, this.camera)
  3. }
  4. animate() {
  5.   requestAnimationFrame(this.animate.bind(this))
  6.   this.cube.rotation.x += 0.01
  7.   this.cube.rotation.y += 0.01
  8.   this.render()
  9. }
复制代码
效果图如下:

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

其实最重要的是获取地图数据, 大家可以相识下openStreetMap
这个是一个可供自由编辑的世界地图。OpenStreetMap允许你检察,编辑或者使用世界各地的地理数据来资助你。
这里我自己把中国地图的数据json拷贝下来了,代码如下:
  1. // 加载地图数据
  2. loadMapData() {
  3.   const loader = new THREE.FileLoader()
  4.   loader.load('../json/china.json', (data) => {
  5.     const jsondata = JSON.parse(JSON.stringify(data))
  6.   })
  7. }
复制代码
其实重要的是下面有个经纬度坐标, 其实这个才是我关心的,有了点才能生成线,最后才能生成平面。 这里涉及到一个知识点, 「墨卡托投影转换」。 墨卡托投影转换可以把我们经纬度坐标转换成我们对应平面的2d坐标。
这里我直接用可视化框架——「d3」 它内里有自带的墨卡托投影转换。
  1. // 墨卡托投影转换
  2.   const projection = d3
  3.     .geoMercator()
  4.     .center([104.0, 37.5])
  5.     .scale(80)
  6.     .translate([0, 0])
复制代码
由于中国有很多省,每个省都对应一个Object3d。
Object3d是three.js 全部的基类, 提供了一系列的属性和方法来对三维空间中的物体举行利用。可以通过.add( object )方法来将对象举行组合,该方法将对象添加为子对象
我这里的整个中国是一个大的Object3d,每一个省是一个Object3d,省是挂在中国下的。 然后中国这个Map挂在scene这个Object3d下。 很明显,在three.js 是一个很典型的树形数据结构,我画了张图给大家看下。

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])
  1.   jsondata.features.forEach((elem) => {
  2.     // 定一个省份3D对象
  3.     const province = new THREE.Object3D()
  4.     this.map.add(province)
  5.   })
  6.   this.scene.add(this.map)
  7. }
复制代码
看到这里我想你可能没有什么题目,我们整体框架定下来了,接下来我们进入核心环节
生成地图几何体

这里用到了 Three.shape() 和 THREE.ExtrudeGeometry() 为什么会用到这个呢? 我给大家表明下, 「首先每一个省份轮廓构成的下标是一个 2d坐标,但是我们要生建立方体,shape() 可以定义一个二维外形平面。 它可以和ExtrudeGeometry一起使用,获取点,或者获取三角面。」
代码如下:
  1. // 每个的 坐标 数组
  2.     const coordinates = elem.geometry.coordinates
  3.     // 循环坐标数组
  4.     coordinates.forEach((multiPolygon) => {
  5.       multiPolygon.forEach((polygon) => {
  6.         const shape = new THREE.Shape()
  7.         const lineMaterial = new THREE.LineBasicMaterial({
  8.           color: 'white',
  9.         })
  10.         const lineGeometry = new THREE.Geometry()
  11.         for (let i = 0; i < polygon.length; i++) {
  12.           const [x, y] = projection(polygon[i])
  13.           if (i === 0) {
  14.             shape.moveTo(x, -y)
  15.           }
  16.           shape.lineTo(x, -y)
  17.           lineGeometry.vertices.push(new THREE.Vector3(x, -y, 4.01))
  18.         }
  19.         const extrudeSettings = {
  20.           depth: 10,
  21.           bevelEnabled: false,
  22.         }
  23.         const geometry = new THREE.ExtrudeGeometry(
  24.           shape,
  25.           extrudeSettings
  26.         )
  27.         const material = new THREE.MeshBasicMaterial({
  28.           color: '#2defff',
  29.           transparent: true,
  30.           opacity: 0.6,
  31.         })
  32.         const material1 = new THREE.MeshBasicMaterial({
  33.           color: '#3480C4',
  34.           transparent: true,
  35.           opacity: 0.5,
  36.         })
  37.         const mesh = new THREE.Mesh(geometry, [material, material1])
  38.         const line = new THREE.Line(lineGeometry, lineMaterial)
  39.         province.add(mesh)
  40.         province.add(line)
  41.       })
  42.     })
复制代码
遍历第一个点的的和canvas2d画图其实是千篇划一的, 移动起点, 然后背面在划线, 画出轮廓。然后我们在这里可以设置拉伸的深度, 然后接下来就是设置材质了。lineGeometry 其实 对应的是轮廓的边线。我们看下图片吧:

相机辅助视图

为了方便调相机位置, 我增长了辅助视图, cameraHelper。 然后你回看下屏幕会出现一个十字架,然后我们就可以不停地调整相机的位置,让我们地地图处于画面的中央:
  1. addHelper() {
  2.   const helper = new THREE.CameraHelper(this.camera)
  3.   this.scene.add(helper)
  4. }
复制代码
经过辅助的视图地不停调整:

增长交互控制器

现在地图是已经生成了,但是用户交互感比较差,这里我们引入three的OrbitControls 可以用鼠标在画面随意转动,就可以看到立方体的每一个部分了。但是这个方法不在three 的包内里, 得单独引入一个文件代码如下:
  1. setController() {
  2.   this.controller = new THREE.OrbitControls(
  3.     this.camera,
  4.     document.getElementById('canvas')
  5.   )
  6. }
复制代码
我们看下效果:

     three.js增长交互控制器效果
  
射线追踪

但是对于我自己而言还是不满足, 我怎么知道的我点击的是哪一个省份呢,OK这时间就要引入我们three中非常重要的一个类了,Raycaster 。
这个类用于举行raycasting(光线投射)。 光线投射用于举行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)。
我们可以对canvas监听的onmouseMove 事件,然后 我们就可以知道当前移动的鼠标是选择的哪一个mesh。但是在这之前,我们先对每一个province这个对象上增长一个属性来表示他是哪一个省份的。
// 将省份的属性 加进来
  1. province.properties = elem.properties
复制代码
Ok, 我们可以引入射线追踪了带入如下:
  1. setRaycaster() {
  2.   this.raycaster = new THREE.Raycaster()
  3.   this.mouse = new THREE.Vector2()
  4.   const onMouseMove = (event) => {
  5.     // 将鼠标位置归一化为设备坐标。x 和 y 方向的取值范围是 (-1 to +1)
  6.     this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
  7.     this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
  8.   }
  9.   window.addEventListener('mousemove', onMouseMove, false)
  10. }
  11. animate() {
  12.   requestAnimationFrame(this.animate.bind(this))
  13.   // 通过摄像机和鼠标位置更新射线
  14.   this.raycaster.setFromCamera(this.mouse, this.camera)
  15.   this.render()
  16. }
复制代码
由于我们不绝地在在画布移动, 所以必要不绝的的射线位置。现在有了射线, 那我们必要场景的全部东西去比较了,rayCaster 也提供了方法代码如下:
  1. const intersects = this.raycaster.intersectObjects(
  2.   this.scene.children, // 场景的
  3.   true  // 若为true,则同时也会检测所有物体的后代。否则将只会检测对象本身的相交部分
  4. )
复制代码
这个intersects得到的交叉很多,但是呢我们只选择其中一个,那就是物体材质个数有两个的, 因为我们上面就是用对mesh用两个材质
  1. const mesh = new THREE.Mesh(geometry, [material, material1])
复制代码
所以过滤代码如下
  1. animate() {
  2.   requestAnimationFrame(this.animate.bind(this))
  3.   // 通过摄像机和鼠标位置更新射线
  4.   this.raycaster.setFromCamera(this.mouse, this.camera)
  5.   // 算出射线 与当场景相交的对象有那些
  6.   const intersects = this.raycaster.intersectObjects(
  7.     this.scene.children,
  8.     true
  9.   )
  10.   const find = intersects.find(
  11.     (item) => item.object.material && item.object.material.length === 2
  12.   )
  13.   this.render()
  14. }
复制代码
我怎么知道我到底找到没,我们对找到的mesh将它的外貌变成灰色,但是如许会导致一个题目,我们鼠标再一次移动的时间要把上一次的材质给他恢复过来。
代码如下:
  1. animate() {
  2.     requestAnimationFrame(this.animate.bind(this))
  3.     // 通过摄像机和鼠标位置更新射线
  4.     this.raycaster.setFromCamera(this.mouse, this.camera)
  5.     // 算出射线 与当场景相交的对象有那些
  6.     const intersects = this.raycaster.intersectObjects(
  7.       this.scene.children,
  8.       true
  9.     )
  10.     // 恢复上一次清空的
  11.     if (this.lastPick) {
  12.       this.lastPick.object.material[0].color.set('#2defff')
  13.       this.lastPick.object.material[1].color.set('#3480C4')
  14.     }
  15.     this.lastPick = null
  16.     this.lastPick = intersects.find(
  17.       (item) => item.object.material && item.object.material.length === 2
  18.     )
  19.     if (this.lastPick) {
  20.       this.lastPick.object.material[0].color.set(0xff0000)
  21.       this.lastPick.object.material[1].color.set(0xff0000)
  22.     }
  23.     this.render()
  24.   }
复制代码
看下效果图:

     three.js射线追踪效果
  
增长tooltip

为了让交互更加完美,找到了同时在鼠标右下方显示个tooltip,那这个肯定是一个div默认是影藏的,然后根据鼠标的移动移动相应的位置。
第一步新建div
  1. <div id="tooltip"></div>
复制代码
第二步设置样式 默认是影藏的
  1. #tooltip {
  2.   position: absolute;
  3.   z-index: 2;
  4.   background: white;
  5.   padding: 10px;
  6.   border-radius: 2px;
  7.   visibility: hidden;
  8. }
复制代码
第三步更改div的位置:
  1.   setRaycaster() {
  2.     this.raycaster = new THREE.Raycaster()
  3.     this.mouse = new THREE.Vector2()
  4.     this.tooltip = document.getElementById('tooltip')
  5.     const onMouseMove = (event) => {
  6.       this.mouse.x = (event.clientX / window.innerWidth) * 2 - 1
  7.       this.mouse.y = -(event.clientY / window.innerHeight) * 2 + 1
  8.       // 更改div位置
  9.       this.tooltip.style.left = event.clientX + 2 + 'px'
  10.       this.tooltip.style.top = event.clientY + 2 + 'px'
  11.     }
  12.     window.addEventListener('mousemove', onMouseMove, false)
  13.   }
复制代码
最后一步设置tooltip的名字:
  1. showTip() {
  2.     // 显示省份的信息
  3.     if (this.lastPick) {
  4.       const properties = this.lastPick.object.parent.properties
  5.       this.tooltip.textContent = properties.name
  6.       this.tooltip.style.visibility = 'visible'
  7.     } else {
  8.       this.tooltip.style.visibility = 'hidden'
  9.     }
  10.   }
复制代码
到这里,整个3d可视化地球项目已经完成了, 我们一起看下效果吧。

     three.js3D可视化地图效果
  
泉源

带你入门three.js——从0到1实现一个3d可视化地图
Three.js教程

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

饭宝

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表