前言

这本Three.js开发指南:基于WebGL和HTML5在网页上渲染3D图形和动画(原书第3版)作者:[美]乔斯·德克森(Jos Dirksen) 是比较经典的three.js的入门书籍,时至今日已经过去了8年,这本书依旧是入门书籍的很好选择。本文可以看作本书的阅读笔记。

Three.js

在最近的几年中,浏览器的功能变得愈发强大,并且成为展现复杂的应用和图形的平台。然而其中大部分都是标准的二维图形。大多数现代浏览器已经支持WebGL,不仅可以在浏览器端创建二维应用和图形,而且可以通过GPU的功能创建好看并且运行良好的三维应用。然而,直接使用WebGL编程还是很复杂的。编程者需要知道WebGL的底层细节,并且学习复杂的着色语言来获得WebGL的大部分功能。

Three.js提供了一个很简单的关于WebGL特性的JavaScript API,所以用户不需要详细地学习WebGL,就能创作出好看的三维图形。Three.js为直接在浏览器中创建三维场景提供了大量的特性和API。

Three.js带来的好处有以下几点:

  • 创建简单和复杂的三维几何图形。
  • 创建虚拟现实(VR)和增强现实(AR)场景。
  • 在三维场景下创建动画和移动物体。
  • 为物体添加纹理和材质。
  • 使用各种光源来装饰场景。
  • 加载三维模型软件所创建的物体。
  • 为三维场景添加高级的后期处理效果。
  • 使用自定义的着色器。
  • 创建点云(即粒子系统)。

获取源码

Github仓库的访问地址:josdirksen/learning-threejs: Code repository for the examples from the Packt book “Learning Threejs” (github.com)

image-20230206232252608

libs下面包含了three.js框架。

搭建HTML框架

创建一个空的HTML框架,后面会经常用得到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!DOCTYPE html>

<html>

<head>
<title>Example 01.01 - Basic skeleton</title>
<script type="text/javascript" src="../libs/three.js"></script>
<style>
body {
/* set margin to 0 and overflow to hidden, to
use the complete page */
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>

<!-- Div which will hold the Output -->
<div id="WebGL-output">
</div>

<!-- Javascript code that runs our Three.js examples -->
<script type="text/javascript">

// once everything is loaded, we run our Three.js stuff.
function init() {
// here we'll put the Three.js stuff
}
window.onload = init

</script>
</body>
</html>

框架包含基本的HTML网页,在标签引入了需要使用的外部Javascript库,three.js不用多说是必须引入的。这里引入了TrackballControls.js库,便于使用鼠标任意移动摄像机,方便从不同角度观察。

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

创建场景

一个场景想要显示任何东西,需要三种类型的组件:

组件 说明
摄像机 决定屏幕上哪些东西需要渲染
光源 决定材质如何显示以及如何用于产生阴影
对象 摄像机透视图里主要的渲染对象
渲染器 基于摄像机和场景提供的信息,调用底层图形API执行真正的场景绘制工作

场景的基本功能

下面将会通过代码示例来解释如何创建场景并渲染三维对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
<!DOCTYPE html>

<html>

<head>
<title>Example 01.02 - First Scene</title>
<script type="text/javascript" src="../libs/three.js"></script>
<style>
body {
/* set margin to 0 and overflow to hidden, to go fullscreen */
margin: 0;
overflow: hidden;
}
</style>
</head>
<body>

<!-- Div which will hold the Output -->
<div id="WebGL-output">
</div>

<!-- Javascript code that runs our Three.js examples -->
<script type="text/javascript">

// once everything is loaded, we run our Three.js stuff.
function init() {

// create a scene, that will hold all our elements such as objects, cameras and lights.
// 创建场景
const scene = new THREE.Scene();

// create a camera, which defines where we're looking at.
// 创建相机
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);

// create a render and set the size
// 创建渲染器并设置大小
const renderer = new THREE.WebGLRenderer();
renderer.setClearColorHex();
renderer.setClearColor(new THREE.Color(0xEEEEEE));
renderer.setSize(window.innerWidth, window.innerHeight);

// show axes in the screen
// 显示坐标系
const axes = new THREE.AxisHelper(20);
scene.add(axes);

// create the ground plane
// 创建平面
const planeGeometry = new THREE.PlaneGeometry(60, 20);
const planeMaterial = new THREE.MeshBasicMaterial({color: 0xcccccc});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);

// rotate and position the plane
plane.rotation.x = -0.5 * Math.PI;
plane.position.x = 15;
plane.position.y = 0;
plane.position.z = 0;

// add the plane to the scene
scene.add(plane);

// create a cube
// 创建立方体
const cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
const cubeMaterial = new THREE.MeshBasicMaterial({color: 0xff0000, wireframe: true});
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);

// position the cube
cube.position.x = -4;
cube.position.y = 3;
cube.position.z = 0;

// add the cube to the scene
scene.add(cube);

// create a sphere
// 创建球形
const sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
const sphereMaterial = new THREE.MeshBasicMaterial({color: 0x7777ff, wireframe: true});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);

// position the sphere
sphere.position.x = 20;
sphere.position.y = 4;
sphere.position.z = 2;

// add the sphere to the scene
scene.add(sphere);

// position and point the camera to the center of the scene
camera.position.x = -30;
camera.position.y = 40;
camera.position.z = 30;
camera.lookAt(scene.position);

// add the output of the renderer to the html element
document.getElementById("WebGL-output").appendChild(renderer.domElement);

// render the scene
// 渲染场景
renderer.render(scene, camera);
}
window.onload = init;

</script>
</body>
</html>

在浏览器中将示例打开,显示效果如下:

image-20230213003400305

来看看示例代码做了些什么:

首先定义了场景(scene)、摄像机(camera)和渲染器(renderer)对象。场景是一个容器,主要用于保存、跟踪所要渲染的物体和使用的光源。如果没有THREE.Scene对象,那么Three.js就无法渲染任何物体。摄像机决定了能够在场景看到什么。渲染器对象会基于摄像机的角度来计算场景对象在浏览器中会渲染成什么样子。最后WebGLRenderer将会使用电脑显卡来渲染场景。

在示例中,调用setClearColor方法将场景的背景颜色设置为接近黑色(new THREE.Color(0X00000000)),并通过setSize方法设置场景的大小。使用window.innerWidthwindow.innerHeight可将整个页面窗口指定为渲染区域。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

// show axes in the screen
// 创建坐标系
const axes = new THREE.AxisHelper(20);
scene.add(axes);

// create the ground plane
// 创建平面
const planeGeometry = new THREE.PlaneGeometry(60, 20);
const planeMaterial = new THREE.MeshBasicMaterial({color: 0xcccccc});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);

// rotate and position the plane
plane.rotation.x = -0.5 * Math.PI;
plane.position.x = 15;
plane.position.y = 0;
plane.position.z = 0;

// add the plane to the scene
scene.add(plane);

上述创建了坐标轴(axes)对象并设置轴线的粗细值为20,最后调用scene.add方法将轴添加到场景中。接下来要创建平面(plane),平面的创建分为两步来完成。首先,使用THREE.Plane Geometry(60,20)来定义平面的大小,在示例中将宽度设置为60,高度设置为20。除了设置高度和宽度,还需要设置平面的外观(比如颜色和透明度),在Three.js中通过创建材质对象来设置平面的外观。在本例中,我们将会创建颜色为0xAAAAAA的基本材质(THREE.MeshBasicMaterial)。然后,将大小和外观组合进Mesh对象并赋值给平面变量。在将平面添加到场景之前,还需要设置平面的位置:先将平面围绕x轴旋转90度,然后使用position属性来定义其在场景中的位置。

使用同样的方式将方块和球体添加到平面中,但是需要将线框(wireframe)属性设置为true,这样物体就不会被渲染为实体物体。

为了确保所要渲染的物体能够被摄像机拍摄到,使用lookAt方法指向场景的中心,默认状态下摄像机是指向(0,0,0)位置的。最后需要做的就是将渲染的结果添加到HTML框架的<div>元素中。我们使用JavaScript来选择需要正确输出的元素并使用appendChild方法将结果添加到div元素中。最后告诉渲染器使用指定的摄像机来渲染场景。

添加材质、光源和阴影效果

在上一节代码的基础上,首先我们在场景中添加一个光源:

1
2
3
4
5
6
7
8
// 添加光源
const spotLight = new THREE.SpotLight(0xFFFFFF);
spotLight.position.set(-40, 40, -15);
spotLight.castShadow = true;
spotLight.shadow.mapSize.width = 1024;
spotLight.shadow.mapSize.height = 1024;
spotLight.shadow.camera.far = 130;
spotLight.shadow.camera.near = 40;

通过THREE.SpotLight定义光源并从其位置(spotLight.position.set(-40,60,-10))照射场景。通过将castShadow属性设置为true,THREE.js的阴影功能被启用。此外,上面的代码还通过设置shadow.mapSizeshadow.camera.farshadow.camera.near三个参数来控制阴影的精细程度。如果这个时候渲染场景,你会发现和没有添加光源时没有什么变化。如果这时候渲染场景,那么你看到的结果和没有添加光源时是没有区别的。这是因为不同的材质对光源的反应是不一样的。基本材质(THREE.MeshBasicMaterial)不会对光源有任何反应,基本材质只会使用指定的颜色来渲染物体。所以,接下来需要改变平面、球体和立方体的材质:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// create the ground plane
// 创建平面
const planeGeometry = new THREE.PlaneGeometry(60, 20);
// const planeMaterial = new THREE.MeshBasicMaterial({color: 0xcccccc});
const planeMaterial = new THREE.MeshLambertMaterial({color: 0xcccccc});
const plane = new THREE.Mesh(planeGeometry, planeMaterial);

...

// create a cube
// 创建立方体
const cubeGeometry = new THREE.BoxGeometry(4, 4, 4);
const cubeMaterial = new THREE.MeshLambertMaterial({color: 0xff0000});
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);

...

// create a sphere
// 创建球形
const sphereGeometry = new THREE.SphereGeometry(4, 20, 20);
const sphereMaterial = new THREE.MeshLambertMaterial({color: 0x7777ff});
const sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);