在移动端渲染开发中(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)。
- 假设你的 UV 是
情况三:动态滚动 / 时间累加 (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 | // Vertex Shader |
即使你在 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**。这能帮你省去无数排查“为什么纹理在抖”或“为什么有锯齿”的时间。