变换、坐标系和场景图
此笔记记录于DISCOVER three.js,大多数为其中的摘要,少数为笔者自己的理解
Object3D 基类
不是为每种类型的对象多次重新定义.position
、.rotation
和.scale
属性,而是在 Object3D
基类上定义一次这些属性,这样可以添加到场景中的所有其他类都 从该基类派生。这些包括网格、相机、灯光、点、线、助手,甚至场景本身。我们将非正式地将派生自Object3D
的类称为 场景对象。
Object3D 除了这三个之外,还有许多属性和方法,由每个场景对象继承。这意味着定位和设置相机或网格的工作方式与设置灯光或场景的方式大致相同。然后根据需要将其他属性添加到场景对象,以便灯光获得颜色和强度设置,场景获得背景颜色,网格获得材质和几何体,等等。
场景图与世界坐标
除了scene.add(mesh)
这样给场景添加网格对象,还可以给网格对象添加网格对象,这样就形成了一个树的结构:
- 使用每个对象的
.add
方法.remove
方法,我们可以创建和操作场景图。 - 场景图中的每个对象(顶级场景除外)只有一个父对象,并且可以有任意数量的子对象。
- 渲染器遍历场景图,从场景开始,并使用每个对象相对于其父对象的位置、旋转和缩放来确定在哪里绘制它。
- 每一个对象都有一个坐标系:顶级场景定义了世界空间,而其他每个对象都定义了自己的局部空间。
mesh
在scene
中变换属于世界空间操作;子mesh
在父mesh
中变换属于局部空间操作。- 子
mesh
始终相对于父mesh
的坐标系,remove 后再 add 替换父对象后,子mesh
的属性如position
是相对于替换后的父坐标系 - 我们最终在屏幕上看到的是世界空间
你可以使用
.children
数组访问场景对象的所有子对象有更复杂的方法可以访问特定的孩子,例如
Object3d.getObjectByName
方法。但是,当您不知道对象的名称或它没有名称时,直接访问.children
数组很有用。
平移变换
我们通过更改对象的 .position
属性来执行平移。平移对象会将其移动到其直接父对象坐标系中的新位置,每个对象都从其父对象坐标系内的原点开始。
我们称这样的有序列表数字为向量,因为有三个数字,所以它是一个3D 向量。
我们可以沿着 X、Y 和 Z 轴一个接一个的平移对象,或者我们可以使用position.set
一次沿所有三个轴平移对象:
// translate one axis at a time
mesh.position.x = 1;
mesh.position.y = 2;
mesh.position.z = 3;
// translate all three axes at once
mesh.position.set(1,2,3);
平移的方向:
位置被存储在Vector3
类中
Three.js 有一个用于表示 3D 向量的特殊类,称为 Vector3
。 这个类有.x
、.y
和.z
属性和方法.set
来帮助我们操作它们。每当我们创建任何场景对象时,例如Mesh
,Vector3
都会被自动创建并存储在.position
中:
// when we create a mesh ...
const mesh = new Mesh();
// ... internally, three.js creates a Vector3 for us:
mesh.position = new Vector3();
缩放转换
只要我们在所有三个轴上缩放相同的数量,缩放对象就会使其变大或变小。如果我们按不同的量缩放轴,对象将被压扁或拉伸。
像.position
一样,.scale
也是存储在Vector3
中的, 对象的初始缩放比例是(1,1,1):
// when we create a mesh...
const mesh = new Mesh();
// ... internally, three.js creates a Vector3 for us:
mesh.scale = new Vector3(1, 1, 1);
统一缩放:
mesh.scale.set(2, 2, 2);
mesh.scale.set(0.5, 0.5, 0.5);
非均匀缩放:
// double the initial width
mesh.scale.x = 2;
// halve the initial width
mesh.scale.x = 0.5;
负比例值镜像对象:
// 镜像对象
// mirror the mesh across the X-axis
mesh.scale.x = -1;
// mirror the mesh across the Y-axis
mesh.scale.y = -1;
// mirror the mesh across the Z-axis
mesh.scale.z = -1;
// 镜像并挤压
// mirror and squash mesh to half width
mesh.scale.x = -0.5;
很好理解,就是位于(1,X,X)的某顶点变换到了(-1,X,X),全部的点都这样变换,就会镜像。
相机和灯光无法缩放
旋转变换
与平移或缩放相比,旋转需要更加小心。这有几个原因,但主要是旋转顺序很重要。
不同的旋转顺序最后可能不会给出相同的结果,这取决于长宽高是否一致
我们用于
.position
和.scale
的不起眼的Vector3
类不足以存储旋转数据。相反,three.js 不是使用一个,而是用 两个 数学类用于存储旋转数据。我们将在这里查看到更详细的内容: 欧拉角。幸运的是,它与Vector3
类相似。
欧拉角在 three.js 中使用类 Euler
表示 。与.position
和.scale
一样,当我们创建一个新的场景对象时,会自动创建一个Euler
实例并为其赋予默认值。
// when we create a mesh...
const mesh = new Mesh();
// ... internally, three.js creates an Euler for us:
mesh.rotation = new Euler();
与Vector3
一样,有.x
、.y
和.z
属性,以及.set
方法;可以自己创建Euler
实例;可以省略参数以使用默认值,同样,所有轴的默认值为零。
默认情况下,three.js 将在对象的局部空间中围绕 X 轴,然后围绕 Y 轴,最后围绕 Z 轴旋转。我们可以使用 Euler.order
属性来改变它。默认顺序称为“XYZ”,但也可以使用“YZX”、“ZXY”、“XZY”、“YXZ”和“ZYX”。
旋转单位是弧度
我们可以使用 .degToRad
实用程序将度数转换为弧度。
import { MathUtils } from 'three';
const rads = MathUtils.degToRad(90); // 1.57079... = π/2
另一个旋转类:四元数 Quaternions
我们可以互换使用四元数和欧拉角。当我们更改mesh.rotation
时,mesh.quaternion
属性会自动更新,反之亦然。这意味着我们可以在欧拉角适用时使用欧拉角,并在四元数适用时切换到四元数。
欧拉角有几个缺点,在创建动画或进行涉及旋转的数学时会变得很明显。特别是,我们不能将两个欧拉角相加(更著名的是,它们还存在一种叫做 万向锁的问题)。四元数没有这些缺点。另一方面,它们比欧拉角更难使用,所以现在我们将坚持使用更简单的Euler
类。
现在,请记下这两种旋转对象的方法:
- 使用欧拉角,使用
Euler
类表示并存储在.rotation
属性中。 - 使用四元数,使用
Quaternion
类表示并存储在.quaternion
属性中。
以下是一些需要注意的重要事项:
- 并非所有对象都可以旋转。比如 我们上一章介绍的
DirectionalLight
就不能旋转。灯光从某个位置照射到目标,灯光的角度是根据目标的位置而不是.rotation
属性计算得出的。 - three.js 中的角度是使用弧度而不是度数指定的。唯一的例外是
PerspectiveCamera.fov
属性使用度数来匹配真实世界摄影惯例的。
转换矩阵
1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1
它有四行四列,所以它是一个 4×4 矩阵,它存储了一个对象的完整变换,这就是我们将其称为变换矩阵的原因。同样的,也有一个 three.js 类来处理这种类型的数学对象,称为 Matrix4
。 还有一个类表示 3×3 的矩阵称为Matrix3
。当矩阵在 主对角线上全为 1 而其他地方都为 0 时,就像上图这样,我们称其为 单位矩阵,I。
与单独的变换相比,矩阵对 CPU 和 GPU 的处理效率要高得多,它代表了一种折衷方案,可以为我们提供两全其美的效果。我们人类可以使用更简单.position
,.rotation
和.scale
属性,然后,每当我们调用.render
时,渲染器都会更新每个对象的矩阵并将它们用于内部计算。
当我们创建一个网格时,会自动创建一个局部变换矩阵:
// when we create a mesh
const mesh = new Mesh();
// ... internally, three.js creates a Matrix4 for us:
mesh.matrix = new Matrix4();
通常,我们不需要手动调用
.updateMatrix
,因为渲染器会在渲染之前更新每个对象的矩阵。但是,在这里,我们希望立即看到矩阵的变化,因此我们必须强制更新。(或者 render 一下,也会更新)
mesh.position.x = 2;
mesh.position.y = 4;
mesh.position.z = 6;
mesh.updateMatrix();
1 0 0 2
0 1 0 4
0 0 1 6
0 0 0 1
mesh.scale.x = 5;
mesh.scale.y = 7;
mesh.scale.z = 9;
mesh.updateMatrix();
5 0 0 2
0 7 0 4
0 0 9 6
0 0 0 1
围绕 X、Y、Z 轴的旋转:
理解:旋转时,视角不变时,除非对称,否则长度会变,这就是为什么缩放会变的原因
世界矩阵:
正如我们多次提到的,对我们来说重要的是对象在世界空间中的最终位置,因为这是我们在渲染对象后所看到的。为了帮助计算这一点,每个对象都有第二个变换矩阵,即世界矩阵,存储在 Object3D.matrixWorld
中。 这两个矩阵在数学上没有区别。他们都是 4×4 变换矩阵,当我们创建网格或任何其他场景对象时,局部矩阵和世界矩阵都会自动创建。