NeHe Tutorials Народный учебник по OpenGL
Статья 01

Interactive Order-Independent Transparency

Рисунок 1.  Показано корректная (a) и некорректная (b) визуализация прозрачных поверхностей.

 

Введение

 

Корректная визуализация прозрачных поверхностей, не преломляющих свет с использованием стандартных функций OpenGL [9] должна выполняться с использованием сортировки по глубине и все полигоны должны быть при этом не пересекающимися. Этот  факт раздражает или является препятствием для большинства разработчиков приложений, использующих OpenGL, потому что естественный порядок прохождения сцены (обычно один объект за раз) редко удовлетворяет этим требованиям. Например, объект может быть составным, с его собственными иерархиями преобразований. Эта задача становится даже более трудной с новейшими видеокартами, где вершины и фрагменты объектов могут быть обработаны определяемыми пользователем по-вершинными или по-фрагментными операциями в GPU. При использовании этих операций становится сложно давать гарантии, что фрагменты будут поступать в отсортированном порядке для каждого пикселя. Метод, представленный здесь, решает эту проблему зависимости от порядка прохождения сцены, посредством использования метода, который мы называем расслоением глубины (depth peeling). Расслоение глубины – это метод сортировки по глубине на уровне фрагментов, описанный Мамменом (Mammen) с использованием карт виртуальных пикселей (Virtual Pixel Maps)[7], и Дайфенбехом (Diefenbach) с использованием двунаправленного буфера глубины (dual depth buffer) [3]. Хотя и нет аппаратно поддерживаемого двунаправленного буфера глубины,  соответствующего описанию Дайфенбеха, Бастос (Bastos) заметил, что аппаратная поддержка карт теней вместе с проверкой прозрачности может быть использована для достижения такого же эффекта [2]. Используя этот вариант расслоения глубины, каждый уникальный уровень глубины сцены извлекается в слой, и слои располагаются в отсортированном по глубине порядке для создания правильно смешанного конечного изображения. Расслоение слоя требует отдельного порядконе-зависимого прохождения сцены. На Рис.1 сопоставлены правильная и неправильная прорисовки прозрачных поверхностей.

 

Целью этого документа является дать возможность OpenGL-разработчикам реализовать этот метод, используя расширения NVIDIA OpenGL и аппаратную поддержку GeForce3. Так как карты теней стали неотъемлемой частью метода, вначале приведем их небольшое описание, но заинтересованный читатель может обратиться к источникам, чтобы расширить этот материал более подробно.

 

Карты теней

 

Карты теней – это многопроходовый метод затенения, разработанный Лансом Вильямсом (Lance Williams) [11] в 1978. За первый проход сцена прорисовывается от источника света. Буфер глубины, созданный на этом проходе, - копируется в специальную «текстуру глубины» или карту теней. На втором проходе карту теней проецируют на экран, используя проективное наложение текстур [10, 4]. В отличие от обычного двухмерного проективного наложения текстур, где r координату не используют, мы используем r координату для вычисления расстояния от растеризованного фрагмента до источника света. Затем вычисляем функцию lookup (s, t) - расстояние до ближайшей поверхности к источнику света (по вектору который задается координатами текстуры s/t и плоскостью просмотра совмещенной с источником света, по направлению света). Если r £ lookup(s,t), то текущий фрагмент является видимым для источника света, и поэтому он не в тени. По сути дела, мы используем в первом проходе буфер глубины для определения того, какие поверхности видны с точки просмотра, которая совмещена с направленным источником света, а во втором проходе мы показываем эти поверхности как освещённые. Рис.2. иллюстрирует эту идею.

Рисунок 2.  Эти схемы, взяты из презентации Марка Килдарда по картам теней с GDC 2001.  На них проиллюстрировано теневое сопоставление, которое используется в картах теней.

 

Мы используем расширения SGIX_shadow и SGIX_depth_texture [8] чтобы воспользоваться аппаратной поддержкой карт теней GeForce3 в OpenGL. Расширение SGIX_shadow даёт возможность произвести сравнение текстурной координаты r с результатом расчета lookup. Расширение SGIX_depth_texture объявляет внутренние текстурные форматы и объявляет семантику для glCopyTex{Sub}Image2D для быстрого копирования из буфера глубины в буфер текстуры. Эти возможности полностью аппаратно реализованы в GeForce3. 

 

Хейдриком (Heidrich) [5] было показано, что мультитекстурирование может быть использовано для выполнения ограниченной формы карты теней. Она ограничена, т.к. требует множество модулей текстур, и она поддерживает только ближайшую фильтрацию и 8-битные тексели глубины (16-бит глубина в GeForce [6]). Для расслоения глубины нам нужна лучшая точность буфера глубины (24 бита), что неизбежно влечёт за собой использование расширения SGIX для теней.

 

Расслоение глубины

 

Расслоение глубины – это основная техника, которая делает возможной  порядко-независимую прозрачность. Стандартный тест глубины даёт нам ближайший фрагмент для каждого пикселя, но есть также фрагмент, который является вторым по удалённости, третьим, и так далее. Стандартная проверка глубины даёт нам только ближайший фрагмент без каких-либо ограничений, однако она не даёт нам непосредственного способа прорисовать вторую или n-ую по удалённости поверхность.

 

Изображения, которое мы получаем при расслоении глубины, показаны на Рис.3. Может быть, довольно трудно понять изображение слоя 1 и других, потому что идея «вторых по удалённости поверхностей» не интуитивна понятна. Чтобы помочь разглядеть различные поверхности, чайник визуализировали с двухсторонним освещением (снаружи – красное, внутри - зелёное), а плоскость подставки нарисована синим. Заметьте, что рисунок с надписью ‘слой 2’ находится в пределах чайника, но большинство фрагментов в этом слое находится на плоскости подставки (они синие). Без окрашивания было бы трудно это представить.

 

Примечание переводчика: при визуализации мы всегда видим только поверхности объектов, которые ближе всего к камере, если удалить эти поверхности, то мы увидим при визуализации поверхности, которые «вторые» по удаленности. Так же можно получить и «третьи» …

 

Рис3. На рисунке изображено простое расслоение глубины. Слой 0 показывает ближайший уровень глубины, слой 1 показывает второй по удалённости уровень глубины, и так далее. Двухстороннее освещение с отчётливой раскраской использованы, чтобы помочь различить поверхности.

 

Рис.4.  Полоски расслоения глубины согласно  удаленности слоев глубины, полученные после каждого успешного прохода.  На кадрах выше ближние (слева) поверхности показаны как черные сплошные линии, скрытые поверхности как тонкие черные линии, и “удаленные отслоенные” поверхности светло серым цветом.

 

На Рис.4. представлен более схематический вид расслоения глубины. Эти диаграммы аналогичны изображениям Рис.3, кроме того, теперь мы видим поперечный разрез объёма просмотра и выделили каждый слой. Как ясно видно из Рис.4., глубина меняется в пределах каждого слоя, и количество образцов уменьшается. Процесс расслоения, очевидно, фрагментирует уровень, так что куски обычно – не целые полигоны.

 

Процесс расслоения глубины – это фактически прямой многопроходовый алгоритм. На первом проходе мы визуализируем сцены обычно, и проверка глубины даёт нам ближайшую поверхность. На втором проходе мы используем буфер глубины, просчитанный на первом проходе для «вышедших за пределы» уровней глубины, которые меньше либо равны ближайшему уровню глубины из первого прохода. Второй проход создаёт буфер глубины для вторых ближайших поверхностей, который может быть использован для выходящих за пределы первой и второй ближайших поверхностей при третьем проходе. Модель простая, но хитрая. Нам надо выполнять две проверки глубины на фрагмент для того, чтобы это работало!

 

for (i=0; i<num_passes; i++)

{

       Очистить буфер глубины

       A = i % 2

       B = (i+1) % 2

       Модуль глубины 0:

              if(i == 0)

                     запретить тест глубины

              else

                     Разрешить тест глубины

              Связать буфер A

              Запретить запись глубины

              Функция глубины GREATER

       Модуль глубины 1:

              Связать буфер B

              Очистить буфер глубины

              Разрешить запись глубины

              Разрешить тест глубины

              Функция глубины LESS

       Визуализация сцены

       Сохранить буфер RGBA как слой i

}

 

Листинг 1. Псевдокод для расслоения глубины используя одновременно несколько буферов глубины.

 

Множественные проверки глубины

 

Наиболее естественный способ описать этот метод – это представить, что OpenGL поддерживает множество модулей глубины, каждый с собственным буфером глубины и своими состояниями. Отличие от API двунаправленного буфера глубины Дайфенбаха состоит в том, что мы допускаем существование n перезаписываемых модулей глубины, которые выполняются в последовательном порядке. При первом тесте глубины выявляются все не нужные фрагменты и они исключаются из дальнейшего процесса.

 

Псевдокод в Листинге 1 выполняет расслоение глубины, используя 2 модуля глубины. На каждом проходе, кроме первого, модуль глубины 0 используется для расслоения предыдущих ближайших фрагментов, а модуль глубины 1 производит «обычную» буферизацию глубины. Мы разделяем буфер глубины на разные модули глубины, потому что это упрощает представление алгоритма и более близко соответствует семантике ARB_multitexture. Это разделение удобно, потому что нам надо использовать буфер глубины, полученный в модуле глубины 1 на i-ом проходе как «расслоенный» буфер глубины для модуля глубины 0 на i+1 проходе.

 

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

 

Карты теней как тест глубины

 

Карты теней являются тестом глубины. В сущности для нас есть только несколько главных отличий между картами теней и алгоритмом буфера глубины:

·      Карты теней устанавливают атрибуты цвета фрагмента;

·      Тест глубины карты теней не привязан к позиции камеры;

·      Карта теней (буфер глубины) является не перезаписываемой во время сравнения теней (проверки глубины).

 

Нетрудно скомпенсировать эти отличия. Мы записываем результат сравнения карт теней во фрагмент прозрачности и используем тест прозрачности, чтобы избавиться от фрагментов, которые не прошли «проверку глубины», которую мы выбрали. Мы делаем ориентацию и разрешение карты теней такими же, как и у камеры. Мы можем использовать карты теней как проверку глубины только для чтения. Это хорошо, потому что это всё, что нам надо для реализации расслоения глубины как было описано в предыдущем параграфе, используя нашу воображаемую множественную проверку глубины в OpenGL. Кроме того, сейчас мы можем действительно реализовать это, используя настоящий OpenGL и аппаратное ускорение.

 

Проблема инвариантности

 

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

·      zw (z в пространстве окна) интерполируется линейно в пространстве окна с точностью текущего буфера глубины, и

·      r и q интерполируются линейно в пространство отсечения (гиперболически в пространство окна) с высокой точностью.

 

Возможные различия в точности и/или выполнении интерполяции являются источниками опасности появления отклонений. Рассмотрим интерполяцию глубины в уравнении 1, которая является линейной в пространстве окна:

 

Здесь zw – это z пространства окна, zc – это z пространства отсечения, wc – это w пространства отсечения, и числовые индексы 1 и 2 показывают две точки, которые интерполируют. Когда мы делаем карту теней, мы должны интерполировать величины как текстурные координаты, которые изменяются линейно в пространстве отсечения, так что мы интерполируем c и wc как текстурные координаты  r и q соответственно, и используем коэффициент r/q для получения значения, которое линейно изменяется в пространстве окна. Для рассматриваемого случая, когда мы считаем карту теней от вида камеры, мы получаем уравнения 2 и 3.

 

Когда мы будем считать коэффициент r/q, то мы заметим, что знаменатели в уравнениях 2 и 3 сокращаются, и что в нашем конкретном случае карт теней от вида камеры числитель уравнения 3 равен 1. Здесь остаётся только числитель уравнения 2, который идентичен выражению в уравнении 1. Хотя это алгебраически верно, «железо» не может делать некоторые из этих сокращений. Для фрагментов с одинаковым уровнем глубины «железо» проводит оценку сравнения, показанную в (4). Левая часть выражения интерполирует три величины и осуществляет четыре деления, в то время как правая просто интерполирует одну величину.

К счастью, расширение NV_texture_shader GeForce 3 [8] поддерживает режим, названный GL_DOT_PRODUCT_DEPTH_REPLACE_NV, который позволяет нам просчитывать значение глубины фрагмента, используя текстурные координаты. Глубина, просчитанная в этом текстурном шейдере, заменяет глубину фрагмента, который был просчитан в растеризаторе. Это означает, что для GeForce 3 мы можем просчитать глубину, которую мы храним в буфере глубины, точно таким же способом, как мы считали его, когда делали сравнение. Когда мы используем этот текстурный шейдер при создании нашей карты теней, у нас не возникает никаких отклонений. Это круто, потому что это означает, что мы не должны отвлекаться на бредовые факторы, чтобы решить задачу с LSB отклонением. Замена глубины текстурным шейдером является очень общей, и её очень просто использовать. Рис.5. иллюстрирует общую операцию замены глубины текстурного шейдера.

Рисунок 5. На этой диаграмме незначительно модифицирован слайд из презентации  Dominé и Spitzer’s на GDC 2001 о текстурных шейдерах GeForce3. Это описание шейдера замены глубины текстуры.

 

Для наших целей мы реально хотим интерполировать только zc и wc, используя отдельную текстурную координату для каждого, так что мы используем 1х1 GL_UNSIGNED_HILO_NV текстуру, где H и L равны нулю. По определению, 3-ий компонент unsigned HILO равен 1, так что мы делаем скалярное произведение (S, T, R) с координатами (0, 0, 1). Таким способом мы можем интерполировать R координату стадий 1 и 2, и мы используем генерацию текстурной координаты, чтобы убедиться, что R1 - это zc и R2 - это wc. Когда мы выполняем деление zc/wc для каждого фрагмента, мы эффективно интерполируем глубину пространства окна по такому же способу, как и s/q -это следующий проход карты теней.

 

Есть одно изменение, которое нам надо сделать. Когда мы рассматриваем стандартную трансформацию на конвейере отображения, мы часто делаем деление перспективы до  окна просмотра, масштаба диапазона глубины  и смещения. Замена глубины текстурным шейдером и вычисление глубины карты теней выполняют деление (zc/wc и s/q соответственно) как финальную операцию. Это означает, что мы должны применить масштаб диапазона глубины и смещение перед делением перспективы. Или, согласно другому способу,  для замены глубины и карт теней, мы должны трансформировать координаты в однородные оконные координаты точнее, чем однородное пространство отсечения.

 

Код в Листинге 2 иллюстрирует, как установить GL_DOT_PRODUCT_DEPTH_-REPLACE_NV текстурные шейдеры для расчёта z окна по способу, который близко соответствует стандартному проективному вычислению наложения текстур z окна. Для упрощения, мы используем генерацию координат текстуры GL_EYE_LINEAR  с единичным наложением для r координаты [0 0 1 0 ], и мы используем текстурную матрицу для выполнения преобразований. Наиболее эффективным подходом будет выполнить трансформацию в плоскости генерации координат.

 

glActiveTextureARB(GL_TEXTURE0_ARB);

simple_1x1_uhilo.bind();

glTexEnvi(GL_TEXTURE_SHADER_NV, GL_SHADER_OPERATION_NV, GL_TEXTURE_2D);

 

matrix4f m;

glActiveTextureARB(GL_TEXTURE1_ARB);

glTexEnvi(GL_TEXTURE_SHADER_NV, GL_SHADER_OPERATION_NV, GL_DOT_PRODUCT_NV);

glTexEnvi(GL_TEXTURE_SHADER_NV, GL_PREVIOUS_TEXTURE_INPUT_NV, GL_TEXTURE0_ARB);

glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_NONE);

glMatrixMode(GL_MODELVIEW);

glPushMatrix();

glLoadIdentity();

eye_linear_texgen();  // установить генерацию EYE_LINEAR с единичными плоскостями

texgen(true);         // разрешить генерацию на s,t,r, и q

glPopMatrix();

glMatrixMode(GL_TEXTURE);

glLoadIdentity();

glTranslatef( 0, 0,.5);

glScalef( 0, 0, .5);

reshaper.apply_perspective();  // применить матрицу проекции камеры

glMatrixMode(GL_MODELVIEW);

             

glActiveTextureARB(GL_TEXTURE2_ARB);

glTexEnvi(GL_TEXTURE_SHADER_NV, GL_SHADER_OPERATION_NV, GL_DOT_PRODUCT_DEPTH_REPLACE_NV);

glTexEnvi(GL_TEXTURE_SHADER_NV, GL_PREVIOUS_TEXTURE_INPUT_NV, GL_TEXTURE0_ARB);

glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_NONE);

glPushMatrix();

glLoadIdentity();

eye_linear_texgen();  // установить генерацию EYE_LINEAR с единичными плоскостями

texgen(true);         // разрешить генерацию на s,t,r, и q

glPopMatrix();

glMatrixMode(GL_TEXTURE);

glLoadIdentity();

m(0,0) = 0;   m(0,1) = 0;   m(0,2) = 0;   m(0,3) = 0;

m(1,0) = 0;   m(1,1) = 0;   m(1,2) = 0;   m(1,3) = 0;

m(2,0) = 0;   m(2,1) = 0;   m(2,2) = 0;   m(2,3) = 1;  // сместить q на r

m(3,0) = 0;   m(3,1) = 0;   m(3,2) = 0;   m(3,3) = 0;

glMultMatrix(m);

reshaper.apply_perspective();  // применить матрицу проекции камеры

glMatrixMode(GL_MODELVIEW);

             

glActiveTextureARB(GL_TEXTURE3_ARB);

glTexEnvi(GL_TEXTURE_SHADER_NV, GL_SHADER_OPERATION_NV, GL_TEXTURE_RECTANGLE_NV);

glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_NONE);

 

glActiveTextureARB(GL_TEXTURE0_ARB);

 

Листинг 2. Пример кода для настройки текстурного шейдера замены глубины для использования с расслоением глубины

 

Другой немного необычный аспект замены глубины текстурным шейдером показан в коде Листинга 2. Суть в том, что однородная оконная координаты должна быть сдвинутой в  четвёртой строке текстурной матрицы на r координату. Так надо, потому что скалярное произведение текстурных шейдеров выполняет только 3-х компонентным скалярным произведением, поэтому все величины должны быть в s, t, или r координатах.

 

Собираем всё вместе

 

Теперь у нас есть способ расчёта RGBA цвета для каждого уникальной  глубины для каждого пикселя. Всё это сохраняется как отдельные слои (или текстуры размером как область просмотра). Всё, что осталось, это рассчитать корректный порядко-зависимый цвет для каждого пикселя посредством компоновки слоёв в нужном порядке. Визуализация каждого слоя как текстуры, с размером области просмотра, делает это. Для компоновки от дальних к ближних (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) использована функция смешивания.

 

Рис.6 иллюстрирует результаты компоновки слоёв в финальном изображении. Заметим также, что два нижних рисунка на Рис.6 выглядят фактически (но не полностью) одинаково. Для полностью корректного результата мы должны извлечь каждый полупрозрачный образец для первого непрозрачного образца, но на практике это не требуется. Суть вычисления прозрачности в том, что к последующим задним образцам применен эффект ослабления, так что усечение является резонной (и целесообразной) формой аппроксимации. Например, сцена на Рис.6 «достаточно хороша» уже при трёх слоях.

 

Рисунок 6. Корректно отсортированные слои расслоенные по глубине. Если мы просто сохраним цвет ( RGBA) для каждого слоя, мы можем смешать их по глубине в финальном проходе. На этих изображениях показано, что смешение большего числа слоев, дает более корректную прозрачность.

 

Заключение

 

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

 

Некоторые изображения в этой статье взяты из демонстраций layerz и order_independent_transparency, которые можно найти в NVIDIA OpenGL SDK, которую можно найти тут http://www.nvidia.com/developer. Демонстрации лишь показывают метод, описанный здесь, но там возможны некоторые отличия от того, что описал Дайфенбах [3]. Презентации с GDC 2001, которые были использованы в некоторых изображениях, так же доступны на вышеупомянутом веб-сайте.

 

Благодарности

 

Руи Бастос (Rui Bastos) выдвинул очень хитрую идею расслоения глубины с использованием аппаратной поддержки карт теней, когда он принял во внимание аппаратную поддержку Woo карт теней для GeForce3 (официальное издание по этой теме вскоре появится). Мне помог Марк Килгард с назначением генерации установки текстурной координаты для замены уровня глубины текстурного шейдера, которая (замена) решает проблему инвариантности. Он также дал бесценную помощь на ранней стадии написания этой статьи.

 

Ссылки

[1]        James F. Blinn. Hyperbolic interpolation. IEEE Computer Graphics (SIGGRAPH) and Applications, 12(4):89 94, July 1992.

[2]        Rui Bastos.  Personal communication. Feb 2001.

[3]        Paul Diefenbach. Pipeline Rendering: Interaction and Realism Through Hardware-Based Multi-PassRendering. University of Pennsylvania, Department of Computer Science, Ph.D. dissertation, 1996.

[4]        Cass Everitt.  Projective texture mapping. http://www.nvidia.com/Marketing/developer/devrel.nsf/bookmark/BAB26B3133023C2088256A38007DE5E6. 2001

[5]        Wolfgang Heidrich.  High quality shading and lighting for hardware-accelerated rendering. http://www.cs.ubc.ca/~heidrich/Papers/phd.pdf. 1999.

[6]        Mark Kilgard. GDC 2001 – Shadow mapping with today’s OpenGL hardware. http://www.nvidia.com/Marketing/developer/devrel.nsf/bookmark/C89B7FC5F0497EFC88256A1800672176. March 2001.

[7]        Abraham Mammen.  Transparency and antialiasing algorithms Implemented with the virtual pixel maps technique. IEEE Computer Graphics and Applications, 9(4): 43-55, July 1989.

 

© Cass Everitt ( cass@nvidia.com )

PMG  12 октября 2005 (c)  Engor