我们要彻底理解这个问题,需要深入到计算机底层是如何存储浮点数(Floating Point)的。这里涉及到一个核心概念:科学计数法(Scientific Notation)的二进制版本。
我们把这个问题拆解成三个部分:结构解析、为什么是 1024 个刻度、为什么范围不同精度不同。
1. 结构解析:计算机怎么存小数?
mediump (FP16) 只有 16 个格子(bits)来存数字。它不可能把所有数字都存下来,所以它采用了“滑动窗口”的策略。
结构如下:
- 符号位 (1 bit): 正数还是负数。
- 指数位 (5 bits, Exponent): 决定了数字的量级(是几十、几千、还是零点几)。你可以把它理解为标尺的缩放倍率。
- 尾数位 (10 bits, Mantissa): 决定了在这个量级下的精度。你可以把它理解为标尺上的刻度线。
类比:
想象你有一把尺子,这把尺子只有 1024 个刻度(由 10 bits 尾数决定,$2^{10}=1024$)。
- 当你测量蚂蚁时(指数小),你把尺子缩得很短,这 1024 个刻度非常密集,能测得很准。
- 当你测量大楼时(指数大),你把尺子拉得很长,这 1024 个刻度就被拉稀疏了,精度就变差了。
2. 为什么在 [1.0, 2.0) 区间只有 1024 个刻度?
这是由浮点数的标准公式决定的(IEEE 754)。
一个归一化(Normalized)的浮点数的值是这样计算的:
$$ \text{数值} = 2^{\text{指数}} \times (1 + \text{尾数}) $$
- 注意那个 **$1 +$**:这是计算机的一个“聪明设计”。因为二进制下的科学计数法,首位永远是 1(就像十进制科学计数法首位不能是 0 一样,比如 $1.23 \times 10^5$),所以计算机默认省略了这个 1,只存小数点后面的部分,这样能多白嫖 1 bit 的精度。
让我们来看看 [1.0, 2.0) 这个区间发生了什么:
要表示 1.0 到 1.999… 之间的数,指数(Exponent)必须固定为 0(因为 $2^0 = 1$)。
- 基准值: $2^0 = 1$。
- 变化部分: 全靠那 10 bits 的尾数 来微调。
尾数有 10 位,每一位可以是 0 或 1。
- 最小的尾数是
0000000000(十进制 0) - 最大的尾数是
1111111111(十进制 1023) - 总共有 $2^{10} = 1024$ 种组合。
因此,在 $1 \times (1 + \text{尾数})$ 这个公式里,尾数把 $[0, 1)$ 这个范围切成了 1024 份。
- 第 0 个刻度 (1.0): $1 \times (1 + \frac{0}{1024}) = 1.000000$
- 第 1 个刻度: $1 \times (1 + \frac{1}{1024}) \approx 1.000976$
- 第 2 个刻度: $1 \times (1 + \frac{2}{1024}) \approx 1.001953$
- …
- 第 1023 个刻度: $1 \times (1 + \frac{1023}{1024}) \approx 1.999023$
结论:
在 1.0 到 2.0 之间,你无法表示 1.0005 这样的数。你只能选择 1.0000 或者 1.000976。
这就是所谓的分辨率(Resolution)为 $1/1024$。
3. 为什么“大约 3 位十进制精度”?
这只是一个换算估值。
我们常用的十进制,3 位数字(000~999)能提供 1000 个刻度。
二进制的 10 bits 提供了 1024 个刻度。
因为 $1024 \approx 1000$,所以我们常说 FP16 的精度大约相当于十进制的 3 位有效数字。
这意味着:
- $1.234$ (4位) 可能存不准。
- $1.23$ (3位) 通常能存准。
4. 扩展:为什么数值越大,精度越差?(关键点)
刚才我们看的是指数为 $2^0=1$ 的情况([1, 2]区间)。
如果我们把数值变大,比如到了 [2048, 4096] 这个区间,会发生什么?
此时指数变成了 $2^{11} = 2048$。
公式变成了:
$$ \text{数值} = 2048 \times (1 + \text{尾数}) $$
尾数依然只能切分 1024 份。
也就是把 2048 到 4096 中间这段长度为 2048 的距离,切成 1024 份。
- 每一步的步长(精度) = $\frac{\text{区间长度}}{\text{刻度数量}} = \frac{2048}{1024} = 2.0$
恐怖的结论:
在 [2048, 4096] 这个范围内,mediump 无法表示奇数!
它只能表示 2048, 2050, 2052…
如果你想存 2049,它会被强制舍入到 2048 或 2050。
这就是为什么我在上一个回答中说,如果你的 UV 坐标通过 Tiling 变得很大(比如 uv * 100),mediump 精度会崩坏,因为“尺子的刻度”变得非常稀疏,已经大于一个像素的宽度了。