mediump(中精度)和 highp(高精度)对比分析

在移动端渲染开发中(OpenGL ES / Vulkan / Metal),mediump(中精度)和 highp(高精度)的选择对性能和画质影响巨大。

对于 UV 坐标(纹理坐标) 而言,精度的选择直接决定了纹理采样的准确性。如果精度不足,会导致画面出现抖动、锯齿、马赛克或拉伸

以下是深度分析:


1. 基础硬指标:FP16 vs FP32

首先我们需要知道这两种修饰符在硬件层面通常对应的数据格式(尽管具体实现取决于 GPU):

  • mediump (半精度浮点数, FP16)

    • 结构: 1 bit 符号 + 5 bits 指数 + 10 bits 尾数 (Mantissa)
    • 有效精度: 约 3 位十进制数字。
    • 分辨率: $2^{-10} = \frac{1}{1024}$。这意味着在 [1.0, 2.0) 区间内,它只能区分 1024 个刻度。
  • highp (单精度浮点数, FP32)

    • 结构: 1 bit 符号 + 8 bits 指数 + 23 bits 尾数 (Mantissa)
    • 有效精度: 约 7 位十进制数字。
    • 分辨率: $2^{-23} \approx \frac{1}{8,388,608}$。精度极高,远超当前屏幕像素密度。

2. 不同数据范围下的表现分析

UV 精度问题的核心在于:浮点数的精度随着数值绝对值的增大而降低
我们将场景分为三种情况:标准范围、平铺/大坐标、动态滚动。

情况一:标准 UV 范围 $[0.0, 1.0]$

这是最常见的模型贴图或 UI 贴图范围。

  • 纹理尺寸 512x512

    • 单像素宽度:$1/512 \approx 0.0019$
    • mediump 精度:在 $0 \sim 1$ 范围内,步长约为 $1/1024 \approx 0.00097$。
    • 结果:$0.00097 < 0.0019$,精度足够。每个纹理像素都能被准确索引。
  • 纹理尺寸 1024x1024

    • 单像素宽度:$1/1024 \approx 0.00097$
    • mediump 精度:约为 $0.00097$。
    • 结果临界状态。FP16 的采样步长刚好等于纹理像素大小。可能会出现轻微的采样偏移,但在双线性插值下通常肉眼难以察觉。
  • 纹理尺寸 2048x2048 及以上

    • 单像素宽度:$1/2048 \approx 0.00048$
    • mediump 精度:依然是 $0.00097$。
    • 结果精度不足。FP16 的最小步长已经是 2 个像素宽了。你会看到纹理变得模糊,或者在缓慢移动视角时纹理出现“跳动”(Snapping)。

情况二:平铺纹理 / 大坐标范围 $[0.0, 10.0]$ 或更大

这是地面重复贴图、墙面平铺的常见场景。UV 值可能达到 10、20 甚至 100。

  • 原理:浮点数在数值越大时,刻度越稀疏。

    • 在 $[0, 1)$ 范围,FP16 精度是 $1/1024$。
    • 在 $[1, 2)$ 范围,FP16 精度是 $1/512$。
    • 在 $[2, 4)$ 范围,FP16 精度是 $1/256$。
    • 在 $[8, 16)$ 范围,FP16 精度是 $1/64$。
  • 灾难推演

    • 假设你的 UV 是 u_tiling * v_uv,结果范围到了 $[8.0, 16.0]$。
    • 此时 mediump 的最小分辨单位是 $1/64$。
    • 如果你的贴图还是 1024x1024 的,你需要 $1/1024$ 的精度。
    • 差距:你需要的精度是现有精度的 16 倍。
    • 视觉效果:严重的马赛克化。原本平滑的直线会变成阶梯状,纹理看起来像是被强制缩小了分辨率(Pixelated)。

情况三:动态滚动 / 时间累加 (Flow map, Water)

在 Shader 中常见代码:vec2 uv = v_uv + vec2(time * speed, 0.0);

  • 问题:随着 time 的增加,UV 的整数部分越来越大。
  • 现象
    • 游戏刚开始运行(Time < 100):水面流动正常。
    • 游戏运行 10 分钟后(Time > 1000):水面流动开始出现卡顿、跳变。
    • 游戏运行久了:水面纹理完全静止不动,因为 time 增加的微小量(Delta Time)已经小于 FP16 在那个数值下的最小精度(Machine Epsilon),导致加法无效。

3. Vertex Shader vs Fragment Shader 的陷阱

这是一个极其容易被忽视的环节:Varying 插值精度

1
2
3
4
5
6
// Vertex Shader
varying mediump vec2 v_uv; // 坑点在这里!
void main() { ... }

// Fragment Shader
varying mediump vec2 v_uv;

即使你在 Fragment Shader 中使用了 highp 进行纹理采样,如果 Varying 变量(从 VS 传到 FS)被声明为 mediump,那么插值过程就是低精度的。

  • 现象:当摄像机贴近物体表面观察时,纹理会随着视角的微小移动而发生剧烈的形变或抖动(Wobble)。这是因为光栅化插值时的坐标精度不够,导致 UV 坐标在像素之间“乱跳”。

4. 总结与最佳实践建议

场景 推荐精度 原因
UI 贴图 / 2D 精灵 mediump 通常坐标在 0-1 之间,且纹理通常不会过大(<1024)。
3D 模型 (UV 0-1) mediump / highp 1024以下纹理可用 mediump;2048以上或对画质要求高必须用 highp。
地形 / 地面 / 墙壁 highp 只要涉及 UV Tiling (uv * 10.0),mediump 必死无疑。
纹理动画 / 滚动 highp 避免随时间推移产生精度丢失导致的卡顿。
法线贴图 / 视差映射 highp 法线对精度极其敏感,UV 的微小抖动会导致光照剧烈闪烁。
Varying 传递 highp 现代 GPU highp 性能开销通常可接受,建议默认 v_uv 使用 highp 以避免插值抖动。

快速判断公式 (针对 mediump)

如果你想用 mediump,请确保满足以下条件:

$$ \text{MaxUV} \times \text{TextureSize} < 1024 $$

  • MaxUV: UV 坐标的最大值(例如 Tiling 是 4,则为 4)。
  • TextureSize: 纹理分辨率(例如 512)。

例子

  • 512px 纹理,UV Tiling = 2.0 $\rightarrow 512 \times 2 = 1024$ (勉强可用)
  • 1024px 纹理,UV Tiling = 1.0 $\rightarrow 1024 \times 1 = 1024$ (勉强可用)
  • 1024px 纹理,UV Tiling = 4.0 $\rightarrow 4096 \gg 1024$ (不可用,会出现严重马赛克)

结论

在移动端现代 GPU(Adreno 5xx/6xx, Mali G-series, Apple GPU)上,highp 的性能损耗已经非常小了。

黄金法则:
除非你是在做极端的性能优化(针对低端机)且确定 UV 永远在 [0,1] 范围内且纹理很小,否则**UV 相关的计算和 Varying 传递请无脑使用 highp**。这能帮你省去无数排查“为什么纹理在抖”或“为什么有锯齿”的时间。