news 2026/2/10 12:17:06

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GPU渲染:顶点如何连成三角形并裁剪

先把一句话摆在最前面:

顶点阶段只是把“点”算好了位置,
图元装配 & 裁剪这一步,干的事情就是:
把这些点按索引连成三角形,然后把跑出摄像机视野外的那部分三角形切掉或扔掉。

你可以把它想象成:

  • 顶点阶段:一个个灯泡都拧好了,位置对了;
  • 图元装配:根据电路图,把这些灯泡连成一串串“灯带”(三角形);
  • 裁剪:把掉到舞台外、遮挡掉的那些灯带剪掉不要,再交给后面去点亮(光栅化、着色)。

这一章节我们专门讲这两步:

  1. 图元装配(Primitive Assembly)
    顶点 → 点/线/三角形;重点讲三角形怎么从“顶点 + 索引”装配起来。

  2. 裁剪(Clipping)
    哪些三角形完全看不到直接扔掉;
    哪些三角形一半在视野里一半在外面,要在边界上“切一刀”,只留里面那半。

目标是:用大白话 + 足够细节,让你脑海里能出现一个清晰的动画:

“顶点变完位置之后,GPU 是怎么把它们连成一个个三角形,再在视锥体的边界上剪来剪去的。”


一、先回顾一下“流水线”:图元装配 & 裁剪在第几道工序?

把整条渲染流水线简单复习一遍(只看传统光栅化管线):

  1. CPU 阶段

    • 准备顶点数据(位置、法线、UV、索引……);
    • 设置各种状态(Shader、纹理、RenderTarget 等);
    • 调用 DrawXxx:告诉 GPU,“把这堆数据画出来”。
  2. 顶点阶段(Vertex Shader)

    • 对每个顶点:
      local → world → view → clip,算出裁剪空间坐标(x, y, z, w)
    • 顺带把法线、UV 等也处理一下,传下去。
  3. 图元装配(Primitive Assembly)← 我们要讲的第一个主角

    • 按照指定方式(点列表/线列表/三角列表/三角条带……),
      把一个个顶点“组装”成一个个图元(通常是三角形)。
  4. 裁剪(Clipping)← 第二个主角

    • 判断三角形是否在可见体(视锥 / 裁剪盒子 / NDC 盒子)里;
    • 完全在外:扔掉;
    • 部分在内:沿裁剪平面把三角形切开,保留可见部分。
  5. 透视除法 & 视口变换(Perspective Divide & Viewport Transform)

    • ndc = clip / w,得到 [-1,1] 立方体坐标;
    • 映射到具体屏幕像素坐标。
  6. 光栅化(Rasterization)

    • 把每个三角形变成一堆像素(片元),准备逐像素着色。
  7. 片元着色器(Fragment / Pixel Shader)

    • 对每个像素算颜色、光照、贴图、阴影。
  8. 测试 & 混合(Depth/Stencil/Blend)

    • 深度测试、模板测试、颜色混合(透明等),写入最终帧缓冲。

所以:

图元装配 & 裁剪
正好夹在“顶点变换”和“光栅化”之间,
起到“从点到面,并剪掉不需要的面”的承上启下作用。


二、图元装配:把点连成三角形这一步到底是怎么做的?

2.1 顶点只是“散点”,要靠“连接规则”变成几何形状

CPU 提供给 GPU 的数据主要有两块:

  1. 顶点缓冲(Vertex Buffer):

    • 一条数组:[v0, v1, v2, v3, v4, v5, ...]
    • 每个 v 里有 position、normal、uv、color 等。
  2. 索引缓冲(Index Buffer):

    • 另一条数组:[i0, i1, i2, i3, i4, i5, ...]
    • 每三个索引组成一个三角形:
      • (i0, i1, i2)→ 三角形 0
      • (i3, i4, i5)→ 三角形 1

GPU 在图元装配阶段要做的就是:

按照你指定的“图元类型(primitive type)”,
用索引把顶点排排坐,拼成一个个图元。

2.2 常见几种图元类型:点 / 线 / 三角形

最常见的是三种:

  1. 点列表(Point List)

    • 每个顶点独立画一个点;
    • 用得不多,更多是调试或点云渲染。
  2. 线列表 / 线条(Line List / Line Strip)

    • 每两个顶点画一条线;
    • 或者头尾相接画连续的折线。
  3. 三角形列表 / 条带(Triangle List / Triangle Strip)

    • 三角形列表:每 3 个顶点一个三角形;
    • 三角形条带:复用前两个顶点,每增一个点多出一个三角形。

游戏里绝大多数可见物体,都是用三角形表示的。

所以我们重点说三角形。

2.3 三角形列表(Triangle List):最常用、最直观

假设:

  • 顶点数组:vertices = [v0, v1, v2, v3, v4, v5, ...]
  • 索引数组:indices = [0, 1, 2, 2, 3, 0, ...]
  • 绘制模式:TRIANGLES(三角形列表)。

图元装配的逻辑大概是:

for(k = 0; k < indices.length; k += 3): i0 = indices[k] i1 = indices[k+1] i2 = indices[k+2] 三角形 T = (vertices[i0], vertices[i1], vertices[i2])

这样:

  • (0,1,2) → 第一个三角形;
  • (2,3,0) → 第二个三角形;
  • ……

每个顶点已经在顶点着色器里被变换到了 Clip Space(有 x,y,z,w),
图元装配时,只是把它们按索引分组。

2.4 三角形条带(Triangle Strip):节省顶点数据的一种方式

为了节省带宽,有时候会用 Triangle Strip:

  • 顶点顺序:v0, v1, v2, v3, v4, v5...

  • 构造方式:

    • 三角形 0:(v0, v1, v2)
    • 三角形 1:(v1, v2, v3)
    • 三角形 2:(v2, v3, v4)

优点:

  • 共边的三角形能共享两个顶点,每增加一个顶点就多一个三角形;
  • 对一些规则网格来说很适合。

缺点:

  • 拓扑受限制;
  • 现在很多引擎直接用 Triangle List 了,简单粗暴又直观。

你理解了 List,再知道 Strip 是“共享前两个”的变种就够了。

2.5 顶点阶段和图元装配的边界:谁干啥?

  • 顶点阶段:对每一个顶点独立计算它的位置(和携带的其他数据);
  • 图元装配:按指定方式把这些独立的顶点组装成图元(线/三角形)。

想象一下:

  • 顶点阶段像是“给每个演员排好站位”;
  • 图元装配就是“导演拿着剧本,说你们三个组成一组跳这个舞、你们三个组下一个”。

三、裁剪(Clipping):把视野外的三角形剪掉或切一刀

装好三角形之后,并不是全都要画。
有些完全在视野外,有些只露出一点点。

裁剪阶段就是:

根据一个“标准裁剪体”(Clip Volume),
把“三角形 + 视野”的交集算出来:

  • 完全在外:扔掉;
  • 完全在内:原样保留;
  • 部分交叉:在边界上切开,生成新的三角形(或多边形),只保留里面的部分。

3.1 裁剪空间里的“标准盒子”

前面顶点阶段,我们把点变到了Clip Space,然后 GPU 做透视除法得到 NDC(Normalized Device Coordinates):

  • NDC 中:
    • x、y、z大概都在 [-1,1] 范围内的点是可见的;
    • 超出这个范围的就是视野外。

裁剪的时候,有两种实现方式:

  • 在 Clip Space 下直接裁剪:
    • 使用约束:-w <= x <= w-w <= y <= w,z 同理;
  • 或在透视除法后,在 NDC 下裁剪:
    • 使用:-1 <= x <= 1-1 <= y <= 1,z 范围等。

具体取决于 GPU 内部实现细节;
对我们而言,可以简单理解为:

有一个“长方体视野盒子”,超出这盒子外的三角形都要处理一下。

3.2 裁剪的六个平面:左、右、上、下、近、远

无论是视锥还是标准裁剪盒子,最终都会变成六个裁剪平面

  • 左平面;
  • 右平面;
  • 上平面;
  • 下平面;
  • 近裁剪面(near plane);
  • 远裁剪面(far plane)。

裁剪的逻辑就是对每个三角形依次判断它对这几个平面的关系。

简单来说,对于一个平面:

  • 三角形三个顶点都在平面的“内侧”:
    • 这个三角形对这个平面来说是“完全在内”的;
  • 三个都在“外侧”:
    • 对这个平面来说“完全在外”,可以整个扔掉;
  • 有的在内、有的在外:
    • 需要在平面和三角形的边的交点处切开。

对所有平面都处理完,剩下的就是实际会被绘制的部分。

3.3 完全在外:扔掉就完了(剪掉整个三角形)

最简单的情况:

  • 比如一个三角形完全在相机视野的左边之外;
  • 对“左裁剪平面”来说,它三个点的 x 坐标全都 < -w(Clip Space)或者 < -1(NDC);
  • 这种直接丢弃:

    这个三角形连光栅化都不需要,不会参与后面任何计算。

这就是 GPU 为什么能很高效:
很多你以为“存在的”三角形,其实根本不会走到像素阶段。

3.4 完全在内:原样送去下一步

如果三个顶点都在所有裁剪平面的“内侧”,那这个三角形:

  • 不需要裁剪;
  • 直接交给后续透视除法 + 光栅化;

也就是:完整三角形原样保留

3.5 部分在内、部分在外:要在边界上“切一刀”

最麻烦的是中间情况:

  • 有一个或两个顶点在视野内,剩下的在外;
  • 三角形一部分在可见空间,有一部分伸出去了。

解决方案:

把“在内”的部分保留,把“在外”的部分切掉,
在裁剪平面和三角形边的交点处,创建新的顶点。

这样,原来的一个三角形,会变成:

  • 一个新的三角形,或者
  • 两个新的三角形(具体看哪几条边和裁剪平面相交)。

下面我们用直观例子仔细讲。


四、详细拆解“裁剪三角形”的过程(带具体情况)

为了好讲,我们只考虑在某一个平面上的裁剪(比如左平面),
然后理解 GPU 是怎么“切三角形”的。

记号:

  • I:inside,顶点在平面内侧(可见空间内);
  • O:outside,在平面外侧。

三角形有三个点,可能有这些情况:

  1. I I I → 全在内:保留,不切;
  2. O O O → 全在外:扔掉;
  3. I I O → 两个内一个外;
  4. I O O → 一个内两个外;
  5. O I O → 仍旧是“一内两外”的组合,只是顺序不同;
  6. 还有其他组合,但本质就是“一个内两个外”或“两个内一个外”。

我们分别说 3 和 4。

4.1 情况一:两内一外(I I O) → 三角形变成两个小三角形

假设顶点是:

  • v0:内;
  • v1:内;
  • v2:外。

三角形 (v0, v1, v2)。

边有三条:

  • v0-v1:全在内,无需切;
  • v1-v2:穿过平面;
  • v2-v0:穿过平面。

在两个穿过平面的边上,会产生两个交点:

  • i1:v1-v2 与平面的交点;
  • i2:v2-v0 与平面的交点。

原三角形的一部分被切掉,剩下的那部分几何形状其实是一个“凸四边形”,
GPU 会把它再拆为两个三角形:

  • T1 = (v0, v1, i1)
  • T2 = (v0, i1, i2)

这样:

  • 原来的三角形变成了两个新的三角形;
  • 而这两个三角形都完全在平面内侧(可见空间内)。

4.2 情况二:一内两外(I O O) → 三角形变成一个小三角形

假设:

  • v0:内;
  • v1:外;
  • v2:外。

三条边:

  • v0-v1:穿过平面;
  • v0-v2:穿过平面;
  • v1-v2:全在外,无需理会。

两个穿过平面的边上产生两个交点:

  • i1:v0-v1 与平面的交点;
  • i2:v0-v2 与平面的交点。

可见的那部分是一个小三角形:

  • T = (v0, i1, i2)

所以:

  • 原来的大三角形被平面“切掉大部分”,只剩下面向内侧的小三角形 T。

4.3 交点坐标怎么算?——线性插值

三角形的边是线段,比如边 v1-v2:

  • v1 在内,v2 在外或相反;
  • 平面可以表示为一个方程:ax + by + cz + d = 0(或 N·p + d = 0)。

我们可以用参数 t 描述从 v1 向 v2 的点:

P(t) = v1 + t * (v2 - v1), t ∈ [0,1]

把 P(t) 代入平面方程,解出 t,就得到交点的位置。

但在实际硬件实现中,Clip Space / NDC 的裁剪范围比较规整,
可以用更简单的方式算 t —— 比如对某一维(x 或 y 或 z)做插值。

举个简单例子:
假设我们在 NDC 中对 x = 1 的平面裁剪(右平面),
v1.x = 0.5(内侧),v2.x = 2.0(外侧):

我们想要 P.x = 1 P = v1 + t * (v2 - v1) P.x = v1.x + t * (v2.x - v1.x) = 0.5 + t * (2.0 - 0.5) = 1 → t = (1 - 0.5) / (2.0 - 0.5) = 0.5 / 1.5 = 1/3

于是:

交点 P = v1 + (1/3) * (v2 - v1)

同理,顶点的其他属性(UV、颜色、法线等)也要用同样的 t 做插值,
这样裁剪后的顶点仍然有合理的纹理和光照信息。

裁剪 = 在视野盒子的边上,沿着边线找到“过界点”,
然后用线性插值算出新的顶点(包括 position、UV、color 等)。


五、裁剪的结果:三角形数量会变化,顶点数量也会增加

裁剪的一个直接影响:三角形数量可能变多,顶点数量会“临时增加”

举例:

  • 原来只有一个三角形 I I O 的情况,被裁剪成两个三角形;
  • 裁剪过程中产生的新顶点不会回写到原始顶点缓冲,而是作为流水线中的临时数据,继续往下传。

这一步一般都在 GPU 的固定功能(或高度优化的模块)里完成,
对我们编程的人来说:

你看不到“裁剪后的新索引缓冲”,
但可以知道:
“经过裁剪以后,被送入光栅化阶段的三角形,都是完整在视野内的”。


六、裁剪存在的意义:省算力、避免乱画、保证正确显示

为什么要费劲裁剪?直接全部三角形往下画不行吗?
从三个角度看它的重要性。

6.1 性能:提前扔掉看不到的三角形

如果不裁剪:

  • 所有三角形都送去光栅化 → 每个三角形都要算覆盖哪些像素 → 再跑片元 Shader;
  • 即使这些三角形完全在屏幕外,仍然浪费大量计算。

裁剪后:

  • 完全在外的,直接扔掉;
  • 部分在内的,只把在内的那一部分交给后面。

节省像素级计算,是高性能渲染的关键之一。

6.2 正确性:防止奇怪的投影或错误插值

当一个三角形大部分在视野外,一小部分在内,如果不正确裁剪:

  • 插值后的 UV/深度可能会出现畸形;
  • 最严重的可能导致将视野外的东西错误地绘制到屏幕上(artifact)。

裁剪时正确计算交点 & 插值,能保证:

  • 三角形的可见部分在边界上被“干净利落地切断”;
  • 没有超出视野的碎片跑进来捣乱。

6.3 为了后面透视除法和深度计算顺利进行

记得:

  • 在 Clip Space 中,我们用规则-w <= x <= w等作为裁剪标准;
  • 透视除法后,我们只希望 NDC 在 [-1,1] 的立方体里,
    后面的视口映射和深度测试都建立在这个基础上。

如果不在 Clip/NDC 阶段裁剪,把非法的点交给后面:

  • 透视除法可能得到无限大(比如 w→0);
  • 视口映射可能把东西打到屏幕远外的某个鬼位置;
  • 深度缓冲也会被写入不可预期的值。

裁剪阶段的存在,保证了:

“进入光栅化阶段的所有三角形,都处在一个合理的合法空间范围内。”


七、和背面剔除(Backface Culling)怎么区分?

裁剪(Clipping)和剔除(Culling)容易混淆,简单区分一下:

7.1 剔除(Culling):不看背面就别画

背面剔除是在图元装配后、光栅化前的另一道操作:

  • 根据三角形顶点在屏幕 / NDC 中的顺序(顺时针/逆时针),判断它是面向相机还是背对相机;
  • 默认只画“正面”,背面直接丢掉;
    • 减少绘制负担;
    • 同时符合“物体内部看不到”的直觉。

不修改三角形形状,只决定画不画整个三角形

7.2 裁剪(Clipping):针对视野边界“几何切割”

  • 裁剪会根据视锥/裁剪盒的边界平面,切割三角形
  • 可能把一个三角形拆成零个(全外)、一个(一内两外)、两个(两内一外);
  • 修改的是几何形状本身。

可以这么记:

剔除:判断“要不要画这个三角形”;
裁剪:判断“这个三角形在视野的哪部分要画、哪部分不要”。


八、从开发者的视角:你能“控制”或“感知”这一阶段的什么?

虽然图元装配 & 裁剪是 GPU 内部的“固定功能阶段”,
但我们可以从几个方向理解或影响它。

8.1 通过 DrawCall 的模式影响图元装配

比如在 OpenGL 中:

glDrawElements(GL_TRIANGLES,...);// 三角形列表glDrawArrays(GL_TRIANGLE_STRIP,...);// 三角形条带glDrawArrays(GL_LINES,...);// 线glDrawArrays(GL_POINTS,...);// 点

在 DirectX / Vulkan 中也是类似,传入一个PrimitiveTopology(图元拓扑)。

你指定了拓扑,就等于告诉 GPU:

“图元装配阶段,请按照我给你的这种规则,把顶点连起来。”

8.2 控制裁剪平面(特别是 near / far)

Projection 矩阵参数(near, far, fov, aspect)会影响裁剪范围:

  • near 过大:离相机太近的东西被裁掉(可能造成“模型被截断”的效果);
  • near 过小:深度缓冲精度变差,Z-fighting 增多;
  • far 过小:远处的物体提前被裁掉(突然消失);
  • far 过大:同样影响深度精度。

注意:

near 不要设得太小,比如 0.001;
一般游戏中 near 用 0.1 / 0.3 / 1 之类比较合理。

8.3 使用裁剪平面做特殊效果(镜子、水面、门户)

有些高级渲染技巧会显式地设置裁剪平面,比如:

  • 渲染镜子反射,只想画镜子平面“另一侧”的世界;
  • 水面反射:只渲染水面以上或水面以下;
  • 传送门:定义一个自定义裁剪平面,只渲染通过门框看到的部分。

虽然细节实现会因 API 而异,但本质上就是:

给 GPU 增加一个(或多个)裁剪平面,让裁剪阶段帮我们“切掉不想画的空间”。


九、把整个“图元装配 & 裁剪”的过程缩成一条直观的“动画”

把这两个阶段串起来,你可以在脑海里想象这样一段动画:

  1. 顶点阶段结束时:

    • 空中漂浮着许多点,每个点都有一个 Clip Space 坐标 (x,y,z,w);
    • 它们各自带着法线、UV、颜色等信息。
  2. 图元装配:

    • GPU 拿着索引缓冲:
      “0,1,2 你们三组成一个三角形;
      2,3,0 你们也是;
      4,5,6 你们再来一组……”
    • 一堆三角形在“裁剪空间”里被连成网。
  3. 裁剪阶段:

    • 想象有一个透明的长方体(-w…w 或 -1…1 的盒子),
      所有三角形都漂在这个盒子里,有的完全在盒子外,有的穿过边界,有的完好地在盒子里面。
    • GPU 开始检查每个三角形:
      • 完全在外:整块扔掉;
      • 完全在内:保留;
      • 穿过盒子:在盒子边缘的地方“切一刀”,把伸出去那部分削掉,只保留盒子里面的多边形(再拆成三角形)。
  4. 最终结果:

    • 被送往光栅化阶段的三角形,全部都老老实实待在裁剪盒子范围内;
    • 每个三角形都精准地只覆盖它在屏幕上“该出现的那一块区域”。

十、最后,用几句话再把这两步刻进脑子里

  1. 图元装配(Primitive Assembly)

    • 把已经在 Clip Space 的顶点,按拓扑(TRIANGLES / STRIP 等)组装成图元;
    • 关键数据来源是:顶点缓冲 + 索引缓冲;
    • 输出的是:一个个三角形(以及线/点)。
  2. 裁剪(Clipping)

    • 针对每个三角形,检查它与“可见空间盒子”的关系:
      • 全在内:保留;
      • 全在外:丢弃;
      • 一部分在内一部分在外:在边界平面算交点,切出新的三角形。
    • 交点用线性插值计算,顶点的所有属性(position/UV/color 等)都一起插值。
  3. 为什么要有它们?

    • 图元装配:

      顶点阶段只算点,不知道谁跟谁是一组;
      图元装配帮你“连线成形”,把数学点变成几何面。

    • 裁剪:

      防止“视野外的面”浪费后续计算;
      保证进入光栅化的几何都在合法范围内;
      避免奇怪投影和深度错误。

  4. 你在代码里能看到什么?

    • 通过 DrawCall 设定 PrimitiveTopology(点/线/三角);
    • 通过 Projection(near/far/fov)间接影响裁剪范围;
    • 有时会用额外裁剪平面做高级效果(镜子、水面、传送门)。

如果用一句特别形象的话收尾:

顶点阶段是“点位算好了”,
图元装配是“把这些点连出三角形来”,
裁剪是“拿剪刀沿着视野盒子的边把跑出去的部分咔嚓掉”,
剩下的干干净净的三角形,才会被送去变成一堆像素,
最终在屏幕上组成你看到的那一帧画面。

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

21、嵌入式 Linux 存储及软件更新全解析

嵌入式 Linux 存储及软件更新全解析 1. 文件系统选择 在选择文件系统时,我们通常可将存储需求分为以下三类: - 永久读写数据:如运行时配置、网络参数、密码、数据日志和用户数据。 - 永久只读数据:像程序、库和恒定的配置文件,例如根文件系统。 - 易失性数据:例如临…

作者头像 李华
网站建设 2026/2/9 19:19:44

React Native Snap Carousel 实战指南:从零构建流畅轮播体验

React Native Snap Carousel 实战指南&#xff1a;从零构建流畅轮播体验 【免费下载链接】react-native-snap-carousel 项目地址: https://gitcode.com/gh_mirrors/rea/react-native-snap-carousel 在移动应用开发中&#xff0c;轮播组件是展示图片、内容推荐和产品展示…

作者头像 李华
网站建设 2026/2/4 21:01:21

AI智能体失控怎么办?构建异常监控系统的终极指南

AI智能体失控怎么办&#xff1f;构建异常监控系统的终极指南 【免费下载链接】awesome-ai-agents A list of AI autonomous agents 项目地址: https://gitcode.com/GitHub_Trending/aw/awesome-ai-agents 当你的AI智能体开始出现不可预测的行为时&#xff0c;你该怎么办…

作者头像 李华
网站建设 2026/2/8 14:28:37

终极方案:Dropzone.js实现高效团队文件协作的完整指南

终极方案&#xff1a;Dropzone.js实现高效团队文件协作的完整指南 【免费下载链接】dropzone 项目地址: https://gitcode.com/gh_mirrors/dro/dropzone 还在为团队协作时文件传输效率低下而烦恼吗&#xff1f;团队成员之间频繁的文件共享往往面临邮件附件过大、网盘链接…

作者头像 李华
网站建设 2026/2/9 17:49:46

还在为Revit族库发愁?2万+免费构件让BIM设计效率翻倍!

还在为寻找合适的Revit族库而烦恼吗&#xff1f;想要提升BIM高效设计却苦于资源匮乏&#xff1f;现在&#xff0c;我们为您带来了革命性的解决方案——一个包含2万专业构件的BIM资源包&#xff0c;搭配智能Revit插件&#xff0c;让您的建筑设计工作如虎添翼&#xff01; 【免费…

作者头像 李华
网站建设 2026/2/9 20:24:59

AppSmith完整指南:零基础打造企业级Web应用

AppSmith完整指南&#xff1a;零基础打造企业级Web应用 【免费下载链接】appsmith appsmithorg/appsmith: Appsmith 是一个开源的无代码开发平台&#xff0c;允许用户通过拖拽式界面构建企业级Web应用程序&#xff0c;无需编写任何后端代码&#xff0c;简化了软件开发流程。 …

作者头像 李华