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。实际鼠标拖拽还需先把世界交点转换到该父节点坐标系,不能把世界位移直接加到带旋转父节点的局部位置。

练习(暂不附答案)

  1. 预测 new THREE.Vector3(1,2,3).set(7,8).toArray() 的结果,再检查源码解释原因。
  2. 写一个三轴输入处理函数,拒绝非法索引和非有限数,不修改原向量直到验证通过。
  3. 从一个包含四个顶点的 BufferAttribute 中读取最后两个顶点,输出独立快照。
  4. 构造一个 equals() 返回 false、但几何距离小于 1e-6 的例子,并说明两种比较分别适合什么场景。