不使用modf()获取浮点数的小数部分

33

我正在为一个没有数学库的平台开发,所以我需要构建自己的工具。我当前获取分数的方式是将浮点数转换为定点数(乘以(float)0xFFFF,转换为int),仅获取低位部分(使用0xFFFF进行屏蔽),然后再将其转换回浮点数。

然而,精度问题让我困扰不已。我使用我的Frac()和InvFrac()函数绘制抗锯齿线条。使用 modf 可以得到完美平滑的线条。但使用自己的方法时,由于精度损失,像素会开始跳动。

以下是我的代码:

const float fp_amount = (float)(0xFFFF);
const float fp_amount_inv = 1.f / fp_amount;

inline float Frac(float a_X)
{
    return ((int)(a_X * fp_amount) & 0xFFFF) * fp_amount_inv;
}

inline float Frac(float a_X)
{
    return (0xFFFF - (int)(a_X * fp_amount) & 0xFFFF) * fp_amount_inv;
}

提前致谢!


3
fp_amount 应该是 0x10000 而不是 0xFFFF 吗? - Mark Ransom
1
天啊!把它变成一个答案,这样我就可以接受它了!你刚刚解决了我整个准确性问题! - knight666
2
如果注释已经修正了问题,为什么没有人编辑原始代码呢? - RufusVS
10个回答

57
如果我理解您的问题正确,您只是想要小数点后面的部分对吗?您实际上不需要一个分数(整数的分子和分母)吗?
所以我们有一个数字,比如说3.14159,我们想要得到只有0.14159。假设我们的数字存储在float f中,我们可以这样做:
f = f-(long)f;

如果我们插入我们的数字,它的工作方式如下:

0.14159 = 3.14159 - 3;
这段代码的作用是去除浮点数的整数位,只留下小数部分。将浮点数转换为长整型时会丢弃小数部分。然后从原始浮点数中减去长整型值,就得到了仅有的小数部分。由于float类型在大多数系统上占用8个字节,因此需要使用长整型。整型(在许多系统上只有4个字节)可能不足以覆盖与float相同范围的数字,但是long应该可以。

5
这个数学函数中有一个if...then...else...语句?我的缓存,它哭了! - knight666
10
f 是负数时这是错误的(你正在相加两个负数)。根本不需要使用 if 语句:f = f - (int) f。如果 f 是负数,它将减去一个朝零舍入的负整数。 - jamesdlin
9
你假设浮点数的整数部分适合于int类型,这是不确定的。 - jamesdlin
1
哦,好吧。这个答案既最容易实现又最快(比我的方法大约快20%)。接受! - knight666
1
f远超出long范围时,(long)f;会失败。(long long)f;可以扩展范围,但仍存在同样的问题。 - chux - Reinstate Monica
显示剩余3条评论

9

正如我所怀疑的那样,modf 并没有使用任何算术运算本身 -- 它全部都是移位和掩码操作,可以看看这里。您能否在您的平台上使用相同的思路呢?


5
我建议你查看一下今天所使用的系统中modf的实现方式。可以查看uClibc的版本。

http://git.uclibc.org/uClibc/tree/libm/s_modf.c

出于法律原因,它似乎是BSD许可的,但你显然需要仔细检查。

一些宏在这里被定义。


为什么要进行位移操作,真的可以提高速度吗?还是我的整数转换技巧有些问题我不知道呢? - Daniel Bingham
1
@Daniel Bingham:很可能是后者。在您使用的平台上,浮点数可能没有以您想象的方式进行编码,因此您的掩码可能会出错。@sharth:您的链接依赖于一些宏,我很难找到它们的定义。您能否尝试运气并查找EXTRACT_WORDS、INSERT_WORDS和GET_HIGH_WORD的定义? - Randolpho
整数转浮点数很快。浮点数转整数非常慢。 - knight666

5

您的常量中存在一个bug。您基本上是尝试将数字左移16位,然后屏蔽除低位以外的所有内容,接着再向右移动16位。移位操作相当于乘以2的幂次方,但是您使用的不是2的幂次方 - 您使用的是0xFFFF,这与正确值差1。将其替换为0x10000即可使公式按预期工作。


真遗憾原帖的作者没有编辑他的答案,如果他这样做的话,实际上会纠正问题。 - RufusVS

3

我不是完全确定,但我认为你的做法是错误的,因为你只考虑了尾数而完全忽略了指数。

你需要使用指数将尾数中的值移位,以找到实际的整数部分。

关于32位浮点数的存储机制的描述,请看这里


1

看起来你可能需要这个。

float f = something;
float fractionalPart = f - floor(f);

floor 比强制类型转换慢。 - aledalgrande

1

为什么在绘制线条时要使用浮点数?您可以坚持使用固定点版本,并使用基于整数/固定点的线条绘制例程,例如Bresenham算法。虽然这个版本没有抗锯齿效果,但我知道还有其他版本可以实现。

Bresenham's线条绘制


有关抗锯齿线,请参见Wu Lines - Bim

0

你的方法假设小数部分有16位(正如Mark Ransom所指出的,这意味着你应该将其左移16位,即乘以0x1000)。但这可能不正确。指数确定了小数部分有多少位。

为了将其表示为公式,你的方法通过计算(x modf 1.0)得到((x << 16) mod 1<<16) >> 16,而硬编码的16应该取决于指数 - 具体替换取决于你的浮点格式。


0
double frac(double val)
{
    return val - trunc(val);
}

// frac(1.0) = 1.0 - 1.0 = 0.0 correct
// frac(-1.0) = -1.0 - -1.0 = 0.0 correct
// frac(1.4) = 1.4 - 1.0 = 0.4 correct
// frac(-1.4) = -1.4 - -1.0 = -0.4 correct

简单易懂,适用于负数和正数


-1

一个选项是使用fmod(x, 1)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接