Есть ли точное приближение функции acos()?

Мне нужна функция acos() с двойной точностью в вычислительном шейдере. Поскольку в GLSL нет встроенной функции acos() с двойной точностью, я попытался реализовать свою.

Сначала я реализовал ряд Тейлора, подобный уравнению из Вики — ряд Тейлора с предварительно рассчитанными значениями преподавателей. . Но это кажется неточным около 1. Максимальная ошибка была около 0,08 с 40 итерациями.

Я также реализовал этот метод, который очень хорошо работает на ЦП с максимальной ошибкой -2.22045e-16, но у меня есть некоторые проблемы с реализацией этого в шейдере.

В настоящее время я использую функцию приближения acos() из здесь, где кто-то разместил свои функции приближения на этого сайта. Я использую самую точную функцию этого сайта, и теперь я получаю максимальную ошибку -7.60454e-08, но эта ошибка слишком высока.

Мой код этой функции:

double myACOS(double x)
{
    double part[4];
    part[0] = 32768.0/2835.0*sqrt(2.0-sqrt(2.0+sqrt(2.0+sqrt(2.0+2.0*x))));
    part[1] = 256.0/135.0*sqrt(2.0-sqrt(2.0+sqrt(2.0+2.0*x)));
    part[2] = 8.0/135.0*sqrt(2.0-sqrt(2.0+2.0*x));
    part[3] = 1.0/2835.0*sqrt(2.0-2.0*x);
    return (part[0]-part[1]+part[2]-part[3]);
}

Кто-нибудь знает другой метод реализации acos(), который очень точен и, если возможно, легко реализуем в шейдере?

Немного системной информации:

  • Нвидиа ГТ 555М
  • запуск OpenGL 4.3 с optirun

person DanceIgel    schedule 10.03.2015    source источник
comment
зачем тебе акос? если это для slerp, вы можете разделять и властвовать с повторными lerps   -  person ratchet freak    schedule 10.03.2015
comment
есть стандартный acos в <cmath>   -  person NathanOliver    schedule 10.03.2015
comment
Святое дерьмо, используйте таблицу поиска, если вам нужно столько sqrt.   -  person Andon M. Coleman    schedule 10.03.2015
comment
@NathanOliver cmath недоступен в шейдере glsl   -  person ratchet freak    schedule 10.03.2015
comment
@ AndonM.Coleman Я бы использовал один, но это было только для проверки концепции. И, как я уже сказал, точность важнее, чем производительность.   -  person DanceIgel    schedule 10.03.2015
comment
Мне интересно, действительно ли эти приближения быстрее ... Вы проводили какие-либо тесты?   -  person Willem Van Onsem    schedule 10.03.2015
comment
почему это помечено как C++, если язык GLSL?   -  person Alnitak    schedule 10.03.2015
comment
@CommuSoft Меня не интересует производительность, поэтому я и не интересовался. Я только что проверил точность процессора в сравнении с реализацией acos() из math.h.   -  person DanceIgel    schedule 10.03.2015
comment
Что вы используете возвращаемый угол для 9 из 10 вы можете получить желаемый результат без акос   -  person ratchet freak    schedule 10.03.2015
comment
Какой диапазон аргументов вам нужно поддержать? [-1.0, 1.0]?   -  person Reto Koradi    schedule 10.03.2015
comment
@RetoKoradi Да. От -1,0 до 1,0.   -  person DanceIgel    schedule 10.03.2015
comment
@ratchetfreak Чтобы решить сложное уравнение в своего рода алгоритме MLS.   -  person DanceIgel    schedule 10.03.2015
comment
Какое ваше целевое оборудование тогда? GPU с Compute 1.3 или выше — карты NVIDIA GT200 и выше поддерживают арифметику с двойной точностью. Я предполагаю, что эти функции также имеют двойную точность.   -  person Robinson    schedule 10.03.2015
comment
@Robinson Я добавил информацию к вопросу. В OpenCL и CUDA есть функции двойной точности для 'acos()', но не внутри шейдера. К сожалению, я немного ограничен в вычислительных шейдерах.   -  person DanceIgel    schedule 10.03.2015
comment
У меня есть некоторые проблемы с реализацией этого в шейдере... например?   -  person genpfault    schedule 10.03.2015
comment
Возможно, (1) asin(x) = x + 1/2 (x^3/3) + (1/2)(3/4)(x^5/5) + (1/2)(3/4 )(5/6)(x^7/7) + ... (2) acos(x) = (pi/2 - asin(x)). В качестве быстрого теста я получил asin = 0,89726889283942934, а stdlib дал 0,90384998229123237. Для acos(x) я получил 0,67415967858914205, а stdlib дал 0,66694634450366430. Не уверен, сколько итераций нужно, чтобы достичь желаемой точности.   -  person Robinson    schedule 11.03.2015
comment
Чтобы добавить, есть другой способ расчета около 1, потому что, как вы говорите, это неточно. Здесь есть вопрос об этом: stackoverflow.com/questions/20196000/   -  person Robinson    schedule 11.03.2015
comment
@Robinson Ваш первый комментарий - это как раз серия Тейлора, которую я уже реализовал. Но я посмотрю на другой вопрос.   -  person DanceIgel    schedule 11.03.2015


Ответы (2)


Графический процессор NVIDIA GT 555M — это устройство с вычислительными возможностями 2.1, поэтому имеется встроенная аппаратная поддержка основных операций с двойной точностью, включая плавное множественное добавление (FMA). Как и во всех графических процессорах NVIDIA, операция извлечения квадратного корня эмулируется. Я знаком с CUDA, но не с GLSL. Согласно версии 4.3 спецификации GLSL, предоставляет FMA двойной точности как функцию fma() и предоставляет квадратный корень двойной точности, sqrt(). Неясно, правильно ли округлена реализация sqrt() в соответствии с правилами IEEE-754. Предположу, что по аналогии с CUDA.

Вместо использования ряда Тейлора можно было бы использовать полиномиальное минимаксное приближение, тем самым уменьшая необходимое количество терминов. Минимаксные приближения обычно генерируются с использованием варианта алгоритма Ремеза. Для оптимизации скорости и точности необходимо использовать FMA. Оценка полинома с помощью схемы Хорнера способствует высокой точности. В приведенном ниже коде используется схема Хорнера второго порядка. Как и в ответе от DanceIgel, acos удобно вычислять с использованием приближения asin в качестве базового строительного блока в сочетании со стандартными математическими тождествами.

При использовании 400 млн тестовых векторов максимальная относительная ошибка, наблюдаемая в приведенном ниже коде, составила 2,67e-16, а максимальная ulp обнаружена ошибка 1,442 ulp.

/* compute arcsin (a) for a in [-9/16, 9/16] */
double asin_core (double a)
{
    double q, r, s, t;

    s = a * a;
    q = s * s;
    r =             5.5579749017470502e-2;
    t =            -6.2027913464120114e-2;
    r = fma (r, q,  5.4224464349245036e-2);
    t = fma (t, q, -1.1326992890324464e-2);
    r = fma (r, q,  1.5268872539397656e-2);
    t = fma (t, q,  1.0493798473372081e-2);
    r = fma (r, q,  1.4106045900607047e-2);
    t = fma (t, q,  1.7339776384962050e-2);
    r = fma (r, q,  2.2372961589651054e-2);
    t = fma (t, q,  3.0381912707941005e-2);
    r = fma (r, q,  4.4642857881094775e-2);
    t = fma (t, q,  7.4999999991367292e-2);
    r = fma (r, s, t);
    r = fma (r, s,  1.6666666666670193e-1);
    t = a * s;
    r = fma (r, t, a);

    return r;
}

/* Compute arccosine (a), maximum error observed: 1.4316 ulp
   Double-precision factorization of π courtesy of Tor Myklebust
*/
double my_acos (double a)
{
    double r;

    r = (a > 0.0) ? -a : a; // avoid modifying the "sign" of NaNs
    if (r > -0.5625) {
        /* arccos(x) = pi/2 - arcsin(x) */
        r = fma (9.3282184640716537e-1, 1.6839188885261840e+0, asin_core (r));
    } else {
        /* arccos(x) = 2 * arcsin (sqrt ((1-x) / 2)) */
        r = 2.0 * asin_core (sqrt (fma (0.5, r, 0.5)));
    }
    if (!(a > 0.0) && (a >= -1.0)) { // avoid modifying the "sign" of NaNs
        /* arccos (-x) = pi - arccos(x) */
        r = fma (1.8656436928143307e+0, 1.6839188885261840e+0, -r);
    }
    return r;
}
person njuffa    schedule 08.03.2017

Моя текущая точная реализация шейдера 'acos()' представляет собой смесь из обычной серии Тейлора и ответа от Bence. С 40 итерациями я получаю точность 4,44089e-16 для реализации acos() из math.h. Может быть, это не самое лучшее, но это работает для меня:

И вот оно:

double myASIN2(double x)
{
    double sum, tempExp;
    tempExp = x;
    double factor = 1.0;
    double divisor = 1.0;
    sum = x;
    for(int i = 0; i < 40; i++)
    {
        tempExp *= x*x;
        divisor += 2.0;
        factor *= (2.0*double(i) + 1.0)/((double(i)+1.0)*2.0);
        sum += factor*tempExp/divisor;
    }
    return sum;
}

double myASIN(double x)
{
    if(abs(x) <= 0.71)
        return myASIN2(x);
    else if( x > 0)
        return (PI/2.0-myASIN2(sqrt(1.0-(x*x))));
    else //x < 0 or x is NaN
        return (myASIN2(sqrt(1.0-(x*x)))-PI/2.0);

}

double myACOS(double x)
{
    return (PI/2.0 - myASIN(x));
}

Есть замечания, что можно было бы сделать лучше? Например, используя LUT для значений фактора, но в моем шейдере 'acos()' вызывается только один раз, поэтому в нем нет необходимости.

person DanceIgel    schedule 11.03.2015