news 2026/1/2 10:12:03

投影到裁剪空间(Clip Space):三维世界压成“标准盒子”

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
投影到裁剪空间(Clip Space):三维世界压成“标准盒子”

把点投影到裁剪空间(Clip Space),本质上就是:
把“从摄像机看到的那块三维视锥世界”,压扁变成一个规则的“标准盒子”,
方便 GPU 后面统一裁剪、归一化、映射到屏幕。

听着很玄,其实特别生活化:

  • 你眼前真实的世界:大小不一、远近不同、形状乱七八糟;
  • GPU 比较懒,喜欢处理“统一格式”的数据;
  • 于是,它想办法把“能看到的那块空间”捏成一个标准尺寸的长方体:
    • 中心是 0,
    • 左右上下前后就是 -1 到 1(一种约定),
    • 所有看得见的点都被塞进这个盒子里;
  • 超出盒子之外的,直接剪掉(裁剪);
  • 盒子内部再往屏幕上“摊平”,就成了一张图。

这篇文章,我们就用大白话,把下面这几个问题讲透:

  1. 裁剪空间(Clip Space)到底是个啥?
  2. 为什么非要把三维世界变成一个“标准盒子”?
  3. 投影矩阵(Projection Matrix)在整个过程里干了啥?
  4. 那个看起来很怪的w分量,以及“除以 w(透视除法)”是在干嘛?
  5. 点是怎么一步步从世界 → 摄像机 → 裁剪空间 → NDC → 屏幕的?
  6. GPU 在 Clip Space 里做了哪些操作(裁剪、可见性)?
  7. 实际写 Shader / 引擎时,你会怎么接触到 Clip Space?

看完之后,你在脑子里看到“Clip Space / 投影矩阵 / gl_Position”这些词时,
就不会觉得是在念咒,而是知道它们到底在做哪一步“压盒子”的事。


一、先回顾一下:我们为什么会走到“裁剪空间”这一步?

把坐标系整个链条先捋一遍:

  1. 模型空间(Model / Local Space)

    • 模型自己家里的坐标,只描述它长啥样。
    • 比如立方体顶点:(-1,-1,-1) ~ (1,1,1)。
  2. 世界空间(World Space)

    • 整个游戏世界统一的坐标系。
    • 所有物体摆到同一张地图上。
  3. 摄像机空间(View / Camera Space)

    • 以摄像机为原点和坐标轴的坐标系。
    • 所有东西变成“离相机前后左右上下多少”。

到这里,我们已经有了:

“以相机为中心、能看到的一块三维区域”——视锥体(Frustum)。

接下来要做两件事:

  1. 把这块“视锥体”的三维点,投影成一个规则的“标准盒子”:
    • 这个标准盒子就是裁剪空间(Clip Space,做透视除法前)
  2. 然后再做除以 w,变成 [-1,1] 的归一化坐标:
    • NDC(Normalized Device Coordinates,归一化设备坐标)

这一整套主要由一个矩阵负责:
投影矩阵(Projection Matrix)


二、先形象一点:视锥体 VS 标准盒子

2.1 视锥体是个啥?

想象你拿着一个相机:

  • 你眼前能看到的空间,大致像一个“前面开的锥形截头体”:
    • 离你太近的(摄像机前面的一小块)看不到(被 near plane 裁掉);
    • 离你太远的也看不到(被 far plane 裁掉);
    • 左右、上下有一个 FOV(视野角)。

这块空间叫做:视锥体(View Frustum)

形状大概像这样(侧视图):

/| / | 相机/ | 可见范围 \ | \ | \|

近裁剪面 + 远裁剪面 + 左右上下 4 个平面,一共 6 个平面包围着这一块空间。

2.2 Clip Space 要做的事情:把这个不规整的锥体 → 变成标准长方体

GPU 数学家们有一个习惯:

不喜欢直接处理“斜着的、不规整的锥体”,
喜欢统一把东西变成一个标准的长方体(盒子)。

为什么?

  • 规则盒子更好判断点在不在里面:
    • 只要 x、y、z 在某个范围内就行;
  • 更适合硬件里一套固定的裁剪逻辑;
  • 为后续的归一化、屏幕映射打基础。

所以,投影矩阵干的第一件大事就是:

把摄像机空间里的视锥体“拉伸 + 扭曲”成一个标准长方体,
这个长方体对应的坐标,就是剪裁空间(Clip Space)。


三、Clip Space 是个什么样子的“标准盒子”?

在大多数学库/API 中,Clip Space 是一个用齐次坐标表示的四维空间:

  • 一个点在 Clip Space 中通常写成(x, y, z, w)
  • 后面会用x/w, y/w, z/w得到 NDC(归一化设备坐标)。

在做“判断点在不在可见空间里面”之前,Clip Space 中有这么一个约定:

在透视除法之前,点要满足:
-w ≤ x ≤ w
-w ≤ y ≤ w
以及 z 与 w 的某些范围关系(比如 -w ≤ z ≤ w,或 0 ≤ z ≤ w)。

简单说,就是一个“以原点为中心,边界是 ±w 的四维盒子”。

虽然听起来抽象,但你可以这样理解:

  • 把 w 当成一个“缩放参考值”;
  • 当前帧能看见的东西,都被挤压到这个“x,y,z ∈ [-w, w]”的区域内;
  • 之后通过/w把这个盒子缩成 [-1,1] 的标准立方体(NDC)。

所以:

  • Clip Space = 投影矩阵刚变换完的、还没除以 w 前的空间;
  • NDC = Clip Space 除以 w 之后的 [-1,1] 标准盒子。

四、投影矩阵(Projection Matrix):负责“压盒子”的核心角色

我们先不用公式吓自己,用直觉描述投影矩阵要做的三个事:

  1. 让远处的东西看起来更小(透视);
  2. 把视野角、宽高比、近远平面这些乱七八糟的参数,编码进坐标变换中;
  3. 把不规整的“斜锥”变成规整可控的“盒子”。

4.1 透视投影 vs 正交投影

一般游戏里常用两种投影方式:

  1. 透视投影(Perspective Projection)

    • 模拟人眼 / 相机的效果:
      • 近大远小;
    • 适合 3D 场景。
  2. 正交投影(Orthographic Projection)

    • 不考虑距离引起的缩放,远处物体大小不变;
    • 看起来像工程图或某些 2D / 策略游戏的效果;
    • 用于 UI、某些编辑器视图。

这两种投影对应不同的 Projection 矩阵,
但它们都完成了“把视锥(或视体)映射到标准盒子”的任务。

4.2 透视投影:近大远小是怎么“装”进矩阵里的?

在 Camera Space 中:

  • 一个点的坐标是(x, y, z)
  • 平常的直觉是:离相机越远(|z|越大),这个点投影到屏幕上应该越接近中心、越小。

简单的透视关系像这样:

x_screen ∝ x / z y_screen ∝ y / z

也就是说:

  • x 和 y 需要除以 z,才能体现近大远小。

但是我们的矩阵变换喜欢做的是:

“线性变换”:M * (x,y,z,1),
/z是个非线性的操作。

怎么办?
人类想出一个骚操作:引入第四个分量 w,用齐次坐标。

投影矩阵会把 Camera Space 坐标(x,y,z,1)变成:

ClipPos = (x', y', z', w')

并且设计成:
x’ 和 y’ 里面“偷偷包含了 z 的信息”,
使得最后:

ndc.x = x' / w' ndc.y = y' / w'

刚好就等价于之前那个x / z,y / z的透视关系。

大白话:

投影矩阵把“除以 z”这件事,藏进了“除以 w”的步骤里。
所以你在代码里只看到clipPos = Proj * viewPos; ndc = clipPos / clipPos.w;
而透视效果就在/w那一下完成了。

4.3 近远平面、视野角、宽高比怎么藏进矩阵?

投影矩阵的参数一般包括:

  • fov(Field of View):视野角;
  • aspect:宽高比(screenWidth / screenHeight);
  • near:近裁剪面距离;
  • far:远裁剪面距离。

这些参数会影响:

  • x,y 的缩放范围(视野越大,x,y 范围越宽);
  • z 的映射(如何把 [near,far] 映射到 Clip Space 的 z范围,再到 NDC,再到深度缓冲的数值)。

你不用背公式,只要记住:

投影矩阵根据 fov、aspect、near、far,
决定“视锥体的形状”和“如何把它压成盒子”。


五、那个神秘的 w 分量 & 透视除法(Perspective Divide)

现在我们来正面聊一聊大家最容易迷糊的地方:齐次坐标和 w

5.1 为啥要多一个 w?原来的 (x,y,z) 不够用吗?

如果我们只用 3 维坐标(x,y,z)做线性变换:

vec3' = M3x3 * vec3 + t

它能表达的是:

  • 平移、旋转、缩放、剪切、线性变换等等;
  • 但没办法用一个矩阵统一表达“透视投影”这种带除法的非线性操作。

引入齐次坐标(x,y,z,w)后:

  • 可以把各种仿射变换、投影变换统一表示成一个 4x4 矩阵乘法;
  • 然后统一通过一次/w完成以前那些“看起来非线性”的缩放。

你可以用一个很粗糙但好记的比喻:

w 就像是一个“还没结算的缩放因子”,
先把所有变换放在 (x,y,z,w) 里,
最后一次除以 w来“结账”,
得到最终的 3D 坐标。

5.2 Clip Space 到 NDC:透视除法

在顶点着色器里,你经常会看到这样的代码(以 OpenGL 为例):

gl_Position = ProjectionMatrix * ViewMatrix * ModelMatrix * vec4(localPos, 1.0);

这里的gl_Position就是 Clip Space 坐标(x', y', z', w')

GPU 在后续阶段会自动做:

ndc.x = gl_Position.x / gl_Position.w; ndc.y = gl_Position.y / gl_Position.w; ndc.z = gl_Position.z / gl_Position.w;

这一行,叫做:透视除法(Perspective Divide)

除完以后:

  • (ndc.x, ndc.y, ndc.z)一般都落在 [-1,1] 的范围内(在视野里的点);
  • 这些点就落到了所谓的 NDC 空间(归一化设备坐标)。

远处的点:z 大,w 也会按设计变大,导致 x/w、y/w 更接近 0——
于是被投影到屏幕中心附近,看上去更小;
近处的点:z 小,比例更大,被投影到屏幕边缘更开阔的位置,看起来更大。

透视感的本质,就体现在这一步/w上。


六、Clip Space 到 NDC,再到屏幕:盒子缩小 → 平铺到屏幕

我们已经知道:

  • Clip Space:(x', y', z', w')
  • 透视除法后:NDC,(x/w, y/w, z/w)

6.1 NDC:一个固定 [-1,1] 的标准立方体

NDC 中:

  • x ∈ [-1, 1]:

    • -1 = 左边界
    • 1 = 右边界
  • y ∈ [-1, 1]:

    • -1 = 下边界(OpenGL) 或上边界(视 API 而定);
    • 1 = 上边界或下边界。
  • z ∈ [-1, 1] 或 [0, 1](视 API 而定):

    • 表示深度,最终会映射到深度缓冲里。

此时我们的三维世界,被统一压到了一个“单位立方体”里。

6.2 再从 NDC 到屏幕像素

最后一步:把 [-1,1] 的立方体投影到实际屏幕的像素坐标系(0 ~ width, 0 ~ height)。

示意公式(以左下角为 (0,0) 为例):

screenX=(ndc.x*0.5f+0.5f)*ScreenWidth;screenY=(ndc.y*0.5f+0.5f)*ScreenHeight;

如果你的屏幕 y 轴向下,则可能是:

screenY=(1.0f-(ndc.y*0.5f+0.5f))*ScreenHeight;

到这一步,我们终于从:

模型空间点 → 世界空间 → 摄像机空间 → Clip Space → NDC → 屏幕像素

一路走到底,完成了“从 3D 点到 2D 像素”的整个投影过程。


七、GPU 在 Clip Space 里做啥?“裁剪”的重点来了

名字叫Clip Space
“Clip” 就是 “裁剪、截断” 的意思。

7.1 裁剪啥?

很简单:

裁剪那些“在视野外”的部分——
也就是超出标准盒子范围的点和三角形。

在 Clip Space(或者 NDC)里:

  • 如果一个顶点的坐标在 [-w, w](或 [-1,1])之外,说明在视野外;
  • GPU 会自动对这些三角形做裁剪:
    • 完全在外面的直接丢弃;
    • 一部分在内、一部分在外的,会在边界上“切一刀”,只保留可见部分。

这样就有两个好处:

  1. 后面光栅化阶段,就不会为看不到的区域生成片元(像素),节省大量计算;
  2. 裁剪逻辑在硬件中是高度优化的,基于 Clip Space 统一处理。

7.2 为什么不在世界空间里裁剪?

理论上也可以直接在世界空间中用视锥平面来裁剪,但那样:

  • 不同摄像机视野形状不同,数学复杂度高;
  • 硬件实现上难以统一和优化。

而 Clip Space / NDC 的好处是:

已经把“各种各样的视锥体”压成了“统一的标准盒子”,
所以裁剪就是检查 ±1/±w 的简单范围问题。

GPU 的设计就是围绕这个“标准盒子”来的。


八、用一个完整的小故事再走一遍:一个点的一生

我们来给一个 3D 点编一个“人生经历”,帮助你把整个流程串成一条线。

场景设定

  • 模型:一辆车,模型空间下顶点之一是 LocalPos = (1,0,0);
  • 这辆车放到世界空间的 (10,0,50),略微旋转了一下;
  • 摄像机在世界位置 (0,3,0),看向世界 +Z;
  • FOV = 60°,近裁剪 0.1,远裁剪 100。

1. 模型空间(Local)

LocalPos = (1,0,0) 表示:

在车的模型内部,这个顶点是“右侧边上某一点”。

2. 世界空间(World)

WorldPos=ModelMatrix*LocalPos;

ModelMatrix 根据车的位置 / 旋转 / 缩放生成。

得到 WorldPos,比如:

(10.8, 0.2, 50.5)

含义:

在整个游戏世界地图中,这个点在 (10.8, 0.2, 50.5)。

3. 摄像机空间(View / Camera)

ViewPos=ViewMatrix*WorldPos;

ViewMatrix 把世界坐标变到“以摄像机为原点”的坐标系中。

假设得到:

ViewPos = (vx, vy, vz)

比如 (1.5, -0.3, 20.0)

含义:

从摄像机眼睛看出去,这个点在右边 1.5、下面 0.3、前方 20 距离处。

4. Clip Space(透视投影前)

ClipPos=ProjectionMatrix*ViewPos;// 得到 (cx, cy, cz, cw)

投影矩阵把视锥体压成“标准盒子”。

ClipPos 可能是:

(30, -6, 19, 20)

暂时不要管数值,只知道:

  • x,y,z,w 内部都编码了“远近位置 + 视野边界”的信息。

5. 透视除法:变成 NDC

ndc.x=30/20=1.5ndc.y=-6/20=-0.3ndc.z=19/20=0.95

明显 ndc.x = 1.5 超出 [-1,1],
说明这个点(或所在三角形的一部分)已经在屏幕右侧外面了。

根据三角形其他顶点位置,GPU 会决定如何裁剪。

如果是另外一个点,ndc 落在 [-1,1] 内,
那么就表示在可见范围中。

6. 映射到屏幕像素

假设 ndc 在 [-1,1] 内,
再做:

screenX=(ndc.x*0.5+0.5)*width;screenY=(1-(ndc.y*0.5+0.5))*height;

最终得到一个(screenX, screenY)
那个像素就是这辆车这个顶点投影到屏幕上的位置。


九、工程实践中:你实际“看见 / 使用” Clip Space 的地方

9.1 顶点着色器里的 gl_Position / SV_Position

在 OpenGL / GLSL 中:

gl_Position = Projection * View * Model * vec4(localPos, 1.0);

这里的gl_Position就是Clip Space 坐标

在 HLSL / DirectX 中,类似:

float4 clipPos = mul(Proj, mul(View, mul(Model, localPos))); return clipPos; // 语义 SV_Position

这个返回值被标记为SV_Position
就是裁剪空间中的位置。

你不需要手写“除以 w”,GPU 会在后续针对每个顶点统一做。

9.2 手写投影矩阵的情况(不常见,但有时需要)

大部分时候你会用引擎 API:

  • Unity:Matrix4x4.Perspective(fov, aspect, near, far)
  • DirectX:XMMatrixPerspectiveFovLH
  • GLM(C++):glm::perspective(...)

但有时候你需要更高级玩法(比如自定义投影、非对称视锥),
就得自己构造 Projection Matrix,
这时理解 Clip Space 的“标准盒子”就很重要了:
你要知道你的矩阵最后要把点变成哪种范围的 box,
才能和 GPU 默认裁剪逻辑对得上。

9.3 做反向推导:从屏幕坐标 / NDC 反推回世界坐标

一些高级效果需要你从屏幕坐标 / 深度信息反推 3D 位置,例如:

  • 屏幕空间反射(SSR);
  • 屏幕空间环境光遮蔽(SSAO);
  • 根据点击屏幕位置射线选中 3D 对象(鼠标 picking)。

这些都需要你:

  1. 把屏幕坐标 → NDC(反向 viewport);
  2. 把 NDC → Clip Space(乘以 w 或构造一个适当的 clipPos);
  3. Clip → View(乘以投影矩阵的逆);
  4. View → World(再乘 View 矩阵的逆)。

这整个反向链路,正是建立在你理解 Clip Space 和 Projection 的基础上。


十、最后,用几句话把“投影到裁剪空间”压缩在脑子里

我们再用特别大白话的方式,把这个概念彻底钉牢:

  1. 摄像机空间里有一个“视锥体”:就是你能看到那一块 3D 空间。

  2. 投影矩阵做的事:

    • 把这个不规整的“视锥体”压扁、拉伸,
    • 变成一个统一标准的“盒子”(Clip Space);
    • 顺便把透视(近大远小)、视野角、近平面/远平面这些都编码进去。
  3. Clip Space 是变换后的中间状态:

    • 点用齐次坐标(x,y,z,w)表示;
    • 还没进行/w的透视除法;
    • 但已经具备了“用 ±w 来判断是否在视野内”的结构。
  4. 透视除法(除以 w) → NDC:

    • ndc = clipPos / clipPos.w
    • 这一步实现了近大远小,把整个可见空间变成 [-1,1] 的标准立方体(NDC)。
  5. GPU 在 Clip/NDC 空间里做裁剪:

    • 超出边界的三角形部分被剪掉;
    • 只留下盒子里面的部分进入光栅化阶段。
  6. 最后从 NDC 映射到屏幕像素:

    • 再把 [-1,1] 映射到 [0,width]/[0,height],
    • 每个点就成了屏幕上的一个像素位置。

如果用一句特别形象的话总结:

“投影到裁剪空间”,
就是把你眼前那块立体的视野世界,
压成一个 GPU 最爱处理的“标准盒子格式”,
然后再从这个盒子里剪剪剪,
剩下的部分摊平成屏幕上的一帧画面。

以后你再看到:

gl_Position = Projection * View * Model * vec4(localPos, 1.0);

就可以在心里翻译成:

“好,现在我要把这个 3D 点一路送到那个标准视锥盒子里去,
方便 GPU 帮我裁剪、投影、画到屏幕上。”

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2025/12/14 9:13:27

GPU渲染:顶点如何连成三角形并裁剪

先把一句话摆在最前面:顶点阶段只是把“点”算好了位置, 图元装配 & 裁剪这一步,干的事情就是: 把这些点按索引连成三角形,然后把跑出摄像机视野外的那部分三角形切掉或扔掉。你可以把它想象成: 顶点阶…

作者头像 李华
网站建设 2025/12/14 9:12:57

SubtitleOCR技术实现:从视频硬字幕到结构化文本的智能转换

SubtitleOCR技术实现:从视频硬字幕到结构化文本的智能转换 【免费下载链接】SubtitleOCR 快如闪电的硬字幕提取工具。仅需苹果M1芯片或英伟达3060显卡即可达到10倍速提取。A very fast tool for video hardcode subtitle extraction 项目地址: https://gitcode.co…

作者头像 李华
网站建设 2025/12/24 6:54:14

uiautomator2图像识别性能优化实战:从卡顿到流畅的完整解决方案

"为什么我的自动化脚本总是卡在图像识别环节?"这是许多Android自动化开发者经常遇到的困扰。当你在凌晨三点盯着CPU占用率飙升到85%的监控面板时,是否也曾怀疑过自己的代码?本文将带你深入剖析uiautomator2图像识别的性能瓶颈&…

作者头像 李华
网站建设 2025/12/19 6:13:40

终极跨平台直播聚合神器:Dart Simple Live完整使用指南

终极跨平台直播聚合神器:Dart Simple Live完整使用指南 【免费下载链接】dart_simple_live 简简单单的看直播 项目地址: https://gitcode.com/GitHub_Trending/da/dart_simple_live 还在为频繁切换不同直播应用而烦恼吗?想要在一个界面中同时观看…

作者头像 李华
网站建设 2025/12/14 9:12:24

Tiled地图渲染优化:提升大型游戏场景性能的关键技术

Tiled地图渲染优化:提升大型游戏场景性能的关键技术 【免费下载链接】tiled 项目地址: https://gitcode.com/gh_mirrors/til/tiled 在游戏开发领域,Tiled地图编辑器作为专业的瓦片地图创建工具,其渲染性能直接影响游戏体验。面对日益…

作者头像 李华
网站建设 2025/12/14 9:12:21

Flame噪声算法实战指南:从基础理论到地形生成应用

Flame噪声算法实战指南:从基础理论到地形生成应用 【免费下载链接】flame A Flutter based game engine. 项目地址: https://gitcode.com/GitHub_Trending/fl/flame 是否曾为重复的地形设计感到困扰?想要创造无限延伸的自然景观却不知从何入手&am…

作者头像 李华