Chapter 20: Raster Algorithms
读完本章后,读者应能把一帧画面中的锯齿、深度穿插、透明叠色、过度绘制和 MSAA 结果,定位到光栅化阶段的覆盖规则、深度/模板状态、采样状态、混合状态或 render target 带宽。光栅化的核心工作是把连续的几何基元转成离散的 fragment 或 sample,并决定哪些 fragment 有资格进入后续 fragment / pixel shader 与输出合并阶段。
本章使用一个贯穿材料:一帧中绘制两块互相遮挡的三角形面片,前方三角形使用不透明材质,后方三角形使用较重的 fragment shader,画面开启深度缓冲;随后把前方三角形改成带透明边缘的叶片或玻璃片,并开启 4x MSAA。这个材料足够小,但会触发光栅化中最常见的五类判断:像素覆盖、共享边归属、深度测试、coverage mask、blend 与 overdraw。
在这帧里,顶点处理和基元装配已经把三角形送到屏幕空间,裁剪也已经完成。光栅化阶段接收的是屏幕空间三角形、viewport、scissor、raster state、depth/stencil state、multisample state、blend state 和 render target / depth target 格式。它输出的不是最终图像本身,而是一组带位置、深度、覆盖样本、插值属性和后续测试条件的 fragment。最终颜色仍要经过 shader、深度/模板、混合和写入路径。
理解 Raster Algorithms 的关键结论是:光栅化质量问题通常来自覆盖规则和采样规则,深度错乱通常来自深度值、深度比较和写入状态,性能问题通常来自 fragment 数量、early-z 是否生效、blend 写回与 render target 带宽。排查时先固定几何输入,再看 coverage,再看 depth/stencil,再看 shader 与 blend,最后看带宽与提交状态。
下面这条路径描述了本章的边界。它覆盖传统 raster pipeline,不展开 ray tracing、mesh shader 的完整执行模型,也不把特定 GPU 内部 tile / ROP 实现写成跨平台事实。
图中最容易被混淆的是覆盖判定与最终颜色。覆盖判定只回答“这个像素或 sample 是否属于当前三角形”;最终颜色还取决于 shader 计算、深度/模板通过情况、混合方式和写入目标格式。把这几个阶段拆开,很多画面问题会从“渲染错了”变成可定位的状态错误。
20.1 光栅化基本原理与深度测试机制
光栅化把屏幕空间几何基元转换成离散 fragment。对三角形来说,输入通常包含三个屏幕空间顶点、每个顶点的深度值和需要插值的属性,例如 UV、normal、color、world position 或 material id。输出的 fragment 至少包含屏幕位置、覆盖信息、插值后的属性和深度值。后续阶段再根据 depth/stencil state、shader 输出、blend state 和 render target 格式完成可见性与颜色写入。
贯穿材料中的第一帧可以从一个最小不透明三角形开始观察。顶点 shader 输出三个 clip space 顶点,经过 perspective divide 和 viewport transform 后变成屏幕坐标。Rasterizer 在屏幕格点上判断每个像素中心或每个 MSAA sample 位置是否落入三角形内部。命中的位置生成 fragment,并把顶点属性按照透视校正规则插值到 fragment 上。
光栅化阶段常见的数据路径如下。这里的 fragment 可以理解成“候选像素贡献”,它还没有保证最终进入 render target。
这条路径里,Triangle Setup 会把三角形转换成适合增量计算的边方程、面积倒数、属性梯度和深度梯度。Coverage Test 决定三角形覆盖哪些 sample。Attribute Interpolation 使用屏幕位置和三角形重心关系恢复 fragment 属性。Depth/Stencil Test 决定当前 fragment 是否被已有图像内容挡住,或者是否满足模板条件。
深度缓冲(depth buffer)是一个与 render target 对齐的图像资源。它为每个像素或每个 sample 保存一个深度值。以 Vulkan / Direct3D 常见的深度范围为例,viewport 之后的深度通常落在 [0, 1],越靠近 near plane 的值越小或越大取决于投影设计与比较函数。许多工程使用 LESS 或 LESS_EQUAL,即新 fragment 的深度值小于已有深度值时通过测试。
贯穿材料中的两个三角形重叠时,深度测试的判断可以写成一个最小伪代码。这里假设使用小值更近的深度约定。
struct Fragment {
int x;
int y;
float depth;
Color color;
};
bool depth_test_less(float incoming_depth, float stored_depth) {
return incoming_depth < stored_depth;
}
void output_fragment(const Fragment& fragment) {
float stored = depth_buffer[fragment.y][fragment.x];
if (depth_test_less(fragment.depth, stored)) {
depth_buffer[fragment.y][fragment.x] = fragment.depth;
color_buffer[fragment.y][fragment.x] = fragment.color;
}
}
这段代码表达了深度测试的主干:读取已有深度,比较当前 fragment 深度,通过后写回深度和颜色。真实 GPU 会把这条路径拆到固定功能单元、cache、tile memory、ROP / output merger 等实现中,并可能把部分深度测试提前到 shader 前执行。工程判断只需要抓住输入、比较函数、写入状态和目标资源这四个点。
Depth test 与 depth write 是两件事。Depth test 控制 fragment 是否通过可见性测试;depth write 控制通过后的 fragment 是否更新 depth buffer。典型不透明物体会同时开启 depth test 和 depth write。透明物体通常开启 depth test、关闭 depth write,再按从远到近的顺序进行 blending。这个设置让透明 fragment 能被前方不透明物体挡住,同时降低透明物体互相写深度造成的遮挡错误。
深度错乱的排查顺序应固定。先确认投影矩阵输出的 z 范围与 API 深度范围匹配,再确认 depth buffer 格式和 clear value,再确认比较函数,再确认 depth write,再确认透明 pass 的排序与写深度策略。很多“面片闪烁”来自两个表面深度过近,也就是 z-fighting。此时修改 depth compare 只能改变症状,稳定做法是调整几何偏移、深度范围、near/far 比例或使用合适的 depth bias。
Early-Z 是深度测试与性能之间的关键连接点。常见 GPU 可以在 fragment shader 之前执行一部分深度测试,把被挡住的 fragment 提前丢弃,减少后续 shader 计算。这个收益依赖几个条件:不透明 pass 深度写入稳定,shader 没有改变深度输出,discard / alpha test 使用受控,blend 和透明 pass 被单独处理。贯穿材料中,后方三角形 shader 很重,前方不透明三角形先写入深度时,后方被挡住区域的 shader 工作量就可能明显下降。
这个阶段的可观察证据有三类。画面上,深度错误表现为前后关系颠倒、闪烁、透明遮挡异常。工具上,RenderDoc 或类似 frame debugger 可以检查 draw call 的 depth/stencil state、depth attachment、clear value 和 fragment 输出。性能上,overdraw view、shader invocation counter、early-z / late-z 相关 counter 可以说明 fragment 是否在 shader 前被过滤。
20.2 扫描线与像素覆盖规则实现细节
像素覆盖规则回答一个具体问题:屏幕上的某个像素中心或 sample 位置是否属于当前三角形。扫描线算法从行的角度填充多边形,现代 GPU 更常用基于边函数和 tile / block 的并行计算,但两者要解决的问题一致:用稳定、可复现的规则把连续边界映射到离散网格。
边函数(edge function)是判断点在三角形哪一侧的常用形式。给定有向边 a → b 和待测点 p,可以定义 E(a, b, p) = (p_x - a_x) * (b_y - a_y) - (p_y - a_y) * (b_x - a_x)。如果三角形顶点顺序固定,三个边函数的符号一致时,点就在三角形内部或边界上。具体符号取决于坐标系方向和顶点绕序,工程实现中必须保持三条边使用同一套约定。
下面的简化代码只展示 coverage 判定。它没有处理透视校正、MSAA、多 viewport 和精度细节,但足够说明边函数如何把几何转成像素候选。
struct Vec2 {
float x;
float y;
};
float edge_function(Vec2 a, Vec2 b, Vec2 p) {
return (p.x - a.x) * (b.y - a.y) - (p.y - a.y) * (b.x - a.x);
}
bool inside_triangle(Vec2 v0, Vec2 v1, Vec2 v2, Vec2 sample) {
float e0 = edge_function(v0, v1, sample);
float e1 = edge_function(v1, v2, sample);
float e2 = edge_function(v2, v0, sample);
return e0 >= 0.0f && e1 >= 0.0f && e2 >= 0.0f;
}
真实光栅化会处理共享边。两个三角形拼成一个四边形时,共享边上的 sample 应归属于其中一个三角形;如果两边都写入会造成重复覆盖,如果两边都跳过会出现缝隙。top-left rule 的作用就是为边界 sample 提供一致归属:某些边界被包含,另一组边界被排除。这样相邻三角形共享边时,边界 sample 只会被一个三角形接收。
Subpixel precision 解决的是顶点落在像素之间时的稳定性。屏幕坐标不是简单取整后再做 coverage,硬件通常在固定小数精度下进行三角形设置和边函数增量。顶点位置的小数部分会影响 coverage、属性插值和深度梯度。工程上,viewport、jitter、TAA、动态分辨率和 UI 对齐都可能受到 subpixel precision 影响。
Sample coverage 是 coverage 的更细粒度版本。不开 MSAA 时,通常每个像素只用一个代表位置参与覆盖判断。开启 4x MSAA 时,一个像素内有 4 个 sample 位置,三角形可能覆盖其中 1、2、3 或 4 个 sample。coverage mask 记录哪些 sample 被覆盖。后续 depth/stencil、shader 执行频率、blend 和 resolve 都会受这个 mask 影响。
贯穿材料中,三角形边缘的锯齿来自连续边界与离散像素网格的冲突。单 sample 时,一个像素要么被当前三角形覆盖,要么不被覆盖,边界呈阶梯状。MSAA 把一个像素内部拆成多个 sample,边界像素可以只覆盖部分 sample。最终 resolve 时,像素颜色由多个 sample 结果合成,边缘会更平滑。
扫描线思路仍然有工程价值。软件 rasterizer、离线 mask 生成、CPU 端调试工具、字体或 2D 填充实现经常使用扫描线。它按 y 行求出多边形与扫描线的交点,再把交点之间的区间填入。对三角形来说,边函数更容易并行化;对复杂多边形来说,扫描线与 fill rule 更直观。
像素覆盖问题的排查顺序是:先确认输入顶点是否已经在正确 viewport 中,再确认三角形绕序和 cull mode,再确认 scissor 是否裁掉目标区域,再确认 fill mode 和 polygon mode,再确认 sample count、sample pattern 和 coverage mask。边缘裂缝、细线消失、UI 半像素偏移和 TAA 抖动残影,通常都能在这条顺序中找到第一处异常。
20.3 多边形填充策略与抗锯齿基础
多边形填充策略决定哪些区域被写入,抗锯齿策略决定边界如何从连续几何过渡到离散像素。三角形是实时渲染的主流输入,因为三点一定共面,硬件覆盖规则清晰,插值路径稳定。n-gon、曲线、字体轮廓或 sprite 最终也要转换成三角形、coverage mask、SDF 或 alpha texture 等可写入的形式。
Fill rule 处理的是区域边界归属。三角形使用固定的半平面测试和边界规则;复杂 2D 路径常见 even-odd 或 non-zero winding 规则。实时 3D 管线里的普通 mesh 通常已经被三角化,fill rule 的主要工程表现会落在三角形边界、front/back face、cull mode、polygon mode 和 stencil 用法上。
Stencil buffer 是 depth/stencil 资源中的模板通道。它不直接表达远近关系,而是保存一个小整数标记,用来控制后续 fragment 是否有资格写入。典型用法包括镜面区域、描边 mask、portal、阴影体和 UI 裁剪。贯穿材料中,如果只希望三角形写入屏幕中的一个圆形区域,可以先用 stencil pass 写入模板,再让三角形 pass 只在模板值匹配的位置通过。
MSAA 的核心是“多 sample 覆盖,少量 shader 计算”。在常见配置下,三角形边缘的每个 sample 会独立参与 coverage 和 depth/stencil,pixel shader 可能按像素执行一次,也可能在需要时按 sample 执行。最终 resolve 把多 sample render target 合成普通单 sample 图像。MSAA 对几何边缘有效,对 shader 内部高频纹理、法线贴图闪烁和透明纹理边缘的帮助有限。
Coverage mask 是理解 MSAA 的关键对象。它记录当前 fragment 覆盖了像素中的哪些 sample。Alpha-to-coverage 会根据 alpha 值修改 coverage mask,使树叶、栅栏、头发卡片等 alpha 边缘在 MSAA 下获得更稳定的边界。它的代价是透明程度被离散 sample 数限制,4x MSAA 只有少量覆盖级别,近距离可能出现颗粒感。
Resolve 是从多 sample 图像到单 sample 图像的合成步骤。普通 color resolve 可以把多个 sample 的颜色平均。Depth resolve 和 stencil resolve 的语义更敏感,不同 API、格式和扩展支持存在边界。工程中需要区分“用于展示的 color resolve”和“后续 pass 要继续读取的 depth / normal / velocity 资源”。延迟渲染中,G-buffer 开启 MSAA 会显著增加内存占用和带宽,resolve 设计会影响后续 lighting pass。
贯穿材料把前方不透明三角形改成带透明边缘的叶片后,填充策略发生变化。普通不透明三角形可以依赖 depth write 与 early-z。叶片常使用 alpha texture,fragment shader 根据 alpha 丢弃低透明度区域,或使用 alpha-to-coverage 生成 coverage mask。前者边缘清晰但可能削弱 early-z,后者在 MSAA 下边缘更平滑但受 sample count 限制。选择取决于材质、目标平台、边缘质量和 overdraw 压力。
Depth/stencil、MSAA、coverage mask 和 blend 的关系需要分层判断。Depth/stencil 决定 sample 是否通过可见性与模板条件。Coverage mask 决定哪些 sample 参与写入。Blend 决定新颜色与已有 render target 颜色如何合成。Resolve 决定多 sample 结果如何变成最终图像。把这四步混在一起,会把透明排序、锯齿、深度穿插和颜色发灰混成同一个问题。
抗锯齿排查顺序可以固定为:先判断锯齿来自几何边缘、纹理采样、shader 高频还是时间采样;几何边缘优先看 MSAA / TAA / 分辨率;透明纹理边缘看 alpha test、alpha-to-coverage 和排序;纹理闪烁看 mipmap、anisotropic filtering 和采样频率;时间残影看 motion vector、jitter 和历史帧权重。本章只覆盖与 raster coverage 直接相关的部分,纹理采样和 TAA 会在后续采样、后处理和 temporal 章节继续展开。
20.4 在 Vulkan / DX12 中实现光栅化阶段设置
Vulkan 和 Direct3D 12 都把光栅化相关状态显式放入 pipeline 或 command list。工程上要把“算法规则”映射成“API 状态”:viewport 和 scissor 决定屏幕区域,raster state 决定填充、剔除和深度偏移,depth/stencil state 决定测试与写入,multisample state 决定 sample count 与 coverage,blend state 决定颜色写回方式,render target / depth target 格式决定写入资源的实际存储。
DX12 的官方文档把大部分图形管线状态放入 Pipeline State Object(PSO),其中包括 rasterizer state、blend state、depth stencil state、primitive topology type、render target / depth stencil format 和 multisampling parameters;viewport、scissor、blend factor、stencil ref、具体 primitive topology order 等状态通过 command list 设置。相关入口可以对应查看 Direct3D 12 pipeline state overview、D3D12_GRAPHICS_PIPELINE_STATE_DESC 和 D3D12_RASTERIZER_DESC。
Vulkan 的设计也把 graphics pipeline 作为核心对象。一个常见 graphics pipeline 会组合 shader stages、vertex input、input assembly、viewport state、rasterization state、multisample state、depth/stencil state、color blend state、render pass / dynamic rendering 相关格式和 pipeline layout。具体字段会随 Vulkan 版本、扩展和 dynamic state 使用方式变化,工程文档应锁定项目使用的 Vulkan SDK 与目标平台。
下面的表把贯穿材料中的渲染意图映射到 API 状态。它只列核心状态,不覆盖扩展、动态状态和平台特性。
| 目标 | Vulkan 常见对象 | DX12 常见对象 | 排查时先看 |
|---|---|---|---|
| 限定屏幕绘制区域 | viewport / scissor state | RSSetViewports / RSSetScissorRects | 坐标、尺寸、动态状态是否已设置 |
| 控制三角形填充与剔除 | rasterization state | D3D12_RASTERIZER_DESC | FillMode、CullMode、front face |
| 控制深度可见性 | depth/stencil state | D3D12_DEPTH_STENCIL_DESC | enable、compare op、write mask、clear value |
| 控制 MSAA 覆盖 | multisample state | SampleDesc、SampleMask | sample count、sample mask、alpha-to-coverage |
| 控制颜色叠加 | color blend state | D3D12_BLEND_DESC | blend enable、src/dst factor、write mask |
| 控制输出格式 | rendering / render pass formats | RTV / DSV formats | color/depth format 与 sample count 是否匹配 |
一个最小不透明 pass 的状态组合通常是:solid fill,back-face culling,depth test 开启,depth write 开启,compare 使用小值更近的约定,blend 关闭,sample count 与 render target / depth target 一致。这个组合适合贯穿材料中前方不透明三角形先写入深度,为后方重 shader 三角形提供 early-z 机会。
// 简化伪代码:表达状态意图,不对应某个完整 API 初始化函数。
RasterPassState opaque_pass;
opaque_pass.fill_mode = FillMode::Solid;
opaque_pass.cull_mode = CullMode::Back;
opaque_pass.depth_test_enabled = true;
opaque_pass.depth_write_enabled = true;
opaque_pass.depth_compare = CompareOp::Less;
opaque_pass.blend_enabled = false;
opaque_pass.sample_count = SampleCount::One;
透明 pass 的状态组合会改变。常见设置是 depth test 开启,depth write 关闭,blend 开启,排序从远到近。带 alpha-to-coverage 的 foliage pass 会开启 MSAA,并让 alpha 值影响 coverage mask。此时 sample count 必须与 color target、depth target 和 pipeline state 匹配。DX12 的 D3D12_GRAPHICS_PIPELINE_STATE_DESC 会把 render target formats、depth stencil format 和 sample description 放在同一个 PSO 描述里,运行时也会验证 sample count 与格式支持关系。
RasterPassState transparent_pass;
transparent_pass.fill_mode = FillMode::Solid;
transparent_pass.cull_mode = CullMode::None;
transparent_pass.depth_test_enabled = true;
transparent_pass.depth_write_enabled = false;
transparent_pass.depth_compare = CompareOp::LessEqual;
transparent_pass.blend_enabled = true;
transparent_pass.src_color = BlendFactor::SrcAlpha;
transparent_pass.dst_color = BlendFactor::OneMinusSrcAlpha;
transparent_pass.sample_count = SampleCount::Four;
transparent_pass.alpha_to_coverage_enabled = true;
Viewport 和 scissor 是最常见的“状态已创建但绘制区域为空”来源。Viewport 把 NDC / normalized device coordinate 映射到 framebuffer 坐标和深度范围。Scissor 再裁出允许写入的矩形区域。一个 draw call 没有显示结果时,除了检查 shader 和资源绑定,还应先看 viewport 宽高、scissor 宽高、framebuffer 尺寸、动态状态绑定顺序和当前 command list / command buffer 中实际记录的状态。
Rasterizer state 影响 coverage 入口。Fill mode 控制实体、线框或点模式;cull mode 根据三角形 front face 丢弃正面或背面;depth bias 修改生成 fragment 的深度值,常用于 shadow map 或 decal;conservative rasterization 会扩大覆盖判定,使与几何相交的像素更容易被覆盖。保守光栅化属于特性相关路径,项目应先查询设备支持。
Depth/stencil state 影响通过条件。Depth enable、compare op、write enable、stencil enable、front/back stencil op 都属于这里。一个常见错误是天空盒、透明物体、decals 和后处理 pass 沿用了不透明 pass 的深度写入状态,导致后续 draw call 被异常遮挡。更稳定的工程做法是按 pass 类型维护状态模板,再在 frame graph 或 render graph 中显式声明 depth read / write 关系。
Blend state 影响输出合并。Opaque pass 通常关闭 blend,transparent pass 开启 alpha blend,additive effect 使用加法混合,UI 可能使用 premultiplied alpha。Blend 发生在 render target 读改写路径上,会增加带宽和 ROP / output merger 压力。多 render target 下,每个 attachment 的 blend 和 write mask 也要分别确认。
API 状态排查的顺序应与 pipeline 数据路径一致:先确认 render target / depth target 格式和 sample count,再确认 viewport / scissor,再确认 rasterizer,再确认 depth/stencil,再确认 shader 输出语义,再确认 blend 和 write mask。这样可以把“没有画出来”“被挡住”“边缘锯齿”“透明颜色错误”“性能下降”分别归到不同状态层。
20.5 性能优化技巧及其影响分析
Raster Algorithms 的性能瓶颈主要来自五个来源:overdraw 造成 fragment 数量增长,heavy fragment shader 放大每个 fragment 成本,depth / stencil / blend 产生读改写压力,MSAA 增加 sample 存储和测试数量,render target 带宽限制最终写入速度。优化前需要先判断瓶颈类型,因为减少 draw call、降低 shader 复杂度、加 depth pre-pass、降低 sample count 和改变 blend 顺序解决的是不同问题。
Overdraw 指同一像素或 sample 被多个 fragment 反复覆盖。贯穿材料中,后方重 shader 三角形被前方不透明三角形挡住时,如果后方仍执行了大量 fragment shader,就出现了可优化的 overdraw。可观察证据包括 overdraw heatmap、shader invocation 数、fragment stage 时间、early-z counter、draw call 顺序和 depth buffer 内容。
Early-Z 优化的判断顺序很直接。先让不透明物体按合适顺序写入 depth buffer,再让后续被遮挡的 fragment 在 shader 前被深度测试过滤。收益最大的场景是前方遮挡面积大、后方 shader 重、overdraw 明显。收益较小的场景是 shader 很轻、几何稀疏、透明物体占主导,或 pass 本身需要读取/修改深度与 sample coverage。
Depth pre-pass 是单独先绘制一遍深度,再在主颜色 pass 中利用深度过滤 fragment。它会额外增加一次几何提交和顶点处理成本,但可以减少昂贵 fragment shader 的执行。适用条件是 fragment shader 成本高、overdraw 高、场景中不透明几何占比较大、深度写入稳定。对低多边形、低 overdraw、简单材质场景,depth pre-pass 可能增加总成本。
Alpha test、discard 和透明 blend 会改变 early-z 的收益边界。Alpha test 需要 shader 读取 alpha 后才知道 fragment 是否保留,很多实现会把相关深度路径推迟。透明 blend 需要读取已有颜色再合成,通常还要关闭 depth write 并排序。透明材质较多的场景,性能优化重点会从“写好深度”转向“减少透明层数、裁剪透明 mesh、降低透明 shader 成本、分辨率缩放或专门的透明 pass 设计”。
Blend cost 的本质是输出阶段的读改写。Opaque 写入可以直接覆盖 render target;alpha blend 需要读取已有颜色,计算混合结果,再写回。HDR render target、多 render target、MSAA render target 和高分辨率都会放大带宽压力。Additive particle、透明 UI、屏幕空间雾和后处理叠层都可能受 blend 和 render target bandwidth 限制。
MSAA 的性能影响要分开看。Coverage、depth/stencil 和存储通常按 sample 增加;pixel shader 是否按 sample 执行取决于 shader 输入和 API 设置。4x MSAA 不等于 shader 成本必定乘以 4,但 color/depth 存储、resolve 和带宽压力通常会上升。移动 GPU、tile-based GPU 和桌面独显在内部实现上差异明显,跨平台项目应使用平台工具验证实际瓶颈。
贯穿材料可以形成一个稳定优化流程。第一步,只绘制不透明前方三角形并确认 depth buffer 正确写入。第二步,绘制后方重 shader 三角形,观察被遮挡区域的 shader invocation 是否下降。第三步,把前方材质改成 alpha test 或 alpha-to-coverage,观察 early-z、coverage mask 和边缘质量变化。第四步,开启 blend 后确认 render target 带宽和排序成本。每一步只改变一个状态,才能把结果归因到具体机制。
下面的检查表适合用于 frame capture 复盘。它按证据顺序组织,降低从画面症状直接跳到优化结论的风险。
- 无输出:检查 render target 绑定、viewport、scissor、cull mode、depth compare、color write mask。
- 前后错乱:检查投影深度范围、clear value、compare op、depth write、透明排序。
- 边缘锯齿:检查 sample count、coverage mask、alpha-to-coverage、resolve 和 TAA 输入。
- 过度绘制:检查 draw order、depth pre-pass、occlusion / culling、透明层数和 shader invocation。
- 带宽受限:检查 render target 格式、MRT 数量、MSAA、blend、resolve 和分辨率。
- 状态切换过多:检查 pipeline state 分组、material sorting、pass 划分和动态状态使用。
性能优化的最终判断应写成“瓶颈类型 + 证据 + 改动路径 + 预期影响”。例如:后方三角形被前方不透明三角形挡住,工具显示 fragment shader invocation 仍高,说明 early-z 没有形成有效过滤;先检查 depth write 与 shader depth/discard,再考虑 depth pre-pass。这个表达把视觉现象、管线阶段、资源状态和工具证据连在一起,比单独说“减少 overdraw”更可执行。
最小自检任务
给定一帧:先绘制一个覆盖屏幕中央的大不透明墙面三角形,再绘制一个位于墙面后方、fragment shader 很重的发光三角形。画面开启 depth buffer,深度比较使用小值更近。现在你观察到发光三角形被墙面挡住后,GPU fragment 时间仍然很高。请设计一个最小排查顺序,判断问题来自 coverage、depth state、shader 行为、blend 还是 render target 带宽。
答案要点
先固定几何输入:确认两个三角形都在 viewport 和 scissor 内,cull mode 没有把墙面剔除,墙面确实覆盖发光三角形所在区域。接着检查墙面 pass:depth test 开启,depth write 开启,compare op 与 clear value 匹配,墙面写入 depth buffer。再检查发光三角形 pass:depth test 开启,compare op 与墙面一致,shader 没有输出自定义 depth,没有大量 discard 或 alpha test 破坏提前深度过滤路径。
如果 depth buffer 中墙面深度正确,但发光三角形仍执行大量 shader,下一步检查 draw order 和 early-z 条件。把发光三角形改成最简单 shader,比较 fragment 时间变化;如果时间仍集中在输出阶段,检查 blend 是否开启、render target 是否为 HDR / MRT / MSAA、是否发生大量读改写。若关闭 blend 后时间下降,瓶颈更可能在 output merger 和 render target bandwidth。若开启 depth pre-pass 后时间下降,瓶颈更可能是被遮挡 fragment 的 shader 执行。最终结论要用工具证据支撑,例如 shader invocation、overdraw heatmap、depth attachment 内容、blend state 和 render target 格式。
本章知识点总结
- 光栅化输入:光栅化接收屏幕空间基元和管线状态,输出候选 fragment 与 sample 覆盖信息。
- 覆盖判定:边函数把三角形半平面关系转换成像素或 sample 是否命中的判断。
- 共享边规则:top-left rule 为共享边 sample 提供稳定归属,使相邻三角形边界保持一致。
- 深度测试:depth buffer 保存每个像素或 sample 的可见性深度,compare op 和 write enable 共同决定遮挡结果。
- 深度写入:不透明 pass 通常写深度,透明 pass 通常测深度但关闭深度写入并依赖排序。
- Early-Z:提前深度测试可以减少被遮挡 fragment 的 shader 执行,收益依赖深度状态和 shader 行为。
- Stencil:模板通道用整数标记控制 fragment 写入资格,适合 mask、portal、描边和局部绘制。
- MSAA:MSAA 通过多 sample coverage 改善几何边缘,无法直接解决所有纹理和 shader 高频走样。
- Coverage mask:coverage mask 记录每个像素内哪些 sample 被当前 fragment 覆盖,并影响后续测试与写入。
- API 状态:Vulkan / DX12 中的 viewport、scissor、rasterizer、depth/stencil、multisample 和 blend 状态共同定义光栅化行为。
- Blend 成本:透明混合需要读取已有颜色并写回结果,高分辨率、HDR、MRT 和 MSAA 会放大带宽压力。
- Overdraw:overdraw 让同一像素或 sample 被多次处理,性能判断要结合 shader invocation、depth 内容和 blend 状态。
- Depth pre-pass:depth pre-pass 用额外几何成本换取更少 fragment shader 执行,适合高 overdraw 和重材质场景。
- 排查顺序:光栅化问题应按 render target、viewport/scissor、coverage、depth/stencil、shader、blend 和带宽顺序定位。