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)
rotationQ 被视为世界→相机,因此 R_wc 直接插入。平移计算为 -R_wc * C,确保相机最终位于原点并看向 -Z 方向。
buildProj(znear, zfar, fovx, fovy)
生成与 WebGPU 兼容的列主序透视矩阵(深度范围 [0, 1])。该函数从半 FOV 的正切值得出 top/bottom/right/left,然后遵循教科书式的透视公式,其中 out[11] = 1 和 out[15] = 0(行/列顺序符合 gl-matrix 预期)。
CameraAdapter (Three.js 桥接)
CameraAdapter 镜像了 Three.js 集成中使用的 DirectCameraAdapter 的行为:
- 调用
camera.updateMatrixWorld()/updateProjectionMatrix()以同步 Three 的状态。 - 将
matrixWorldInverse复制到viewMat中,并预乘diag(-1, 1, -1, 1),使视图矩阵符合预处理使用的右手约定。 - 将 Three 的投影矩阵乘以相同的旋转,以便
P' * V'等于原始的P_three * V_three。 - 可选地对第二行 (Y) 取反,以补偿稍后在预处理打包期间执行的单次 Y 翻转。
- 捕获相机位置和各轴焦距 (
viewport / (2 tan(fov/2)))。 - 暴露与核心
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 上传的可预测性,而无需在整个代码库中复制投影逻辑。