Углы в моей программе выражены от 0 до 2pi. Я хочу, чтобы добавить два угла и обернуть вокруг 2pi до 0, если результат выше 2pi. Или, если бы я вычитал угол под углом и был ниже 0, он бы обернулся вокруг 2pi.
Есть ли способ сделать это?
Спасибо.
Углы в моей программе выражены от 0 до 2pi. Я хочу, чтобы добавить два угла и обернуть вокруг 2pi до 0, если результат выше 2pi. Или, если бы я вычитал угол под углом и был ниже 0, он бы обернулся вокруг 2pi.
Есть ли способ сделать это?
Спасибо.
То, что вы ищете, - это модуль. Функция fmod не будет работать, потому что вычисляет остаток, а не арифметический модуль. Что-то вроде этого должно работать:
inline double wrapAngle( double angle )
{
double twoPi = 2.0 * 3.141592865358979;
return angle - twoPi * floor( angle / twoPi );
}
Edit:
Остальная часть обычно определяется как то, что осталось после длительного деления (например, остаток 18/4 равен 2, потому что 18 = 4 * 4 + 2). Это становится волосатым, когда у вас есть отрицательные числа. Общим способом найти остаток от подписанного деления является то, что остаток имеет тот же знак, что и результат (например, остаток от -18/4 равен -2, поскольку -18 = -4 * 4 + - 2).
Определение модуля x y является наименьшим положительным значением m в уравнении x = y * c + m, поскольку c является целым числом. Таким образом, 18 mod 4 будет 2 (где c = 4), однако -18 mod 4 также будет 2 (где c = -5).
Простейшим вычислением x mod y является xy * floor (x/y), где floor - наибольшее целое число, которое меньше или равно вводимому.
angle = fmod(angle, 2.0 * pi);
if (angle < 0.0)
angle += 2.0 * pi;
Edit: перечитав это (и посмотрев на ответ Джонатана Леффлера), я был немного удивлен его заключением, поэтому переписал код на то, что я считал несколько более подходящей формой (например, распечатывая результат из чтобы компилятор не мог просто полностью отказаться от вычислений, потому что он никогда не использовался). Я также изменил его, чтобы использовать счетчик производительности Windows (так как он не включил его класс таймера, а std::chrono::high_resolution_timer
полностью сломан в обоих компиляторах, которые мне сейчас очень удобны).
Я также немного обработал код (это помечен С++, а не 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
выполняется быстрее как при малом угле, так и при больших углах.
Я немного больше тестировал, экспериментировал с увеличением количества повторений и уменьшением размеров шага в тесте больших углов. Хотя я полагаю, что это возможно из-за разницы в платформе или компиляторе, я не смог найти никаких обстоятельств или ситуаций, которые даже приблизились к утверждению результатов или результатов Джонатана.
Нижняя строка: если у вас есть много предварительных знаний о вашем вводе и знаете, она будет почти нормализована, прежде чем вы ее нормализуете, тогда вам удастся избежать вычитания в петле. При любых других обстоятельствах fmod
- это четкий выбор. Кажется, нет никаких обстоятельств, в которых версия floor
имеет смысл вообще.
Oh, for what it worth:
OS: Windows 7 ultimate
Compiler: g++ 4.9.1
Hardware: AMD A6-6400K
Из любопытства я экспериментировал с тремя алгоритмами в других ответах, определяя их.
Когда нормализуемые значения близки к диапазону 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);
}
Тестирование на 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)
). Показан "близкий к нормализованному" тестовый код; "далеко не нормализованные" тестовые данные были созданы путем раскомментации комментария //
в тестовых данных.
Сроки с встроенным 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
Вы можете использовать что-то вроде этого:
while (angle > 2pi)
angle -= 2pi;
while (angle < 0)
angle += 2pi;
В принципе, вам нужно изменить угол на 2pi, пока не убедитесь, что он не превышает 2pi или опускается ниже 0.
Простой трюк: просто добавьте смещение, которое должно быть multiple из 2pi, чтобы довести результат до положительного диапазона до выполнения fmod(). Fmod() вернет его обратно в диапазон [0, 2pi) автоматически. Это работает до тех пор, пока вы знаете априори ряд возможных входов, которые вы можете получить (что вы часто делаете). Чем больше применяется смещение, тем больше бит точности FP вы потеряете, поэтому вы, вероятно, не захотите добавить, скажем, 20000pi, хотя это, безусловно, будет "более безопасным" с точки зрения обработки очень больших внеочередных входов, Предполагая, что никто никогда не пройдет входные углы, которые суммируются вне довольно сумасшедшего диапазона [-8pi, + inf), мы просто добавим 8pi перед fmod() ing.
double add_angles(float a, float b)
{
ASSERT(a + b >= -8.0f*PI);
return fmod(a + b + 8.0f*PI, 2.0f*PI);
}
Аналогично, если вы хотите находиться в диапазоне от -2pi до 2pi, fmod отлично работает
В зависимости от вашего варианта использования есть несколько эффективных особых случаев, если у вас есть возможность выбрать собственное представление угла. (Обратите внимание, что нижний предел равен 0 - это уже особый случай, обеспечивающий эффективность.)
Если вы можете представить углы в виде значений между [0 и 1) вместо [0 и 2π), вам просто нужно взять дробную часть:
float wrap(float angle) {
// wrap between [0 and 2*PI)
return angle - floor(angle);
}
Отрицательные углы просто работают.
Вы также можете нормализовать, обернуть, затем уменьшить до радиан с некоторой потерей точности и эффективности.
Это может быть полезно в коде, который работает аналогично большому количеству шейдерного кода, особенно в среде "все является единичным вектором".
Если вы можете представлять углы в виде значений между [0 и 2 ^ n) вместо [0 и 2π), то вы можете обернуть их с точностью до двух с помощью побитовой операции и операции:
unsigned int wrap(unsigned int angle) {
// wrap between [0 and 65,535)
return angle & 0xffff;
}
Еще лучше, если вы можете выбрать степень двойки, равную размеру целочисленного типа, числа просто переносятся естественным образом. uint16_t
всегда переносится в [0 и 2 ^ 16), а uint32_t
всегда переносится в [0 и 2 ^ 32). Шестьдесят пять тысяч заголовков должно быть достаточно для каждого, не так ли? (-:
Я использовал это в играх и демо-типах в 8-битные дни и даже для наложения текстур перед 3D-видеокартами. Я полагаю, это все еще было бы полезно в коде для эмуляторов и ретро-игр, но, возможно, даже на крошечных микроконтроллерах?