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

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

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

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

Ответ 1

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

two regions

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

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

выполняется для всех non-origin (x, y), где это undefined, и мы говорим о 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).

graph

Ответ 2

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

Далее я бы не стал полагаться на микс, чтобы переключаться между двумя случаями. Я не уверен, как это реализовано/скомпилировано, но правила float IEEE для x * NaN и x + NaN снова приводятся в NaN. Поэтому, если ваш компилятор действительно использовал микс/интерполяцию, результат должен быть 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 в этом случае, это нормально.

Ответ 3

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

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

Ответчик/ответчик правилен в том, что некоторые реализации делают быстрые клавиши. Однако принятое решение делает предположение, что плохая реализация всегда будет неустойчивой, когда x близок к нулю: на каком-то оборудовании (например, у моей Galaxy S4) значение стабильно, когда x близок к нулю, но неустойчиво, когда y равно около нуля.

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

GLSL atan(y,x) test pattern

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

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

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

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

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

Ответ 4

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

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

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

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

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

Ответ 5

Формула, которая дает угол в четырех квадрантах для любого значения

координат х и у. Для x = y = 0 результат не определен.

f (x, y) = pi() -pi()/2 * (1 + знак (x)) * (1-знак (y ^ 2)) -pi()/4 * (2 + знак ( х)) * знак (у)

   -sign(x*y)*atan((abs(x)-abs(y))/(abs(x)+abs(y)))