Эффективная порядко-независимая прозрачность на Android* с использованием фрагментного шейдера

Download Sample Code ZIPfile

Введение

Этот образец демонстрирует использование расширения GL_INTEL_fragment_shader_ordering, написанного под профиль OpenGL 4.4 и технические требования GLES 3.1. Минимальная требуемая версия OpenGL – 4.2 или ARB_shader_image_load_store. Расширение представляет новую встроенную функцию GLSL,  beginFragmentShaderOrderingINTEL(),которая блокирует выполнение вызова фрагментного шейдера до тех пор, пока вызовы от предыдущих базовых элементов, отображаемые на тех же ху-координатах окна, не будут завершены. В примере эта линия поведения используется для предоставления решений, обеспечивающих порядко-независимую прозрачность в типичной 3D-сцене в реальном времени.

Порядко-независимая прозрачность

Прозрачность – это фундаментальная проблема для рендеринга в реальном времени, ввиду сложности наложения случайного числа прозрачных слоёв в правильном порядке. Этот пример построен на работе, изначально описанной в статьях adaptive-transparency и multi-layer-alpha-blending (Марк Сальви, Джефферсон Монтгомери, Картик Вайданатан и Аарон Лефон). Эти статьи показывают, как прозрачность может точно соответствовать реальным результатам, полученным от компоновки с использованием А-буфера, но может быть от 5 до 40 раз быстрее, благодаря использованию различных техник необратимого сжатия применительно к прозрачности данных. Данный пример представляет собой алгоритм на базе этих техник сжатия, который подходит для включения в такие приложения, как, например, игры.

Прозрачность бросает вызов

Пример рендеринга тестовой сцены с использованием стандартного альфа-смешивания показан на Рис. 1:


Рис. 1:Пример порядко-независимой прозрачности (OIT)

Геометрия визуализируется в фиксированном порядке: за землей следуют объекты внутри свода, затем свод и, наконец, растения снаружи. Блочные объекты рисуются первыми и обновляют буфер глубины, а затем рисуются прозрачные объекты в том же порядке без обновления буфера глубины. Увеличенное изображение демонстрирует один из визуальных артефактов, получающихся в результате: листва находится внутри свода, но перед несколькими плоскостями стекла. К сожалению, порядок рендеринга диктует правила таким образом, что все плоскости стекла, даже те, что находятся позади листвы, рисуются поверх. Обновление буфера глубины прозрачным объектом создает другой ряд проблем. Традиционно их можно решить разбивкой объекта на несколько небольших частей и их сортировкой front-to-back, исходя из точки расположения камеры. Но даже так идеального результата не достичь, поскольку объекты могут перекрещиваться, а затраты рендеринга, тем временем, возрастают с прибавлением числа отсортированных объектов.

Рис. 2 и рис. 3 показывают увеличенный визуальный артефакт, где на рис. 2 все плоскости стекла нарисованы перед листвой и на рис. 3 корректно отсортированы.


Рис. 2: Не отсортированы


Рис. 3: Отсортированы

Порядко-независимая прозрачность в реальном времени

Было множество попыток применить компоновку произвольно упорядоченных базовых геометрических элементов без необходимости сортировки на CPU или разбивки геометрии на непересекающиеся элементы. Среди таких попыток - depth-peeling, требующий многократного представления геометрии и техник А-буфера, где все фрагменты, связанные с заданным пикселем, хранятся в связном списке, отсортированы и затем перемешаны в корректном порядке. Несмотря на успех А-буфера в офлайн-рендеринге, он мало используется при рендеринге в реальном времени из-за неограниченных требований к памяти и, как правило, низкой производительности.

Новый подход

Вместо А-буфера: хранения всех цветов и данных глубины в попиксельных списках и последующей их сортировки и компоновки, пример использует исследование Марко Сальви и реструктурирует уравнение альфа-смешивания с целью избегания рекурсии и сортировки, создавая «функцию видимости» (Рис. 4):


Рис. 4:Функция видимости

Число шагов в функции видимости соответствует числу узлов, используемых для хранения информации по видимости на попиксельном уровне в процессе рендеринга сцены. По мере добавления пиксели хранятся в структуре узла до его полного заполнения. Затем при попытке включения большего числа пикселей алгоритм подсчитывает, какой из предыдущих узлов может быть присоединен для создания самой маленькой вариации в функции видимости, при этом сохраняя размер набора данных. Финальный этап – вычисление функции видимости vis() и компоновка фрагментов при помощи формулы final_color= .

Образец визуализирует сцену на следующих этапах:

  1. Очистка Shader Storage Buffer Object до стандартных значений по умолчанию.
  2. Визуализация всей блочной геометрии в основной фреймбуфер с обновлением буфера глубины.
  3. Визуализация всей прозрачной геометрии без обновления буфера глубины; финальные фрагментные данные отброшены из фреймбуфера. Фрагментные данные хранятся в наборе узлов внутри Shader Storage Buffer Object.
  4. Резолв данных внутри Shader Storage Buffer Object и подмешивание финального результата в основной фреймбуфер.

Рис 5:

Априори, затраты чтения Shader Storage Buffer Object на стадии резолва могут быть крайне высокими из-за требований пропускной способности. В оптимизации, задействованной в примере, для маскировки участков, где прозрачные пиксели могли бы быть вмешаны во фреймбуфер, используется стенсил буфер. Это меняет рендеринг так, как показано на Рис. 6.

  1. Clear the Stencil buffer.
  2. Clear the Shader Storage Buffer Object to default values on the first pass.
  3. Постановка следующей стенсил операции:
    1. glDisable(GL_STENCIL_TEST);
  4. Визуализация всей блочной геометрии в основной фреймбуфер с обновлением глубины.
  5. Постановка следующих стенсил операций:
    1. glEnable(GL_STENCIL_TEST);
    2. glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
    3. glStencilFunc(GL_ALWAYS, 1, 0xFF);
  6. Визуализация всей прозрачной геометрии без обновления буфера глубины; финальные фрагментные данные интегрированы в основной фреймбуфер со значением альфа 0. Стенсил буфер отмечен для каждого фрагмента во фреймбуфере. Фрагментные данные хранятся в наборе узлов внутри Shader Storage Buffer Object. Отбрасывание фрагмента невозможно, так как это мешает обновлению стенсил.
  7. Постановка следующих стенсил операций:
    1. glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
    2. glStencilFunc(GL_EQUAL, 1, 0xFF);
  8. Резолв данных внутри Shader Storage Buffer Object только для фрагментов, прошедших стенсил тест и подмешивание финального результата в основной фреймбуфер.
  9. Постановка следующих стенсил операций:
    1. glStencilFunc(GL_ALWAYS, 1, 0xFF);
    2. glDisable(GL_STENCIL_TEST);

Рис. 6: Stencil Render Path

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

 
01 void PSOIT_InsertFragment_NoSync( float surfaceDepth, vec4 surfaceColor )
02{	
03	ATSPNode nodeArray[AOIT_NODE_COUNT];    
04
05	// Load AOIT data
06	PSOIT_LoadDataUAV(nodeArray);
07
08	// Update AOIT data
09	PSOIT_InsertFragment(surfaceDepth,		
10		1.0f - surfaceColor.w,  // transmittance = 1 - alpha
11		surfaceColor.xyz,
12		nodeArray);
13	// Store AOIT data
14	PSOIT_StoreDataUAV(nodeArray);
15}

Рис. 7: GLSL Shader Storage Buffer Code

Алгоритм, представленный выше, может быть применен на любом устройстве, которое поддерживает Shader Storage Buffer Objects. Однако существует один очень значимый недостаток: возможно наличие множества фрагментов в работе, отображаемых на тех же ху-координатах окна.

Если множественные фрагменты выполняются на тех же xy-координатах окна в одно и то же время, они будут использовать одни и те же начальные данные в PSOIT_LoadDataUAV,но приведут к разным значениям, которые будут испытываться и храниться в inPSOIT_StoreDataUAV – и последнее из них завершит перезапись всех прежних, что были обработаны. Такой эффект – вполне рутинная процедура компрессии, которая может варьироваться от фрейма к фрейму. Его можно заметить в примере при отмене Pixel Sync. Пользователь должен увидеть легкое мерцание в тех местах, где перекрываются прозрачности. Чтобы это было проще это увидеть, применяется функция зума. Чем больше фрагментов графический процессор в состоянии исполнять параллельно, тем больше вероятность увидеть мерцание.

По умолчанию пример избегает эту проблему, применяя новую встроенную GLSL-функцию, beginFragmentShaderOrderingINTEL(),которая может быть использована, когда строка расширения  GL_INTEL_fragment_shader_ordering показывается применительно к оборудованию. Функция ThebeginFragmentShaderOrderingINTEL()блокирует исполнение фрагментного шейдера до момента завершения всех вызовов шейдера от предыдущих базовых элементов, соответствующих тем же xy-координатам окна. Все операции обращения к памяти от предыдущих вызовов фрагментного шейдера, отображаемых на тех же ху-координатах, становятся видимыми для текущего вызова фрагментного шейдера при возврате функции. Это делает возможным слияние предыдущих фрагментов для создания функции видимости в детерминированной модели. Функция thebeginFragmentShaderOrderingINTEL не влияет на применение шейдера для фрагментов с неперекрывающимися ху-координатами.

Пример того, как вызвать beginFragmentShaderOrderingINTE,показан на Рис. 8.

01GLSL code example
02    -----------------
03
04    layout(binding = 0, rgba8) uniform image2D image;
05
06    vec4 main()
07    {
08        ... compute output color 
09        if (color.w > 0)        // potential non-uniform control flow
10        {
11            beginFragmentShaderOrderingINTEL();
12            ... read/modify/write image         // ordered access guaranteed 
13        }
14        ... no ordering guarantees (as varying branch might not be taken) 
15 
16        beginFragmentShaderOrderingINTEL();
17
18        ... update image again                  // ordered access guaranteed 
19    }

Рис. 8: beginFragmentShaderOrderingINTEL

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

В случае с OIT примером, она просто добавляется, как показано на Рис. 9:

void PSOIT_InsertFragment( float surfaceDepth, vec4 surfaceColor )
{	
    // from now on serialize all UAV accesses (with respect to other fragments shaded in flight which map to the same pixel)
#ifdef do_fso
    beginFragmentShaderOrderingINTEL();
#endif
    PSOIT_InsertFragment_NoSync( surfaceDepth, surfaceColor );
}

Рис. 9: Добавление упорядочения фрагмента в доступ к Shader Storage Buffer

Запрашивается из любого фрагментного шейдера, который потенциально может записывать прозрачные фрагменты, как показано на рис. 10.

 

01 out vec4 fragColor;// -------------------------------------
02 void main( )
03 {
04    vec4 result = vec4(0,0,0,1);
05
06    // Alpha-related computation
07    float alpha = ALPHA().x;
08    result.a =  alpha;
09    vec3 normal = normalize(outNormal);
10
11    // Specular-related computation
12    vec3 eyeDirection  = normalize(outWorldPosition - EyePosition.xyz);
13    vec3 Reflection    = reflect( eyeDirection, normal );
14    float  shadowAmount = 1.0;
15
16    // Ambient-related computation
17    vec3 ambient = AmbientColor.rgb * AMBIENT().rgb;
18    result.xyz +=  ambient;
19    vec3 lightDirection = -LightDirection.xyz;
20
21    // Diffuse-related computation
22    float  nDotL = max( 0.0 ,dot( normal.xyz, lightDirection.xyz ) );
23    vec3 diffuse = LightColor.rgb * nDotL * shadowAmount  * DIFFUSE().rgb;
24    result.xyz += diffuse;
25    float  rDotL = max(0.0,dot( Reflection.xyz, lightDirection.xyz ));
26    vec3 specular = pow(rDotL,  8.0 ) * SPECULAR().rgb * LightColor.rgb;
27    result.xyz += specular;
28    fragColor =  result;
29
30 #ifdef dopoit	
31   if(fragColor.a > 0.01)
32   {
33	PSOIT_InsertFragment( outPositionView.z, fragColor );
34	fragColor = vec4(1.0,1.0,0.0,0.0);
35   }
36 #endif
37 }

Рис. 10: Типичный фрагментный шейдер

Только те фрагменты, которые имеют альфа-фактор выше граничного значения, добавляются в Shader Storage Buffer Object, при этом отбраковываются любые фрагменты, не представляющие сцене никаких значимых данных.

Сборка тестового примера

Требования к билду

Установите последние версии Android* SDK и NDK:

Добавьте NDK и SDK в свою ветвь:

export PATH=$ANDROID_NDK/:$ANDROID_SDK/tools/:$PATH

Для сборки:

  1. Проследуйте в папку OIT_2014\OIT_Android*
  2. Только единожды вам может понадобиться инициализировать проект:                                                                                           android update project –path . --target android-19.
  3. Соберите NDK-компонент:                                                                                                                                                                    NDK-BUILD
  4. Соберите APK:
    ant debug
  5. Установите APK:
    adb install -r bin\NativeActivity-debug.apk or ant installd
  6. Выполните его

Выводы

Пример демонстрирует, как исследование адаптивной порядко-независимой прозрачности под руководством Марко Сальви, Джефферсона Монтгомери, Картика Вайданатана и Аарона Лефон, первоначально произведенное на высокопроизводительных дискретных видеокартах с использованием DirectX 11, может быть применено в реальном времени на планшете Android при помощи GLES 3.1 и упорядочения фрагментным шейдером. Алгоритм выполняется внутри постоянного требуемого объема памяти, который может варьироваться, исходя из требований визуальной достоверности. Оптимизации вроде стенсил буфера разрешают применение техники на широком ряде устройств на допустимом уровне производительности, обеспечивая практическое решение одной из самых насущных проблем рендеринга в реальном времени. Принципы, продемонстрированные в образце OIT, могут быть применены к целому спектру других алгоритмов, которые могли бы в нормальном режиме создавать попиксельные связные списки, включая техники объемного затенения и пост-процессинговое сглаживание.

Статьи по теме

https://www.opengl.org/registry/specs/INTEL/fragment_shader_ordering.txt
https://software.intel.com/ru-ru/articles/adaptive-transparency
https://software.intel.com/ru-ru/articles/multi-layer-alpha-blending

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