28 · 商品模型自动居中与完整取景

返回系列目录

需求与选择

商品模型尺寸和原点不可预测,直接把相机固定在 (0, 0, 5) 会造成裁切。本例在模型世界包围盒上建立包围球,将相机放到足够远的位置。包围球方案对观察方向不敏感、实现简单,但对细长模型留白较大;更紧凑的方案是在相机基底中检查包围盒八角。

透视相机的 fov 是垂直视场角。考虑 zoom 后应使用 getEffectiveFOV()。若半垂直视角是 θv,宽高比为 a:

θh=arctan⁡(atan⁡θv),d≥r/sin⁡(min⁡(θv,θh)). \theta_h=\arctan(a\tan\theta_v),\qquad d\ge r/\sin(\min(\theta_v,\theta_h)).

这里用 sin 是因为要求整个球落在视锥内;平面矩形的高度公式才是 halfHeight / tan(halfFov)。

完整核心代码

import { createDemo } from './runtime.js';

const { THREE, scene, camera, renderer, onDispose } = createDemo();
const product = new THREE.Mesh(
  new THREE.BoxGeometry(3, 2, 1),
  new THREE.MeshNormalMaterial()
);
product.position.set(10, 2, -4); // 故意使用偏离原点的模型。
scene.add(product);
const box = new THREE.Box3();
const sphere = new THREE.Sphere();
const outward = new THREE.Vector3(1, 0.7, 1).normalize();

function fitPerspective(object) {
  object.updateWorldMatrix(true, true);
  box.setFromObject(object);
  if (box.isEmpty()) return;
  box.getBoundingSphere(sphere);
  const radius = Math.max(sphere.radius, 1e-3);
  const rect = renderer.domElement.getBoundingClientRect();
  if (rect.width === 0 || rect.height === 0) return;
  camera.aspect = rect.width / rect.height;
  const halfVertical = THREE.MathUtils.degToRad(camera.getEffectiveFOV()) / 2;
  const halfHorizontal = Math.atan(Math.tan(halfVertical) * camera.aspect);
  const distance = radius / Math.sin(Math.min(halfVertical, halfHorizontal)) * 1.15;
  // 此处 camera 是没有变换父节点的相机,position 就是世界位置。
  camera.position.copy(sphere.center).addScaledVector(outward, distance);
  camera.lookAt(sphere.center);
  camera.near = Math.max(1e-4, distance - radius * 1.2);
  camera.far = distance + radius * 1.2;
  camera.updateProjectionMatrix();
  camera.updateWorldMatrix(true, false);
}

fitPerspective(product);
window.addEventListener('resize', refit);
function refit() {
  fitPerspective(product);
}
onDispose(() => window.removeEventListener('resize', refit));

模型异步加载完成后再调用取景函数。容器布局变化但窗口没变化时应使用 ResizeObserver;与渲染器尺寸更新使用同一宽高来源。若有 OrbitControls,还需把 controls.target 设置为球心,并调用 controls.update()。

居中与正交相机

为了观察模型,不必真的修改模型顶点或位置:让相机看向中心已经能解决许多问题。若产品编辑器要求绕模型中心旋转,可创建一个 pivot,把模型中心平移到 pivot 原点;保留导入模型层级,避免破坏骨骼或动画。

正交相机没有“远一点就变小”的关系。可令可见半高 h = radius * margin * max(1, 1 / aspect),设置 top = h、bottom = -h、left = -h * aspect、right = h * aspect,并令 zoom = 1。相机距离只需满足前后裁剪与观察方向要求。

验证与限制

测试立方体、很高的模型、很宽的模型及空组,在竖屏下也不能裁切。动画和 shader 顶点位移可能让静态包围盒失效,需要预先扩展包围范围或按业务重新计算。不要每帧无条件扫描大型模型;在模型、姿态或容器尺寸变化时重算。设置过小 near、过大 far 会损失深度精度,应根据场景整体范围选择,而不是只考虑一个商品。

练习(暂不提供答案)

  1. 将示例改为正交相机,并验证相机远近不改变模型大小。
  2. 通过投影包围盒八角验证取景是否完整。
  3. 为细长模型实现比包围球更紧凑的取景。
  4. 在模型外增加地面后,调整 near/far 保证地面没有意外裁切。