对于大多数用户而已,从 npm 包注册表中心 安装并使用 构建工具 会是一个更推荐的方案。因为项目需要的依赖越多,就越有可能遇到静态托管无法轻易解决的问题。而使用构建工具,导入本地 JavaScript 文件和 npm 软件包将会是开箱即用的,无需导入映射(import maps)。
在项目文件夹中通过 终端 安装 three.js 和构建工具 Vite。Vite 将在开发过程中使用,但不会被打包成为最终网页的一部分。当然,除了 Vite 你也可以使用其他支持导入 ES Modules 的现代构建工具。
Three.js经常会和WebGL混淆, 但也并不总是,three.js其实是使用WebGL来绘制三维效果的。 WebGL是一个只能画点、线和三角形的非常底层的系统. 想要用WebGL来做一些实用的东西通常需要大量的代码, 这就是Three.js的用武之地。它封装了诸如场景、灯光、阴影、材质、贴图、空间运算等一系列功能,让你不必要再从底层WebGL开始写起。
这套教程假设你已经了解了JavaScript,且大部分内容会使用 ES6的语法。点击这里查看你需要提前掌握的东西。 大部分支持Three.js的浏览器都会自动更新,所以绝大多数用户应该都能运行本套教程的代码。 如果你想在非常老的浏览器上运行此代码, 你需要一个像Babel一样的语法编译器 。 当然使用非常老的浏览器的用户可能根本不能运行Three.js。
人们在学习大多数编程语言的时候第一件事就是让电脑打印个"Hello World!"。 对于三维来说第一件事往往是创建一个三维的立方体。 所以我们从"Hello Cube!"开始。
在我们开始前,让我们试着让你了解一下一个three.js应用的整体结构。一个three.js应用需要创建很多对象,并且将他们关联在一起。下图是一个基础的three.js应用结构。
上图需要注意的事项:
注意图中摄像机(Camera)是一半在场景图中,一半在场景图外的。这表示在three.js中,摄像机(Camera)和其他对象不同的是,它不一定要在场景图中才能起作用。相同的是,摄像机(Camera)作为其他对象的子对象,同样会继承它父对象的位置和朝向。在场景图这篇文章的结尾部分有放置多个摄像机(Camera)在一个场景中的例子。
有了以上基本概念,我们接下来就来画个下图所示的"Hello Cube"吧。
首先是加载three.js
- <script type="module">
- import * as THREE from 'three';
- </script>
把type="module"放到script标签中很重要。这可以让我们使用import关键字加载three.js。还有其他的方法可以加载three.js,但是自r106开始,使用模块是最推荐的方式。模块的优点是可以很方便地导入需要的其他模块。这样我们就不用再手动引入它们所依赖的其他文件了。
下一步我们需要一个
- <body>
- <canvas id="c"></canvas>
- </body>
Three.js需要使用这个canvas标签来绘制,所以我们要先获取它然后传给three.js。
- <script type="module">
- import * as THREE from 'three';
-
- function main() {
- const canvas = document.querySelector('#c');
- const renderer = new THREE.WebGLRenderer({antialias: true, canvas});
- ...
- </script>
拿到canvas后我们需要创建一个WebGL渲染器(WebGLRenderer)。渲染器负责将你提供的所有数据渲染绘制到canvas上。
注意这里有一些细节。如果你没有给three.js传canvas,three.js会自己创建一个 ,但是你必须手动把它添加到文档中。在哪里添加可能会不一样这取决你怎么使用, 我发现给three.js传一个canvas会更灵活一些。我可以将canvas放到任何地方, 代码都会找到它,假如我有一段代码是将canvas插入到文档中,那么当需求变化时, 我很可能必须去修改这段代码。
接下来我们需要一个透视摄像机(PerspectiveCamera)。
- const fov = 75;
- const aspect = 2; // 相机默认值
- const near = 0.1;
- const far = 5;
- const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
fov是视野范围(field of view)的缩写。上述代码中是指垂直方向为75度。 注意three.js中大多数的角用弧度表示,但是因为某些原因透视摄像机使用角度表示。
aspect指画布的宽高比,在默认情况下 画布是300x150像素,所以宽高比为300/150或者说2。
near和far代表近平面和远平面,它们限制了摄像机面朝方向的可绘区域。 任何距离小于或超过这个范围的物体都将被裁剪掉(不绘制)。
这四个参数定义了一个 "视椎(frustum)"。 视椎(frustum)是指一个像被削去顶部的金字塔形状。换句话说,可以把"视椎(frustum)"想象成其他三维形状如球体、立方体、棱柱体、截椎体。
近平面和远平面的高度由视野范围决定,宽度由视野范围和宽高比决定。
视椎体内部的物体将被绘制,视椎体外的东西将不会被绘制。
摄像机默认指向Z轴负方向,上方向朝向Y轴正方向。我们将会把立方体放置在坐标原点,所以我们需要往后移一下摄像机才能显示出物体。
camera.position.z = 2;
下图是我们想要达到的效果。
我们能看到摄像机的位置在z = 2。它朝向Z轴负方向。我们的视椎体范围从摄像机前方0.1到5。因为这张图是俯视图,视野范围会受到宽高比的影响。画布的宽度是高度的两倍,所以水平视角会比我们设置的垂直视角75度要大。
然后我们创建一个场景(Scene)。场景(Scene)是three.js的基本的组成部分。需要three.js绘制的东西都需要加入到scene中。 我们将会在场景是如何工作的一文中详细讨论。
const scene = new THREE.Scene();
然后创建一个包含盒子信息的立方几何体(BoxGeometry)。几乎所有希望在three.js中显示的物体都需要一个包含了组成三维物体的顶点信息的几何体。
- const boxWidth = 1;
- const boxHeight = 1;
- const boxDepth = 1;
- const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
然后创建一个基本的材质并设置它的颜色. 颜色的值可以用css方式和十六进制来表示。
const material = new THREE.MeshBasicMaterial({color: 0x44aa88});
再创建一个网格(Mesh)对象,它包含了:
const cube = new THREE.Mesh(geometry, material);
最后我们将网格添加到场景中。
scene.add(cube);
之后将场景和摄像机传递给渲染器来渲染出整个场景。
renderer.render(scene, camera);
这里有一个实例。
很难看出来这是一个三维的立方体,因为我们直视Z轴的负方向并且立方体和坐标轴是对齐的,所以我们只能看到一个面。
我们来让立方体旋转起来,以便更好的在三维环境中显示。为了让它动起来我们需要用到一个渲染循环函数 requestAnimationFrame.
代码如下:
- function render(time) {
- time *= 0.001; // 将时间单位变为秒
-
- cube.rotation.x = time;
- cube.rotation.y = time;
-
- renderer.render(scene, camera);
-
- requestAnimationFrame(render);
- }
- requestAnimationFrame(render);
requestAnimationFrame函数会告诉浏览器你需要显示动画。传入一个函数作为回调函数。本例中的函数是render函数。如果你更新了跟页面显示有关的任何东西,浏览器会调用你传入的函数来重新渲染页面。我们这里是调用three.js的renderer.render函数来绘制我们的场景。
requestAnimationFrame会将页面开始加载到函数运行所经历的时间当作入参传给回调函数,单位是毫秒数。但我觉得用秒会更简单所以我将它转换成了秒。
然后我们把立方体的X轴和Y轴方向的旋转角度设置成这个时间。这些旋转角度是弧度制。一圈的弧度为2Π所以我们的立方体在每个方向旋转一周的时间为6.28秒。
最后渲染我们的场景并调用另一个帧动画函数来继续我们的循环。
回调函数之外在主进程中我们调用一次requestAnimationFrame来开始整个渲染循环。
效果好了一些但还是很难看出是三维的。我们来添加些光照效果,应该会有点帮助。three.js中有很多种类型的灯光,我们将在后期文章中详细讨论。现在我们先创建一盏平行光。
- {
- const color = 0xFFFFFF;
- const intensity = 3;
- const light = new THREE.DirectionalLight(color, intensity);
- light.position.set(-1, 2, 4);
- scene.add(light);
- }
平行光有一个位置和目标点。默认值都为(0, 0, 0)。我们这里 将灯光的位置设为(-1, 2, 4),让它位于摄像机前面稍微左上方一点的地方。目标点还是(0, 0, 0),让它朝向坐标原点方向。
我们还需要改变下立方体的材质。MeshBasicMaterial材质不会受到灯光的影响。我们将他改成会受灯光影响的MeshPhongMaterial材质。
- const material = new THREE.MeshBasicMaterial({color: 0x44aa88}); // 绿蓝色
- const material = new THREE.MeshPhongMaterial({color: 0x44aa88}); // 绿蓝色
这是我们新的项目结构
下面开始生效了。
现在应该可以很清楚的看出是三维立方体了。
我们再添加两个立方体来增添点趣味性。
每个立方体会引用同一个几何体和不同的材质,这样每个立方体将会是不同的颜色。
首先我们创建一个根据指定的颜色生成新材质的函数。它会根据指定的几何体生成对应网格,然后将网格添加进场景并设置其X轴的位置。
- function makeInstance(geometry, color, x) {
- const material = new THREE.MeshPhongMaterial({color});
-
- const cube = new THREE.Mesh(geometry, material);
- scene.add(cube);
-
- cube.position.x = x;
-
- return cube;
- }
然后我们将用三种不同的颜色和X轴位置调用三次函数,将生成的网格实例存在一个数组中。
- const cubes = [
- makeInstance(geometry, 0x44aa88, 0),
- makeInstance(geometry, 0x8844aa, -2),
- makeInstance(geometry, 0xaa8844, 2),
- ];
最后我们将在渲染函数中旋转三个立方体。我们给每个立方体设置了稍微不同的旋转角度。
- function render(time) {
- time *= 0.001; // 将时间单位变为秒
-
- cubes.forEach((cube, ndx) => {
- const speed = 1 + ndx * .1;
- const rot = time * speed;
- cube.rotation.x = rot;
- cube.rotation.y = rot;
- });
-
- ...
这里是结果。
如果你对比上面的示意图可以看到此效果符合我们的预想。位置为X = -2 和 X = +2的立方体有一部分在我们的视椎体外面。他们大部分是被包裹的,因为水平方向的视角非常大。
我们的项目现在有了这样的结构
正如你看见的那样,我们有三个网格(Mesh)引用了相同的立方几何体(BoxGeometry)。每个网格(Mesh)引用了一个单独的MeshPhongMaterial材质来显示不同的颜色。
希望这个简短的介绍能帮助你起步。接下来我们将介绍如何使我们的代码具有响应性,从而使其能够适应多种情况.
es6模块,three.js,和文件夹结构
从r106版本开始,使用three.js的首选方式是通过es6模块。
在一个脚本中,es6模块可以通过import关键字加载或者通过