第 34 章 动手三:渲染现象实验室——看到 bug 就能反推原理

前面第 26、28、29、30 章,我们一笔一划把四块最折磨人的图形学现象扒了底朝天:z-fighting 是深度精度近密远疏惹的祸,Mipmap 闪烁是远处纹素被稀疏像素错误采样,阴影 acne 与 Peter Panning 是 shadow map 的 bias 两难,画面发灰是颜色空间没设对。你读完那一刻,大概率会有一种”我懂了”的爽感——但我想泼你一盆冷水,也是替你压着的一句实话:

在纸上、在屏幕前”看懂原理”,和在你自己项目里”看到现象三秒反推出是哪个旋钮没设对”,中间隔着一条很宽的河。这条河,只有你亲手把现象造出来、再亲手拨旋钮修好它,才能真正趟过去。

你是个写 Go 后端的老手,对”看懂设计模式”和”线上出 bug 能秒定位”是两码事,体会应该最深。你看懂了限流算法不等于你能从监控曲线反推是哪个服务把令牌耗光了。图形学也一样。这一章,我就是你的”实验台”——我带你搭四个”最小复现实验”,每个实验只动最少的参数,让你在 Unity 里亲眼看到异常、亲手把它修好。从此以后,别人截图发你一句”为啥我地面在闪”,你脑子里不是一片空白,而是立刻浮现”这是 Mipmap 没开,去查贴图导入设置”——这就是我们要练成的肌肉记忆。

我先把全章的钥匙交给你:

读原理是”知道世界长这样”,做实验是”亲手确认世界真的长这样”。只有后者,才能把知识变成排查真实 bug 的直觉。 这一章不教新原理,只教你怎么用实验把旧原理”焊”进你的肌肉里。

34.1 为什么这章存在:从”看懂”到”会查”之间还差一次动手

我先问你一句:第 26 章讲 z-fighting 时,我说”near/far 比值过大会让远处精度被摊薄”。你当时点头了。可如果明天你打开自己的 Unity 工程,看到地面和路面白线在交界处疯狂闪烁,你第一反应是去翻第 26 章,还是先在材质面板乱点一通?

我赌大部分人是后者。为什么?因为”原理”还停在你的”短期记忆”和”理性理解”里,它没和”屏幕上那个具体的闪烁画面”建立神经连接。真正能救命的连接,是这种:

  • 你亲眼见过:把 Camera 的 Far 从 1000 拉到 100000,闪烁立刻爆发。
  • 你亲手拨过:把 Near 从 0.3 调到 1,闪烁消失。
  • 你的身体记住了:那种像素级打架的闪 = near/far 比值 = 调 near 调 far。

这三步,文字永远给不了你,只有实验能。这就像你学数据库索引,看书说”B+ 树能加速查询”,不如你亲手 EXPLAIN 一条慢 SQL、看它从全表扫描变成 index scan 来得刻骨铭心。

所以这一章的定位,是前面第 26/28/29/30 章的”实训场”,不是新内容。我们做四件事:

  1. 给你一个能在一分钟内搭好的最小场景。
  2. 告诉你拨哪个旋钮、往哪个方向拨,能肉眼看到现象出现。
  3. 告诉你再拨哪个旋钮,现象消失——并让你同步念出”这对应第 X 章哪个原理”。
  4. 配一小段 C# 思路代码,让你知道怎么在运行时量化地”看”到这些参数,而不是纯靠肉眼猜。

而且别忘了,你前面已经亲手写过两样东西了:第 32 章你写过坐标变换的 demo,第 33 章你写过雾的 shader。你已经从一个”只会看概念”的人,变成”能写 demo 验证概念”的人。这一章,是把这条线的最后一截补全——从”能写 demo”走到”能排查 bug”。

反问你一句:你回想一下自己学 Go 的经历,是先把《Go 语言实战》翻完才敢写接口,还是写到第三个线上服务、被一次死锁坑过之后,才真正”懂”了 channel?图形学排查能力,也是被坑出来的。本章就是我帮你”安全地把四个坑预先踩一遍”,等你真在线上踩到时,已经是熟人了。

34.2 实验一·z-fighting:把两面逼到几乎共面,看它们”打架”

我们第一个实验,复现第 26 章的 z-fighting。目标是:你搭一个场景,亲眼看到”两个几乎贴在一起的面在像素级闪烁穿插”,然后亲手调 near/far 把它治好。

34.2.1 搭最小场景(三分钟)

你在 Unity 里新建一个空场景,做三件事:

  1. 放一个 Plane(地面),默认躺在 y=0。
  2. 再放一个 Plane(路面标线/贴花),让它和地面几乎共面——比如把它挪到 y=0.001(只高 1 毫米)。给它一个明显不同的颜色或贴图,方便你看出”谁在上”。
  3. 放一台 Main Camera,默认 Near=0.3、Far=1000,稍微斜着俯看这两块面。

你现在运行,从某些角度看,两块面交界、尤其是远处那块,开始像素级闪烁:这一帧标线在上,下一帧地面在上,像两块玻璃在抢”谁更近”。这就是 z-fighting。

为什么偏偏是”远处”闪、近处不闪?连回第 26 章 26.4 的伏笔:深度缓冲的精度分布是近密远疏的。两面在 y 上只差 0.001,近处这段”尺子刻度”够密,能分出谁近谁远;远处那段刻度极疏,0.001 的差距在精度里被舍入成同一个深度值,于是深度测试反复横跳——闪。

34.2.2 制造极端:把 Far 拉到极大

现在你亲手把”比值过大”这个成因演出来。选中 Camera,把 Far 从 1000 改成 100000(甚至更大),Near 保持 0.3 不动。重新运行。

你观察到了什么?闪得更凶了,而且蔓延到更近的地方。这正是第 26.5 讲的”near/far 比值过大,有限精度被摊薄”。Far 一拉大,深度缓冲那把”尺子”要量的范围从 1000 暴涨到 100000,刻度被摊得像一层纱,本来能分清的近处也快分不清了。

这里有个反直觉点,我请你亲手验证:把 Near 从 0.3 调到 1.0(推远近裁剪面),Far 保持 100000 不变,重新看。你会发现——远处闪烁反而好转了。是不是违背直觉?近裁剪面推远,按说”近处更粗”,可远处却变好。原因正是深度精度近密远疏:把 Near 推远,等于把”密的那些刻度”让出来给中间和远处用,整体远处可用精度反而提升。这是面试常考的反直觉点,你亲眼验证一次,一辈子忘不了。

34.2.3 修复:三选一,理解各自的代价

治好它,你有几把扳手(对应第 26.5 的清单),逐一试:

  1. 调大 Near、缩小 Far:把 Near 设回 0.3~1,Far 设成你场景真正需要的距离(比如小房间 200)。最干净,不动几何体。代价:比 Near 更近的东西(贴脸的枪管)会被裁掉——这就是第 26.6 说的 FPS 双摄像机 trick 的由来。
  2. Polygon Offset(多边形偏移):给”应该在上层”的材质加一点深度偏移,让它在深度比较时”假装有那么一点点更近”,人为拉开两面的深度差。代价:偏移太大物体会肉眼”浮”或”陷”。
  3. 贴花专用:直接把标线 Plane 沿法线”浮”离地面一点(比如 0.005),制造人为深度差。最可控。

你试完三把,会建立一条排查纪律:看到两面闪烁穿插 → 先确认是不是共面/随角度闪(排除贴图问题)→ 优先调 near/far → 不行再 Polygon Offset 或抬标高。

34.2.4 一段 C# 思路:在运行时”看”到深度量程

肉眼看闪还不够,工程师要能”量化”。我给你一段在运行时打印 Camera 深度量程和比值的代码,让你把”现象”对应到”数字”:

using UnityEngine;
 
// 挂到任意物体上,运行时在 Console 看 Camera 的深度量程与 near/far 比值
// 需在 Unity 工程中验证
public class DepthRangeProbe : MonoBehaviour
{
    void Update()
    {
        Camera cam = Camera.main;
        if (cam == null) return;
 
        float near = cam.nearClipPlane;   // 近裁剪面,世界单位(米)
        float far  = cam.farClipPlane;    // 远裁剪面,世界单位(米)
        float ratio = far / near;         // near/far 比值,过大是 z-fighting 温床
 
        // 把每帧的量程与比值打到控制台,观察你拨旋钮时数字怎么变
        Debug.Log($"[DepthRange] near={near} far={far} ratio={ratio:F1} " +
                  $"{(ratio > 5000 ? "⚠ 比值偏大,z-fighting 风险高" : "比值安全")}");
 
        // 面试官爱问的反直觉验证:比值不变时,near 推大,远处可用精度提升
        // 你可以在编辑器里拖 Near/Far,看 ratio 数字,再对比画面闪烁强度
    }
}

这段代码的用意不在”跑出什么结果”,而在训练你一种习惯:排查渲染现象时,把相关参数实时打印出来,用数字佐证你的肉眼判断。你拨 Far 从 1000 到 100000,Console 里 ratio 从 3333 跳到 333333,你立刻知道”哦,这就是爆炸点”。比纯靠眼睛猜专业得多。

自检:如果你同事发你一张截图说”地面和路面标线在闪”,你按本节清单,第一句会问他什么?(提示:先确认是不是两面几乎共面、且闪烁随视角变化——这两点成立,才锁定 z-fighting,否则可能是别的事。)

34.3 实验二·Mipmap 闪烁:远处地面开/关 Mipmap 对比

第二个实验,复现第 28 章的纹理高频走样。目标:你亲眼看到”远处地面在抖雪花”,并确认它就是 Mipmap 没开,然后亲手开 Mipmap + 提 Anisotropic 治好它。

34.3.1 搭最小场景

  1. 放一块大 Plane 当地面,给它一张细节很密的贴图(草地、砖墙、棋盘格都行——越密越好,因为密纹才惹事)。
  2. 把这张贴图导入设置里的 Generate Mip Maps 先关掉(我们故意制造病)。
  3. Camera 放在地面一端,贴着地面往前看,让远处那片地在屏幕上被压成薄薄一条(斜看,越远越扁)。

运行。你往地平线方向看——那片远处地面,开始疯闪、抖动,像信号不好的电视雪花。你走近它,不闪了;你转头,它又在跳。你啥也没改,它就自己抖。

这正是第 28.2 的全部真相:远处那片地,密密的草纹(高频图案)被硬塞进稀疏的屏幕像素,一个纹素被映射到几十个屏幕像素。GPU 按屏幕像素位置去贴图”点名”纹素,可 3D 面斜、视角一动,相邻帧同一屏幕像素点到的纹素在亮暗处之间跳——连起来就是闪烁。它和拍照出摩尔纹是同一个病。

34.3.2 开 Mipmap:闪烁瞬间消失

现在你亲手治病。选中那张贴图,Inspector 里把 Generate Mip Maps 勾上。重新运行,往远处看。

闪烁没了。为什么?连回第 28.3:Mipmap 是”提前把大图缩小好的一串链”,远处那个面在屏幕上只有几十像素高,GPU 就直接读 64×64 那张”已经把草纹平均掉”的小图。小图只剩低频色块,屏幕像素刚好够采,采样公平、不再跳变。这就是”远处用小的那张”。

你在这里会获得一个极其重要的直觉:Mipmap 治的是”闪”,不是”糊”。 开了 Mipmap,远处是用小图,确实比原图”糊”一点,但那点糊是人眼远处本就接受、且第 8 章雾也在帮你藏的;而”闪”是假高频、随视角乱跳,是绝对不能忍的。这就是第 28 章说的”用远处看不清的感知弱点换稳定”。

34.3.3 斜看地面:提 Anisotropic

但你把 Camera 压得更低、贴地往前看公路/地面时,可能发现远处还是有点糊、发虚。这是第 28.4.4 的 anisotropic 问题:你斜着看地面,那个面在”纵深方向”被拉得极长、横向没那么长,是非等比的。普通过滤按正方形采样,在拉长方向采不准。

你选中贴图,把 Aniso Level(各向异性等级)从 1 提到 8 或 16。重新看——斜看的远处地面清晰锐利了。这就是 Anisotropic Filtering 在”按拉长方向补偿采样”。

34.3.4 一段 C# 思路:运行时打印贴图的 Mipmap 状态

我给你一段代码,让你在运行时确认某张贴图到底有没有 Mipmap、有几级,而不是每次都去翻导入面板:

using UnityEngine;
 
// 挂到地面物体上,运行时打印其材质主贴图的 Mipmap 信息
// 需在 Unity 工程中验证
public class TextureMipProbe : MonoBehaviour
{
    void Start()
    {
        var renderer = GetComponent<MeshRenderer>();
        if (renderer == null || renderer.sharedMaterial == null) return;
 
        Texture tex = renderer.sharedMaterial.mainTexture;
        if (tex == null)
        {
            Debug.Log("[Mip] 该材质没有主贴图");
            return;
        }
 
        // 用 mipmapCount 判断引擎是否真的生成了 Mipmap 链
        int mipCount = tex.mipmapCount;          // 1 表示没开 Mipmap(只有原图),>1 表示开了
        bool hasMip  = mipCount > 1;
        string aniso = tex.anisoLevel.ToString(); // 各向异性等级
 
        Debug.Log($"[Mip] 贴图={tex.name} mipCount={mipCount} " +
                  $"{(hasMip ? "已开Mipmap" : "⚠ 未开Mipmap,远处会闪")} " +
                  $"anisoLevel={aniso}");
 
        // 排查口诀:远处闪 → 先看 mipCount 是不是 1;是 1,先去导入面板勾 Generate Mip Maps
    }
}

你会注意到,mipmapCount == 1 就是”没开 Mipmap”的铁证。以后同事问你”为啥远处闪”,你不用猜,让他挂这段脚本跑一下,Console 一句话就锁定了。

反问你:如果”远处地面闪”和”物体边缘有台阶状狗牙”同时出现,你该先拧 Mipmap 还是先拧抗锯齿?——两味药管两种病,别混。闪是纹理内部高频走样(开 Mipmap+Aniso),狗牙是几何轮廓被方块网格切碎(换 Camera 的 Anti-aliasing)。认错病,白拧。

34.4 实验三·阴影 acne 与 Peter Panning:bias 两难亲手演

第三个实验,复现第 29 章的阴影两难。目标:你亲眼看到”墙面一片条纹暗疮”和”物体脚不沾地浮空”,并确认它们是同一个旋钮(Bias)的两端,然后学会用 Normal Bias 缓解。

34.4.1 搭最小场景

  1. 放一个 DirectionalLight(平行光,像太阳),把它的 Shadow Type 设成 Soft Shadows 或 Hard Shadows,并打开阴影。
  2. 放一块大 Plane 当地面(接收阴影)。
  3. 在地面上放一个物体(比如一个 Cube 或一本书状的薄 Box),让它投影到地面。

运行。默认 Bias 下,应该一切正常——物体投影清晰,地面平整。

34.4.2 制造 acne:把 Bias 调到极小

选中 DirectionalLight,找到阴影区的 Bias(Shadow Bias),把它从默认的 0.05 之类调到接近 0(比如 0.0001)。重新运行,斜着看那块地面和物体表面。

你看到什么?本该平整受光的地面上,爬满一片片条纹状、像晒伤的暗纹,随视角蠕动。这就是 shadow acne(阴影痤疮)。物体表面在”自己阴影里自己”。

原理连回第 29.3:Shadow Map 是一张分辨率有限的深度图。斜面上相邻像素在阴影图里对应格子边缘不齐,采样到的”最浅深度”偶尔比真实深度略小(量化误差),于是引擎误判”前面有东西挡着”→ 判成影中→变暗。误差随表面斜率周期变化,显成条纹。

34.4.3 制造 Peter Panning:把 Bias 调太大

现在反向操作。把 Shadow Bias 从极小值一路调大(比如调到 1 甚至 2)。重新运行,盯那个放在地面上的物体。

你看到什么?物体底和地面之间,漏出一条被照亮的细缝,本该连着的接触阴影断开了——书像悬浮在空中、脚不沾地。这就是 Peter Panning(彼得潘效应),得名于那个永远长不大、会飞的小男孩。

原理连回第 29.4:Bias 是把表面往光源方向”抬”一点去比深度。抬太多,引擎就认为”书和地面之间那段光其实照得到”,接触缝断了,物体便”飞”起来。

34.4.4 两难与解法:Normal Bias 出场

你现在亲手体验了第 29 章最核心的那个两难:Bias 太小→acne,Bias 太大→浮空。一个旋钮,两端都是病。

解法不是”找个中间值硬拧”,而是请出第 29.5 的 Normal Bias(法线偏移):它不整体往光源抬,而是沿表面法线方向外推一点再去比深度。对 acne 重灾区的陡斜面,沿法线外推正好降敏、消 acne;对贴地物体,推的方向是”朝天(沿法线)“,不会把接触缝撑开,所以既压 acne 又不那么易浮空。

你动手验证:先把 Shadow Bias 设回一个中等偏小值(比如 0.02),再把 Normal Bias 从 0 提到 0.4 左右,边看画面边拧。你会发现条纹消失、且物体脚还踩在地上——两个病同时压住,这才是调好。

排查纪律(请焊死):看到墙面条纹暗疮 → 调大 Shadow Bias / 加 Normal Bias;看到物体浮空漏光缝 → 调小 Shadow Bias / 减 Normal Bias。先认病,再拧旋钮,别无脑拉高 Resolution(acne 不是精度问题,拉分辨率没用)。

34.4.5 一段 C# 思路:运行时打印阴影 bias 与分辨率

我给你一段代码,让你在运行时量化 Light 的阴影设置,方便排查时”用数字说话”:

using UnityEngine;
 
// 挂到带 Light 的物体上,运行时打印阴影类型、Bias、Normal Bias、分辨率
// 需在 Unity 工程中验证
public class ShadowProbe : MonoBehaviour
{
    void Update()
    {
        Light light = GetComponent<Light>();
        if (light == null) return;
 
        // shadowBias 太小→acne;太大→Peter Panning。两者都从这里读
        float bias       = light.shadowBias;       // 阴影偏移(深度偏移)
        float normalBias = light.shadowNormalBias; // 法线偏移(沿法线智能偏移)
        LightShadowResolution res = light.shadowResolution; // 阴影图分辨率
 
        string type = light.shadows.ToString();     // None / Hard / Soft
 
        Debug.Log($"[Shadow] type={type} bias={bias:F4} normalBias={normalBias:F4} res={res}");
 
        // 排查口诀:
        // 表面条纹暗疮 → 看 bias 是不是过小;浮空漏光缝 → 看 bias 是不是过大。
        // 两边都想压,优先动 normalBias,再小幅动 bias。
    }
}

跑起来后,你拨 Inspector 里的滑块,Console 实时跟着跳。久了你会建立一种”数字直觉”:bias 在 0.02 左右通常安全,冲到 1 以上基本就是浮空预备役。

34.5 实验四·画面发灰(颜色空间):Linear 与 Gamma 一键切换

第四个实验,复现第 30 章的”灰扑扑”。目标:你亲眼看到”同一个场景,切一下 Color Space 就从阴天变通透”,并理解它和灯光无关、是颜色空间这块地基没铺对。

34.5.1 搭最小场景

  1. 放几个用 PBR 材质(Standard / Lit)的物体,打一盏正的 DirectionalLight(正午阳光感)。
  2. 确保贴图导入时颜色类贴图勾了 sRGB(否则实验会被别的坑干扰)。
  3. 相机打开 HDR、后处理里加 Tone Mapping(ACES)。

先保持 Color Space = Linear,运行。你看到的是通透、对比强的正午阳光感——亮处该放光、暗处该有层次。

34.5.2 制造”阴天灰”:切到 Gamma

打开 Project Settings → Player → Other Settings → Color Space,把它从 Linear 改成 Gamma。重新运行同一个场景。

你看到什么?中午的阳光硬是照出了阴天的感觉,亮处不死白但也绝不放光,整体像隔着一层纱,红不红、绿不绿、对比度怪怪的。把灯光 Intensity 拉到爆也没用——要么全糊成白、要么还是灰。

这正是第 30.5 的全部真相:光照数学(相乘、相加、衰减)只有”线性空间”才算得对。Gamma 空间是弯曲的 sRGB 编码空间,在里头算光,光强相加偏暗、衰减不对,累积成发灰。颜色空间不是装饰,它是你前面学的所有光照数学能不能算对的”地基”。

你在这里获得一个极反直觉、也极值钱的排查纪律:画面发灰,第一反应不是去拧灯,而是去查 Color Space 和贴图 sRGB 勾选。 灯是”上层”,颜色空间是”地基”,地基歪了,你在上面怎么装修都白搭。

34.5.3 数据类贴图别勾 sRGB:另一个隐藏坑

顺手演一个更隐蔽的坑(第 30.6 提过):把一张法线贴图(Normal Map)的 sRGB 勾上。重新看——高光变怪异,因为法线方向被 Gamma 曲线扭曲了。

你确认:颜色类贴图(Albedo、自发光)勾 sRGB,数据类(法线、粗糙度、金属度、AO)不勾。勾错一类,画面就出怪相。

34.5.4 一个 Editor 脚本:用代码读/切 Color Space

这个实验里,我给你一段只能在 Unity Editor 下运行的脚本(注意:不是运行时 MonoBehaviour,而是编辑器工具)。它用 PlayerSettings.colorSpace 读取和切换颜色空间,方便你写自动化排查、或在编辑器里一键 A/B 对比:

using UnityEditor;
using UnityEngine;
 
// 编辑器菜单工具:一键读取 / 切换项目颜色空间,做 A/B 对比
// 需在 Unity Editor 下运行(含 UNITY_EDITOR 的编辑器脚本,打进构建会报错)
public class ColorSpaceSwitcher
{
    [MenuItem("渲染实验室/读取当前 Color Space")]
    static void LogColorSpace()
    {
        // PlayerSettings.colorSpace 是只读属性,反映 Project Settings 里的设置
        ColorSpace cs = PlayerSettings.colorSpace;
        Debug.Log($"[ColorSpace] 当前颜色空间 = {cs} " +
                  $"{(cs == ColorSpace.Linear ? "(线性,正确)" : "(Gamma,画面易发灰)")}");
    }
 
    [MenuItem("渲染实验室/切到 Linear")]
    static void ToLinear()
    {
        // 切换后 Unity 会重新编译 shader、重新导入相关贴图,画面会刷新
        if (PlayerSettings.colorSpace != ColorSpace.Linear)
        {
            PlayerSettings.colorSpace = ColorSpace.Linear;
            Debug.Log("[ColorSpace] 已切到 Linear,请观察画面是否变通透");
        }
    }
 
    [MenuItem("渲染实验室/切到 Gamma(制造发灰现象用)")]
    static void ToGamma()
    {
        if (PlayerSettings.colorSpace != ColorSpace.Gamma)
        {
            PlayerSettings.colorSpace = ColorSpace.Gamma;
            Debug.Log("[ColorSpace] 已切到 Gamma,观察画面是否发灰(阴天感)");
        }
    }
}

注意三点:第一,这是带 MenuItem 的编辑器脚本,必须放在 Editor/ 文件夹下或包了 UNITY_EDITOR 宏;第二,PlayerSettings.colorSpace 的切换会触发 shader 重编译和贴图重导入,所以你切完要等一两秒再看画面;第三,它不能在打包后的游戏里跑,是纯开发期排查工具。你做实验时,用菜单一键在 Linear / Gamma 之间跳,对比同一场景的明暗与对比度——这种”同一输入、只换一个变量”的 A/B 实验,最能让你把”发灰=颜色空间”焊进肌肉。

反问你:线上有人反馈”画面整体发灰、中午像阴天”,你作为排查者,第一句该问”灯光 Intensity 设多少”还是”Color Space 是 Linear 吗、颜色类贴图 sRGB 勾了没”?——记住,先查地基,再查装修。

34.6 速查表:看到异常 → 背后原理 → 该拨的旋钮

这一节是你这一章、也是前面图形学深度部分的”总账本”。我把四个实验的现象,加上第 27 章穿模、第 7 章近大远小等,汇成一张”看到什么 → 是哪个原理 → 拧哪个旋钮”的表。面试前、实战中,你就背这张表,它是你的肌肉记忆清单。

你看到的异常现象背后原理(哪一章)该拨的旋钮
两个几乎共面的面在像素级闪烁穿插z-fighting,深度精度近密远疏、near/far 比值过大(第 26 章)调大 Near、缩小 Far;或 Polygon Offset;或抬标高
远处地面/草地/砖墙抖动雪花纹理高频走样,一个纹素映射到几十屏幕像素(第 28 章)开 Generate Mip Maps;提 Anisotropic Level
斜看公路远处糊成一条各向异性未补偿,拉长方向采不准(第 28 章)提 Anisotropic Filtering(2x~16x)
物体边缘台阶状狗牙几何走样,轮廓被方块网格切碎(第 3/28 章)Camera 的 Anti-aliasing(TAA/FXAA/MSAA)
墙面一片条纹暗疮、随视角蠕shadow acne,Shadow Map 精度误差(第 29 章)调大 Shadow Bias / 加 Normal Bias
物体脚不沾地、底下漏光缝Peter Panning,Bias 太大(第 29 章)调小 Shadow Bias / 减 Normal Bias
近处影子边缘锯齿马赛克Shadow Map 分辨率低(第 29 章)调高 Light Shadow Resolution
远处山/建筑影子糊掉消失一张图覆盖太广、精度摊薄(第 29 章)加 Cascade Count(2~4)
画面发灰、中午像阴天、对比怪颜色空间设错,光照数学在弯曲空间算(第 30 章)Color Space 设 Linear;颜色类贴图勾 sRGB
亮处死白无光晕、像贴纸没开 HDR/Tone Mapping/Bloom(第 30 章)开 HDR + 加 Tone Mapping + 开 Bloom
远处清晰近处糊(对焦反了)DOF 对焦距离设反(第 30 章)把 Focus Distance 设到想清楚的主体
物体穿插穿模、该挡没挡深度测试/渲染顺序或碰撞体缺失(第 26/27 章)查深度写入、查碰撞体;穿模另查 Collider
近大远小不对、透视怪投影矩阵/FOV 或正交透视设错(第 7 章)查 Camera Projection(Perspective/Ortho)与 FOV

这张表的价值,不在”背住”,而在你每次看到怪现象,先来这里对号入座、认准病,再去拧对应的旋钮。新手 90% 的浪费,都花在”认错病、乱拧一通”上。

自检:线上反馈”远处栅栏在闪”,你按表第一反应是查哪一格?——纹理高频走样那格,去开 Mipmap+Aniso。如果反馈是”墙上有条纹暗疮”,那是另一格,去调 Bias。同一句”闪”,病根可能完全不同,先分清楚。

34.7 你现在拥有的能力:从”概念懂”到”能排查 bug”闭环

我带你回头看一眼你这一路走来的路线,因为这一章是这条线的收口。

最早(第 2、6、8 章前后),你是个”懂编程但不懂图形”的人,连顶点怎么到屏幕都不清楚。后来我们啃下图形学深度四块硬骨头:第 26 章深度缓冲与 z-fighting、第 28 章纹理 Mipmap 与抗锯齿、第 29 章阴影 Shadow Map 与 bias 两难、第 30 章后处理与颜色空间。你”概念懂”了。

再后来,进入第十二部分”动手实践”:第 32 章你亲手写过坐标变换的 demo,把”一个顶点怎么从局部坐标搬到屏幕”用代码验证了一遍;第 33 章你亲手写过雾的 shader,把”按距离把颜色混进雾色”从公式变成能跑的着色器。你已经”能写 demo 验证概念”了。

今天第 34 章,你把那四个最易在线上爆的渲染现象,亲手造出来、又亲手修好。于是这条线彻底闭环:

概念懂(第 26/28/29/30 章)→ 能写 demo 验证(第 32/33 章)→ 能亲手复现并排查 bug(本章)= 一个能独立撑起图形排查的工程师。

你以后面对”地面闪""墙面痤疮""画面发灰”这类反馈,不再去论坛乱搜,而是:复现 → 对照 34.6 速查表认病 → 拨对应旋钮 → 用 34.2~34.5 的 C# 探针量化确认。这一套动作,就是”会排查”和”只会看教程”的分水岭。

而且你发现没有,全教程的概念像一张网一样越收越紧:第 26 章的 Z-buffer 和第 29 章的 Shadow Map 共用”深度图”;第 3 章分辨率、第 8 章远裁剪、第 29 章 CSM 共用”按距离分层求精度”;第 5/6 章光照数学和第 30 章颜色空间共用”线性空间才是真值”。你今天做的四个实验,每个背后都是这张网的一个结。网越密,你排查越快。

反问你:如果现在有人问你”我该怎么学图形学才不算白学”,你会怎么用你自己的这条线回答他?(提示:先读原理,再写 demo 验证,最后做最小复现实验把现象和旋钮焊死——三层缺一不可。)

34.8 收个尾

这一章我们没有讲任何新原理,只做了一件事:把你前面在纸上”看懂”的四个现象,在 Unity 里”亲手确认”了一遍。我相信,当你亲手把 Far 拉到 100000 看到闪烁爆发、亲手把 Color Space 切到 Gamma 看到画面发灰的那一刻,那些原理才真正从”脑子里的句子”变成”手上的直觉”。

记住这一章留给你的最实用的东西:34.6 那张速查表。它就是你以后面试和实战的”肌肉记忆清单”——看到现象,先认病,再拧旋钮,用探针量化确认。别再”认错病、乱拧一通”了。

你已经走完”概念→demo→排查”的全闭环。下一站如果你想继续,可以去碰全局光照、去碰自建 Shadow Map 的 shader(你第 33 章写雾 shader 的手感直接用得上),或者把这些探针脚本真的跑一遍、记一组你自己的”安全数值”。路在你脚下。

【思考题】

  1. 线上有人远程反馈”远处栅栏在闪”。你作为排查者,第一反应该去查哪一类设置,为什么不是先去调灯光或材质?请按 34.6 速查表说出对应原理和该拨的旋钮,并说明你会让对方挂哪段探针脚本确认。
  2. 为什么”画面发灰、中午像阴天”时,你的第一反应应该是查颜色空间而不是查灯光 Intensity?如果 Color Space 已经是对的 Linear,画面还是灰,下一步你该查贴图导入面板的什么勾选(颜色类 vs 数据类有何不同)?
  3. 阴影 acne 和 Peter Panning 是同一个旋钮的两端。请描述你做实验时,把 Shadow Bias 调到极小和调到极大分别看到了什么现象,并解释为什么单纯拧 Shadow Bias 不够、需要 Normal Bias 出场来缓解两难。
  4. 本章四个实验,每个背后都连着前面某一章的原理。请任选其中两个实验,分别说明它”复现的是第几章的哪个现象”,以及”修好它对应的引擎旋钮落在哪个组件/设置面板上”(比如 Camera、Light、贴图导入、Project Settings)。

【参考答案】

  1. 第一反应查”纹理高频走样”那类设置——开没开 Mipmap、Anisotropic 够不够,而不是灯光/材质。原因是”远处栅栏闪”是 28.2 的典型病:远处密纹被稀疏屏幕像素错误采样,产生随视角乱跳的假高频,和灯光/材质无关。对应旋钮:贴图导入的 Generate Mip Maps 勾上 + 提 Anisotropic Level。我会让对方挂 34.3.4 的 TextureMipProbe,看 Console 里 mipCount 是不是 1(1=没开 Mipmap,铁证),用数字锁定,而不是凭肉眼猜。这正体现”认病→拧旋钮→探针量化”的纪律。
  2. 因为颜色空间是”地基”:光照数学(相乘/相加/衰减)只在线性空间算得对,Gamma 空间是弯曲的 sRGB 编码,在里头算光会累积成发灰,和灯光强度无关——你把 Intensity 拉爆也救不回地基。所以先查 Color Space,再查贴图。若已 Linear 还灰,下一步查贴图导入的 sRGB 勾选:颜色类(Albedo/自发光)必须勾 sRGB(让引擎解码回线性再算),数据类(法线/粗糙度/金属度/AO)必须不勾(勾了会被 Gamma 扭曲、出怪高光)。勾错一类就发灰或发怪。
  3. 实验里:Shadow Bias 调到极小(≈0)→ 地面/物体表面爬满条纹状暗疮(acne),因阴影图量化误差让表面”自己挡自己”;调到极大(≈1~2)→ 物体底和地面间漏出被照亮细缝、物体浮空(Peter Panning),因 Bias 把表面抬太高、接触阴影断开。单纯拧 Shadow Bias 不够,因为它”整体往光源抬”,抬大了一边压 acne 一边又撑开接触缝,两难此消彼长。Normal Bias 改为”沿表面法线方向外推”再去比深度:对陡斜面降敏、消 acne,又不会把贴地物体的接触缝撑开,于是既压 acne 又不那么易浮空,从根上缓和两难。
  4. (任选两例)实验一 z-fighting:复现第 26 章”两面几乎共面时深度精度近密远疏、near/far 比值过大导致像素级闪烁”,修复旋钮在 Camera 组件的 Near/Far Clip Planes(以及材质 Polygon Offset)。实验四发灰:复现第 30 章”颜色空间设错、光照数学在弯曲空间算导致画面发灰”,修复旋钮在 Project Settings → Player → Color Space(切 Linear),及贴图导入面板的 sRGB 勾选。实验三阴影两难:复现第 29 章的 acne 与 Peter Panning,旋钮在 Light 组件的 Shadow Bias / Normal Bias。实验二 Mipmap 闪:复现第 28 章纹理走样,旋钮在贴图导入的 Generate Mip Maps 与 Aniso Level。每个实验都用”挑最小变量复现→拨对应旋钮→探针量化”三步走,把现象和原理焊死。