最近有个做渲染的朋友给我发来一段录屏问我为什么远处广场砖地面像水波一样荡来荡去近处人物衣服边缘又像锯齿一样一跳一跳的。他说自己已经换了8K贴图问题不但没解决波浪纹反而更明显了。这个问题其实很典型属于纹理映射里最容易被误解的一类现象摩尔纹和毛刺。它不是贴图不够清晰的问题而是采样方式出了问题本质上是信号处理里的混叠aliasing。这篇文章我会把纹理映射中这两类视觉缺陷的成因拆开讲清楚再从纹理金字塔、各向异性过滤到各种进阶方案逐一分析最后分享我排查这些问题时的固定动作和实际经验。适合正在做渲染、游戏客户端、引擎底层的人参考做贴图美术的同事也可以读一读知道哪些问题该从源头规避。1. 摩尔纹和毛刺不是一回事先分清现象、成因和感受1.1 一张贴图里同时出现的两种脏东西先把现象分清楚。摩尔纹和毛刺虽然经常一起出现但看起来完全不同。摩尔纹是远处规则纹理上出现的波浪状色带通常是黑白相间或者彩色渐变条纹会随着相机移动而缓慢滚动变形。典型场景是远距离看砖墙、栅栏、网格地面、百叶窗。你会发现这些场景里的纹理都是高度重复的周期信号一旦采样不充分就会产生这种大范围干涉条纹。毛刺则是像素级别的锯齿和闪烁。最明显的出现在几何边缘——一条斜线变成了阶梯状转动相机时阶梯位置来回跳动俗称shimmering。另一种毛刺出现在纹理内部比如高光点、细小字母、树枝缝隙这类高频细节它们会以随机亮点的方式闪来闪去特别干扰视线。很多人把这两者混为一谈但它们的视觉机制不完全一样。摩尔纹是全局干涉是高频信号被折叠后形成的低频长波毛刺是局部跳变是采样点位置微小变化导致相邻帧间颜色大幅变动。明白了这个区别后面排查方向才不会跑偏。1.2 一个采样过程两个不同的失败侧面纹理映射的直观理解是把一张图贴到模型表面。但图形学视角下这件事的本质是对二维纹理场做采样然后用重建滤波器把采样结果还原成像素颜色。每个像素中心是一个采样点像素覆盖纹理上的一个区域这个区域越大意味着采样频率相对纹理频率越低。摩尔纹的出现是因为纹理里的周期信号频率超过了当前采样网格能承受的上限。采样定理告诉我们采样频率至少要达到信号最高频率的两倍否则高频分量就会折叠到低频和真实低频信号叠在一起形成莫须有的波浪条纹。这个折叠过程对周期纹理尤其致命因为折叠后的频率和原始频率会差拍出非常长、非常慢的变化。毛刺的根源也是混叠但它更多体现在边缘和瞬态上。比如三角形边缘的锯齿是几何栅格化的混叠而纹理内部高光的闪烁则来自高频能量无法被稳定重建。一个像素覆盖的纹理区域里可能有一个很亮的高光点相机微动一下采样点落在高光上这个像素就特别亮再动一下采样点偏离高光像素立刻变暗。这种全有或全无的状态就产生了刺眼的闪烁。1.3 为什么静止截图看不出来动态看起来特别糟还有一个容易忽略的层面摩尔纹和毛刺在动态画面里的危害远大于静态截图。实时渲染每一帧都是重新采样相机一旦移动采样点相对纹理的位置每帧都在变化。摩尔纹的干涉条纹会跟着滚动毛刺会沿着边缘上下跳动。人眼对时间上的高频变化非常敏感这种滚动和跳动会被大脑自动放大感知。更麻烦的是视频编码。视频压缩算法假设帧间变化是连续的、可预测的而摩尔纹滚动和毛刺闪烁恰恰是时间上极不稳定的高频模式。编码器在压缩这类画面时往往会丢失大量细节同时又产生额外的块状失真和振铃效应导致问题在录屏、直播、推流环节被进一步放大。所以很多游戏在本地跑着还行一旦录制成视频发给别人看问题就变得特别明显。2. 从采样定理看纹理映射为什么会碰巧出现两种现象2.1 像素不是取色点而是采样重建我经常跟团队里的人说不要把像素理解成从纹理上取一个颜色而要理解成在纹理场上做一次采样。采样点落在哪决定了你从连续信号中取到哪个值采样之后还需要重建也就是把离散样本转换为像素的连续色值。举个生活化的例子用手机拍一件细格纹衬衫。衬衫的网格每厘米有几十根线而手机摄像头传感器的采样点像素密度可能还赶不上衣服的纹理密度。拍出来的照片上就会出现一圈一圈的彩色波纹那不是衣服上有的是传感器采样不足制造出来的假象。纹理映射里的摩尔纹和这个原理完全一样只是把传感器像素替换成了屏幕像素。如果把纹理看成一张连续信号图那么每个像素覆盖在纹理上是一小块区域这个区域的宽度决定了局部采样频率的上限。在近处像素覆盖区域小能采样到的纹理细节多在远处一个像素可能覆盖了好几个纹理单元texel细节自然就糊了。糊不可怕可怕的是糊的过程中产生了不该出现的图案和闪烁。2.2 奈奎斯特临界点频率之间的躲猫猫采样定理有个非常实用的描述方式为了避免混叠采样频率应该至少是信号最高频率的两倍这就是奈奎斯特准则。对于纹理映射一个像素覆盖的纹理区域宽度为w当前纹理层级的分辨率是N那频率超过 N/(2w) 的信号分量就必然产生混叠。这句话直接解释了为什么提高纹理分辨率往往适得其反。你从2K换到8K纹理里的最高频率变高了但像素覆盖的纹理区域宽度在同一个距离下也变大了两者同时变化奈奎斯特临界条件并没有变好甚至因为高频能量更多混叠反而更严重。这就是我那个朋友困惑的根源分辨率越高摩尔纹越明显因为你在给混叠提供更多的高频原料。对于周期性纹理混叠后的低频分量与原信号形成差拍空间上呈现大尺度渐变也就是摩尔纹。对于非周期高频细节比如树叶、砂砾、高光点混叠后没有明显的长波结构而是散落成随机的亮点闪烁看起来就是毛刺。这两者本质是同一枚硬币的两面。2.3 三种基础过滤方式各自的边界图形API里常见的纹理过滤方式大致分三类最近邻采样Nearest、双线性过滤Bilinear、三线性MipMap过滤Trilinear。很多项目直到出问题了才发现自己一直在用错误的过滤方式。过滤方式采样行为现象适用场景最近邻只取采样点最近的那个texel放大时严重块状感缩小时剧烈摩尔纹和闪烁像素风游戏、需要保持硬边缘的UI图标双线性采样点周围4个texel做线性插值放大平滑但缩小依旧混叠严重2D UI、放大场景三线性MipMap预生成分辨率金字塔按像素足迹选层并跨层插值缩小混叠大幅降低但斜视角下可能过渡模糊绝大多数3D场景这几种方式我都会在后续章节展开讲。这里先记住一个最关键的结论双线性解决的是放大时的颗粒感而摩尔纹和缩小时产生的毛刺只有靠预滤波或者更复杂的采样策略来解决。如果你还在用只开双线性过滤的方式跑3D场景那画面出现摩尔纹是必然的。3. 纹理金字塔为什么能压住一大半摩尔纹层级选择的本质3.1 MipMap在采样之前就把高频提前滤掉MipMap也叫纹理金字塔是当前解决缩小混叠的主流方案。思路很朴素预先为一张纹理生成一系列分辨率逐级减半的副本每一层都是上一层经过box滤波缩小的结果。GPU在采样时根据像素覆盖的纹理区域大小自动选择合适层级的副本。这为什么能治本关键在于MipMap的预生成过程相当于对纹理做了一次低通滤波。上一层已经把超过该层级分辨率的细节抹掉了GPU从合适的层级采样相当于在采样之前就对信号做了频带限制保证采样频率和信号最大频率匹配。这属于源头抑制而不是事后修补。很多不明白原理的人会觉得MipMap只是让远处的纹理糊一点其实它的核心价值是消除混叠。糊只是表象如果只有糊而没有摩尔纹和闪烁那说明滤波是在正确工作的。MipMap的内存开销也常被高估——完整金字塔从原始分辨率开始逐层对半全部层级加起来大约是原图的1.33倍多出1/3显存而已。3.2 GPU如何决定取哪一层屏幕空间导数与足迹GPU选择MipMap层级的依据是像素在纹理空间覆盖区域的近似大小这个覆盖区域叫像素足迹pixel footprint。要计算足迹GPU会对纹理坐标在屏幕空间求偏导数也就是计算纹理坐标U和V相对于屏幕坐标X和Y的变化速度。简化后的对应关系是如果一个像素覆盖了大约2×2个texel那就选LOD 1层覆盖4×4个texel选LOD 2层覆盖8×8个texel选LOD 3层。用公式表示就是 LOD 0.5 × log2(足迹面积)其中足迹面积可以用两个导数向量围成的平行四边形面积近似。这里放一段我在调试时经常手算的伪代码帮你直观理解层级选择逻辑// 伪代码从屏幕空间导数估算足迹并选LOD vec2 dUVdx dFdx(uv); // uv对屏幕x的偏导 vec2 dUVdy dFdy(uv); // uv对屏幕y的偏导 // 足迹面积 两个导数向量围成的平行四边形面积 float footprint abs(dUVdx.x * dUVdy.y - dUVdx.y * dUVdy.x); // LOD log2(足迹边长的平方)的一半 float lod 0.5 * log2(footprint); // 应用LOD偏移和钳制 lod clamp(lod lodBias, lodMin, lodMax);三线性过滤还会在LOD相邻两层之间按小数部分做一次线性插值避免层级切换时出现明显的清晰度跳变线。这个插值对于动态画面非常重要否则相机后退时你会看到一条清晰分界线在地面上一层层往外推。3.3 那些纹理糊但摩尔纹还在闪的特殊情况MipMap不是万能药我在实际项目里遇到过几种失效情况非常值得记录。第一种是纹理图集texture atlas场景。一张大图集里排列着许多不同的UV岛岛与岛之间的纹理内容完全不同。GPU计算屏幕空间导数时如果采样点恰好跨过图集内两个不同区域的边界导数会出现大幅跳变导致LOD算出错误层级边界附近就会闪出摩尔纹或毛刺。这个问题的处理方案通常是启用每页MipMap偏移或者让UV岛之间留出足够边距。第二种是程序化纹理。有些项目在着色器里动态生成纹理图案比如网格线、地形等高线、分形噪声。这类纹理如果不走预生成MipMap而是直接按原始公式采样每一个像素都会命中极高频率的细节摩尔纹浮现得很厉害。所以程序化纹理通常需要单独的解析滤波方案不能简单套用MipMap思路。第三种情况是光照烘焙图。当光照贴图内容包含高频AO、高光细节时MipMap会把这些细节模糊掉。远处画面虽然不再闪烁但原本的明暗层次信息被抹平整个场景看起来糊且平也容易给人一种问题没解决的错觉。这种情况下需要通过调整LOD偏置或者增加额外的方向性存储来缓解。4. 各向异性过滤对斜视角的那点补偿逻辑4.1 MipMap的正方形假设为什么不够MipMap有一个隐藏的假设像素在纹理空间的覆盖区域近似是正方形。这个假设在正视平面时基本成立但一旦表面和视线方向形成锐角比如看远处的地面、墙面、路面像素覆盖在纹理空间里会变成一个非常扁长的椭圆——平行于表面的方向覆盖很长垂直于表面的方向覆盖很短。举个具体场景你站在马路边看远处的地砖地砖接缝在竖直方向随着透视急剧缩小一个像素在纹理空间里沿竖直方向可能跨越好几个砖缝而水平方向只跨了一小块。各向同性MipMap按面积估算LOD会选择一个中等分辨率的层级然后对整块正方形区域做模糊。结果就是本该在水平方向保留的砖缝细节被抹掉了而竖直方向的高频接缝又没被充分滤波远处地面既糊又闪。这种既糊又闪同时出现的情况在传统MipMap下非常常见因为正方形足迹和椭圆足迹不匹配。想要好效果就需要在垂直方向多滤波一些水平方向少滤波一些也就是各向异性过滤。4.2 各向异性过滤在做什么各向异性过滤的思路是放弃一个像素对应一个正方形区域的假设改为计算像素在纹理空间的椭圆足迹然后沿着椭圆的长轴方向做多次采样最后把多次采样结果加权平均。数学上这相当于用椭圆核做卷积滤波比正方形核更贴合真实的投影关系。实际操作中GPU具体做法是先用屏幕空间导数算出椭圆主轴的方向和长度然后沿主轴方向以固定步长采样多个位置每个位置再做一次双线性或三线性采样最后把结果混合。采样次数越多对椭圆的逼近越准确这就是我们常说的各向异性档位2x、4x、8x、16x的含义——档位数字基本对应沿主轴的最大采样次数。各向异性过滤的价值主要体现在斜视角上的定向细节保留。地面砖缝、长条栅栏、铁轨这类强方向性纹理是最大受益者。它能做到在视线平行的方向上保留清晰的结构线在视线垂直的方向上充分滤波消除混叠两者兼顾。4.3 档位与性能的真实体验各向异性的档位不是越高越好需要结合实际场景来判断。我整理了大致效果对比方便你做取舍档位等效采样次数视觉收益性能代价关闭1次采样斜视角地面糊、远处闪无额外开销2x最多2次斜视角有轻微改善可忽略4x最多4次地面砖缝明显更清晰部分中低端设备有压力8x最多8次视觉质量基本满足大多数场景性能消耗明显但通常可接受16x最多16次与8x差异较难肉眼分辨性能开销大仅在高端平台值得开我在项目里的经验是桌面平台默认开到8x高端场景可以16x移动端控制在4x以内。很多时候16x和8x在最终画面上的差异非常小但带宽压力会实打实翻倍。各向异性过滤还有一个不算缺点的缺点并不能完全消除极斜视角下的夹缝毛刺想要彻底干净还需要配合LOD偏置和纹理内容本身的控制。5. 换了MipMap之后毛刺反而变多了LOD、精度与锐度的三笔账5.1 负LOD偏移为了锐利付出的混叠代价这个坑我踩过不止一次。很多团队觉得MipMap把纹理搞得太糊于是给采样器加了负的LOD偏置强制取更接近原始分辨率的层级。这样画面确实锐利了远处细节看起来也更清楚。但代价是原本被MipMap滤掉的高频分量又被请了回来它们再次越过采样率的边界摩尔纹和闪烁随之复发。负LOD偏移合理的场景只有一个源纹理分辨率确实低于当前显示需求。比如你有一张贴图只有512×512但模型在近景需要撑到4K级别的细节此时给一点负偏置可以让MipMap不至于过早下降算是从低分辨率里多捞一点信息。但如果你的纹理本来是2K负偏移反而会让画面更脏。我建议负偏置控制在 -0.5 到 -1.0 之间超过 -1.0 基本就是在制造混叠垃圾了。而且偏置应当配合相机距离动态调整而不是全局写死。比如地面可以使用近略微负、远略微正的曲线让近处保持锐利远处优先保证稳定。5.2 法线贴图过滤后的重归一化问题法线贴图的毛刺属于隐蔽性很强的一种问题。法线贴图在MipMap降采样时如果只是简单平均两个法线方向得到的法线长度不再是1。光栅化阶段采样出来的法线如果没做归一化光照计算结果会出现偏色、高光偏移在动态光照下表现为表面隐隐发闪。正确做法是两件事第一MipMap生成时对法线贴图层做法线方向平均而不是普通颜色平均很多纹理压缩工具默认就这么干但如果你用自定义管线生成MipMap务必记得处理第二采样法线贴图之后必须重新normalize即使原始贴图保存时都是单位法线经过插值和压缩后长度早就变了。我见过一个项目所有地面纹理在移动端都有明显的高光毛刺查了很久发现是法线贴图在某一级Mip中出现了大量长度接近0.05的烂法线导致高光计算完全失去了方向性。归一化之后所有闪烁瞬间消失。5.3 高光毛刺的独立解法高光毛刺的另一个难点是即使纹理过滤完全正确高光项本身也容易闪。原因是高光对微表面法线方向极其敏感微小的法线抖动在高光区域会放大成剧烈亮度变化。这个问题在日常渲染中很常遇远处瓷砖地面的反光在转动视角时像碎玻璃一样跳。单纯靠MipMap无法根除因为你滤波的是法线贴图本身但高光计算中法线方向对反射向量的微小扰动会在像素输出上形成很大变化。实际解决思路有几个一是对高光项做额外的屏幕空间平滑二是对粗糙度贴图做更积极的上限钳制避免极端镜面区域三是在采样环境贴图时配合更积极的LOD选择等效于对反射信号做低通滤波。5.4 纹理精度与色带毛刺低精度纹理和低精度插值还会带来另一种毛刺色带banding。典型表现是光照渐变的墙上出现一道道清晰的分层像等高线一样转动视角时分层边缘还会轻微跳动。这个问题严格说不算采样混叠而是量化精度的伪影。解决色带通常用两个手段一是提高中间纹理格式的量化精度能上12位或16位浮点就不要用8位固定二是给像素输出加一点点时间抖动和空间抖动把固定的分界线打散为人眼不易察觉的噪声。很多做影视离线渲染的项目会为了这个细节专门上高比特率输出实时渲染里用抖动是最经济有效的妥协。6. 非传统路线解析过滤、压缩纹理与流送方案中的取舍6.1 解析反走样在像素内直接积掉摩尔纹对于少数可以用数学公式描述的程序化纹理还有一种比MipMap更彻底的方案——解析反走样。思路是不采样纹理函数的具体值而是直接对像素覆盖区域内的纹理函数做解析积分得到精确的滤波后颜色。比如棋盘格纹理你不用在像素中心取一个点而是计算该像素覆盖了几个黑色格子、几个白色格子然后按面积比例混合颜色。这样无论相机拉多远永远不会出现摩尔纹因为信号在被采样前就已经被数学上正确地滤波了。解析反走样的局限性也很明显只适用于能写出解析表达式的纹理。砖纹、棋盘格、条纹、网格线这类规则纹理可行但照片级贴图毫无办法。我在某个跨平台渲染Demo里给地面网格线用过这种方法效果非常好——无限缩小都不会闪。如果你的项目里恰好有类似的网格线、参考线纹理非常建议尝试。6.2 纹理压缩格式与摩尔纹的关系纹理压缩格式的选择会影响摩尔纹的严重程度。常见的块压缩算法大多基于块内颜色插值的思路对边缘和细节的保真度有限。当纹理经过压缩后块状伪影本身会引入额外的高频分量这些伪影在缩小采样时同样会混叠表现为边缘附近的彩色颗粒或额外闪烁。压缩格式的抗混叠能力大致与它的保真度成正比。高保真格式在渐变和边缘上更干净摩尔纹相对轻低比特率格式伪影明显摩尔纹也更难压制。实际操作层面建议对高频纹理使用更高压缩质量的格式档位并且在后处理环节对压缩后的MipMap做肉眼检查——重点看砖缝、网格线这类边缘有没有出现马赛克状毛刺。还有一个被经常忽略的坑有些引擎在生成MipMap时用的是未压缩的源图但MipMap上传到显存后会被独立压缩不同层级的压缩伪影叠加会导致LOD切换时出现明显的清晰度抖动。遇到这种情况最好是人为对比相邻两层Mip的视觉差异必要时人为调整LOD偏置把切换点移到不易察觉的位置。6.3 流送纹理和虚拟纹理中的特殊层流失毛刺显存有限时很多项目会使用纹理流送或者虚拟纹理系统只上传当前需要的MipMap层级。这种方案有一个独特的毛刺问题当相机快速拉近时高细节层级还没有上传完成画面先用低层级撑场等高层级到位后突然啪地一下变清晰。这种瞬间清晰度跳变在动态画面里非常扎眼。处理方式主要有三种一是动态调整LOD偏置让偏置值随纹理加载进度从高往低走画面过渡更平缓二是设置常驻层级保证距离相机一定范围内的层级预上传三是给流送系统加预判逻辑提前在相机移动方向加载高细节层。我建议至少加第一层加载进度偏置实现简单效果立竿见影。6.4 抖动把规律闪烁换成噪声最后一个思路比较剑走偏锋既然摩尔纹的本质是规律性混叠那能不能在采样过程中加入微小抖动打散规律性答案是可以的。在像素输出前加入幅度极小的噪声能把大范围规律的波浪纹拆解成类白噪声颗粒。人眼对均匀噪声的容忍度远高于规律闪烁噪声看起来像胶片颗粒而摩尔纹看起来像画面坏了。这个手段我在阴影贴图采样时用过效果不错。当时是阴影边缘有一圈快速抖动的摩尔纹靠提高采样精度都压不掉最后在阴影采样坐标上引入了幅度约0.5个texel的抖动边缘就变成了一种动态的细小噪声主观观感反而稳定了。需要注意抖动幅度一定要小。幅度太大就成了明显的噪点观众会觉得你贴图脏。一般控制在1到2个灰度级内既能打散规律模式又不会引入可感知的颗粒。7. 排查纹理毛刺问题的固定动作我现在不纠结分辨率了这章算是我个人实践的小总结。每次有新项目出现远处闪、地面波浪、边缘跳这些问题我不再像以前一样先怀疑纹理分辨率而是按下面这套顺序排查。第一步静止截图。先拍一张静态图判断摩尔纹是全局规则干涉还是局部随机闪烁。全局规则干涉通常说明LOD选择整体偏低局部闪烁则可能涉及导数异常、法线归一化或者高光通路。第二步开屏幕空间导数可视化。很多调试工具能直接显示LOD值或者导数大小检查地面上是否存在异常的导数跳变线重点看图集分界、UV接缝附近。导数跳变的区域往往就是毛刺重灾区。第三步切换各向异性过滤档位。把各向异性从关闭到16x逐个试观察斜视角地面是否改善。如果改善明显说明主要瓶颈在方向性混叠如果没什么变化问题可能出在LOD偏置或者纹理内容。第四步检查法线和高光通路。关掉法线贴图或者把高光强度降为零看闪烁是否消失。如果消失问题大概率出在法线归一化或高光敏感度上而不是基础纹理过滤。第五步最后才动源纹理。如果前面都查完还没解决我会去检查纹理本身的最高频率是否过高比如细密网格、高速噪点这类贴图它们天生就容易混叠需要在美术资源阶段就做适当的频带限制。这一套动作走下来大多数问题都能定位。坦白讲最终结论常常是MipMap设置错了或者LOD偏置加了太多而不是分辨率不够。纹理分辨率这个参数真的不是解决混叠问题的第一杠杆。