WEBKT

移动端GPU上ASTC格式法线贴图的高精度双线性过滤调优实践

8 0 0 0

在移动端游戏开发中,法线贴图的视觉质量直接决定了光影细节的细腻程度。然而,在使用ASTC(Adaptive Scalable Texture Compression)压缩格式后,法线贴图在移动端GPU进行双线性过滤(Bilinear Filtering)时,经常会出现明显的块状暗斑、阶梯状条纹(Banding)以及高光闪烁。

这些问题并非完全由压缩率导致,而是ASTC编码机制、硬件插值精度与法线重构数学公式共同作用的结果。以下将从底层原理出发,提供一套从纹理压制、硬件对齐到Shader端高精度重构与自定义过滤的完整调优方案。


一、 为什么ASTC法线贴图会出现过滤瑕疵?

1. 块内端点拟合导致的通道间干扰(Crosstalk)

标准的ASTC压缩算法在默认情况下会将RGB通道作为一个整体进行端点选择和权重拟合。法线贴图的 $X$ 和 $Y$ 分量(通常存储在 $R$ 和 $G$ 通道)数值分布极不规律。若强行使用单一颜色端点(Single Plane)进行压缩,会导致两个通道的局部量化误差相互干扰,产生块(Block)边界的法线突变。

2. 双线性插值在非线性重构下的失效

硬件双线性过滤是在采样时对存储的 $X$ 和 $Y$ 原始量化值进行线性插值,然后在片元着色器中进行非线性重构:
$$Z = \sqrt{1 - X^2 - Y^2}$$
由于 $\sqrt{1 - x^2 - y^2}$ 是高度非线性的,对 $X$ 和 $Y$ 进行线性插值后重构得到的 $Z$ 值,与直接对正常 $Z$ 进行插值的结果存在偏差。在法线斜率接近 $1$(即贴图表面极度平缓,接近主法线方向)时,微小的 $X, Y$ 差值会导致 $Z$ 计算结果发生剧烈跳变,从而在双线性过滤过渡带产生肉眼可见的黑斑和硬边。

3. 移动端GPU硬件插值的精度限制

部分移动端GPU(如中低端Mali或早期的Adreno)在执行纹理采样过滤(Texture Filtering Unit)时,为了节省功耗,内部插值计算可能在FP16(半精度)甚至更低精度的专用硬件逻辑中进行。量化误差与低精度插值叠加,使双线性过滤无法平滑过渡。


二、 压缩端优化:双通道独立平面的最佳实践

要提升过滤精度,首先必须在源头上消灭通道间的量化干扰。最有效的手法是启用ASTC的双平面(Dual-Plane)模式,将 $X$ 和 $Y$ 分量彻底解耦。

1. 推荐编码参数

推荐使用 astcenc 官方编译器,将法线贴图的 $X$ 存储在 $R$ 通道,$Y$ 存储在 $A$ 通道,并强制指定双平面模式,让两个通道拥有独立的权重值。

使用以下命令行进行压制:

astcenc-sse4.2 -cl normal.png normal_compressed.astc 5x5 -medium -normal -cw 1 0 0 1
  • -normal 参数:此参数是专门针对法线的优化模式。它会自动将贴图转为双通道($X$ 在 $R$ 别名,$Y$ 在 $A$ 别名,其余通道丢弃),并自动启用双平面编码,分配最充足的比特率给这两个通道。
  • -cw 1 0 0 1:配置通道权重。我们只关心 $R$(第1通道)和 $A$(第4通道),强制忽略 $G$ 和 $B$,使编码器的误差评估完全聚焦在 $X, Y$ 的重建精度上。
  • 尺寸选择
    • 5x5 (5.12 bpp):高端写实角色、核心武器法线贴图首选,能完美消灭大部分块状边界。
    • 6x6 (3.56 bpp):场景大面积硬表面、次要道具的黄金折中点。
    • 8x8 (2.00 bpp):不推荐用于精细法线,过滤后会有严重的马赛克感。

三、 采样端优化:UV精度与硬件状态对齐

在移动端GPU上,Shader内的纹理坐标(UV)精度会直接决定双线性过滤的寻址质量。

1. 保证UV的 highp 精度

如果UV使用 mediump(16位浮点数),当贴图分辨率较大(如2048x2048)或瓦片平铺次数(Tiling)较高时,微小的UV阶梯变化会破坏硬件双线性过滤器的插值权重计算,导致过滤失效产生锯齿。

// 顶点着色器与片元着色器中均须强制声明
v2f {
    highp vec2 uv; 
};

2. 规避 sRGB 转换

确保法线贴图在引擎(如Unity, Unreal)中被标记为 Non-Color (Linear) 纹理。如果误启用了 sRGB,硬件在采样时会先进行 Gamma 校正逆转换再执行双线性插值,这会彻底摧毁法线数值的线性度,导致重构出的法线产生严重的畸变。


四、 Shader端高精度重构调优

在片元着色器中重构法线时,传统的直接重构会放大双线性过滤的瑕疵。我们需要通过数学手段对边缘区域进行平滑补偿。

1. 基础重构算法的精度修正

传统的半球重构公式:

// 基础写法:在临界区极易产生NaN或剧烈跳变
half2 normalXY = packedTex.ra * 2.0 - 1.0;
half normalZ = sqrt(1.0 - dot(normalXY, normalXY));

为了避免双线性插值后的 $X^2 + Y^2$ 略微大于 $1.0$ 从而导致 sqrt 产生 NaN(表现为黑点),以及平缓地带的过渡硬边,应引入微小的缩放因子与安全阈值保护:

mediump vec4 packedTex = texture(u_NormalMap, v_UV);
// 提取 R & A 分量
mediump vec2 normalXY = vec2(packedTex.r, packedTex.a) * 2.0 - 1.0;

// 1. 引入微量收缩,容忍由于硬件双线性插值越界导致的误差
normalXY *= 0.996; 

// 2. 使用安全截断重构Z,平滑过渡极值区
mediump float dotXY = dot(normalXY, normalXY);
mediump float normalZ = sqrt(clamp(1.0 - dotXY, 0.0, 1.0));
vec3 normal = vec3(normalXY, normalZ);

五、 极致画质解决方案:Shader内手动FP32双线性过滤

在某些对画质要求极高的前台核心物件上(例如展厅模式下的车辆外壳、写实角色的面部),如果移动端GPU的硬件插值精度(FP16)依然导致高光出现轻微的块状斑点,我们可以通过在Shader内使用 textureGather 提取原始邻域像素,并在片元着色器内执行完全基于FP32(单精度)的手动双线性过滤

这种做法绕过了硬件低精度的纹理过滤单元,能够提供物理级别的最高精度平滑过渡。

1. 算法实现原理

  1. 使用 textureGather 一次性获取相邻的4个Texel。由于ASTC使用双平面($R$ 和 $A$),我们需要分别针对 $R$ 通道和 $A$ 通道各进行一次 Gather 采样。
  2. 在片元着色器中计算当前UV在Texel坐标系下的浮点小数部分(即插值权重)。
  3. 使用FP32精度手动执行 mix 完成双线性插值,最后再进行正常法线重构。

2. GLSL ES 3.1 落地实现代码

#version 310 es
precision highp float;

uniform sampler2D u_NormalMap;
uniform vec4 u_NormalMap_TexelSize; // x: 1/w, y: 1/h, z: w, w: h

in highp vec2 v_UV;
out vec4 outColor;

// 手动高精度双线性插值函数
vec2 GetHighPrecisionNormalXY(highp vec2 uv)
{
    // 计算纹理空间坐标,并平移0.5个像素以对齐采样点中心
    highp vec2 texelPos = uv * u_NormalMap_TexelSize.zw - 0.5;
    highp vec2 f = fract(texelPos); // 计算小数部分(插值权重)
    
    // textureGather在GLSL ES 3.1+中支持
    // 分别获取相邻4个Texel的R通道 (X分量) 和 A通道 (Y分量)
    // 返回值顺序:[x: (u, v+1), y: (u+1, v+1), z: (u+1, v), w: (u, v)]
    highp vec4 texelsX = textureGather(u_NormalMap, uv, 0); // Component 0 = Red
    highp vec4 texelsY = textureGather(u_NormalMap, uv, 3); // Component 3 = Alpha (对于RGBA格式通常是3)

    // 对X分量进行FP32线性插值
    highp float x_bottom = mix(texelsX.w, texelsX.z, f.x); // (u, v) -> (u+1, v)
    highp float x_top    = mix(texelsX.x, texelsX.y, f.x); // (u, v+1) -> (u+1, v+1)
    highp float interpolatedX = mix(x_bottom, x_top, f.y);

    // 对Y分量进行FP32线性插值
    highp float y_bottom = mix(texelsY.w, texelsY.z, f.x);
    highp float y_top    = mix(texelsY.x, texelsY.y, f.x);
    highp float interpolatedY = mix(y_bottom, y_top, f.y);

    // 重构到 [-1, 1] 空间
    return vec2(interpolatedX, interpolatedY) * 2.0 - 1.0;
}

void main()
{
    // 执行高精度过滤
    highp vec2 normalXY = GetHighPrecisionNormalXY(v_UV) * 0.996;
    
    // FP32精度下的Z轴重构
    highp float dotXY = dot(normalXY, normalXY);
    highp float normalZ = sqrt(max(0.0, 1.0 - dotXY));
    
    vec3 worldNormal = vec3(normalXY, normalZ);
    
    // 正常执行光照计算...
    outColor = vec4(worldNormal * 0.5 + 0.5, 1.0);
}

注:此方案仅建议针对高精度要求物件局部开启。虽然 textureGather 的开销低于4次独立的 texture 采样,但在中低端芯片上,两次 Gather 配合高精度ALU插值仍会带来一定的性能开销。


六、 调优效果及开销评估

在实际调优中,应当结合项目预算在画质与开销之间取得平衡。下表展示了不同配置组合在移动端硬件上的表现:

方案名称 压制配置 采样/重构方式 VRAM消耗 ALU/带宽开销 边缘暗斑控制 高光平滑度 推荐适用场景
标准法线 ASTC 8x8 单平面 硬件Bilinear + 默认重构 2.00 bpp 极低 差 (块状感重) 差 (条纹明显) 大面积草地、无高光的粗糙地形
主流性能级 ASTC 6x6 双平面 硬件Bilinear + 0.996缩放 + Z截断 3.56 bpp 良 (偶有暗缝) 中等 场景通用物件、硬表面建筑
高精表现级 ASTC 5x5 双平面 硬件Bilinear + 0.996缩放 + Z截断 5.12 bpp 玩家第一视角武器、核心载具
极致画质级 ASTC 5x5 双平面 手动FP32 textureGather 过滤 5.12 bpp 中等 (需要支持ES 3.1) 极佳 (物理级无损) 极佳 展厅展示、特写CG级角色皮肤

调优总结步骤

  1. 第一步:全面废弃单平面普通ASTC法线贴图,统一将管线切为 astcenc -normal(双平面 R&A 编码) 模式。
  2. 第二步:检查引擎纹理导入设置,确保法线贴图的 Linear 属性被勾选,关闭 sRGB。
  3. 第三步:在Shader中将法线贴图采样UV提升至 highp,并在重构 $Z$ 分量时引入 0.996 的缩放因子,消除硬件插值溢出。
  4. 第四步:针对极限高光瑕疵,局部启用 textureGather 手动FP32双线性插值,消除硬件硬件插值精度不足造成的条纹现象。
技艺工匠 ASTC压缩移动端GPU法线贴图优化

评论点评