原文: At Home on the Range - Why Floating Point Formats Matter in Graphics
熟练掌控数值范围 - 浮点数格式在图形中的重要性
投稿人:seanellis,2011 年 9 月 6 日
图形处理器硬件现在已支持许多不同的浮点数据格式,因此务必要了解如何为你的计算选择适当的格式,也要理解为何这一选择很重要。即使最简单的运算也能从细微的思考中大大获益。下面是我们最近碰到一个具有启发性的例子。
动画着色器
如果你要在 OpenGL ES 中创建动画着色器,你需要告知其时间。显而易见的操作方式是创建一个 uniform 变量,比如将名字取作 animation_time,再用它来修改我们要绘制的对象的某个方面,如颜色、位置或纹理。我们来看看可能会出现的一些并发症。
以下是一个非常简单的动画像素着色器,它使用该时间反复闪烁红光:
uniform mediump float animation_time;
gl_FragColor = vec3(fract(animation_time), 0.0, 0.0);
fract 函数返回 animation_time 值的小数部分,随时间推移为我们提供一个锯齿般的斜坡(斜坡曲线值)。该斜坡(值)被放在片段(像素)颜色的红色部分(分量)中,让该对象看起来在反复闪烁红光。
我们利用类似这样的 C 代码将时间传递给着色器。我们会假设以合理的帧率运行,例如每秒 30 帧(每帧 0.033 秒),并在每帧上适当提前(递增时间):
GLint location = glGetUniformLocation(myProgramObject, “animation_time”);
float animation_time = 0.0f;
while (game_not_finished_yet)
{
glUniform1f(location, true, animation_time);
/* … do some rendering here … */
/* Now advance the time (assume 30 frames/sec) */
animation_time += 0.033f; /* 0.033s = 33ms per frame */
}
起伏不定的动画
然而,这里存在一个微妙的问题,在大多数(但恼人的是并非全部)OpenGL ES 实施(产品实现)中可能会导致你的动画很快失常(你的动画会很快失常)。在典型的实施(实现)中,这一动画会变得忽动忽停(忽快忽慢),然后起伏不定,情况也会越来越糟,直到最后在大约一分半钟后完全停止。
原因难以查明。如果在循环中放入一个 printf,我们看不到任何错误。随着我们前进,该时间会平稳增加,但我们仍然会看到这一不良现象。将 C 程序改为使用双精度而不是浮点(单精度),也没效果,因此这不是精度问题。
的确如此?
范围/精度问题
关键是在着色器程序第一行中 uniform 声明上的精度说明符。OpenGL ES 着色语言指定“mediump”精度变量只需要 10 位(比特位)精度,对应于使用大约 3 位有效小数表示数字(十进制有效数字)。
如果你的实施(实现)使用这一最小值(通常是这样 - 因为整个浮点值恰好符合 16 位),从 C 的浮点值到内部值的隐式转换会四舍五入到 3 位小数(十进制数)精度。因为存在有效小数位(它们是有效数字),值的范围会影响到我们要使用的绝对精度。我将使用小数(十进制数)精度来说明发生的情况,因为这比二进制来得简单,而且也能很好地说明问题。
起初,我们没发现问题。值很小,3 位小数(十进制数)足以准确表示它们。
在我们通过 1 秒标记时,我们无法再用三位有效小数(十进制有效数字)表示这些值,开始丢失精度。动画的前进不再平滑。一些帧上我们以 0.03 单位前进,一些则为 0.04。
当我们到达 10 秒时,问题变得越来越严重,我们在多个帧之间看不到颜色变化。
在 100 秒后,我们的 3 位小数(十进制)精度无法提供任何小数部分,动画基本上永久停止(永久停止了)。
当然,在实际应用中,浮点值是以二进制来表示的,这种质量衰减(退化)会以较小步进值(2 的幂)发生,但原理是相同的。在典型的 mediump 16 位实施(实现)中,我们会在几秒之后看到质量衰减(退化),在不到 2 分钟内看到完全失败。
怎么办?
提高精度?
将 uniform animation_time 的精度提高到 highp 在某些系统上可能有帮助,但绝不是全部。OpenGL ES 规范为 highp 设定了最低限值,使得硬件能够(允许硬件)以与 mediump 相同的精度在片段(像素)着色器中实施(实现)它。由于片段(像素)着色器预计将主要用于处理颜色,受到限制的范围(取值范围)不会造成大问题,因而许多实施(实现)中实际上也正是这么做的。
匹配范围和精度
所幸的是,我们有一个简单而通用的解决办法。
在大多数动画中,我们希望在一定时间期间内重复动画。在此处的示例中,我们仅使用了 fract,因此期间为一秒。由于我们丢弃了时间的整数值,我们可以完全不传递它,效果也会完全相同。
所以,我们在着色器中取消对 fract 的调用,而将它放在 C 代码中。这意味着我们不需要以无限制的增量发送时间,而且我们能够确保留在可获得良好精度的范围(取值范围)之中。
不同的动画可能使用不同的期间(时间区间)。如果使用 sin 或 cos,可以将着色器中的输入时间固定在 0 到 2π 的范围内,或者使它保持在 0 到 1 范围并以 2π 缩放。
提及 2π 带来了最后一点。-n 到 +n 对称范围的精度是等效的 0 到 2n 范围的两倍,因为始终存在符号位,并且不会计入我们对精度位的分配。只计入 n 的绝对大小。因此对于 sin 和 cos 而言,使用 –π 到 +π 范围要优于 0 到 2π,而且也常常更加方便。
结论
总而言之,请仔细考虑你要传递到着色器中的数字的精度和范围,尤其是片段(像素)着色器。使用的范围太大会浪费精度,而精度受限时效果则不佳。
你在浮点精度上遇到过有趣的经历吗?为何不在评论中分享一下呢?
Sean Ellis 是 ARM 媒体处理分部的一名技术人员。他从 1988 年就开始从事 3D 图形工作,包括处理 ARM 最初的机器 Archimedes。他在制定 Java 移动 3D 图形标准(M3G 和 M3G2)这一规范的过程中发挥了关键作用,目前正致力于定义下一代 ARM GPU 的架构。他也参与专利工作,帮助保护 ARM 在多媒体领域的创新成果。