three.js 基础

Three.js 简介

随着 HTML5 标准的颁布,以及主流浏览器的功能日益强大,直接在浏览器中展示三维图像和动画已经变得越来越容易。WebGL 技术为在浏览器中创建丰富的三维图形提供了丰富且
强大的接口,它们不仅可以创建二维图形和应用,还可以充分利用 GPU,创建漂亮的、高性能的三维应用。但是直接使用 WebGL 编程非常复杂,需要了解 WebGL 的内部细节,学习
复杂的着色器语法和足够多的数学和图形学方面的专业知识才能学好 WebGL。
Three.js 就是一款基于原生 WebGL 封装运行在浏览器中的 3D 引擎,可以用它创建各种三维场景,包括了摄影机、光影、材质等各种对象。使用 Three.js 不必学习 WebGL 的
详细细节,就能轻松创建出漂亮的三维图形。

Three.js 长短单位默认是米,时间是秒,除过相机的fov 是角度其余都是弧度

Scene

Scene 是场景对象,所有的网格对象、灯光、动画等都需要放在场景中,使用 new THREE.Scene 初始化场景,下面是场景的一些常用属性和方法。

  1. fog:设置场景的雾化效果,可以渲染出一层雾气,隐层远处的的物体。
    Fog(color, near, far)
    color: 表示雾的颜色,如设置为白色,场景中远处物体为蓝色,场景中最近处距离物体是自身颜色,最远和最近之间的物体颜色是物体本身颜色和雾颜色的混合效果。
    near:表示应用雾化效果的最小距离,距离活动摄像机长度小于 near 的物体将不会被雾所影响。
    far:表示应用雾化效果的最大距离,距离活动摄像机长度大于 far 的物体将不会被雾所影响。
  2. overrideMaterial:强制场景中所有物体使用相同材质。
  3. autoUpdate:设置是否自动更新。
  4. background:设置场景背景,默认为黑色。
  5. children:所有对象的列表。
  6. add():向场景中添加对象。
  7. remove():从场景中移除对象。
  8. getChildByName():根据名字直接返回这个对象。
  9. traverse():传入一个回调函数访问所有的对象。

在 react 里 使用 THREE

利用React Hooks 封装 THREE,方便使用

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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
import { useCallback, useEffect, useRef } from "react";
// 0.144 版没有默认导出,需要什么导出什么
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
import _ from "lodash";
import Stats from "stats.js";
import { GUI } from "dat.gui";

/** 非常重要 需要通过gui控制必须要给 mesh 或 group 设置 name 属性, 并且 key 里要包含 name 属性
* key 可以是 ‘,’ 逗号分隔的对象 第一个是父对象的名称,第二个是子对象名称或属性名 */
export type guiChangePropertyType = { [key: string]: { name: string; min?: number; max?: number; step?: number } | { name: string; min?: number; max?: number; step?: number }[] };


//自定义hooks
function useTHREE(props: {
//画布的父容器
placeRenderingRef: React.RefObject<HTMLElement>;
//是否使用辅助轴
isUseAxes?: boolean;
//是否使用相机轨道控制器
isUseOrbitControls?: boolean;
//是否使用性能监控
isUsePerformanceMonitor?: boolean;
//是否使用辅助相机
isUseAssistCamera?: boolean;
/** dat.gui 工具可以改变的属性 */
guiChangePropertys?: guiChangePropertyType;
/** 点击画布的元素拾取 mesh */
clickHandle?: (clickMesh: (THREE.Mesh | THREE.Group)[]) => void;
/**相机轨道控制器变化了 */
orbitControlsChange?: () => void;
}) {
//渲染器
const renderer = useRef<THREE.WebGLRenderer>();
//场景
const scene = useRef<THREE.Scene>();
//相机
const camera = useRef<THREE.Camera>();
//辅助相机
const assistCamera = useRef<THREE.Camera>();
//辅助轴
const axesHelper = useRef<THREE.AxesHelper>();

//THREE 提供的对象可以计算俩次渲染的时间差
const clock = useRef<THREE.Clock>();
//性能监视器
const starts = useRef<Stats>();

const orbitControls = useRef<OrbitControls>();

//在页面加载的时候的初始动画
const primaryAnimation = useRef<Map<string, Function>>();

//根据传入的 json 配置 dat.ui 界面
useEffect(() => {
if (!props.guiChangePropertys) return;

const func = () => {
const gui = new GUI({ closed: false });
const allPropertysArr = _.keys(props.guiChangePropertys);

allPropertysArr.forEach((p) => {
const objs = p.split(",");
//获取mesh 对象
// eslint-disable-next-line @typescript-eslint/no-unused-vars

//嵌套获取对象
let object3D: THREE.Object3D | null = null;

objs.forEach((p, index) => {
try {
object3D = index === 0 ? (scene.current?.getObjectByName(p) as THREE.Object3D) : _.has(object3D, p) ? _.get(object3D, p) : (object3D?.getObjectByName(p) as THREE.Object3D);
} catch (error) {
return;
}
});

if (!object3D) return;

//如果是数组说明是一个属性有多个子属性
if (props.guiChangePropertys && _.isArray(props.guiChangePropertys[p])) {
const folder = gui.addFolder(p);
folder.open();
const obj = props.guiChangePropertys[p] as { name: string; min?: number; max?: number; step?: number }[];
//展开子属性
obj.forEach((q) => {
if (q.name === "color") {
folder.addColor(object3D as THREE.Object3D, q.name);
} else {
if (_.isFunction(_.get(object3D, q.name))) {
const bindParameter = {
[q.name]: 0,
};
const tmp = folder.add(bindParameter, q.name, q.min, q.max, q.step).listen();
tmp.onFinishChange((value) => {
const tmp = _.get(object3D, q.name) as Function;
tmp.call(object3D, value);
});
} else folder.add(object3D as THREE.Object3D, q.name, q.min, q.max, q.step).listen();
}
});
// 不是数组
} else {
const obj = props.guiChangePropertys && (props.guiChangePropertys[p] as { name: string; min?: number; max?: number; step?: number });
if (!obj) return;
if (obj.name === "color") {
gui.addColor(object3D as THREE.Object3D, obj.name);
} else {
if (_.isFunction(_.get(object3D, obj.name))) {
const bindParameter = {
[obj.name]: 0,
};
const tmp = gui.add(bindParameter, obj.name, obj.min, obj.max, obj.step).listen();
tmp.onFinishChange((value) => {
const tmp = _.get(object3D, obj.name) as Function;
tmp.call(object3D, value);
});
} else gui.add(object3D as THREE.Object3D, obj.name, obj.min, obj.max, obj.step).listen();
}
}
});
};
window.onload = func;
}, [props.guiChangePropertys]);

useEffect(() => {
clock.current = new THREE.Clock();
}, []);

//初始化性能监视器
const initStarts = useCallback((placeRenderingRef: React.RefObject<HTMLElement>) => {
var tmp = new Stats();
//默认显示的界面,点击的时候可以切换
tmp.showPanel(0); // 0: fps, 1: ms, 2: mb, 3+: custom
// 将stats的界面对应左上角
tmp.dom.style.position = "absolute";
tmp.dom.style.left = "0px";
tmp.dom.style.top = "0px";
placeRenderingRef.current && (placeRenderingRef.current as HTMLElement).appendChild(tmp.dom);
starts.current = tmp;
}, []);

//配置相机
const configCamera = useCallback((fov: number, cameraPosition: THREE.Vector3, cameraLookAt: THREE.Vector3) => {
if (camera.current instanceof THREE.PerspectiveCamera) {
(camera.current as THREE.PerspectiveCamera).fov = fov;
}
//相机的位置
camera.current?.position.set(cameraPosition.x, cameraPosition.y, cameraPosition.z);
//相机看向的位置
camera.current?.lookAt(cameraLookAt);

//如果有辅助相机,一并更新
if (assistCamera.current instanceof THREE.PerspectiveCamera) {
(assistCamera.current as THREE.PerspectiveCamera).fov = fov;
}
assistCamera.current?.position.set(cameraPosition.x, cameraPosition.y, cameraPosition.z);
assistCamera.current?.lookAt(cameraLookAt);
}, []);

//增加几何体
const addObjectToScene = useCallback(
(mesh: THREE.Object3D[]) =>
mesh.forEach((p) => {
scene.current && scene.current.add(p);
}),
[]
);
//增加各种光源
const addLightsToScene = useCallback(
(lights: THREE.Light[]) =>
lights.forEach((p) => {
if (p instanceof THREE.AmbientLight) {
//环境光不能有俩个
const tmp1 = scene.current?.children.find((q) => q instanceof THREE.AmbientLight);
!tmp1 && scene.current && scene.current.add(p);
} else {
//相同位置不能添加俩次
const tmp2 = scene.current?.children.find((q) => {
return _.isEqual(p.position, q.position);
});
!tmp2 && scene.current && scene.current.add(p);
}
}),
[]
);

//设置初始动画
const configPrimaryAnimation = useCallback((animation: Map<string, Function>) => {
primaryAnimation.current = animation;
}, []);

//执行渲染
const render = useCallback(() => {
//clear 的效果还不知道
// renderer.current && renderer.current?.clear();
primaryAnimation.current &&
primaryAnimation.current.forEach((p) => {
p && p();
});
// 以每秒60次的频率来绘制场景。requestAnimationFrame这个函数,它用来替代 setInterval,
//这个新接口具备多个优点,比如浏览器Tab切换后停止渲染以节约资源、和屏幕刷新同步避免无效刷新、
//在不支持该接口的浏览器中能安全回退为setInterval。
scene.current && camera.current && renderer.current && renderer.current?.render(scene.current, camera.current);
starts.current?.update();

requestAnimationFrame(render);
}, []);

//页面加载的时候生成渲染器
useEffect(() => {
// 需要控制相机的宽高比和屏幕的保持一致,near 尽可能小 far 尽可能大,方便进行缩放,这里的值没有单位是相对的
// 场景渲染用透视相机,符合人眼的近大远小
!camera.current && (camera.current = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 10000));
!scene.current && (scene.current = new THREE.Scene());
!renderer.current &&
(renderer.current = new THREE.WebGLRenderer({
//抗锯齿
antialias: true,
}));
//设置场景的颜色
renderer.current && renderer.current.setClearColor("white");
renderer.current && (renderer.current.pixelRatio = window.devicePixelRatio);
//设置平面网格
const gridHelper = new THREE.GridHelper(80, 30, 0x888888, 0x888888);
gridHelper.position.set(0, 0, 0);
scene.current.add(gridHelper);
}, []);

//点击获取几何体
useEffect(() => {
function onDocumentMouseDown(event: { clientX: number; clientY: number }) {
if (!camera.current || !props.placeRenderingRef.current || !scene.current) return;

//鼠标是二维向量
let mouse = new THREE.Vector2();
//射线是相机到鼠标点击位置之间的一条线
let ray = new THREE.Raycaster();

const px = props.placeRenderingRef.current.getBoundingClientRect().left;
const py = props.placeRenderingRef.current.getBoundingClientRect().top;
//鼠标点位的坐标转换为3D 坐标
//通过鼠标点击位置,计算出 raycaster 所需点的位置,映射到,以画布为中心点,范围 -1 到 1的位置去
//以下的公式是等比列函数换算的
mouse.x = ((event.clientX - px) / props.placeRenderingRef.current.offsetWidth) * 2 - 1;
mouse.y = -((event.clientY - py) / props.placeRenderingRef.current.offsetHeight) * 2 + 1;

//通过鼠标点击的位置和当前相机的矩阵计算出射线位置
ray.setFromCamera(mouse, camera.current);

// intersectObjects 第一个参数是一个数组 传 场景(scence)的各个对象 第二个参数 是否检查后代对象
// 有些模型是是在group 组里,需要检查后代对象
const intersects = _.unionBy(ray.intersectObjects([...scene.current.children.filter((p) => p.type === "Mesh" || p.type === "Group")], true), "uuid");

/*鼠标点和相机之间射线上的所有对象
这将返回一个 Array,其中包含与cubes的孩子的所有光线交点,按距离排序(最近的对象排在第一位)。每个交集都是一个具有以下属性的对象:
距离:相交发生的距离相机多远
点:光线与其相交的对象中的确切点
面:相交的面。
对象:与哪个对象相交 */
if (intersects.length > 0) {
const meshs = intersects.filter((q) => q.object.type === "Mesh").map((q) => q.object as THREE.Mesh);
const groups = intersects.filter((q) => q.object.type === "Group").map((q) => q.object as THREE.Group);
props.clickHandle && props.clickHandle([...meshs, ...groups]);
}
}
if (props.placeRenderingRef.current && !props.placeRenderingRef.current.onclick) {
const ref = props.placeRenderingRef.current;
ref.addEventListener("click", onDocumentMouseDown);
return () => {
ref.removeEventListener("click", onDocumentMouseDown);
};
}
}, [props, props.clickHandle, props.placeRenderingRef]);

//容器大小变化后,刷新画布
useEffect(() => {
const func = () => {
const ele = props.placeRenderingRef.current as HTMLElement;
//尺寸变化了,必须要更新相机
camera.current && ((camera.current as THREE.PerspectiveCamera).aspect = window.innerWidth / window.innerHeight);
camera.current && (camera.current as THREE.PerspectiveCamera).updateProjectionMatrix();
renderer.current && (renderer.current.pixelRatio = window.devicePixelRatio);
renderer.current && renderer.current.setSize(ele.clientWidth, ele.clientHeight);
//窗口大小变化后立马刷新下
renderer.current && scene.current && camera.current && renderer.current.render(scene.current, camera.current);
};
window.addEventListener("resize", func);
return () => {
window.removeEventListener("resize", func);
};
}, [props.placeRenderingRef]);

useEffect(() => {
//配置辅助相机
if (scene.current && props.isUseAssistCamera && !assistCamera.current) {
assistCamera.current = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 10000);
scene.current.add(assistCamera.current);
const cameraHelper = new THREE.CameraHelper(assistCamera.current);
scene.current.add(cameraHelper);
cameraHelper.update();
}

//显示辅助轴
if (scene.current && props.isUseAxes && !axesHelper.current) {
axesHelper.current = new THREE.AxesHelper(800);
scene.current.add(axesHelper.current);
}

//显示相机轨道控制器
if (renderer.current && camera.current) {
if (orbitControls.current && props.isUseOrbitControls) {
orbitControls.current.enabled = true;
return;
}
if (orbitControls.current && !props.isUseOrbitControls) {
orbitControls.current.enabled = false;
return;
}
if (!orbitControls.current && props.isUseOrbitControls) {
orbitControls.current = new OrbitControls(camera.current, renderer.current.domElement);
orbitControls.current.enableDamping = true;
orbitControls.current.addEventListener("change", (e) => {
props.orbitControlsChange && props.orbitControlsChange();
scene.current && camera.current && renderer.current && renderer.current?.render(scene.current, camera.current);
});
}
}
}, [props, props.isUseAssistCamera, props.isUseAxes, props.isUseOrbitControls, render]);

//渲染到dom
useEffect(() => {
if (renderer.current && props.placeRenderingRef.current) {
if ((props.placeRenderingRef.current as HTMLElement).childElementCount === 0) {
const ele = props.placeRenderingRef.current as HTMLElement;
//设置画布初始大小和容器一样大
renderer.current.setSize(ele.clientWidth, ele.clientHeight);
ele.appendChild(renderer.current.domElement);
}
!starts.current && props.isUsePerformanceMonitor && initStarts(props.placeRenderingRef);
}
}, [initStarts, props.isUsePerformanceMonitor, props.placeRenderingRef]);
//返回
return { configCamera, configPrimaryAnimation, addObjectToScene, addLightsToScene, camera, render, clock };
}

export default useTHREE;

使用自定义 hooks

自定义的 useTHREE(mainCanvas, true, true, true) hooks

需要注的是需要去除 react 的 <React.StrictMode> ,此标记是 react 用来做 hooks 副作用的并发测试,总是会重复执行 hooks 一次

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
import React, { useEffect, useRef } from 'react';
import * as THREE from "three";
import { MathUtils, Vector3 } from 'three';
import useTHREE from './myHooks'
import './App.css';


// dat.gui 更改的属性,函数也可以改变 mesh1 是名字
// 提供此对象,能方便的可需要更改属性的几何体进行解耦
const changePropertys: guiChangePropertyType = {
//这种会分组
'mesh1,position': [
{ name: 'x', min: -10, max: 10, step: .01 },
{ name: 'y', min: -10, max: 10, step: .01 },
{ name: 'z', min: -10, max: 10, step: .01 }],

'mesh1,scale': [
{ name: 'x', step: .01 },
{ name: 'y', step: .01 },
{ name: 'z', step: .01 }],

'mesh1': [
{ name: 'rotateX', min: -Math.PI, max: Math.PI, step: 1 },
{ name: 'rotateY', min: -Math.PI, max: Math.PI, step: 1 },
{ name: 'rotateZ', min: -Math.PI, max: Math.PI, step: 1 },
{ name: 'translateX', min: -10, max: 10, step: 0.1 },
{ name: 'translateY', min: -10, max: 10, step: 0.1 },
{ name: 'translateZ', min: -10, max: 10, step: 0.1 },
{ name: 'visible', },
],
'mesh1,material':
{ name: 'color' },
}

function App() {

const mainCanvas = useRef(null)

//使用自定义 THREE hooks
const { configCamera, configPrimaryAnimation, addObjectToScene, addLightsToScene, camera, render, clock } =
useTHREE({
placeRenderingRef: mainCanvas,
isUseAxes:true,
isUseOrbitControls: true,
isUsePerformanceMonitor:true,
guiChangePropertys: changePropertys,
/** 点击画布的元素拾取 mesh,group */
clickHandle: (clickMesh => {
clickMesh.forEach(console.log)
},
orbitControlsChange: () => {}
});

//页面加载的时候配置初始的THREE
useEffect(() => {
//配置相机 相对单位
configCamera(60, new Vector3(5, 5, 5), new Vector3(0, 0, 0));

//增加显示的几何体
const box = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial(
{ color: 0x00ffff, transparent: true, opacity: 0.5, wireframe: false, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(box, material);
//如果需要 dar.gui 控制属性必须设置 name
mesh.name='mesh1'

addObjectToScene([mesh]);

//配置光照
//0xffffff 白色
/** 光线太亮的话,物体的棱角不会太明显 pointLight 光源的位置不对的话,棱角也不会明显 */
const ambientLight = new THREE.AmbientLight('white', 0.6);
const pointLight = new THREE.PointLight('white', 0.1)
pointLight.position.set(1, 20, 30)
// 只有环境光,没有点光或聚光的话,显示不出暗面,就不能突出棱角
addLightsToScene([ambientLight, pointLight]);


//配置初始动画
configPrimaryAnimation(new Map<string, Function>([["a1", () => mesh.rotateX(MathUtils.degToRad(1))]]))

//改变相机,也可以做动画的效果
//camera.current && (camera.current.position.x+=0.01)

}, [addLightsToScene, configPrimaryAnimation, configCamera, addObjectToScene])



useEffect(() => {
if (!mainCanvas.current) return
render();
}, [render])

//更新相机的 fov ,相机的距离并没有改变,但是物体的远近效果变了
const changeFov=(fov:number)=>{
(camera.current as THREE.PerspectiveCamera).fov = fov;
(camera.current as THREE.PerspectiveCamera)?.updateProjectionMatrix()
}

return (
<div className="App">
{/* 画布要设置大小 */}
<div ref={mainCanvas} style={{width:'90rem',height:'40rem',border:'2px solid blue'}}/>
</div>
);
}

export default App;

光源

threejs 渲染的真实性和光源的使用有很大的关系,THREE.Light 光源的基类。
threejs 在光源上常见的种类:

THREE.AmbientLight 环境光,处于多次漫反射的形成的光,这种光任何几何体的都能够照射到,没有方向性,所以不会产生阴影效果,光的强度在任何地方都一样的,不会衰减。
主要是均匀整体改变Threejs物体表面的明暗效果,这一点和具有方向的光源不同,比如点光源可以让物体表面不同区域明暗程度不同。

THREE.DirectionalLight 平行光(方向光) 可以看做是模拟太阳发出的光源,这个光源所发出的光都是相互平行的。光的强度在照射的范围内是一样的,不会产生阴影效果
平行光顾名思义光线平行,对于一个平面而言,平面不同区域接收到平行光的入射角一样。
点光源因为是向四周发散,所以设置好位置属性.position就可以确定光线和物体表面的夹角,对于平行光而言,主要是确定光线的方向,光线方向设定好了,光线的与物体表面入射角就确定了,
仅仅设置光线位置是不起作用的。
在三维空间中为了确定一条直线的方向只需要确定直线上两个点的坐标即可,所以Threejs平行光提供了位置.position和目标.target两个属性来一起确定平行光方向。目标.target的属性
值可以是Threejs场景中任何一个三维模型对象,比如一个网格模型Mesh,这样Threejs计算平行光照射方向的时候,会通过自身位置属性.position和.target表示的物体的位置属性.position计算出来。

THREE.SpotLight 聚灯光,类似于射灯,在照射的中心很亮,边缘区域很暗,有阴影效果
聚光源可以认为是一个沿着特定方会逐渐发散的光源,照射范围在三维空间中构成一个圆锥体。通过属性.angle可以设置聚光源发散角度,聚光源照射方向设置和平行光光源一样
是通过位置.position和目标.target两个属性来实现。

THREE.PointLight 点光源就像生活中的白炽灯,光线沿着发光核心向外发散,同一平面的不同位置与点光源光线入射角是不同的,点光源照射下,同一个平面不同区域是呈现出不同的明暗效果。
和环境光不同,环境光不需要设置光源位置,而点光源需要设置位置属性.position,光源位置不同,物体表面被照亮的面不同,远近不同因为衰减明暗程度不同。

仅仅使用环境光的情况下,你会发现整个立方体没有任何棱角感,这是因为环境光只是设置整个空间的明暗效果。如果需要立方体渲染要想有立体效果,需要使用具有方向性的点光源、平行光源等。

光线线是和几何体的法线和材质有关系

  • MeshBasicMaterial 光线影响几何体的明暗,不会影响颜色的反射。这种材质不会反射光线
  • MeshLambertMaterial 材质的法线和颜色都会影响光线,这种材质会反射光线,环境光和法线没有关系,是整体的变亮或变暗,带有方向的光源受法线影响
    如果光线是红色的,几何体是绿色,显示的效果是几何体是黑色的,因为几何体吸收的是除过绿色的其他光

点,线,三角面,几何体

THREE 所有的几何体都是通过 THREE.BufferAttribute THREE.BufferGeometry 俩个API 生成的
在 R125版本开始移除了THREE.Geometry,代码被移到 three/examples/jsm/deprecated/Geometry 里

顶点位置数据渲染

通过 THREE.BufferGeometry 可以够着自定义的图形

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
const geo=new THREE.BufferGeometry();

//有点可以组成线,线组成面。 THREE 的面是三角面,需要3个顶点
const arr =new Float32Array([
0,0,0,
2,0,2,
2,0,0,

0,0,0,
2,0,2,
2,2,2,

0,0,0,
0,0,2,
2,2,2,
])

//缓存 3个一组
const points=new THREE.BufferAttribute(arr,3);
//给几何体增加点
geo.setAttribute('position',points)
//自定义面测下只能用 MeshBasicMaterial 材质
const material = new THREE.MeshBasicMaterial(
{ color: 'red',side:DoubleSide});
const obj = new THREE.Mesh(geo, material);
addObjectToScene([obj]);

如上代码绘制的3个三角面
绘制三角面

渲染成点模型,修改材质

1
2
3
4
5
6
var material=new THREE.PointsMaterial({
color:'red'
size: 0.5 //点大小
});
const obj=new THREE.Points(geo,material)
addObjectToScene([obj]);

如上代码绘制的9个点
绘制三角面

渲染成线模型,修改材质

1
2
3
4
var material=new THREE.LineBasicMaterial({
color:'red' })//线条颜色
var obj=new THREE.Line(geo,material)
addObjectToScene([obj]);

如上代码绘制的9条线
绘制三角面

几何体的本质

立方体几何体BoxGeometry本质上就是一系列的顶点构成,只是Threejs的APIBoxGeometry把顶点的生成细节封装了,用户可以直接使用。比如一个立方体网格模型,有6个面,每个面至少两个三角形拼成一个矩形平面,每个三角形三个顶点构成,
对于球体网格模型而言,同样是通过三角形拼出来一个球面,三角形数量越多,网格模型表面越接近于球形。

几何体

如下的球体,查看顶点,和线 mesh ,漂亮的球体也是有大量的点连城的线绘制的三角面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const sphere=new THREE.SphereGeometry(2)
const materia=new THREE.PointsMaterial({
color:'pink',
size:0.1
})
const points=new THREE.Points(sphere,materia)
addObjectToScene([points]);


const sphere=new THREE.BoxGeometry(2)
const materia=new THREE.LineBasicMaterial({
color:'pink',
side:DoubleSide,
})
const points=new THREE.Line(sphere,materia)
addObjectToScene([points]);

球体点
球体线

颜色插值计算

线段颜色有顶点颜色插值得来

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
const line = new THREE.BufferGeometry();

//线段起点终点
const pointArr = new Float32Array([
0, 0, 0,
0, 0, 2,
])
line.setAttribute('position', new THREE.Float32BufferAttribute(pointArr, 3))

//线段端点颜色 插值的颜色是0-1 而不是 0-255
const colorArr = new Float32Array([
1, 0, 0, //顶点1颜色
0, 1, 0, //顶点2颜色
]);
line.setAttribute('color', new THREE.Float32BufferAttribute(colorArr, 3))

//线段材质
const material = new THREE.LineBasicMaterial({
//颜色有顶点决定
vertexColors: true,
side: THREE.DoubleSide,
});
const obj = new THREE.Line(line, material)
obj.position.z = 0.5
obj.position.y = 0.5
addObjectToScene([obj]);

线段插值

面颜色插值

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
//有顶点组成面
const rectangle = new THREE.BufferGeometry();

//逆时针放置点
const pointArr = new Float32Array([
0, 0, 0,
0, 0, 2,
2, 0, 0,

0, 0, 2,
2, 0, 2,
2, 0, 0,
])
rectangle.setAttribute('position', new THREE.Float32BufferAttribute(pointArr, 3))


const colorArr = new Float32Array([
1, 0, 0, //顶点0颜色 红
0, 1, 0, //顶点1颜色 绿
0, 0, 1, //顶点2颜色 蓝

0, 1, 0, //顶点3颜色
1, 0, 0, //顶点4颜色
0, 0, 1, //顶点5颜色
]);
rectangle.setAttribute('color', new THREE.Float32BufferAttribute(colorArr, 3))


const material = new THREE.MeshBasicMaterial({
//面的颜色有顶点插值得来
vertexColors: true,
side: THREE.DoubleSide,
});
const obj = new THREE.Mesh(rectangle, material)
addObjectToScene([obj]);

面插值

带法线的几何体

如果渲染的材质是可以反射光的材质,必须要设置法向

上面的例子面插值,如果更改材质为 MeshLambertMaterial,面显示黑色,就是因为没有设置法线的原因

1
2
3
4
5
const material = new THREE.MeshLambertMaterial({
//面的颜色有顶点插值得来
vertexColors: true,
side: THREE.DoubleSide,
});

增加法线后 MeshLambertMaterial 材质才知道如何反射光线

1
2
3
4
5
6
7
8
9
10
11
 //增加法线,用来反射光线,每个顶点都要定义
const normalArr = new Float32Array([
0, 1, 0,
0, 1, 0,
0, 1, 0,

0, 1, 0,
0, 1, 0,
0, 1, 0,
]);
rectangle.setAttribute('normal', new THREE.Float32BufferAttribute(normalArr, 3))

通过索引减少重复的点

绘制一个正方形常规是用4个点,而不是6个点,有俩个点是重复的,但是在THREE 里正方形是由三角形拼接的,不考虑分段,一个正方形需要俩个三角形才能拼接出来
俩个三角形是6个顶点,有俩个是重复的,通过索引复用顶点可以用四个点绘制正方形

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
//逆时针放置四个点 
const pointArr = new Float32Array([
0, 0, 0, //顶点0
0, 0, 2, //顶点1
2, 0, 2, //顶点2
2, 0, 0, //顶点3
]);

//四个点的颜色
const colorArr = new Float32Array([
1, 0, 0, //顶点0颜色 红
0, 1, 0, //顶点1颜色 绿
0, 0, 1, //顶点2颜色 蓝
1, 0, 0, //顶点4颜色 红
])

//四个点的法线增加法线,用来反射光线
const normalArr = new Float32Array([
0, 1, 0, //顶点1法线
0, 1, 0, //顶点2法线
0, 1, 0, //顶点3法线
0, 1, 0, //顶点4法线
]);

//通过索引绘制俩个三角形,逆时针绘制三角形
//必须是无符号整数
const indexArr = new Uint16Array([
0, 1, 3, //第一个三角形
1, 2, 3 //第二个三角形
]);

//有顶点组成面
const rectangle = new THREE.BufferGeometry();

rectangle.setAttribute('position', new THREE.Float32BufferAttribute(pointArr, 3));
rectangle.setAttribute('normal', new THREE.Float32BufferAttribute(normalArr, 3));
rectangle.setAttribute('color', new THREE.Float32BufferAttribute(colorArr, 3));

//设置索引
rectangle.index = new THREE.BufferAttribute(indexArr, 1)

const material = new THREE.MeshLambertMaterial({
//面的颜色有顶点插值得来
vertexColors: true,
side: THREE.DoubleSide,
});
const obj1 = new THREE.Mesh(rectangle, material)
obj1.position.x = -5
addObjectToScene([obj1]);

通过索引自定义正方体

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
 //逆时针放置点
const pointArr = new Float32Array([
0, 0, 1,
1, 0, 1,
1, 1, 1,
0, 1, 1,
0, 1, 0,
1, 1, 0,
1, 0, 0,
0, 0, 0,
]);

const colorArr = new Float32Array([
1, 0, 0, //顶点0颜色 红
0, 1, 0, //顶点1颜色 绿
0, 0, 1, //顶点2颜色 蓝
1, 0, 0, //顶点4颜色 红

1, 0, 0, //顶点0颜色 红
0, 1, 0, //顶点1颜色 绿
0, 0, 1, //顶点2颜色 蓝
1, 0, 0, //顶点4颜色 红
])

//增加法线,用来反射光线
const normalArr = new Float32Array([
0, 1, 0, //顶点1法线
0, 1, 0, //顶点2法线
0, 1, 0, //顶点3法线
0, 1, 0, //顶点4法线

0, 1, 0, //顶点1法线
0, 1, 0, //顶点2法线
0, 1, 0, //顶点3法线
0, 1, 0, //顶点4法线
]);

//必须是无符号整数
const indexArr = new Uint8Array([
0, 1, 2,
2, 3, 0,

3, 2, 5,
5, 4, 3,

6, 7, 4,
4, 6, 6,

1, 6, 2,
2, 6, 5,

0, 7, 4,
4, 3, 0,

0, 1, 7,
7, 1, 6
]);

//有顶点组成面
const rectangle = new THREE.BufferGeometry();


//geometry.setIndex( indices );
rectangle.setAttribute('position', new THREE.Float32BufferAttribute(pointArr, 3));
rectangle.setAttribute('normal', new THREE.Float32BufferAttribute(normalArr, 3));
rectangle.setAttribute('color', new THREE.Float32BufferAttribute(colorArr, 3));
rectangle.index = new THREE.BufferAttribute(indexArr, 1)


const material = new THREE.MeshLambertMaterial({
//面的颜色有顶点插值得来
vertexColors: true,
side: THREE.DoubleSide,
wireframe: true
});
const obj = new THREE.Mesh(rectangle, material)

addObjectToScene([obj]);

自定义正方体

访问几何体数据及拷贝复制

访问几何体并更改数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const box = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial(
{ color: 0x00ffff, transparent: true, opacity: 0.5, wireframe: false, side: THREE.DoubleSide });

const mesh = new THREE.Mesh(box, material);
//可以访问几何体的高度可以,但是更改几何体的尺寸后,THREE.js 并不会去渲染修改后的数据
mesh.geometry.parameters.height=2
//只能通过 scale 去更改
//几何体的 scale 会改变几何体的顶点坐标
box.scale.set(1,2,1)
//mesh的 scale 不会改变顶点数据
mesh.scale.set(1,2,1)
//如果更改属性长宽高要通过 mesh 的缩放

//可以更改几何体的材质属性
mesh.material.color=new THREE.Color('red')
addObjectToScene([mesh]);

拷贝复制

对几何体 colone 是深拷贝,对mesh colone 是浅 colone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const box = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial(
{ color: 0x00ffff, transparent: true, opacity: 0.5, wireframe: false, side: THREE.DoubleSide });

console.log(box.id) //5
const newBox= box.clone()
console.log(newBox.id) //6

console.log(material.id) //8
const newMaterial= material.clone()
console.log(newMaterial.id) //9


const mesh = new THREE.Mesh(box, material);
console.log(mesh.id) //13
console.log(mesh.geometry.id)//5
console.log(mesh.material.id) //8
const newMesh= mesh.clone()
console.log(newMesh.id) //14
console.log(newMesh.geometry.id) //5
console.log(newMesh.material.id) //8

mesh 的copy 是浅 copy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//增加显示的几何体
const box = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial(
{ color: 0x00ffff, transparent: true, opacity: 0.5, wireframe: false, side: THREE.DoubleSide });

console.log(box.id) //5
const newBox=new THREE.BoxGeometry().copy(box)
console.log(newBox.id) //6

const mesh = new THREE.Mesh(newBox, material);

console.log(mesh.geometry.id)//6
console.log(mesh.material.id) //8
const newMesh = new THREE.Mesh().copy(mesh);
console.log(newMesh.geometry.id) //6
console.log( (newMesh.material as THREE.MeshLambertMaterial).id) //8

本地坐标和世界坐标

世界坐标中心点默认在画布的中心位置,一个 scene 只有一个世界坐标,方向参考右手定则
默认世界坐标的中心画布中心,通过如下方法可以更改世界坐标的中心
scene.current.position.set(1,1,1)

本地坐标,每个 mesh 都有一个本地坐标,默认坐标的中心在几何体的中心位置
在不指定 mesh 本地坐标的情况下,默认本地坐标和世界坐标的中心重合
本地坐标的 position 是参考世界坐标的,对 mesh 进行 平移,缩放,旋转 会改变 mesh 的 position 位置

上面的自定义正方体的例子,默认本地坐标和世界坐标是重合的,对 mesh 进行平移,就能看出来本地坐标的中心在几何体的中心位置(图中的几何体的中心点在坐标 (0,0,0) 位置)
对几何体的进行平移,缩放,旋转的参考都参考几何体本地坐标进行的,会改变几何体的顶点数据,因为顶点的坐标是参考本地坐标定义的

对 mesh 进行 平移,缩放,旋转 ,参考的是本地坐标,mesh 内的几何体的顶点坐标相对 mesh 的本地坐标并没有发生改变(除过缩放),因为 THREE.js 会一起更新几何体,

对 mesh 进行 平移,缩放,旋转 ,能改变 mesh 相对于世界坐标的 position 位置

1
2
3
4
5
6
7
const obj = new THREE.Mesh(rectangle, material)
//让 mesh 显示坐标辅助
const axesHelper=new THREE.AxesHelper(3)
obj.add(axesHelper)
// mesh 进行移动,其实改变的是 mesh 的本地坐标,几何体的顶点数据并没有改变
obj.translateX(2);
obj.translateZ(-2);

坐标种类

旋转 缩放 平移 ,旋转方向是右手定则

旋转 底层是欧拉角Euler

欧拉角是用来表示三维坐标系中方向和方向变换的

对几何进行旋转,会改变几何体的顶点坐标,因为本地坐标没有旋转,相对的几何体的顶点坐标就变化了
对 mesh ,group 对象进行旋转不对改变几何体的顶点坐标,因为几何体的顶点坐标是参考本地坐标的
在本地坐标旋转的时候,几何体的本地顶点坐标也会跟着一起变化。

rotateX, rotateY,rotateZ,

要有旋转的效果必须渲染函数里重复调用

测试的版本是 R144

对于mesh 或 geometry 旋转效果是围绕几何体自身的本地坐标轴旋转
group 对象是围绕世界坐标系旋转

还是自定义正方体的例子.本地坐标没有改变

几何体的本地坐标和世界坐标重复

对 mesh 进行旋转参考的坐标轴是几何体本地的坐标轴,

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = new THREE.Mesh(rectangle, material)
console.table(rectangle.getAttribute('position'))
//对几何体的本地坐标参考自己的 Z 轴进行旋转90度
obj.rotateZ(Math.PI*0.5)
//对几何体的本地坐标 在自己的 Y 轴方向移动 单位1
obj.translateY(1)
//在目前的本地坐标内,对几何体在 x 轴移动 单位1
rectangle.translate(1,0,0)

console.table(rectangle.getAttribute('position'))
const axesHelper=new THREE.AxesHelper(3)
obj.add(axesHelper)
addObjectToScene([obj]);

旋转本地坐标

rotateOnAxis,

rotateOnWorldAxis 以世界坐标系为起点

因为向量的原因只能是mesh group 使用此API

如果绕 Y 轴旋转如下设置
rotateOnAxis(new THREE.Vector3(0,1,0) ,MathUtils.degToRad(1))
如果绕 Y 轴反向旋转如下设置
rotateOnAxis(new THREE.Vector3(0,-1,0) ,MathUtils.degToRad(1))

以 mesh 本地坐标中心为起点 new Vector3(1,1,1) 为终点的自定义旋转轴

1
2
3
4
5
6
7
8
9
10
11
const obj1 = new THREE.Mesh(rectangle, material)
obj1.translateX(1)
const obj2= obj1.clone()

const v3= new Vector3(1,1,1)
obj2.rotateOnAxis(v3,Math.PI*0.25)
obj2.translateOnAxis(v3,1)
const axesHelper=new THREE.AxesHelper(1.5)
obj1.add(axesHelper)
obj2.add(axesHelper.clone())
addObjectToScene([obj1,obj2]);

向量方向旋转

当父对象旋转的时候,子对象绕父对象旋转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const cylinder = new THREE.CylinderGeometry(0.5, 1, 4);

const cylinderMesh = new THREE.Mesh(cylinder, material);
cylinderMesh.position.set(5,cylinder.parameters.height/2,0)

const box = new THREE.BoxGeometry(1.2, 1.2, 1.2);
const boxMesh = new THREE.Mesh(box, material);
boxMesh1.position.set(2,box1.parameters.height/2-cylinder.parameters.height/2,2)
//添加子对象
cylinderMesh.add(boxMesh1)
addMeshesToScene([cylinderMesh]);

//父对象旋转
configPrimaryAnimation(new Map<string, Function>([
["a1", () => cylinderMesh.rotateY(MathUtils.degToRad(1))],
]))

平移

translateX ,translateY, translateZ

对几何进行translate,会改变几何体的顶点坐标, 因为本地坐标没有 translate
对 mesh ,组对象进行 translate 不会改变顶点坐标,因为几何体的顶点坐标会跟着 mesh 的本地坐标进行 translate

还是自定义正方体的例子.本地坐标没有改变

几何体的本地坐标和世界坐标重复

此正方体的长宽高都是1,先对几何体的本地坐标参考自己的Y 轴旋转 90 度
在对几何体的本地坐标x轴上参考自己的 X 轴 平移1,在对几何体在参考本地坐标 X 轴平移 -1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const obj = new THREE.Mesh(rectangle, material)
console.table(rectangle.getAttribute('position'))
// 让 mesh 的本地坐标进行平移,几何体的顶点数据并没有改变
// 平移的参考点并不是世界坐标,是参考自己的本地坐标

console.table(rectangle.getAttribute('position'))
//本地坐标参考自己旋转 90 度
obj.rotateY(Math.PI*0.5)
//本地坐标在当前的本地坐标系内 X 轴平移 1 个单位
obj.translateX(1);
console.table(rectangle.getAttribute('position'))
//几何体参考自己本地坐标 X 轴平移 -1 个单位
rectangle.translate(-1,0,0);

console.table(rectangle.getAttribute('position'))
const axesHelper=new THREE.AxesHelper(3)
obj.add(axesHelper)

移动本地坐标

translateOnAxis 沿着着指定的向量方向平移一定的距离

translateOnAxis 可以灵活的向任意方向平移,因为向量的原因只能是mesh group 使用此API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = new THREE.Mesh(rectangle, material)

//对第一个mesh X 轴平移 1个单位
obj1.translateX(1)
//克隆第一个 mesh
const obj2= obj.clone()

//对第二个 mesh2 以自己本地坐标系的中心为起点,以 new Vector3(1,1,1)为终点作为平移的参考线
const v3= new Vector3(1,1,1)
obj2.translateOnAxis(v3,1)

const axesHelper=new THREE.AxesHelper(3)
obj1.add(axesHelper)
obj2.add(axesHelper.clone())
addObjectToScene([obj1,obj2]);

向量方向平移

缩放

对几何体进行 scale,会改变几何体的顶点坐标, 缩放是三个方向都可以的 scale(x,y,z)

几何体 scale(-x,-y,-z) 的效果是几何体会到反方向的底部。

几何体反方向缩放

对 mesh , group 进行 scale ,几何体的体积会变化,但是比较奇怪的是顶点坐标不会变化。或许是为了和 平移,旋转 保持一致吧

mesh scale(-x,-y,-z) 的效果是几何体会到反方向的底部。坐标轴也会到反方向的底部

mesh反方向缩放

设置几何体的几何中心

center 几何体调用此方法后会让几何体的中心居于本地坐标的中心,而不是默认的顶点坐标最小点
会改变几何体的所有顶点坐标

center前

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = new THREE.Mesh(rectangle, material)
console.table(rectangle.getAttribute('position'))
console.table(obj.position)

obj.translateX(1)
obj.translateZ(1)
rectangle.center();
console.table(rectangle.getAttribute('position'))
console.table(obj.position)

const axesHelper = new THREE.AxesHelper(3)
obj.add(axesHelper)
addObjectToScene([obj]);

center后

矩阵变换、欧拉、四元数、

几何体的层次结构

通过 group 建立层级关系
嵌套的层级关系,里面的对象参考外面对象的坐标

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
const ax = new THREE.AxesHelper(5)

//增加显示的几何体
const box = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial(
{ color: 0x00ffff, transparent: true, opacity: 0.5, wireframe: false, side: THREE.DoubleSide });

const mesh = new THREE.Mesh(box, material);
mesh.translateX(1)
mesh.translateZ(1)
mesh.translateY(0.5)

//通过 group 可以建立层级关系
const group = new THREE.Group()
group.add(mesh)
group.position.set(1,1,1)

console.log(group.position) // 1,1,1
console.log(mesh.position) // 1,0.5,1

//通过 getWorldPosition 获取对象的相对世界坐标的位置
const v3 = new THREE.Vector3()
mesh.getWorldPosition(v3)
console.log(v3)//2,1.5,2

group.add(ax.clone())
mesh.add(ax)
addObjectToScene([group]);

层级关系group

group.remove 方法也可以移除添加的对象

1
2
3
4
group.add(mesh)
const newMesh=mesh.clone().translateX(2)
group.add(newMesh)
group.remove(newMesh)

mesh 之间也可以建立层级关系
嵌套的层级关系,里面的对象参考外面对象的坐标
** mesh.remove 也可以移除内部的对象

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
const ax = new THREE.AxesHelper(5)

//增加显示的几何体
const box = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshLambertMaterial(
{ color: 0x00ffff, transparent: true, opacity: 0.5, wireframe: false, side: THREE.DoubleSide });
const v3 = new THREE.Vector3()
const mesh = new THREE.Mesh(box, material);
mesh.translateX(1)
mesh.translateZ(1)
mesh.translateY(0.5)

const group = new THREE.Group()
group.position.set(1,0,1,)

group.add(ax.clone())
group.add(mesh)
const newMesh=mesh.clone().translateX(1)
newMesh.material=new THREE.MeshLambertMaterial().copy(mesh.material)
newMesh.material.color=new THREE.Color('red')
newMesh.position.set(1,0.5,1)
//mesh 之间建立的层级关系
mesh.add(newMesh.add(ax.clone()))

newMesh.getWorldPosition(v3)//3,1,3
console.log(v3);
mesh.add(ax)
addObjectToScene([group]);

层级关系mesh

绘制平面图形

直线

1
2
3
4
5
6
7
8
9
10
11
const ax = new THREE.AxesHelper(5)
const startV2=new THREE.Vector2(0,0)
const endV2=new THREE.Vector2(5,5)
const line=new THREE.LineCurve(startV2,endV2);
const geo=new THREE.BufferGeometry().setFromPoints(line.getPoints(50))
const mesh=new THREE.Line(geo,new THREE.LineBasicMaterial({
color:'pink',
}))
mesh.rotateX(Math.PI*0.5)
mesh.add(ax)
addObjectToScene([mesh]);

平面直线

三位空间的直线

1
2
3
4
5
6
7
8
9
10
const ax = new THREE.AxesHelper(5)
const startV3=new THREE.Vector3(0,0,0)
const endV3=new THREE.Vector3(2,2,2)

const geo=new THREE.BufferGeometry().setFromPoints([startV3,endV3])
const mesh=new THREE.Line(geo,new THREE.LineBasicMaterial({
color:'pink',
}))
mesh.add(ax)
addObjectToScene([mesh]);
1
2
3
4
5
6
7
8
9
10
const ax = new THREE.AxesHelper(5)
const startV2=new THREE.Vector3(0,0)
const endV2=new THREE.Vector3(5,5,5)
const line=new THREE.LineCurve3(startV2,endV2);
const geo=new THREE.BufferGeometry().setFromPoints(line.getPoints(50))
const mesh=new THREE.Line(geo,new THREE.LineBasicMaterial({
color:'pink',
}))
mesh.add(ax)
addObjectToScene([mesh]);

圆弧

ArcCurve

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const ax = new THREE.AxesHelper(5)

// aClockwise 表示绘制的是是否按照顺时针绘制
const arc=new THREE.ArcCurve(0,0,5,0,Math.PI,false);

//平面图形必须要转换为三维图形
//arc.getPoints(50) 在指线上取指定的顶点
const geo=new THREE.BufferGeometry().setFromPoints(arc.getPoints(50))
const mesh1=new THREE.Line(geo,new THREE.LineBasicMaterial({
color:'red',
}))
mesh.rotateX(Math.PI*0.5).translateX(2).translateY(1)
mesh.add(ax)
addObjectToScene([mesh]);

平面圆弧

椭圆

Ellipse

1
2
3
4
5
6
7
8
9
10
11
const ax = new THREE.AxesHelper(5)

//最后一个参数指定旋转椭圆的弧度
const ellipse=new THREE.EllipseCurve(0,0,5,2,0,2*Math.PI,false,Math.PI*0.5);
const geo=new THREE.BufferGeometry().setFromPoints(ellipse.getPoints(50))
const mesh=new THREE.Line(geo,new THREE.LineBasicMaterial({
color:'red',
}))
mesh.rotateX(Math.PI*0.5)
mesh.add(ax)
addObjectToScene([mesh]);

平面椭圆

样条曲线

  1. SplineCurve 二维样条曲线

  2. CatmullRomCurve3 三维样条曲线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const ax = new THREE.AxesHelper(5)

const curve1=new THREE.CatmullRomCurve3([
new THREE.Vector3(0,0,0),
new THREE.Vector3(1,1,1),
new THREE.Vector3(1,1,-1),
],true)

const curve2=new THREE.CatmullRomCurve3([
new THREE.Vector3(0,0,0),
new THREE.Vector3(-1,1,1),
new THREE.Vector3(-1,1,-1),
],true)

const geo1=new THREE.BufferGeometry().setFromPoints(curve1.getPoints(50))
const geo2=new THREE.BufferGeometry().setFromPoints(curve2.getPoints(50))
const mesh1=new THREE.Line(geo1,new LineBasicMaterial({color:'#B09A37'}))
const mesh2=new THREE.Line(geo2,new LineBasicMaterial({color:'#B09A37'}))
const group=new THREE.Group()
group.add(mesh1,mesh2)
group.add(ax)

addObjectToScene([group]);

三维样条曲线

贝塞尔曲线

贝塞尔曲线有二维和三维的

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
const ax = new THREE.AxesHelper(5)

//二阶三维贝塞尔
const curve1=new THREE.QuadraticBezierCurve3(
new THREE.Vector3(0,0,0),
new THREE.Vector3(1,1,1),
new THREE.Vector3(1,1,-1),
)

const curve2=new THREE.CubicBezierCurve3(
new THREE.Vector3(0,0,0),
new THREE.Vector3(-1,1,1),
new THREE.Vector3(-1,1,-1),
new THREE.Vector3(-1,1,-2),
)

//三阶三维贝塞尔
const geo1=new THREE.BufferGeometry().setFromPoints(curve1.getPoints(50))
const geo2=new THREE.BufferGeometry().setFromPoints(curve2.getPoints(50))
const mesh1=new THREE.Line(geo1,new LineBasicMaterial({color:'#B09A37'}))
const mesh2=new THREE.Line(geo2,new LineBasicMaterial({color:'#B09A37'}))
const group=new THREE.Group()
group.add(mesh1,mesh2)
group.add(ax)

addObjectToScene([group]);

三维贝塞尔曲线

多个线条组合曲线CurvePath

绘制跑道,注意要按照固定的顺序绘制,此线是按照顺时针绘制的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const ax = new THREE.AxesHelper(5)
const curve1=new THREE.ArcCurve(0,5,2,Math.PI,0,true)
const curve2=new THREE.LineCurve(new THREE.Vector2(2,5),new THREE.Vector2(2,2))
const curve3=new THREE.ArcCurve(0,2,2,0,Math.PI,true)
const curve4=new THREE.LineCurve(new THREE.Vector2(-2,2),new THREE.Vector2(-2,5))

const curvePath=new THREE.CurvePath()
curvePath.curves.push(curve1,curve2,curve3,curve4)

const geo=new THREE.BufferGeometry()
geo.setFromPoints(curvePath.getPoints(200) as THREE.Vector2[])
const line=new THREE.Line(geo,new THREE.LineBasicMaterial({
color:'#022239',
}))
line.rotateX(Math.PI*0.5).translateX(2)
line.add(ax)
addObjectToScene([line]);

curvepath

path

只是线段,没有三角面,path 能显示的只能是点及有点组成的线段,不能渲染成形状

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//也可以在构造函数里传入点
const path = new THREE.Path()
path.moveTo(2, 1)
path.lineTo(3, 1)

//贝塞尔曲线 这里的坐标是连续的
// bezierCurveTo() 方法通过使用表示三次贝塞尔曲线的指定控制点,向当前路径添加一个点。
//三次贝塞尔曲线需要三个点。前两个点是用于三次贝塞尔计算中的控制点,第三个点是曲线的结束点。曲线的开始点是当前路径中最后一个点。
path.bezierCurveTo(3, 6, 5, 6, 5, 1)

//不带 to的 是坐标从零开始计算
path.arc(1, 0, 1, Math.PI, 0, true)
//在回到之前的坐标
path.lineTo(8, 1)

path.lineTo(8, 0.5)
path.lineTo(2, 0.5)
path.lineTo(2, 1)
//shape.arc(1,3,1,-Math.PI*0.5,Math.PI*0.5,false)

const geo = new THREE.BufferGeometry().setFromPoints(path.getPoints(30))
const mesh = new THREE.Line(geo, new LineBasicMaterial({ color: 'red', side: THREE.DoubleSide}))

addObjectToScene([mesh]);

path

shape

shape 类似 HTML DOM Canvas 对象,通过路径来绘制二维形状平面。简单理解就是在一个平面上用不规则的线连接成一个图形

多个线条的组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 // shape 类似 HTML DOM Canvas 对象
const shape = new THREE.Shape()
shape.moveTo(2, 1)
shape.lineTo(3, 1)

//贝塞尔曲线 这里的坐标是连续的
// bezierCurveTo() 方法通过使用表示三次贝塞尔曲线的指定控制点,向当前路径添加一个点。
//三次贝塞尔曲线需要三个点。前两个点是用于三次贝塞尔计算中的控制点,第三个点是曲线的结束点。曲线的开始点是当前路径中最后一个点。
shape.bezierCurveTo(3, 6, 5, 6, 5, 1)

//不带 to的 是坐标从零开始计算
shape.arc(1, 0, 1, Math.PI, 0, true)
//在回到之前的坐标
shape.lineTo(8, 1)

shape.lineTo(8, 0.5)
shape.lineTo(2, 0.5)

//shape.arc(1,3,1,-Math.PI*0.5,Math.PI*0.5,false)

const geo = new THREE.ShapeGeometry(shape)
const mesh = new THREE.Mesh(geo, new MeshLambertMaterial({ color: 'red', side: THREE.DoubleSide, wireframe: false }))
mesh.add(ax)
addObjectToScene([mesh]);

(canvas绘制2dShape

多个点的组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const points = [
new THREE.Vector2(1, 1),
new THREE.Vector2(2, 0),
new THREE.Vector2(3, 1),
new THREE.Vector2(3, 2),
new THREE.Vector2(2, 3),
new THREE.Vector2(1, 2),
]
// shape 类似 HTML DOM Canvas 对象
const shape = new THREE.Shape(points)

const geo = new THREE.ShapeGeometry(shape)
const mesh = new THREE.Mesh(geo, new MeshLambertMaterial({ color: 'red', side: THREE.DoubleSide, wireframe: true }))

addObjectToScene([mesh]);

(point绘制Shape

在形状内部挖孔

shape.holes 可以用path 或 shape 去挖 shape 的孔

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
const shape = new THREE.Shape()
shape.moveTo(2, 1)
shape.lineTo(3, 1)

//贝塞尔曲线 这里的坐标是连续的
// bezierCurveTo() 方法通过使用表示三次贝塞尔曲线的指定控制点,向当前路径添加一个点。
//三次贝塞尔曲线需要三个点。前两个点是用于三次贝塞尔计算中的控制点,第三个点是曲线的结束点。曲线的开始点是当前路径中最后一个点。
shape.bezierCurveTo(3, 6, 5, 6, 5, 1)

//不带 to的 是坐标从零开始计算
shape.arc(1, 0, 1, Math.PI, 0, true)
//在回到之前的坐标
shape.lineTo(8, 1)

shape.lineTo(8, 0.5)
shape.lineTo(2, 0.5)
shape.lineTo(2, 1)


const path1=new THREE.Path()
path1.arc(4,3,0.5,0,Math.PI*2,false)
const path2=new THREE.Path()
path2.arc(6,1.2,0.5,0,Math.PI*2,true)

//用path 去挖孔
shape.holes.push(path1,path2)
const geo = new THREE.ShapeGeometry(shape)
const mesh = new THREE.Mesh(geo, new MeshLambertMaterial({ color: 'red', wireframe:false, side: THREE.DoubleSide}))

addObjectToScene([mesh]);

(shapeHole

多个shape 的组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const shape1 = new THREE.Shape()
shape1.arc(1,1,1,0,Math.PI*2,false)
const shape2 = new THREE.Shape()
shape2.arc(2.4,1,1,0,Math.PI*2,false)
const shape3 = new THREE.Shape()
shape3.arc(1.6,2.3,1,0,Math.PI*2,false)
const geo = new THREE.ShapeGeometry([shape1,shape2,shape3])
const mesh = new THREE.Mesh(geo, new MeshLambertMaterial({ color: 'red', wireframe:false, side: THREE.DoubleSide}))



const shape4 = new THREE.Shape()
shape4.arc(1,1,1,0,Math.PI*2,false)
const shape5 = new THREE.Shape()
shape5.arc(3,1,1,0,Math.PI*2,false)
const shape6 = new THREE.Shape()
shape6.arc(2,2.7,1,0,Math.PI*2,false)
const geo1 = new THREE.ShapeGeometry([shape4,shape5,shape6])
const mesh1= new THREE.Mesh(geo1, new MeshLambertMaterial({ color: 'red', wireframe:false, side: THREE.DoubleSide}))
mesh1.translateX(5)

addObjectToScene([mesh,mesh1]);

(组合shape

绘制三维图形

tube

TubeGeometry

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const curve1 = new THREE.CatmullRomCurve3(
[
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(3, 3, 3),
new THREE.Vector3(3, 3, -3),
new THREE.Vector3(-3, 3, -3),
new THREE.Vector3(1, 6, 0),
]
)

const curvePath = new THREE.CurvePath<THREE.Vector3>()
curvePath.curves.push(curve1)

const tube = new THREE.TubeGeometry(curvePath, 200, 0.5, 30, false)
const mesh = new THREE.Mesh(tube, new MeshLambertMaterial({
color: 'red'
}))

addObjectToScene([mesh]);

tube

旋转 Lathe

提供2维或3维的点集合

1
2
3
4
5
6
7
8
9
10
11
const ax = new THREE.AxesHelper(5)
const points=[
new Vector2(1,1),
new Vector2(3,5),
new Vector2(2,6),
]
//默认绕 Y 轴旋转
const geo=new THREE.LatheGeometry(points,30,0,Math.PI)
const mesh=new THREE.Mesh(geo,new MeshLambertMaterial({ color:'#B09A37',side:THREE.DoubleSide}))
mesh.add(ax)
addObjectToScene([mesh]);

旋转成型

借助Shape对象的方法.splineThru(),把上面的三个顶点进行样条插值计算, 可以得到一个光滑的旋转曲面

shape.getPoints(20)的作用是利用已有的顶点插值计算出新的顶点,两个顶点之间插值计算出20个顶点,如果细分数是1不是20,相当于不进行插值计算, 插值计算的规则通过Shape对象的方法.splineThru()定义,几何曲线的角度描述,splineThru的作用就是创建一个样条曲线,除了样条曲线还可以使用贝赛尔等曲线进行插值计算。

1
2
3
4
5
6
7
8
9
10
11
const ax = new THREE.AxesHelper(5)
const points=[
new Vector2(1,1),
new Vector2(3,5),
new Vector2(2,6),
]
const shape=new THREE.Shape();
shape.splineThru(points)
const geo=new THREE.LatheGeometry(shape.getPoints(20),30)
const mesh=new THREE.Mesh(geo,new MeshLambertMaterial({ color:'#B09A37',side:THREE.DoubleSide}))
mesh.add(ax)

光滑的旋转成型

拉伸,扫描成型 ExtrudeGeometry

拉伸成型 是直线拉伸,拉伸的轨迹不能自定义,只能之定义拉伸的距离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const shape1 = new THREE.Shape()
shape1.arc(1, 1, 1, 0, Math.PI * 2, false)
const shape2 = new THREE.Shape()
shape2.arc(2.4, 1, 1, 0, Math.PI * 2, false)
const shape3 = new THREE.Shape()
shape3.arc(1.6, 2.3, 1, 0, Math.PI * 2, false)


const shape4 = new THREE.Shape()
shape4.arc(6, 1, 1, 0, Math.PI * 2, false)
const shape5 = new THREE.Shape()
shape5.arc(8, 1, 1, 0, Math.PI * 2, false)
const shape6 = new THREE.Shape()
shape6.arc(7, 2.7, 1, 0, Math.PI * 2, false)

const geo = new THREE.ExtrudeGeometry([shape1, shape2, shape3, shape4, shape5, shape6],{
depth:10,
bevelEnabled:false//无倒角
});
const mesh = new THREE.Mesh(geo, new MeshLambertMaterial({ color: 'red', wireframe: false, side: THREE.DoubleSide }))
addObjectToScene([mesh]);

拉伸成型

扫描成型,不能定义距离,但是要指定扫描的轨迹

1
2
3
4
5
6
7
8
9
10
11
12
13
const shape = new THREE.Shape()
shape.arc(0, 0, 1, 0, Math.PI * 2, false)
const curve1 = new THREE.CatmullRomCurve3([
new THREE.Vector3(0, 0, 0),
new THREE.Vector3(5, 5, 8),

], false)
const geo = new THREE.ExtrudeGeometry([shape], {
extrudePath: curve1,
bevelEnabled: false//无倒角
});
const mesh = new THREE.Mesh(geo, new MeshLambertMaterial({ color: 'red', wireframe: false, side: THREE.DoubleSide }))
addObjectToScene([mesh]);

扫描拉伸成型

绘制3d文字

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
//字体加载器
import { FontLoader } from 'three/examples/jsm/loaders/FontLoader'
//三维字体几何体
import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry'

{
const fontLoader = new FontLoader();
// fonts/DengXian_Regular.json 是一个大文件,最好的办好是根据显示的汉字选择对应的字体
// 这个文件需要绝对路径,一般放在 public 目录下
// 把操作系统下的 ttf 格式的字体文件 在这个 https://gero3.github.io/facetype.js/ 网站转换为json 文件
const font = await fontLoader.loadAsync('fonts/DengXian_Regular.json')
/*
parameters:
font[Font]:THREE.Font 实例。
size[Float]:字体大小,默认值为 100。
height[Float]:挤出文本的厚度,默认值为 50。
curveSegments[Integer]:表示文本的曲线上点的数量,默认值为 12,越多越光滑
bevelEnabled[Boolean]:是否开启斜角,默认为 false。
bevelThickness[Float]:文本斜角的深度,默认值为 20。
bevelSize[Float]:斜角与原始文本轮廓之间的延伸距离,默认值为 8。
bevelSegments[Integer]:斜角的分段数,默认值为 3。
*/
const geo = new TextGeometry('哈喽 THREE.JS', {
font: font,
size: 0.2,
height: 0.1,
curveSegments: 10,
bevelEnabled:true,
bevelThickness:0.01,
bevelSize:0.01,
})
geo.center()
const mesh = new THREE.Mesh(geo,
[
//字体表面材质
new THREE.MeshLambertMaterial({ color: 'pink' }),
//厚度方向材质
new THREE.MeshLambertMaterial({ color: 'red' }),
])
addObjectToScene([mesh])
}

绘制3d文字

纹理贴图

纹理贴图表达的是图片和几何体的面坐标之间的映射关系

uvz坐标填写的顺序一定要和坐标点的顺序能对应,才能正确的映射
默认 uv 的左下角是0,0 右上角是1,1

裁剪图片一半

uv裁剪坐标1

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
const ax = new THREE.AxesHelper(5)

// 修改 uv 坐标本质上就是对图片进行裁剪

{
const geo = new THREE.PlaneGeometry(1, 1);
console.dir((((geo.getAttribute('uv') as THREE.Float32BufferAttribute).array) as Float32Array))

// 默认的 uv 坐标和 顶点坐标是重合的,不会对图进行裁剪
// 0,1 1,1 0,0 1,0


//在原有的UV 坐标的情况下,选取上半部分的图片区域
//填写的顺序一定要和坐标点的顺序能对应
const newPoints = new Float32Array([
0, 1,
1, 1,
0, 0.5,
1, 0.5
]);
geo.setAttribute('uv', new THREE.BufferAttribute(newPoints, 2))
console.dir((((geo.getAttribute('uv') as THREE.Float32BufferAttribute).array) as Float32Array))
//0,1 1,1 0,0.5 1,0.5

const textureLoader = new THREE.TextureLoader()

textureLoader.load(flower, (texture) => {
const materia = new THREE.MeshLambertMaterial({
//color:'red',
side: THREE.DoubleSide,
map: texture,
wireframe: false
})
const mesh = new THREE.Mesh(geo, materia)
mesh.translateX(1.1)
mesh.add(ax)
addObjectToScene([mesh]);
})

}

// uv 没有裁剪
{
const geo = new THREE.PlaneGeometry(1, 1);
const textureLoader = new THREE.TextureLoader()

textureLoader.load(flower, (texture) => {
const materia = new THREE.MeshLambertMaterial({
//color:'red',
side: THREE.DoubleSide,
map: texture,
wireframe: false
})
const mesh = new THREE.Mesh(geo, materia)
mesh.add(ax)

addObjectToScene([mesh]);
})

}

UV坐标裁剪的效果
uv裁剪图片的一半

三角面uv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const shape=new THREE.Shape()
shape.moveTo(0,0)
shape.lineTo(1,0)
shape.lineTo(0.5,1)
const textureLoader = new THREE.TextureLoader()
textureLoader.load(flower,t=>{
const geo=new THREE.ShapeGeometry(shape)
const mesh=new THREE.Mesh(geo,new THREE.MeshLambertMaterial({
side:THREE.DoubleSide,
map:t
}))

console.table((((geo.getAttribute('uv') as THREE.Float32BufferAttribute).array) as Float32Array))
// 三角面只有三个顶点,也就是默认只有三个 uv 点,默认注定会裁剪
// 0.5,1 1,0 0,0
addObjectToScene([mesh]);
})

三角面uv

裁剪后的面积减少了,但是 uv 坐标的个数不能减少

uv裁剪4分之一

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
const ax = new THREE.AxesHelper(5)
{
const geo = new THREE.PlaneGeometry(1, 1, 2, 2);
console.table((((geo.getAttribute('uv') as THREE.Float32BufferAttribute).array) as Float32Array))
//9个 uv 坐标
console.table((((geo.getAttribute('position') as THREE.Float32BufferAttribute).array) as Float32Array))
//9个 顶点坐标

//即使现在的裁剪后的面积减少了,新的 uv 坐标也必须是 9个,主要顺序要和顶点坐标一直
const newPoints = new Float32Array([
0.5, 1,
0.75, 1,
1, 1,


0.5, 0.75,
0.75, 0.75,
1, 0.75,

0.5, 0.5,
0.75, 0.5,
1, 0.5,

]);

geo.setAttribute('uv', new THREE.BufferAttribute(newPoints, 2))
console.table((((geo.getAttribute('uv') as THREE.Float32BufferAttribute).array) as Float32Array))

const textureLoader = new THREE.TextureLoader()

textureLoader.load(flower, (texture) => {
const materia = new THREE.MeshLambertMaterial({
//color:'red',
side: THREE.DoubleSide,
map: texture,
wireframe: false
})
const mesh = new THREE.Mesh(geo, materia)
mesh.translateX(1.1)
mesh.add(ax)
addObjectToScene([mesh]);
})
}


// uv 没有裁剪
{
const geo = new THREE.PlaneGeometry(1, 1);
const textureLoader = new THREE.TextureLoader()

textureLoader.load(flower, (texture) => {
const materia = new THREE.MeshLambertMaterial({
//color:'red',
side: THREE.DoubleSide,
map: texture,
wireframe: false
})
const mesh = new THREE.Mesh(geo, materia)
mesh.add(ax)

addObjectToScene([mesh]);
})
}

uv坐标个数一致

自定义几何体的纹理贴图

自定义几何体要确保法线和坐标数和顶点坐标数数量匹配

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
const pointArr = new Float32Array([
0, 0, 1, //0顶点
1, 0, 1, //1顶点
1, 1, 1, //2顶点
0, 1, 1, //3顶点

1, 0, 1, //4顶点
2, 0, 1, //5顶点
2, 1, 1, //6顶点
1, 1, 1, //7顶点
]);

//面索引 必须是无符号整数
const indexArr = new Uint8Array([
//左边
0, 1, 2,
2, 3, 0,

//右边
4, 5, 6,
6, 7, 4
]);

const uv = new Float32Array([
//第一组四个uv 坐标对应前四组四个顶点坐标
0, 0, 1, 0, 1, 1, 0, 1,
//第二组四个uv 坐标对应前四组四个顶点坐标
0, 0, 1, 0, 1, 1, 0, 1,
])

//有顶点组成面
const rectangle = new THREE.BufferGeometry();

rectangle.setAttribute('position', new THREE.Float32BufferAttribute(pointArr, 3));
rectangle.index = new THREE.BufferAttribute(indexArr, 1)

//自动生成法向量,目前不知道,法线自动计算的规则
rectangle.computeVertexNormals()
//增加 uv
rectangle.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2))
rectangle.rotateX(-Math.PI*0.25)
const textureLoader = new THREE.TextureLoader()
textureLoader.load(flower, t => {
const material = new THREE.MeshLambertMaterial({
//面的颜色有顶点插值得来
//vertexColors: true,
side: THREE.DoubleSide,
wireframe: false,
map: t,
});

console.table(rectangle.getAttribute('normal'));

const obj = new THREE.Mesh(rectangle, material)
const axesHelper = new THREE.AxesHelper(3)
obj.add(axesHelper)

addObjectToScene([obj]);
})

自定义几何体

数组材质

几何体可以赋予数组材质,如果材质是数组形式提供,数组的个数少于几何体的面,则几何体有面不会渲染

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
const geo = new THREE.BoxGeometry(1, 1, 1)

const textureLoader = new THREE.TextureLoader()

const texture1 = textureLoader.load(dice1)
const texture2 = textureLoader.load(dice2)
const texture3 = textureLoader.load(dice3)
const texture4 = textureLoader.load(dice4)
const texture5 = textureLoader.load(dice5)
const texture6 = textureLoader.load(dice6)

const materia1 = new THREE.MeshLambertMaterial({
map: texture1,
side: THREE.DoubleSide,
})
const materia2 = new THREE.MeshLambertMaterial({
map: texture2,
side: THREE.DoubleSide,
})
const materia3 = new THREE.MeshLambertMaterial({
map: texture3,
side: THREE.DoubleSide,
})
const materia4 = new THREE.MeshLambertMaterial({
map: texture4,
side: THREE.DoubleSide,
})
const materia5 = new THREE.MeshLambertMaterial({
map: texture5,
side: THREE.DoubleSide,
})
const materia6 = new THREE.MeshLambertMaterial({
map: texture6,
side: THREE.DoubleSide,
})

//按顺序给几何体的每个面赋予不同的材质
const mesh = new THREE.Mesh(geo, [materia1, materia2, materia3, materia4, materia5, materia6])
addObjectToScene([mesh])

dice

纹理阵列

  1. wrapS
    纹理在水平方向上纹理包裹方式,在UV映射中对应于U,默认THREE.ClampToEdgeWrapping,表示纹理边缘与网格的边缘贴合。中间部分等比缩放。还可以设置为:THREE.RepeatWrapping(重复平铺) 和 THREE.MirroredRepeatWrapping(先镜像再重复平铺)

  2. wrapT
    纹理贴图在垂直方向上的包裹方式,在UV映射中对应于V,默认也是THREE.ClampToEdgeWrapping,与wrapS属性一样也可以设置为:THREE.RepeatWrapping(重复平铺) 和 THREE.MirroredRepeatWrapping(先镜像再重复平铺)

  3. repeat
    用来设置纹理将在表面上,分别在U、V方向重复多少次。
    repeat属性是Vector2类型,可以使用如下语句来为它赋值
    this.cube.material.map.repeat.set(repeatX, repeatY)

    repeatX 用来指定在x轴方向上多久重复一次,repeatY用来指定在y轴方向上多久重复一次
    这两个变量取值范围与对应效果如下

    如果设置等于1,则纹理不会重复
    如果设置为 大于0小于1 的值,纹理会被放大
    如果设置为 小于0 的值,那么会产生纹理的镜像
    如果设置为 大于1 的值,纹理会重复平铺

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const geo = new THREE.BoxGeometry(1, 1, 1)

const textureLoader = new THREE.TextureLoader()

const texture1 = textureLoader.load(flower)
texture1.wrapS = THREE.RepeatWrapping;
texture1.wrapT = THREE.RepeatWrapping;
// uv两个方向纹理重复数量 重复及镜像
texture1.repeat.set(-2, -2);

const materia = new THREE.MeshLambertMaterial({
map: texture1,
side: THREE.DoubleSide,
})

const mesh = new THREE.Mesh(geo, materia)
addObjectToScene([mesh])

纹理阵列

纹理偏移

texture.offset.set(x, y) x和y 的范围-1到1

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
const pointArr = new Float32Array([
0, 0, 1, //0顶点
1, 0, 1, //1顶点
1, 1, 1, //2顶点
0, 1, 1, //3顶点

1, 0, 1, //4顶点
2, 0, 1, //5顶点
2, 1, 1, //6顶点
1, 1, 1, //7顶点
]);

//面索引 必须是无符号整数
const indexArr = new Uint8Array([
//左边
0, 1, 2,
2, 3, 0,

//右边
4, 5, 6,
6, 7, 4
]);

const uv = new Float32Array([
//第一组四个uv 坐标对应前四组四个顶点坐标
0, 0, 1, 0, 1, 1, 0, 1,
//第二组四个uv 坐标对应前四组四个顶点坐标
0, 0, 1, 0, 1, 1, 0, 1,
])

//有顶点组成面
const rectangle = new THREE.BufferGeometry();

rectangle.setAttribute('position', new THREE.Float32BufferAttribute(pointArr, 3));
rectangle.index = new THREE.BufferAttribute(indexArr, 1)

//自动生成法向量,目前不知道,法线自动计算的规则
rectangle.computeVertexNormals()
//增加 uv
rectangle.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2))

const textureLoader = new THREE.TextureLoader()
textureLoader.load(flower, t => {

t.offset.set(0, 0)
const material = new THREE.MeshLambertMaterial({
//面的颜色有顶点插值得来
//vertexColors: true,
side: THREE.DoubleSide,
wireframe: false,
map: t,
});

const obj = new THREE.Mesh(rectangle, material)
const axesHelper = new THREE.AxesHelper(3)
obj.add(axesHelper)

addObjectToScene([obj]);


configPrimaryAnimation(new Map<string, Function>([
["a1", () => {
if (obj.material.map) {
const tmp = obj.material.map.offset.x
if (tmp <=-1) {
obj.material.map.offset.setX(0)
}else{
obj.material.map.offset.setX((-0.02)+tmp)
}

}
}]
]))
})

纹理偏移动画

纹理旋转

1
2
3
4
// 设置纹理旋转角度
texture.rotation = Math.PI * 0.5
// 设置纹理的旋转中心,默认(0,0)
texture.center.set(0.5, 0.5)

纹理旋转

canvas 作为纹理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const geo = new THREE.PlaneGeometry(1, 1)
const canvas = document.createElement('canvas')
//canvas 画布大小,单位 px
canvas.width=500;
canvas.height=500;
const ctx= canvas.getContext('2d');
if(ctx){
ctx.fillStyle='#3F535E'
ctx.fillRect(0,0,500,500)
ctx.fillStyle='pink'
ctx.font='30px solid'
ctx.fillText('你好 canvasTexture',10,50);
ctx.fillText('今天是2022-10-16 周末',10,100);
}
const canvasTexture = new THREE.CanvasTexture(canvas)

const mesh = new THREE.Mesh(geo, new MeshLambertMaterial({
map: canvasTexture,
side:THREE.DoubleSide,
}))

addObjectToScene([mesh])

canvasTexture纹理

视频作为纹理

视频作为纹理的不方便支持在于浏览器不允许带声音的自动播放

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
// 创建video对象
const video = document.createElement('video') as HTMLVideoElement
video.autoplay = true
video.muted = true
video.loop = true
video.id = "video"

const source = document.createElement('source') as HTMLSourceElement
source.src = require('./static/movie.mp4')
source.type = 'video/mp4'
video.appendChild(source)

window.addEventListener('load', (e) => {
video.play()
})

// video对象作为VideoTexture参数创建纹理对象
const texture = new THREE.VideoTexture(video)
texture.wrapS = texture.wrapT = THREE.ClampToEdgeWrapping;
//视频纹理是把视频播放后的画面进行采集图片渲染的
texture.minFilter = THREE.LinearFilter;
const geometry = new THREE.PlaneGeometry(4,4); //矩形平面
geometry.translate(2,2,0)
const material = new THREE.MeshPhongMaterial({
map: texture, // 设置纹理贴图
side: THREE.DoubleSide
});
const mesh = new THREE.Mesh(geometry, material); //网格模型对象Mesh

addObjectToScene([mesh])

纹理视频

凹凸贴图 ,法线贴图

法线贴图是凹凸贴图的一种

在3D计算机图形学中,法线贴图是一种用于伪造凹凸光照的技术,是凹凸贴图的一种实现。它用于添加细节,
而不使用更多的多边形。这种技术的一个常见用途是,通过从高精度多边形或高度图生成法线贴图,来极大地增强低精度多边形的外观和细节

帧动画

Threejs提供了一系列用户编辑和播放关键帧动画的API,例如关键帧KeyframeTrack、剪辑AnimationClip、操作AnimationAction、混合器AnimationMixer

  • AnimationMixer
    作为特定对象的动画混合器,可以管理该对象的所有动画
  • AnimationAction            
    为播放器指定对应的片段存储一系列行为,用来指定动画快慢,循环类型等
  • AnimationClip
    表示可重用的动画行为片段,用来指定一个动画的动画效果(放大缩小、上下移动等)
  • KeyframeTrack
    与时间相关的帧序列,传入时间和值,应用在指定对象的属性上。目前有 BooleanKeyframeTrack VectorKeyframeTrack 等。

自绘模型动画帧

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
const box = new THREE.BoxGeometry(1, 1, 1)
const sphere = new THREE.SphereGeometry(0.5)
const materia = new THREE.MeshLambertMaterial({ color: 'pink' })

const boxMesh = new THREE.Mesh(box, materia)
boxMesh.name = 'box';
const sphereMesh = new THREE.Mesh(sphere, materia.clone())
sphereMesh.name = 'sphere'
sphereMesh.translateX(1)
const group = new THREE.Group()
group.name = 'myGroup'
group.add(boxMesh)
group.add(sphereMesh)

const times = [0, 10]; //关键帧时间数组,离散的时间点序列
const values = [1, 0, 0, 5, 0, 0];//位置数据

//位置动画,sphere.position 这里的 sphere 是 mesh 的 name
const posTrack = new THREE.KeyframeTrack('sphere.position', times, values);
//颜色动画这里的颜色是0-1数据范围, 这里的 sphere 是 mesh 的 name
const colorKF = new THREE.KeyframeTrack('sphere.material.color', [3, 10],
[0.5, 0.5, 0, 0.5, 0.5, 1])
//缩放动画 这里的 box 是 mesh 的 name
const scaleTrack = new THREE.KeyframeTrack('box.scale', [0, 3], [1, 1, 1, 2, 2, 2]);

// duration决定了默认的播放时间,一般取所有帧动画的最大时间
// duration偏小,帧动画数据无法播放完,偏大,播放完帧动画会继续空播放
const duration = 10;
//动画剪辑器
const clip = new THREE.AnimationClip("default", duration, [posTrack, colorKF, scaleTrack]);

// group作为混合器的参数,可以播放group中所有子对象的帧动画
const mixer = new THREE.AnimationMixer(group);

// 剪辑clip作为参数,通过混合器clipAction方法返回一个操作对象AnimationAction
const animationAction = mixer.clipAction(clip);
//通过操作Action设置播放方式
animationAction.timeScale = 5;//默认1,可以调节播放速度
animationAction.loop = THREE.LoopOnce; //不循环播放,默认是循环播放
//暂停在最后一帧播放的状态,如果不循环播放就停在动画的最后一帧,而不是默认回到最初位置
animationAction.clampWhenFinished = true;
animationAction.play();//开始播放

//可以控制总的动画的开始时间
//animationAction.time=4
//如果更改动画的结束时间可开始时间等于是播放特定的帧
//clip.duration=4


//暂停动画的播放
const btn= document.querySelector('#pauseBtn')
btn?.addEventListener('click',()=>{
animationAction.paused= !animationAction.paused
})


addObjectToScene([group])

//配置初始动画
configPrimaryAnimation(new Map<string, Function>([
["a1", () => {
//混合器更新
clock.current && mixer.update(clock.current.getDelta());
}]
]))

动画帧

外部模型动画帧

变形动画

几何体的顶点的位置坐标发生变化,从一个状态过渡到另一个状态自然就产生了变形动画

微信跳一跳小游戏就是变形动画

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
var geometry = new THREE.BoxGeometry(5, 5, 5); //立方体几何对象
// 为geometry提供变形目标的数据
var box1 = new THREE.BoxGeometry(6, 3, 6); //为变形目标1提供数据
var box2 = new THREE.BoxGeometry(4, 10, 4); //为变形目标2提供数据

// 设置变形目标的顶点数据数组
geometry.morphAttributes.position = [];
//第一组变形数据
geometry.morphAttributes.position[0] = new THREE.Float32BufferAttribute(box1.getAttribute('position').array, 3);
//第二组变形数据
geometry.morphAttributes.position[1] = new THREE.Float32BufferAttribute(box2.getAttribute('position').array, 3);

var material = new THREE.MeshLambertMaterial({
color: 'red'
}); //材质对象
var mesh = new THREE.Mesh(geometry, material); //网格模型对象

//配置第一组顶点在没有动画的情况下的是否影响 0->不影响 1->完全影响 0-1 之间的状态
mesh.morphTargetInfluences && (mesh.morphTargetInfluences[0] = 1);
//配置第二组顶点在没有动画的情况下的是否影响 0->不影响 1->完全影响 0-1 之间的状态
mesh.morphTargetInfluences && (mesh.morphTargetInfluences[1] = 0);


//第一组顶点变化 时间在 0, 10, 20 之间 权重分别是 0, 1, 0 (0秒不会改变原来的顶点,10秒完全改变到设置的第一组变形顶点,20秒恢复到默认的顶点
const Track1 = new THREE.KeyframeTrack('.morphTargetInfluences[0]', [0, 10, 20], [0, 1, 0]);
//第二组顶点变化
const Track2 = new THREE.KeyframeTrack('.morphTargetInfluences[1]', [20, 30, 40], [0, 1, 0]);

const clip = new THREE.AnimationClip("default", 40, [Track1, Track2]);
const mixer = new THREE.AnimationMixer(mesh); //创建混合器
const AnimationAction = mixer.clipAction(clip); //返回动画操作对象
AnimationAction.timeScale = 5;
AnimationAction.clampWhenFinished = true;//暂停在最后一帧播放的状态
AnimationAction.play();

//配置初始动画
configPrimaryAnimation(new Map<string, Function>([
["a1", () => {
clock.current && mixer.update(clock.current.getDelta());
}]
]))

addObjectToScene([mesh])

变形动画帧

使用外部模型

大部分情况下对于复杂的模型,都是采用3D 软件设计模型导入到 THREE.js 进行交互

常用的外部模型:obj, fbx, gltf

主流的是 gltf(GL TransmissionFormat),即图形语言交换格式,它是一种3D内容的格式标准,由 Khronos Group管理
(Khronos Group还管理着OpenGL系列、OpenCL等重要的行业标准)
glTF的设计是面向实时渲染应用的,尽量提供可以直接传输给图形API的数据形式,不再需要二次转换;
glTF对OpenGL ES、WebGL非常友好,glTF的目标是:3D领域的JPEG;
GLTF具体的数据存储格式可以去官方网站上看:https://www.khronos.org/gltf/,使用 json 描述模型的结构

gltf 常用的格式:

  1. 用二进制格式(.glb),通常较小且独立,但不易编辑
  2. 嵌入式格式(.gltf), 其他模型利用转换工具也可以转成 .gltf 格式,基于json的文本文件
  3. 分离式的

如果不想使用THREE.JS 渲染 可以直接显示一个模型在网页里,还是伟大的谷歌公司开源的组件 model-viewer

谷歌公司开源的一个 JS 项目: model-viewer
项目 Github 地址:https://github.com/google/model-viewer
项目官网:https://modelviewer.dev/
项目介绍:Easily display interactive 3D models on the web & in AR
简单来说就是:在 Web 或 AR 中,一个简单的用来显示 3D 模型的 JS 库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'

const loader = new GLTFLoader()

//模型文件如果是本地加载的话,要放在 public 目录内
loader.load('models/untitled.gltf', gltf => {
gltf.scene.traverse(p=>{
if(p.type==='Mesh'){
const mesh= p as THREE.Mesh;
const material= mesh.material as THREE.MeshStandardMaterial 
material.color=new THREE.Color('pink')
}
})
gltf.scene.translateX(2)
addObjectToScene([gltf.scene])
}, undefined, error => {
console.log(error.message)
})

精灵模型和粒子系统

精灵模型对象Sprite。精灵模型对象和网格模型一样需要设置材质,不过精灵模型不需要程序员设置几何体,Threejs系统渲染的时候会自动设置。
通过Threejs精灵模型可以给场景中模型对象设置标签,也快成大量精灵模型对象模拟一个粒子系统,实现下雨或下雪的渲染效果

它的特性是,永远正对相机平面。不像广告牌,可以绕到后面看看,绕到侧面看看,它永远会正对着你。所以我们可以用它来显示一些标签,比如在可视化监控系统中,就可以显示传感器的数据,

模拟下雨

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
const texture = new THREE.TextureLoader().load(rain);
//创建精灵材质对象SpriteMaterial
const spriteMaterial = new THREE.SpriteMaterial({
color: 0xffffff,//设置精灵矩形区域颜色
//rotation:Math.PI/4,//旋转精灵对象45度,弧度值
map: texture,//设置精灵纹理贴图
});
const sprite = new THREE.Sprite(spriteMaterial);
//z 方向的分量不会使用
sprite.scale.set(1, 1, 0)

const group = new THREE.Group()

for (let index = 0; index < 800; index++) {
const clone = sprite.clone()
clone.position.set(100 * (Math.random()-0.5), 30 * (Math.random()), 100 * (Math.random()-0.6))
clone.rotateX(Math.PI * Math.random())
clone.rotateY(Math.PI * Math.random())
clone.rotateZ(Math.PI * Math.random())
group.add(clone)
}
const grasslandTextureLoader=new THREE.TextureLoader();
const grasslandTexture= grasslandTextureLoader.load(grassland)
const planeGeometry=new THREE.PlaneGeometry(120,120)
const plan=new THREE.Mesh(planeGeometry,new THREE.MeshLambertMaterial({map:grasslandTexture}))
plan.rotateX(-Math.PI*0.5)
plan.position.set(0,1,0)

addObjectToScene([group,plan]);
configPrimaryAnimation(new Map<string, Function>([
["a1", () => {
group.children.forEach(p => {
if (p.position.y < -1) {
p.position.y =30
}
p.position.y -= 0.2;

})
}]
]))

rainSprint

dat.gui 工具的使用

Google Data Arts Team 开源一款插件,three.js 发起人参与此开源项目,此工具最大优势就是可插拨的加入项目,由于改变对象的属性

官方文档https://github.com/dataarts/dat.gui/blob/master/API.md

在这个开源工具的基础有有基于react 开发的其他工具

GUI 会根据你设置的属性类型来渲染不同的控件

  1. 如果是Number 类型则用 slider来控制
  2. 如果是 Boolean 类型,则用 Checkbox来控制
  3. 如果是 Function 类型则用 button 来控制
  4. 如果是 String 类型则用 input 来控制
1
2
3
4
5
6
7
8
9
gui.add(text, 'noiseStrength').step(5); // 增长的步长
gui.add(text, 'growthSpeed', -5, 5); // 最大、最小值
gui.add(text, 'maxSize').min(0).step(0.25); // 最大值和步长

// 文本输入项
gui.add(text, 'message', [ 'pizza', 'chrome', 'hooray' ] );

// 下拉框形式选择文案
gui.add(text, 'speed', { Stopped: 0, Slow: 0.1, Fast: 5 } );

为了方便使用进行封装,见 hooks

dat-gui使用效果