你的第一个 three.js 场景:你好,立方体!
在本章中,我们将创建 three.js 的 Hello World 应用程序:一个简单的白色立方体。由于我们已经建立了一个简单的网页,如上一章所述,我们需要做的就是在 src/main.js 中编写几行 JavaScript 代码,我们的应用程序就会运行起来。我们将在此过程中介绍相当多的理论,但实际代码很短。下面是该文件在本章结束时的样子。不算导入语句和注释,总共有不到二十行代码。这就是创建一个简单的“你好,立方体!”的 three.js 应用程序所需要的全部内容。
import {
BoxBufferGeometry,
Color,
Mesh,
MeshBasicMaterial,
PerspectiveCamera,
Scene,
WebGLRenderer,
} from 'three';
// Get a reference to the container element that will hold our scene
const container = document.querySelector('#scene-container');
// create a Scene
const scene = new Scene();
// Set the background color
scene.background = new Color('skyblue');
// Create a camera
const fov = 35; // AKA Field of View
const aspect = container.clientWidth / container.clientHeight;
const near = 0.1; // the near clipping plane
const far = 100; // the far clipping plane
const camera = new PerspectiveCamera(fov, aspect, near, far);
// every object is initially created at ( 0, 0, 0 )
// move the camera back so we can view the scene
camera.position.set(0, 0, 10);
// create a geometry
const geometry = new BoxBufferGeometry(2, 2, 2);
// create a default (white) Basic material
const material = new MeshBasicMaterial();
// create a Mesh containing the geometry and material
const cube = new Mesh(geometry, material);
// add the mesh to the scene
scene.add(cube);
// create the renderer
const renderer = new WebGLRenderer();
// next, set the renderer to the same size as our container element
renderer.setSize(container.clientWidth, container.clientHeight);
// finally, set the pixel ratio so that our scene will look good on HiDPI displays
renderer.setPixelRatio(window.devicePixelRatio);
// add the automatically created <canvas> element to the page
container.append(renderer.domElement);
// render, or 'create a still image', of the scene
renderer.render(scene, camera);
点击在编辑器左上角的切换按钮以 查看此代码的运行情况,或者,如果您更喜欢在 本地工作,您可以单击按钮下载包含编辑器中所有文件的 zip 存档。如果您不熟悉此处的 JavaScript,请参阅附录中的 A.2:JavaScript 参考 和 A.3:文档对象模型和 DOM API。
实时 3D 应用程序组件
在开始编写代码之前,让我们先看看构成每个 three.js 应用程序的基本组件。首先是场景、相机和渲染器,它们构成了应用程序的基本脚手架。接下来是 HTML
<canvas>
元素,我们可以在其中看到结果。最后但并非最不重要的一点是,有一个可见的对象,例如网格。除了画布 canvas(特定于浏览器)之外,在任何 3D 图形系统中都可以找到与这些组件中的每一个等效的组件,从而使您在这些页面中获得的知识具有高度可转移性。
场景:小宇宙
场景是我们能看到的一切的载体。您可以将其视为所有 3D 对象都存在于其中的“小宇宙”。我们用来创建场景的 three.js 类简称为
Scene
. 其构造函数不带参数。
场景scene
定义了一个名为**World Space(世界空间)**的坐标系,它是我们在 three.js 中处理可见对象时的主要参考框架。世界空间是一个
3D 笛卡尔坐标系。我们将在
1.5:变换和坐标系中更详细地探讨这个怎么理解以及如何使用世界空间。
场景的中心是点$(0,0,0)$,也称为坐标系的原点。每当我们创建一个新对象并将其添加到我们的场景中时,它将被放置在原点,并且每当我们移动它时,我们说的都是在这个坐标系中移动它。
当我们将对象添加到场景中时,它们会被放入 场景图中,这是一个树形结构,场景位于顶部。
 
这类似于 HTML 页面上元素的结构方式,不同之处在于 HTML 页面是 2D 而场景图是 3D。
相机:指向小宇宙的望远镜
场景的小宇宙是指纯数学的领域。要查看场景,我们需要打开一个进入这个领域的窗口,并将其转换为对我们人眼感觉合理的东西,这就是相机的用武之地。有几种方法可以将场景图形转换为人类视觉友好的格式,使用称为投影的技术。对我们来说,最重要的投影类型是透视投影,它旨在匹配我们的眼睛看待世界的方式。要使用透视投影查看场景,我们使用
PerspectiveCamera
。 这种类型的相机是现实世界中相机的 3D 等效物,并使用许多相同的概念和术语,例如视野和纵横比。与场景Scene
不同的是,PerspectiveCamera
构造函数有几个参数,我们将在下面详细解释。
另一种重要的投影类型是正交投影,我们可以使用
OrthographicCamera
。 如果您曾经研究过工程图或蓝图,您可能会熟悉这种类型的投影,它对于创建 2D 场景或覆盖 3D 场景的用户界面很有用。在本书中,我们将使用 HTML 来创建用户界面,并使用 three.js 来创建 3D 场景,所以我们将在大部分情况下坚持使用PerspectiveCamera
。
以下示例显示了这两款相机之间的区别。左侧显示使用OrthographicCamera
(按键 O)或PerspectiveCamera
(按键 P)渲染的场景,而视图右侧显示相机的缩小概览:
渲染器:具有非凡才能和速度的艺术家
如果场景是一个小宇宙,而相机是一个指向那个宇宙的望远镜,那么渲染器就是一个艺术家,他通过望远镜观察并将他们看到的东西 非常快 的绘制到一个<canvas>
中去。 我们把这个过程叫做渲染,得到的图片就是一个渲染效果图。在本书中,我们将专门使用
WebGLRenderer
—— 它使用
WebGL2来渲染我们的场景 (如果可用),如果不可用则回退到WebGL V1。渲染器的构造函数确实接受了几个参数,但是,如果我们不显示传入这些参数,它将使用默认值,目前这对于我们来说没问题。
场景、相机和渲染器一起为我们提供了 three.js 应用程序的基本脚手架。但是,一个都看不到。在本章中,我们将介绍一种称为网格的可见对象。
我们的第一个可见对象:网格 Mesh
网格是 3D 计算机图形学中最常见的可见对象,用于显示各种 3D 对象——猫、狗、人类、树木、建筑物、花卉和山脉都可以使用网格来表示。还有其他种类的可见对象,例如线条、形状、精灵和粒子等,我们将在后面的部分中看到所有这些,但在这些介绍性章节中我们将坚持使用网格。
如您所见,Mesh
构造函数有两个参数:几何和材质。在创建网格之前,我们需要创建这两个。
几何体
几何体定义了网格的形状。我们将使用一种称为
BufferGeometry
的几何体。在这里,我们需要一个盒子形状,所以我们将使用
BoxBufferGeometry
,它是 three.js 核心中提供的几个基本形状之一。
构造函数最多需要六个参数,但在这里,我们只提供前三个参数,它们指定盒子的长度、宽度和深度。默认值将被提供给我们省略的任何其他参数。您可以在下面的场景中使用所有六个参数。
材料
虽然几何体定义了形状,但材质定义了网格表面的外观。我们将在本章中使用
MeshBasicMaterial
,这是可用的最简单的材质,更重要的是,不需要我们在场景中添加任何灯光。现在,我们将省略所有参数,这意味着将创建默认的白色材质。
许多参数可在此处进行测试。Material 菜单具有所有 three.js 材质通用的参数,而 MeshBasicMaterial 菜单具有仅属于该材质的参数。
我们的第一个 three.js 应用程序
现在我们准备好编写一些代码了!我们已经介绍了构成我们简单应用程序的所有组件,因此下一步是弄清楚它们如何组合在一起。我们将把这个过程分成六个步骤。您创建的每个 three.js 应用程序都需要所有这六个步骤,尽管更复杂的应用程序通常需要更多。
1. 初始设置
初始设置的一个重要部分是创建某种网页来托管我们的场景,这个我们在上一章中已经介绍过。在这里,我们将专注于我们需要编写的 JavaScript。首先,我们将从 three.js 中导入必要的类,然后我们将从 index.html 文件中获取对该scene-container
元素的引用。
从 three.js 中导入类
总结到目前为止我们介绍的所有组件,我们可以看到我们需要这些类:
BoxBufferGeometry
Mesh
MeshBasicMaterial
PerspectiveCamera
Scene
WebGLRenderer
我们还将使用Color
类来设置场景的背景颜色:
Color
我们可以仅使用单个import
语句从 three.js 核心导入我们需要的所有内容。
import {
BoxBufferGeometry,
Color,
Mesh,
MeshBasicMaterial,
PerspectiveCamera,
Scene,
WebGLRenderer,
} from 'three';
如果您在本地工作(而不是使用 Webpack 之类的捆绑程序),则必须更改导入路径。例如,您可以改为从 skypack.dev 导入。
import {
BoxBufferGeometry,
Color,
Mesh,
MeshBasicMaterial,
PerspectiveCamera,
Scene,
WebGLRenderer,
} from "https://cdn.skypack.dev/[email protected]";
如果您需要有关导入 three.js 类的工作原理的提示,请参考 0.5:如何在您的项目中包含 three.js ,或者如果您想复习 JavaScript 模块,请跳至 A.4:JavaScript 模块。
JavaScript 中访问 HTML 的scene-container
元素
在 index.html 中,我们创建了一个scene-container
元素。
<body>
<h1>Discoverthreejs.com - Your First Scene</h1>
<div id="scene-container">
<!-- Our <canvas> will be inserted here -->
</div>
</body>
渲染器会自动为我们创建一个<canvas>
元素,我们将把它插入到这个容器中。通过这样做,我们可以通过使用 CSS 设置容器的大小来控制场景的大小和位置(如
上一章所述)。首先,我们需要在 JavaScript 中访问容器元素,我们将使用
document.querySelector
。
// Get a reference to the container element that will hold our scene
const container = document.querySelector('#scene-container');
2.创建场景
设置好之后,我们将从创建场景开始,我们自己的小宇宙。我们将使用
Scene
构造函数(带有大写的“S”)来创建一个scene
实例(带有小写的“s”):
// create a Scene
const scene = new Scene();
设置场景的背景颜色
接下来,我们将
场景背景的颜色更改为天蓝色。如果我们不这样做,将使用默认颜色,即黑色。我们将使用我们在上面导入的Color
类,将字符串'skyblue'
作为参数传递给构造函数:
// Set the background color
scene.background = new Color('skyblue');
'skyblue'
是一个
CSS 颜色名称,我们可以在这里使用全部 140 种命名颜色中的任何一种。当然,不仅限于使用这几种颜色。您可以使用您的显示器可以显示的任何颜色,并且有几种指定它们的方法,就像在 CSS 中一样。
3.创建相机
在 three.js 核心中有几个不同的相机可用,但正如我们上面讨论的,我们将主要使用
PerspectiveCamera
,因为它绘制的场景视图看起来类似于我们的眼睛看到的真实世界。PerspectiveCamera
构造函数有四个参数:
fov
,或视野:相机的视野有多宽,以度为单位。aspect
,或纵横比:场景的宽度与高度的比率。near
, 或近剪裁平面:任何比这更靠近相机的东西都是不可见的。far
,或远剪裁平面:任何比这更远离相机的东西都是不可见的。
// Create a camera
const fov = 35; // AKA Field of View
const aspect = container.clientWidth / container.clientHeight;
const near = 0.1; // the near clipping plane
const far = 100; // the far clipping plane
const camera = new PerspectiveCamera(fov, aspect, near, far);
这四个参数一起用于创建一个有边界的空间区域,我们称之为 视锥体。
相机的视锥体
如果scene
是一个微小的宇宙,永远向四面八方延伸,那么相机的视锥体就是我们可以 看到 的部分。平截头体 是一个数学术语,意思是一个顶部被切掉的四边矩形金字塔。当我们通过PerspectiveCamera
查看场景时,截锥体内的一切都是可见的,而它外面的一切都是不可见的。在下图中,Near Clipping Plane和Far Clipping Plane之间的区域是相机的视锥。
我们传递给构造函数的四个参数PerspectiveCamera
分别创建了截锥体的一个方面:
- 视野定义了平截头体扩展的角度。小视场会产生窄截锥体,而宽视场会产生宽截锥体。
- 纵横比将平截头体与场景容器元素相匹配。当我们将其设置为容器的宽度除以其高度时,我们确保可以将类似矩形的平截头体完美的扩展到容器中。如果我们弄错了这个值,场景看起来会伸展和模糊。
- 近剪切平面定义了平截头体的小端(最接近相机的点)。
- 远剪裁平面定义了平截头体的大端(距相机最远)。
渲染器不会绘制场景中不在平截头体内的任何对象。如果一个物体部分在平截头体体内部,部分在平截头体外部,则外部的部分将被切掉(剪掉)。
定位相机
我们创建的每个对象最初都位于场景的中心,$(0,0,0)$。 这意味着我们的相机当前位于$(0,0,0)$,我们添加到场景中的任何对象也将定位在$(0,0,0)$, 都在彼此之上混杂在一起。艺术性地放置相机是一项重要的技能,但是,现在,我们将简单地将其移回( 朝向我们 )以给我们一个场景的概览。
const camera = new PerspectiveCamera(fov, aspect, near, far);
// every object is initially created at ( 0, 0, 0 )
// move the camera back so we can view the scene
camera.position.set(0, 0, 10);
设置任何对象的位置的方法都是一样的,无论是相机,网格,灯还是其他任何东西。我们可以一次设置位置的所有三个组成部分,就像我们在这里所做的那样:
或者,我们可以单独设置 X,Y 和 Z 轴:
两种设置位置的方式都会给出相同的结果。位置存储在一个
Vector3
,一个表示 3D 向量的 three.js 类中,我们将在
1.5:变换和坐标系中更详细地探讨它。
4.创建一个可见对象
我们创建了一个camera
用来查看事物,以及一个scene
用来把它们放进去。下一步是创建我们可以看到的东西。在这里,我们将创建一个简单的盒子形状
Mesh
。正如我们上面提到的,网格有两个我们需要首先创建的子组件:几何体和材质。
创建几何体
网格的几何定义了它的形状。如果我们创建一个盒子形状的几何体(就像我们在这里所做的那样),我们的网格将被塑造成一个盒子。如果我们创建一个球形几何体,我们的网格将呈球体形状。如果我们创建一个猫形几何体,我们的网格将被塑造成一只猫……你明白了。在这里,我们使用
BoxBufferGeometry
。 这三个参数定义了盒子的宽度、高度和深度:
// create a geometry
const geometry = new BoxBufferGeometry(2, 2, 2);
大多数参数都有默认值,因此即使文档说BoxBufferGeometry
应该采用六个参数,我们也可以省略大部分参数,而 three.js 将使用默认值填充空白。我们不必传入 任何 参数。
如果我们省略所有参数,我们将得到一个默认框,它是$1 \times 1 \times 1$立方体。我们想要一个更大的立方体,所以我们传入上面的参数来创建一个$2 \times 2 \times 2$盒子。
创建材质
材料定义了对象的表面属性,或者换句话说,定义了对象 看起来 是由什么制成的。几何体告诉我们网格是一个盒子、一辆汽车或一只猫,而材质告诉我们它是一个金属盒子、一辆石质汽车或一只涂成红色的猫。
在 three.js 中有不少资料。在这里,我们将创建一个
MeshBasicMaterial
,这是可用的最简单(也是最快)的材料类型。此材质还会忽略场景中的任何灯光,并根据材质的颜色和其他设置为网格着色(阴影),这非常棒,因为我们还没有添加任何灯光。我们将在不向构造函数传递任何参数的情况下创建材质,因此我们将获得默认的白色材质。
// create a default (white) Basic material
const material = new MeshBasicMaterial();
创建网格
现在我们有了几何体和材质,我们可以创建我们的网格,将两者都作为参数传入。
// create a geometry
const geometry = new BoxBufferGeometry(2, 2, 2);
// create a default (white) Basic material
const material = new MeshBasicMaterial();
// create a Mesh containing the geometry and material
const cube = new Mesh(geometry, material);
之后,我们可以随时使用mesh.geometry
和mesh.material
访问几何体和材质。
将网格添加到场景中
创建完成mesh
后,我们需要将其添加到场景中。
// add the mesh to the scene
scene.add(cube);
稍后,如果我们想删除它,我们可以使用scene.remove(mesh)
。 一旦网格被添加到场景中,我们称网格为场景的 子节点,我们称场景为网格的 父节点。
5. 创建渲染器
我们这个简单应用程序的最后一个组件是渲染器,它负责将场景绘制(渲染)到<canvas>
元素中。我们将在这里使用
WebGLRenderer
。还有一些其他渲染器可用作插件,但WebGLRenderer
是迄今为止最强大的渲染器,通常是您唯一需要的渲染器。让我们现在继续创建一个WebGLRenderer
,再次使用默认设置。
// create the renderer
const renderer = new WebGLRenderer();
设置渲染器的大小
我们快到完成了!接下来,我们需要使用容器的宽度和高度告诉渲染器我们的场景大小。
// next, set the renderer to the same size as our container element
renderer.setSize(container.clientWidth, container.clientHeight);
如果你还记得,我们使用 CSS 使容器占据了整个浏览器窗口的大小(如 上一章所述),因此场景也将占据整个窗口。
设置设备像素比(DPR)
我们还需要告诉渲染器设备屏幕的像素比是多少。这是防止 HiDPI 显示器模糊所必需的 (也称为视网膜显示器)。
// finally, set the pixel ratio so that our scene will look good on HiDPI displays
renderer.setPixelRatio(window.devicePixelRatio);
我们不会在这里讨论技术细节,但你不能忘记设置它,否则你的场景在你测试它的笔记本电脑上可能看起来很棒,但在带有视网膜显示器的移动设备上会模糊。与往常一样, 附录有更多细节。
将<canvas>
元素添加到我们的页面
渲染器将 从相机的角度将我们的场景绘制到一个<canvas>
元素中去。这个元素已经为我们自动创建并存储在renderer.domElement
中,但是在我们看到它之前,我们需要将它添加到页面中。我们将使用一个
名为.append
的内置 JavaScript 方法来做到这一点:
// add the automatically created <canvas> element to the page
container.append(renderer.domElement);
现在,如果您打开浏览器的开发控制台(按 F12)并检查 HTML,您将看到如下内容:
这假设浏览器窗口大小为 $800 \times 600$, 所以你看到的可能看起来略有不同。请注意,renderer.setSize
它还设置了画布上的宽度、高度和样式属性。
6. 渲染场景
一切就绪后,剩下要做的就是渲染场景!,将以下也是最后一行添加到您的代码中:
// render, or 'create a still image', of the scene
renderer.render(scene, camera);
通过这一行,我们告诉渲染器使用相机创建场景的静态图片并将该图片输出到<canvas>
元素中。如果一切设置正确,您将看到蓝色背景下的白色立方体。很难看出它是一个立方体,因为我们直接看的是一个正方形的脸,但我们将在接下来的几章中解决这个问题。
做得好!读完这一章,您已经迈出了作为 three.js 开发人员职业生涯的第一次巨大飞跃。我们的场景可能还没有那么有趣,但我们已经奠定了一些重要的基础,并涵盖了计算机图形学的一些基本概念,您将在以后构建的每个场景中使用这些概念,无论您使用的是 three.js 还是任何其他 3D 图形系统。