Прежде чем приступать к изучению данного раздела, рекомендуется скачать исходный код к статье [23]. Это позволит ускорить последующий процесс изучения представленной технологии.
В архиве с реализацией содержится несколько директорий. Основной директорией является prj. В ней содержится весь исходный код (в поддиректориях inc и src) и медиа-контент (поддиректория media), в том числе шейдеры. Реализация для удобства разнесена по следующим файлам:
Ключевыми элементами реализации являются файлы k3d_oit11_d3d.h и k3d_oit11_oit.h. Рассмотрение следует начинать с k3d_oit11_d3d.h. Этот файл содержит реализацию класса CWindowD3D, являющегося потомком CWindowSDL. CWindowSDL, в свою очередь, реализует функции по созданию стандартного оконного приложения и управление им. Класс CWindowD3D содержит следующие методы:
Основные методы
Переопределяемые методы для взаимодействия с базовым классом из k3d_oit11_wnd.h
Переопределяемые методы для взаимодействия с классом-потомком из k3d_oit11_oit.h
Вспомогательные методы
Загрузка сцены в память видеокарты и визуализация
Все указанные методы используют стандартные вызовы DirectX11 API. Также, для того, чтобы упростить некоторые рутинные действия, несколько вспомогательных функций вынесено в k3d_oit11_api.h. Помимо этого, используется несколько структур для описания блоков данных класса, и все указатели являются «умными» – используется класс CComPtr (объявленный в atlbase.h). Класс CWindowD3D при этом работает с нижеприведенными структурами.
Листинг 1. Структура батча: геометрия сцены, константы, видимость объекта и его свойства
struct SBatch { std::vector<CComPtr<ID3D11Buffer> > pCBs; CComPtr<ID3D11VertexShader> pVS; CComPtr<ID3D11PixelShader> pPS, pPS_Ex; CComPtr<ID3D11Buffer> pVB, pIB; CComPtr<ID3D11InputLayout> pIL; CD3D11InputElementDesc ieDesc; UINT ieStride, ieVerts, ieInds; bool bVisible; // struct SConstantsVS { float matrWVP[16]; }; struct SConstantsPS { float color[4]; }; };
Листинг 2. Структура контекста: устройство, цепочка буферов, буфер глубины, состояний конвейера и статистика визуализации
struct SContext { CComPtr<ID3D11Device> pD; CComPtr<ID3D11DeviceContext> pC; CComPtr<IDXGISwapChain> pSC; CComPtr<ID3D11Query> pQ; CComPtr<ID3D11Texture2D> pDS; CComPtr<ID3D11DepthStencilView> pDSV; CComPtr<ID3D11RenderTargetView> pRTV; CComPtr<ID3D11UnorderedAccessView> pUAV; bool bDebug, bHW11, bOIT; size_t screenWidth, screenHeight; // statistics double timeStart, timePresentSec; size_t cntTranslucentRendered, cntTranslucentReserved; CComPtr<ID3D11ShaderResourceView> pSRV_Legend; bool bLegend; SBatch screenQuad; };
Листинг 3. Объект, описывающий 3D-сцену: прозрачные и непрозрачные объекты, настройки состояний конвейера для их корректной визуализации
struct SScene { struct SSolidContent { CComPtr<ID3D11DepthStencilState> pDSS; CComPtr<ID3D11BlendState> pBS; CComPtr<ID3D11RasterizerState> pRS; SBatch teapot; double timeRenderSec; } } solid; struct STranslucentContent { CComPtr<ID3D11DepthStencilState> pDSS; CComPtr<ID3D11BlendState> pBS; CComPtr<ID3D11RasterizerState> pRS; CComPtr<ID3D11SamplerState> pSS; CComPtr<ID3D11ShaderResourceView> pSRV_Particle; SBatch robot, particles; double timeBeginSec, timeRenderSec, timeEndSec; } translucent; } m_Scene;
Применение класса CWindowD3D без надстройки позволяет визуализировать полупрозрачные объекты с альфа-смешиванием по стандартной. Так как в этом случае порядок смешивания произвольный, результат визуализации в большинстве случаев будет некорректным (рис. 1).
Основная часть реализации технологии порядко-независимой прозрачности с использованием динамических списков находится в файле k3d_oit11_oit.h. В этом файле располагается класс CWindowOIT, отвечающего за реализацию технологии и являющегося потомком класса CWindowD3D. Класс CWindowOIT имеет следующие методы:
Основные методы
Переопределяемые методы
Используемые константы
А также класс использует приводимые в последующих листингах структуры.
Листинг 4. Структура, описывающая настройки прохода визуализации полупрозрачных объектов
struct SPathMain { // buffers CComPtr<ID3D11Texture2D> pT2D_Heads, pT2D_ZLinks, pT2D_Colors; CComPtr<ID3D11Texture2D> pT2D_Lens; CComPtr<ID3D11Buffer> pBUF_Offset, pBUF_Dump; CComPtr<ID3D11UnorderedAccessView> pUAV_Heads, pUAV_ZLinks, pUAV_Colors; CComPtr<ID3D11UnorderedAccessView> pUAV_Lens, pUAV_Offset; CComPtr<ID3D11ShaderResourceView> pSRV_Heads, pSRV_ZLinks, pSRV_Colors; CComPtr<ID3D11ShaderResourceView> pSRV_Lens; // states CComPtr<ID3D11DepthStencilState> pDSS; CComPtr<ID3D11BlendState> pBS; };
Листинг 5. Структура, описывающая настройки прохода пост-обработки
struct SPathPostprocess { // states CComPtr<ID3D11BlendState> pBS; CComPtr<ID3D11DepthStencilState> pDSS; // quad CComPtr<ID3D11PixelShader> pPS; CComPtr<ID3D11PixelShader> pPS_Debug; CComPtr<ID3D11ComputeShader> pCS; };
Указанные классы используют для задания команд управления конвейером визуализации следующие файлы шейдеров:
Для внедрения описываемой технологии в любой существующий пиксельный шейдер полупрозрачного материала достаточно добавить несколько маркеров. Их определения находятся в файле oit11_buffers.h и используются как показано в листинге 6.
Листинг 6. Пример использования технологии в качестве расширения пиксельного шейдера
#include <draw.h> //CODE #include <oit11_buffers.h> // PS_OIT_HEADER void PS_ENTRY_DrawTeapot ( in float4 iPosition : SV_Position, out float4 oTarget0 : SV_Target0 ) { oTarget0 = cColor; PS_OIT_END(iPosition, oTarget0); }
Далее приводится код сортировки и альфа-смешивания, исполняемый на этапе пост-обработки (листинг 7).
Листинг 7. Пиксельный шейдер этапа пост-обработки
#include <oit11_buffers.h> //CODE void PS_ENTRY_MergeStage11 ( in float4 iPosition : SV_Position, out float4 oColor : SV_Target0 ) { oColor = ProcessLayers(iPosition.xy); }
Реализация вспомогательных функций содержится в файле oit11_buffers.h. В следующем листинге предлагается реализация функции ProcessLayers.
Листинг 8. Функция обработки слоев прозрачности
float4 ProcessLayers ( in const uint2 iPosition ) { uint2 kpixel[OIT_PIXEL_LAYERS]; uint offset = srvHeads[iPosition.xy]; const int N = srvLens[iPosition.xy]; // [unroll] for(int i = 0; i < OIT_PIXEL_LAYERS; i++) { [flatten] if(i < N) { const uint2 pos = uint2(offset % MEMORY_DIM_X, offset / MEMORY_DIM_X); const float4 color = srvColors[pos]; const uint2 dl = srvZLink[pos]; kpixel[i] = uint2(dl.x, packColorUint(color)); offset = dl.y; } else { kpixel[i] = uint2(0xffffffff, 0); } } // #if (0 == OIT_OVERDRAW_DEBUG) return MergeLayers(kpixel, N); #else return float4(DebugColor(N), 0); #endif }
В этом же файле находится реализация функции смешивания слоев MergeLayers (листинг 9).
Листинг 9. Функция сортировки и смешивания слоев прозрачности
float4 MergeLayers ( in uint2 kpixel[OIT_PIXEL_LAYERS], in const uint N ) { float4 dest = float4(0, 0, 0, 1); // switch(N) { case 0: { return dest; } break; case 1: { float4 src = unpackColorUint(kpixel[0].y); return lerp(dest, float4(src.rgb, 0.0), src.a); } break; case 2: { [flatten] if(kpixel[0].x > kpixel[1].x) { float4 src = unpackColorUint(kpixel[0].y); dest = lerp(dest, float4(src.rgb, 0.0), src.a); src = unpackColorUint(kpixel[1].y); dest = lerp(dest, float4(src.rgb, 0.0), src.a); } else { float4 src = unpackColorUint(kpixel[1].y); dest = lerp(dest, float4(src.rgb, 0.0), src.a); src = unpackColorUint(kpixel[0].y); dest = lerp(dest, float4(src.rgb, 0.0), src.a); } return dest; } break; } // OddEvenMergeSort4(kpixel); if(N > 4) { OddEvenMergeSort8ex4(kpixel); if(N > 8) { #if (OIT_PIXEL_LAYERS > 8) OddEvenMergeSort16ex8(kpixel); #if (OIT_PIXEL_LAYERS > 16) if(N > 32) { #if (OIT_PIXEL_LAYERS > 32) OddEvenMergeSort64ex16(kpixel); #endif } else { OddEvenMergeSort32ex16(kpixel); } #endif #endif } } // for(int i = N - 1; i >=0; --i) { const float4 src = unpackColorUint(kpixel[i].y); dest = lerp(dest, float4(src.rgb, 0.0), src.a); } return dest; }
Для упаковки и распаковки значений цветов используется вспомогательный код листинга 10.
Листинг 10. Код упаковки и распаковки значений цветов
inline uint packColorUint ( in const float4 iColor ) { uint4 color = saturate(iColor) * 255.0 + 0.5; return (color.r << 24) + (color.g << 16) + (color.b << 8) + color.a; } // inline float4 unpackColorUint ( in const uint iPacked ) { uint4 color = uint4(iPacked >> 24, iPacked >> 16, iPacked >> 8, iPacked); color &= 0xff; return (color.rgba / 255.0); }
Буфера для записи-чтения и чтения задаются в заголовочном файле шейдеров (листинг 11).
Листинг 11. Назначение буферов в шейдере
RWTexture2D<float4> uavRT : register (u0); globallycoherent RWStructuredBuffer<uint> uavOffset : register (u1); globallycoherent RWTexture2D<uint> uavHeads : register (u2); globallycoherent RWTexture2D<uint> uavLens : register (u3); RWTexture2D<float4> uavColors : register (u4); RWTexture2D<uint2> uavZLink : register (u5); // Texture2D<uint> srvHeads : register (t0); Texture2D<uint> srvLens : register (t1); Texture2D<float4> srvColors : register (t2); Texture2D<uint2> srvZLink : register (t3);
На этапе сортировки, как сказано ранее, используется развернутый код сортирующей сети, построенной методом «чет-нечет» Батчера. В листинге 12 приводится вариант реализации для случая четырех сортируемых значений.
Листинг 12. Сортировка методом «чет-нечёт» Батчера четырех значений
inline void SwapBigEndian(inout uint2 kpixel[SORT_ARRAY_LENGTH], int i, int j) { [flatten] if(kpixel[i].x > kpixel[j].x) { uint2 t = kpixel[i]; kpixel[i] = kpixel[j]; kpixel[j] = t; } } inline void OddEvenMergeSort4(inout uint2 kpixel[SORT_ARRAY_LENGTH]) { SwapBigEndian(kpixel, 0, 1); SwapBigEndian(kpixel, 2, 3); SwapBigEndian(kpixel, 0, 2); SwapBigEndian(kpixel, 1, 3); SwapBigEndian(kpixel, 1, 2); }
Представленных фрагментов кода достаточно для внедрения технологии в любую систему виртуальной реальности, поддерживающую DirectX11. В демонстрационном приложении к статье также можно обнаружить и другие методы сортировки (вставками, пузырьком и битоническую). Включить их можно, закомментировав соответствующие строки. Кроме этого, в архиве находится решение с использованием вычислительных шейдеров. Все дополнительные материалы приводятся с целью предоставить возможность экспериментировать с предложенной технологией.
В следующей таблице приводится серия изображений, полученных на видеокарте ATI Radeon HD 5670.
![]() |
![]() |
![]() |
![]() |
Без прозрачности 800 кадров/сек |
Неправильная прозрачность 615 кадров/сек |
Правильная прозрачность 40 кадров/сек |
Число слоев |
В первой колонке приводится пример визуализации непрозрачного робота. Во второй - результат альфа-смешивания без учета порядка растеризации. В третьей - результат использования технологии порядко-независимой прозрачности с использованием динамических списков. Четвертое изображение - диагностический режим, показывающий информацию о числе слоев прозрачности для различных пикселей буфера кадра. Максимальное число на тестовой сцене достигает 60 слоев на пиксель. Таким образом, в худшем случае приходится сортировать 64 слоя. Этот случай довольно ресурсоемкий для расчёта на используемом тестовом аппаратном обеспечении. Из собранной статистики получено, что чтение памяти и сортировка с последующим альфа-смешиванием, то есть весь проход пост-обработки, при текущем ракурсе и разрешении изображения 800 на 600 пикселей занимает 22 мс. Кроме этого, в среднем на один пиксель приходится один слой прозрачности. В этом случае динамические списки позволяют существенно экономить видеопамять, выделяемую под хранение слоев прозрачности.
В таблице 3 представлена еще одна серия изображений. На них, кроме робота, представлены непрозрачный чайник и 1024 разноцветные частицы. Этот пример демонстрирует взаимодействие непрозрачные и прозрачных объектов сцены между собой. На изображении видно, что модель непрозрачного чайника корректно взаимодействует с полупрозрачными объектами, перекрывая их – в области перекрытия число слоев существенно меньше, чем в других частях полученного изображения.
![]() |
![]() |
![]() |
Неправильная прозрачность 377 кадров/сек |
Правильная прозрачность 22 кадра/сек |
Число слоев |
Стадия пост-обработки для случая, представленного в таблице 3, занимает 33 мс и среднее число слоев равно 6. Данные по-прежнему указаны для разрешения 800 на 600 пикселей.
Полученные результаты свидетельствуют о том, что наиболее ресурсоемким является чтение большого числа пикселей из памяти (ограниченность texture fillrate). А также производительность существенно падает при сортировке 64 слоев. В следующей таблице (таблица 4) приводится более детальное сравнение зависимости производительности от числа обрабатываемых слоев прозрачности. Разрешение по-прежнему остается 800 на 600 пикселей.
Дополнительная обработка подразумевает использование более длинной сортировки и цикла смешивания с большим числом итераций.
В статье мы познакомились с различными методами реализации порядко-независимой прозрачности на современных графических процессорах и получили необходимый набор знаний для самостоятельной реализации одного из методов. Кроме этого, была рассмотрена эффективная реализация технологии, элементы которой вы можете свободно использовать в любых разработках.
В будущем, судя по результатам тестирования на видеокарте бюджетного уровня, эта технология будет широко применяться большинством разработчиков игр. Этот день наступит когда большинство пользователей систем виртуальной реальности перейдет на аппаратные решения уровня DirectX версии 11. На сегодняшний же день этот API еще не настолько распространен в силу того, что требуемые аппаратные решения не представлены широко.
Так, на момент написания статьи на рынке предлагались только решения компании AMD и, к моменту публикации, на рынке только-только начинали появляться первые решения от компании NVIDIA на базе архитектуры Fermi [24]. Их доступность массовому потребителю является первым шагом к качественно новым фотореалистичным спецэффектам, одним из которых является порядко-независимая прозрачность.
Бинарная версия демонстрационного приложения: kore_oit11_dlists_x86_win7.zip
Исходный код: kore_oit11_dlists_src.zip
Для сборки демонстрационного приложения потребуются следующие версии продуктов:
Часть из необходимых для работы приложения библиотек находится в архиве. Кроме этого, для работы приложения с использованием аппаратного ускорения потребуется видеокарта с поддержкой DirectX11. Такими видеокартами являются:
Без соответствующего аппаратного обеспечения приложение будет использовать программную эмуляцию, что приведет к очень небольшому числу кадров в секунду.
Поскольку компиляция сложных шейдеров в ассемблерные инструкции графического процессора занимает длительный промежуток времени, рекомендуется создавать каталог cache в папке shaders (prj\media\shaders\). Кэширование будет полезно, если планируется неоднократно запускать приложение с одинаковыми значениями макро-подстановок, используемых в шейдере. В этом случае при последующем запуске стадия компиляции будет происходить почти мгновенно.
Статья участвовала в конкурсе статей по программированию #4 (2009).