最近业务需要,重新开始整起了three.js,之前的three.js的使用还是在2018年,现在重新使用感觉有了区别,所以现在算是重新拾起,做个记录。
安装
照旧,我们是在Angular项目中使用,所以需要使用npm管理的方式。
使用npm的方式安装:
1 | npm i three |
同时安装three的type:
1 | npm i --save-dev @types/three |
这样我们可以在ts中有很好的类型提示。
基本场景
使用three.js构建场景,那么我们需要:
- 场景
scene
是一个容器,用来保存和跟踪我们想渲染的物体对象(Object3D),我们想要渲染的各种物体都需要放入到场景中。 - 相机
camera
定义了我们能在渲染好的场景中能看到什么。 - 渲染器
renderer
负责在指定相机角度下,浏览器中场景应该渲染成什么样子。
我们在组件的ngAfterViewInit()
生命周期中来初始化这些变量:
1 | // 场景 |
这里我们渲染器使用了WebGLRenderer()
。相比于基于Canvas
和SVG
的渲染器,WebGL渲染器有更好的性能。
我们将渲染的窗口设置为浏览器的窗口,并将this.renderer.domElement
加入到了document.body
元素里面。默认情况下这里会创建一个canvas
的元素,我们审查元素可以看到在根节点这里插入了一个canvas
,canvas
的宽高是浏览器视窗的大小。
我们为了方便观察,创建了一个辅助坐标系axes
,最后通过渲染器,以场景和相机为参数渲染。我们得到的页面:
关于坐标系的基本知识可以参照之前的一篇文章:three.js 系列一 创建第一个3d场景
加入几何体
现在我们的场景只有一个辅助坐标系,我们往里面加入一些简单的几何体。
在three.js里面,添加立体对象需要两个元素:几何体(geometry)和材质(material)。
内置的几何体有基本的立方体(cube)、球体(sphere)、平面(plane),以及其他不规则体。材质有网格基础材质、网格朗伯材质(感光材质)、着色器材质、直线基础材质等。
可以这样理解,几何体相当于骨头,材质相当于皮肤。一个物体必须要几何体结合材质才能创造出来。
方体
创建方体我们需要几何体BoxGeometry
,创建一个4x4x4的立方体。
材质的话用网格朗伯材质MeshLambertMaterial
,传递一个颜色初始化材质,颜色可以用16进制的数字0x0076ff
,也可以直接传递一个表示颜色的字符串#0076ff
,是一样的效果。
然后使用Mesh
方法将几何体和材质结合创造出立方体的3d对象。
我们可以使用position
属性设置3d对象在场景中的坐标位置,可以使用rotation
来旋转,使用scale
来缩放大小。
来看具体代码:
1 | const cubeGeometry = new BoxGeometry(4, 4, 4); |
需要注意的是,创建好3d对象后,需要将其加入到场景scene
中,并重新渲染this.renderer.render(this.scence, this.camera)
。
球体
我们再加入一个球体。
球体的几何体是SphereGeometry
,它接受参数:
SphereGeometry
构造函数接受的参数:
- radius 球体半径,默认为1
- widthSegments 水平分段数,沿着经线分段,最小值为3, 默认值为32.
- heightSegments 垂直分段数,沿着纬线分段,最小值为2,默认值为16.
- phiStart 指定水平(经线)起始角度,默认值为0
- phiLength 指定水平(经线)扫描角度的大小,默认值为Math.PI * 2
- thetaStart 指定垂直(纬线)起始角度,默认值为0
- thetaLength 指定垂直(纬线)扫描角度的大小,默认值为Math.PI
我们创建一个半径为4,垂直段数为20,横向段数为20。垂直段数和横向段数的意义是啥?这个需要后续研究下。
1 | const sphereGeometry = new SphereGeometry(4, 20, 20); |
球体是通过扫描并计算围绕着Y轴(水平扫描)和X轴(垂直扫描)的顶点来创建的。因此,我们可以利用phiStart
、phiLength
、thetaStart
、thetaLength
来创建不完整的球形切片。
平面体
平面几何体,在3d世界中一个平面可以想象成一个玻璃,和立方体相比,它有长宽,但是没有高度(厚度)。
创建平面体我们使用PlaneGeometry
,它接受参数:
- width 平面沿着X轴的宽度
- height 平面沿着y轴的长度
- widthSegments 平面宽度的分段数,默认值是1
- heightSegments 平面长度的分段数,默认值是1
我们这里创建一个60X20的平面:
1 | const planeGeometry = new PlaneGeometry(60, 20, 1, 1); |
这里receiveShadow
是表示这个3D对象接受光影的投射。
PlaneGeometry
只能创建长方形的平面,如果需要创建不规则的平面可以使用ShapeGeometry
。
添加灯光
如果在添加灯光之前看上面的代码运行结果,我们会在页面上看到大致的形状,但都是黑暗一片,这时候,就需要,光!!!
光源也分很多种,我们这里用一个具有锥形效果的光源SpotLight
:
1 | const spotLight = new SpotLight(0xffffff) |
SpotLight
接受一个颜色参数,用来决定发出光的颜色,castShadow
表示在它的照射下可以产生阴影。
基于上面几个,我们来看下程序渲染的情况:
加入动画
我们想一个场景:需要方体一直旋转。
思路是这样,我们需要先获得这个方体的对象,然后通过设置rotation
来让它旋转。
我们怎么获取这个方体对象?
这个问题其实就是怎么获取添加在场景里面的3D对象?
我们可以简单粗暴点,直接声明一个组件变量,将创建的方体对象放入这个变量里面。但这样不好,如果有十几个甚至于几十个3D对象,难道我们要写几十个变量?
我们可以在创建的时候将对象的id放入一个字典中,然后在需要用的时候,通过this.scence.getObjectById(id)
来获取到目标对象。
所以我们需要创建一个字典对象,我们可以在组件中随时访问:
1 | objectMap: Map<string, number> = new Map<string, number>() |
然后在创建cube的时候将id存起来:
1 | // 保存对象的id |
这样我们可以写一个方法来控制旋转:
1 | rotationCube() { |
开始动画
用requestAnimationFrame
来控制动画:
1 | renderScene() { |
注意这里requestAnimationFrame
调用本身的函数的时候,需要绑定this,否则会报错。
然后需要调用一次renderScene()
就可以启动动画, 我们将调用加在ngAfterViewInit()
方法的尾部:
1 | ngAfterViewInit() { |
再刷新页面,即可看到方体在随着X轴旋转:
监控动画的FPS
我们希望可以直观的看到动画每秒显示的帧数(FPS),我们可以使用Stats
这个库来做这个工作。
我们需要在模板里面放一个显示FPS的区域:
1 | <div id="Stats-output"></div> |
引入Stats
:
1 | import Stats from 'three/examples/jsm/libs/stats.module'; |
然后增加一个方法来初始化:
1 | initStats() { |
我们需要把初始化后得到的Stats
实例也放在组件变量里面,需要在每次渲染的时候调用stats.update()
方法更新fps:
1 |
|
然后运行即可看到浏览器左上角出现了fps监控:
回首看18年的那篇文章,没有用到three.js的类型定义库,没办法全部引入的,而且有些不可考了,基本概念是通用的,但是没法顺畅的走下来,所以这次重拾three.js,记录下历程。