Надежный atan (y, x) в GLSL для преобразования координаты XY в угол

В GLSL (конкретно 3.00, который я использую), есть две версии
atan(): atan(y_over_x) может возвращать только углы между -PI / 2, PI / 2, в то время как atan(y/x) может принимать во внимание все 4 квадранта, поэтому диапазон углов охватывает все от -PI, PI, как atan2() в C ++.

Я хотел бы использовать второй atan преобразовать координаты XY в угол.
Тем не мение, atan() в GLSL, кроме того, не в состоянии справиться, когда x = 0не очень стабильный. Особенно где x Если значение близко к нулю, деление может переполниться, что приведет к противоположному результирующему углу (вы получите что-то близкое к -PI / 2, где вы предполагаете получить приблизительно PI / 2).

Какая хорошая, простая реализация, которую мы можем построить поверх GLSL atan(y,x) сделать его более надежным?

11

Решение

Я собираюсь ответить на свой вопрос, чтобы поделиться своими знаниями. Сначала мы замечаем, что нестабильность возникает, когда x близок к нулю. Тем не менее, мы также можем перевести это как abs(x) << abs(y), Итак, сначала мы делим плоскость (при условии, что мы находимся на единичной окружности) на две области: одна, где |x| <= |y| а другой где |x| > |y|, как показано ниже:

два региона

Мы знаем это atan(x,y) гораздо более устойчив в зеленой области — когда x близок к нулю, мы просто имеем что-то близкое к atan (0.0), которое очень стабильно численно, в то время как обычный atan(y,x) является более стабильным в оранжевой области. Вы также можете убедиться, что эти отношения:

atan(x,y) = PI/2 - atan(y,x)

имеет место для всех не-происхождения (x, y), где оно не определено, и мы говорим о atan(y,x) который может возвращать значение угла во всем диапазоне -PI, PI, а не atan(y_over_x) который возвращает угол только между -PI / 2, PI / 2. Поэтому наш надежный atan2() рутина для GLSL довольно проста:

float atan2(in float y, in float x)
{
bool s = (abs(x) > abs(y));
return mix(PI/2.0 - atan(x,y), atan(y,x), s);
}

Как примечание, тождество для математической функции atan(x) на самом деле:

atan(x) + atan(1/x) = sgn(x) * PI/2

что верно, потому что его диапазон (-PI / 2, PI / 2).

график

8

Другие решения

Ваше предлагаемое решение все еще терпит неудачу в случае x=y=0, Здесь оба atan() функции возвращают NaN.

Кроме того, я бы не стал полагаться на микс для переключения между двумя случаями. Я не уверен, как это реализовано / скомпилировано, но правила IEEE с плавающей запятой для x * NaN и x + NaN снова приводят к NaN. Так что, если ваш компилятор действительно использовал mix / interpolation, результат должен быть NaN для x=0 или же y=0,

Вот еще одно исправление, которое решило проблему для меня:

float atan2(in float y, in float x)
{
return x == 0.0 ? sign(y)*PI/2 : atan(y, x);
}

когда x=0 угол может быть ± π / 2. Какой из двух зависит от y только. Если y=0 Кроме того, угол может быть произвольным (вектор имеет длину 0). sign(y) возвращается 0 в этом случае, который просто в порядке.

6

В зависимости от вашей целевой платформы, это может быть решенной проблемой. Спецификация OpenGL для атан (у, х) указывает, что он должен работать во всех квадрантах, оставляя поведение неопределенным, только если x и y равны 0.

Так что один ожидать любая достойная реализация должна быть устойчивой почти по всем осям, так как в этом и заключается весь смысл двух аргументов загар (или же atan2).

Спрашивающий / ответчик прав в том, что некоторые реализации используют ярлыки. Тем не менее, принятое решение предполагает, что плохая реализация всегда будет нестабильной, когда x близок к нулю: на некотором оборудовании (например, в моей Galaxy S4) это значение стабильный, когда Икс близок к нулю, но нестабилен, когда Y близок к нулю.

Чтобы проверить реализацию вашего рендерера GLSL atan(y,x)Вот тестовый шаблон WebGL. Перейдите по ссылке ниже, и пока ваша реализация OpenGL приличная, вы должны увидеть что-то вроде этого:

GLSL atan (y, x) тестовая таблица

Тестовый шаблон с использованием нативного atan(y,x): http://glslsandbox.com/e#26563.2

Если все хорошо, вы должны увидеть 8 разных цветов (игнорируя центр).

Связанные демонстрационные образцы atan(y,x) для нескольких значений x и y, включая 0, очень большие и очень маленькие значения. Центральный ящик atan(0.,0.)— не определено математически, и реализации могут отличаться. Я видел 0 (красный), PI / 2 (зеленый) и NaN (черный) на тестируемом оборудовании.

Вот тестовая страница для принятого решения. Замечания: не хватает версии WebGL хоста mix(float,float,bool)поэтому я добавил реализацию, соответствующую спецификации.

Тестовый образец с использованием atan2(y,x) из принятого ответа: http://glslsandbox.com/e#26666.0

6

Иногда лучший способ улучшить производительность фрагмента кода — это вообще не вызывать его. Например, одна из причин, по которой вам может потребоваться определить угол вектора, заключается в том, что вы можете использовать этот угол для построения матрицы вращения, используя комбинации синуса и косинуса угла. Тем не менее, синус и косинус вектора (относительно начала координат) уже скрыты на виду внутри самого вектора. Все, что вам нужно сделать, это создать нормализованную версию вектора путем деления каждой векторной координаты на общую длину вектора. Вот двумерный пример для вычисления синуса и косинуса угла вектора [x y]:

double length = sqrt(x*x + y*y);
double cos = x / length;
double sin = y / length;

Если у вас есть значения синуса и косинуса, теперь вы можете напрямую заполнить матрицу вращения этими значениями, чтобы выполнить вращение произвольных векторов по часовой стрелке или против часовой стрелки на тот же угол, или вы можете объединить вторую матрицу вращения, чтобы повернуть на угол, отличный от нуль. В этом случае вы можете думать о матрице вращения как о «нормализации» угла к нулю для произвольного вектора. Этот подход также расширяем для трехмерного (или N-мерного) случая, хотя, например, у вас будет три угла и шесть пар sin / cos для расчета (один угол на плоскость) для трехмерного вращения.

В ситуациях, когда вы можете использовать этот подход, вы получите большой выигрыш, полностью обойдя вычисление atan, что возможно, поскольку единственной причиной, по которой вы хотели определить угол, было вычисление значений синуса и косинуса. Пропустив преобразование в угловое пространство и обратно, вы не только не будете беспокоиться о делении на ноль, но также улучшите точность для углов, которые находятся около полюсов и в противном случае пострадают от умножения / деления на большие числа. Я успешно использовал этот подход в программе GLSL, которая поворачивает сцену на ноль градусов, чтобы упростить вычисления.

Может быть легко оказаться настолько вовлеченным в непосредственную проблему, что вы можете упустить из виду, зачем вам эта информация вообще нужна. Не то чтобы это работало в каждом случае, но иногда это помогает мыслить нестандартно …

2