4.构建Three.js应用的基本组件

说明

代码地址与效果

可以将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.MeshTHREE.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属性。在数学上物体旋转─周的弧度值为,所以可以用如下三种方式设置旋转:

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属性truefalse,来控制对象的显示与隐藏。

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-3030的移动动画。将摄像机所指向的位置与球体每一帧的位置同步,就达到了摄像机追随球体的效果。如下:

相关推荐
zhangxingchao37 分钟前
Flutter中的页面跳转
前端
烛阴1 小时前
Puppeteer入门指南:掌控浏览器,开启自动化新时代
前端·javascript
全宝2 小时前
🖲️一行代码实现鼠标换肤
前端·css·html
小小小小宇2 小时前
前端模拟一个setTimeout
前端
萌萌哒草头将军2 小时前
🚀🚀🚀 不要只知道 Vite 了,可以看看 Farm ,Rust 编写的快速且一致的打包工具
前端·vue.js·react.js
芝士加3 小时前
Playwright vs MidScene:自动化工具“双雄”谁更适合你?
前端·javascript
Carlos_sam4 小时前
OpenLayers:封装一个自定义罗盘控件
前端·javascript
前端南玖4 小时前
深入Vue3响应式:手写实现reactive与ref
前端·javascript·vue.js
wordbaby5 小时前
React Router 双重加载器机制:服务端 loader 与客户端 clientLoader 完整解析
前端·react.js
itslife5 小时前
Fiber 架构
前端·react.js