快速笔记
前言
本书使用的 Three.JS 版本为 r147. 需要注意的是, 安装不同的版本, 使用 TS 的时候 @types/three 的版本也需要对应.
其他没什么重要的, 然后介绍了每一章的主要内容.
ch01 使用 Three.js 创建第一个3D场景
快速介绍了 three.js 的背景, 以及常见的应用场景. 并给出了即将介绍的三个案例:
- 官网的一个示例
- 几何图形 (这里案例会在本章详细介绍)
- 加载现有模型
使用 three.js 可以实现:
- 在浏览器中创建并渲染 3D 几何体
- 创建 3D 场景的动画
- 在对象上使用纹理与材质
- 使用不同的灯光来展示场景
- 使用其他程序导出的 3D 模型
- 添加后期特效
- 使用不同着色器 (shader)
- 粒子云
- VR (virtual reality) 与 AR (augmented reality) 场景
简要说明了实现 3D 特效还可以使用 CSS 技术. 但这不在本书讨论范围内.
作者简要介绍了基本要求
- 文本编辑器 (推荐 VSCode)
- 浏览器 (作者使用 火狐)
- git 等工具, 然后下载随书源代码, 并介绍如果演示与运行, 以及修改代码并预览
作者使用 webpack 并封装了一些工具, 这里不会详细介绍.
作者 node 环境 使用 16.14.0.
然后详细介绍了几何体的创建过程
- HTML 骨架
- JS 骨架
- 每一个 3d 应用都需要三个对象, 场景, 摄像机, 以及渲染器
- 基本步骤:
- 创建场景
- 创建摄像机
- 创建渲染器
- 创建灯光
- 创建几何体: 立方体, 环面扭结体
- 创建一个大的平面
- 创建轨迹控制工具
- 添加帧数统计工具
- 渲染场景
然后是一步步演示代码
注意, 作者在创建场景后, 设置场景的背景色使用
backgroundColor, 该语法有误. 应该使用background属性, 赋值为THREE.Color(0xffffff).
案例添加的步骤为:
- 创建静态场景, 添加立方体, 曲面扭结体
- 添加平面, 灯光等
- 介绍动画, 并说明了
requestAnimationFrame与setTimeout的区别. - 让几何体运行起来
- 介绍第三方工具
lil-gui, 并引入修改运动速度 - 介绍内置的统计帧数模块
- 介绍内置的轨迹控制器 (
OrbitControls)
虽然书中没有介绍, 但现在使用
vite是主流工具, 官方网站在文档中已经加以说明. 直接基于vite和TS进行开发.
整个代码都很 OOP, 创建对象, 设置属性, 添加到场景. 对于工具, 创建工具, 设置属性, 加入页面 (DOM), 在动画中 render 或 update.
创建几何对象的基本思路:
- 创建骨架数据 (
Geometry对象) - 创建材质 (
material对象), 用于设置颜色等外观 - 创建网格 (
mesh对象), 利用骨架数据和材质数据来生成几何体 - 在将几何体添加到场景中
最后演示了保时捷的模型加载, 仅仅是演示. 然后介绍了一个辅助工具 (也是演示), 来显示坐标系, 平面坐标网格, 极坐标网格.
ch02 组成 threejs 应用的基本组件
本章可以理解为第一章那个案例的进阶. 本章会介绍构成 threejs 应用的基本组件. 并且一些高级组件的用法与基础组件的用法思路一致.
本章主题:
- 创建场景
- 几何体 (
Geometry) 与网格 (Mesh) 是如何关联的 - 在不同场景 (
Scene) 使用不同的相机 (Camera)
2.1 创建场景
从 ch01 了解到, 一个应用应该包含: 相机, 灯光, 网格, 与渲染器.
而场景是一个主容器, 其中包含了需要渲染的所有内容. 场景常常被称为场景图, 如其名, 它并不是一个数组, 而是一个树结构. three.js 提供了 THREE.Group 对象作为容器, 存储几何对象. 而该对象, 以及其他几何对象均继承自 THREE.Object3D 对象. 而场景也继承自 THREE.Object3D, 从抽象树的角度来看, 就可以统一维护了.
场景的基本功能
要展示场景的功能与配置, 参考随书示例 (chapter-2/basic-scene.html), 运行起来后, 利用 lil-GUI 来修改配置, 直观的查看 (作者来做了提示可以用鼠标交互).

开始加载项目时, 只有一个地板, 似乎什么也没有

但实际上其中至少有三个对象
THREE.Mesh, 用于表示地板对象.THREE.PerspectiveCamera用于展示可以看到的内容.- 其中添加了环境光源 (
THREE.AmbientLight) 与直线光源 (THREE.DirectionalLight).
然后作者说明了源代码的位置, 以及其引用了什么代码. 然后作者抽象出代码逻辑:
- 创建透视相机
- 创建渲染器
- 创建场景
- 创建光源
- 创建地板
const camera = new THREE.PerspectiveCamera(75, w / h, 0.1, 1000)
const renderer = new THREE.WebGLRenderer({ antialias: true })
const scene = new THREE.Scene()
// 添加光源
scene.Add(new THREE.AmbientLisght(0x666666))
scene.Add(THREE.DirectionalLight(0xaaaaaa))
// 创建地板
const geo = new THREE.BoxBufferGeometry(10, 0.25, 10, 10, 10, 10)
const mat = new THREE.MeshStandardMaterial({ color: 0xffffff })
const mesh = new THREE.Mesh(geo, mat)
scene.Add(mesh)
然后作者对代码进行了解释, 并说明可以加入动画, 以及轨迹控制等内容.
最后作者提示用户如何操作案例, 操控右上角的控制工具.
添加与移除对象
lil-gui 使用很方便, 使用函数会自动加载成按钮, 使用 boolean 值自动转换为复选框, 使用数字, 定义范围会成为拖拽框, 使用数组就是下拉框.
- 添加立方体就是, 随机生成位置坐标, 颜色, 尺寸, 然后添加到场景中 (数据会存储在
scene.children中). - 移除立方体就是
scene.children.pop().
const addCube = (scene) => {
const color = randomColor()
const pos = randomVector({
xRange: { fromX: -4, toX: 4 },
yRange: { fromY: -3, toY: 3 },
zRange: { fromZ: -4, toZ: 4 }
})
const rotation = randomVector({
xRange: { fromX: 0, toX: Math.PI * 2 },
yRange: { fromY: 0, toY: Math.PI * 2 },
zRange: { fromZ: 0, toZ: Math.PI * 2 }
})
const geometry = new THREE.BoxGeometry(0.5, 0.5, 0.5)
const cubeMaterial =
new THREE.MeshStandardMaterial({
color: color,
roughness: 0.1,
metalness: 0.9
})
let cube = new THREE.Mesh(geometry, cubeMaterial)
cube.position.copy(pos)
cube.rotation.setFromVector3(rotation)
cube.castShadow = true
scene.add(cube)
}
const removeCube = (scene) => {
scene.children.pop()
}
其中 randomVector 就是在指定范围生成随机数, 然后返回 THERE.Vector3 实例. randomColor 是随机生成三原色来创建 THREE.Color 对象.
然后作者介绍了这段代码的逻辑. 其中强调了 name 属性, 随书示例代码中为每一个创建的立方体添加了一个名字 cube.name = 'cube-' + parent.children.length.
该名字有一个非常重要的作用, 可用于调试. 使用场景的 getObjectByName(name) 方法可以直接获得该对象的引用, 调试中可以直接设置其属性来进行调试.
Three.js 的 Scene 提供了很多有用的方法:
add()方法, 将对象添加到场景中. 与DOM一样, 一个对象只允许有一个父节点, 如果对象已经在某个节点下, 那么会被移动到新位置中.attach()方法, 附加对象, 作用与add类似, 但会保留该对象被作用的平移与旋转 (这个有待测试).getObjectById()方法. 当对象添加到场景中时, 会根据添加的顺序 (base-1) 生成id属性值. 根据id值, 该方法可以获得该对象的引用.getObjectByName()方法. 基于name来查找对象 (需要提前为对象的name赋值).remove()删除对象.clear()清空所有对象.
这些方法由 THREE.Object3D 提供, THREE.Scene 继承自该类. 这些方法会贯穿整本书, 用于操作场景中的对象.
添加 fog
fog 属性用于在场景中添加雾影特效. 即远离相机的界面会慢慢消失.
添加 fog 的方法就是实例化, 为场景的 fog 属性赋值即可. 有两个对象可以使用: THREE.Fog 和 THREE.FogExp2.
THREE.Fog设置near和far, 用于设置雾的范围, 它是线性变化的.THREE.FogExp2设置颜色与浓度. 它是指数变化的.
按照作者的意思, 建议在使用时进行测试选择.
修改背景
修改背景的方法可以调用 renderer.clearColor(颜色数值) 来实现. 还可以通过设置场景的 background 属性来修改. 有三种选项:
- 修改为固定颜色.
- 可以使用
texture纹理. 它本质是一张图片, 以展开来填满整个屏幕. 第10章会进行讨论. - 使用环境题图. 它本质也是纹理 (texture), 但它完全包裹相机, 支持随相机的移动而移动.
这里是设置的 canvas 的背景色, 而不是页面的 body 背景色, 如果需要一个透明的 canvas, 可以在渲染器中设置 alpha 为 true.
const renderer = new THREE.WebGLRenderer({ alpha: true })
在随书案例中, 右上角 lil-GUI 的 backGround 下拉框中可以演示不同的效果.

第10章会讨论 Texture 和 Cubemap 的细节. 下面仅仅是快速看看其如何实现.
// 设置 null, 清空
scene.background = null;
// 设置简单颜色
scene.background = new THREE.Color(0xff0000);
// 使用 TextureLoader 来加载纹理
const textureLoader = new THREE.TextureLoader();
textureLoader.load('path.jpg', loaded => {
scene.background = loaded;
});
// cubemap 也使用 TextureLoader 来加载
textureLoader.load('path.jpg', loaded => {
loaded.mapping = THREE.EquirectangularReflectionMapping;
scene.background = loaded;
});
注意加载纹理是异步的.
更新场景中的所有材质
之前看到创建一个立方体, 需要准备 Geometry 和指定的 Material, 场景可以强制更新场景中所有对象的材质. 场景有两个属性可以修改所有的材质.
- 一是为场景 (
THREE.Scene) 实例的overrideMaterial属性赋值. 会覆盖场景中所有的材质. - 一是创建环境映射 (environment map), 它可模拟网格所在环境, 并可提供反光等特性, 让材质看起来更为真实.
创建环境映射的步骤:
- 加载材质
- 设置材质的
mapping属性 - 为场景 (
Scene) 的environment属性赋值
textureLoader.load('path.jpg', loaded => {
loaded.mapping = THREE.EquirectangularReflectionMapping
scene.environment = loaded
})
然后作者提示在案例中体验其区别, 使用环境映射的方法, 会有表面反光特效, 这一点与单一的颜色是不同的.
2.2 几何体与网格的关联
每次在使用几何体时, 都会创建几何对象, 材质对象, 然后创建网格对象将其整合. 最后才会将网格对象添加到场景中.
geometry 的功能与属性
three.js 内置了很多几何对象. 随书案例中 (chapter-2/geometries) 列举了一些几何体:

在第 5 章与第 6 章会讨论几何体. 这里仅仅讨论几何体是什么.
threejs 中所有的几何体, 所有的 3D 模型都是由 3D 空间中的点集所构成. 这些点也称为顶点. 而面都通过这些顶点连接起来. 以立方体为例:
- 立方体有 8 个顶点, 这些顶点可以定义 x, y, z 轴. 每一个顶点都是 3D 空间中的点
- 立方体有 6 个面. 每一个角都有一个顶点. 3D 模型中, 每一个面都是由三个顶点构成的三角形来组成. 因此立方体的每一个面都是由两个三角形所构成.
置于三角形什么的, 可以深入一下图形学的资料.
如果使用 three.js 提供的几何体, 不需要我们来创建顶点与面, 只需要使用对应的类 (构造函数), 与参数, three.js 会生成正确的顶点与面, 来构成几何体.
也可以手动的创建:
const v = [
[1, 3, 1],
[1, 3, -1],
[1, -1, 1],
[1, -1, -1],
[-1, 3, -1],
[-1, 3, 1],
[-1, -1, -1],
[-1, -1, 1]
]
const faces = new Float32Array([
...v[0], ...v[2], ...v[1],
...v[2], ...v[3], ...v[1],
...v[4], ...v[6], ...v[5],
...v[6], ...v[7], ...v[5],
...v[4], ...v[5], ...v[1],
...v[5], ...v[0], ...v[1],
...v[7], ...v[6], ...v[2],
...v[6], ...v[3], ...v[2],
...v[5], ...v[7], ...v[0],
...v[7], ...v[2], ...v[0],
...v[1], ...v[3], ...v[4],
...v[3], ...v[6], ...v[4]
])
const bufferGeometry = new THREE.BufferGeometry()
bufferGeometry.setAttribute('position', new THREE.BufferAttribute(faces, 3))
bufferGeometry.computeVertexNormals()
这是生成 Geometry 数据的代码. 配合材质与网格后:
const material = new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true })
const cube = new THREE.Mesh(bufferGeometry, material)
scene.add(cube)
得到下面效果

然后作者介绍了每一段代码的含义.
- 在数组
v中定义了构成该立方体需要的点. 使用这些点来创建面. - 在
three.js中需要在Float32Array数组中提供所有面的信息. 每一个面由三个顶点构成, 因此我们创建一个面需要 9 个数字, 构成三个顶点的x,y,z值. 然后作者简要解释了解构运算符, 以及得到的结果. - 需要注意的是构成面的数组的顺序. 顺序决定了
three.js是否判定其为正面.- 使用顺时针方向表示面对相机方向
- 使用逆时针表示背对相机方向
这个逆时针与顺时针需要验证.
添加辅助用的坐标系
const axes = new THREE.AxesHelper(20)
scene.add(axes)

其中 x, y, z 轴分别对应红, 绿, 蓝色. 使用右手标架.
可以利用
position属性来调整x,y,z值来验证.
然后作者简要说明了四边形方格与三角形方格. 一般选用方格更容易建模, 但在游戏等渲染要求高的情况, 三角方格效率更高. 在随书案例中 chapter2/custom-geometry 演示了这个矩形, 并提供了工具栏, 来控制各个参数, 可以调整形状.
但是为了性能, three.js 会有一个假定, 就是每一个对象在其生命周期中都是不会变的 (和字符串逻辑一样). 如果修改背后的数组, 即修改本例中为 const faces = new Float32Array([...]) 数组, 那么需要通知 three.js 更新. 这里需要修改属性 needsUpdate:
mesh.geometry.attributes.position.needsUpdate = true;
mesh.geometry.computeVertexNormals();
这里需要重新计算的很多, 细节会在 ch10 章中详细讨论.
作者对代码进行了较高度的封装.
Geometry 实例有一个 clone() 方法可以赋值几何体, 使用该方法可以很容易的创建出不同材质的网格. 获得点数组后, 就可以修改坐标.
const cloneGeometry = scene => {
const clonedGeometry = bufferGeometry.clone()
const backingArray = clonedGeometry.getAttribute('position').array
// 修改点的 x 坐标值
for (const i in backingArray) {
if ((i + 1) % 3 === 0) {
backingArray[ i ] = backingArray[ i ] + 3
}
}
clonedGeometry.getAttribute('position').needsUpdate = true
const cloned = meshFromGeometry(clonedGeometry)
cloned.name = 'clonedGeometry'
const p = scene.getObjectByName('clonedGeometry')
if (p) scene.remove(p)
scene.add(cloned)
}
const meshFromGeometry = geometry => {
const materials = [
new THREE.MeshBasicMaterial({ color: 0xff0000, wireframe: true }),
new THREE.MeshLambertMaterial({ opacity: 0.1, color: 0xff0044, transparent: true })
]
const mesh = createMultiMaterialObject(geometry, materials)
mesh.name = 'customGeometry'
mesh.children.forEach(e => {
e.castShadow = true
})
return mesh
}
使用 clone() 函数获得几何体对象的点集副本. 通过修改点集对象的值就可以修改坐标 (也可以使用 TranslateX ). 这里使用了自定义方法 meshFromGeometry(). 这个方法中, 在一个网格中使用了多个材质, threejs 扩展库中提供了一个方法 createMultiMaterialObject 来实现. 这个实现本质上是创建了一个 Group, 在同一个位置创建了多个实体, 不同实体使用不同的材质. 让将所有实体存储在这个 Group 中.
import { createMultiMaterialObject } from "three/examples/jsm/utils/SceneUtils";
需要注意的是, 如果要使用阴影, 表示每一个对象都需要设置.
有种图层的感觉.
上述代码中使用 createMultiMaterialObject 来添加线框, three.js 还有一个新方法来添加线框: THREE.WrieframeGeometry. 例, 有一个 Geometry 名为 geom, 那么可以使用:
const wireframe = new THREE.firewareGeometry(geom)
然后使用 THREE.LineSegments 对象来绘制线
const line = new THREE.LineSegments(wireframe)
接着将其添加到场景中:
scene.add(line)
该辅助方法内部是使用 THREE.Line 对象, 可以设置样式来控制显示, 例如:
line.material.linewidth = 2
网格 (mesh) 的属性与方法
来看下面属性
position表示对象在父容器中的位置.rotation用于控制对象沿轴线的旋转.three.js提供了几个快捷方法:rotateX(),rotateY(), 和rotateZ().scale控制对象沿某轴进行缩放.translateX()/translateY()/translateZ()控制对象沿某轴移动.lookAt()用于指向控制, 用这也算是手动控制旋转的一种方法.visible控制该网格是否被渲染.castShadow控制是否渲染阴影. 考虑性能, 默认是false.
旋转对象就是将对象在其轴上进行旋转 (轴可以是 x, y, z 三个轴). 有三个方法控制旋转
rotateN(), 这里的N指X,Y,Z. 它表示在 局部空间 (local space) 中进行旋转. 即 3D 对象的父容器中旋转. 如果对象直接放在场景中, 就是针对场景的轴进行旋转; 如果对象放在某个组中, 那么就以该对象的父容器的轴进行旋转.rotateOnWorldAxis(), 基于世界坐标系进行旋转.rotateOnAxis()基于对象空间 (object space) 进行旋转.
在随书案例中 chapter-2/mesh-properties 可以通过控制右侧的控制来演示查看特效.
这部分都待验证. 并验证其代码实现.
通过 position 属性来设置网格位置
有三种用法:
分别设置
cube.position.x = 10 cube.position.y = 3 cube.position.z = 1一次性设置
cube.position.set(10, 3, 1)对象赋值
cube.position = new THREE.Vector3(10, 3, 1)
使用 rotation 属性定义网格旋转
可用方法
cube.rotation.x = 0.5 * Math.PI
cube.rotation.set(0.5 * Math.PI, 0, 0)
cube.rotation = new THREE.Vector3(0.5 * Math.PI, 0, 0)
默认使用弧度制. 如果想要使用角度制, 需要进行转换
cube.rotation.x = degrees * (Math.PI / 180)
three.js 中提供了 MathUtils 类, 来提供一些辅助方法.
jk: 置于提供了什么方法, 书中的案例没有详细的介绍.
使用 translate 来修改位置
使用 translate 可以修改对象的位置. 该方法不是设置绝位置, 而是从当前对象的位置进行调整. 在随书案例的 chepter-2/mesh-properties 中.
设置
visible可以控制对象的显示与隐藏, 连同阴影也会显示与隐藏.
2.3 使用不同的相机
three.js 提供了两种相机:
- 透视相机
PerspectiveCamera - 正交相机
OrthographicCamera
three.js 还支持一些特殊的相机 (和 VR 等有关), 有关细节不在本书范围, 可以参考:
- Anaglyph effect
- Parallax barrier
- Stereo effect
基本上只要切换相机, 其他内容交给 Threejs 即可.
要体验简单的 VR 相机, 只需要使用 THREE.StereoCamera 来创建 3D 场景.
有关 WebVR 的细节可以参考 https://webvr.info/developers/
正交相机与透视相机
随书案例为 chapter2/cameras

正交相机将所有的立方体渲染成相同尺寸, 不在意物体间的距离和相机距离的时候使用. 一般在一些 2D 游戏中使用.
透视相机的属性
首先看看 THREE.PerspectiveCamera.
作者说, 可以在随书案例中修改对应参数来查看效果.

fovField of View, 视野, 表示在相机中可以看到的场景内容. 人类的视野大概是 180 度. 但是鸟类可以达到 360 度. 但是一般屏幕无法达到该角度. 一般在游戏中会使用 60 到 90 度. 默认会使用 50 度.aspect宽高比, 一般会使用window.innerWidth / window.innerHeight.near表示渲染的场景可以离相机有多近, 一般会是一个很小的值, 通常使用0.1.far类似于near, 但是取值不能太小, 否则超出的不显示. 一般可以取100.zoom允许在场景中缩放. 取值如果大于 1 则是放大 (zoom in), 小于 1 则是缩小 (zoom out).
zoom没演示出来其作用.
下图展示了这些参数的作用

fov 表示水平视野的角度, 基于 aspect 决定垂直视角. 渲染的内容只会渲染在 near 到 far 之间的内容. 超出的不渲染.
正交相机
正交相机不是近大远小, 将所有的尺寸按照等尺寸渲染. 在定义一个正交相机时, 就是在定义一个渲染的矩形空间 (立方体空间)
leftrighttopbottomnearfarzoom

export class OrthographicCamera extends Camera {
/**
* @param left Camera frustum left plane.
* @param right Camera frustum right plane.
* @param top Camera frustum top plane.
* @param bottom Camera frustum bottom plane.
* @param [near=0.1] Camera frustum near plane.
* @param [far=2000] Camera frustum far plane.
*/
constructor(left?: number, right?: number, top?: number, bottom?: number, near?: number, far?: number);
...
Looking at specific points
前面介绍的是如何创建相机, 以及如何摆放相机. 一般相机会指向 (0, 0, 0) 的位置. 使用下面语法来更改指向:
camera.lookAt(new THREE.Vector3(x, y, z))
jk: 但是演示后不晓得为什么没有效果
使用该方法可以实现相机跟随某个几何体 camera.lookAt(<mesg>.position).
调试相机内看到的内容
chapter-2/debug-camera
使用两个相机, 通过一个相机来查看另一个相机的位置与视角. 代码片段
const helper = new THREE.CameraHelper(camera)
scene.add(helper)
// 然后在渲染动画函数中写入
helper.update()
ch03 使用灯光
前面介绍了如何搭建一个 3D 应用, 涉及的对象包括: 场景, 摄像机, 渲染器, 几何体, 材质, 网格, 动画等.
但是没有灯光, 只能显示纯色或网格几何体, 无法显示正常的样式的对象.
本章介绍可用的灯光类型, 以及怎么进行选择.
注意 WebGL 中并不包括灯光, 如果没有
three.js我们需要自己编写着色器, 这是比较困难的. 入门可以参考文档.
3.1 Three.JS 提供了哪些灯光
不同的灯光有不同的效果, 我们将讨论下面灯光:
THREE.AmbientLight. 这是基础灯光. 其颜色是场景中当前对象的颜色.THREE.PointLight. 该灯光是空间中的一个点光, 光线会朝所有方向发射. 该光线会产生阴影.THREE.SpotLight. 该灯光是一个锥形效果, 如同桌上的台灯. 该灯光会产生阴影.THREE.DirectionalLight. 平行光, 也称为无限光, 如同太阳发出的光. 该光会产生阴影.THREE.HemisphereLight. 这是一中特殊光源, 用于模拟室外的自然光.THREE.RectAreaLight. 区别于点光源, 用一个区域来呈现光源.THREE.LightProbe. 这是一种特殊光源, 基于环境地图, 创建一个环境光源来照亮场景.THREE.LensFlare. 这不是一个光源, 但可以用来创建光晕效果.