Квадратные артефакты GLSL NVidia

Я столкнулся с проблемой, когда шейдер GLSL генерирует неправильное изображение на следующих графических процессорах:
GT 430
GT 770
GTX 570
GTX 760

Но нормально работает на этих: Intel HD Graphics 2500, Intel HD 4000, Intel 4400, GTX 740M, Radeon HD 6310M, Radeon HD 8850.

Код шейдера следующий:

bool PointProjectionInsideTriangle(vec3 p1, vec3 p2, vec3 p3, vec3 point)
{
  vec3 n = cross((p2 - p1), (p3 - p1));

  vec3 n1 = cross((p2 - p1), n);
  vec3 n2 = cross((p3 - p2), n);
  vec3 n3 = cross((p1 - p3), n);

  float proj1 = dot((point - p2), n1);
  float proj2 = dot((point - p3), n2);
  float proj3 = dot((point - p1), n3);

  if(proj1 > 0.0)
    return false;
  if(proj2 > 0.0)
    return false;
  if(proj3 > 0.0)
    return false;
  return true;
}

struct Intersection
{
    vec3 point;
    vec3 norm;
    bool valid;
};

Intersection GetRayTriangleIntersection(vec3 rayPoint, vec3 rayDir, vec3 p1, vec3 p2, vec3 p3)
{
    vec3 norm = normalize(cross(p1 - p2, p1 - p3));

    Intersection res;
    res.norm = norm;
    res.point = vec3(rayPoint.xy, 0.0);
    res.valid = PointProjectionInsideTriangle(p1, p2, p3, res.point);
    return res;
}

struct ColoredIntersection
{
    Intersection geomInt;
    vec4 color;
};

#define raysCount 15
void main(void)
{
    vec2 radius = (gl_FragCoord.xy / vec2(800.0, 600.0)) - vec2(0.5, 0.5);

    ColoredIntersection ints[raysCount];

    vec3 randomPoints[raysCount];
    int i, j;


    for(int i = 0; i < raysCount; i++)
    {
        float theta = 0.5 * float(i);
        float phi = 3.1415 / 2.0;
        float r = 1.0;
        randomPoints[i] = vec3(r * sin(phi) * cos(theta),  r * sin(phi)*sin(theta), r * cos(phi));

        vec3 tangent = normalize(cross(vec3(0.0, 0.0, 1.0), randomPoints[i]));
        vec3 trianglePoint1 = randomPoints[i] * 2.0 + tangent * 0.2;
        vec3 trianglePoint2 = randomPoints[i] * 2.0 - tangent * 0.2;

        ints[i].geomInt = GetRayTriangleIntersection(vec3(radius, -10.0), vec3(0.0, 0.0, 1.0), vec3(0.0, 0.0, 0.0), trianglePoint1, trianglePoint2);
        if(ints[i].geomInt.valid)
        {
            float c = length(ints[i].geomInt.point);
            ints[i].color = vec4(c, c, c, 1.0);
        }
    }

    for(i = 0; i < raysCount; i++)
    {
        for(j = i + 1; j < raysCount; j++)
        {
            if(ints[i].geomInt.point.z < ints[i].geomInt.point.z - 10.0)
            {
                gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
                ColoredIntersection tmp = ints[j];
                ints[j] = ints[i];
                ints[i] = tmp;
            }
        }
    }

    vec4 resultColor = vec4(0.0, 0.0, 0.0, 0.0);
    for(i = 0; i < raysCount + 0; i++)
    {
        if(ints[i].geomInt.valid)
            resultColor += ints[i].color;
    }

    gl_FragColor = clamp(resultColor, 0.0, 1.0);
}

Upd: Я заменил векторную нормализацию встроенными функциями и на всякий случай добавил claming gl_FragColor.

Код представляет собой упрощенную версию реального шейдера, ожидаемое изображение:
http: //img.owely.  com / screen / 131316 / original_owely20140426-19006-1w4w4ye.? 1398554177
Но я получаю следующее:
http://img.owely.com/screens/131315/original_owely20140426-18968-a7fuxu.?1398553652

Случайные повороты кода полностью удаляют артефакты. Например, если я изменю строку

if(ints[i].geomInt.valid) //1

to

if(ints[i].geomInt.valid == true) //1

который, по-видимому, не должен каким-либо образом влиять на логику или полностью удалять двойной цикл, который ничего не делает (отмечен как 2), артефакты исчезают. Обратите внимание, что двойной цикл вообще ничего не делает, так как условие

if(ints[i].geomInt.point.z < ints[i].geomInt.point.z - 10.0)
{
  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  return;
  ColoredIntersection tmp = ints[j];
  ints[j] = ints[i];
  ints[i] = tmp;
}

Никогда не может быть удовлетворено (левая и правая стороны имеют индекс i, а не i, j) и нет NaN. Этот код абсолютно ничего не делает, но каким-то образом производит артефакты.

Вы можете протестировать шейдер и демонстрацию самостоятельно, используя этот проект (полный проект MSVS 2010 + исходники + скомпилированный двоичный файл и шейдер, использует включенный SFML): https://dl.dropboxusercontent.com/u/25635148/ShaderTest.zip

Я использую sfml в этом тестовом проекте, но это на 100% нерелевантно, потому что реальный проект, о котором я упоминал об этой проблеме, не использует эту библиотеку.

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


person Suslik    schedule 27.04.2014    source источник
comment
Интересные артефакты. На настройках проблема воспроизводится, полностью ли повторяется? Допустим, вы запускаете его 10 раз подряд, всегда ли вы получаете артефакты? Всегда один и тот же плохой результат или в этом есть какая-то случайность?   -  person Reto Koradi    schedule 27.04.2014
comment
Он на 100% воспроизводится на большинстве современных графических процессоров NVidia. Если я запускаю шейдер на встроенной графике Intel, артефактов нет, если на gf640m - они появляются. Сами квадратные артефакты изменяют каждый кадр без какого-либо заметного рисунка.   -  person Suslik    schedule 27.04.2014
comment
Если вы удалите return из своей ветки, это что-нибудь изменит?   -  person Andon M. Coleman    schedule 27.04.2014
comment
Я добавил этот возврат в качестве меры отладки, чтобы показать, что условие никогда не выполняется. И нет, я перепроверил, и этот возврат никак не влияет на изображение результата.   -  person Suslik    schedule 27.04.2014
comment
Ах, это в основном напоминает мне несинхронизированное чтение / запись в буфер цвета. Вы можете произвести почти идентичный эффект, используя загрузку / сохранение изображений без барьера памяти. Однако, поскольку некоторые из этих блоков на экране (особенно в верхнем левом углу) не выровнены строго по окну, я думаю, что это проблема вычислений, а не хранения. Вы пробовали использовать прагмы для отключения оптимизации компилятора? У NV довольно много проприетарных, ищите #pragma optionNV.   -  person Andon M. Coleman    schedule 27.04.2014
comment
Вы также можете использовать встроенные функции для таких вещей, как нормализация, чтобы избежать трудностей с отслеживанием FP.   -  person Andon M. Coleman    schedule 27.04.2014
comment
Но разве любая оптимизация не предназначена для оптимизации кода без изменения его логики? Единственный случай, когда оптимизация может заставить код вести себя иначе, - это когда код имеет какое-либо неопределенное поведение. Так зачем мне отключать любую оптимизацию, если я действительно хочу оптимизировать свой код (шейдер на самом деле довольно дорогостоящий)? И спасибо за встроенную нормализацию, я про нее забыл.   -  person Suslik    schedule 27.04.2014
comment
Необязательно, оптимизация может делать забавные вещи с помощью арифметики с плавающей запятой. Графические процессоры не полностью соответствуют IEEE 754, а более старые модели - тем более. Рендеринг гораздо более привередлив, когда дело доходит до такого рода вещей, чем приложение общего назначения, работающее на ЦП. Компиляция общего программного обеспечения с быстрым математическим вычислением и точным FP часто вообще не показывает никакой разницы, но компиляция шейдеров с быстрым и точным FP может иметь огромную разницу. И чем больше вы сами реализуете определенные фундаментальные операции, а не используете встроенные, тем больше вероятность, что вы столкнетесь с этим.   -  person Andon M. Coleman    schedule 28.04.2014
comment
Я заменил все нормализации встроенными функциями и добавил защиту - ограничение для gl_FragColor. Не влияет на артефакты. Загрузил обновленную демоверсию и изменил источник в исходном посте.   -  person Suslik    schedule 28.04.2014
comment
Собственно, использование констант типа 1e-5 определенно может привести к такому случаю. В зависимости от точности используемого представления с плавающей запятой эти магические числа могут даже не представляться. lowp, например, гарантирует не меньше 2 ^ -8.   -  person Andon M. Coleman    schedule 28.04.2014


Ответы (3)


Я не думаю, что с вашим шейдером что-то не так. Конвейер openGL выполняет рендеринг в буфер кадра. Если вы воспользуетесь этим кадровым буфером до завершения рендеринга, вы часто получите то, что видели. Имейте в виду, что glDrawArrays и подобные им асинхронны (функция возвращается до того, как графический процессор завершит рисование вершин).

Чаще всего эти квадратные артефакты используются, когда вы используете результирующий буфер кадра в качестве текстуры, которая затем используется для дальнейшего рендеринга.

Предполагается, что драйвер OpenGL отслеживает зависимости и должен знать, как ждать, пока зависимости будут выполнены.

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

В качестве обходного пути вы можете обнаружить, что вызов glFinish или даже glReadPixels (с одним пикселем) решает проблему.

Также имейте в виду, что эта проблема связана с таймингом, и упрощение шейдера вполне может решить эту проблему.

person doron    schedule 29.04.2014
comment
Нет, причина не в синхронизации. По крайней мере, мне не удалось удалить артефакты никакими параметрами синхронизации (glFlush () / glFenceSync () / wglSwapInterval (), Sleep ()). И я выполняю рендеринг непосредственно в окно, поэтому проблемы с синхронизацией рендеринга с текстурой тоже не имеют значения. Если вы мне не верите, вы можете легко проверить это самостоятельно, если у вас есть Windows и графический процессор Nvidia: все исходные коды и библиотеки включены во вложение. - person Suslik; 30.04.2014
comment
Что ж, это вполне может быть ошибка в драйверах NVidia OpenGL. - person doron; 30.04.2014

Если кому-то все еще интересно, я задавал этот вопрос на многочисленных специализированных сайтах, включая opengl.org и devtalk.nvidia.com. Я не получил конкретного ответа о том, что не так с моим шейдером, только несколько предложений, как решить мою проблему. Например, используйте if (condition == true) вместо if (condition), используйте как можно более простые алгоритмы и тому подобное. В конце концов, я выбрал одну из самых простых ротаций моего кода, которая избавляет от проблемы: я просто заменил

struct Intersection  
{  
    vec3 point;  
    vec3 norm;  
    bool valid;  
};  

с участием

struct Intersection  
{  
    bool valid;  
    vec3 point;  
    vec3 norm;  
};  

Было множество других ротаций кода, из-за которых артефакты исчезли, но я выбрал этот, потому что я смог протестировать на большинстве других систем, с которыми у меня были проблемы раньше.

person Suslik    schedule 29.04.2014
comment
Честно говоря, я бы попытался поместить bool valid между двумя vec3, зная, что я делаю с предпочтительным выравниванием данных графических процессоров. vec3 любит начинаться с 16-байтовой границы и в основном обрабатывается так же, как vec4. Вы можете довольно эффективно поместить 4-байтовую переменную (да, bool является 32-битной в GLSL) между двумя vec3, не нарушая выравнивания и не тратя впустую пространство. - person Andon M. Coleman; 30.04.2014

Я видел, как именно это происходило в GLSL, когда переменные не инициализированы. Например, на некоторых видеокартах vec3 будет (0,0,0) по умолчанию, но на других видеокартах это будет другое значение. Вы уверены, что не используете переменную без предварительного присвоения ей значения? В частности, вы не инициализируете ColoredIntersection.color, если Insersection.valid имеет значение false, но я думаю, что вы используете его позже.

person MusicMan3001    schedule 12.11.2014