先把核心结论用最直白的话说出来:
裁剪的六个平面,其实就是:
给摄像机前面那一块“能看到的空间”做了一个看不见的长方体外壳,
这个壳有六个面:左、右、上、下、近、远。
凡是跑到这六个面外面的东西——统统不画;
被压在这六个面之间的那一块空间,就是 GPU 真正要渲染的“视野空间”。
这六个平面就是:
- 左平面(Left Plane)
- 右平面(Right Plane)
- 上平面(Top Plane)
- 下平面(Bottom Plane)
- 近平面(Near Plane)
- 远平面(Far Plane)
它们一起围出一个“空间盒子”:
在透视投影时是个截掉尖端的锥体(视锥体),
在投影/变换之后,经常会变成一个规整的盒子(剪裁空间/NDC 里的立方体)。
这篇我们用非常大白话、带点数学但不吓人的方式,
把这六个平面讲清楚,目标是:
- 你脑子里能有一个立体画面:摄像机 + 六个平面 + 视锥体;
- 知道为什么必须有这六个平面,而不是三五个;
- 知道这些平面跟FOV / near / far / aspect / 投影矩阵 / Clip Space / NDC有什么关系;
- 大致明白“一个点/一个三角形是怎么被判定在不在视野里的”;
尽量用生活比喻说明白,公式只在需要时上,保证能看懂。
一、六个平面到底是“啥”?先把画面感建立起来
1.1 想象你拿着一个手电筒
手电筒前面发出一束锥形的光:
中间亮、两边越来越暗,边缘之外是黑的。
我们只关心“亮光照到的那一块空间”,
这块空间大致像一个锥体:
- 离你太近:光没发散开,甚至还没出筒口,可以看作是“不可见”;
- 离你太远:光也照不到;
- 左右、上下都有一个“边界角度”,再往外就出光束了。
如果我们用六块透明玻璃,把这束光“包起来”,
就得到了六个平面:
- 左边一块玻璃:左边界;
- 右边一块:右边界;
- 上下各一块:上边界、下边界;
- 离手电筒很近的那块:近平面;
- 离手电筒比较远的那块:远平面。
玻璃外面的世界统统不管,
玻璃里面那一块,就是“视野空间”。
1.2 摄像机视锥体 = 手电筒光束 + 六块玻璃
在 3D 渲染里:
- 手电筒换成“摄像机”;
- 光束换成“摄像机能看到的那一块空间”(视锥体);
- 六块玻璃换成“六个裁剪平面”。
这六个平面一起围出摄像机的视锥体(View Frustum):
- 左、右、上、下:由视野角(FOV)和宽高比(aspect)决定的斜面;
- 近、远:与相机平行的两个平面,决定能看到多近、多远。
视锥体里——才是 GPU 要认真算的那一段世界。
二、为什么不是“一个大球”而是“六个平面”?
你可能会问:
反正就是决定“看到哪儿、不看到哪儿”呗,
为啥非要搞“六个平面”,不能说“一个球半径多大”之类的?
原因有两个大方面:符合透视 + 数学简单。
2.1 透视的视野天生是“锥形”的,而不是球
摄像机的视野是一个角度,不是一个固定半径的球:
- 左右有一个水平方向的 FOV(比如 90°);
- 上下也有一个垂直方向的 FOV(比如 60°);
- 近/远各有一个距离(比如 0.1 到 100 单位)。
所以你能看到的是“从相机出发的一个角度范围内的空间段”,
这东西最像的形状就是:截头锥(Truncated Frustum)。
要用平面来描述“角度 + 距离”的边界,刚好就是六个:
- 左边界:在空间中对应一块“向内倾斜”的平面;
- 右边界:同理;
- 上、下两块;
- 再加前后(近、远)两块与相机平行的平面。
2.2 用平面来定义边界,数学和实现都特别好搞
一个平面可以写成很简单的方程:
ax + by + cz + d = 0或者用向量形式:
N · P + d = 0 // N 是法线,P 是点 (x,y,z)判断一个点在平面的哪一侧,只要看:
v = N · P + d v > 0 → 在平面某一侧(比如“内侧”) v < 0 → 在另一侧(比如“外侧”) v = 0 → 正好在平面上把六个平面都写成这种形式:
- 左平面:点在内侧 → 保留;
- 右平面:内侧 → 保留;
- ……六个都通过 → 在视锥体里。
对 GPU 来说:
判断一个点/三角形是否在视锥体里,就是做六次“点乘 + 加法 + 比较”的事,简单到爆,高度可并行。
而且:
- 裁剪三角形与平面的交点,在线性代数里也是基础操作;
- 比起球、复杂曲面,平面是最适合硬件大规模处理的。
三、先在“摄像机空间”里看这六个平面:最直观
为了直觉清楚,我们先在摄像机空间(View / Camera Space)下看这六个平面。
假设约定:
- 摄像机位置就是原点 (0,0,0);
- 摄像机看向 -Z 方向(OpenGL 常用约定);
- +X 往右,+Y 往上。
这样,视锥体大概长这样(侧视 & 俯视综合想象):
上平面 /| / | / | / | 远平面 相机/____| \ | \ | 近平面 \ | \ | \| 下平面从这个角度看:
- 近平面:z = -near(离相机最近的一块“可见墙”);
- 远平面:z = -far(最远能看到的那块“可见墙”);
- 左右上下平面:都是通过相机原点的一组斜面,
由 FOV 和宽高比决定它们的倾斜程度。
我们挨个用大白话解释。
四、近平面 & 远平面:前后两个“剪刀口”
先从最容易理解的两个讲起。
4.1 近平面(Near Plane):离相机最近的“可见起点”
定义:
- 在摄像机空间里,是一个与相机平行的平面;
- 一般是:
z = -near(看向 -Z 时),或z = +near(看向 +Z 时,看约定); - near 是一个 >0 的距离,比如 0.1、0.3、1.0 等。
它的作用就两点:
防止太靠近相机的东西被“穿过镜头”显示得奇形怪状;
比如你把一个物体塞到相机内部,会看到各种穿模怪象。
近平面相当于说:“离我近到一定程度的东西,我就当看不见。”
配合远平面决定深度缓冲精度:
- near 设太小,深度分布极度不均,近处特别敏感,远处全挤在一起,
结果就是各种 Z-fighting(闪动、贴图打架); - near 适当大一点,可以显著改善深度精度。
- near 设太小,深度分布极度不均,近处特别敏感,远处全挤在一起,
你可以把近平面理解成:
相机前面的一块“无形玻璃”:
玻璃后面开始才是真正的世界,玻璃前面统统忽略。
4.2 远平面(Far Plane):最远能看到的那堵“墙”
定义:
- 在摄像机空间里,也是一个与相机平行的平面;
- 一般是:
z = -far(或 +far,看约定); - far 是一个比较大的距离,比如 100、1000、10000。
作用:
限制“最远能看到的距离”:
远平面后面的东西,直接认为不可见;
否则无限远的世界无法渲染完。同样影响深度精度:
- far 越远,整个 [near, far] 区间越大;
- 而深度缓冲位数是有限的(比如 24bit);
- 远处一大段挤在很小的深度差里,导致精度不足。
你可以把远平面理解成:
世界尽头的一堵“隐形墙”:
超过这个墙,你就算再往前走,摄像机也不管你了。
五、左平面 & 右平面:视野左右边界
有了前后边界,再看左右。
5.1 再用“手机相机”的视野感受一下
你举起手机:
- 屏幕两侧的边缘,就是“你能看到的世界”的左右边界;
- 再往外的东西就不在画面里,等于你眼睛/相机视野的左右极限。
在 3D 空间里,这两个边界对应的就是两块斜着的平面:
左平面、右平面。
5.2 这些平面是怎么决定方向的?
在摄像机空间下:
- 摄像机在原点 (0,0,0);
- 看向 -Z;
- 设置一个水平 FOV(水平视野角)或者用垂直 FOV + 宽高比推出来。
假设垂直视野角是fovY,宽高比是aspect = width/height。
通过几何关系可以推得一个水平视角fovX或 x/z 的关系。
简化理解:
- 在 z = -1 的截面上,能看到的 x 范围是
[-right, right]; - 这个 right = tan(fovX/2);
- 在更远的 z,比如 z = -k,这个范围就变成
[-k*right, k*right],
也就是一个“向外发散”的斜界线。
把这些边界线在 3D 中延长,就成了一块斜平面。
这块平面就是“左平面”或“右平面”,
所有可见点都必须在这些平面的“内侧”。
5.3 左右平面的平面方程(感受一下,不要害怕公式)
在摄像机空间中,
如果我们取视锥体的一侧边作为一个平面,可以表示成:
N_left · P + d_left >= 0 → 在左平面的内侧 N_right · P + d_right >= 0 → 在右平面的内侧其中:
- N_left 是左平面的法线,指向视锥体内部;
- P 是任意空间中的点;
- d 是偏移。
你不需要死记推导,只要知道:
这些平面是通过相机原点的斜面,
用几何关系(FOV、aspect)明确了它们的“倾斜角度”;
点如果跑到“外侧”,P 插进去就会算出 “<0” 的值,被裁掉。
六、上平面 & 下平面:视野上下边界
同理,上下两个边界也对应两块斜平面。
6.1 垂直 FOV 决定“头顶能看到多少天花板”
垂直 FOV(Field of View in Y)表示:
相机中轴线以上能看到多少度、以下能看到多少度。
比如 fovY = 60°:
- 中线以上 30°;
- 中线以下 30°。
这样在 z = -1 的截面上:
- y 范围是
[-top, top], - top = tan(fovY / 2)。
往更远处 z = -k,这个范围变成[-k*top, k*top],
在空间中展开同样形成两个向外倾斜的平面:
- 上平面;
- 下平面。
6.2 上下平面的方程同理
同样可以写成:
N_top · P + d_top >= 0 N_bottom· P + d_bottom>= 0这些平面围成了视锥体的上、下边界。
你可以脑补成:
把相机前面的世界夹在一个“插上了四面斜玻璃板”的漏斗形空间里,
四个斜玻璃板就是“左、右、上、下”平面。
七、把六个平面合在一起:一个完整的视锥体
六块平面一起,就形成这么一个东西:
- 近平面:靠你比较近的那一块截面;
- 远平面:远处那一块截面;
- 左平面 + 右平面 + 上平面 + 下平面:从相机原点出发的四面斜墙。
几何形状就是:
/|\ ← 上平面 / | \ / | \ / | \ /----+----\ ← 远平面 \ | / \ | / \ | / \ | / \|/ ← 下平面中间是相机眼睛的位置(原点),
你可以想象自己身处顶点,四面八方是这六面看不见的“玻璃墙”。
墙里面的世界,才会被渲染。
八、从“摄像机空间”到“裁剪空间”再到 NDC:平面的“变形记”
前面讲的六个平面是在摄像机空间里的原始定义。
但在实际 GPU 实现里,裁剪经常发生在Clip Space / NDC中。
我们大致梳理一下它们是怎么“变形”的。
8.1 Camera Space → Clip Space(乘投影矩阵)
在顶点阶段,我们做了:
clipPos = Projection * viewPosviewPos 是摄像机空间坐标(x, y, z, 1),
clipPos 是裁剪空间坐标(x', y', z', w')。
投影矩阵的作用包括:
- 把视锥体压成一个更规整的“标准盒子”的形状;
- 同时为后续透视除法准备好 w。
在 Clip Space 下,裁剪规则经常被写成:
-w <= x <= w -w <= y <= w -w <= z <= w // 或 0 <= z <= w,视 API 约定你可以理解为:
原来那六个斜面,在这个空间里变成了“居然就是一些简单的坐标范围限制”。
8.2 Clip Space → NDC(除以 w)
GPU 做了透视除法:
ndc = clipPos / w得到 NDC:
-1 <= ndc.x <= 1 -1 <= ndc.y <= 1 -1 <= ndc.z <= 1 或 0<=z<=1在 NDC 下,视锥体就变成了一个标准立方体:
[x,y,z] ∈ [-1,1]^3 或 z∈[0,1]所以:
- 六个平面最终在 NDC 中,就是这个立方体的六个“正方形面”:
- x = -1(左)、x = +1(右);
- y = -1(下)、y = +1(上);
- z = -1/0(近)、z = +1(远)。
在这个空间中,裁剪变得非常容易:
判断点/三角形是否在 [-1,1] 盒子里。
九、一个点 / 一个三角形如何通过“六个平面”的考验?
现在我们用“考试通关”的比喻讲一遍流程。
9.1 一个点的判定:过六关才能见相机
假设有个点 P(不管是世界空间还是摄像机空间的坐标,都能转换过来),
要判断它在不在视锥体里,简单数学版就是:
把 P 变到摄像机空间(或直接用摄像机空间的 P_view);
用六个平面的方程来测试:
对于每个平面 i: v_i = N_i · P_view + d_i 如果 v_i < 0:在平面外 → 扔掉 如果对所有 i 都 >= 0:在所有平面内 → 点在视锥体中
形象一点:
P 要经过“左、右、上、下、近、远”这六道门的审查,
在任何一道被打回票,就不会出现在渲染结果中。
在 Clip / NDC 空间下,这个判断甚至更简单直接:
if (x < -1 || x > 1 || y < -1 || y > 1 || z < zMin || z > zMax) 外;否则内。9.2 三角形的判定 & 裁剪:三个人一起考试
对于一个三角形,有三个顶点 P0, P1, P2,每个都要被平面测试。
对某个平面来说:
- 三个顶点都在“内侧”:这个三角形对这个平面来说安全;
- 三个都在“外侧”:对这个平面来说整个三角形 OUT,可以直接丢掉;
- 有的内,有的外:
- 在这条平面上,“边 + 平面”的交点处会产生新的顶点;
- 旧的三角形被切成新的一个或两个三角形,保留在内侧那部分。
对六个平面都执行一遍后:
这个三角形原地发生了“瘦身、截断、或者死亡(被裁掉)”,
剩下的就是最终要光栅化的形状。
十、这六个平面跟投影矩阵 / FOV / near / far 的关系再帮你捋一下
你写代码时,其实是通过几个简洁的参数间接设置这六个平面:
- fov(视野角);
- aspect(宽高比);
- near(近平面距离);
- far(远平面距离)。
比如在 Unity / OpenGL / DirectX 中,你可能写过:
Matrix4x4P=Matrix4x4.Perspective(fov,aspect,near,far);或者:
autoP=glm::perspective(fov,aspect,near,far);这些函数内部做的事可以概括为:
- 先在摄像机空间里,根据 fov + aspect 推出左右/上下平面的大致“开口角度”;
- 用 near、far 确定前后两个平面的位置;
- 用一套数学推导(透视投影矩阵),把这六个平面映射到 Clip Space 的统一形式;
- 这样:
- 你就只需要记住 fov/near/far 这几个高层概念;
- GPU 内部则能把所有点根据那六个平面进行裁剪。
所以你可以把这套关系记成一句:
你输入的是“摄像机视野参数”,
投影矩阵则在后台帮你算出了“六个裁剪平面”的具体形状,
GPU 利用这些平面把看不见的东西裁掉。
十一、实际开发中,六个裁剪平面能被你“用”在什么地方?
除了 GPU 自动裁剪之外,六个平面的概念还经常在你写游戏逻辑时直接用到,叫:视锥体裁剪(Frustum Culling)。
11.1 CPU 端的“粗略裁剪”:不送给 GPU 的物体就更省事
例如:
- 一个场景里有几千个物体;
- 其中一大部分压根不在摄像机的视野内(比如在你背后、非常远的地方);
- 你可以在 CPU 上先做一遍Frustum Culling:
- 用视锥体的六个平面判断物体的包围盒(AABB / 球)在不在视锥体中;
- 不在的物体,连 DrawCall 都不发——直接不让它上 GPU。
这样能大幅减轻 GPU 负担。
常用写法(伪代码):
foreach object:if(object.BoundingBox 与 ViewFrustum 有交集):提交绘制else:跳过而 ViewFrustum 就是由那六个平面定义的。
11.2 做特殊效果:镜面、水面、传送门的“自定义裁剪平面”
有时你会把“裁剪平面”的思路,用在有趣的画面技巧上,例如:
- 渲染水面反射时,只想渲染水面上方的场景:
在水面那一层设置一个裁剪平面,剪掉下方世界; - 渲染镜子:只渲染镜子所面对的一侧空间;
- 做某种“只显示一个世界窗口”的效果。
这些本质上就是:
在原来的六个平面基础上,人为再加一块平面,
用它去把空间截成两半,只在一边画东西。
高级引擎 & GPU API 常提供接口来设置“额外的 Clip Plane”。
十二、最后,用一大段顺口的大白话把它总结到底
你可以把下面这段当成“六个裁剪平面”的记忆卡片:
摄像机前面能看到的那一块空间,并不是无限大,也不是随便乱的形状,而是一个由六个平面围起来的视锥体:
- 左平面、右平面:决定视野的左右边界;
- 上平面、下平面:决定视野的上下边界;
- 近平面:离相机最近,从这个平面以后才算“开始可见”;
- 远平面:最远能看到哪一堵“隐形墙”,后面就算存在也不渲染。
这六个平面起源于:
- 摄像机的视野角(FOV)、宽高比(aspect);
- 和你设置的 near / far 距离;
- 投影矩阵会把这六个平面编码进数学里,最终在 Clip / NDC 空间里变成一个规规矩矩的“标准盒子”。
GPU 在进行裁剪时,本质上就是:
- 针对每个顶点/三角形,检查它们是否全部落在六个平面的内侧;
- 全在外:丢弃;
- 全在内:保留;
- 一部分在内一部分在外:沿着平面边缘算交点,把三角形切一刀,只留下里面那一块。
在 NDC 空间中,这六个平面变得超简单:
x = -1 / +1→ 左右平面;y = -1 / +1→ 上下平面;z = -1/0 / +1→ 近、远平面;- 判断“在不在视野里”就变成了简单的范围判断:
-1 <= x,y,z <= 1(或 z ∈[0,1])。
在自己写游戏逻辑时,你也可以显式使用“六个平面”的视锥体来做:
- CPU 端的视锥裁剪(Frustum Culling);
- 自定义裁剪效果(镜子、水面、传送门等)。
如果把它特别俗地再说一遍:
六个裁剪平面,就是摄像机前面那一块“无形的玻璃盒子”的六个壁。
盒子里面的世界 GPU 才去管,
盒子外面的一概不理。你告诉引擎“我视野多大、最近多近、最远多远”,
引擎就帮你把这个玻璃盒子算出来,
GPU 就靠它来决定——
“哪些东西该出现在屏幕上,哪些东西压根就不要算”。