10 · Vector3:创建、访问、复制与数据交换
本章及后续六章按本仓库 Three.js r185 的 src/math/Vector3.js 核对。示例统一使用 import * as THREE from 'three';,可放到已有 Vite 示例的 JavaScript 模块中运行。Vector3 只是三个数的容器:它不会记录单位、坐标空间,也不知道自己代表位置还是速度。这些含义必须由变量名和调用上下文维护。
constructor(x = 0, y = 0, z = 0)
创建新对象,三个参数分别是分量,缺省为零。new THREE.Vector3(2, 3, 4) 是 (2,3,4),new THREE.Vector3(2) 是 (2,0,0),不是三个分量都等于 2。若作为位置,它是相对某个坐标系原点的位置;若作为位移,则与原点无关。构造函数不校验数据有效性,不应把字符串或 undefined 混进业务计算。
x、y、z
三个可读写的数值属性。position.y = 2 只改变高度,不改变另外两个分量。直接赋值表达式返回被赋的数,不能继续链式调用 Vector3 方法。写入位置属性也不等于立即更新 matrixWorld:如果接着进行坐标转换,需要按 Object3D 的更新规则更新矩阵。
isVector3
原型上的只读约定标记,值为 true,用于识别 Three.js 向量。vector.isVector3 === true 可做轻量类型判断,但不是严格的数据校验,普通对象也能伪造标记;外部输入仍应检查三个分量是否为有限数。
set(x, y, z)
覆盖分量并返回当前对象 this。r185 有一个容易忽略的兼容行为:省略 z 时保留原 z。
const position = new THREE.Vector3(1, 2, 9);
position.set(4, 5); // (4, 5, 9)
position.set(4, 5, 0); // (4, 5, 0)
这不意味着任意参数都可省略:x、y 不会自动保留。日常三维代码建议显式传三个分量,避免意外残留深度。
setScalar(scalar)
把三个分量都写成同一个数,返回 this。scale.setScalar(2) 得到 (2,2,2),常用于统一缩放。它不是“把长度设成 2”:此向量长度为 2√3,控制长度应使用 setLength()。
setX(x)
只修改 X,返回 this。new THREE.Vector3(1,2,3).setX(8) 得 (8,2,3);可用于滑块单独调整水平坐标,且允许继续链式调用。
setY(y)
只修改 Y,返回 this。new THREE.Vector3(1,2,3).setY(8) 得 (1,8,3);例如设置角色离地高度。它不检查是否穿过地面。
setZ(z)
只修改 Z,返回 this。new THREE.Vector3(1,2,3).setZ(8) 得 (1,2,8);适合编辑深度,注意相机观察方向不必与世界 Z 轴一致。
setComponent(index, value)
索引 0/1/2 对应 x/y/z,覆盖相应分量并返回 this。v.setComponent(1, 7) 等价于 v.setY(7)。适合由配置生成的三轴编辑器;其他索引,包括字符串 '1',会抛出异常,因此应先把表单值转换并验证为合法整数。
getComponent(index)
读取指定分量,返回数值,不修改向量。new THREE.Vector3(4,5,6).getComponent(2) 返回 6。索引规则和异常行为与 setComponent() 一致,遍历时使用 index < 3。
clone()
返回数值相同的新向量,不修改原对象。适合保存拖拽起点、记录动画关键帧等需要独立快照的情况。每次调用都会分配对象,不宜在大量粒子的每帧循环里反复使用。
const start = new THREE.Vector3(1, 0, 0);
const alias = start;
const snapshot = start.clone();
alias.x = 5;
console.log(start.x, snapshot.x); // 5, 1
const 只防止变量重新绑定,不会冻结向量内部的数据。
copy(v)
读取 v.x/y/z 写入当前对象,返回 this,不修改源向量,也不建立持续同步关系。scratch.copy(position) 是复用临时对象的常见写法。与 scratch = position 不同,后者只是让变量引用同一个对象。
equals(v)
逐分量执行严格相等比较,返回布尔值,不修改输入。适合比较直接复制的状态;不适合检验多次旋转后是否“足够接近”。0.1 + 0.2 与 0.3 可能不相等,NaN 甚至不等于自身。
function nearPosition(a, b, tolerance = 1e-5) {
return a.distanceToSquared(b) <= tolerance * tolerance;
}
这里容差与位置单位相同;毫米级模型与千米级场景不应盲目共用固定容差。
fromArray(array, offset = 0)
从数组的 offset、offset+1、offset+2 读取三个值,写入当前向量并返回 this。支持具有数值索引的 TypedArray。v.fromArray([99, 4, 5, 6], 1) 得 (4,5,6)。offset 是数值元素偏移,不是字节偏移;不会检查长度,越界会得到 undefined 并在后续运算产生 NaN。
toArray(array = [], offset = 0)
把分量写入目标数组并返回该数组,向量不变。new THREE.Vector3(4,5,6).toArray() 返回 [4,5,6],这个返回值不是 Vector3。提供数组可以复用存储;TypedArray 必须提前分配足够容量,越界写入不会扩容。
const packed = new Float32Array(6);
new THREE.Vector3(1, 2, 3).toArray(packed, 0);
new THREE.Vector3(4, 5, 6).toArray(packed, 3);
fromBufferAttribute(attribute, index)
通过 attribute 的 getX/getY/getZ 读取第 index 个记录,覆盖向量并返回 this。index 是顶点记录索引,不需要乘 itemSize。对归一化整数属性,这些 getter 会按属性规则读取归一化值;不要假定与直接读原始数组完全相同。
const attribute = new THREE.BufferAttribute(new Float32Array([1, 2, 3, 4, 5, 6]), 3);
const vertex = new THREE.Vector3().fromBufferAttribute(attribute, 1); // (4,5,6)
通常从几何体的 position 属性读到的是模型局部坐标。读取不会绑定数据,修改 vertex 不会自动修改几何体;反向写入要调用 attribute.setXYZ(),完成批量更新后设置 attribute.needsUpdate = true。属性需提供至少三个分量且索引在范围内。
Symbol.iterator
按 X、Y、Z 顺序产生分量,可以解构、展开或 for...of。[...new THREE.Vector3(1,2,3)] 得 [1,2,3]。不修改向量,但展开会创建数组,适合调试与数据交换。迭代器逐项读取值,不应在迭代过程中修改向量并把结果当作固定快照。
实践:保存拖拽起点且复用计算对象
const dragStart = new THREE.Vector3();
const displacement = new THREE.Vector3();
function beginDrag(object) {
dragStart.copy(object.position);
}
function updateDrag(object, localOffset) {
// 输入和 object.position 都属于同一个父节点的局部空间。
displacement.copy(localOffset);
object.position.copy(dragStart).add(displacement);
}
起点必须保持不变,所以更新阶段修改的是 object.position。实际鼠标拖拽还需先把世界交点转换到该父节点坐标系,不能把世界位移直接加到带旋转父节点的局部位置。
练习(暂不附答案)
- 预测
new THREE.Vector3(1,2,3).set(7,8).toArray()的结果,再检查源码解释原因。 - 写一个三轴输入处理函数,拒绝非法索引和非有限数,不修改原向量直到验证通过。
- 从一个包含四个顶点的 BufferAttribute 中读取最后两个顶点,输出独立快照。
- 构造一个
equals()返回 false、但几何距离小于1e-6的例子,并说明两种比较分别适合什么场景。