05 · Clip Space、NDC 与屏幕坐标

返回目录 · 上一章 · 下一章

本章针对项目的 WebGLRenderer 默认坐标约定,不把 WebGPU 的深度范围混入同一套公式。

1. 投影先生成四维裁剪坐标

透视投影让近处物体显得大、远处物体显得小。它通过投影矩阵和后续透视除法完成:

pclip=Ppview,pndc=(xclip/wclip,yclip/wclip,zclip/wclip) p_{clip}=Pp_{view},\qquad p_{ndc}=(x_{clip}/w_{clip},y_{clip}/w_{clip},z_{clip}/w_{clip})

常规透视矩阵中 w_clip = -z_view。因此相机前方越远的点 w 越大,除法后横纵坐标越靠近中心。正交投影中通常 w=1,同样大小的物体不会随距离缩小。

import * as THREE from 'three';

const camera = new THREE.PerspectiveCamera(60, 1, 0.1, 100);
camera.updateWorldMatrix(true, false);
const clip = new THREE.Vector4(1, 0, -5, 1).applyMatrix4(camera.projectionMatrix);
const ndc = new THREE.Vector3(clip.x, clip.y, clip.z).divideScalar(clip.w);
console.log(clip.w); // 5
console.log(ndc.x); // 约 0.3464

将 view.z 改为 -10,ndc.x 约减半。这就是透视缩小,不是模型在世界空间里真的缩放了。

2. 裁剪与 NDC 的边界

WebGL 的标准齐次裁剪体满足 -w ≤ x,y,z ≤ w。GPU 在透视除法前处理图元裁剪;一个三角形的某个顶点在外面,不等于整个三角形都要丢弃。

透视除法后的可见范围通常是 x、y、z 各在 [-1,1]。NDC 的 z 不是线性世界距离,深度缓冲值通常再映射到 [0,1]。不能用 ndc.z * far 恢复真实深度。

w 接近零时不要直接用投影结果布局 DOM;相机背后的点还可能投影到看似合理的横纵坐标。显示标签时应同时检查 view.z 和 NDC 范围。

3. 从 NDC 到浏览器 client 坐标

NDC 原点在视口中心、Y 向上;浏览器 client 坐标原点在浏览器内容视口左上、Y 向下。对于没有旋转、边框和内边距的画布:

xclient=left+(xndc+1)width/2 x_{client}=left+(x_{ndc}+1)width/2
yclient=top+(1−yndc)height/2 y_{client}=top+(1-y_{ndc})height/2
function ndcToClient(ndc, canvas) {
  const rect = canvas.getBoundingClientRect();
  return {
    x: rect.left + (ndc.x + 1) * rect.width / 2,
    y: rect.top + (1 - ndc.y) * rect.height / 2
  };
}

getBoundingClientRect() 与 event.clientX/clientY 都使用 CSS 像素,因此这里不乘 devicePixelRatio。只有读取渲染缓冲或设置以实际像素为单位的区域时,才需要转换成 drawing buffer 坐标,并确认 Y 原点。

如果 DOM 标签相对于父容器绝对定位,应再减去该容器的 client 原点。若画布采用 scissor 或多个 viewport,公式需使用对应子视口的矩形,而不是整个 canvas。

4. 改变窗口尺寸时的职责

透视相机要同步 camera.aspect 并调用 updateProjectionMatrix();renderer 要更新尺寸。仅改画布 CSS 会拉伸画面,且鼠标拾取可能不再与画面匹配。正交相机则应根据业务更新 left/right/top/bottom。

near 应大于 0(透视相机),并尽量避免 near 极小而 far 极大;这会浪费深度精度,产生表面闪烁。修改投影参数后必须更新投影矩阵。

练习(不附答案)

  1. 给定 rect 为 left=100、top=50、width=800、height=400,计算 NDC 四角和中心的 client 坐标。
  2. 用相同 view 点对比透视与正交投影,改变深度后观察 x 的变化。
  3. 解释为什么高 DPI 屏幕下,DOM 标签位置不应该再乘 pixelRatio。
  4. 找一个位于相机背后的点,检验仅检查 NDC.x/y 是否会误判。