核心定义
NDC,Normalized 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 表示靠近远平面
先注意 x 和 y:
- 它们定义了一个标准的二维显示区域
- 不管屏幕多大,这个区域永远是
[-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/w,y/w更小(更靠近屏幕中心,视觉上更小) - 近处点的
w更小,除完后x/w,y/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 ~ width、0 ~ 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后,和绘制渲染区域的换算关系