做图形学的人,应该都见过这种画面:地面上的格子纹理往远处延伸,细密的线条开始扭动,像水波纹一样一层层“游”过去;或者模型边缘的贴图区域出现一亮一暗的闪烁,远看像长了毛刺。第一次遇到这些现象时,很多人会怀疑是显存崩了、贴图导错了、甚至驱动抽风,但把渲染管线调出来看一圈,才发现问题出在纹理映射本身。摩尔纹和毛刺不是显示 bug,它们是离散采样和有限分辨率下必然出现的走样现象。
这篇文章不打算从数学公式堆到三维重建,而是站在一个实际做渲染项目的人的角度,把纹理映射里最常见的两类视觉瑕疵讲清楚:它们是怎么产生的,为什么改了过滤方式还压不住,以及工程上到底有哪些能落地的处理手段。不管你是在写自己的小渲染器、调移动端渲染效果,还是在排查某个跨平台系统的图像异常,这篇内容都可以直接对照排查。
1. 先把问题看明白:摩尔纹和毛刺到底长什么样
1.1 摩尔纹:两套周期性图案“打架”的产物
摩尔纹最经典的场景就是远处的地砖、铁丝网、栅栏,或者任何包含规则高频细节的物体。当你把一台相机慢慢拉远,屏幕像素网格和纹理原本的网格之间开始形成频率差,两套规则图案叠加在一起,肉眼就会看到一种新的、实际上不存在的低频波纹。
用一个容易理解的例子:你拿手机拍电脑屏幕时,屏幕自身的像素点和摄像头感光元件的像素点重合,会出现斜向的彩色条纹,这就是摩尔纹。3D 渲染里的摩尔纹原理完全一样,只不过不是两个真实硬件在互拍,而是屏幕像素的采样点和纹理像素的采样网格发生了“错位叠加”。在渲染公式里,这其实是采样频率低于纹理频率时产生的混叠。
判断一个渲染画面里的花纹是不是摩尔纹,有个很直观的特征:它不会老老实实跟随模型表面,而是会随着视角移动产生漂移、变形,甚至在原地蠕动。因为它是采样产生的副产物,不是模型本身的材质属性。
1.2 毛刺:像素边缘的高频闪烁和高亮抖动
毛刺在画面上表现为贴图区域边缘的锯齿状亮线、高光点跳跃,或者像玻璃反射一样突然冒出来的小白点。它多数出现在纹理被严重缩小、或者纹理坐标梯度变化非常剧烈的区域。
很多人会把毛刺单纯理解为“边缘锯齿”,但它们和几何边缘锯齿并不完全一样。几何锯齿是三角形边界的像素走样,毛刺更多来自纹理内容的局部高频突然和采样率失配。比如一张贴图里有细小的字母、碎石子或高光点,当相机移动时,这些细点在不同的像素位置上被采到或漏采,看起来就像屏幕上跳出一堆小毛刺,远看甚至感觉物体表面一直在“沙沙响”。
如果纹理带有透明通道,毛刺还会表现为半透明边缘的亮边或黑边。这种问题往往不是采样导致,而是 Mipmap 生成时透明像素和边缘像素被混在一起,导致边缘颜色被污染。后面会专门展开。
1.3 摩尔纹和毛刺的同源性
这两类问题经常一起出现,因为它们的根源都是同一个:屏幕像素的采样频率和纹理信号的频率不匹配。摩尔纹偏重“低频伪纹理”,毛刺偏重“高频单点闪烁”,但如果你只是去调颜色、调 UV,不解决采样问题,它们会反复出现。
理清这一点,排查的时候才能顺着同一条主线走,而不是今天改贴图、明天调 LOD,最后越调越乱。
2. 从采样数学看根因:为什么纹理映射这么容易走样
2.1 一个屏幕像素对应的不是纹理上的一个点
纹理映射最基础的想法是给模型表面的每个点找一张颜色。但在光栅化过程中,一个屏幕像素往往覆盖了纹理上一大片区域,而不是精确的一个纹理像素。
试想一个地面平面:离相机近的地方,一个屏幕像素可能只覆盖一两个纹理像素;到了远处,一个屏幕像素可能覆盖几十甚至上百个纹理像素。如果这时候你仍然用屏幕像素中心点去采样一个点,那么远处的纹理细节会被随机跳过,有些细线、亮点就被漏掉了,有些则被放大显示,产生明显的闪烁。
图形学里用微分几何来描述这个过程,通常会算dTdx和dTdy,也就是纹理坐标在屏幕 x 方向和 y 方向的变化率。这两个值放大了看,就是屏幕像素在纹理空间里的“足迹”。足迹越大,说明当前屏幕像素覆盖的纹理区域越大,越需要做预过滤,而不是单点采样。
2.2 不滤波就是暴力抽签
假设一张地板贴图是 1024x1024,分辨率相当高。当它在屏幕上只占 100 个像素时,纹理里 99% 的信息根本无法显示。如果渲染器不做任何过滤,每个屏幕像素就只抽签抽一个纹理像素,抽中哪个显示哪个。
从频率角度说,这就是采样率低于纹理信号的奈奎斯特频率,高频信号混叠成了低频假信号。摩尔纹就是你看到的“假低频信号”,毛刺就是“漏采和偶采出来的高频点”。
很多开发者在排查时想到的第一个方法是“把贴图画小一点”。这确实能缓解高频混叠,因为贴图小了,高频能量就少了,但这是治标不治本,还会让近距离的贴图模糊。正确的思路不是去掉纹理里的高频,而是根据屏幕像素的足迹大小,把纹理预先过滤成不同的分辨率层级,按需取用。
2.3 像素足迹的形态还会变化
屏幕上不同地方的像素足迹大小并不均匀。比如俯视地面时,近处足迹接近正方形,远处却可能拉得很长,像一条细长的椭圆。只看足迹面积还不够,还得看它的长宽比例和方向。
这也解释了为什么有时候你已经开了 Mipmap,远处不闪了,但斜向的地面还是花。因为普通 Mipmap 用一个正方形区域去近似椭圆形的像素足迹,误差被直接保留下来。要处理这种场景,就得用各向异性过滤,后面会讲。
3. 工程上的主流解法:从过滤状态到纹理内容
3.1 纹理过滤状态是第一步,也是最容易被忽略的一步
很多图形 API 默认把纹理过滤设为Nearest或者Bilinear。Nearest就是前面说的抽签,放大时马赛克、缩小时摩尔纹,效果最差。Bilinear在放大时比Nearest好很多,但在缩小时依然会触发走样,因为它仍然只做最近 2x2 纹理像素的插值,没有覆盖远距离采样的大片足迹。
至少要做到线性过滤,但“线性过滤开完了还是会有摩尔纹”是很正常的,因为线性过滤只解决了相邻纹理像素的平滑,没有解决“采样足迹过大”的问题。要让远处不出现摩尔纹,必须搭配分级纹理,也就是 Mipmap。
3.2 Mipmap:把纹理预先“压扁”,按需取用
Mipmap 的原理简单说,就是提前把一张大纹理逐级缩小成一半、四分之一、八分之一……直到 1x1,形成一组分辨率递减的纹理链。渲染时根据屏幕像素的足迹大小,选一个合适的层级来采样,保证每个屏幕像素采到的纹理信息刚好匹配它的显示能力。
选择层级的核心是求纹理坐标在屏幕方向上的变化率,也就是前面说的dTdx、dTdy。常用方法是用两者的长度比算出一个粗糙度值,再取对数得到 Mipmap 层级:
- 足迹小,就用高分辨率层级;
- 足迹大,就用低分辨率层级;
- 足迹介于两层之间,就在两层之间做三线性插值。
三线性过滤的意思是:先在同一层级内做双线性插值,再在相邻两级之间做混合。这样相机移动时,纹理清晰度的跳变会被平滑掉,不会出现“一格一格”突然变糊的效果。
这里要提醒一个关键点:Mipmap 只能解决“缩小后信息溢出的问题”,它不能解决放大后贴图模糊的问题。放大时应该用GL_LINEAR_MAG_FILTER,并且接受纹理分辨率本身的限制,不要指望 Mipmap 能把放大后的画面救回来。
3.3 各向异性过滤:处理斜面和掠射角的关键
地面、墙面、马路这类大平面,往往与相机成很斜的角度,屏幕像素在纹理空间里的足迹是一个很扁的椭圆。普通 Mipmap 用正方形足迹近似这种椭圆,导致近处那层贴图被过度模糊,远处又残留摩尔纹。
各向异性过滤的思路,是对这个椭圆足迹沿着长轴方向做多次采样,再把结果加权平均。实现上通常用一个比率表示最大各向异性程度,比如 4x、8x、16x。数值越大,斜视角下的清晰度越好,但采样次数和带宽压力也越大。
各向异性不是越高越好。很多项目在 8x 和 16x 之间几乎看不出区别,性能开销却翻倍。移动端甚至 4x 就够用。更多时候,解决斜视角摩尔纹的关键不是把比率拉到最高,而是要保证 Mipmap 和三线性过滤先开对,因为各向异性过滤是建立在此基础上的。
3.4 贴图内容层面的辅助手段
采样手段能解决掉大部分问题,但有些材质的特性决定了它很容易产生视觉噪点,比如高频噪波纹理、小字标签、密集的栅栏花纹。这类纹理即便用了 Mipmap,某个层级仍然可能残留强烈的高频信号,导致运行时出现随机闪烁。
比较实用的做法是在美术层面做预过滤。例如用带低通滤波的缩图工具生成 Mipmap,或者直接在原始贴图上做模糊降频。很多老项目会把珠宝、金属刮痕、纸张纤维这类高频贴图单独处理,在保留近景细节的同时,控制中远距离的视觉噪点。
另一种思路是把高频细节从颜色贴图里拆出来,改成法线贴图或者粗糙度贴图。颜色通道负责大色块,法线通道负责高频细节。这样即使细节在远处无法逐像素显示,也能通过光照方向的低频变化表现出材质质感,而不是直接变成摩尔纹或闪烁。
4. 实操:在模拟项目里修复地面摩尔纹和边缘毛刺
4.1 先检查渲染状态配置
我在一个模拟项目里做过一次典型修复。当时症状非常明显:地面网格远处全是蠕动的彩色条纹,墙面高光处有小亮点跳跃。后来检查发现,纹理的滤波参数根本没有被正确设置,Mipmap甚至没有生成。
以 OpenGL 为例,最基本的配置是这样的:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); glGenerateMipmap(GL_TEXTURE_2D);注意MIN_FILTER和MAG_FILTER要区分开。很多人的习惯是把两个过滤方式都设置成GL_LINEAR,但MAG_FILTER没有 Mipmap 层级概念,不能设置成GL_LINEAR_MIPMAP_LINEAR,否则部分驱动会直接报错或者忽略设置。
设置完不是立刻生效,还得确认当前采样的着色器没有手动绕过过滤状态。有些自定义 shader 会直接用textureLod强制指定某一层 Mipmap,如果这里写死了层级,上面配置全部白费。
4.2 处理 UV 接缝和纹理边缘的毛刺
开了 Mipmap 后,另一个高频问题会浮现出来:贴图接缝处出现一条亮边或黑边。
这通常发生在纹理图集或者重复纹理的边界。当 Mipmap 层级变低时,低分辨率贴图会把相邻区域的像素混在一起,于是本应透明的接缝位置混入了旁边的颜色,或者重复铺贴时边界采样到了 UV 范围之外的数据。
最简单的处理方式是给纹理边缘加 Padding,也叫出血边。也就是在纹理图集里,每个子图周围复制一圈相邻像素,保证任何插值操作都不会采样到图集外像素。对于单张连续纹理,要确保WRAP模式正确,Repeat 模式下边缘会自动循环,Clamp 模式下边缘会拉伸,视觉表现完全不同。
排查时有个技巧:贴一张黑白棋盘格的 UV 检查纹理,把模型的 UV 拉伸、方向、采样范围全部显示出来。纹理接缝处的毛刺如果和棋盘格条纹重叠,基本可以确定是 UV 边界问题,而不是单纯采样问题。
4.3 用各向异性过滤压制斜视角
平铺地板出现远处摩尔纹,而 Mipmap 已经开启,下一个要调的就是各向异性过滤。在 OpenGL 里可以这样开:
GLfloat maxAniso = 0.0f; glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, &maxAniso); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, maxAniso);直接把最大值拉满不是好做法。先设成 4x,看远处斜面还有没有明显“磨砂感”,不行再提到 8x。很多硬件上 16x 和 8x 的视觉差异极小,但带宽消耗却相差很大,特别是在移动端。
还有一个容易被忽略的细节:各向异性过滤获取的是纹理坐标的偏导数,如果使用了程序化纹理或者经过复杂 UV 变换,驱动算出来的偏导数可能不准。这时候需要手动在 shader 里用dFdx、dFdy计算并传递给采样函数,否则过滤方向会选错,开了等于没开。
4.4 验证修复效果的对比方法
修完以后,不要只看“好像顺眼了一点”。最可靠的方法是把相机放到倾斜角度,用连续移动的方式观察地面远处。摩尔纹和毛刺都是动态现象,单帧截图容易骗人。
还可以用 Debug 工具把当前选中的 Mipmap 层级可视化出来。比如在 shader 里输出textureQueryLod的结果,就能看到每一块屏幕区域实际在使用第几层贴图。如果某个区域长期停留在最模糊的层级,但画面里仍有摩尔纹,说明层级选择逻辑出了问题,而不是过滤没开。
5. 常见问题排查与避坑记录
5.1 快速对照表
| 现象 | 可能原因 | 优先处理手段 |
|---|---|---|
| 远处规则纹理出现蠕动波纹 | 未开 Mipmap 或 MIN_FILTER 不对 | 设置三线性过滤并生成 Mipmap |
| 贴图放大后满屏马赛克 | MAG_FILTER 用了 Nearest | 改为线性过滤 |
| 斜面远处又糊又花 | 各向异性过滤未开或层级不足 | 调高各向异性滤过比率 |
| 边缘毛刺闪烁 | 图集接缝采样到未覆盖像素 | 加出血边或调整 UV Padding |
| 相机移动时有明显贴图层级跳变 | 使用了双线性 Mipmap 而非三线性 | 改用 LINEAR_MIPMAP_LINEAR |
| 透明贴图边缘发亮 | Mipmap 生成污染衰减通道 | 在生成 Mipmap 时保留透明通道,或使用预乘 Alpha |
5.2 实战中容易踩的坑
第一,不要在贴图导入流程里直接丢弃 mipmap。很多美术规范喜欢“为了节省包体”把所有贴图改成无 mipmap,导致运行时所有远处纹理都是摩尔纹。节省包体可以用纹理压缩,但压缩纹理也要在压缩前生成好 mipmap 链。
第二,谨慎使用负方向的 LOD Bias。把 Mipmap 层级往高分辨率方向偏移能让画面更清晰,但同时会把大量高频信号带回采样管线,毛刺和摩尔纹会卷土重来。我在项目里见过美术为了追求锐度把 LOD Bias 设成 -2,结果整个场景远看都在闪。正确的做法是用各向异性过滤去补清晰度,而不是靠负 Bias 硬拉。
第三,纹理图集的排序会影响采样质量。如果把高对比度纹理和纯色纹理放在同一张图集里,低 Mipmap 层级时颜色会互相渗透,边缘出现彩色晕染。制作图集时要充分考虑低分辨率下的表现,必要时隔离高频贴图。
6. 最后分享一点实际经验
纹理映射里的摩尔纹和毛刺,说到底是采样频率不足的问题,理解了这一点,排查就会变得很有条理:先看过滤状态,再看 mipmap 生成,最后看各向异性设置。多数场景下,这三步按顺序走完,画面问题能解决八成以上。
剩下那两成,往往是美术资源和运行时参数打架的问题。比如贴图颜色对比度过高、UV 摆放过于密集、LOD Bias 设置不合理。这类问题没法靠单一设置修复,需要渲染程序员和美术一起把资源参数重新梳理一遍。
我个人在实际项目里还有一个土办法:在正式制作整套贴图前,先给一块带大量规则花纹的测试地面调好过滤参数,之后所有材质都以这个地面作为基准。这样每次新材质进来,只需要看它和基准地面的差距,就能快速判断是资源问题还是管线问题,不用每次从头排查。这个方法虽然简单,但在我处理跨平台渲染适配时省了非常多的时间。