Skip to main content

Chapter 23: Fragment and Pixel Shaders

Fragment Shader 和 Pixel Shader 是光栅化之后决定屏幕样本颜色的可编程阶段。OpenGL、Vulkan、WebGPU 常用 Fragment Shader 这个名字,Direct3D / HLSL 常用 Pixel Shader 这个名字;本章把二者放在同一条管线位置讨论:一个三角形经过顶点处理、裁剪、透视除法和光栅化后,覆盖了哪些屏幕采样点,每个采样点如何读取插值属性、纹理、材质参数和深度状态,最后如何写入一个或多个 render target。

读完本章后,读者应能追踪一个材质像素从三角形覆盖到颜色附件写入的路径,判断 Fragment Shader 的执行条件,区分颜色计算、纹理采样、光照组合、discard、early depth、late depth 和 MRT 各自处在什么位置,并能根据画面异常或帧耗时症状设计排查顺序。

本章贯穿一个具体 frame:场景中有一个带 normal map 的金属球,材质使用 base color、roughness、metallic、normal、emissive 五类输入;渲染路径先执行深度相关测试,再在 Fragment Shader 中采样纹理、构造法线、计算直接光,最后写入 HDR color attachment 或 G-buffer 的多个 attachment。这个 frame 足够覆盖本章的主要判断:fragment 从哪里来,什么时候被执行,哪些输入会增加成本,哪些输出会增加带宽,哪些状态会改变最终像素。

官方规范对阶段位置给出了基础边界。Vulkan Shaders 规范把 fragment shader 放在 rasterization 之后,并说明一次 fragment shader invocation 处理一个 fragment;Vulkan Fragment Operations 规范把 fragment operations 放在 shader 输出进入 framebuffer 之前;Microsoft HLSL semantics 文档规定 Pixel Shader 通过 SV_Target[n] 写入 render target,通过 SV_Depth 写入深度;WGSL 2026-06-05 Candidate Recommendation Draft使用 @fragment 标注 fragment entry point。工程判断要从这些固定边界出发,再结合当前 API、pipeline state、attachment format 和 GPU 工具证据落地。

下面这条路径是本章的主线。它描述的是一个 draw call 内部从几何覆盖到 framebuffer 写入的最小可观察链条。

图中的每个节点都对应一个可调试对象。覆盖和插值来自 rasterization;shader 输入来自上游 stage、descriptor、texture 和 sampler;深度、模板、混合和附件格式来自 pipeline state;最终图像来自 attachment 写入。Fragment Shader 的排查顺序要沿这条链推进,先确认 fragment 是否产生,再确认 shader 是否执行,再确认输出是否被后续 fragment operations 接收。

23.1 Fragment Shader 的产生与执行条件

Fragment Shader 的产生条件由光栅化阶段决定。顶点着色器输出裁剪空间坐标,固定功能阶段完成裁剪、透视除法、viewport transform 和三角形覆盖测试;当一个 primitive 覆盖某个屏幕采样点时,管线会形成 fragment,并为它准备插值后的 varyings、屏幕坐标、front-facing 标记、sample coverage 和深度值。Fragment Shader 并非按顶点执行,也并非按 mesh 面片执行;它的基本粒度是光栅化产生的 fragment,MSAA 或 per-sample shading 启用后还会涉及 sample 级 coverage 和可能的 sample 级执行。

在贯穿 frame 中,金属球网格的一个三角形覆盖了屏幕上的一片像素区域。每个 covered sample 都携带从三角形三个顶点插值得到的 UV、world normal、world position 或 view vector。Fragment Shader 读取这些输入后,才有条件采样 base color texture 和 normal map。若三角形经过背面剔除、视口裁剪、scissor 或 sample mask 后没有覆盖当前采样点,shader 入口函数没有对应 invocation,后面的材质计算也没有发生。

Fragment Shader 的执行还受 fragment operations 影响。深度测试、模板测试、sample mask、coverage 和 discard 都会改变 fragment 是否继续向 framebuffer 写入。工程上常见的 early depth 指 GPU 在 shader 主体之前完成可提前判断的深度或模板测试,用于减少被遮挡 fragment 的 shader 工作;late depth 指 shader 执行之后再完成深度或模板相关判断,常见于 shader 写深度、使用 discard、写 storage resource 或存在其他副作用时。具体时机由 API 规则、pipeline state、shader 声明和驱动实现共同决定,不能把某一帧工具中看到的行为推成所有平台规则。

贯穿 frame 中,如果金属球前方有一面已经写入深度的墙,且球的 Fragment Shader 只是输出颜色、没有写 gl_FragDepth / SV_Depth、没有使用会影响覆盖的 discard,GPU 有机会在 shader 主体之前剔除被墙遮住的 fragment。这样 texture fetch、normal map unpack、光照计算都被减少。若 shader 根据 alpha texture 执行 discard,硬件通常需要先运行 shader 才知道 fragment 是否保留,early depth 的空间会缩小,overdraw 区域的纹理采样成本会显著上升。

helper invocation 是理解 fragment 执行条件时必须补齐的细节。纹理自动 mip 选择和 dFdxdFdy 这类导数操作需要相邻 fragment 形成导数组,GPU 可能为没有真实覆盖的屏幕位置启动辅助 invocation 来计算导数。辅助 invocation 可以参与导数计算,但它不应被当成最终 framebuffer 写入。调试 normal map 采样或屏幕空间效果时,边缘处的导数异常、discard 后的 mip 抖动和 quad 边界伪影都可能和这类执行细节有关。

下面的简化 GLSL 片段展示了 alpha discard 对 fragment 路径的影响。代码的目的不是完整材质实现,而是把“采样 alpha 后决定覆盖”这个判断点显式放出来。

layout(location = 0) in vec2 v_uv;
layout(location = 0) out vec4 out_color;

layout(binding = 0) uniform sampler2D base_color_tex;

void main() {
vec4 base = texture(base_color_tex, v_uv);
if (base.a < 0.35) {
discard;
}
out_color = vec4(base.rgb, 1.0);
}

这段代码把覆盖判断推迟到了 texture fetch 之后。对于树叶、铁丝网、毛发卡片这类 alpha cutout 材质,这种写法能表达形状,但会让被遮挡或被裁掉的 fragment 仍然消耗部分 shader 和纹理采样成本。优化时要先确认材质确实需要 alpha cutout,再考虑 alpha-to-coverage、深度预写 pass、材质分桶或几何替代方案。

Fragment Shader 的调试入口也要按执行条件排序。画面完全缺失时,先检查 draw call 是否提交、primitive 是否被裁剪、viewport/scissor 是否覆盖目标区域、depth/stencil 状态是否阻断写入;画面颜色异常时,再检查 varying 插值、texture binding、sampler、color space 和输出 attachment。把所有问题直接归因到 shader 公式,会跳过大量固定功能阶段和 API 状态证据。

23.2 颜色计算、纹理采样与光照模型融合

Fragment Shader 的核心任务是把材质输入、几何方向、光照信息和颜色空间规则组合成输出颜色。材质输入通常来自 uniform / constant buffer、texture、sampler、push constant 或 bind group;几何方向来自上游插值的 normal、tangent、view vector、world position;光照信息来自 light buffer、shadow map、环境贴图或前一阶段生成的 lighting texture。输出颜色进入 color attachment 后,还可能经过 blending、format conversion、tone mapping 或后处理。

贯穿 frame 中,金属球 fragment 的最小输入集包括 UV、TBN 矩阵、view direction、light direction、baseColor、roughness、metallic 和 normal map。颜色计算可以写成一条工程链:先采样 base color,按 sRGB 规则转换到 linear space;再采样 normal map,把切线空间法线变换到 world 或 view space;随后根据光照方向计算 diffuse 和 specular;最后把 HDR 颜色写入 floating-point color attachment。这个顺序的价值在于可逐项 debug,每一步都有可观察输出。

layout(location = 0) in vec2 v_uv;
layout(location = 1) in mat3 v_tbn;
layout(location = 2) in vec3 v_world_pos;
layout(location = 3) in vec3 v_world_normal;
layout(location = 0) out vec4 out_hdr_color;

layout(binding = 0) uniform sampler2D base_color_tex;
layout(binding = 1) uniform sampler2D normal_tex;

layout(binding = 2) uniform LightData {
vec3 light_dir;
vec3 light_color;
vec3 camera_pos;
} light_data;

void main() {
vec3 base_color = texture(base_color_tex, v_uv).rgb;
vec3 normal_sample = texture(normal_tex, v_uv).xyz * 2.0 - 1.0;
vec3 normal_ws = normalize(v_tbn * normal_sample);
vec3 view_dir = normalize(light_data.camera_pos - v_world_pos);
vec3 light_dir = normalize(-light_data.light_dir);

float ndotl = max(dot(normal_ws, light_dir), 0.0);
vec3 diffuse = base_color * ndotl * light_data.light_color;

float rim = pow(max(1.0 - dot(normal_ws, view_dir), 0.0), 4.0);
vec3 color = diffuse + rim * 0.08;
out_hdr_color = vec4(color, 1.0);
}

这段代码覆盖了 Fragment Shader 中最常见的三类数据路径。第一类是纹理采样路径,base_color_texnormal_tex 通过 descriptor / binding 进入 shader,采样坐标来自 v_uv,采样结果受 sampler filter、mip、wrap mode 和纹理格式影响。第二类是几何方向路径,v_tbnv_world_posv_world_normal 来自上游插值,精度、归一化和坐标空间一致性会直接影响光照方向。第三类是输出路径,out_hdr_color 写入 HDR attachment,后续 tone mapping 才负责把线性 HDR 值映射到显示空间。

颜色空间是颜色异常的高频来源。Base color 贴图通常以 sRGB 方式存储,采样后需要在计算前进入 linear space;normal、roughness、metallic、AO 这类数据贴图通常按线性数据使用。若把 normal map 当作 sRGB 解码,法线方向会被扭曲;若把 base color 当作线性数据直接用于光照,漫反射会显得灰暗或过亮。排查时可以分别输出 base_colornormal_ws * 0.5 + 0.5ndotl 和最终 HDR 值,把材质采样、空间转换和光照组合拆开观察。

光照模型融合需要控制边界。本章只使用简化的 diffuse 与 rim 项说明 Fragment Shader 的数据流;更完整的 BRDF、Fresnel、microfacet、IBL 和阴影将在后续光照与材质章节展开。当前阶段的关键判断是:Fragment Shader 并不天然知道“金属”“粗糙度”“高光”这些语义,它只接收数值输入并执行公式。材质系统要负责把 texture slot、scalar 参数和 shader variant 对齐,lighting pass 要负责提供光照输入,pipeline state 要负责保证输出 attachment 支持目标精度。

纹理采样还会牵动 mip 选择。GPU 通常根据屏幕空间导数估算一个 fragment 对纹理空间的 footprint,再选择合适 mip。UV 缩放、三角形大小、相机距离、anisotropic filtering 和动态分支都会影响采样质量。若金属球远处出现闪烁,优先检查 mip chain 是否生成、sampler 的 min/mag/mip filter 是否正确、normal map 是否使用合适压缩格式、roughness mip 是否在资产管线中被校验。

颜色计算的可靠排查顺序可以固定为五步。第一步输出纯常量颜色,确认 fragment 执行和 attachment 写入路径正常。第二步输出 UV 或 normal debug color,确认插值和空间转换。第三步只采样 base color,确认 texture binding、format 和 sampler。第四步加入 normal map,确认 TBN 与 normal unpack。第五步加入光照和颜色空间处理,确认 BRDF 输入、shadow term、HDR 范围和 tone mapping。这个顺序把 shader 问题从“公式看起来错”改成“哪条输入路径开始失真”。

23.3 降低分支和内存访问开销

Fragment Shader 的性能通常受三个对象约束:执行次数、每次执行的采样次数、每次执行的控制流复杂度。执行次数由屏幕覆盖面积、overdraw、MSAA、分辨率和 early depth 机会决定;采样次数由材质纹理数量、shadow map、IBL、noise 和后处理输入决定;控制流复杂度由材质分支、light loop、alpha test、feature toggle 和动态索引决定。优化要先定位当前瓶颈属于哪一类,再选择对应动作。

贯穿 frame 中,金属球如果占据屏幕 40% 面积,它的 fragment 数量会远高于顶点数量。一个顶点 shader 多做几次矩阵乘法可能影响有限,而一个 Fragment Shader 每个像素多采样两张 4K 纹理会直接推高纹理带宽、cache miss 和 shader latency。RenderDoc 可以检查 draw call 的绑定资源和 attachment,Nsight 或 Xcode GPU tools 可以观察 texture throughput、shader occupancy、fragment workload、overdraw 和 bandwidth 相关指标;工具名称只是入口,关键是看它回答的是“执行太多”“采样太多”还是“输出太宽”。

降低分支开销的第一条原则是把分支粒度拉到 draw call 或 material variant 层级。若一个材质始终没有 normal map,最佳路径是使用无 normal map 的 shader variant 或 uniform branch,让同一个 draw 内的 fragment 走一致路径。若同一个 draw 中每个 fragment 根据纹理值决定执行完全不同的 light loop,GPU wave / warp 内的 lane 会出现分歧,两个路径都可能占用执行时间。数据并行阶段的分支成本取决于同组 invocation 的一致性,和 CPU 上单个线程的分支成本模型差异很大。

下面的写法把 feature 开关放在 uniform 层级,适合材质级能力选择。它仍然保留分支,但同一个 draw 中所有 fragment 的判断结果一致,执行路径更容易被编译器和驱动处理。

Texture2D normalTex : register(t0);
SamplerState linearSampler : register(s0);

cbuffer MaterialFlags : register(b0) {
bool useNormalMap;
};

float3 LoadNormal(float2 uv, float3 vertexNormal, float3x3 tbn) {
if (useNormalMap) {
float3 n = normalTex.Sample(linearSampler, uv).xyz * 2.0 - 1.0;
return normalize(mul(n, tbn));
}
return normalize(vertexNormal);
}

降低内存访问开销的第一条原则是减少无效采样和无效带宽。纹理通道打包可以把 roughness、metallic、AO、mask 合并到一张 RGBA texture,减少 descriptor 数量和采样次数;材质分级可以让低重要性物体使用较少纹理;LOD texture 或 lower-resolution mask 可以降低远处物体成本;采样复用可以把相同 UV、相同 sampler、相同 mip 的结果保留在局部变量中,多次参与公式。

Texture packing 的收益来自访问路径变短,但它会改变资产管线和格式选择。Roughness 适合高精度平滑变化,metallic 常接近二值,AO 对低频变化更敏感;三者放入同一张 texture 后,压缩格式、mip 生成和通道 swizzle 都要统一管理。若使用 BCn、ASTC 或 ETC 系列压缩格式,还要按平台支持和通道相关性评估画质。优化评估要同时看采样次数下降、压缩块伪影、mip 泄漏和材质作者工作流。

Branchless math 适合处理连续插值和小型条件。比如使用 mix / lerpstepsaturate 可以把一些二选一颜色混合写成数据表达,使同组 fragment 保持统一指令流。它适用于阈值柔化、mask 混合和简单权重计算;对大型材质功能开关、复杂 light loop 或需要跳过昂贵 texture fetch 的路径,branchless 写法可能把原本可跳过的工作全部执行,导致成本上升。判断时要把 ALU、texture、带宽和分支一致性放在同一张成本表里。

动态 light loop 是 Fragment Shader 里的高风险结构。若每个 fragment 遍历大量光源并执行阴影采样,成本会随屏幕面积快速增长。常见做法是把光源筛选前移到 CPU、cluster、tile 或 compute pass,Fragment Shader 只遍历当前 tile / cluster 的短列表。此处的核心优化动作是减少每个 fragment 的输入集合,而非单纯改写 for 循环语法。真正改变的是 light list buffer、cluster index、descriptor binding 和 fragment 内部访问次数。

排查性能时可以使用四个证据点。第一,看 overdraw 或 fragment count,判断 shader 执行次数。第二,看 texture cache、sampler、memory bandwidth,判断采样路径。第三,看 shader instruction、register、occupancy,判断 ALU 和寄存器压力。第四,看 color/depth attachment bandwidth,判断输出路径。只有证据指向 Fragment Shader 成本时,再进入 shader 代码级优化;若瓶颈来自 CPU draw submission、vertex processing 或 post-processing,修改 Fragment Shader 收益有限。

23.4 高精度计算与多渲染目标(MRT)处理

高精度计算和 MRT 改变的是 Fragment Shader 的输出合同。单一 forward pass 通常把最终颜色写入一个 color attachment;deferred shading 的 G-buffer pass 会把 albedo、normal、roughness、metallic、emissive、motion vector、object id 等信息写入多个 attachment。每增加一个 attachment,就增加一次格式转换、带宽写入、可能的 blending 限制和显存占用。MRT 的收益是后续 lighting pass 可以复用几何和材质信息;代价是 G-buffer pass 的写入更宽,移动 GPU 和带宽受限平台尤其敏感。

在 HLSL 中,Pixel Shader 可以通过 SV_Target[n] 写入多个 render target;在 GLSL 中常见写法是多个 layout(location = n) out 输出;在 WGSL 中通过不同 @location(n) 输出字段表达。语法不同,工程含义一致:shader 输出位置必须和 pipeline / render pass / framebuffer 中的 attachment 顺序、格式和 blend state 对齐。

下面的 HLSL 片段展示一个简化 G-buffer 输出。代码目的在于说明 MRT 输出合同,而非完整 deferred renderer。

struct PixelOut {
float4 gbuffer0 : SV_Target0;
float4 gbuffer1 : SV_Target1;
float4 gbuffer2 : SV_Target2;
};

PixelOut main(PixelIn input) {
PixelOut output;
float3 baseColor = SampleBaseColor(input.uv);
float3 normalVS = EncodeReadyNormal(input.uv, input.tbn);
float roughness = SampleRoughness(input.uv);
float metallic = SampleMetallic(input.uv);

output.gbuffer0 = float4(baseColor, metallic);
output.gbuffer1 = float4(normalVS * 0.5 + 0.5, roughness);
output.gbuffer2 = float4(input.motionVector, 0.0, 1.0);
return output;
}

这段代码把同一个 fragment 的结果拆到三个 attachment。gbuffer0 承担颜色和 metallic,gbuffer1 承担 view-space normal 和 roughness,gbuffer2 承担 motion vector。后续 lighting pass 再读取这些 attachment 重建材质和几何信息。这里的关键边界是:G-buffer 存储的是后续 pass 所需的中间数据,它会牺牲写入带宽换取光照阶段的复用能力。

格式精度直接影响画面稳定性。HDR color attachment 常使用 RGBA16F 或类似浮点格式,以承载超过 1.0 的线性光照值;base color 可以使用 RGBA8 sRGB 格式;normal 可以用 RG16FRGB10A2、octahedral encoding 或 view-space two-channel encoding;roughness、metallic、AO 可打包到 8-bit 通道。选择格式时要同时考虑数值范围、量化误差、blend 支持、采样成本、压缩支持和平台能力。

Normal / depth encoding 是 MRT 中最容易被低估的部分。直接写 world-space normal 需要三个通道,且插值、压缩和量化会改变长度;view-space normal 可以降低后续 lighting 重建成本;octahedral encoding 可以用两个通道保存单位向量,但需要编码和解码 ALU。Depth 既可以来自硬件 depth attachment,也可以在线性化后写入 color attachment。硬件 depth 适合测试和层级 Z,线性 depth 适合屏幕空间效果读取。二者可以并存,但要明确哪个 pass 使用哪个深度语义。

Blending 与 MRT 的关系也要提前设计。透明材质、粒子和 UI 往往需要 blending;G-buffer pass 通常要求每个 fragment 写入确定材质数据,透明对象则会被放入 forward transparent pass 或单独的 order-independent transparency 路径。若在多个 MRT 上启用复杂 blending,带宽和格式支持都会收紧。工程上常把 opaque deferred、alpha cutout、transparent forward 分成不同 pass,降低单个 Fragment Shader 需要同时承担的输出合同。

高精度计算还涉及中间变量精度。移动平台和 WebGPU / Metal / GLSL ES 环境中,halfmediumpfloat 的代价和精度边界可能不同。颜色累积、法线归一化、视线方向、roughness remap 和 specular 计算对精度比较敏感;UV、mask、简单权重或低频参数可以使用较低精度。精度优化要用画面差异和 profiler 数据共同验证,先保证 banding、normal 闪烁、HDR clipping 和深度冲突可控,再压缩格式和变量精度。

MRT 的排查顺序应从 attachment 合同开始。第一,检查 render pass / framebuffer 中 attachment 数量、顺序、格式和 load/store 操作。第二,检查 shader 输出 location / semantic 是否和 attachment 对齐。第三,检查 pipeline blend state、color write mask、depth state 和 layout transition。第四,逐个 attachment 输出 debug color,确认每个通道的语义和范围。第五,在后续 lighting pass 中单独读取每个 G-buffer attachment,确认采样坐标、解码公式和颜色空间一致。

回到贯穿 frame,金属球使用 forward HDR 路径时,Fragment Shader 的目标是一次性输出最终线性 HDR 颜色;使用 deferred 路径时,Fragment Shader 的目标是输出可重建材质和几何状态的 G-buffer。前者更直接,透明和材质特殊项处理更自然;后者适合大量灯光和屏幕空间复用,但写入带宽和格式设计更重。选择哪条路径,要看当前 frame 的主成本来自光照循环、材质复杂度、透明物体、带宽,还是后处理复用。

最小自检任务

给定一个 draw call:它渲染一片带 alpha cutout 的树叶材质,Fragment Shader 采样 base color、normal、roughness-metallic-AO 打包图和 shadow map;shader 中根据 base color 的 alpha 执行 discard;渲染路径使用 depth pre-pass,主 pass 写入三个 G-buffer attachment。请按管线顺序判断:哪些条件决定 Fragment Shader 是否执行,哪些输入最可能造成采样成本,哪些输出最可能造成带宽成本,以及排查画面缺失时先看哪些状态。

答案要点

Fragment Shader 的执行条件先由几何覆盖决定。draw call 需要提交成功,primitive 需要通过裁剪、viewport、scissor、背面剔除和 rasterization,目标采样点需要形成 coverage。Depth pre-pass 可以让主 pass 的被遮挡 fragment 在 shader 前被深度测试过滤,但 alpha cutout 的 discard 需要读取 alpha 后才能决定覆盖,主 pass 的 early depth 空间会受到 shader 行为、depth state 和 API 实现影响。

采样成本主要来自四类 texture fetch:base color 同时提供颜色和 alpha cutout 判断,normal map 需要解包并参与 TBN 变换,roughness-metallic-AO 打包图提供材质参数,shadow map 可能包含比较采样或 PCF 多次采样。若树叶覆盖屏幕面积大或 overdraw 高,这些采样会随 fragment 数量放大。优先观察 overdraw、texture throughput、sampler 状态、mip 选择和 cache 行为。

带宽成本主要来自三个 G-buffer attachment 的写入。每个 surviving fragment 都要写多个 render target,attachment format 越宽,store 成本越高。若 G-buffer 同时写 normal、material、motion 或其他 mask,要检查 format 精度、color write mask、blend state、load/store 操作和后续 lighting pass 的读取需求。

画面缺失的排查顺序应从 shader 外部开始:先检查 draw call、index / vertex buffer、pipeline、viewport、scissor、cull mode、depth / stencil state 和 framebuffer attachment;再输出常量颜色确认 Fragment Shader 与 color attachment;随后检查 base color alpha 和 discard 阈值;最后检查 texture binding、sampler、G-buffer 输出 location / semantic、attachment 格式和 render pass layout。这个顺序能区分“没有 fragment”“fragment 被测试丢弃”“shader 采样异常”和“输出没有写入目标”。

本章知识点总结

  • 阶段位置:Fragment Shader 位于光栅化之后,负责处理 rasterization 产生的 fragment,并把结果交给后续 fragment operations。
  • 命名边界:Fragment Shader 和 Pixel Shader 描述同一类可编程阶段在不同 API 生态中的命名差异。
  • 执行条件:一个 invocation 产生前需要 primitive 覆盖屏幕采样点,并通过 viewport、scissor、coverage、depth 和 stencil 等状态约束。
  • Early depth:可提前判断的深度或模板测试能减少 shader 工作,写深度、discard 和副作用会压缩这个优化空间。
  • Helper invocation:导数和 mip 选择可能需要辅助 invocation,它们服务采样计算,最终 framebuffer 写入仍受真实覆盖和测试结果控制。
  • 颜色路径:材质颜色来自 texture、uniform、varying、光照输入和颜色空间转换的组合,调试时应逐步输出中间量。
  • 采样边界:Texture fetch 成本受采样次数、mip、filter、压缩格式、cache locality 和屏幕覆盖面积共同影响。
  • 分支成本:Fragment Shader 中的分支要看同组 invocation 的一致性,材质级 uniform branch 通常比像素级复杂分歧更稳定。
  • 打包取舍:Texture packing 可以减少采样和 descriptor 压力,同时要求资产管线统一处理通道语义、压缩格式和 mip 质量。
  • MRT 合同:多渲染目标要求 shader 输出位置、attachment 顺序、格式、blend state 和后续 pass 解码语义全部对齐。
  • 精度设计:HDR、normal、depth 和材质参数需要按数值范围、量化误差、带宽和平台支持选择格式。
  • 排查顺序:Fragment 问题应按覆盖、执行、采样、计算、fragment operations、attachment 写入的顺序定位。