29

我的程序中的角度以 0 到 2pi 表示。如果结果高于 2pi,我想要一种方法来添加两个角度并让它围绕 2pi 到 0。或者,如果我从一个角度减去一个角度并且它低于 0,它将环绕 2pi。

有没有办法做到这一点?

谢谢。

4

8 回答 8

55

您正在寻找的是模数。fmod 函数将不起作用,因为它计算的是余数而不是算术模数。像这样的东西应该工作:

inline double wrapAngle( double angle )
{
    double twoPi = 2.0 * 3.141592865358979;
    return angle - twoPi * floor( angle / twoPi );
}

编辑:

余数通常定义为长除法后的余数(例如,18/4 的余数是 2,因为18 = 4 * 4 + 2)。当您有负数时,这会变得很棘手。查找带符号除法余数的常用方法是使余数与结果具有相同的符号(例如,-18/4 的余数是 -2,因为-18 = -4 * 4 + -2)。

x 模数 y 的定义是方程 x=y*c+m 中 m 的最小正值,给定 c 是整数。所以18 mod 4将是 2(其中 c=4),但是-18 mod 4也将是 2(其中 c=-5)。

x mod y的最简单计算是xy*floor(x/y),其中 floor 是小于或等于输入的最大整数。

于 2012-08-16T03:33:09.280 回答
20
angle = fmod(angle, 2.0 * pi);
if (angle < 0.0)
   angle += 2.0 * pi;

编辑:在重新阅读后(并查看 Jonathan Leffler 的回答)我对他的结论有点惊讶,所以我将代码重写为我认为更合适的形式(例如,打印出计算结果以确保编译器不能完全丢弃计算,因为它从未被使用过)。我还将它更改为使用 Windows 性能计数器(因为他没有包含他的计时器类,并且在std::chrono::high_resolution_timer我现在方便的两个编译器中都完全损坏了)。

我还做了一些一般性的代码清理(这是标记为 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";

}

我得到的结果如下:

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

至少对我来说,结果似乎支持了与乔纳森得出的完全不同的结论。查看在循环中进行减法的版本,我们看到两点:对于大角度测试,它产生的和与其他两个不同(即,它不准确),第二,它非常慢。除非你确定你的输入总是开始接近标准化,否则这基本上是不可用的。

fmod版本和版本之间floor似乎没有争论的余地——它们都产生了准确的结果,但fmod版本在小角度和大角度测试中都更快。

我做了更多的测试,尝试增加重复次数并减少大角度测试中的步长。虽然我认为这可能只是由于平台或编译器的差异,但我无法找到任何甚至接近于支持 Jonathan 的结果或结论的情况或情况。

底线:如果您对输入有很多先验知识,并且知道在标准化之前它总是几乎标准化,那么您可能能够在循环中进行减法。在任何其他情况下,fmod都是明确的选择。似乎没有任何情况下该floor版本有任何意义。

Oh, for what it's worth:
OS: Windows 7 ultimate
Compiler: g++ 4.9.1
Hardware: AMD A6-6400K
于 2012-08-16T03:26:53.413 回答
8

出于好奇,我在其他答案中尝试了三种算法,并对它们进行计时。

当要归一化的值接近 0..2π 范围时,while算法最快;使用的算法fmod()最慢,使用的算法floor()介于两者之间。

当要归一化的值不接近范围 0..2π 时,while算法最慢,使用的算法floor()最快,使用的算法fmod()介于两者之间。

所以,我得出结论:

  • 如果角度(通常)接近归一化,则该while算法就是要使用的算法。
  • 如果角度不接近归一化,那么该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

测试代码:

测试代码使用了显示的值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);
}

/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))在 Mac OS X 10.7.4 上进行测试。显示了“接近标准化”的测试代码;“远未标准化”测试数据是通过取消注释测试数据中的注释创建的//

使用自制 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
于 2012-08-16T04:30:00.893 回答
4

你可以使用这样的东西:

while (angle > 2pi)
    angle -= 2pi;

while (angle < 0)
    angle += 2pi;

基本上,您必须将角度更改 2pi,直到您确信它不超过 2pi 或低于 0。

于 2012-08-16T03:34:43.483 回答
1

简单的技巧:只需添加一个偏移量,它必须是 2pi 的倍数,以便在执行 fmod() 之前将结果带入正范围。fmod() 会自动将其带回到 [0, 2pi) 范围内。只要您先验地知道您可能获得的可能输入的范围(您经常这样做),这就会起作用。您应用的偏移量越大,您丢失的 FP 精度就越多,因此您可能不想添加 20000pi,尽管在处理非常大的越界输入方面肯定会“更安全” . 假设没有人会传递总和超出相当疯狂的范围 [-8pi, +inf) 的输入角度,我们将在 fmod()ing 之前添加 8pi。

double add_angles(float a, float b)
{
    ASSERT(a + b >= -8.0f*PI);
    return fmod(a + b + 8.0f*PI, 2.0f*PI);
}
于 2017-12-05T19:43:16.403 回答
1

如果您可以选择自己的角度表示,则根据您的用例,有一些有效的特殊情况。(请注意,将 0 作为下限已经是一种特殊情况,可以提高效率。)

将角度表示为单位向量

如果您能够将角度表示为 [0 和 1) 而不是 [0 和 2π) 之间的值,那么您只需要取小数部分:

float wrap(float angle) {
    // wrap between [0 and 2*PI)
    return angle - floor(angle);
}

负角只是工作。

您还可以标准化、换行,然后缩小到弧度,但会损失一些精度和效率。

这在与许多着色器代码类似的代码中很有用,尤其是在“一切都是单位向量”环境中。

将角度表示为无符号整数,限制为 2 的幂

如果您能够将角度表示为 [0 和 2^n) 之间的值,而不是 [0 和 2π) 之间的值,那么您可以使用按位与运算将它们包装到 2 的幂:

unsigned int wrap(unsigned int angle) {
    // wrap between [0 and 65,535)
    return angle & 0xffff;
}

更好的是,如果您可以选择等于整数类型大小的 2 的幂,则数字自然会自动换行。Auint16_t总是包裹在 [0 和 2^16) 内,而 auint32_t总是包裹在 [0 和 2^32) 内。六万五千个标题应该对任何人都足够了吧?(-:

我在 8 位时代的游戏和演示类型中使用了它,甚至在 3D 显卡之前用于纹理映射。我想它在模拟器和逆向游戏的代码中仍然有用,但甚至可能在微型微控制器上有用?

于 2019-10-07T01:15:16.033 回答
0

同样,如果你想在 -2pi 到 2pi 的范围内,那么 fmod 效果很好

于 2017-09-19T01:12:17.213 回答
0

这工作得很好而且足够快:

#ifndef M_PI
#define M_PI 3.14159265358979323846    
#endif // !M_PI
#ifndef M_2PI
#define M_2PI (2.0 * M_PI)
#endif // !M_2PI
#define to0_2pi(x) ((x) - ((int)((x) / (M_2PI)) - signbit(x)) * (M_2PI))
于 2021-07-08T20:34:27.750 回答