Chapter 17: Vertex Processing
顶点处理决定一个 draw call 的几何数据以什么姿态、什么深度、什么插值上下文进入光栅化。读完本章后,读者应能从一帧中的一个 draw call 出发,追踪顶点属性怎样从 buffer 进入 Vertex Shader,怎样经由 model、view、projection 变换进入 clip space,并能判断几何消失、形变错误、深度异常和顶点阶段性能瓶颈分别应检查哪条证据链。
本章的贯穿材料是一帧里绘制一组可实例化角色道具的 draw call。它的 vertex buffer 提供 position、normal、tangent、UV、joint index、joint weight,instance buffer 提供每个实例的 model transform,uniform buffer 提供 view-projection matrix 和骨骼矩阵。这个 draw call 的目标很直接:让每个模型顶点在屏幕上出现在正确位置,同时把法线、UV、颜色或其他 varyings 交给后续插值和 Fragment Shader。
顶点处理的主问题可以压缩成一句话:GPU 如何把离散顶点记录转成可裁剪、可透视投影、可插值的屏幕几何。这个问题跨过 API 状态、shader 代码、坐标数学和 GPU 执行成本,因此本章不会把 Vertex Shader 当作孤立函数讲解,而是把它放在输入装配、坐标变换、裁剪、viewport transform 和 profiler 证据之间讨论。
Microsoft 的 Direct3D 11 graphics pipeline 把 Vertex Shader 描述为从 input assembler 接收顶点并执行 per-vertex 操作的阶段;Khronos 的 Vulkan specification 把 Vertex Shader、vertex input interface、fixed-function vertex post-processing 和 coordinate transformations 分成不同章节;W3C 的 WGSL specification 通过 @builtin(position) 明确 vertex output 中的裁剪空间位置。三类资料的共同点是:顶点阶段的正确性由 shader 输出和固定功能后处理共同决定,性能则由输入属性、执行次数、矩阵与蒙皮计算、缓存复用和无效顶点工作共同决定。
下面这张图限定本章讨论范围。它从 CPU 侧 draw call 已经提交之后开始,到三角形进入 Primitive Assembly 和后续光栅化之前结束;资源创建、命令缓冲录制和 Fragment Shader 细节留给相邻章节展开。
这条路径的关键判断是:Input Assembly 决定“读哪条顶点记录”,Vertex Shader 决定“把这条记录变成什么裁剪空间位置和插值输出”,Vertex Post-processing 决定“基于 w、clip volume、perspective divide 和 viewport state 继续处理这个位置”。一旦画面出现几何错位、翻转、消失、深度异常或顶点阶段耗时上升,排查顺序也应沿着这条路径向前回溯。
17.1 顶点处理的输入、输出与坐标变换路径
顶点处理的输入来自多个层级,需要同时查看 shader 函数参数和管线状态。一个 vertex invocation 的数据通常由 vertex buffer、index buffer、instance buffer、uniform 或 storage buffer、pipeline vertex layout、draw 参数共同确定。以贯穿 draw call 为例,position 来自 mesh 的 vertex buffer,jointIndex 和 jointWeight 来自同一条顶点记录,instanceMatrix 来自 instance buffer,viewProjection 和骨骼矩阵来自 uniform 或 storage buffer。shader 代码只看到已经按 layout 解码后的值,读取地址、格式转换和步进规则则由 API 状态和 draw 参数控制。
输入装配阶段先根据 draw call 的索引和实例编号定位顶点。非索引绘制使用连续 vertex_index,索引绘制先读取 index buffer,再用 index 找到 vertex buffer 中的记录;实例化绘制还会把 instance_index 映射到每个实例的 transform、颜色或材质参数。这里的错误通常表现为网格被拉成长条、UV 乱跳、实例位置全部重合或同一模型只有部分三角形异常。排查时先看 pipeline 中的 vertex attribute layout、stride、offset、format、step mode,再看 shader 的 location 或 semantic 是否与布局一致。
顶点处理的主要输出有两类:一类是裁剪空间位置,一类是交给后续阶段插值的 varyings。裁剪空间位置是 Vertex Shader 对后续固定功能阶段的最低交付物;没有正确的 position,Primitive Assembly、clipping、rasterization 和 depth test 都会失去可靠输入。varyings 包括 UV、normal、tangent、color、world position、motion vector 所需数据等,它们会在三角形内部按插值规则生成 fragment 输入。position 决定顶点能否进入屏幕,varyings 决定屏幕上每个 fragment 如何着色。
贯穿 draw call 的最小坐标路径可以写成:model position → skinned position → world position → view position → clip position → NDC → viewport coordinates。其中 model position 是模型资产中的局部坐标;skinned position 是骨骼或 morph 修改后的局部坐标;world position 把实例摆到场景中;view position 把场景放入相机坐标;clip position 使用投影矩阵编码透视和裁剪信息;NDC 由 perspective divide 得到;viewport coordinates 再映射到 render target 的像素范围。
最常见的矩阵路径是 $p_{clip} = P \times V \times M \times p_{model}$。这个式子里的 $M$ 是 model 或 instance transform,$V$ 是 camera view matrix,$P$ 是 projection matrix,$p_{model}$ 通常写成四维齐次向量 vec4(position, 1.0)。矩阵乘法顺序取决于数学约定、shader 语言和矩阵存储方式;工程里应固定一种约定,并用一个已知顶点做手算复查。例如模型原点经过 model transform 后应落到实例中心,经过 view transform 后应位于相机前方,经过 projection 后其 w 应与视空间深度保持一致的符号和尺度关系。
下面这个 WGSL 风格示例展示了贯穿 draw call 的输入和输出边界。它省略了完整骨骼矩阵数组,只保留坐标、法线、UV、实例矩阵和 view-projection 的关键路径。
struct VertexInput {
@location(0) position: vec3<f32>,
@location(1) normal: vec3<f32>,
@location(2) uv: vec2<f32>,
@location(3) model0: vec4<f32>,
@location(4) model1: vec4<f32>,
@location(5) model2: vec4<f32>,
@location(6) model3: vec4<f32>,
};
struct FrameData {
viewProjection: mat4x4<f32>,
};
@group(0) @binding(0)
var<uniform> frame: FrameData;
struct VertexOutput {
@builtin(position) clipPosition: vec4<f32>,
@location(0) worldNormal: vec3<f32>,
@location(1) uv: vec2<f32>,
};
@vertex
fn main(input: VertexInput) -> VertexOutput {
let model = mat4x4<f32>(
input.model0,
input.model1,
input.model2,
input.model3
);
let worldPosition = model * vec4<f32>(input.position, 1.0);
var output: VertexOutput;
output.clipPosition = frame.viewProjection * worldPosition;
output.worldNormal = normalize((model * vec4<f32>(input.normal, 0.0)).xyz);
output.uv = input.uv;
return output;
}
这个示例说明了两个检查点。第一,@builtin(position) 承担裁剪空间输出,后续固定功能阶段以它为位置依据。第二,normal 使用 w = 0.0 表示方向向量,不参与平移;存在非均匀缩放时应使用 normal matrix 或在资产管线中约束缩放,否则光照方向会产生可见偏差。顶点处理的“输入正确”需要同时满足 buffer layout 正确、矩阵数据正确、shader 语义正确和输出空间正确。
17.2 坐标变换与裁剪空间原理
裁剪空间(clip space)是 Vertex Shader 输出到固定功能后处理的四维空间。它的核心对象是 (x_c, y_c, z_c, w_c),其中 w_c 参与可见性判断和透视除法。使用四维齐次坐标的原因是:透视投影需要让远处物体在屏幕上变小,同时裁剪阶段需要在除法前判断几何与视锥边界的关系。先在 clip space 裁剪,再做 perspective divide,可以让穿过近平面或视锥边缘的三角形被稳定切分。
透视除法把 clip position 转成 normalized device coordinates,常写作 $p_{ndc} = (x_c / w_c, y_c / w_c, z_c / w_c)$。这一步解释了很多顶点错误的画面症状:w_c 的符号错了,物体可能翻到相机背后;w_c 接近零,三角形会被极端拉伸;z_c / w_c 超出深度范围,几何可能在裁剪或深度测试中消失;x_c / w_c 和 y_c / w_c 超出范围,几何在屏幕外或被视锥切掉。
不同 API 对 NDC 的 z 范围、屏幕 y 方向和 viewport state 有差异。Direct3D、Metal、WebGPU 和 Vulkan 的常规深度范围使用 0 到 1;OpenGL 的传统 NDC z 范围使用 -1 到 1。Vulkan 还允许通过 viewport 高度符号、扩展或变换状态处理 y 方向和渲染目标坐标差异。跨 API 迁移 projection matrix 时,深度范围和 y 方向是第一组需要复查的坐标约定。把 OpenGL 风格投影矩阵直接搬到 WebGPU 或 Direct3D 路径,常见结果是深度分布错误、近远平面判断异常或画面上下方向不符合预期。
clip volume 是裁剪空间中的可见范围。以常见规则表达,x 和 y 通常要满足 -w_c <= x_c <= w_c、-w_c <= y_c <= w_c;z 的范围随 API 约定变化,例如 0 到 w_c 或 -w_c 到 w_c。这个判断发生在 perspective divide 之前,因此调试几何消失时应同时查看 clip position 和 NDC。只看屏幕坐标会丢失 w_c 信息,排查时应回到投影矩阵、相机方向、近远平面和 viewport state 分段确认。
viewport transform 把 NDC 映射到 render target 的像素坐标和深度范围。概念上可以写成:x_screen = viewport.x + (x_ndc + 1) * viewport.width / 2,y_screen 根据 API 和 viewport 高度方向映射,z_screen 映射到 viewport 的 minDepth 和 maxDepth。这个阶段由固定功能状态完成,Vertex Shader 不直接写像素坐标。若 mesh viewer 中 NDC 已正确落入范围,但画面仍偏移、缩放异常或上下翻转,应检查 viewport、scissor、render target 尺寸、动态分辨率和 y 方向约定。
贯穿 draw call 的坐标排查可以用一个顶点建立闭环。选取模型局部坐标中容易识别的点,例如角色脚底中心或道具原点,记录它经过 model、view、projection 后的值。若 world position 错误,问题在实例矩阵、单位、坐标轴或资产导入;若 view position 的 z 符号与相机约定冲突,问题在 view matrix 或 handedness;若 clip position 的 w 异常,问题集中在 projection matrix 或矩阵乘法顺序;若 NDC 正常而屏幕异常,问题集中在 viewport、scissor 或 render target 映射。
这一节的关键结论是:裁剪空间属于屏幕坐标之前的中间表示。Vertex Shader 输出的位置仍要经过裁剪、透视除法和 viewport transform。正确调试顶点变换时,应把 model → world → view → clip → NDC → viewport 当作一条可逐段验证的数据路径,把最终画面位置拆回每个阶段分别确认。
17.3 Vertex Shader 在现代渲染中的作用
现代 Vertex Shader 的第一项工作是把资产数据转换成渲染管线需要的几何状态。资产为了存储和传输效率,常使用 half float、normalized integer、压缩法线、量化 position、packed tangent 或 joint index。Vertex Shader 读取这些属性后,需要还原到计算空间,再输出后续阶段能稳定使用的值。这个过程改变的是 attribute bandwidth 和 ALU 的分配:压缩属性减少 buffer 读取量,同时增加解包计算;未压缩属性减少解包计算,同时提高内存带宽压力。
第二项工作是执行顶点级形变,包括 skinning 和 morph。skinning 把多个骨骼矩阵按权重混合到同一个顶点上,morph 则按目标形状的 delta 修改 position、normal 或 tangent。贯穿 draw call 中,角色顶点的局部位置先经过骨骼或 morph 处理,再进入 instance model transform。顺序错误会产生很明显的结果:角色整体位置可能正确,但手臂、衣服或面部局部形变会围绕错误原点运动;法线若没有同步更新,几何轮廓正确但光照会抖动或出现脏块。
第三项工作是处理 instancing。实例化把同一份 mesh 数据复用到多个场景对象上,每个实例通过 instance transform、颜色、风摆参数、LOD 参数或材质索引产生差异。Vertex Shader 读取 instance_index 对应的数据,把同一个 position 映射到不同 world position。性能上,instancing 可以减少 CPU draw 提交和重复资源绑定;顶点阶段仍会为每个实例执行对应的 vertex invocation,因此实例数量、顶点数量和实例矩阵读取会直接进入 Vertex Shader 成本。
第四项工作是生成后续插值所需的 varyings。Fragment Shader 中的 UV、world normal、view direction、shadow coordinate、motion vector 通常都来自 Vertex Shader 输出。varyings 数量越多,primitive 到 fragment 的插值数据越宽,带宽和寄存器压力也会上升。设计 varyings 时应问两个问题:这个值是否能在 Fragment Shader 中以更低成本重建;这个值是否需要 perspective-correct interpolation。比如屏幕空间 UV 可以由 fragment position 推出,world position 可以在 deferred pass 中由 depth 重建;法线、切线和材质 UV 则通常需要稳定传递。
第五项工作是提供裁剪或剔除提示。传统 Vertex Shader 每次 invocation 输出一个顶点,完整可见集通常由 draw call 之前的剔除路径生成;它可以通过 clip distance、position 输出、varying 标记或配合前置 CPU/GPU culling 影响后续阶段。更大规模的剔除通常放在 CPU scene culling、compute culling、mesh shader 或 indirect draw 生成路径中。对本章范围而言,Vertex Shader 的责任是让仍然进入 draw call 的顶点产生正确位置和必要输出;draw call 级、meshlet 级或 cluster 级剔除属于更高层几何提交策略。
下面这个职责表可以用于审查一个 Vertex Shader 是否承担了过多工作。
| 职责 | 输入 | 输出 | 主要风险 |
|---|---|---|---|
| 属性解包 | 压缩 position、normal、UV、tangent | 可计算的 float 向量 | 解包约定与资产管线不一致 |
| 坐标变换 | model、view、projection、instance data | clip position、world position | 矩阵顺序、坐标系、深度范围错误 |
| skinning / morph | joint、weight、pose、morph delta | 形变后的 position、normal | 重复计算、权重未归一、法线不同步 |
| instancing | instance index、instance buffer | 每实例 world position 和 varyings | stride、step mode、矩阵布局错误 |
| varyings 输出 | UV、normal、color、derived values | Fragment Shader 输入 | 插值数据过宽、空间不统一 |
| 裁剪提示 | clip distance、特殊 position | 后处理阶段的裁剪依据 | API 支持与 shader 输出约定不匹配 |
这张表的使用方式是先看视觉或性能症状,再反查职责边界。几何位置错,优先查坐标变换和 instancing;局部动画错,优先查 skinning 和 morph;材质纹理漂移,优先查 UV 传递和插值空间;顶点阶段耗时高,优先查属性宽度、skinning ALU、实例数量和重复 pass。Vertex Shader 的职责越集中,越容易从 frame capture 中定位错误来源。
17.4 构建高效的 Vertex Shader
高效 Vertex Shader 的第一条原则是控制属性带宽。GPU 在执行 Vertex Shader 前需要读取 vertex attributes,属性越宽、stride 越大、访问越分散,输入阶段和缓存层级的压力越高。静态 mesh 的 position、normal、tangent、UV、color 可以按 pass 需求拆分;depth-only pass 常只需要 position 和少量形变数据;复杂材质 pass 才需要 tangent、UV、color 等完整属性。属性布局应服务当前 pass,而非把所有资产字段绑定给所有 draw call。
压缩属性的收益来自减少内存读取,成本来自解包计算和精度边界。normal 和 tangent 常使用 normalized 10-bit、16-bit 或 octahedral encoding;UV 可按资产范围选择 half float 或 normalized integer;joint index 可使用整数压缩;weight 可使用 normalized integer 并在 shader 中还原。压缩方案要和资产导出、运行时解码、法线空间和调试工具显示保持一致。若工具中看到的 attribute 值已经异常,问题优先定位在导入、压缩、layout 或 format 声明,先不进入矩阵和光照推断。
矩阵读取和乘法是第二类成本。每个顶点重复读取大矩阵会增加 uniform、storage 或 vertex input 带宽;每个顶点重复计算 P * V * M 会增加 ALU。常见做法是在 CPU 或 compute 阶段预先得到 model、viewProjection 或 modelViewProjection,让 Vertex Shader 执行最少的必要乘法。实例矩阵可以用 3 到 4 个 vec4 表达仿射变换,具体行列含义要和 shader 的矩阵约定保持一致。存在非均匀缩放时,normal transform 仍需单独处理,position 的矩阵路径直接套给 normal 会带来方向错误。
分支成本要结合执行粒度判断。Vertex Shader 通常在 wave 或 warp 中批量执行,多条顶点 invocation 同时运行同一段 shader。若分支条件来自 uniform,例如当前 pass 是否启用风摆,整组 invocation 走同一路径,代价较低;若分支条件来自每个顶点属性,例如某些顶点有 4 个骨骼权重、某些有 8 个权重,同一批 invocation 可能出现路径分裂,执行效率下降。工程上更稳定的做法是按材质、形变类型、骨骼影响数量或 pass 生成不同 shader variant,把高分支路径拆成更明确的渲染批次。
skinning 是 Vertex Shader 中最常见的 ALU 热点之一。一个顶点若有 4 个骨骼影响,就要读取 4 个矩阵或双四元数,并做多次乘法和加权;若同一角色在 color pass、shadow pass、velocity pass 中重复执行相同 skinning,顶点阶段成本会随 pass 数累加。可选优化包括限制骨骼影响数量、使用更紧凑的 pose 表示、在 compute pass 中预处理 skinned vertex buffer、为 shadow pass 使用低成本变体、按 LOD 降低远处角色的骨骼复杂度。选择哪种方案取决于瓶颈证据:ALU 饱和倾向更适合减少矩阵乘法,带宽受限倾向更适合减少 pose 和 attribute 读取。
无效顶点工作是第三类高频浪费。视锥外、被遮挡、屏幕占比很小或 LOD 过高的 mesh 仍进入 draw call 时,Vertex Shader 会照常处理这些顶点。CPU frustum culling、GPU compute culling、LOD、meshlet culling、indirect draw compaction 都可以减少进入顶点阶段的工作量。本章只关心它们对 Vertex Shader 的结果:减少 vertex invocation 数量、减少重复属性读取、减少后续 primitive 和 raster work。优化是否成立,要看 profiler 中顶点 invocation、primitive count、clipped primitive、GPU time 和后续 pass 时间是否同步下降。
post-transform cache 影响索引网格的复用效率。索引绘制中,相邻三角形常共享顶点;GPU 可以缓存已经经过 Vertex Shader 处理的结果,后续三角形再次引用同一顶点时复用输出。索引顺序差会降低复用率,让同一个逻辑顶点多次执行 Vertex Shader。资产管线可以使用 vertex cache reorder、mesh optimization 或 meshlet 构建改善这一点。判断这类问题时,看的是 indexed draw、唯一顶点数、Vertex Shader invocation 数和 post-transform cache 命中相关指标之间的关系。
一个可复用的优化顺序是:先确认画面正确和输入 layout 正确;再用 profiler 判断瓶颈在 CPU 提交、Vertex Shader ALU、attribute bandwidth、cache 复用还是后续光栅化;接着选择对应动作。attribute bandwidth 高就压缩、拆 stream、减少 pass 属性;ALU 高就减少 skinning、预计算矩阵、拆 variant;invocation 数高就做 culling、LOD、index reorder;cache 复用差就重排 index 或调整 meshlet。这个顺序比直接改 shader 更稳,因为它把优化动作绑定到可观察证据。
17.5 Vertex Processing Bottleneck Evidence Map
顶点处理瓶颈需要用证据图定位。画面症状说明“结果异常”,profiler counter 说明“某类资源压力上升”;二者都要回到 draw call、shader 和资源布局解释。Evidence Map 的作用是把症状、可能原因、观察入口和下一步动作放在同一张表里,让排查从猜测变成顺序验证。
| 现象或指标 | 可能定位 | 观察入口 | 下一步检查 |
|---|---|---|---|
| 模型整体位置错误 | model / view / projection 路径 | mesh viewer、shader debug、矩阵 dump | 手算一个顶点的 world、view、clip 值 |
| 模型局部扭曲 | skinning、morph、attribute layout | vertex attribute view、pose buffer、骨骼权重 | 检查 joint index、weight 归一、矩阵数组索引 |
| 几何在相机附近闪烁 | near plane、w_c、projection matrix | clip position、NDC、depth range | 检查 w_c 符号、near/far、z 范围约定 |
| 上下翻转或深度异常 | API 坐标约定、viewport、projection | pipeline state、viewport、render target | 检查 y 方向、NDC z、viewport minDepth/maxDepth |
| Vertex Shader GPU time 高 | ALU、skinning、矩阵计算 | shader profiler、GPU timing | 统计矩阵乘法、骨骼影响数、shader variant |
| Attribute fetch 压力高 | stride、format、stream 组织 | vertex input state、memory counters | 减少无用属性、压缩格式、拆 pass stream |
| Invocation 数远高于预期 | index 顺序、实例数、culling 不足 | draw stats、VS invocation、primitive count | 检查 LOD、frustum culling、index reorder |
| cache 复用差 | post-transform cache、索引局部性 | vendor profiler、mesh optimizer 输出 | 重排 index、构建 meshlet、合并小三角片段 |
使用 Evidence Map 时先从最稳定的事实开始。第一个事实是 draw call 的输入规模:index count、instance count、vertex stride、绑定的 vertex buffers、实际启用的 shader variant。第二个事实是输出规模:Vertex Shader invocation、生成 primitive、被裁剪 primitive、进入 rasterizer 的 primitive。第三个事实是耗时分布:当前 draw 或 pass 的 GPU time、attribute fetch、shader ALU、缓存和内存相关指标。三类事实能把“顶点阶段慢”拆成输入太多、每个顶点太贵、复用率太差或后续阶段转移压力。
RenderDoc 适合先确认输入和坐标正确性。它的 pipeline state、mesh viewer 和 shader debugging 能回答“shader 实际读到了什么 attribute”“Vertex Shader 输出的 position 是什么”“某个顶点在 VS 前后如何变化”。Nsight Graphics、Radeon GPU Profiler、Intel Graphics Performance Analyzers 和 Xcode GPU Frame Capture 更适合进一步观察 GPU 时间、shader 指令、内存访问、cache 和 occupancy 等硬件相关证据。工具名称并不改变判断顺序:先确认数据路径,再解释性能指标。
对于贯穿 draw call,若角色数量增加后帧时间上升,排查可以这样做。先看 CPU draw 数量是否增加;若 draw 数稳定,继续看 Vertex Shader invocation 是否近似等于 index count × instance count 或明显更高;再看 shader profiler 中 skinning 相关指令和 pose buffer 读取是否占主导;接着对比 depth-only pass 和 color pass 是否重复执行同样的 skinning;最后选择预计算 skinning、降低远处 LOD、减少骨骼影响或拆分 pass 属性。这个过程每一步都有指标支撑,不依赖经验猜测。
若画面上模型完全消失,排查路径不同。先确认 draw call 是否存在且 index count 大于 0;再看 input layout 是否能正确解码 position;接着查看 Vertex Shader 输出的 clip position,重点关注 w_c、z_c 和 NDC;然后检查 viewport、scissor、cull mode、front face、depth range 和 render target。若 clip position 已经超出可见范围,问题集中在矩阵、坐标系、实例数据或相机参数;若 NDC 正常但屏幕仍无输出,问题转移到 Primitive Assembly、culling、rasterization、depth/stencil 或 fragment 阶段。
Vertex Processing Bottleneck Evidence Map 的核心结论是:顶点阶段的正确性和性能都要回到同一条数据链。输入规模决定 invocation 基数,attribute layout 决定读取成本,Vertex Shader 决定每个 invocation 的 ALU 与输出,post-transform cache 决定共享顶点的复用,固定功能后处理决定裁剪、透视除法和 viewport 映射。把这些证据放在一张图里,才能区分“顶点太多”“每个顶点太贵”“属性太宽”“矩阵错了”“坐标约定错了”和“后续阶段挡住了输出”。
最小自检任务
给定一个 draw call:它渲染 200 个实例化角色道具,每个 mesh 有 18,000 个索引、7,200 个唯一顶点,Vertex Shader 执行 position 变换、normal 变换和 4-weight skinning。修改相机投影矩阵后,画面中一半实例在靠近相机时消失;profiler 同时显示该 pass 的 Vertex Shader 时间明显高于其他 pass。请按本章方法回答两个问题:第一,几何消失应先检查哪条坐标证据;第二,顶点阶段耗时应如何区分 invocation 数量、attribute bandwidth、skinning ALU 和 post-transform cache。
答案要点
几何消失先从 draw call 和 Vertex Shader 输出开始确认。先检查 index count、instance count 和 input layout,确认 draw call 确实提交且 position 解码正确;再选一个靠近相机的顶点,沿 model → world → view → clip → NDC 记录数值。重点看 clip position 的 w_c、z_c、x_c / w_c、y_c / w_c 和 z_c / w_c。若 w_c 符号、near/far、NDC z 范围或 handedness 与当前 API 约定冲突,几何会在裁剪、透视除法或深度映射阶段出问题。若 NDC 正常,再转向 viewport、scissor、cull mode、front face、depth range 和后续 raster/depth 状态。
顶点阶段耗时按证据拆分。先用 index count × instance count 估算 invocation 基数,再和工具里的 VS invocation、primitive count、clipped primitive 对照;若 invocation 数随实例数线性上升,culling 和 LOD 是第一类检查项。接着看 vertex stride、启用属性和 attribute fetch 或内存相关指标;若属性宽、pass 只使用 position,拆 stream 或减少该 pass 输入属性。再看 shader 指令和 pose buffer 读取;若 4-weight skinning 占主导,考虑限制影响数量、预处理 skinning、为 shadow/depth pass 使用低成本变体。最后看 indexed draw 的唯一顶点数、VS invocation 和 post-transform cache 指标;若复用率低,使用 index reorder 或 meshlet 构建改善共享顶点复用。
本章知识点总结
- 顶点路径:顶点处理把 buffer 中的离散记录转成裁剪空间位置和可插值输出。
- 输入来源:Vertex Shader 参数由 vertex buffer、index buffer、instance buffer、draw 参数和 pipeline layout 共同决定。
- 输出边界:裁剪空间 position 决定几何能否进入后续阶段,varyings 决定 fragment 输入如何插值。
- 坐标链路:稳定排查顺序是
model → world → view → clip → NDC → viewport。 - 齐次坐标:
w_c同时参与裁剪和透视除法,是定位投影错误的关键证据。 - API 约定:深度范围、y 方向和 viewport 映射在跨 API 迁移时需要逐项复查。
- 属性解包:压缩属性减少读取量,同时增加解包计算和资产管线一致性要求。
- 形变成本:skinning 和 morph 改变顶点局部形状,也常成为 Vertex Shader ALU 热点。
- 实例化成本:instancing 减少 CPU 提交和资源绑定,但每个实例仍会产生对应的顶点执行量。
- varyings 设计:varyings 应只传递后续阶段确实需要且空间定义清楚的数据。
- 属性带宽:高效 pass 应绑定当前 pass 需要的属性,减少无用 stride 和格式读取。
- 矩阵优化:预计算
viewProjection、modelViewProjection或紧凑实例矩阵可以降低重复顶点计算。 - 缓存复用:post-transform cache 依赖索引局部性,索引重排能减少共享顶点的重复执行。
- 证据定位:顶点瓶颈要用 invocation、attribute fetch、ALU、cache 和 primitive 统计共同判断。
- 工具边界:RenderDoc 适合确认输入和坐标,厂商 profiler 适合解释 GPU 时间和硬件压力。