C++ 如何环绕一个范围
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/11980292/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to wrap around a range
提问by Jigglypuff
Angles in my program are expressed in 0 to 2pi. I want a way to add two angles and have it wrap around the 2pi to 0 if the result is higher than 2pi. Or if I subtracted an angle from an angle and it was below 0 it would wrap around 2pi.
我的程序中的角度以 0 到 2pi 表示。如果结果高于 2pi,我想要一种方法来添加两个角度并让它环绕 2pi 到 0。或者,如果我从一个角度减去一个角度并且它低于 0,它将环绕 2pi。
Is there a way to do this?
有没有办法做到这一点?
Thanks.
谢谢。
回答by Tom Holmes
What you are looking for is the modulus. The fmod function will not work because it calculates the remainder and not the arithmetic modulus. Something like this should work:
您正在寻找的是模数。fmod 函数将不起作用,因为它计算余数而不是算术模数。这样的事情应该工作:
inline double wrapAngle( double angle )
{
double twoPi = 2.0 * 3.141592865358979;
return angle - twoPi * floor( angle / twoPi );
}
Edit:
编辑:
The remainder is commonly defined as what is left over after long division (eg. the remainder of 18/4 is 2, because 18 = 4 * 4 + 2). This gets hairy when you have negative numbers. The common way to find the remainder of a signed division is for the remainder to have the same sign as the result (eg. the remainder of -18/4 is -2, because -18 = -4 * 4 + -2).
余数通常定义为长除法后剩下的部分(例如,18/4的余数是 2,因为18 = 4 * 4 + 2)。当你有负数时,这会变得毛茸茸的。找到有符号除法余数的常用方法是使余数与结果具有相同的符号(例如,-18/4的余数是 -2,因为-18 = -4 * 4 + -2)。
The definition of x modulus y is the smallest positive value of m in the equation x=y*c+m, given c is an integer. So 18 mod 4would be 2 (where c=4), however -18 mod 4would also be 2 (where c=-5).
x 模数 y 的定义是方程 x=y*c+m 中 m 的最小正值,给定 c 是一个整数。因此,18 mod 4将是 2(其中 c=4),但是-18 mod 4也将是 2(其中 c=-5)。
The simplest calculation of x mod yis x-y*floor(x/y), where floor is the largest integer that is less than or equal to the input.
x mod y的最简单计算是xy*floor(x/y),其中 floor 是小于或等于输入的最大整数。
回答by Jerry Coffin
angle = fmod(angle, 2.0 * pi);
if (angle < 0.0)
angle += 2.0 * pi;
Edit: After re-reading this (and looking at Jonathan Leffler's answer) I was a bit surprised by his conclusion, so I rewrote the code to what I considered a somewhat more suitable form (e.g., printing out a result from the computation to ensure the compiler couldn't just discard the computation completely because it was never used). I also changed it to use the Windows performance counter (since he didn't include his timer class, and the std::chrono::high_resolution_timer
is completely broken in both the compilers I have handy right now).
编辑:在重新阅读这篇文章(并查看 Jonathan Leffler 的回答)后,我对他的结论感到有些惊讶,因此我将代码重写为我认为更合适的形式(例如,打印出计算结果以确保编译器不能完全丢弃计算,因为它从未被使用过)。我还将它更改为使用 Windows 性能计数器(因为他没有包含他的计时器类,而且std::chrono::high_resolution_timer
我现在手边的两个编译器都完全损坏了它)。
I also did a bit of general code cleanup (this is tagged C++, not C), to get this:
我还做了一些通用代码清理(这是标记为 C++,而不是 C),以获得这个:
#include <math.h>
#include <iostream>
#include <vector>
#include <chrono>
#include <windows.h>
static const double PI = 3.14159265358979323844;
static double r1(double angle)
{
while (angle > 2.0 * PI)
angle -= 2.0 * PI;
while (angle < 0)
angle += 2.0 * PI;
return angle;
}
static double r2(double angle)
{
angle = fmod(angle, 2.0 * PI);
if (angle < 0.0)
angle += 2.0 * PI;
return angle;
}
static double r3(double angle)
{
double twoPi = 2.0 * PI;
return angle - twoPi * floor(angle / twoPi);
}
struct result {
double sum;
long long clocks;
result(double d, long long c) : sum(d), clocks(c) {}
friend std::ostream &operator<<(std::ostream &os, result const &r) {
return os << "sum: " << r.sum << "\tticks: " << r.clocks;
}
};
result operator+(result const &a, result const &b) {
return result(a.sum + b.sum, a.clocks + b.clocks);
}
struct TestSet { double start, end, increment; };
template <class F>
result tester(F f, TestSet const &test, int count = 5)
{
LARGE_INTEGER start, stop;
double sum = 0.0;
QueryPerformanceCounter(&start);
for (int i = 0; i < count; i++) {
for (double angle = test.start; angle < test.end; angle += test.increment)
sum += f(angle);
}
QueryPerformanceCounter(&stop);
return result(sum, stop.QuadPart - start.QuadPart);
}
int main() {
std::vector<TestSet> tests {
{ -6.0 * PI, +6.0 * PI, 0.01 },
{ -600.0 * PI, +600.0 * PI, 3.00 }
};
std::cout << "Small angles:\n";
std::cout << "loop subtraction: " << tester(r1, tests[0]) << "\n";
std::cout << " fmod: " << tester(r2, tests[0]) << "\n";
std::cout << " floor: " << tester(r3, tests[0]) << "\n";
std::cout << "\nLarge angles:\n";
std::cout << "loop subtraction: " << tester(r1, tests[1]) << "\n";
std::cout << " fmod: " << tester(r2, tests[1]) << "\n";
std::cout << " floor: " << tester(r3, tests[1]) << "\n";
}
The results I got were as follows:
我得到的结果如下:
Small angles:
loop subtraction: sum: 59196 ticks: 684
fmod: sum: 59196 ticks: 1409
floor: sum: 59196 ticks: 1885
Large angles:
loop subtraction: sum: 19786.6 ticks: 12516
fmod: sum: 19755.2 ticks: 464
floor: sum: 19755.2 ticks: 649
At least to me, the results seem to support a rather different conclusion than Jonathon reached. Looking at the version that does subtraction in a loop, we see two points: for the large angles test it produces a sum that's different from the other two (i.e., it's inaccurate) and second, it's horriblyslow. Unless you know for certainthat your inputs always start out nearly normalized, this is basically just unusable.
至少对我来说,结果似乎支持了一个与乔纳森得出的结论截然不同的结论。查看在循环中进行减法的版本,我们看到两点:对于大角度测试,它产生的和与其他两个不同(即,它不准确),其次,它非常慢。除非您确定您的输入始终几乎是标准化的,否则这基本上是无法使用的。
Between the fmod
version and the floor
version there seems to be no room for argument--they both produce accurate results, but the fmod
version is faster in both the small angle and large angle tests.
之间fmod
的版本与floor
版本似乎是没有空间的说法-他们都产生准确的结果,但fmod
版本是在小角度和大角度的测试速度更快。
I did a bit more testing, experimenting with increasing the number of repetitions and decreasing the step sizes in the large angles test. Although I suppose it's possibleit's simply due to a difference in platform or compiler, I was unable to find anycircumstance or situation that even came close to upholding Jonathan's results or conclusion.
我做了更多的测试,尝试在大角度测试中增加重复次数和减少步长。虽然我认为这可能仅仅是由于平台或编译器的不同,但我无法找到任何情况或情况甚至接近支持 Jonathan 的结果或结论。
Bottom line: if you have a lot of prior knowledge about your input, and knowit'll always be nearly normalized beforeyou normalize it, then you mightbe able to get away with doing subtraction in a loop. Under any other circumstance, fmod
is the clear choice. There seems to be nocircumstance in which the floor
version makes any sense at all.
底线:如果你对你的输入有很多先验知识,并且知道它在归一化之前总是几乎归一化,那么你可能能够在循环中进行减法。在任何其他情况下,fmod
都是明确的选择。似乎在任何情况下该floor
版本都没有任何意义。
Oh, for what it's worth:
OS: Windows 7 ultimate
Compiler: g++ 4.9.1
Hardware: AMD A6-6400K
回答by Jonathan Leffler
Out of curiosity, I experimented with three algorithms in other answers, timing them.
出于好奇,我在其他答案中尝试了三种算法,并对它们进行计时。
When the values to be normalized are close to the range 0..2π, then the while
algorithm is quickest; the algorithm using fmod()
is slowest, and the algorithm using floor()
is in between.
当要归一化的值接近范围 0..2π 时,while
算法最快;使用的算法fmod()
最慢,使用的算法floor()
介于两者之间。
When the values to be normalized are not close to the range 0..2π, then the while
algorithm is slowest, the algorithm using floor()
is quickest, and the algorithm using fmod()
is in between.
当要归一化的值不接近 0..2π 范围时,则while
算法最慢,使用的算法floor()
最快,使用的算法fmod()
介于两者之间。
So, I conclude that:
所以,我的结论是:
- If the angles are (generally) close to normalized, the
while
algorithm is the one to use. - If the angles are not close to normalized, then the
floor()
algorithm is the one to use.
- 如果角度(通常)接近归一化,则
while
使用该算法。 - 如果角度不接近归一化,则使用该
floor()
算法。
Test results:
检测结果:
r1 = while
, r2 = fmod()
, r3 = floor()
r1 = while
, r2 = fmod()
, r3 =floor()
Near Normal Far From Normal
r1 0.000020 r1 0.000456
r2 0.000078 r2 0.000085
r3 0.000058 r3 0.000065
r1 0.000032 r1 0.000406
r2 0.000085 r2 0.000083
r3 0.000057 r3 0.000063
r1 0.000033 r1 0.000406
r2 0.000085 r2 0.000085
r3 0.000058 r3 0.000065
r1 0.000033 r1 0.000407
r2 0.000086 r2 0.000083
r3 0.000058 r3 0.000063
Test code:
测试代码:
The test code used the value shown for PI
. The C standard does not define a value for π, but POSIXdoes define M_PI
and a number of related constants, so I could have written my code using M_PI
instead of PI
.
测试代码使用了显示的值PI
。C 标准没有定义 π 的值,但POSIX确实定义M_PI
了一些相关的常量,所以我可以使用M_PI
代替 来编写我的代码PI
。
#include <math.h>
#include <stdio.h>
#include "timer.h"
static const double PI = 3.14159265358979323844;
static double r1(double angle)
{
while (angle > 2.0 * PI)
angle -= 2.0 * PI;
while (angle < 0)
angle += 2.0 * PI;
return angle;
}
static double r2(double angle)
{
angle = fmod(angle, 2.0 * PI);
if (angle < 0.0)
angle += 2.0 * PI;
return angle;
}
static double r3(double angle)
{
double twoPi = 2.0 * PI;
return angle - twoPi * floor( angle / twoPi );
}
static void tester(const char * tag, double (*test)(double), int noisy)
{
typedef struct TestSet { double start, end, increment; } TestSet;
static const TestSet tests[] =
{
{ -6.0 * PI, +6.0 * PI, 0.01 },
// { -600.0 * PI, +600.0 * PI, 3.00 },
};
enum { NUM_TESTS = sizeof(tests) / sizeof(tests[0]) };
Clock clk;
clk_init(&clk);
clk_start(&clk);
for (int i = 0; i < NUM_TESTS; i++)
{
for (double angle = tests[i].start; angle < tests[i].end; angle += tests[i].increment)
{
double result = (*test)(angle);
if (noisy)
printf("%12.8f : %12.8f\n", angle, result);
}
}
clk_stop(&clk);
char buffer[32];
printf("%s %s\n", tag, clk_elapsed_us(&clk, buffer, sizeof(buffer)));
}
int main(void)
{
tester("r1", r1, 0);
tester("r2", r2, 0);
tester("r3", r3, 0);
tester("r1", r1, 0);
tester("r2", r2, 0);
tester("r3", r3, 0);
tester("r1", r1, 0);
tester("r2", r2, 0);
tester("r3", r3, 0);
tester("r1", r1, 0);
tester("r2", r2, 0);
tester("r3", r3, 0);
return(0);
}
Testing on Mac OS X 10.7.4 with the standard /usr/bin/gcc
(i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.9.00)
). The 'close to normalized' test code is shown; the 'far from normalized' test data was created by uncommenting the //
comment in the test data.
在 Mac OS X 10.7.4 上使用标准/usr/bin/gcc
( i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.9.00)
) 进行测试。显示了“接近标准化”的测试代码;“远非标准化”测试数据是通过取消注释测试数据中的注释创建的//
。
Timing with a home-built GCC 4.7.1 is similar (the same conclusions would be drawn):
使用自制 GCC 4.7.1 的时间是相似的(会得出相同的结论):
Near Normal Far From Normal
r1 0.000029 r1 0.000321
r2 0.000075 r2 0.000094
r3 0.000054 r3 0.000065
r1 0.000028 r1 0.000327
r2 0.000075 r2 0.000096
r3 0.000053 r3 0.000068
r1 0.000025 r1 0.000327
r2 0.000075 r2 0.000101
r3 0.000053 r3 0.000070
r1 0.000028 r1 0.000332
r2 0.000076 r2 0.000099
r3 0.000050 r3 0.000065
回答by MMS
You can use something like this:
你可以使用这样的东西:
while (angle > 2pi)
angle -= 2pi;
while (angle < 0)
angle += 2pi;
Basically, you have to change the angle by 2pi until you are assured that it doesn't exceed 2pi or fall below 0.
基本上,您必须将角度更改 2pi,直到您确定它不超过 2pi 或低于 0。
回答by Jason Q Gregory
Simple trick: Just add an offset, which must be a multipleof 2pi, to bring the result into a positive range prior to doing the fmod(). The fmod() will bring it back into the range [0, 2pi) automagically. This works as long as you know a priori the range of possible inputs you might get (which you often do). The larger the offset you apply, the more bits of FP precision you loose, so you probably don't want to add, say, 20000pi, although that would certainly be "safer" in terms of handling very large out-of-bounds inputs. Presuming no one would ever pass input angles that sum outside the rather crazy range [-8pi, +inf), we'll just add 8pi before fmod()ing.
简单的技巧:只需添加一个偏移量,它必须是2pi的倍数,以便在执行 fmod() 之前将结果带入正范围内。fmod() 会自动将其带回到 [0, 2pi) 范围内。只要您先验地知道您可能获得的可能输入范围(您经常这样做),这就会起作用。你应用的偏移量越大,你失去的 FP 精度就越多,所以你可能不想添加,比如 20000pi,尽管在处理非常大的越界输入方面这肯定会“更安全” . 假设没有人会通过总和超出相当疯狂的范围 [-8pi, +inf) 的输入角度,我们将在 fmod() 之前添加 8pi。
double add_angles(float a, float b)
{
ASSERT(a + b >= -8.0f*PI);
return fmod(a + b + 8.0f*PI, 2.0f*PI);
}
回答by hippietrail
Depending on your use case there are some efficient special cases if you have the luxury of choosing your own representation of an angle. (Note that having 0 as the lower bound is already a special case allowing for efficiency.)
如果您可以选择自己的角度表示,那么根据您的用例,有一些有效的特殊情况。(请注意,将 0 作为下限已经是考虑到效率的特殊情况。)
Expressing angles as unit vectors
将角度表示为单位向量
If you are able to represent angles as values between [0 and 1) instead of [0 and 2π) then you merely need to take the fractional part:
如果您能够将角度表示为 [0 和 1) 之间的值而不是 [0 和 2π),那么您只需要取小数部分:
float wrap(float angle) {
// wrap between [0 and 2*PI)
return angle - floor(angle);
}
Negative angles just work.
负角才有效。
You can also normalize, wrap, then scale back to radians with some loss of precision and efficiency.
您还可以标准化、包裹,然后缩小到弧度,但精度和效率会有所损失。
This can be useful in code that works similar to a lot of shader code, and especially in an "everything is a unit vector" environment.
这在与许多着色器代码类似的代码中很有用,尤其是在“一切都是单位向量”的环境中。
Expressing angles as unsigned integers limited at a power of two
将角度表示为以 2 的幂为限的无符号整数
If you are able to represent angles as values between [0 and 2^n) instead of [0 and 2π) then you can wrap them to within a power of two with a bitwise and operation:
如果您能够将角度表示为 [0 和 2^n) 而不是 [0 和 2π) 之间的值,那么您可以使用按位与操作将它们包裹在 2 的幂内:
unsigned int wrap(unsigned int angle) {
// wrap between [0 and 65,535)
return angle & 0xffff;
}
Even better, if you can choose a power of two that's equal to the size of an integer type, the numbers just wrap naturally. A uint16_t
always wraps to within [0 and 2^16) and a uint32_t
always wraps to within [0 and 2^32). Sixty five thousand headings should be enough for anyone right? (-:
更好的是,如果您可以选择等于整数类型大小的 2 的幂,则数字会自然换行。Auint16_t
始终环绕在 [0 和 2^16) 之内,而 auint32_t
始终环绕在 [0 和 2^32) 之内。六万五千个标题对任何人来说都足够了吧?(-:
I used this in game and demo type things in the 8-bit days and even for texture mapping before 3D graphics cards. I suppose it would still be useful in code for emulators and retrogaming but perhaps even on tiny microcontrollers?
我在 8 位时代的游戏和演示类型的东西中使用了它,甚至在 3D 显卡之前用于纹理映射。我想它在模拟器和逆向游戏的代码中仍然有用,但甚至可能在微型微控制器上使用?
回答by singingsingh
Likewise if you want to be in the range -2pi to 2pi then fmod works great
同样,如果你想在 -2pi 到 2pi 的范围内,那么 fmod 效果很好