Three.js学习记录

154 阅读7分钟

1.1 前言

客户需求要能预览 3D 口腔模型,来观察上传的模型是否正确。后期可能还需要编辑,我们决定使用 three.js 开发,本文是我初学 three.js 的记录总结。

1.2 四要素

  1. 创建场景 Scene
  2. 添加物体 Mesh(形状 Geometry + 外观 Material)
  3. 添加摄像机 Camera,为了获得一个视角
  4. 创建渲染器 Renderer,把摄像机看到的内容画出来

默认情况下,所有东西都在场景的中心点(0, 0, 0),所以看起来一片黑,需要调整摄像机的位置。

1.3 对象变换

所有继承自 Object3D 的类,比如:Mesh、Camera、Group

都能使用它的属性、方法和事件,比如:Transform(变换)属性、lookAt

Transform 属性: position(位置)、scale(缩放)、rotation(旋转)、Quaternion(四元数)

position:对象位置,是 Vector3(三维向量),默认 x 轴向右,y 轴向上,z 轴向后

  • set:设置向量的 x、y、z,也可以通过 position.x(/y/z)单独设置
  • length:获取向量到中心点的直线距离
  • distanceTo:获取两个向量之间的距离
  • normalize:向量方向不变,但长度设置为 1 在空间中定位物体是比较困难的,可以使用 AxesHelper 来帮助定位 AxesHelper:轴辅助工具,在场景中显示坐标轴,X 轴为红色,Y 绿色,Z 蓝色

scale:对象缩放,也是 Vector3,默认是(1, 1, 1),和 position 用法一样

rotation:对象旋转,是 Euler(欧拉角),默认是(0, 0, 0, 'XYZ')

  • 旋转首先要考虑绕哪个轴旋转,找到旋转轴,然后再考虑轴的旋转顺序
  • set:设置欧拉的 x、y、z 和旋转顺序,以弧度为单位
  • reorder:设置旋转顺序,如果使用这个方法,必须先设置旋转顺序,再单独设置 x、y、z
  • 旋转 Math.PI 正好是半圈,以此类推:一圈 = Math.PI _ 2 = 3.14 _ 2 = 6.28 弧度
  • 万向节死锁(Gimbal Lock):写的是按 x 轴旋转,实际却按照 z 轴旋转的问题,旋转轴不生效了

Quaternion:四元数,旋转的另一种表示方式,更复杂可靠,可以避免万向节死锁

  • 有时候会得到四元数形式的坐标,只需要将它应用到对象上,旋转就会更新

lookAt:旋转对象使它“看向”某个 Vector3,就是 z 轴指向目标,可以修改 up 属性来调整方向

在创建复杂对象的时候,比如一个房子,里面有门、墙、窗户等,怎么对整体进行 Transform,而不是一个个设置?这时就需要 Group,创建一个组,把所有东西都 add 进去,再操作组。

1.4 动画

在 three.js 中做动画就像制作【定格动画】,移动物品,拍照,再移动,再拍照…

FPS:frames per second,每秒帧数,60FPS 表示屏幕每 1 秒经过 60 次刷新

window.requestAnimationFrame:在下一帧调用函数

const tick = () => {
  console.log('tick');
  // 更新对象
  mesh.rotation.y += 0.01;
  // 重新渲染
  renderer.render(scene, camera);
  // 在下一帧调用 tick 函数,然后 tick 函数执行又会调用 requestAnimationFrame,形成循环
  window.requestAnimationFrame(tick);
};
tick();

帧率越高,执行次数越多,导致动画效果越快,我们希望动画效果在任何帧率下都保持一致,怎么做? 方法一:利用时间差,因为不管是什么电脑,好的坏的,每一秒经过的时间是相等的

let lastTime = Date.now();
const tick = () => {
const currentTime = Date.now();
const deltaTime = currentTime - lastTime;
lastTime = currentTime;
mesh.rotation.y += 0.001 _ deltaTime; // 0.001 弧度/毫秒 = 每秒 1 弧度
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
};
tick();

方法二:利用 three 提供的 Clock

const clock = new THREE.Clock(); // 创建时钟并默认启动
const tick = () => {
const elapsedTime = clock.getElapsedTime(); // 获取时钟启动以来经过的秒数
console.log('elapsedTime:', elapsedTime);
mesh.rotation.y = elapsedTime _ (Math.PI \* 2); // 每秒转一圈
renderer.render(scene, camera);
window.requestAnimationFrame(tick);
};
tick();

用数学函数实现的简单动画效果:

// 正弦,动画效果:上下移动,像电梯,一开始在 0
mesh.position.y = Math.sin(elapsedTime);

// 余弦,动画效果:也是上下移动,一开始在 1
mesh.position.y = Math.cos(elapsedTime);

// 正弦和余弦组合,动画效果:顺时针旋转(把 x 和 y 调换就是逆时针旋转)
mesh.position.x = Math.sin(elapsedTime);
mesh.position.y = Math.cos(elapsedTime);

如果你需要实现复杂的动画,那就使用动画库,最常用的是 GSAP,它能做时间线动画

gsap.to(mesh.position, { x: 2, duration: 1, delay: 0 });
gsap.to(mesh.position, { x: 0, duration: 1, delay: 1 });
const tick = () => {
  // 不要把 gsap 的执行放在这里,因为 gsap 内部会自行调用 requestAnimationFrame 更新 x 属性,我们只需要在每一帧都进行渲染即可,动画自然就有了
  renderer.render(scene, camera);
  window.requestAnimationFrame(tick);
};
tick();

1.5 相机和控制器

1.5.1 相机

PerspectiveCamera:最核心,最常用的相机,模拟人眼所见,透视物体近大远小。常用于:3D

  • fov:垂直视野角度,决定了相机“看”出去的广度。建议值在 25-75 之间,25 的效果类似望眼镜,75 的效果类似广角镜头,能看到更多的物体,但边缘的物体会明显变形
  • aspect:宽高比,渲染宽度除以渲染高度
  • near:近裁剪面,任何距离小于 near 值的物体都不会被渲染,常用值 0.1
  • far:远裁剪面,常用值 100,最好设置成能容纳场景中最远物体的距离
  • 合理设置 near 和 far,否则可能导致 Z-fighting
  • 修改属性后,必须调用 updateProjectionMatrix 以生效

OrthographicCamera:渲染出来的场景,没有透视效果,物体远近一样大。常用于:2D 游戏、固定在屏幕上的 UI 界面

  • 左/右/上/下边界:保持宽高比
  • 也有 near 和 far 参数

CubeCamera:从一个点向周围六个方向(上、下、左、右、前、后)拍摄场景,生成一个“环境贴图”(CubeMap),给其他物体对象使用。常用于:反射

ArrayCamera:摄像头阵列(想象成“监控墙”),用多个子相机来渲染场景,通过不同的视角渲染同一场景的不同部分。常用于:分屏多人游戏

StereoCamera:用两台相机来渲染场景,模拟一双眼睛,创造出立体感。常用于:VR 开发

1.5.2 控制器

OrbitControls:轨道控制器,最常用

  • new OrbitControls(object:要控制的相机, domElement:用于事件监听的元素)
  • target(Vector3):更新旋转焦点
  • enableDamping: 启用阻尼(惯性)效果,注意开启后必须在动画循环中调用 update()
  • update():更新控件设置,所有设置更新后,都需要重新调用以生效

TrackballControls:轨迹球控制器,类似 OrbitControls,但更自由没有限制角度(老旧)

ArcballControls:弧球控制器,比 TrackballControls 更好,用于查看一个物体的细节

MapControls:地图控制器,类似 OrbitControls,但专为俯瞰视角优化


FirstPersonControls:第一人称控制器,y 轴被限制且不能翻转(老旧)

PointerLockControls:指针锁定控制器,比 FirstPersonControls 更好,用于任何第一人称的游戏(FPS)或应用


FlyControls:飞行控制器

DragControls:拖拽控制器,用来移动场景中的物体

TransformControls:变换控制器,用于编辑物体位置、旋转和缩放

1.6 全屏渲染

始终充满视口:

.webgl {
position: fixed;
top: 0;
left: 0;
}

const sizes = {
width: window.innerWidth,
height: window.innerHeight,
};

window.addEventListener('resize', () => {
// 更新 sizes 对象中的宽高
sizes.width = window.innerWidth;
sizes.height = window.innerHeight;

// 更新相机的 aspect(宽高比),防止物体变形
camera.aspect = sizes.width / sizes.height;
camera.updateProjectionMatrix();

// 更新渲染器的尺寸,并设置像素比,防止在高分辨率屏幕上出现模糊
// 2 像素比是上限,像素比再高,肉眼也看不出来,还会增大 GPU 的渲染压力
renderer.setSize(sizes.width, sizes.height);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
});

1.7 模型预览/编辑

1.7.1 3D 模型常见格式

常见的 3D 模型格式(5 个):stl、obj、ply 以及为 web 优化的 gltf/glb

image.png 做口腔模型的软件:3Shape、EXO(exocad)

1.7.2 模型预览/编辑

如果只是需要预览模型,并且后期也没有编辑模型的需求,那可以使用 Online3DViewer,支持格式多,预览效果清晰,缺点是不好扩展

如果预览只是早期功能,后期可能需要编辑模型,那就使用 three 自己开发。可以参考 three editor 的源码,常见格式直接拖拽进去都能预览编辑

  • ply 格式需要计算顶点法线、启用顶点颜色,否则不能正确预览
  • 多文件的 obj、gltf 格式,需要加载全部文件才能正确预览,所以一般是拖拽一个压缩包(包含所有模型信息文件),然后解压预览

从 three editor 迁移的模型预览功能(仅做参考):3DModelViewer

如果要做 3D 模型的编辑,强烈建议:后端在源头将所有模型统一转换为 .GLB 格式。为什么?

  1. glb 是为 web 而生的格式,二进制、文件小、加载快,并且可以内嵌材质和动画信息
  2. 统一的格式能极大简化前端处理逻辑,否则不同格式需要不同的加载器,前端分别处理会很复杂麻烦

1.8 最后

学习资料:threejs-journey.com/

2025/07/19:发布文章