页面

顶点、三角形与插值:三个离散点怎样铺出连续表面

← 返回兔子洞总览 · rendering 主题地图

顶点不是只有位置的点,而是一整包离散属性;三角形确定屏幕覆盖范围,光栅化再用重心坐标把三个顶点的 UV、颜色、深度等属性连续地传给内部片元。

我追问的链

  • GPU 说的"顶点"只是一个坐标吗?
  • 三个顶点为什么刚好能表示一块稳定表面?
  • 一个复杂模型怎样由大量三角形组成?
  • 相邻三角形为什么要共享顶点?索引缓冲解决了什么?
  • GPU 怎样判断一个屏幕采样点是否位于三角形内部?
  • 三角形内部没有提前存 UV 和颜色,片元着色器从哪里得到它们?
  • 重心坐标到底表达了什么?
  • 为什么普通线性插值会让透视场景中的纹理变形?
  • 绕序、正面、背面剔除分别是什么?
  • 为什么模型只有 8 个几何角点,渲染时却可能需要 24 个顶点?

1. 顶点不是一个点,而是一条完整记录

日常说"顶点"时,很容易只想到一个位置:

1
position = (x, y, z)

但在渲染管线里,一个顶点通常是一整包属性:

1
2
3
4
5
6
7
8
Vertex
├─ position:位置
├─ color:颜色
├─ uv:纹理坐标
├─ normal:法线
├─ tangent:切线
├─ boneIndices:受哪些骨骼影响
└─ boneWeights:每根骨骼影响多少

不同渲染任务需要的属性不同:

  • 纯色 2D 图形可能只需要位置和颜色。
  • 图片矩形通常需要位置、UV 和颜色。
  • 受光照的 3D 模型通常还需要法线、切线。
  • 骨骼动画模型还需要骨骼索引和权重。

所以更准确的理解是:

顶点是一组在几何关键位置上提供的离散输入数据。

它只是几何表面上的少量样本,不会为三角形内部每个位置都提前保存一份属性。

2. 三角形把离散顶点连接成表面

只有顶点时,GPU 得到的只是一组孤立数据:

1
2
3
4
5
A •

• B

• C

还必须告诉 GPU 怎样连接它们。常见的图元类型包括:

1
2
3
点 Point
线 Line
三角形 Triangle

实时图形最常用三角形,因为三个不共线的点一定确定一个平面:

1
2
3
4
5
    A
/ \
/ \
/ \
B───────C

四边形却不一定稳定。如果四个点不在同一平面上,"四边形内部"该怎样弯曲并没有唯一答案:

1
2
3
4
5
A────B
│ \
│ C ← 可能偏离 A、B、D 所在平面
│ /
D───┘

把它拆成三角形后,每一块的平面和内部插值方式就确定了:

1
2
3
4
A────B
│ / │
│ / │
D────C

拆分方式可能是:

1
2
三角形 ABD
三角形 BCD

因此,圆形、角色、地形和复杂模型最终都可以近似为许多小三角形:

1
2
3
4
5
复杂曲面
↓ 切分
大量小三角形
↓ 光栅化
屏幕上的片元

三角形越密,通常越能近似复杂轮廓和曲面,但也会增加几何数据和处理工作。

3. Mesh 是什么

一组能共同描述形状的顶点与连接关系,通常称为网格(Mesh)。

最基础的 Mesh 可以先理解为:

1
2
3
4
Mesh
├─ Vertex Buffer:每个顶点有哪些属性
├─ Index Buffer:顶点按什么顺序组成三角形
└─ Primitive Topology:按点、线还是三角形解释索引

真实引擎中的 Mesh 还可能包含:

1
2
3
4
5
6
包围盒
子网格
材质分组
骨骼绑定信息
多套 UV
压缩格式

这些是为了剔除、分批绘制、动画和资源管理补出的上层结构。几何地基仍然是:

1
顶点属性 + 连接关系

4. Vertex Buffer 怎样保存顶点

顶点数据通常集中放在顶点缓冲(Vertex Buffer)中。

假设每个顶点包含:

1
2
3
position:3 个 float
color:4 个 float
uv:2 个 float

一个顶点可以排列成:

1
[px, py, pz, r, g, b, a, u, v]

多个顶点连续存放:

1
2
3
4
5
6
7
8
Vertex 0
[px, py, pz, r, g, b, a, u, v]

Vertex 1
[px, py, pz, r, g, b, a, u, v]

Vertex 2
[px, py, pz, r, g, b, a, u, v]

图形 API 还需要知道怎样解释这段字节:

1
2
3
4
position 从第 0 byte 开始,读取 3 个 float
color 从第 12 byte 开始,读取 4 个 float
uv 从第 28 byte 开始,读取 2 个 float
下一个顶点距离当前顶点 36 bytes

这里"下一个顶点距离当前顶点多少字节"通常叫 stride

所以 Vertex Buffer 只是连续字节;是配套的顶点布局告诉 GPU:

哪几段字节分别代表位置、颜色、UV 和其他属性。

顶点属性可以交错存储,也可以拆到多个独立缓冲。两种方式各有数据更新和访问上的权衡,先不用把某一种当成唯一结构。

5. 为什么还需要 Index Buffer

考虑两个相邻三角形组成的矩形:

1
2
3
4
5
0 左上 ───── 1 右上
│ / │
│ / │
│ / │
2 左下 ───── 3 右下

如果完全不使用索引,需要按三角形重复写六份顶点记录:

1
2
第一个三角形:0, 2, 1
第二个三角形:1, 2, 3

顶点 12 被重复保存。

使用索引缓冲(Index Buffer)后,可以只保存四份顶点,再用整数引用它们:

1
2
vertices = [0, 1, 2, 3]
indices = [0, 2, 1, 1, 2, 3]

索引缓冲的价值包括:

  • 减少相邻三角形重复保存完全相同的顶点属性。
  • 让多个三角形明确共享同一份顶点记录。
  • 有机会利用 GPU 的顶点处理缓存,减少重复变换。

但"空间中位置相同"不代表一定能共享渲染顶点。只有整包顶点属性都兼容时,才可以共用同一条记录。

例如同一个几何位置,在立方体不同面上可能需要不同法线:

1
2
3
右面法线 = (1, 0, 0)
上面法线 = (0, 1, 0)
前面法线 = (0, 0, 1)

虽然三条记录的位置相同,法线不同,所以必须拆成不同渲染顶点。

UV 接缝也一样。一个位置如果在纹理左边界需要 u = 0,在另一块纹理区域又需要 u = 1,就不能用同一条顶点记录同时表达两个 UV。

这解释了一个常见现象:

模型的几何角点数,不一定等于提交给 GPU 的顶点数。

例如立方体只有 8 个空间角点,但为了给六个面配置独立法线和 UV,常见渲染数据可能需要 24 个顶点记录。

6. 绕序怎样定义三角形的正面

把三个顶点按顺序连接,会形成一个方向:

1
A → B → C

从屏幕这一侧看,它可能是:

1
顺时针 Clockwise,CW

也可能是:

1
逆时针 Counter-clockwise,CCW

这个方向叫绕序(Winding Order)。

图形管线会根据配置,把其中一种绕序认作正面,另一种认作背面:

1
2
Front Face:正面
Back Face:背面

具体默认哪一种与图形 API、坐标约定和当前渲染状态有关,不能脱离上下文死记"顺时针一定是正面"。

背面剔除

一个封闭物体朝外的三角形,背面通常看不到。GPU 可以在进入昂贵片元计算前丢掉这些三角形:

1
2
3
背面三角形
↓ Back-face Culling
不继续光栅化

这叫背面剔除

它对封闭 3D 物体通常有价值,但不是所有对象都适合:

  • 双面纸片需要看到正反两侧。
  • 植物叶片常用薄片模拟。
  • 某些透明或特效几何需要双面渲染。
  • 2D UI 可能直接关闭面剔除。

如果三角形绕序意外翻转,又开启了背面剔除,表面可能从某一侧完全消失。

7. GPU 怎样判断一个采样点在三角形内部

顶点着色器处理完位置以后,图元装配会把顶点组成三角形。接下来光栅化器要回答:

哪些屏幕采样点被这个三角形覆盖?

可以先把三角形的三条边看成三条有方向的直线:

1
2
3
4
5
    A
/ \
/ P \
/ \
B───────C

对点 P,分别检查它位于边 ABBCCA 的哪一侧。如果它对三条边都处在一致的内部方向,就在三角形内。

二维中可以用类似叉积符号的边函数表达某个点在边的哪一侧。以边 AB 为例:

1
2
3
E_AB(P)
= (P.x - A.x) × (B.y - A.y)
- (P.y - A.y) × (B.x - A.x)

结果的正负表示 P 位于有向边的一侧还是另一侧;到底哪一侧是内部,取决于三角形绕序和公式约定。

对三条边都进行判断:

1
2
3
E_AB(P)
E_BC(P)
E_CA(P)

符号满足内部规则,说明这个采样点被覆盖;否则不属于该三角形。

真实 GPU 会并行、高效地完成覆盖测试,还会用统一的边界归属规则避免两个相邻三角形在公共边上既产生裂缝,又重复覆盖同一采样点。这里先抓住原理:

光栅化不是把三角形当成图片缩放,而是在屏幕采样位置上做几何覆盖判断。

8. 覆盖范围有了,内部属性从哪里来

假设三个顶点带有不同颜色:

1
2
3
A:红色
B:绿色
C:蓝色

三角形内部并没有提前保存每个片元的颜色,但最终会出现连续渐变:

1
2
3
4
5

/ \
/ 混 \
/ 合 \
绿──────蓝

GPU 需要根据片元位置,计算它更接近哪个顶点,然后混合三个顶点的属性。

这个过程叫插值(Interpolation)。

不仅颜色可以插值,很多顶点着色器输出都会交给光栅化器插值:

1
2
3
4
5
6
颜色
UV
深度相关值
世界位置
法线
自定义 Shader 数据

所以顶点着色器不需要为每个片元运行。它只为顶点计算输出,光栅化阶段再为三角形内部产生连续值,片元着色器接收插值结果。

9. 重心坐标表达"更靠近谁"

三角形内任意一点 P,都可以表示为三个顶点的加权组合:

1
P = λA × A + λB × B + λC × C

并且:

1
λA + λB + λC = 1

这三个权重就是 P 相对于三角形的重心坐标(Barycentric Coordinates)。

如果:

1
2
3
λA = 1
λB = 0
λC = 0

那么 P 就是顶点 A

如果:

1
2
3
λA = 0.5
λB = 0.5
λC = 0

那么 P 在边 AB 的中点。

如果:

1
λA = λB = λC = 1/3

那么 P 位于三个顶点平均的位置。

三角形内部的点通常满足:

1
2
3
4
λA ≥ 0
λB ≥ 0
λC ≥ 0
λA + λB + λC = 1

一旦得到这三个权重,就可以用同样方式插值其他属性。

例如顶点颜色:

1
2
3
4
color(P)
= λA × color(A)
+ λB × color(B)
+ λC × color(C)

顶点 UV:

1
2
3
4
uv(P)
= λA × uv(A)
+ λB × uv(B)
+ λC × uv(C)

因此,重心坐标不是只为位置服务,而是一套把三个顶点属性传播到三角形内部的通用权重。

10. 用一个具体数值理解插值

假设三个顶点颜色分别是:

1
2
3
A = 红色 (1, 0, 0)
B = 绿色 (0, 1, 0)
C = 蓝色 (0, 0, 1)

某个片元的重心坐标是:

1
2
3
λA = 0.5
λB = 0.25
λC = 0.25

那么片元颜色为:

1
2
3
4
5
6
color(P)
= 0.5 × (1, 0, 0)
+ 0.25 × (0, 1, 0)
+ 0.25 × (0, 0, 1)

= (0.5, 0.25, 0.25)

因为红色顶点的权重最大,所以结果偏红。

UV 也是同一逻辑。假设:

1
2
3
uv(A) = (0, 1)
uv(B) = (0, 0)
uv(C) = (1, 0)

那么:

1
2
3
4
5
6
uv(P)
= 0.5 × (0, 1)
+ 0.25 × (0, 0)
+ 0.25 × (1, 0)

= (0.25, 0.5)

片元着色器随后会到纹理的 (0.25, 0.5) 附近采样颜色。

这就连接了两章:

1
2
3
4
5
6
7
顶点保存少量 UV

光栅化器为片元插值 UV

片元着色器根据 UV 采样纹理

片元影响帧缓冲像素

11. 插值后的法线为什么经常要重新归一化

假设两个顶点法线长度都是 1:

1
2
N0 = (1, 0, 0)
N1 = (0, 1, 0)

在它们之间取一半:

1
2
N = 0.5 × N0 + 0.5 × N1
= (0.5, 0.5, 0)

它的长度是:

1
2
3
sqrt(0.5² + 0.5²)
= sqrt(0.5)
≈ 0.707

已经不再是单位向量。

因此,用插值法线参与光照前,片元着色器通常需要:

1
normal = normalize(normal);

这说明:

插值保证属性连续变化,但不会自动维护属性自身的额外约束。

颜色没有"长度必须等于 1"的要求,法线却通常有,所以它们虽然都能插值,后续处理不同。

12. 为什么普通屏幕线性插值会让纹理变形

到这里还有一个隐藏问题。

在没有透视的 2D 平面中,直接按屏幕距离线性插值 UV 通常符合直觉。但在透视投影下,远处看起来更小,几何在投影到屏幕前经历了透视除法:

1
2
3
4
5
6
7
三维位置
↓ 投影
裁剪空间 (x, y, z, w)
↓ 透视除法
(x/w, y/w, z/w)

屏幕位置

这个除以 w 的过程是非线性的。原本在三维表面上均匀变化的 UV,投影到屏幕后不再能直接按屏幕距离做普通线性插值。

如果仍然这样做,纹理会像在三角形内部滑动、弯曲或折断,这类错误常被称为仿射纹理扭曲

GPU 通常会进行透视校正插值。可以把核心公式理解为:

1
2
3
attribute(P)
= [Σ λi × (attribute_i / wi)]
/ [Σ λi × (1 / wi)]

其中:

  • λi 是屏幕空间算出的重心权重。
  • attribute_i 是顶点属性,例如 UV。
  • wi 是顶点进入透视除法前的裁剪空间 w

直观上,GPU 不是直接插值 UV,而是先考虑每个顶点的透视尺度,再恢复表面上正确的变化。

因此要分清:

1
2
3
4
5
重心坐标
→ 告诉我们片元在投影后三角形中的相对位置

透视校正
→ 修正透视除法造成的非线性,让属性沿原始表面正确变化

现代图形管线通常默认对需要平滑传递的属性做透视校正,但 Shader 语言也可能允许选择其他插值方式。

13. 属性不一定都要平滑插值

不同属性可能需要不同传递规则。

平滑、透视校正插值

适合:

1
2
3
4
UV
颜色渐变
世界位置
法线

它们通常希望在三角形内部连续变化。

不做透视校正的屏幕线性插值

某些屏幕空间效果希望属性只按屏幕位置线性变化,可以显式选择不做透视校正。

Flat:整块三角形使用同一个值

有些数据不能取平均,例如:

1
2
3
材质分类编号
面 ID
离散状态

这时可以让整个三角形使用某个顶点提供的同一个值,而不是插值出 1.37 这样的无意义结果。

所以"顶点输出会被插值"是默认主线,不是所有数据无条件遵循的唯一规则。

14. 为什么相邻三角形通常不会出现裂缝

两个相邻三角形如果共享同一条边:

1
2
3
4
A────B
│ / │
│ / │
D────C

理想结果是:

  • 公共边上的采样点至少归其中一个三角形。
  • 不因为浮点误差露出背景裂缝。
  • 也尽量避免公共边被两个三角形重复算两次。

光栅化器会使用一致的边界归属规则,例如常被概括为 top-left rule,让共享边的两个三角形对边界采样点得到互补决定。

这里的重要认识不是背规则名字,而是:

相邻三角形能无缝拼接,不只是因为顶点坐标相同,还因为光栅化阶段对公共边采用统一约定。

如果两边的顶点位置本身不一致,或者变换后产生不同结果,统一边界规则也无法修复真实几何裂缝。

15. 退化三角形是什么

如果三个顶点共线,或者两个顶点重合:

1
A────B────C

它们围成的面积为 0,这叫退化三角形(Degenerate Triangle)。

退化三角形通常不会覆盖正常面积的屏幕采样点,因此不会产生可见表面。但它仍可能:

  • 占用索引数据。
  • 经过部分顶点处理。
  • 在某些拓扑组织方式中被用来连接条带。
  • 因错误数据导致模型局部消失。

当三角形非常小但还没有完全退化时,也可能只覆盖少量甚至没有覆盖采样点。大量亚像素级小三角形会让 GPU 做了很多几何工作,却产生很少有效像素。

16. 顶点多和片元多是两种不同压力

现在可以进一步区分渲染成本:

几何压力

主要与这些因素相关:

1
2
3
4
5
6
7
顶点数量
三角形数量
顶点属性宽度
顶点着色器复杂度
骨骼蒙皮计算
顶点缓存复用情况
大量过小三角形

片元压力

主要与这些因素相关:

1
2
3
4
5
6
三角形在屏幕上覆盖多少面积
同一位置被覆盖多少次
片元着色器复杂度
纹理采样次数
透明混合
分辨率

同样是一个三角形:

1
2
3
4
5
占满整个屏幕
→ 顶点只有 3 个,但可能产生数百万片元

缩成一个很小的远处碎片
→ 仍要处理顶点,却可能几乎没有有效片元

所以不能只用"三角形数量"判断所有渲染性能,也不能只用"屏幕面积"判断几何成本。

17. 把常见现象放回这一层

现象 优先追问的方向
模型缺一块面 索引、绕序、背面剔除、退化三角形
硬边变成奇怪的平滑面 共享顶点的法线是否本应拆开
UV 在接缝处拉穿 UV 接缝是否错误共享顶点
纹理沿透视表面扭曲 是否缺少透视校正插值
光照明暗异常 插值法线是否重新归一化
顶点数比建模角点多 法线、UV、颜色等属性是否要求拆点
面数下降但性能变化小 当前瓶颈可能不在几何阶段
远处小三角形很多 几何处理多,但有效片元利用率低

这张表继续使用同一种诊断方法:

1
2
3
先区分位置、连接关系、覆盖测试和属性插值

再检查是哪一份数据或哪一个状态不符合预期

逻辑闭环 / 锚点

这一课可以收束成:

1
2
3
4
5
6
7
8
9
10
11
12
13
Vertex Buffer 提供离散顶点属性
+
Index Buffer 提供连接顺序

图元装配得到有绕序的三角形

光栅化判断哪些屏幕采样点被覆盖

重心坐标描述片元相对三个顶点的位置

透视校正插值得到片元的 UV、颜色、法线等输入

片元着色器继续计算候选颜色

几个不能混淆的锚点:

  1. 顶点不是只有位置:位置相同但 UV 或法线不同,仍然是不同渲染顶点。
  2. 索引不产生属性:它只是引用顶点并规定怎样组成图元。
  3. 覆盖与插值是两件事:先判断片元是否属于三角形,再计算它应得到哪些属性。
  4. 重心坐标是权重:它统一描述位置、颜色、UV 等属性怎样由三个顶点混合而来。
  5. 屏幕线性不等于表面线性:透视除法以后,需要透视校正才能让 UV 等属性沿三维表面正确变化。
  6. 三角形数不是全部性能:几何工作量和片元工作量必须分开观察。

下一篇要继续追的是:

顶点的位置写着 (x, y, z),它到底属于哪个坐标系?物体移动、相机观察和透视投影,又怎样一步步把它变成屏幕位置?

这会把我们带到向量、局部空间、世界空间、观察空间、裁剪空间、NDC、矩阵与齐次坐标。

关联

  • 从场景到像素 直接衔接:第一章说光栅化产生片元,这一章补上了覆盖判断和属性插值的中间过程。
  • 分层 + 封装 是同一个母题:顶点缓冲负责属性,索引缓冲负责连接,光栅化器负责覆盖和插值,职责不同但首尾咬合。

来源:与 Codex 的对话,2026-07。