内容工厂. / 演示空间 / 管线各阶段的数据处理 · 第 1 课 · 顶点怎么变成裁剪空间坐标 段 2 / 4
管线各阶段的数据处理 | 第 1 课 | 下一课:三角形怎么变成像素

顶点怎么变成裁剪空间坐标

读时 约 11 分钟 本课 4 段 每段 三件套 保真度 真机可证
课题骨架(本课题只声明一次)

GPU 干两件事:① 把顶点流处理成裁剪空间顶点;② 据此画像素。 本课整段都在 ① 里面。

01
顶点 → 裁剪空间顶点
02
据此画像素
本课四段的位置
1.1
顶点被读进来
1.2
搬进裁剪空间
1.3
剪掉看不见的
1.4
变成屏幕坐标
1.1

顶点是怎么被读进来的

真机可证

本节点在骨架 ① 的第 1 环——也是整条管线的入口。

在顶点被搬进裁剪空间之前,它得先存在。而顶点并不自动存在——它们只是躺在顶点缓冲里的一串字节, 等着输入装配按规则把它们捞出来。规则就两个数字:步长 stride 和起始偏移 offset。 本段只增加一样东西:取址规则。

件一 含代码的数据流示意图 · 固定功能阶段放的是取址规则
左边是一串字节,不是顶点——每 12 字节才是"一个顶点"。中间那个框是输入装配, 它按 stride 跳着读,右边才是 GPU 眼里的顶点。
注意看

左边 12 个 float、右边只有 3 个顶点:一块内存要经过"取址规则"才变成顶点。规则不对,读出来的就不是你想要的东西。

件二 可操作活例 · 纯 WebGPU · 步长可切
启动中…
步长 stride(字节)

缓冲里真实布局是每顶点 12 字节(x、y 加一个无关的第三个 float)。 只有 12 才读对;8 和 16 会把别人的字节当成坐标,三角形立刻变形。

取址结果

工程上有一点值得注意:stride 烘在管线里,所以换步长 = 换一条管线。 这里预建了三条,切换时只是换一条来用——这也是热重建要处理的那类失效。

动手

点 8 或 16,看三角形怎么塌掉。同一个缓冲、同一份数据,只有取址规则变了。

件三 中间量数值表 · 与件一、件二同源
① 缓冲真身 · 12 个 float
idx值属于
② 按 stride 读到的顶点 · (x, y)
#xy
③ 图元组装
项值
② 里的坐标就是件二画面上那三个顶点——页面上没有第二套算式,表格和画面都由同一个取址函数算出。
本节获得

知道顶点来自缓冲、由 stride / offset 决定怎么读;也知道这份规则一旦写错,读出来的就是一堆错的坐标。

1.2

顶点被搬进裁剪空间

真机可证

本节点在骨架 ① 的第 2 环——顶点已经读进来了,但它们还在你自己的坐标系里。

上一段只把顶点从缓冲里读了出来,它们的坐标还是你随手写的数值。GPU 不认这套坐标,它只认裁剪空间。 中间那次搬运就是顶点着色器干的活——而搬运的旋钮,是一个矩阵。 本段只增加一样东西:一个矩阵。

件一 含代码的数据流示意图
左边是你给的数据,中间是顶点着色器——它的代码就写在框里,右边是它写出的裁剪空间顶点。 动画是 3 个顶点依次流过:一个进,一个出。
注意看

左边 3 个格子进、右边 3 个格子出:顶点着色器被调用了 3 次,一次只处理一个顶点。它不是「一次算完整个三角形」。

件二 可操作活例 · 纯 WebGPU
启动中…
矩阵的四个旋钮
动手

拖动滑块移动、旋转、缩放这个三角形。颜色不跟着三角形走——因为颜色由裁剪空间坐标算出,它钉在画布上;三角形动,颜色从它身上扫过去。

顶点着色器(WGSL)
// 每个顶点调用一次
@vertex
fn vs(p : vec2f) -> VSOut {
  let clip = u_matrix * vec4f(p, 0, 1);
  o.pos = clip;
  o.color = clip * 0.5 + 0.5;
  return o;
}
每帧重算矩阵(JS)
m = projection(400, 300);
m = translate(m, x, y);
m = rotate(m, angle);
m = scale(m, sx, sy);
件三 中间量数值表 · 与件一、件二同源
① 你给的数据 · a_position
#xy
② 写进 gl_Position · 裁剪空间
#xy
③ 写进 v_color · = clip×0.5+0.5
#rg
三张表、件一右列的数字、件二的画面,全部由同一个 buildMatrix() 算出——所以你可以互相验算。
本节获得

知道顶点着色器是「每顶点调用一次」的函数,输出裁剪空间坐标;也知道矩阵就是改这个输出的旋钮。

1.3

看不见的部分被剪掉

真机可证 · 裁剪 示意 · 新顶点数

本节点在骨架 ① 的第 3 环——顶点已经在裁剪空间里,但还没变成屏幕坐标。

上一段把顶点搬进了裁剪空间。可裁剪空间是个 [-1, 1] 的立方体:落在外面的顶点,本来就不该出现在屏幕上。 这一环是固定功能阶段——没有 shader,硬件自己干两件事:把越界的切掉,把切出来的新顶点插在边界上。 本段只增加一样东西:越界判定与切割。

件一 含代码的数据流示意图 · 固定功能阶段放的是判定式
中间那个框是固定功能阶段——它没有源码,所以框里放的是它执行的判定式。 动画后半段:第三个顶点越界,它在边界上被切成两个新顶点。
注意看

左边 3 个格子进、右边出来的是 4 个格子——多出来的两个是在边界上插值得到的新顶点。

件二 可操作活例 · 裁剪前 / 裁剪后 对照
裁剪前 · 条纹部分会被切掉 裁剪后 · 硬件已切掉
启动中…
控制与上一段完全相同
切割状态
动手

把平移 x 推到两端,或者把缩放拉到 3 以上。左边是裁剪前的完整几何——框外那部分用条纹标了出来,它就是要被切掉的; 右边是裁剪后的真实结果,硬件已经把它切掉了,切口是直的。 两个窗格用的是同一套矩阵、同一个 shader,唯一区别是左边给了一张放宽的矩阵把越界部分也画出来。

件三 中间量数值表 · 与件一、件二同源
① 裁剪空间 · gl_Position
#xy
② NDC · = clip ÷ w(本课 w 恒为 1,故与 ① 相等)
#xy
③ 越界判定 · |ndc| > 1
#max|xy|判定
③ 里的「裁剪后顶点数」是示意值——新顶点由硬件在边界上插出,页面拿不到它的真实数量。这一条与件一的动画同属示意,已用角标标出。
本节获得

知道裁剪空间是 [-1, 1] 的立方体、越界部分由固定功能切掉、并在边界上插出新顶点 ——所以顶点数在管线里不是恒定的,「进来几个、出去几个」并不相等。

1.4

裁剪空间怎么变成屏幕

真机可证

本节点在骨架 ① 的最后一环——顶点已经是合法的 NDC 了,但它还不是像素。

裁剪之后,顶点落在 [-1, 1] 的方框里。可屏幕上没有「NDC」,只有像素。把这个方框搬到屏幕上的哪一块、搬多大, 由一个叫视口的矩形决定——这一步同样是固定功能,也没有 shader。 本段只增加一样东西:视口。

件一 含代码的数据流示意图 · 固定功能阶段放的是映射式
左边是 NDC——它只跟矩阵有关、跟屏幕多大无关;中间是视口变换;右边才是像素坐标,它依赖视口。
注意看

同一个 NDC,右边的像素坐标会随着视口变——而左边的 NDC 一个数都没动。

件二 可操作活例 · 纯 WebGPU · 视口可拖
视口
启动中…
视口矩形(以 720 × 450 画布计)
比例

把宽与高调成不相等的两个数:三角形会被拉扁或拉长。NDC 没变,变的是 1 个 NDC 单位等于多少像素——水平和垂直两个比例不等,形状就失真。

动手

先只拖视口 x / y:三角形整体在屏幕上平移,形状不变。再只拖视口宽:它在横向上被拉伸。 NDC 全程一个数都没动。

件三 中间量数值表 · 与件一、件二同源
① NDC · 与屏幕无关
#xy
② 屏幕像素 · 由视口算出
#sxsy
③ 视口参数
项值
③ 里最关键的是水平比例与垂直比例两行:它们不相等时形状就失真——这就是「被拉扁」的全部原因。
本节获得

知道 NDC 到像素是视口说了算、不是矩阵说了算;也知道视口不等比时画面会被拉伸。 到这里,一个顶点从缓冲一路到屏幕的整条路就走完了。

运行时自检 等待脚本…

跑不起来先看设备要求:活例由真 GPU 出画面 —— 手机需 Android 12+ 且 Chrome 121+,或 iOS / iPadOS 26+;电脑需 Chrome / Edge 113+ 或 Safari 26+。Android 上取不到适配器多是设备不支持 Vulkan 1.1+,会自动改走兼容模式(需 Chrome 146+)。不满足时画面区会写出原因与自查步骤,同一页的示意图与数值表照常可读。