跳到主要内容
腾讯题库 · Q18

原题 Q18 · 当前收录 17 / 49

学习状态 未开始

Shader 溢出调试

Shader debugging · numeric 对应课程:Shader 与材质

面试官问

PDF 第 40 页

你如何调试 Shader 中的“颜色溢出”或“数值溢出”问题?请列举至少三种工具或方法。(常问题,理清逻辑)

学习进度

0/9 已勾

只保存在这台浏览器;勾满 9 项即视为本题完成。

先把题目说成人话

玩游戏时,画面有时会突然整片发白,亮斑旁边还冒出黑点。最省事的做法是在最后把颜色压回屏幕能显示的范围,可这就像给报警灯贴胶带:症状暂时不见,上游算坏的数字仍会继续污染后面的效果。这题要解决的现实问题,是怎样沿着一连串颜色计算找到第一处出错的位置,而不是猜最后一屏为什么太亮。

让图形处理器逐点计算颜色的小程序叫着色器(Shader);允许保存“比屏幕白还亮”数值的颜色范围叫高动态范围(HDR)。把过亮区域向周围扩散成光晕的后处理叫泛光(Bloom),把高动态范围压进显示设备可呈现范围的映射叫色调映射(Tonemapping)。这里的大数可以完全合法,真正需要警惕的是“不是可用数字”(NaN)、“无限大”(Inf),或某种数值精度装不下的有限值。后面会把每一道计算当成一个可检查的工序,逐段找证据。

这是虚构教学故事,不是个人项目经历。

场景:先让问题变得看得见

目标中端移动设备(Android)的图形接口(Vulkan)运行版本(Player)中,高动态范围自发光依次经过泛光和色调映射后偶发整屏白闪。团队先固定设备、构建、相机路径、光源参数和出错帧,保存一张正常帧与一张异常帧作为基线。这一步不讨论帧率,因为问题是数值稳定性。

(右脑)画面:夜里一串舞台灯原本依次亮着柔和的颜色。忽然从中间某一盏开始,颜色被刺眼的白吞掉了;它后面的灯也一盏接一盏变白,只有前面的几盏仍然正常。站在远处,只能看见整片白光。走近以后,才发现白色不是在最后一盏灯里凭空出现的,而是从中间某处一路传了过来。

(左脑)对表:本题对象是同一异常帧中各绘制工序(Pass)的输入、输出、精度限定符和中间画面纹理(RenderTexture)格式。已观察的事实只有“固定条件下白闪”;“色调映射超范围”或“半精度(half)不足”都只是待检假设。证据必须来自逐阶段调试输出、抓帧工具(RenderDoc)中的资源与常量、以及只改变精度的一次对照。颜色值本身无物理单位,但合法范围由高动态范围约定、数据格式和运算前提决定;向量长度平方、分母和容差下限(epsilon)必须在同一数值尺度下比较。漏水管类比只帮助安排检查顺序,不等于证明故障在哪一段。

小明:先形成一个完整猜测

小明发现 Tonemap 输出超过显示范围,于是在最后加 saturate。白闪暂时没了,但 Bloom 中出现黑点;这条结果有价值:它证明显示端截断只能遮住症状,上游坏值仍会参与模糊并污染邻近像素。

小红:把猜测放回现实检查

小红保留同一异常帧和所有输入,只把观察出口沿管线逐段前移:依次把自发光输出、Bloom 输入、各级 mip 和 Tonemap 输入写成调试颜色。有限值显示灰色,超出预期范围显示红色,isnan 显示绿色,isinf 显示白色。第一处绿色出现在自发光 Pass,而不是 Bloom。

她再用 RenderDoc 抓同一帧,检查该 Draw Call 的纹理、常量和目标格式;最后把可疑计算从 half 临时改成 float。目标移动 GPU 的编译结果与调试输出表明,这条 half 路径确实采用了较低精度:接近零的长度平方被舍入成 0,随后的倒数平方根产生 Inf;换成 float 只是暂时保住了这个小数。继续回溯把“接近零的视角向量仍被归一化”和“结果随后参与除法”缩成两个相邻候选,但这一步还没有用拆分干预区分零长度分支与分母下限各自的因果贡献。

我:用检查结果修正模型

我在源头处理退化输入:长度平方小于 epsilon 时走明确的备用方向,除法前限制分母的绝对值下限;在 acos 等定义域明确的运算入口做合法范围保护,并在最终显示边界保留输出 clamp。每一道保护都要对应已声明的数学域,不能拿统一截断洗掉未知坏值。然后恢复原有 half 路径,用零长度向量、极亮 HDR、负值和快速运动相机逐组复测,并连续检查异常标记。

复测没有再产生 NaN/Inf,合法 HDR 高光仍能进入 Bloom。这个结论只覆盖当前 Android/Vulkan 设备与编译路径;桌面平台常把 half 按 32 位处理,其他移动 GPU 的精度行为也可能不同,因此还要在项目支持的图形 API、设备和 RenderTexture 格式上重复同一组退化输入。

(右脑)画面修正:再看那串灯,小明只是给最后一盏罩上了灯罩,远处不再刺眼,中间那盏却仍在把异常一路传下去。小红从前往后逐盏查看,终于找到第一盏失去颜色的灯。修好它以后,后面的灯自然恢复;最后的灯罩仍可以留下,但它只是防止意外再次刺眼,不是这次故障的答案。

(左脑)严格对表:排错顺序是:固定异常帧与平台 → 为每个阶段标记有限值、预期越界、NaN、Inf → 找到第一处坏值 → 用抓帧核对该 Draw Call 的输入、常量和目标格式 → 只改变 half/float 验证精度是否参与 → 从原始异常版本分别只启用零长度分支、分母下限和末端 clamp,记录哪一步使第一处 Inf 消失 → 再联合防护并恢复目标精度,用零向量、极亮 HDR、负值和运动镜头回归。只有单变量结果与联合回归一致时才归因;验收还要求合法 HDR 未被误截断,并在各目标设备与格式重复通过。

老师解惑

  • 中间值可视化:回答“坏值第一次在哪个阶段出现”,比只看最终颜色可靠。
  • RenderDoc 抓帧:核对具体 Draw Call、资源、常量、Render Target 和逐阶段输出;它提供一帧的证据。
  • 精度替换实验:把 half 暂换成 float 是定位手段。若症状改变,说明精度参与了问题,但不能直接把“全改 float”当根治。
  • 数值防护:常见源头包括除零、零向量归一化、负数开方、越界 acos、未初始化值和格式溢出。
  • clamp 的边界:它适合保护明确的输出域,不能洗掉上游 NaN/Inf 后假装问题已解决。

工具分工

  • Shader 调试输出:逐 Pass 标记有限值、越界、NaN 和 Inf。
  • RenderDoc:检查异常帧里第一个产生坏值的 Draw Call 与中间纹理。
  • Unity Frame Debugger:确认 Pass 顺序、目标缓冲和“这一帧画了什么”;它不负责给每个 Pass 做 GPU 计时。
  • 目标设备回归用例:用退化向量、极端 HDR 和不同精度证明修复不是只对一个镜头有效。

记忆钩子

先找第一处坏值,再修产生坏值的运算;最终 clamp 是护栏,不是诊断。

这是教学场景,不是目标项目的真实捕获或个人经历。

下一步:验证这条因果

先定位第一个坏中间量,再判断是修操作数、显式 clamp,还是检查目标格式。

教学边界:它只验证这一条画面因果,不替代完整答案、项目经历或目标设备数据。

进入教学 Lab:「Shader 数值故障:先找到第一处坏算术」