Three.js 基础
数学基础
常用三角函数值:
- sqrt(3)=1.732
- sqrt(2)=1.414
- sqrt(2)/2=0.707
常用向量值:
-
sqrt(3)/3=0.577 立方体对角线单位向量
仅讨论四维向量与矩阵,欧拉角和四元数作为补充
Vector
一个四维列向量:
vector=[x,y,z,w]T
其中
那么
- 如果w为0,那么vector就表示从原点到(x, y, z)的连线指向(x, y, z)的这个方向
- 如果w不为0,那么vector就表示一个点的位置,w为从原点到这个点的距离
Matrix

其中[m11,m21,m31,0]T表示旋转后X轴所在的新轴位置,及缩放系数(新向量长度/1)
同理,[m12,m22,m32,0]T表示旋转后Y轴所在的新轴位置及缩放系数;[m13,m23,m33,0]T表示旋转后Z轴所在的新轴位置及缩放系数
而[tx,ty,tz,1]T表示旋转后基于新X, Y, Z轴进行平移
缩放、旋转、平移的顺序参考opengl-tutorial##累积变换
参考注意行优先列优先的顺序,注意three.js中的Matrix,用set赋值时是行优先,而打印出来是列优先填充的,即
const m = new THREE.Matrix4();
m.set( 11, 12, 13, 14,
21, 22, 23, 24,
31, 32, 33, 34,
41, 42, 43, 44 );
console.log(m);

补充:欧拉角 Euler
欧拉角是除了变换矩阵之外的另一种描述物体旋转的方式,通过给定绕XYZ三个轴进行旋转的角度和旋转顺序(旋转顺序不同会导致结果不同,这里不详细说明),来描述物体旋转。
欧拉角的局限性:
万向节死锁
动态欧拉角:当一个轴已经旋转过了,那么它不会再随着后面的角再次旋转。
示例如图,按照红绿蓝的顺序从外向里转动,那么先动的轴不会随着后动的轴转动 (参考无伤理解欧拉角中的“万向死锁”现象)
没有万向节死锁时,对任意方向上的旋转都可以只按一个轴旋转达成
假设旋转顺序为XYZ,
- 第一次旋转绕X轴,带动Y轴和Z轴旋转
- 第二次旋转绕Y轴,带动Z轴旋转,并将Z轴旋转到了X轴平行的位置(例如旋转90度)
- 第三次旋转绕Z轴,但是此时Z轴与X轴平行(蓝圈与红圈共面)
此时出现了万向节死锁问题:即此时缺少了一个方向上的自由度,如果想要在这个方向上移动的话,那么需要动用其他两个轴来达成想要的移动,那么会导致中间状态不是线形变化,如果只观测首尾状态那么没有影响,但如果需要动画观测,则会出现弧线变化 (参考 Euler (gimbal lock) Explained)
补充:四元数 Quaternion
简单理解,四元数是欧拉角的上位替换,用四个值表示绕任意轴的旋转并且没有万向节死锁问题、支持线形插值。

Matrix4、欧拉角与四元数
欧拉角和四元数可以替换,四元数没有万向节死锁并且支持线形插值。
但是Matrix4中的旋转矩阵可以表示的内容并不能被欧拉角或四元数完全替代,例如以原点对称镜像
原点对称镜像:
const addBox = (x,y,z) => {
const geometry = new THREE.BoxGeometry(10, 5, 1);
const material = new THREE.MeshBasicMaterial({ color: 0xffffff });
const cube = new THREE.Mesh(geometry, material);
// 将所有轴正负方向反转
const matrix = new THREE.Matrix4(
-1, 0, 0, x,
0, -1, 0, y,
0, 0, -1, z,
0, 0, 0, 1,
)
cube.applyMatrix4(matrix)
console.log(
{
'cube.rotation': cube.rotation,
'cube.scale': cube.scale
}
)
const axis = new THREE.AxesHelper(10);
cube.add(axis);
scene.add(cube);
return cube;
};
const box = addBox(10,10,10);

根据原点镜像对称不是一个可以用欧拉角表示的变换,需要在欧拉角的基础上再附加scale来进行表示

场景
Scene
一切继承自Object3D的类(Cameras、Lights、Mesh)的实例都应该被添加到Scene中使用。
光源主要是按照来源进行区分。
- 不能投射阴影的光源: 环境光、半球光、平面光
- 可以投射阴影的充分条件:
- 光源是一个点(点光源、聚光灯)
- 光源方向一致(平行光源、聚光灯)
-
环境光会均匀的照亮场景中的所有物体。
环境光不能用来投射阴影,因为它没有方向。
-
- 从一个点向各个方向发射的光源。一个常见的例子是模拟一个灯泡发出的光。
- 该光源可以投射阴影
-
- 平行光是沿着特定方向发射的光。这种光的表现像是无限远,从它发出的光线都是平行的。常常用平行光来模拟太阳光的效果。 太阳足够远,因此我们可以认为太阳的位置是无限远,所以我们认为从太阳发出的光线也都是平行的。
- 平行光可以投射阴影
-
- 光源直接放置于场景之上,光照颜色从天空光线颜色渐变到地面光线颜色。
- 半球光不能投射阴影。
-
- 平面光光源从一个矩形平面上均匀地发射光线。这种光源可以用来模拟像明亮的窗户或者条状灯光光源。
- 不支持阴影。
-
- 光线从一个点沿一个方向射出,随着光线照射的变远,光线圆锥体的尺寸也逐渐增大。
- 该光源可以投射阴影
实体
这里讨论的实体是添加在场景中的部分对象。
还有很多实体,参考three.js文档/物体。
Mesh、Line、Group
-
Mesh
- Mesh基于 Geometry和Material 描述的一个实体,继承自Object3D,是一个具有局部坐标系的实体
-
const points = [];
points.push( new THREE.Vector3( - 10, 0, 0 ) );
points.push( new THREE.Vector3( 0, 10, 0 ) );
points.push( new THREE.Vector3( 10, 0, 0 ) );
const geometry = new THREE.BufferGeometry().setFromPoints( points );
- Line 也是Object3D的子类,跟Mesh同层,基于
BufferGeometry().setFromPoints
方法绘制出的线框几何体再结合material绘制出Line实体
const line = new THREE.Line( geometry, material );
-
Group
- 将几个实体组合在一起,看做一个整体,提供一个新的局部坐标系,便于统一操作位姿
Material
材质描述了对象Object3D的外观。
材质决定了实体如何跟光照互动,模拟现实世界中的各种材质在光照下的不同视觉效果。
材质分类:
绘制Line
绘制Mesh
Geometries(GeometryBuffer)
TextGeometry
Cameras
参考这个例子来直观看到透视相机和正交相机的区别。
模拟人眼,近大远小
具有这些属性来定义相机的视锥体:
- fov — 摄像机视锥体垂直视野角度
- aspect — 摄像机视锥体长宽比
- near — 摄像机视锥体近端面
- far — 摄像机视锥体远端面
通过fov
和aspect
可以修改可见视野角度和宽高比,注意是fov是从垂直方向给定的角度
通过near
和far
来修改可见视野距离
const perspectiveCamera = new THREE.PerspectiveCamera(
60, // fov
2, // aspect
10, // near
20 // far
)
const addPerspectiveCamera = () => {
scene.add(perspectiveCamera)
perspectiveCamera.position.set(0, 0, 0);
const destination = new THREE.Vector3(0, 0, 1);
perspectiveCamera.lookAt(destination);
const rotation = new Euler().copy(perspectiveCamera.rotation);
const cameraHelper = new THREE.CameraHelper(perspectiveCamera);
scene.add(cameraHelper);
}
addPerspectiveCamera();
对以上代码,借助 CameraHelper,可以将添加的相机视锥体可视化:
垂直方向上为设定的fov = 60, 水平方向上为根据fov和aspect计算出的结果,这里水平方向计算结果应该是90deg
 |
 |
垂直方向视锥角度 60度 |
水平方向视锥角度 90度 |
物体大小始终保持不变
具有这些属性来定义相机的视锥体:
- left — 摄像机视锥体左侧面。
- right — 摄像机视锥体右侧面。
- top — 摄像机视锥体上侧面。
- bottom — 摄像机视锥体下侧面。
- near — 摄像机视锥体近端面。
- far — 摄像机视锥体远端面。
const orthographicCamera = new THREE.OrthographicCamera(
-10, // left
10, // right
10, // top
-10, // bottom
10, // near
20 // far
)
const addOrthographicCamera = () => {
scene.add(orthographicCamera)
orthographicCamera.position.set(0, 0, 0);
const destination = new THREE.Vector3(0, 0, 1);
orthographicCamera.lookAt(destination);
const cameraHelper = new THREE.CameraHelper(orthographicCamera);
scene.add(cameraHelper);
}
addOrthographicCamera();
对以上代码,借助 CameraHelper,可以将添加的相机视体可视化:
正交相机视体是一个矩形,根据left、right、top、bottom、near、far圈定正交相机视体。
透视相机与正交相机对比
在相机的视体中添加一个矩形Mesh:
const addBox = (x, y, z) => {
const geometry = new THREE.BoxGeometry(5, 3, 1);
const material = new THREE.MeshBasicMaterial({color: 0xffffff});
const box = new THREE.Mesh(geometry, material);
box.position.set(x, y, z);
const axis = new THREE.AxesHelper(10);
box.add(axis);
scene.add(box);
return box;
};
const box = addBox(0, 0, 15);

- 切换到正交相机观看:
补充:Layers
相机具有一个layers属性,其值为一个Layers对象。Layers类是与Object3D同层的一个类。对每个Object3D对象,都具有一个layers属性,用于控制该Object3D对象是否在某个camera中显示。当 camera 的内容被渲染时,与其共享图层相同的物体会被显示。每个对象都需要与一个 camera 共享图层。
补充:
如何获取任意一个点在相机上投影的二维坐标(相对屏幕坐标系,原点位于canvas左上角)?
借助该点在世界坐标系中的三维向量表示,并将其使用vector3.project()
方法投射到相机上:
window.getProjectedPointPosition = () => {
const point = box.position.clone();
const projectedPoint = point.clone().project(currentCamera);
const width = window.innerWidth;
const height = window.innerHeight;
const screenX = Math.round((projectedPoint.x + 1) * width / 2);
const screenY = Math.round((-projectedPoint.y + 1) * height / 2);
console.log(projectedPoint);
console.log("Screen Coordinates:", screenX, screenY);
}
- projectedPoint 是标准化的坐标,范围在 [-1, 1] 之间, -1 是屏幕的最左边,1 是屏幕的最右边,-1 是屏幕的最下边,1 是屏幕的最上边
- 计算结果超出这个范围则不在视锥可见范围内
project方法内,相机的世界坐标的逆矩阵和projectionMatrix被用来计算投影:
project( camera ) {
return this.applyMatrix4( camera.matrixWorldInverse ).applyMatrix4( camera.projectionMatrix );
}
其中,applyMatrix4执行的是如下运算,行向量右乘:
Vectorthis×worldTcamera×cameraTscreen
另:如何获取屏幕上任意一点在三维世界坐标系中的位置?
这个问题是从2D获取3D位置,有一个自由度上的信息缺失,所以需要补充额外信息,即问题变为
如何获取屏幕上任意一点,其在三维世界坐标系中的射线,与某个Mesh上第一次相交的点的三维坐标系位置?
借助于Three.js的核心类:光线投射Raycaster
window.addEventListener('click', (event) => {
// 同样需要将屏幕坐标转为[-1, 1]的标准化设备坐标(**Normalized Device Coordinates**)
const NCD = new THREE.Vector2();
console.log('x,y', event.clientX, event.clientY);
// 一元方程转换
// 对于x,屏幕从左到右x递增,NCD.x从-1变到1,NCD.x与x正相关,NCD.x = ax + b, a > 0, b < 0
// 对于y,屏幕从上到下y递增,NCD.y从1变到-1,NCD.y与y负相关,NCD.y = ax + b, a < 0, b > 0
NCD.x = (event.clientX / window.innerWidth) * 2 - 1;
NCD.y = -(event.clientY / window.innerHeight) * 2 + 1;
console.log('NCD', NCD)
const raycaster = new THREE.Raycaster();
raycaster.setFromCamera(NCD, camera); // 从相机沿NCD方向发出射线
// 与box的相交点
const intersects = raycaster.intersectObject(box);
if (intersects.length > 0) {
const point = intersects[0].point;
console.log("Intersection point:", point);
}
});
设置几何体的位姿的方法:
1. 通过设置四维matrix
参考数学基础
优势:可以对不同几何体中方便根据已有位置进行copy
缺点:对使用者来说需要阅读matrix,转为人类语言较不容易
2. 通过position、up、lookAt属性
- step1:
position
可以确定物体的位置,固定3个自由度,还剩3个自由度
- step2:
lookAt
将物体 z 轴指向 世界坐标系中的目标点,还剩1个自由度
- step3:
up
在step1和step2基础上,将物体绕z轴旋转,使得物体的y轴与物体的up
属性的向量在世界坐标系中重合/平行/共面;这一步可能无法达成,如果up向量与step2确定的z轴平行。(Three.js 会自动处理这种情况,选择一个合理的up
方向。)
注意:up
属性的设置需要在lookAt
调用之前
示例:
- 默认情况,
up
为[0,1,0]
const addBox = (
// position={x:0, y:0, z:0},
// lookAt={x:0, y:0, z:0},
// up={x:0, y:1, z:0},
x,y,z
) => {
const geometry = new THREE.BoxGeometry(10, 5, 1);
const material = new THREE.MeshBasicMaterial({ color: 0xffffff });
const box = new THREE.Mesh(geometry, material);
box.position.set(x, y, z); // step1: 设置位置
// 默认情况,这行代码可以省略
box.up = new THREE.Vector3(0, 1, 0); // step3: 沿 z 轴旋转,使得 y 轴 尽可能 与 世界坐标中的 cube.up向量平行/重合/共面
box.lookAt(new THREE.Vector3(0, 0, 0)); // step2: z 轴指向 世界坐标原点
const axis = new THREE.AxesHelper(100);
box.add(axis);
scene.add(box);
return box;
};
const box = addBox(10,10,10);
box的z轴指向世界坐标原点,并尽量让box的y轴与设定的up
向量重合/平行/共面,图中是共面的情况。
如果up
为[−1,1,−1], 会得到相同的显示结果,但是已经是重合/平行的情况了
-
up
为[1,0,−1]
const addBox = (x,y,z) => {
const geometry = new THREE.BoxGeometry(10, 5, 1);
const material = new THREE.MeshBasicMaterial({ color: 0xffffff });
const cube = new THREE.Mesh(geometry, material);
cube.position.set(x, y, z); // step1: 设置位置
cube.up = new THREE.Vector3(1, 0, -1); // step3: 沿 z 轴旋转,使得 y 轴 尽可能 与 世界坐标中的 cube.up向量平行/重合
cube.lookAt(new THREE.Vector3(0, 0, 0)); // step2: z 轴指向 世界坐标原点
const axis = new THREE.AxesHelper(100);
cube.add(axis);
console.log(cube);
scene.add(cube);
return cube;
};
const box = addBox(10,10,10);
当up
为[1,0,−1],box的z轴指向世界坐标原点,并尽量让box的y轴与设定的up
向量重合/平行/共面,图中是重合/平行的情况。
补充:
-
当启用Control时,再对Camera使用lookAt方法需要注意:
如果对camera启用了Control,那么再使用lookAt时,需要同步更新controls的target。
-
如果lookAt的target为相机当前所在位置,那么会出错,显示为视野内无内容
camera.lookAt(cube.position);
trackballControls.target.copy(cube.position);
3. 通过translate 和 rotation方法
对平移和旋转,有些场景希望能够在局部坐标系内动作(人在地球场景上的火车上行走/地球在太阳系场景内自转),而有些场景希望能够在全局坐标系内动作(火车在地球场景上运动/地球在太阳系场景内公转),这里的例子可能不准确,但核心都是为了使用不同坐标系来简化一个动作的描述。
rotation
Three.js提供了物体相对局部坐标旋转和相对世界坐标系旋转的API:
基于局部坐标系旋转
// 基于XYZ轴
.rotateX ( rad : Float ) : this
.rotateY ( rad : Float ) : this
.rotateZ ( rad : Float ) : this
// 基于任意轴
.rotateOnAxis ( axis : Vector3, angle : Float ) : this
.setRotationFromAxisAngle ( axis : Vector3, angle : Float ) : undefined //会采用四元数旋转
// 基于给定值
// 基于转换矩阵
.setRotationFromMatrix ( m : Matrix4 ) : undefined // 务必确保 m 具有有效的 旋转矩阵,否则请使用 .applyMatrix4
.matrix = new THREE.Matrix4()
// 基于欧拉角
.setRotationFromEuler ( euler : Euler ) : undefined
.rotation = new THREE.Euler(0,0,0, Euler.DEFAULT_ORDER)
// 基于四元数
.setRotationFromQuaternion ( q : Quaternion ) : undefined
.quaternion = new THREE.Quaternion(0,0,0,1);
基于全局(世界/场景)坐标系旋转
// 基于任意轴
.rotateOnWorldAxis ( axis : Vector3, angle : Float) : this
.applyMatrix4 ( matrix : Matrix4 ) : undefined
applyMatrix4
是常用且有效的方法
translate
同样具有基于局部坐标旋转和相对世界坐标系旋转的API:
基于局部坐标系平移
// 基于任意轴
.translateOnAxis ( axis : Vector3, distance : Float ) : this
// 基于XYZ轴
.translateX ( distance : Float ) : this
.translateY ( distance : Float ) : this
.translateZ ( distance : Float ) : this
基于全局(世界/场景)坐标系平移
.applyMatrix4 ( matrix : Matrix4 ) : undefined
applyMatrix4
是常用且有效的方法
补充:Scale
一个 Vector3对象,表示在XYZ轴上的坐标缩放系数。
注意顺序:缩放、旋转、平移是Matrix4中约定的顺序
如果通过scale.set()
rotation.set()
position.set()
进行位姿设定,也需要遵循这个顺序。
用户交互
Controls
简单罗列提供的手势交互方式:
开发调试