GPU自动处理专题2-NDC是什么(透视除法)

核心定义

  • NDCNormalized Device Coordinates,中文一般叫标准化设备坐标
  • 它是 GPU 在裁剪空间之后、屏幕坐标之前的一个中间坐标空间。
  • 你可以把它理解成:GPU 把所有最终可见内容,先统一映射到一个标准立方体里,再决定它在屏幕上的像素位置。

在 WebGL / OpenGL 体系里,NDC 的范围通常是:

text 复制代码
x ∈ [-1, 1]
y ∈ [-1, 1]
z ∈ [-1, 1]

这 3 个范围非常重要。

如何理解

  • 不管你的画布是 300x150,还是 1920x1080,GPU 都不会一上来就直接处理"像素坐标"。
  • 它会先把可见空间里的点,统一变成一个标准范围:
    • 最左边是 x = -1
    • 最右边是 x = 1
    • 最下边是 y = -1
    • 最上边是 y = 1
  • 这样后续再映射到任何尺寸的屏幕都很方便。

所以,NDC 本质上是一个"和具体分辨率无关的标准坐标空间"。

NDC 在整个渲染流程里处于哪一步

一个顶点从模型到屏幕,大致要经历:

text 复制代码
局部坐标
-> 世界坐标
-> 观察坐标 / 摄像机坐标
-> 裁剪空间 Clip Space
-> NDC
-> 视口坐标 / 屏幕坐标

对应矩阵链路:

glsl 复制代码
gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aPosition, 1.0);

这里要注意:

  • 顶点着色器输出的 gl_Position 还不是 NDC
  • gl_Position裁剪空间坐标
  • GPU 后续会做一次透视除法
  • 透视除法之后,才得到 NDC

也就是:

text 复制代码
NDC = gl_Position.xyz / gl_Position.w

为什么叫"标准化设备坐标"

可以拆开理解:

Normalized,标准化

  • 表示坐标被压缩到了统一范围内
  • 不再依赖真实世界单位,也不依赖屏幕像素尺寸

Device,设备

  • 指最终显示设备之前的统一表示
  • 虽然叫 device,但它还不是具体屏幕像素坐标

Coordinates,坐标

  • 它仍然是空间位置,只不过是标准范围的位置

所以 NDC 其实就是:

  • 为了适配各种屏幕和后续光栅化流程,GPU 定义的一套标准中间坐标

NDC 为什么一定要存在

如果没有 NDC,会有很多麻烦:

  • 不同分辨率下,顶点坐标处理逻辑不统一
  • 不同投影方式不好接入后续流程
  • 裁剪、光栅化、视口映射不容易标准化
  • GPU 无法用统一规则处理所有图元

有了 NDC 之后,GPU 可以先假设:

  • 可见空间就是一个标准立方体
  • 任何投影结果,最终都要落到这个立方体里

然后再统一映射到屏幕像素。

所以 NDC 是整个图形流水线标准化的关键环节。

NDC 的范围具体代表什么

在 WebGL 中:

text 复制代码
x = -1   表示屏幕最左侧
x =  1   表示屏幕最右侧

y = -1   表示屏幕最下侧
y =  1   表示屏幕最上侧

z = -1   表示靠近近平面
z =  1   表示靠近远平面

先注意 xy

  • 它们定义了一个标准的二维显示区域
  • 不管屏幕多大,这个区域永远是 [-1, 1]

再注意 z

  • 它不是屏幕上下左右位置
  • 它是深度方向的位置
  • 后续会用于深度测试、遮挡关系等

最直观的例子

假设一个点经过所有矩阵变换后,透视除法得到:

text 复制代码
NDC = (0, 0, 0)

这表示:

  • 它在屏幕正中心
  • 深度在中间位置附近

如果一个点是:

text 复制代码
NDC = (-1, 0, 0)

表示:

  • 它在屏幕最左边中间

如果一个点是:

text 复制代码
NDC = (1, 1, 0)

表示:

  • 它在屏幕右上角

如果一个点是:

text 复制代码
NDC = (0.5, -0.5, 0)

表示:

  • 它在右半边
  • 偏下方

这就是 NDC 最直观的意义。

NDC 是怎么"算出来"的

这是最关键的一点。

NDC 不是直接由 model/view/projection 矩阵一步得到的。 它来自两步:

第一步:先得到裁剪空间坐标

也就是顶点着色器输出的:

glsl 复制代码
gl_Position

比如:

text 复制代码
gl_Position = (x_clip, y_clip, z_clip, w_clip)

第二步:GPU 自动做透视除法

text 复制代码
x_ndc = x_clip / w_clip
y_ndc = y_clip / w_clip
z_ndc = z_clip / w_clip

这一步之后,才得到真正的 NDC。

为什么要除以 w

这是理解 NDC 的灵魂。

因为透视投影要实现"近大远小"。

如果只是普通线性变换,很难直接表达这种效果。

所以 GPU 先在投影矩阵中构造出一个特殊的 w,然后在后续做:

text 复制代码
x / w
y / w
z / w

结果就是:

  • 远处点的 w 更大,除完后 x/wy/w 更小(更靠近屏幕中心,视觉上更小)
  • 近处点的 w 更小,除完后 x/wy/w 更大(更远离屏幕中心,视觉上更大)

于是就出现了透视效果。

所以:

  • Clip Space 是投影矩阵的直接输出
  • NDC 是透视除法后的结果
  • 透视感主要是在 Clip -> NDC 这一步真正体现出来的

你可以把 NDC 看成一个"标准立方体"

透视除法之后,GPU 认为所有可见内容应该都落在一个标准盒子里:

text 复制代码
左   = -1
右   =  1
下   = -1
上   =  1
近   = -1
远   =  1

这个盒子就是 NDC 空间。

如果某个点在这个范围之外,比如:

text 复制代码
x = 1.5

那它已经超出了可见区域,不会正常显示在当前视口里。

NDC 和裁剪空间有什么区别

很多人会把这两个混在一起。

裁剪空间,Clip Space

  • 顶点着色器输出 gl_Position 所在的空间
  • 坐标范围还没有标准化
  • 判断可见性时要和 w 一起比较:
text 复制代码
-w <= x <= w
-w <= y <= w
-w <= z <= w

NDC

  • 裁剪之后再除以 w 得到的空间
  • 范围已经标准化成:
text 复制代码
[-1, 1]

你可以这样记:

  • Clip Space:还带着 w,还没标准化
  • NDC:已经除完 w,进入标准范围

NDC 和屏幕坐标有什么区别

这也是最常见的混淆点。

NDC

  • 与分辨率无关
  • 范围固定是 [-1, 1]
  • 原点在中间

屏幕坐标

  • 与画布大小有关
  • 范围通常是像素范围,比如 0 ~ width0 ~ height
  • 原点位置受 API 和窗口系统约定影响

举个例子,假设画布大小是:

text 复制代码
800 x 600

那么 NDC 中的点:

text 复制代码
(0, 0)

会映射到屏幕中心,大约:

text 复制代码
(400, 300)

NDC 中:

text 复制代码
(-1, 1)

会对应屏幕左上角附近。

NDC 中:

text 复制代码
(1, -1)

会对应屏幕右下角附近。

所以 NDC 是"标准坐标",屏幕坐标是"具体像素坐标"。

视口变换就是把 NDC 变成屏幕坐标

这一步由 GPU 自动做,并且受:

js 复制代码
gl.viewport(x, y, width, height);

影响。

比如:

js 复制代码
gl.viewport(0, 0, canvas.width, canvas.height);

表示:

  • 把 NDC 的 [-1,1] x [-1,1]
  • 映射到整个画布区域

简单理解就是:

text 复制代码
x_ndc = -1  -> 屏幕左边
x_ndc =  1  -> 屏幕右边

y_ndc = -1  -> 屏幕下边
y_ndc =  1  -> 屏幕上边

所以 GPU 后续就能知道某个三角形到底覆盖了哪些像素。

为什么 z 也要在 NDC 里

很多人只关注 x/y,忽略 z

其实 NDC 的 z 非常重要,因为它决定:

  • 深度测试
  • 前后遮挡关系
  • 深度缓冲中的值分布

当两个物体在屏幕上重叠时,GPU 要知道谁挡住谁。 这就要看它们的深度值,而这个深度值就是从 NDC 的 z 继续转换而来的。

所以:

  • x/y 主要管"画在哪"
  • z 主要管"谁在前面"

为什么 NDC 的 z 范围也要标准化

因为 GPU 后续做深度测试时,需要统一规则。 如果每种投影、每种屏幕、每种场景都用不同的深度范围,整个硬件管线会很复杂。

所以 GPU 会先把深度压到统一范围。 WebGL / OpenGL 中,这个统一范围通常是:

text 复制代码
z ∈ [-1, 1]

然后再映射到深度缓冲使用的范围。

透视投影和正交投影都会产生 NDC 吗

会。

无论你用的是:

  • 透视投影矩阵
  • 正交投影矩阵

最终都要进入裁剪空间,再进入 NDC。

区别只是:

透视投影

  • w 很关键
  • 透视除法会产生近大远小

正交投影

  • 没有透视缩小
  • 即使也有 Clip -> NDC 这一步,视觉上不会出现近大远小

也就是说:

  • NDC 不是透视投影独有的
  • 它是整个图形管线通用的标准空间

一个简单数值例子

假设顶点着色器输出:

text 复制代码
gl_Position = (2, 1, 3, 2)

那么透视除法后:

text 复制代码
x_ndc = 2 / 2 = 1
y_ndc = 1 / 2 = 0.5
z_ndc = 3 / 2 = 1.5

得到:

text 复制代码
NDC = (1, 0.5, 1.5)

这里:

  • x = 1,刚好在右边界
  • y = 0.5,在上半部分
  • z = 1.5,已经超出标准深度范围

说明这个点在深度上可能已经超出可见体积,不会正常保留。

再看一个例子:

text 复制代码
gl_Position = (0.5, -0.5, 0, 1)

透视除法后:

text 复制代码
NDC = (0.5, -0.5, 0)

这表示:

  • 点在屏幕右下区域
  • 深度居中

初学者最容易犯的误解

  • 误解 1:gl_Position 就是屏幕坐标:不是,它先是裁剪空间坐标,之后才会变成 NDC,再变成屏幕坐标。
  • 误解 2:NDC 是 JavaScript 算出来的:一般不是,通常是 GPU 在顶点着色器之后自动完成的透视除法结果。
  • 误解 3:NDC 只和 x/y 有关:不是,z 也很重要,它决定深度和遮挡关系。
  • 误解 4:只有透视投影才有 NDC:不是,正交投影同样会进入 NDC。
  • 误解 5:NDC 和像素坐标是一回事:不是,NDC 是标准范围坐标,像素坐标是具体画布上的实际位置。

总结

  • NDC 就是 GPU 把顶点从裁剪空间通过除以 w变换后,得到的一套统一的标准坐标空间,范围通常是 x/y/z ∈ [-1,1]
  • 它的作用是让 GPU 用统一规则完成可见性判断、视口映射、光栅化和深度处理。

下一篇介绍得到NDC后,和绘制渲染区域的换算关系

相关推荐
后台开发者Ethan3 小时前
setState组件更新
前端·javascript·react
蜡台4 小时前
uniapp vue2 转 vue3
前端·javascript·uni-app·vue3·vue2
WindfallSheng4 小时前
从Vibe Coding到Spec Coding:一套可落地的AI-SDD企业级研发实战工程
javascript·vue.js
Asize4 小时前
Agent 入门:从 LLM 与 Agent 的区别到 Function Calling
javascript·人工智能
我有满天星辰5 小时前
Vue 指令完全指南:从 Vue 2 到 Vue 3 的演进与实战
前端·javascript·vue.js
小白学过的代码5 小时前
# flv.js / mpegts.js 播不了国标摄像头(H.265 + G.711A)?纯前端 Jessibuca 无后端方案(附完整代码)
前端·javascript·h.265
weixin_436804077 小时前
毒蘑菇性能测试增强版 - 毒蘑菇性能测试 - 在线 WebGL GPU 帧率压力测试工具
webgl
我有满天星辰8 小时前
Vue 3 响应式双雄:ref 与 reactive 完全指南
前端·javascript·vue.js
随风一样自由16 小时前
【前端独角兽】刚进入前端领域的前端开发用jQuery还是Vue3?
前端·javascript·vue3·jquery
YHHLAI17 小时前
[特殊字符] 面试中的 Promise —— 从入门到精通
前端·javascript·面试