说明
可以将APP.vue
中的注释放开,来查看效果。文件名与本篇目录小节是一一对应的。
1.创建场景
前面的章节中我们有创建场景,想必你已经了解了Three.js
库的基础知识。我们可以看到,一个场景想要显示任何东西,都需要如下三种类型的组件。
组件 | 说明 |
---|---|
摄像机 | 决定屏幕上哪些东西需要渲染 |
光源 | 决定材质如何显示以及用于产生阴影 |
渲染器 | 基于摄像机和场景的信息,调用底层图形API执行真正的场景绘制工作 |
THREE.Scene
对象是所有不同对象的容器,但这个对象本身没有那么多的选项和方法。
每个你添加到场景中的对象,甚至包括THREE.Scene
本身,都是继承自一个名为THREE.Object3D
的对象。一个THREE.Object3D
对象也可以有自己的子对象,你甚至可以使用它的子对象来创建一个Three.js
能解释和渲染的对象树。
js
import * as THREE from 'three';
const scene = new THREE.Object3D();
scene.add(camera);
scene.add(cube);
...
下面我们将通过案例的形式,来聊聊一些有关场景的知识点。在这之前我们需要先做好准备工作,将一些简单的或者说之前学过的知识就不重复了。将重心放在有关场景的知识点上,准备的案例效果如下所示:
我们需要提前准备如下东西:
- 创建场景。
- 创建透视相机,并将其位置定位到
(-30, 40, 30)
。 - 创建一个宽
60
,高40
的白色平面。并绕X
轴旋转-90°
,这一次我们将采用不同的材质。
js
const planeGeometry = new THREE.PlaneGeometry(60, 40);
// MeshLambertMaterial是一种非光泽表面的材质,没有镜面高光。
// 因为MeshBasicMaterial材质不受光照影响,而此次我们会加入环境光和聚光灯。
const planeMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });
const plane = new THREE.Mesh(planeGeometry, planeMaterial);
plane.rotation.set(-Math.PI / 2, 0, 0);
- 创建聚光灯其灯光颜色为白色并将其位置至于
(-40, 60, -10)
和环境光其颜色为灰色。
js
// 创建聚光灯
const spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(-40, 60, -10);
// 创建环境光
const ambientLight = new THREE.AmbientLight(0x0c0c0c);
- 将相机、平面、光源都添加到场景中。
- 创建渲染器、轨道控制器、和
animate
函数 - 最后进行渲染。
1.1 场景的基本功能
接下来我们将通过以写demo
的方式学习如下一些和场景相关的方法:
THREE.Scene.Add
:用于向场景中添加对象。THREE.Scene.Remove
:用于移除场景中的对象。THREE.Scene.children
:用于获取场景中所有的子对象。THREE.Scene.getObjectByName
:利用name属性,用于获取场景中特定的对象。
向场景中添加对象
需求:使用dat.gui
创建一个按钮,当点击按钮时会向场景中的平面上添加一个立方体。这个立方体的大小在1-3
之间,颜色随机,位置随机(在白色平面内),并且每个立方体都会有个旋转动画。
js
function addCube() {
const cubeSize = Math.ceil(Math.random() * 3);
const cubeColor = Math.random() * 0xffffff;
const x = -30 + Math.round(Math.random() * 60);
const y = Math.round(Math.random() * 5);
const z = -20 + Math.round(Math.random() * 40);
const cubeGeometry = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const cubeMaterial = new THREE.MeshLambertMaterial({ color: cubeColor });
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.set(x, y, z);
gsap.to(cube.rotation, {
x: Math.PI * 2,
y: Math.PI * 2,
duration: 5,
repeat: -1,
yoyo: true,
});
scene.add(cube);
}
const controlsGUI = {
addCube
}
gui.add(controlsGUI, "addCube");

移除场景中的对象
需求:创建一个删除按钮,当点击这个删除按钮之后,就将场景中最后一个立方体对象移除。
js
function removeLastCube() {
const allChildren = scene.children;
const lastCube = scene.children[allChildren.length - 1];
if (lastCube instanceof THREE.Mesh) {
scene.remove(lastCube);
}
}
const controlsGUI = {
addCube,
removeLastCube,
};
gui.add(controlsGUI, "removeLastCube");

获取场景中所有的子对象
需求:添加一个输入框控件,用来实时显示场景中所有子对象的个数。
diff
function addCube() {
...
scene.add(cube);
++controlsGUI.countSceneChildren = scene.children.length;
}
function removeCube() {
...
if(...){
scene.remove(lastCube);
++controlsGUI.countSceneChildren = scene.children.length;
}
}
const controlsGUI = {
countSceneChildren: scene.children.length,
addCube,
removeLastCube,
};
// listen方法的作用是监听其基础对象上的更改。
gui.add(controlsGUI, "countSceneChildren").listen();

利用name属性,获取场景中特定的对象
当我们已经知道了对象的名字时,就可以通过scene.getObjectByName
方法获取其对象。
js
plane.name = "plane";
console.log(scene.getObjectByName("plane"));

traverse方法
这些方法是和场景相关的重要方法,通常情况下这些方法就可以满足大部分需求了。但是,还有几个辅助方法可能会被用到。例如:scene.traverse()
方法。
它接收一个回调函数,这个传递过来的回调函数会在每一个子对象上执行。由于THREE.Scene
对象存储的是对象树,所以如果子对象本身还有子对象,traverse()
方法会在所有的子对象上执行,直到遍历完场景树中的所有对象为止。
我们在之前的基础之上,向白色面板plane
中添加一个子对象Object3D
。
js
const planeChildren = new Object3D();
planeChildren.name = "planeChildren";
plane.add(planeChildren);
scene.traverse((obj) => {
console.log(obj);
});

在我们深入讨论THREE.Mesh
和THREE.Geometry
对象之前,先来介绍THREE.Scene
对象的两个属性:fog(雾化) 和overrideMaterial(材质覆盖)。
1.2 给场景添加雾化效果
使用fog
属性就可以为整个场景添加雾化效果。雾化效果是:场景中的物体离摄像机越远就会越模糊。
js
// 雾化颜色为白色,从近处0.015开始,到远处100结束。
scene.fog = new THREE.Fog(0xffffff, 0.015, 100);

当我们利用鼠标拉远摄像机时,能够明显看到雾化的效果。
使用THREE.Fog
创建的对象,雾的浓度是线性增长的,除此之外还有另外一种雾化效果的方法,如下:
js
scene.fog = new THREE.FogExp2(0xffffff, 0.01);
在这个方法中不再指定near
和1far
属性,只需要设置雾的颜色(Oxffffff)
和浓度(0.01)
即可。需要注意的是,该方法中雾的浓度不再是线性增长的,而是随着距离呈指数增长。
1.3 使用overrideMaterial属性
overrideMaterial
是场景的一个属性,当设置了overrideMaterial
属性后,场景中所有的物体都会使用该属性指向的材质,即使物体本身也设置了材质。当某一个场景中所有物体都共享同一个材质时,使用该属性可以通过减少Three.js
管理的材质数量来提高运行效率,但是实际应用中,该属性通常并不非常实用。
js
scene.overrideMaterial = new THREE.MeshLambertMaterial({ color: 0xffffff });

从上面图中可以看出,所有的立方体都使用的相同的材质和颜色进行渲染。
2 几何体和网格
在前面的例子中我们已经使用了几何体和网格。比如在向场景中添加立方体时,代码如下:
js
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshBasicMaterial({ color: "blue" });
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
我们使用THREE.BoxGeometry
定义了物体的形状、使用THREE.MeshBasicMaterial
定义了物体的外观和材质,并将它们合并成能够添加到场景中的网格THREE.Mesh
。下面我们将进一步了解什么是几何体和网格
2.1 几何体的属性和方法
Three.js
提供了很多可以在三维场景中使用的几何体,如长方体BoxGeometry
、平面PlaneGeometry
等几何体。这些几何体都是基于BufferGeometry类构建的,BufferGeometry
是一个没有任何形状的空几何。我们可以通过它发挥自己的想象自定义任何几何体形状,具体一点就是定义几何的顶点数据。
这里有点接近于底层,如果你学习过WebGL
的话,下面的内容听起来就比较轻松。
js
// 创建一个空的几何体对象
const geometry = new THREE.BufferGeometry();
通过类型化数组Float32Array
创建一组xyz坐标用来表示平面的顶点坐标。
js
`const vertices = new Float32Array([
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, -1.0, 1.0
]);`
上面我们定义了6
个顶点,平面不是只有4
个顶点吗,为什么我们要定义6
个?
这是因为对于渲染引擎来说,使用绘制三角形更容易,并且三角形渲染起来效率更高。而我们绘制的平面,实际上是由两个三角形组成的。
注意:逆时针绘制的是正面,顺时针绘制的是反面。
然后通过通过Three.js
的属性缓冲区对象BufferAttribute表示几何体的顶点数据
js
// 这里的3表示每个顶点是一个三元组(xyz)
const attribute = new THREE.BufferAttribute(vertices, 3);
最后将属性缓冲区写入到空几何体对象的position
属性中。
js
geometry.setAttribute("position", attribute);
const material = new THREE.MeshBasicMaterial({ color: "blue" });
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

此时我们发现当我们将相机移至平面后面时,平面就消失了。其实并不是消失了,而是Three.js
的材质默认正面可见,反面不可见。
js
// 默认为THREE.BackSide正面可见
const material = new THREE.MeshBasicMaterial({ color: "blue", side: THREE.BackSide });
我们可以根据自己的需要,来设置如下值:
值 | 描述 |
---|---|
THREE.FrontSide | 背面 |
THREE.BackSide | 前面 |
THREE.DoubleSide | 双面 |
之前我们通过6
个顶点绘制出了一个平面,发现这6
个点中其实有一些顶点是一样的。那么我们是否可以只定义4
个顶点,然后重复的顶点共用呢?
当然是可以的,但是我们要告诉WebGL
几何体对应顶点的索引是怎样的。
js
const vertices = new Float32Array([ // 绘制平面
-1.0, 1.0, 1.0,// v0
1.0, 1.0, 1.0,// v1
1.0, -1.0, 1.0,// v2
-1.0, -1.0, 1.0// v3
])
// 顶点索引
const indexes = new Uint16Array([0, 3, 2, 1, 0, 2]);
geometry.index = new THREE.BufferAttribute(indexes, 1);
geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);

使用4
个顶点加顶点索引的方式也能够绘制出同样的效果。
几何体还有一个强大的方法clone()
,调用这个方法能够帮我们复制一份几何体的形状。
js
const gui = new dat.GUI();
const guiControl = {
clone: function () {
const cloneGeometry = mesh.geometry.clone();
const cloneMesh = new THREE.Mesh(cloneGeometry, material);
cloneMesh.position.set(5, 0, 1);
scene.add(cloneMesh);
}
};
gui.add(guiControl, "clone");

2.2 网格对象的属性和方法
我们已经知道,创建一个网格需要一个几何体,以及一个或多个材质。网格对象提供了几个属性用来改变它在场景中的位置和显示效果。下面我们来看一下网格对象提供的属性和方法,如下:
方法(属性) | 描述 |
---|---|
position | 该属性决定该对象相对于父对象的位置。通常父对象是 THREE.Scene 对象或者THREE.Object3D 对象。 |
rotation | 通过该属性可以设置绕每个轴的旋转弧度。 |
scale | 通过该属性可以沿着x、y和z轴缩放对象。 |
translateX(amount) | 沿x轴将对象平移amount距离。 |
translateY(amount) | 沿y轴将对象平移amount距离。 |
translateZ(amount) | 沿z轴将对象平移amount距离。 |
visible | 该属性值为 false 时,THREEMesh将不会被渲染到场景中。 |
position
设置对象的位置我们可以直接修改xyz
属性值,又或者调用position.set()
方法一次性地设置x、y、和z坐标的值。
js
// 方法一:单个修改
cube.position.x = 2;
cube.position.y = 2;
cube.position.z = 2;
// 方法二:一次性修改
cube.position.set(2, 2, 2);
rotation
通过这个属性可以设置对象绕轴的旋转弧度。我们可以像设置position
属性那样来设置rotation
属性。在数学上物体旋转─周的弧度值为2π
,所以可以用如下三种方式设置旋转:
js
cube.rotation.x = Math.PI / 2;
cube.rotation.y = Math.PI / 2;
cube.rotation.z = Math.PI / 2;
cube.rotateX(Math.PI / 2);
cube.rotateY(Math.PI / 2);
cube.rotateZ(Math.PI / 2);
plane.rotation.set(-Math.PI / 2, 0, 0);
scale
该属性让我们可以沿指定轴缩放对象。如果设置的缩放值小于1
,那么物体就会缩小。如果设置的缩放值大于1
,那么物体就会变大。
ini
cube.scale.x = 2;
cube.scale.y = 2;
cube.scale.z = 2;
cube.scale.set(2, 2, 2);
translate
我们可以使用translate()
方法改变对象的位置,但是该方法设置的不是物体的绝对位置,而是物体相对于当前位置的平移距离。
js
cube.translateX(4);
cube.translateY(4);
cube.translateZ(4);
visible
通过设置对象的visible
属性true
或false
,来控制对象的显示与隐藏。
js
cube.visible = true //显示
cube.visible = false //隐藏
3 选择合适的摄像机
Three.js
库提供了两种不同的摄像机:正交投影摄像机 和透视投影摄像机。下面将会使用几个示例来了解正交投影摄像机和透视投影摄像机的使用和不同之处。
3.1 正交投影摄像机和透视投影摄像机
透视投影摄像机所呈现的透视视图,也是最自然的视图。因为透视投影摄像机模拟了人眼的工作方式,使呈现出来的效果与人眼在现实世界中看到的景象相符。
Three.js
中我们可以使用PerspectiveCamera
类来创建透视投影摄像机,
js
import * as THREE from "three";
const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(45, width / height, 0.01, 1000);
其构造函数需要传递4
个参数,具体如下:
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
fov | number | 摄像机视锥体垂直视野角度 | 50 |
aspect | number | 摄像机视锥体长宽比 | 1 |
near | number | 摄像机视锥体近端面距离 | 0.1 |
far | number | 摄像机视锥体远端面距离 | 2000 |

透视投影摄像机呈现的透视视图如下图所示,这些立方体距离摄像机越远,它们就会被渲染得越小。相反距离摄像机越近,渲染得越大。
正交投影摄像机与透视投影摄像机的区别在于前者渲染场景中的物体时,大小不会受摄像机与物体之间的距离影响。
我们可以使用Three.js
中的OrthographicCamera
类,来创建正交投影摄像机。
js
import THREE from 'three';
const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.OrthographicCamera(width / -2, width / 2, height / 2, height / -2);
其构造函数接收6
个参数,具体如下:
参数 | 类型 | 说明 | 默认值 |
---|---|---|---|
left | number | 摄像机距离视锥体左侧面距离 | - |
right | number | 摄像机距离视锥体右侧面距离 | - |
top | number | 摄像机距离视锥体上侧面距离 | - |
bottom | number | 摄像机距离视锥体下侧面距离 | - |
near | number | 摄像机视锥体近端面距离 | 0.1 |
far | number | 摄像机视锥体远端面距离 | 2000 |

正交投影摄像机呈现的正交视图如下图所示,这些立方体无论是距离正交投影摄像机近或者是远渲染的时候大小都是一样的。 因此这种摄像机通常被用于二维游戏中。
3.2 将摄像机聚焦在指定点上
通常来说,摄像机会指向场景的中心,用坐标来表示就是position(0, 0, 0)
。当然我们也可以很容易的改变摄像机所指向的位置,代码如下:
js
camera.lookAt(new THREE.Vector3(x, y, z))
我们可以使用相机的lookAt
方法来指定摄像机所指向的位置,当我们将场景中的某个物体的位置传递过去,摄像机就会指向这个物体。
js
const sphereGeometry = new THREE.SphereGeometry(2);
const shpereMaterial = new THREE.MeshBasicMaterial({ color: "green" });
const shpere = new THREE.Mesh(sphereGeometry, shpereMaterial);
shpere.position.set(-30, 8, 0);
gsap.to(shpere.position, {
x: 30,
duration: 2,
repeat: -1,
yoyo: true
});
scene.add(shpere);
function animate() {
// 追随球体
camera.lookAt(shpere.position);
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
上面示例中我们创建了一个球体,并给予了球体一个X
从-30
到30
的移动动画。将摄像机所指向的位置与球体每一帧的位置同步,就达到了摄像机追随球体的效果。如下: