基础概念

1. 场景 Scene
相当于现实中的话剧舞台, 可以包含人物, 沙发, 电视, 灯光, 水果, 金属, 玻璃等等
https://threejs-docs.netlify.app/docs/index.html#manual/zh/introduction/Creating-a-scene
Scene 对象就是3D世界的容器, 所有的对象, 包括灯光, 网格, 物体, 流体等都要放到这个容器里. 还包含背景, 烟雾等属性
创建场景
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>three.js app</title>
</head>
<body>
<script src='/src/js/three.js'></script>
<script>
// Our Javascript will go here.
</script>
</body>
</html> import { Scene } from 'three';
const scene = new Scene();
// 或: const scene = new THREE.Scene();
// 伪代码
scene.add('物体', '灯光', '环境', '...');2. 几何体 Geometry
简单理解: 物体的形状
几何体是3D图形的基本元素。表示3D物体的形状和大小,如立方体、球体、圆柱体等
常用内置几何体:
- 平面几何体 PlaneGeometry
- 立方几何体 BoxGeometry
- 圆形几何体 CircleGeometry
- 圆锥几何体 ConeGeometry
- 圆柱几何体 CylinderGeometry
- 球几何体 SphereGeometry
- 管道几何体 TubeGeometry
- ...
2-1. 计算机三维世界的组成
- 点
Three.Vector3 (x, y, z)
- 线
Three.Line
- 面
三个不在1条直线上的点可以组成1个三角形面, n个三角形面通过组合可以拼接成任何形状的物体

3. 基础材质 MeshBasicMaterial
简单理解: 皮肤
是threejs中的基本材质, 这种材质只会显示几何体的
颜色,不会受到光照影响
常用的内置材质
- 基础网格材质 MeshBasicMaterial
- 标准网格材质 MeshStandardMaterial
- 点材质 PointsMaterial
- 法线网格材质 MeshNormalMaterial
- 卡通网格材质 MeshToonMaterial
- MeshLambertMaterial (非光泽表面, 没有镜面高光)
- Phong网格材质 MeshPhongMaterial (具有镜面高光的光泽表面)
- ...
4. 网格对象 Mesh
网格用来将几何体和材质结合起来形成可视化的3D物体, 是 Three.js 中最基础的可视化对象
由几何体和材质两部分组成,可以被添加到
Three.js的场景中,并且可以被控制,如移动、旋转、缩放等

Mesh网格模型
为什么是三角形
- 三角形是最简单的多边形,少于3个顶点就不能成为一个表面;
- 三角形必然是平坦的,含4个或以上的顶点的多边形,不一定平坦,三个点确定一个平面,多余的点可能在这个面之上或者之下;
- 三角形经多种转换之后,仍然是三角形,这对于仿射转换和透视转换也成立。最坏的情况下,从三角形的边去看,三角形会退化为线段。在其它角度观察,仍能维持是三角形;
- 几乎所有商用图形加速硬件都是为三角形光栅化而设计的。
5. 相机 camera
相机是指场景中的观察者,它决定了你看到的是场景中的哪些内容. 3D 场景中可以放置多个镜头, 而且可以在相机之间进行切换
https://threejs-docs.netlify.app/docs/index.html#api/zh/cameras/Camera
5-1. 3D场景中用到的坐标系
- 世界坐标系
公共坐标, 比如公共场景对象
相机默认在世界坐标原点

- 本地坐标系
每个元素都有自己的坐标系, 也就是自身的本地坐标系
- 右手坐标系
手指方向均为正方向
5-2. 透视相机 PerspectiveCamera
模拟人眼看到的投影效果(近大远小)
透视相机
视锥
视锥宽高比 aspect
默认长宽比为1,即表示看到的是正方形, 那么如何适配不同的屏幕宽高比?
俯视角度
5-3. 正交相机 OrthographicCamera
设置left, right top, bottom, near, 和far指定长方体, 使得视野是平行的而不是透视的
正交相机视野
使用正交相机展示三视图
5-4. 其他相机
摄像机阵列(arraycamera), 立方相机(cubecamera)等
6. 渲染器: WebGLRenderer
- WebGLRenderer 将场景绘制到画布上
- WebGLRenderTarget 缓冲, 后台渲染
- WebGLMultipleRenderTargets 高级渲染器, 常用于后期处理等
6-1. threejs 渲染结构图

threejs 渲染结构图
所有的物体, 灯光, 纹理都要经过render才能看到, 也就是说 render 用来呈现结果, render 通过算法把 3D 场景投影成2D画面绘制到画布上
Mesh, Light 是单个对象, 而 Object3D, Group 可以添加n个子对象, 也就是可以对对象进行分组, 创建集合.
所有的物体, 灯光, 纹理等3D对象都要放到场景中才能被查看, 也就是说 Scene 是最底层的容器, 而且Scene 是树结构的对象. 也就是说可以通过遍历拿到所有的子集
相机在场景内, 也在场景外, 说明 Camera 比较特殊, 可以看向场景, 但看不到自己, 也可以不看向场景, 此时就是'黑屏', 可以对物体样式产生影响, 光线照到物体上与物体的材质产生反应, 比如变亮变暗, 变色等
Mesh(网格)继承自Object3D, 其自身包含两个部分, Geometry(几何体)和Material(材质)
Geometry(几何体)相当于HTML中的布局, 用来描述物体的形状, 具有物体顶点的相关信息, 比如顶点坐标, 索引等
Material(材质)相当于css, 用来描述物体的样式, 对应了webgl 中着色功能, 存储了物体的颜色, 阴影, 光泽度, 漫反射等, 这些效果可以通过 Texture 贴图来实现.
Texture(纹理)是种特殊的图像, 可以是png, img等格式, 也可以是canvas画布, 视频等形式
隐含信息:
相机也可以包含在Group中, 跟随物体移动
对 Group 应用缩放移动等方法, 内部的3D对象会同时被操作, 不用单独给每个3D对象应用变换
每个对象的变换都是相对于父级做相对运动, 如 #_5-3-正交相机 俯视视角
场景发生了移动, 观察者本地坐标不变, 但世界坐标发生了变化
Geometry Material Texture 均可以复用, 用于性能优化
threejs 内置了丰富的 Geometry/Material, 也可以通过建模创建自定义几何体/材质
7. 在React中使用
Demo1-0 使用 react 渲染
import * as THREE from 'three';
console.log(THREE);
const canvas = document.getElementById('c');
// 创建3D场景
const scene = new THREE.Scene();
// 创建立方体
const cube = new THREE.BoxGeometry(1, 1, 1);
// 设置立方体表面颜色
const material = new THREE.MeshBasicMaterial({
color: '#1890ff',
});
// 生成物体对象
const mesh = new THREE.Mesh(cube, material);
scene.add(mesh);
// 创建观察场景的相机
const camera = new THREE.PerspectiveCamera(75, width / height); // 透视相机
// 设置相机位置
camera.position.set(2, 2, 3); // 相机默认的坐标是在(0,0,0);
// 设置相机方向
camera.lookAt(scene.position); // 将相机朝向场景
// 将相机添加到场景中
scene.add(camera);
// 创建渲染器
const renderer = new THREE.WebGLRenderer({
canvas,
});
// 设置渲染器大小
renderer.setSize(window.innerWidth, window.innerHeight);
// 执行渲染
renderer.render(scene, camera);threejs 中没有单位, 都是向量
position.set 也可以写成: position.x = 2, position.y = 2, position.z = 3;
因为都是向量, 所以向量长度: mesh.position.length()
mesh.position.distanceTo(camera.position) 可以得到两个向量坐标的距离
.lookAt(new THREE.Vector3(2, 2, 3)) 可以让3D物体自动旋转朝向某个坐标
8. 遗留的问题
- 边缘有锯齿
- 没有3D效果
- 每个面颜色相同
9. 去除锯齿
创建渲染器时增加 antialias 属性
const renderer = new WebGLRenderer({
canvas,
antialias: true, // 抗锯齿
});
// 根据屏幕像素比进行渲染, 避免HiDPI设备上绘图模糊
renderer.setPixelRatio(window.devicePixelRatio);设置像素比效果更加明显
renderer 更多属性和方法, 详见官网:
.autoClear
...
.getClearColor()
.getContext()
.getCurrentViewport()
10. 3D效果
Demo1-1
// const material = new THREE.MeshBasicMaterial({
const material = new THREE.MeshLambertMaterial({
color: '#1890ff',
});MeshLambertMaterial 表面粗糙的材质,可以起镜面反射作用. 不适用于金属、玻璃等物体
11. 为什么换了材质却什么都看不见
Demo1-2
// 添加全局光照
const ambientLight = new THREE.AmbientLight(0xffffff);
scene.add(ambientLight);AmbientLight 是全局/环境光照
12. 添加方向光
Demo1-3
// 添加方向光
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
scene.add(directionalLight);DirectionalLight 是方向光
13. 给每个面添加不同的颜色
Demo1-4
const geometry = new THREE.BoxGeometry(1, 1, 1);
let mats = [];
console.log(geometry);
for (var i = 0; i < geometry.groups.length; i++) {
// 重新生成新的材质
const material = new THREE.MeshBasicMaterial({
color: new THREE.Color(Math.random() * 0xffffff),
});
mats.push(material);
}
const mesh = new THREE.Mesh(geometry, mats);
// ...THREE.Color 支持多种颜色格式
geometry.groups 中包含几何模型的元素
14. 坐标轴辅助工具
Demo1-5
const width = window.innerWidth;
const height = window.innerHeight;
// 创建3D场景
const scene = new THREE.Scene();
// 创建立方体
const geometry = new THREE.BoxGeometry(2, 2, 2);
// 设置立方体表面颜色
const material = new THREE.MeshBasicMaterial({
color: '#1890ff',
});
// 生成物体对象
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 辅助坐标系
const axesHelper = new THREE.AxesHelper(4);
scene.add(axesHelper);
// 创建观察场景的相机
const camera = new THREE.PerspectiveCamera(75, width / height); // 透视相机
// 设置相机位置
camera.position.set(2, 2, 3); // 相机默认的坐标是在(0,0,0);
// 设置相机方向
camera.lookAt(scene.position); // 将相机朝向场景
// 将相机添加到场景中
scene.add(camera);
// 创建渲染器
const renderer = new THREE.WebGLRenderer({
canvas,
antialias: true, // 抗锯齿
});
renderer.setPixelRatio(window.devicePixelRatio);
// 设置渲染器大小
renderer.setSize(width, height);
// 执行渲染
renderer.render(scene, camera);axesHelper 内置的坐标轴辅助工具
红色是x轴正方向, 绿色是y轴正方向, 蓝色是z轴正方向, 垂直于屏幕
15. 添加控制器
Demo1-6
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls';
const controls = new OrbitControls(camera, canvas);
// controls.enabled = false; // 禁用拖拽控制
controls.enableDamping = true; // 启用拖拽惯性效果轨道控制器是以物体为中心,对物体做环绕运动
为什么拖拽惯性没有生效?
// ...
let timer;
const tick = () => {
clearTimeout(timer);
timer = setTimeout(() => {
// update objects
controls.update();
// 重新渲染整个场景
renderer.render(scene, canvas);
// 调用下次更新函数
tick();
});
};
tick();16. 改写成 requestAnimationFrame
Demo1-7
const tick = () => {
// update objects
controls.update();
renderer.render(scene, canvas);
window.requestAnimationFrame(tick);
};
tick();17. 匀速运动
Demo1-8
mesh.rotation.set(Math.PI / 4, 0, 0, 'XYZ'); // 'XYZ' 表示旋转顺序的字符串 默认为 xyz
mesh.scale.set(3, 1, 1); // 物体缩放
let timestamp = Date.now();
const tick = () => {
const currentTime = Date.now();
const deltaTime = currentTime - timestamp; // 两次渲染时间间隔
timestamp = currentTime;
mesh.rotation.y += 0.001 * deltaTime; // 可以保持匀速
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
}所有3D对象都有4个用于变换的属性: position, scale, rotation, quaternion (四元数)
mesh.rotation.reorder('yxz') 可以设置旋转轴应用顺序, 使用 quaternion 方式也可以进行旋转, 但不用担心旋转轴应用顺序的问题
使用 rotation 或 quaternion, 两个属性都会同步更新
18. 加速运动
Demo1-9
const clock = new THREE.Clock();
const tick = () => {
const elapsedTime = clock.getElapsedTime(); // 获取时钟运行的总时长
// clock.getDelta(); 两次渲染时间间隔
// update objects
mesh.rotation.y += 0.005 * elapsedTime; // 加速
// 重新渲染整个场景
renderer.render(scene, camera);
// 调用下次更新函数
window.requestAnimationFrame(tick);
}19. 物体做圆周运动
Demo1-10
// 添加网格辅助线
const gridHelper = new THREE.GridHelper(100, 50, 0xcd37aa, 0x4a4a4a);
scene.add(gridHelper);
const tick = () => {
cube.position.y = Math.sin(elapsedTime);
cube.position.x = Math.cos(elapsedTime);
}
tick();物体运动的方法 1
19-1. 相机做圆周运动
Demo1-10
const tick = () => {
camera.position.y = Math.sin(elapsedTime);
camera.position.x = Math.cos(elapsedTime);
camera.lookAt(mesh.position);
}
tick();物体运动的方法 2
20. 监听窗口大小变化
Demo1-11
window.addEventListener( 'resize', function () {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}, false );camera.aspect 相机视锥体长宽比
在大多数属性发生改变之后,需要调用 .updateProjectionMatrix 来使得这些改变生效。
21. 画布性能监控
Demo1-12
import Stats from 'three/examples/jsm/libs/stats.module';
// 创建性能监视器
const stats = new Stats();
// 设置监视器面板,传入面板id(0: fps, 1: ms, 2: mb)
stats.setMode(0);
// 将监视器添加到页面中
document.body.appendChild(stats.domElement);
const tick = () => {
// ...
// 更新帧数
stats.update();
// 调用下次更新函数
window.requestAnimationFrame(tick);
}
手指方向均为正方向