NDC裁剪与Y轴
Vertex shader 必须写出 clip space。GPU 做透视除法后落到 NDC:x,y,z ∈ [-1, 1](WebGL)。在这个盒子外面的图元被裁掉。CSS 像素、Canvas 2D 用户空间、WebGL NDC 不是同一套,命中和 overlay 全靠你换算。
1. 三套空间
| 空间 | 原点 | 用途 |
|---|---|---|
| CSS 盒 | 左上,Y 向下 | getBoundingClientRect、指针 |
| WebGL NDC / clip | 中心,Y 向上 | VS 输出 |
| drawing buffer | 左下,Y 向上 | viewport、readPixels |
export function cssToNdc(
cssX: number,
cssY: number,
cssWidth: number,
cssHeight: number,
): { x: number; y: number } {
return {
x: (cssX / cssWidth) * 2 - 1,
y: 1 - (cssY / cssHeight) * 2,
};
}
readPixels(0, 0, 1, 1) 读的是 左下角 像素,不是 CSS 的左上。把读回的 Uint8Array 当 ImageData 用,必须翻转行,或渲染时就按 2D 约定把投影 Y 乘 -1。
纹理从 <img> / 2D canvas 上传:浏览器图像原点在左上。生产几乎总会开:
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
和「自己生成的、已经是 GL 原点」的 Uint8Array 不要混用同一套 unpack 状态。
2. WebGPU 的 Y
WebGPU 默认 NDC Y 向上,但 framebuffer / 纹理的 texel (0,0) 在左上,更接近 CPU 图像。从 WebGL 迁过去时,投影矩阵和 blit pass 的翻转经常漏一次。写两套相机矩阵,标清楚目标 API,不要共用一个 mat4 常量当「世界到 clip」。
3. viewport 必须跟 drawingBuffer
export function syncViewport(gl: WebGL2RenderingContext): void {
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
}
CSS 把 canvas 拉到 800×400、backing store 仍是 300×150 时,只用 clientWidth 设 viewport,三角形会按错误纵横比裁。resize 流程见 DPR 与 drawingBuffer。
4. 失败形态
| 症状 | 原因 |
|---|---|
| 指针点在上方,高亮在下方 | NDC Y 没翻,直接用了 CSS Y |
| 截图上下颠倒 | readPixels 当 PNG 存却没翻行 |
| 贴图上下颠倒 | 忘了 UNPACK_FLIP_Y_WEBGL,或翻了两次 |
| 画面被裁一半 | viewport 用了 CSS 像素而不是 drawingBufferWidth |
权威资料
核对日期:2026-08-26