跳转至

Camera模块架构

Camera Module 将所有视图/投影数学运算集中在一个地方,以便预处理、排序和渲染可以将相机视为简单的数据源。它由一个轻量级的 Camera 接口、一对具体的 PerspectiveCamera + PerspectiveProjection 类,以及一个可选的用于 Three.js 场景的 CameraAdapter 组成。

模块布局

src/camera/
├── perspective.ts
│   ├── Camera 接口
│   ├── PerspectiveProjection
│   ├── PerspectiveCamera
│   ├── world2view()
│   └── buildProj()
├── CameraAdapter.ts (Three.js 桥接)
└── index.ts (重新导出 + 数学辅助函数)

设计原则

  • 接口优先 (Interface first) – 渲染器和预处理器仅依赖于 Camera,因此未来的正交或立体相机可以直接插入,而无需触及 GPU 代码。
  • 组合 (Composition)PerspectiveCamera 拥有一个 PerspectiveProjection;投影数学与位置/方向逻辑保持隔离。
  • 类不可变性 (Immutable-ish) – Setters 克隆输入(vec3, quat, 矩阵)以避免意外突变;Getters 出于同样原因返回克隆副本。
  • GPU 友好 (GPU friendly) – 矩阵是列主序的、右手的,并且已经针对 WebGPU 的 [0, 1] 深度范围进行了定向。

PerspectiveProjection 内部机制

class PerspectiveProjection {
  constructor(viewport, [fovx, fovy], znear, zfar) {
    this.fovx = fovx;
    this.fovy = fovy;
    this.znear = znear;
    this.zfar = zfar;
    this.fov2viewRatio = (viewport.w / viewport.h) / (fovx / fovy);
  }

  resize(viewport) {
    const viewportRatio = viewport.w / viewport.h;
    const targetFovRatio = viewportRatio / this.fov2viewRatio;
    const newFovy = 2 * Math.atan(Math.tan(this.fovy / 2) * targetFovRatio / (this.fovx / this.fovy));
    this.fovy = newFovy;
    this.fovx = this.fovy * targetFovRatio;
  }
}
  • fov2viewRatio 存储原始视口宽高比与 FOV 宽高比之间的关系,因此调整大小可以保持外观一致。
  • projectionMatrix() 简单地委托给 buildProj(znear, zfar, fovx, fovy)
  • focal(viewport) 将 FOV 转换为预处理使用的各轴焦距(像素)。
  • lerp(other, t) 对每个标量 (fovx/y, near/far) 以及存储的比率进行插值,以实现平滑的相机动画。

PerspectiveCamera 内部机制

class PerspectiveCamera implements Camera {
  positionV: vec3;
  rotationQ: quat; // 世界 -> 相机
  projection: PerspectiveProjection;

  viewMatrix(): mat4 {
    const R_wc = mat3.fromQuat(mat3.create(), this.rotationQ);
    return world2view(R_wc, this.positionV);
  }
}
  • positionV & rotationQ 作为 gl-matrix 对象存储,并在构造时克隆。
  • fitNearFar(aabb) 计算相机位置与 AABB 中心之间的距离,然后为了安全起见设置 znear = max(d - radius, zfar / 1000)zfar = (d + radius) * 1.5
  • frustumPlanes() 计算 P * V,提取行,并使用 Gribb–Hartmann 技巧形成归一化的平面方程。

数学辅助函数

world2view(R_wc, C)

V = [ R_wc  | -R_wc * C ]
    [ 0 0 0 |     1     ]

rotationQ 被视为世界→相机,因此 R_wc 直接插入。平移计算为 -R_wc * C,确保相机最终位于原点并看向 -Z 方向。

buildProj(znear, zfar, fovx, fovy)

生成与 WebGPU 兼容的列主序透视矩阵(深度范围 [0, 1])。该函数从半 FOV 的正切值得出 top/bottom/right/left,然后遵循教科书式的透视公式,其中 out[11] = 1out[15] = 0(行/列顺序符合 gl-matrix 预期)。

CameraAdapter (Three.js 桥接)

CameraAdapter 镜像了 Three.js 集成中使用的 DirectCameraAdapter 的行为:

  1. 调用 camera.updateMatrixWorld() / updateProjectionMatrix() 以同步 Three 的状态。
  2. matrixWorldInverse 复制到 viewMat 中,并预乘 diag(-1, 1, -1, 1),使视图矩阵符合预处理使用的右手约定。
  3. 将 Three 的投影矩阵乘以相同的旋转,以便 P' * V' 等于原始的 P_three * V_three
  4. 可选地对第二行 (Y) 取反,以补偿稍后在预处理打包期间执行的单次 Y 翻转。
  5. 捕获相机位置和各轴焦距 (viewport / (2 tan(fov/2)))。
  6. 暴露与核心 Camera 实现相同的 viewMatrix(), projMatrix(), position(), 和 projection.focal() 签名,因此预处理就像使用任何其他相机一样使用该适配器。

three-integration/GaussianSplattingThreeWebGPU.ts 广泛使用了此适配器,允许 Three.js 相机驱动预处理 + 渲染器,而无需重写数学逻辑。

Uniforms & GPU 布局

预处理和渲染器都期望以下结构(std140 对齐):

struct CameraUniforms {
  view_matrix : mat4x4<f32>;
  proj_matrix : mat4x4<f32>;
  view_proj_matrix : mat4x4<f32>;
  camera_position : vec3<f32>; // 填充到 16 字节
  camera_direction : vec3<f32>; // 填充到 16 字节
}

PerspectiveCamera 直接生成前三个矩阵;应用或渲染器在上传到 UniformBuffer 时处理打包/复制。

精度与稳定性

  • 近/远拟合在添加安全乘数之前强制执行最小比率 (≈1:1000),以防止预处理/投影中的 Z-fighting(深度冲突)。
  • rotationQ 在构建或变异相机时都会进行归一化,避免四元数数学运算累积时的漂移。
  • 来自 frustumPlanes() 的平面法线已归一化,因此距离测试非常直接。
  • 调整大小和 FOV 更改通过一致的辅助函数进行,因此 UI 操作不会将投影推入无效范围。

集成摘要

消费者 它读取什么
预处理 (dispatchModel) camera.viewMatrix(), camera.projMatrix(), camera.position(), camera.projection.focal(viewport)
渲染器 (GaussianRenderer) 同上;在绘制前填充相机 Uniform 缓冲。
控制/轨道/FPS 系统 变异 positionV, rotationQ, 或调用 projection.resize
Three.js 演示 CameraAdapter 将 Three 相机转换为核心接口。

通过保持数学逻辑独立并暴露一个微小的接口,Visionary 可以更换相机,插入外部引擎,并保持 GPU 上传的可预测性,而无需在整个代码库中复制投影逻辑。