threejs
构成
主要构成:
- 渲染器(renderer)
- 场景(scene):基本组成部分,需要绘制的东西都需要加入到scene中。
- 网格(mesh):用一种特定的材质(material)来绘制的一个特定的几何体(geometry)
- 光源(light):表示不同种类的光
- 群组(group)
- 三维物体(object3d)
- 相机(camera):不一定要在场景图中才起作用,作为其他对象的子对象,会继承它父对象的位置和朝向。
- 几何体(geometry):标识三维物体(球体、立方体、平面、猫、树、建筑等)的顶点信息。
- 材质(material):代表绘制几何体的表面属性,包括颜色、光亮程度等。一个材质可以引用一个或多个纹理(texture)。
- 纹理(texture):表示一个图像
- 场景(scene):基本组成部分,需要绘制的东西都需要加入到scene中。
基本概念
threejs需要使用canvas标签进行绘制,所以要先获取它然后传给threejs。如果没有canvas标签,threejs会自动创建一个。
javascript
import * as THREE from 'three';
function main () {
const canvas = document.querySelector('#c');
// 创建一个渲染器
const renderer = new THREE.WebGLRenderer({
antialias: true,
canvas
});
// 创建一个透视摄像机
const fov = 75; // 视角
const aspect = 2; // 画布宽高比
const near = 0.1; // 近平面(小于这个值的物体不会被渲染)
const far = 5; // 远平面(大于这个值的物体不会被渲染)
// 这四个参数定义了一个视锥(frustum),即削去顶部的金字塔
// 摄像机默认指向z轴负方向,上方向朝向y轴正方向
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
// 所以需要往后移动摄像机才会看到物体
camera.position.z = 2; // 相机位置
// 创建一个场景
const scene = new THREE.Scene();
// 创建一个包含盒子信息的立方几何体(BoxGeometry)
// 几乎所有希望在threejs中显示的物体都需要一个包含了组成三维物体的顶点信息的几何体(geometry)
const boxWidth = 1; // 盒子宽度
const boxHeight = 1; // 盒子高度
const boxDepth = 1; // 盒子深度
const geometry = new THREE.BoxGeometry(boxWidth, boxHeight, boxDepth);
// --------------- 单个物体的创建 start
// 创建一个材质(用于绘制几何体的表面属性,比如颜色)
// const material = new THREE.MeshBasicMaterial({ color: 0x44aa88 }); // 0x44aa88是一个十六进制数,代表一个颜色
// 切换材质,便于光照效果观察
const material = new THREE.MeshPhongMaterial({ color: 0x44aa88 }); // 0x44aa88是一个十六进制数,代表一个颜色
// 创建一个网格(mesh),它包含了一个几何体和一个材质
const cube = new THREE.Mesh(geometry, material);
// 把网格添加到场景中
scene.add(cube);
// 渲染,将场景和相机作为参数传给渲染器,渲染出整个场景
renderer.render(scene, camera);
// 截止到这里,场景中就有一个蓝色的立方体,但是我们看到的确实一个面,为了看起来像一个立方体,需要进行一个循环渲染
function render (time) {
time *= 0.001; // 将时间转换为秒
// 旋转
cube.rotation.x = time; // x轴旋转
cube.rotation.y = time; // y轴旋转
// 渲染
renderer.render(scene, camera);
// 循环渲染
requestAnimationFrame(render);
}
// 该方法会告诉浏览器你希望执行一个动画,如果更新了跟页面显示有关的内容,就会调用该方法(renderer.render)重新渲染页面
requestAnimationFrame(render);
// --------------- 单个物体的创建 end
// --------------- 多个物体的创建 start
function makeCubeInstance (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;
}
const cubes = [
makeCubeInstance(geometry, 0x44aa88, 0), // 0x44aa88是一个十六进制数,代表一个颜色
makeCubeInstance(geometry, 0x8844aa, -2), // 0x44aa88是一个十六进制数,代表一个颜色
makeCubeInstance(geometry, 0xaa8844, 2) // 0x44aa88是一个十六进制数,代表一个颜色
]
function render (time) {
time *= 0.001; // 将时间转换为秒
cubes.forEach((cube, ndx) => {
const speed = 1 + ndx * .1; // 速度
const rot = time * speed; // 旋转
cube.rotation.x = rot; // x轴旋转
cube.rotation.y = rot; // y轴旋转
})
renderer.render(scene, camera); // 渲染
requestAnimationFrame(render); // 循环渲染
}
requestAnimationFrame(render);
// --------------- 多个物体的创建 end
// 截止到这里,可以看到一个旋转的立方体,但是还是很难看出是三维的
// 我们需要添加光照效果,让立方体的面产生光照,才能看到
const color = 0xffffff; // 白色
const intensity = 3; // 光照强度
// 平行光有一个灯光位置和目标点,默认为0,0,0
const light = new THREE.DirectionalLight(color, intensity); // 平行光
light.position.set(-1, 2, 4); // 设置灯光位置
scene.add(light); // 将光照添加到场景中
// 截止到此处,仍然存在问题:即立方体被拉伸,立方体过于模糊
// 解决拉伸问题:
// 将相机的宽高比设置为画布的宽高比
function render (time) {
time *= 0.001; // 将时间转换为秒
// 解决模糊问题:
if (resizeRendererToDisplaySize(renderer)) { // 如果需要重新设置渲染器的尺寸
const canvas = renderer.domElement; // 获取canvas
camera.aspect = canvas.clientWidth / canvas.clientHeight; // 设置相机的宽
camera.updateProjectionMatrix(); // 更新相机的投影矩阵
}
// 解决拉伸问题:
// 将相机的宽高比设置为画布的宽高比
// const canvas = renderer.domElement; // 获取canvas
// camera.aspect = canvas.clientWidth / canvas.clientHeight; // 设置相机的宽
// camera.updateProjectionMatrix(); // 更新相机的投影矩阵
cubes.forEach((cube, ndx) => {
const speed = 1 + ndx *.1; // 速度
const rot = time * speed; // 旋转
cube.rotation.x = rot; // x轴旋转
cube.rotation.y = rot; // y轴旋转
})
renderer.render(scene, camera); // 渲染
requestAnimationFrame(render); // 循环渲染
}
requestAnimationFrame(render);
// 解决模糊问题:
// 编写函数判断渲染器的canvas尺寸和canvas的显示尺寸是否一致,如果不一致,就重新设置渲染器的尺寸
function resizeRendererToDisplaySize (renderer) {
const canvas = renderer.domElement; // 获取canvas
const width = canvas.clientWidth; // 获取canvas的宽度
const height = canvas.clientHeight; // 获取canvas的高度
const needResize = canvas.width !== width || canvas.height !== height; // 判断canvas的宽度和高度是否一致
if (needResize) { // 如果不一致,就重新设置渲染器的尺寸
renderer.setSize(width, height, false); // 设置渲染器的尺寸
}
return needResize; // 返回是否需要重新设置渲染器的尺寸
}
// 另一种方法
function resizeRendererToDisplaySize (renderer) {
const canvas = renderer.domElement; // 获取canvas
const pixelRatio = window.devicePixelRatio; // 获取设备像素比
const width = Math.floor(canvas.clientWidth * pixelRatio); // 获取canvas的宽度
const height = Math.floor(canvas.clientHeight * pixelRatio); // 获取canvas的高度
const needResize = canvas.width!== width || canvas.height!== height; // 判断canvas的宽度和高度是否一致
if (needResize) { // 如果不一致,就重新设置渲染器的尺寸
renderer.setSize(width, height, false); // 设置渲染器的尺寸
}
return needResize; // 返回是否需要重新设置渲染器的尺寸
}
}
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
材质
材质定义了对象在场景中的外形。
threejs中常用的材质有:
简单的数学模型:
- MeshBasicMaterial:不受光照影响的基础材质
- MeshLambertMaterial:受光照影响的基础材质(只在顶点计算光照)
- MeshPhongMaterial:受光照影响的基础材质(在每个像素计算光照)支持镜面高光
- shininess:决定了镜面高光的光泽度,默认为30
- MeshToonMaterial: 和meshPhongMaterial类似,但是不是平滑着色,而是使用渐变图进行着色,让其看起来有两种色调
复杂的数学(接近真实的物理模型):
- MeshStandardMaterial:受光照影响的基础材质(在顶点和片段计算光照)
- roughness:决定了材质的粗糙程度,0为完全光滑,1为完全粗糙
- metalness:决定了材质的金属度,0为非金属,1为金属
- MeshPhysicalMaterial:受光照影响的基础材质(在顶点和片段计算光照),参数和MeshStandardMaterial类似,但增加了一个透明度参数
- clearcoat:决定了要涂抹的清漆光亮层的程度
- clearcoatRoughness:决定了光泽层的粗糙程度
- MeshStandardMaterial:受光照影响的基础材质(在顶点和片段计算光照)
特殊用途的材质:
- ShadowMaterial:用于创建阴影
- MeshDepthMaterial:用于创建深度图
- MeshNormalMaterial:用于创建法线图
- ShaderMaterial:用于创建着色器,制作自定义材质(通过threejs)
- RawShaderMaterial:用于创建着色器,制作完全自定义材质(不通过threejs)
设置材质属性的方式有:
javascript
// 1. 直接设置
const material = new THREE.MeshBasicMaterial({
color: 0x44aa88, // 0x44aa88是一个十六进制数,代表一个颜色
flatShading: true, // 平面着色,让每个平面都渲染颜色,让其更有立体感
side: THREE.DoubleSide, // 双面渲染,让物体的背面也渲染颜色
});
// 2. 先创建一个材质,然后再设置
const material = new THREE.MeshBasicMaterial(); // 创建一个材质
material.color.set(0x44aa88); // 设置颜色
material.flatShading = true; // 设置平面着色
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
其中,color的设置方式有:
javascript
// set 十六进制
material.color.set(0x44aa88);
// set css颜色字符串
material.color.set('red');
material.color.set('#ff0000');
material.color.set('rgb(255, 0, 0)');
material.color.set('hsl(0, 100%, 50%)');
// setHSL 色相、饱和度、亮度
material.color.setHSL(0, 1, 0.5);
// setRGB 红色、绿色、蓝色
material.color.setRGB(1, 0, 0);
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
纹理
使用纹理是有隐性成本的,纹理是threejs应用中使用内存最多的部分,一般来说,纹理会占用宽度 * 高度 * 4 * 1.33
个字节的内存。例如一张像素3024 * 3721的图片,占用的内存为3024 * 3721 * 4 * 1.33
个字节,约为60MB。所以应该让纹理文件和大小尽可能的小,尽可能的压缩。
纹理的六种形态(texture.minFilter
, texture.magFilter
):
NearestFilter
:在纹理中选择最近的像素LinearFilter
:在纹理中选择4个像素,然后进行混合NearestMipmapNearestFilter
:选择合适的mip,然后选择一个像素NearestMipmapLinearFilter
:选择2个mip,从每个mip各选一个像素混合LinearMipmapNearestFilter
:选择合适的mip,然后选择4个像素混合LinearMipmapLinearFilter
:选择2个mip,从每个mip各选4个像素混合
纹理常用属性:
- 重复:设置重复使用
repeat.set(x, y)
,开启两个方向的包裹使用:wrapS(水平包裹),wrapT(垂直包裹):RepeatWrapping
:重复ClampToEdgeWrapping
:每条边的最后一个像素无限重复MirroredRepeatWrapping
:镜像重复
- 偏移:使用
offset.set(x, y)
,纹理偏移的单位是以纹理的大小作为单位的 - 旋转:使用
rotation = THREE.MathUtils.degToRad(45)
,旋转的单位是以弧度为单位的
光照
光源类型:
- 环境光(AmbientLight):没有方向,无法产生阴影。作用是提亮场景。场景内的物体看起来没有立体感,简单的将材质颜色和光照颜色进行叠加,再乘以光照强度
- 半球光(HemisphereLight):基本上无立体感。颜色是从天空到地面两个颜色之间的渐变,与物体材质的颜色叠加后得到。面向正上方受到天空的光照,面向下方受到地面的光照,其他角度是两个颜色渐变区间的混合。与其他类型光照结合使用,可以很好的体现天空和地面颜色照射到物体上的效果
- 方向光(DirectionalLight):常用来表现太阳光照的效果
- 点光源(PointLight):从一个点朝各个方向发射出光线的光源
- 聚光灯(SpotLight):可以看成一个点光源被一个圆锥体限制了光照的范围
- 矩形区域光(RectAreaLight):一个矩形区域发射出来的光照,例如日光灯
摄像机
摄像机分类:
- 透视摄像机(PerspectiveCamera):定义了一个视锥(frustum),即削去顶部的三角锥,或者说实心金字塔型(solid pyramid),包括立方体、圆锥、球、圆柱、锥台。通过四个参数定义一个视锥,near和far定义了近平面和远平面,fov是视野,定义的是视锥前端和后端的高度,aspect定义了视锥前端和后端的宽度,实际上的宽度是通过高度乘以aspect计算出来的。
- 正交摄像机(OrthographicCamera):需要设置left、right、top、bottom、near、far六个参数,定义了一个长方体,使得视野是平行的而不是透视的,主要用于绘制2d图像