Three.js 相机切换:位置、目标与投影的连续过渡
从 3D 叠层编辑器进入等轴测有动画、重置却跳变的问题出发,梳理相机状态、投影过渡和中途反向操作。
在 3D 叠层编辑器里,从默认透视视图切到等轴测视图已经有动画,点击重置却突然跳回。这种问题很容易让人先调整 easing 或延长时长,但现场的关键不在动画曲线:两个入口没有走同一条状态过渡路径。
我后来把进入预设、返回默认和中途反向放到一起检查,才逐渐理清相机动画需要保存哪些状态。本文复盘这一思路,代码是用于解释的简化片段,没有附带新的实时 3D 演示,也不把历史浏览器检查当作本次重新运行的结果。
相机位置只是画面的一部分
相机的位置决定从哪里看,但屏幕上的画面还受到观察目标、朝向、投影类型、缩放和视口比例影响。透视投影和正交投影即使使用相近位置,也不一定有相同的画面尺度。
Three.js 将投影矩阵和对应逆矩阵作为相机状态公开,相关定义见 Camera 文档。在叠层模型里,如果只让 position 变化,而投影在第一帧直接切换,物体的尺度和透视感仍会突然改变。
OrbitControls 的 target 也必须纳入考虑。用户可能已经平移到板边的一处细节,预设视图却默认看向原点。如果只改变相机而保留旧 target,下一次拖动就可能围绕错误中心旋转。其 target 与 update 行为可在 OrbitControls 文档中核对。
每个入口都交给同一个过渡函数
我先枚举入口:透视预设、等轴测预设、重置,以及过渡期间再次点击。它们最终都应把“目标状态”交给一个共同入口,而不是分别设置相机。
共同入口首先读取当前帧,复制位置、target、朝向和投影状态。必须复制值,不能直接保存对可变 Vector3 或 Matrix4 的引用;否则后续帧修改相机时,所谓的起点也会一起变化。
// 简化:只展示同类相机的位置与目标插值,不含投影切换。
const fromPosition = camera.position.clone();
const fromTarget = controls.target.clone();
const toPosition = destination.clone();
const toTarget = destinationTarget.clone();
function updateTransition(progress: number) {
const t = Math.max(0, Math.min(1, progress));
const eased = t * t * (3 - 2 * t);
camera.position.lerpVectors(fromPosition, toPosition, eased);
controls.target.lerpVectors(fromTarget, toTarget, eased);
camera.lookAt(controls.target);
// 需与 controls 的阻尼、约束和帧更新顺序共同协调。
}
这段代码依赖 Three.js 场景中的 camera、controls 等对象,不是独立可运行的应用。它刻意省略投影处理,避免让短示例看起来已经解决了透视与正交之间的全部问题。
中途反向时,同样从当时画面重新保存起点。比如动画走到 40% 时点击重置,不应该从上一次预设的标准坐标重新开始,否则第一帧就会跳回去。新的动画可以有新的目标,但起点必须连续。
投影过渡是这个场景的特殊取舍
在该项目的实现中,切换透视与等轴测视图时会处理投影尺度,再对投影矩阵做过渡;结束后恢复目标相机按标准参数生成的投影矩阵。过渡期间逆矩阵也要同步更新,避免依赖它的射线计算与显示状态脱节。
这里有两个容易遗漏的问题。第一,投影矩阵在齐次坐标意义下允许尺度等价,直接拿任意尺度的矩阵逐元素插值,可能产生不符合预期的视觉变化,因此需要针对所用相机与目标距离处理尺度。第二,逐元素插值并不保证任意两种投影之间都得到稳定、可逆、可交互的中间状态。
因此我把它视为该叠层视图里的实现取舍,而不是通用相机动画公式。裁剪面、深度精度、拾取、视口缩放和极端参数都需要检查。对其他项目,也可以考虑保持一种投影、调整视角和尺度,或设计更明确的切换过渡,而不是直接复制矩阵插值。
用户操作应该能够接管
自动动画与 OrbitControls 如果同时写入位置,就会出现拉扯。用户开始拖动或缩放时,应按产品约定取消自动过渡,并把当前状态整理为后续交互可以接管的状态。
启用阻尼时还要检查残余运动。仅清掉 transition 对象,未必能清掉 controls 已积累的变化;反过来,粗暴重置 controls 也可能造成第二次跳变。需要在实际帧循环中确定谁在何时拥有相机状态的写入权。
视口 resize 同样可能改写投影。如果动画第一帧刚设置起始投影,布局 effect 又用新视口更新它,画面仍可能闪一下。初始化、resize 和帧更新的顺序,需要与投影过渡共同考虑。
用户启用减少动画后,应直接应用完整目标状态,保持位置、target、投影和 controls 一致。跳过动画不意味着可以漏掉这些状态同步。
看中间帧,不能只看终点
两个终点正确,只能证明最终参数大致正确,不能证明过渡连续。我会检查进入等轴测、返回透视、重置、动画中反向、用户拖动打断、resize 和减少动画这些路径,并在慢速观察或录屏中看第一帧、中间帧与结束帧。
测试可以断言状态复制、两向入口和矩阵更新;真实浏览器则检查模型的尺度变化、板层裁剪、拖动接管与视觉跳动。历史修复曾完成双向浏览器中间帧检查,但本文没有重新运行那套工程,新的参数或控件变化仍需要回归。
这类问题让我意识到,相机动画其实是一段状态所有权的交接。先把入口统一、保存当前帧,再处理投影和交互,通常比只调节动画时长更接近问题本身。