2. 绘制第一个立方体

179 阅读9分钟

1.本篇目标

我们这个篇章的实现效果图如下:

QQ录屏20230902170552.gif

代码地址与演示效果

下面我们将一步一步的通过three.js来实现上面的效果。

2.实现步骤

在开始编写代码之前,让我们先看看构成每个three.js应用程序的基本组件。首先是场景相机渲染器,它们构成了应用程序的基本脚手架。接下来是 HTML <canvas>元素,我们可以在其中看到结果。最后但并非最不重要的一点是,有一个可见的对象,例如网格。除了画布canvas(特定于浏览器)之外,在任何 3D 图形系统中都可以找到与这些组件中的每一个等效的组件,从而使您在这些页面中获得的知识具有高度可转移性。

image.png

2.1 创建场景

场景是我们能够看到一切的载体,我们可以将其理解为Three.js中装载一切对象的顶层容器。例如接下来要讲到的相机、立方体这些都是要载入场景中,才能被我们看到的。

import { Scene } from 'three';

const scene = new Scene();

我们用来创建场景的类在Three.js中为Scene类,其构造函数是不带参数的。

当我们创建了场景对象之后,就等同于定义了一个世界坐标系通常也称称笛卡尔坐标系,在Three.js中处理可见对象时主要参考它。场景的中心点(0,0,0)又称为原点,当我们向场景中添加新的对象时其默认初始位置就在原点。

image.png

如果你无法在脑海中形成上面这张图的样子,不妨像下面这张图一样。通过右手直角笛卡尔坐标系来辅助你的大脑发挥想象。我们的大拇指指向的方向为笛卡尔坐标系中的X轴,食指所指方向为Y轴,中指所指方向为Z轴。

image.png

2.2 创建透视相机

场景的小宇宙是指纯数学的领域。要查看场景,我们需要打开一个进入这个领域的窗口,并将其转换为对我们人眼感觉合理的东西,这就是相机的用武之地。有几种方法可以将场景图形转换为人类视觉友好的格式,使用称为投影的技术。

Three.js中提供了透视投影正交投影两种投影,对应的类分别是PerspectiveCameraOrthographicCamera。本篇我们使用的是透视投影,因为其更匹配我们看待世界的方式。

import { PerspectiveCamera } from 'three;

const width = window.innerWidth;
const height = window.innerHeight;
const camera = new PerspectiveCamera(75, width / height, 0.1, 100);

PerspectiveCamera类构造函数的参数如下:

参数类型默认值说明
fovNumber50摄像机视锥体垂直视野角度,从视图的底部到顶部,以角度来表示。
aspectNumber1(正方形画布)摄像机视锥体的长宽比。
nearNumber0.1摄像机到近截面距离。
farNumber2000摄像机到远截面距离。
image.png

透视相机与正交相机的区别

在透视相机中同一个物体大小一样,距离视点近的要比距离视点远的呈现在屏幕上大些。而同一个物体大小一样在正交相机中,不管距离视点是远还是近呈现的大小都是一样的。

我们可以通过官方给的案例,来查看两种相机下呈现的效果。默认为透视相机的效果,可以通过按op来切换两种相机带来的效果。

2.3 创建立方体

我们想要创建立方体就需要使用到Three.js中的Mesh(网格)类。网格是 3D 计算机图形学中最常见的可见对象,用于显示各种3D对象——猫、狗、人类、树木、建筑物、花卉和山脉都可以使用网格来表示。还有其他种类的可见对象,例如线条、形状、精灵和粒子等。

而网格由于Geometry(几何体)Material(材质)组成,所以在创建网格之前我们需要先准备好几何体和材质。

image.png

2.3.1 几何体

几何体用于定义网格的形状,这里我们将使用BoxGeometry类来定义立方体的形状,它是Three.js中提供的几个基本形状之一。

import { BoxGeometry } from 'three';

const cubeGeometry = new BoxGeometry(1, 1, 1);

BoxGeometry类的构造函数参数如下:

参数类型默认值说明
widthFloat1X轴上面的宽度。
heightFloat1Y轴上面的高度。
depthFloat1Z轴上面的深度。
widthSegments(可选)Integer1宽度的分段数。
heightSegments(可选)Integer1高度的分段数。
depthSegments(可选)Integer1深度的分段数。

2.3.2 材质

有了网格的形状之后,我们还需要用材质来定义网格的外观。这里我们将使用MeshBasicMaterial来指定网格的外观,它是可用的最简单的材质,更重要的是,不需要我们在场景中添加任何灯光。

import { MeshBasicMaterial } from 'three';

const cubeMaterial = new MeshBasicMaterial({ color: 'blue' })

上面示例中,我们指定了材质的颜色color为蓝色。因为参数比较多我们就不一一列出来了,我们可以根据自己的需要去官网上进行查看。

2.3.3 Mesh

有了网格的形状材质之后,我们就可以通过Mesh类来创建我们的立方体了。

import { Mesh } from 'three';

const cube = new Mesh(cubeGeometry, cubeMaterial);

2.4 将相机与立方体添加到场景中

有了相机和立方体之后,我们需要将它们添加到场景之中我们才能够看到。这里我们可以通过调用Sceneadd()方法进行添加。

scene.add(camera);
scene.add(cube);

image.png

此时当你运行程序就会发现浏览器显示的却是一片空白,可能这时你就会好奇我们不是有了场景,然后在场景中摆放了一个蓝色的立方体。并且还创建了模拟我们眼睛的相机来看这个立方体,可屏幕上为什么却是一片空白?

因为相机只是模拟我们的眼睛去观察场景中的物体,但并不能够代替我们真实眼睛看到的东西。如果可以那将太荒谬了,所以如果我们想要在屏幕上看到我们准备的东西还得将其渲染到屏幕上才行。

2.5 渲染

如果将场景比作一个小宇宙,相机比作一个指向那个宇宙的望远镜,那么渲染器就是一个艺术家,他通过望远镜观察并将他们看到的东西非常快的绘制到一个<canvas>中去。 我们把这个过程叫做渲染,我们可以通过WebGLRenderer类来进行渲染。

import { WebGLRenderer } from 'three';

// 初始化渲染器
const renderer = new WebGLRenderer();
// 设置渲染器尺寸
renderer.setSize(window.innerWidth, window.innerHeight);
// 将webgl渲染的canvas内容添加到body中
document.body.appendChild(renderer.domElement);
// 使用渲染器,通过相机将场景渲染出来
renderer.render(scene, camera);

场景、相机和渲染器一起为我们提供了 three.js 应用程序的基本脚手架。

当我们再次运行程序发现浏览器屏幕变成黑色的了,但是屏幕中却没有我们事前添加到场景中的蓝色立方体。

这是因为我们创建相机和立方体时都没有指定其在笛卡尔坐标系中的位置,所以它们两个默认都被放在了原点,挤在一块了。

我们可以通过改变相机或者立方体的位置,将立方体放入到相机的可视范围之内即可。

// 设置相机的位置
camera.position.set(0, 0, 5);
// 设置立方体的位置
cube.position.set(0, 0, -5);

当我们再次启动程序时,发现立方体呈现在屏幕中了。

image.png

虽然我们成功的将立方体显示在屏幕上了,但是因为相机被固定在了一个特定的位置。使得立方体看上去像是一个正方形。

如果要实现开头的效果我们需要让相机能够围绕目标进行运动。

2.6 轨道控制器

我们可以创建轨道控制器OrbitControls用来解决上面那一问题。

作用:轨道控制器它允许用户以鼠标进行交互,让相机围绕目标进行轨道运动。例如旋转、缩放和平移相机,以便更好地查看场景。

import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";

const controls = new OrbitControls(camera, renderer.domElement);

注意:官网上的OrbitControls的引入路径有问题。

轨道控制的构造函数的参数如下:

参数类型说明
object(必须)Camera将要被控制的相机。
domElementHTMLDOMElement用于事件监听的HTML元素。

创建好轨道控制器之后我们通过鼠标按下滑动屏幕发现依然只能看到立方体的正面。难道是轨道控制器不起作用?

其实并不是轨道控制器没有起作用,事实上当我们滑动屏幕时相机的位置已经发生了改变。只是因为改变位置后相机中新的场景没有被渲染出来而已。

我们可以写一个定时器,每个3秒打印一下相机的当前位置。

setInterval(() => {
  console.log(camera.position);
}, 3000);

然后不断的滑动屏幕,此时我们发现。相机的位置在不断发生变化。

image.png

明白了原因之后,就好办了。我们只需要编写一个定时任务并在这个定时任务中不断的重新绘制场景即可。

setInterval(() => {
  renderer.render(scene, camera);
});

虽然通过设置定时器也能够实现效果,但是其并不是最完美的。我们可以通过requestAnimationFrame()函数来实现,它能够更平滑、更高性能的展示动画效果。

requestAnimationFramesetInterval 都是用于执行重复任务的 JavaScript 方法,但它们之间有一些重要的区别,尤其是在创建动画或进行性能优化时。

使用requestAnimationFrame方法接收一个回调函数,这个回调函数会在下一次重绘之前执行。

function animate() {
  requestAnimationFrame(animate);
  renderer.render(scene, camera);
}
animate();

至此我们就实现了开篇的效果。

3.总结

至此我们总结一下实现步骤和顺序如下:

  1. 创建场景
  2. 创建相机
  3. 创建立方体
  4. 将相机与立方体添加入场景中
  5. 调整相机位置
  6. 创建轨道控制器
  7. 循环渲染

最后可以看看下面的结构图,有利于我们记忆。

image.png