Введение
Этот образец демонстрирует использование расширения 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= .
Образец визуализирует сцену на следующих этапах:
|
Рис 5:
Априори, затраты чтения Shader Storage Buffer Object на стадии резолва могут быть крайне высокими из-за требований пропускной способности. В оптимизации, задействованной в примере, для маскировки участков, где прозрачные пиксели могли бы быть вмешаны во фреймбуфер, используется стенсил буфер. Это меняет рендеринг так, как показано на Рис. 6.
|
Рис. 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:
1 void PSOIT_InsertFragment( float surfaceDepth, vec4 surfaceColor ) 2 { 3 // from now on serialize all UAV accesses (with respect to other fragments shaded in flight which map to the same pixel) 4 #ifdef do_fso 5 beginFragmentShaderOrderingINTEL(); 6 #endif 7 PSOIT_InsertFragment_NoSync( surfaceDepth, surfaceColor ); 8 }
Рис. 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
Для сборки:
- Проследуйте в папку OIT_2014\OIT_Android*
- Только единожды вам может понадобиться инициализировать проект: android update project –path . --target android-19.
- Соберите NDK-компонент: NDK-BUILD
- Соберите APK:
ant debug - Установите APK:
adb install -r bin\NativeActivity-debug.apk or ant installd - Выполните его
Выводы
Пример демонстрирует, как исследование адаптивной порядко-независимой прозрачности под руководством Марко Сальви, Джефферсона Монтгомери, Картика Вайданатана и Аарона Лефон, первоначально произведенное на высокопроизводительных дискретных видеокартах с использованием 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