Three.js是一个3D库,能让我们尽可能轻松地在网页中使用3D内容。
Three.js经常被人们和WebGL混淆,然而通常情况下(不完全是),Three.js是使用WebGL去绘制3D的。WebGL是一个非常底层的系统,它只用来绘制点、线、三角形。要使用WebGL去做一些有意思的东西的话,需要写很多很多代码,于是Three.js就诞生了。它能直接处理很多你在WebGL中需要自己去写代码的东西,比如:场景(Scenes)、灯光(Lights)、阴影(Shadows)、材质(Materials)、贴图(Textures)、3d计算(3d Math)等。
本系列默认你已经掌握JavaScript,很大部分会使用ES6的语法。现在大部分支持Three.js的浏览器都会自动更新,所以很大部分的用户都能够运行这些代码。如果你实在想在比较老的浏览器运行,你可以查询一些编译器,比如babel。事实上使用很老的浏览器的电脑应该也不能运行Three.js。
学一门新的语言的时候,人们都喜欢让电脑打印出"Hello World"。而学习3D,通常第一件事就是去制造一个3D立方体。我们就从"Hello Cube"开始吧!
在开始之前,我们了解一下Three.js应用的大致结构。一个Three.js应用需要你去创建一些对象,然后将它们连接在一起。下面这张图就代表了一个小的Three.js应用。
对于上面这张图,我们需要注意:
-
Renderer(渲染器)。这是在Three.js中最重要的对象。你传递一个Scene和一个Camera给一个Renderer,然后它会将在Camera的截锥内的部分3D场景作为2D图像渲染在canvas中。
-
整个场景图就像一个树形结构,包括了多种对象:Scene对象、多种Mesh对象、Light对象、组、3D对象、Camera对象。一个Scene对象定义了整个场景图的根,包含了像background、fog这样的属性。这些对象定义了一个具有父子层级的树状结构,并且表示了这些对象的位置以及其方向。子节点的位置和方向都是相对于父节点的。就像轮胎与汽车的关系,当改变汽车的位置和方向时,轮胎的位置和方向也会自动改变。了解更多场景图的信息,可以查看the article on scenegraphs。
-
注意在图中,Camera是一半在图中一半在图外的。这表示在Three.js中,Camera不像其他对象,他不一定需要在场景图中才能工作。但Camera的位置和方向同样是相对于其父对象的。在the article on scenegraphs中的最后有放置多个Camera对象的例子。
-
Mesh对象表示用特定的Material去绘制特定的Geometry。Material对象和Geometry对象都可以被多个Mesh对象使用。比如在不同的位置去绘制两个蓝色的立方体,我们可以用两个Mesh对象去表示每个立方体的位置和方向。但我们只需要一个Geometry对象存放立方体的顶点数据,我们也只需要一个Material对象去确定蓝色。两个Mesh对象都可以去引用同一个Geometry对象和同一个Material对象。
-
Geometry对象代表了一些几何体的顶点数据,比如sphere、cube、plane、dog、cat、human、tree、building等等。Three.js提供了多种内置的基本几何体。你也可以创建自定义的几何体,同样也可以从文件加载一个。
-
Material对象表示用来绘制几何体的表面属性,包括像颜色以及光泽。一个Material同样可以引用一个或多个Texture对象,用来把图像裹在几何体的表面。
-
Texture对象通常表示从图像文件加载的图像、从canvas生成的图像、从另一个场景渲染的图像。
-
Light对象就表示多种灯光。
接下来我们就开始制作一个最小的"Hello Cube",如下图所示
首先我们加载Three.js
<script type="module">
import * as THREE from './resources/threejs/r119/build/three.module.js';
</script>
给script标签添加type="module"
是很重要的。它让我们能使用import
关键字去加载Three.js。也有其他的方式去加载Three.js但这种比较推荐。模块化的方式可以允许引入其他需要的模块。
下一步我们需要一个canvas标签。
<body>
<canvas id="c"></canvas>
</body>
我们要让Three.js去找到那个canvas。
<script type="module">
import * as THREE from './resources/threejs/r119/build/three.module.js';
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
...
</script>
我们找到canvas之后,创建一个WebGLRenderer。这个renderer就负责获取你提供的所有数据并且将其渲染到canvas上。在过去有其他的renderer比如CSSRenderer、CanvasRenderer,并且在以后将会有WebGL2Renderer或者WebGPURenderer。但现在我们使用WebGLRenderer,使用WebGL在canvas上去渲染3d。
注意这里有些细节。如果你不传一个canvas给Three.js,它就会自己创建一个,但你需要将其加到document中。在哪里添加取决于你的需求,如果你改变了需求,你需要去改动你的代码。所以我发现直接传给three.js一个canvas是更灵活的。我可以将canvas放在任何地方,但是这里代码不用去改。
接下来我们需要一个Camera。我们创建一个PerspectiveCamera。
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 5;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
fov
是field of view的缩写。在这个case里面,垂直方向是75度。
注意,Three.js中大部分是用弧度表示的,但perspectiveCamera是用角度。
aspect
就是canvas的显示部分。后续会详细讲解。默认情况下,canvas是300*150的像素比,所有aspect就是 300 / 150 也就是2。
near
和far
表示了camera前将渲染的空间。在这个范围之外的就会被剪切,不会被绘制。
以上四个设置决定了一个"截锥"。
near
和far
平面的高度是由fov
来决定的。而宽度则是由fov
和aspect
共同决定的。
所有在"截锥"中的东西都会被绘制,在之外的不会被绘制。
camera默认是看向-Z轴和+Y轴。立方体是放在原点,所有我们需要把camera移动到后面一点才能看到所有东西。
如图,我们可看到camera在z=2的位置。它看向-Z轴。我们的"截锥"是camera前面的0.1开始到5结束。
接下来我们创造一个Scene。一个Scene在Three.js中就是一种场景图的根。所有你想让Three.js绘制的都需要加到Scene中。更多细节在how scenes work in a future articl。
const scene = new THREE.Scene()
接着我们创建一个BoxGeometry,它含有一个box的数据。几乎所有想要展示在Three.js中的东西都需要一个定义了定点信息的Geometry去组成我们的3D对象。
const boxWidth = 1;
const boxHeight = 1;
const boxDepth = 1;
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
然后我们创建一个基本的Material并附上color。Colors可以使用标准CSS的样式6位16进制颜色值。
const material = new THREE.MeshBasicMaterial({color: 0x44aa88});
紧接着我们创建一个Mesh。一个Mesh表示以下三个东西的组合:
- Geometry(对象的形状)
- Material(怎么去绘制对象,光泽度,平坦度,什么颜色,应用什么材质等)
- 在Scene中相对于其父对象的位置、方向、比例。下面的代码中,父对象就是Scene。
const cube = new THREE.Mesh(geometry, material);
最后,我们将mesh加入到Scene中。
scene.add(cube);
我们现在可以在Scene中渲染了。调用renderer的render方法,把Scene和Camera传给它。
renderer.render(scene, camera);
现在我们就可以看到如下
我们现在还看不出来它是一个3D立方体,因为我们直接沿着-Z轴去观察的,我们只能看到一个面。
我们可以让它旋转起来。使用requestAnimationFrame
来进行循环渲染。
function render(time) {
time *= 0.001; // convert time to seconds
cube.rotation.x = time;
cube.rotation.y = time;
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
requestAnimationFrame
是想要动画化东西的使用对浏览器的请求。你传给它一个需要被调用的函数。在这个case里,这个函数是render。浏览器将在你更新任何与页面显示相关的内容的时候,就会调用这个函数,浏览也会重新渲染网页。
requestAnimationFrame
会传递从页面加载到我们的函数的时间。这个时间是以毫秒为单位的。将其变为秒比较方便。
接着我们同时设置一下立方体的x、y方向的旋转值。这些旋转值就是弧度制了。一个圆是2π,所以我们的立方体在各自轴上转一圈大概需要6.28秒。
接着我们渲染这个Scene并且请求另一个动画帧去继续我们的循环。
在循环之外我们调用一次requestAnimationFrame
去启动循环。
// TODO GIF
现在看上去好点了,但还是有点难分辨3d。我们需要加上一些灯光。Three.js中有许多类型的灯光,现在我们先创建一个directional light。
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
Directional lights有一个position和一个target。两个默认值都是0,0,0。在我们的例子中将position设到了-1,2,4。就在Camera后面稍微左边上面一点。target还是0,0,0,指向原点。
我们同样需要改变一下Material。现在的MeshBasicMaterial不会被灯光影响。把它改为会被灯光影响的MeshPhongMaterial。
const material = new THREE.MeshPhongMaterial({color: 0x44aa88}); // greenish blue
以下是我们新的场景图:
现在看上去就比较像3d的了。
做点更有意思的,我们再添加两个立方体。
我们使用同一个Geometry给每个立方体,但我们给不同的Material,让他们有不同的颜色。
我们新建一个函数生成特定颜色的Material。它来创建使用特定的Geometry的mesh,然后把它添加到scene中并设置它的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位置调用它三次,将这些Mesh实例放入一个数组里。
const cubes = [
makeInstance(geometry, 0x44aa88, 0),
makeInstance(geometry, 0x8844aa, -2),
makeInstance(geometry, 0xaa8844, 2),
];
最后我们把三个立方体在我们的render函数中旋转起来。我们给每一个算不同的旋转值。
function render(time) {
time *= 0.001; // convert time to seconds
cubes.forEach((cube, ndx) => {
const speed = 1 + ndx * .1;
const rot = time * speed;
cube.rotation.x = rot;
cube.rotation.y = rot;
});
...
左右两边的立方体有部分是在"截锥"外面了。并且因为fov太极端了,它们也有点变扭曲了。
现在的场景图:
如图所示,我们有三个Mesh对象,每个都引用了同一个BoxGeometry,引用了不同的MeshPhongMaterial使其有不同的颜色。