PAYATHON 2026

Three.js 切换预设视角时,如何让相机正确围绕模型中心旋转?

数据阿明

结论

以模型中心为观察点,通过不同的方向向量设置相机位置:

camera.position = center + direction * distance;

这表示向量关系,实际代码应使用 copy()、addScaledVector() 等方法。

如果使用 OrbitControls,还要同步设置 controls.target,然后调用 controls.update()。单独调用 camera.lookAt(center) 不会修改控制器的旋转中心。OrbitControls 官方文档

原代码中的 direction 始终是 (1, 1, 1),所以这个函数只会把相机放到同一个相对方向,无法单独实现正面、顶部和侧面之间的切换。

为什么位置变了,观察效果仍然不对?

原代码的位置计算形式是正确的:

const newPosition = center.clone().add(direction.multiplyScalar(distance));
this.camera.position.copy(newPosition);
this.camera.lookAt(center);

lookAt() 调整相机朝向,使其看向指定的世界坐标点;它不会移动相机,也不会生成绕行路径。画面上方的方向还受 camera.up 影响。Object3D 官方文档

如果没有其他逻辑修改相机,且相机没有经过变换的父节点,这段代码应能得到一个朝向模型包围盒中心的静态斜视角。

如果项目使用了 OrbitControls,而 controls.target 仍是原来的位置,下一次控制器更新时就会围绕原目标计算相机状态,可能导致位置偏移或朝向被重新调整。仅凭当前片段不能认定这就是原因,需要检查控制器的设置。

链接中的完整源码未能读取,以下示例按 PerspectiveCamera + OrbitControls 的常见结构说明。

如何实现多个预设视角?

示例基于以下假设:

  • 相机采用 Y 轴向上的约定,并在创建控制器前设置好该约定。
  • 相机没有经过平移、旋转或缩放的父节点。
  • 使用居中的普通透视投影,没有设置偏轴投影。
  • 控制器的距离、角度和目标范围限制允许这些预设位置。

示例将世界坐标的 +Z 定义为正面、+X 定义为侧面。这个约定需要根据模型的实际朝向调整。

import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';

type PresetView = 'front' | 'top' | 'side' | 'isometric';

function setModelView(
  model: THREE.Object3D,
  view: PresetView,
  camera: THREE.PerspectiveCamera,
  controls: OrbitControls
): void {
  model.updateWorldMatrix(true, true);

  const box = new THREE.Box3().setFromObject(model);
  if (box.isEmpty()) return;

  const sphere = box.getBoundingSphere(new THREE.Sphere());
  const center = sphere.center;
  const radius = sphere.radius;
  if (radius <= 0) return;

  const directions: Record<PresetView, THREE.Vector3> = {
    front: new THREE.Vector3(0, 0, 1),
    // 略微偏离 Y 轴,避免观察方向与 up 完全平行。
    top: new THREE.Vector3(0, 1, 0.0001),
    side: new THREE.Vector3(1, 0, 0),
    isometric: new THREE.Vector3(1, 1, 1)
  };

  const direction = directions[view].clone().normalize();

  // 同时考虑垂直、水平视场角,使用包围球估算取景距离。
  const halfVerticalFov =
    THREE.MathUtils.degToRad(camera.getEffectiveFOV()) / 2;
  const halfHorizontalFov = Math.atan(
    Math.tan(halfVerticalFov) * camera.aspect
  );
  const halfFov = Math.min(halfVerticalFov, halfHorizontalFov);
  const distance = (radius / Math.sin(halfFov)) * 1.1;

  controls.target.copy(center);
  camera.position.copy(center).addScaledVector(direction, distance);
  controls.update();
}

调用方式:

setModelView(model, 'front', camera, controls);
setModelView(model, 'top', camera, controls);
setModelView(model, 'side', camera, controls);
setModelView(model, 'isometric', camera, controls);

每次选择一个预设调用即可。控制器会根据新的位置和目标更新相机朝向,无需再单独调用 lookAt()。

如果没有使用控制器,可以在计算位置之后直接设置朝向:

camera.position.copy(center).addScaledVector(direction, distance);
camera.lookAt(center);

距离计算和顶部视角需要注意什么?

包围盒距离估算不等于绕行逻辑

原代码使用 maxDim 和垂直 fov 估算距离,没有充分考虑画布宽高比和斜视角下的模型深度。部分视角可能因此取景不完整,但这不能说明旋转中心有误。

示例改用包围球取景。在普通居中透视投影下,使用较小的半视场角计算容纳整个球体所需的距离。这种方法通常会留出更多空白,但更适合多个观察方向共用距离。

getEffectiveFOV() 会考虑 camera.zoom;camera.aspect 应与渲染区域宽高比一致。PerspectiveCamera 官方文档

顶部视角存在方向退化

当相机恰好位于模型正上方,观察方向与默认的 camera.up = (0, 1, 0) 平行时,仅靠这两个方向无法唯一确定画面的水平朝向。

示例中的顶部方向带有很小的 Z 分量,因此得到的是接近正俯视的视角。如果需要严格的正俯视并明确规定画面上方,需要单独设计朝向。还要考虑 OrbitControls 在极点附近的处理,不能只修改一次 up 就假定后续交互一定符合要求。

如果切换后仍然异常,检查哪些地方?

  1. 检查控制器是否修改了预设结果,包括 minDistance、maxDistance、minPolarAngle、maxPolarAngle 等限制,以及是否有自动旋转或尚未结束的交互惯性。
  2. 检查是否混用了坐标系。这里使用包围盒中心进行世界坐标计算,而 camera.position 是相对于父节点的位置。相机处于经过变换的父节点下时,不能直接把世界坐标写入它的局部位置。
  3. 检查是否有其他代码持续写入相机。动画循环、补间动画或另一个控制器可能覆盖本次设置。
  4. 检查是否发生裁剪或未触发渲染。确认 near、far 是否容纳模型。修改投影参数后调用 updateProjectionMatrix();采用按需渲染时,切换后还要触发一次渲染。

如果需要看到相机沿轨道连续移动,还要增加过渡动画。直接设置 camera.position 会立即切换视角;对两个位置做线性插值则会走直线。若要保持绕中心运动,可以对绕固定中心的球坐标角度进行插值,并在每一帧更新位置与朝向。

备注:内容仅供参考。