Процедурный рендеринг разреженного пространства (SPVR) — это методика рендеринга пространственных эффектов в реальном времени. Мы очень рады, что в будущей книге «GPU Pro 6» будет глава, посвященная SPVR. В этом документе приводятся некоторые дополнительные сведения
SPVRс высокой эффективностью отрисовывает большой объем, разделяя его на маленькие кусочки и обрабатывая только используемые кусочки. Мы называем эти кусочки «метавокселями». Воксель — это наименьшая часть объема. Метавоксель — это трехмерный массив вокселей. А весь объем в целом — это трехмерный массив метавокселей. В примере есть константы времени компиляции для определения этих чисел. В настоящее время он настроен на объем, равный 10243 вокселям, в виде 323 метавокселей, каждый из которых состоит из 323 вокселей.
В примере также повышается эффективность за счет заполнения объема примитивами объема1. Поддерживаются многие типы примитивов объема. В примере реализован один из них — сфера с радиальным сдвигом. В примере используется кубическая карта для представления сдвига над поверхностью сферы (подробнее см. ниже). Мы выявляем метавоксели, на которые воздействуют примитивы объема, вычисляем цвет и плотность затронутых вокселей, распространяем освещение и проводим отслеживание лучей с точки зрения глаза.
В примере также эффективно используется память. Примитивы объема можно считать сжатым описанием содержания объема. Алгоритм эффективно распаковывает их на лету, последовательно переходя к наполнению и к отслеживанию лучей метавокселей. Такое переключение может выполняться для каждого метавокселя. Но переключение между заполнением и отслеживанием лучей связано с расходом ресурсов (например, смена шейдеров), поэтому алгоритм поддерживает заполнение списка метавокселей перед переходом к отслеживанию лучей для них. Он выделяет относительно небольшой массив метавокселей и повторно использует их при необходимости для обработки всего объема. Обратите внимание, что во многих типичных случаях используется только малая часть всего объема.
Рисунок 1. Частицы, воксели, метавоксели и различные системы координат
На рис. 1 показаны все составные части: воксели, метавоксели, частица примитива объема, источник света, камера и мир. У каждой составной части есть эталонная точка, которая определяется положением P, направленным вверх вектором U и направленным вправо вектором R. Система трехмерна, но на рис. 1 используется упрощенное двухмерное представление.
- Воксель — наименьшая часть объема. Для каждого вокселя задается цвет и плотность.
- Метавоксель — это трехмерный массив вокселей. Каждый метавоксель хранится в виде трехмерной текстуры (например, трехмерной текстуры 323
DXGI
_
FORMAT
_
R
16
G
16
B
16
A
16_
FLOAT
). - Объем — общий объем, состоящий из нескольких метавокселей. На рис. 1 показан упрощенный объем из метавокселей 2х2.
- Частица — сферический примитив объема с радиальным сдвигом. Обратите внимание, что это трехмерная частица, а не двухмерная плоскость.
- Камера — та самая камера, которая используется для рендеринга всей остальной сцены из точки наблюдения.
Описание алгоритма
// Рендеринг карты теней для каждой модели сцены, видимой из представления освещения
// Рендеринг предварительного Z-прохода для каждой модели сцены, видимой с точки зрения наблюдателя
// Применение частиц для каждой частицы: для каждого покрываемого частицами метавокселя путем присоединения частиц к списку частиц метавокселя
// Рендеринг метавокселей на цели рендеринга: для каждого непустого метавокселя, заполнение метавокселей частицами и картой теней в качестве входных данных, отслеживание лучей для метавокселей с точки зрения наблюдателя с использованием буфера глубины в качестве входных данных
// Отрисовка сцены в обратный буфер для каждой модели сцены с точки зрения наблюдателя
// Совмещение цели рендеринга с точки зрения наблюдателя с рендерингом полноэкранного спрайта обратного буфера с использованием цели отрисовки как текстуры
Обратите внимание, что в этом примере поддерживается заполнение нескольких метавокселей перед отслеживанием лучей. Заполняется «кэш» метавокселей, затем проводится отслеживание лучей, и процедура повторяется вплоть до обработки всех метавокселей. Также поддерживается заполнение метавокселей только в каждом n-номкадре или только однократное заполнение. Если в реальном приложении нужна статическая или медленно изменяющаяся объемная сцена, то приложение будет работать намного быстрее, если оно не будет обновлять все метавоксели в каждом кадре.
Заполнение объема
В примере метавоксели заполняются покрывающими их частицами. «Покрывающими» означает, что границы частиц пересекаются с границами метавокселя. Пустые метавоксели не обрабатываются. Приложение сохраняет цвет и плотность в каждом вокселе метавокселя (цвет в формате RGB, плотность в виде значения альфа-канала). Эта работа выполняется в пиксельном шейдере. Приложение записывает данные в трехмерную текстуру в виде неупорядоченного представления доступа (UAV) RWTexture3D. Образец приложения отображает двухмерный квадрат, состоящий из двух треугольников, такого размера, чтобы он совпадал с метавокселем (т. е. 32x32 пикселя для метавокселя 32x32x32). Пиксельный шейдер проходит каждый воксель в соответствующем столбце вокселей, вычисляет плотность и цвет каждого вокселя на основе частиц, покрывающих метавоксель.
В примере определяется, находится ли каждый воксель внутри каждой частицы. На двухмерной схеме показана упрощенная частица. (На схеме показана двухмерная окружность с радиальным сдвигом. В трехмерной системе используются сферы с радиальным сдвигом.) На рис. 1 показан радиус окружения частицы rP и ее радиус смещения rPD. Частица покрывает воксель, если расстояние между центром частицы PP и центром вокселя PV меньше расстояния сдвига rPD. Например, воксель в PVIнаходится внутри частицы, а воксель в PVO — снаружи.
Внутри = |PV– PP| < rPD
Скалярное произведение вычисляет квадрат длины вектора без затраты лишних ресурсов. Это позволяет избежать ресурсоемкой операции извлечения квадратного корня sqrt(), поскольку можно сравнивать не длины, а квадраты значений длины.
Внутри = (PV– PP) ∙ (PV– PP) < rPD2
Цвет и плотность зависят от положения вокселя внутри частицы и от расстояния до поверхности частицы rPDвдоль линии, проходящей от центра частицы через центр вокселя.
C = цвет (PV– PP, rPD)
D = плотность (PV– PP, rPD)
Интересны различные функции плотности. Вот некоторые возможные варианты.
- Двоичная — если воксель находится внутри частицы, то цвет = C и плотность = D (где C и D — константы). В противном случае цвет = черный, плотность = 0.
- Градиент — цвет изменяется от C1 до C2, а плотность изменяется от D1 до D2 по мере изменения расстояния из положения вокселя от центра частицы до поверхности частицы.
- Подстановка по текстуре — цвет может быть сохранен в одномерной, двухмерной или трехмерной текстуре, он подстанавливается в зависимости от расстояния (X, Y, Z).
В образце реализованы два примера: 1) постоянный цвет, оттенок которого определяется значением смещения, 2) градиент от ярко-желтого до черного на основе радиуса и возраста частицы.
Существует не менее трех интересных способов указания положения в метавокселе.
- Нормализованный
- Число с плавающей запятой
- Начало координат — в центре метавокселя
- Диапазон от -1,0 до 1,0
- Координаты текстуры
- Число с плавающей запятой (преобразуется в число с фиксированной запятой алгоритмом получения)
- Начало координат — в верхнем левом углу двухмерного изображения (верхний левый задний угол трехмерного изображения).
- Диапазон от 0,0 до 1,0
- Центр вокселя находится на половине metavoxelDimensions (т. е. 0,0 — угол вокселя, а 0,5 — центр).
- Индекс вокселя
- Целое число
- Начало координат — в верхнем левом углу двухмерного изображения (верхний левый задний угол трехмерного изображения).
- Диапазон от 0 до metavoxelDimensions — 1
- Z — направление света, а X и Y — плоскости, перпендикулярные направлению света.
Положение вокселя в пространстве метавокселя определяется положением метавокселя PM и индексами вокселя (X, Y). Индексы вокселя дискретны, их значение составляет от 0 до N-1 на протяжении метавокселя. На рис. 1 показана упрощенная сетка 2х2 метавокселей размером 8х8 вокселей с PVIв метавокселе (1, 0) в позиции вокселя (2, 6).
Освещение
После вычисления цвета и плотности каждого вокселя в метавокселе вычисляется освещенность вокселей. В образце используется простая модель освещения. Пиксельный шейдер последовательно обходит столбец вокселей, умножая цвет вокселя на текущее значение света. Затем значение света корректируется с учетом плотности вокселя. Существует как минимум два способа корректировки освещения. Вреннинге и Зафар1используют степень e-плотность. Мы используем 1/(1+плотность). В обоих случаях результат изменяется от 1 на нулевом расстоянии до 0 на бесконечности. Результаты выглядят схоже, но деление может быть быстрее, чем exp().
Ln+1 = Ln/(1+плотностьn)
Обратите внимание, что этот цикл распространяет свет по единственному метавокселю. В коде свет распространяется от одного метавокселя к другому с помощью текстуры распространения света. Код записывает последнее значение распространения света в текстуру. Следующий метавоксель считывает свое начальное значение распространения света из этой текстуры. Размер этой двухмерной текстуры задается для всего объема. Это дает два преимущества: можно параллельно обрабатывать несколько метавокселей; итоговое содержимое можно использовать в качестве карты света для отбрасывания теней от объема на остальную сцену.
Тени
В примере используются тени, отбрасываемые сценой на объем, и тени, отбрасываемые объемом на сцену. Сначала отрисовываются непрозрачные объекты сцены на простой карте теней. Объем получает тени путем ссылки на карту теней в начале распространения света. Тени отбрасываются путем проецирования последней текстуры распространения света на сцену. Более подробно о текстуре распространения света мы поговорим чуть позже.
Шейдер проводит выборку карты теней один раз на метавоксель (на столбец вокселей). Шейдер определяет индекс (т. е. строку в столбце), в котором первый воксель попадает в тень. Воксели, находящиеся перед shadowIndex, не в тени. Воксели, находящиеся в позиции shadowIndex
или после нее, находятся в тени.
Рисунок 2.Взаимосвязь между значениями Z теней и индексами
Значение shadowIndex
составляет от 0 до METAVOXEL_WIDTH
по мере изменения значения тени от верхней части метавокселя к его нижней части. Центр локального пространства метавокселя находится в точке с координатами (0, 0, 0), пространство занимает область от -1,0 до 1,0. Поэтому верхний край имеет координаты (0, 0, -1),
а нижний край — (0, 0, 1). Преобразование в пространство света/теней:
Top = (LightWorldViewProjection._m23 - LightWorldViewProjection._m22) Bottom = (LightWorldViewProjection._m23 + LightWorldViewProjection._m22)
Что дает следующий код шейдера в FillVolumePixelShader.fx:
float shadowZ = _Shadow.Sample(ShadowSampler, lightUv.xy).r; float startShadowZ = LightWorldViewProjection._m23 - LightWorldViewProjection._m22; float endShadowZ = LightWorldViewProjection._m23 + LightWorldViewProjection._m22; uint shadowIndex = METAVOXEL_WIDTH*(shadowZ-startShadowZ)/(endShadowZ-startShadowZ);
Обратите внимание, что метавоксели являются кубами, поэтому ширина METAVOXEL_WIDTH
также является высотой и глубиной.
Текстура распространения света
В дополнение к вычислению цвета и плотности каждого вокселя FillVolumePixelShader.fx записывает итоговое значение распространенного света в текстуру распространения света. В образце кода эта текстура называется $PropagateLighting. Это двухмерная текстура, покрывающая весь объем. Например, образец, настроенный как объем 10243 (323 метавокселя по 323 вокселя в каждом), будет иметь текстуру распространения света 1024x1024 (32*32=1024). Отметим два момента: эта текстура включает место для границы каждого метавокселя толщиной в один воксель; значение, сохраненное в текстуре, является последним значением до тени.
Каждый метавоксель имеет границу толщиной в один воксель, чтобы работала фильтрация текстур (при выборке в ходе отслеживания лучей из точки наблюдения). Объем отбрасывает тени на остальную сцену путем проецирования текстуры распространения света на эту сцену. При простой проекции появились бы визуальные артефакты в местах, где значения текстуры удваиваются для поддержки границы толщиной в один воксель. Артефактов удается избежать путем настройки координат текстуры с учетом этой границы. Вот этот код (из DefaultShader.fx):
float oneVoxelBorderAdjust = ((float)(METAVOXEL_WIDTH-2)/(float)METAVOXEL_WIDTH); float2 uvVol = input.VolumeUv.xy * 0.5f + 0.5f; float2 uvMetavoxel = uvVol * WIDTH_IN_METAVOXELS; int2 uvInt = int2(uvMetavoxel); float2 uvOffset = uvMetavoxel - (float2)uvInt - 0.5f; float2 lightPropagationUv = ((float2)uvInt + 0.5f + uvOffset * oneVoxelBorderAdjust ) * (1.0f/(float)WIDTH_IN_METAVOXELS);
Текстура распространения света сохраняет значение света в последнем вокселе, который не находится в тени. Если процесс распространения света встречает поверхность затенения, то распространение света обнуляется (свет не распространяется дальше объекта, отбрасывающего тень). Сохранение последнего значения освещения дает возможность использовать текстуру в качестве карты освещенности. Проецирование этого последнего значения освещения на сцену означает, что поверхность отбрасывания теней получает ожидаемое значение освещения. Поверхности, находящиеся в тени, не используют эту текстуру.
Отслеживание лучей
Рисунок 3.Отслеживание лучей из точки наблюдения
В образце производится отслеживание лучей метавокселя с помощью пиксельного шейдера (EyeViewRayMarch.fx), выборка берется из трехмерной текстуры в виде представления ресурсов шейдера (SRV). Отслеживается каждый луч от дальнего края до ближнего по отношению к точке наблюдения. Отфильтрованные выборки отбираются из соответствующей трехмерной текстуры метавокселя. Каждый отобранный цвет добавляется в итоговый цвет, а каждая отобранная плотность затеняет итоговый цвет и добавляется в итоговый альфа-канал.
blend = 1/(1+density) colorresult = colorresult * blend + color * (1-blend) alpharesult = alpharesult * blend
В образце каждый метавоксель обрабатывается независимо. Отслеживание лучей происходит по очереди для каждого метавокселя. Результаты смешиваются с целью рендеринга с точки зрения наблюдателя. Отслеживание лучей каждого метавокселя осуществляется путем рисования куба (т. е. 12 треугольников) из точки зрения наблюдателя. Пиксельный шейдер отслеживает лучи, проходящие через каждый пиксель, покрываемый кубом. При рендеринге куба используется исключение передней поверхности, поэтому пиксельный шейдер выполняется только один раз для каждого покрываемого пикселя. При рендеринге без исключения каждый луч приходилось бы отслеживать дважды — один раз для передних поверхностей, второй раз для задних. При рендеринге с исключением задней поверхности камера находилась бы внутри куба, пиксели были бы исключены и отслеживание лучей было бы невозможно.
В примере на рис. 3 показано отслеживание двух лучей в четырех метавокселях. Показано, как шаги лучей распределены вдоль каждого луча. Расстояние между шагами одинаковое при проецировании на вектор взгляда. Это означает, что у лучей, направленных в сторону от оси, шаги длиннее. На практике этот подход дал наилучшие результаты (например, по сравнению с равными шагами для всех лучей). Обратите внимание, что точки выборки начинаются на дальней плоскости, а не на задней поверхности метавокселя. Такой подход совпадает с выборкой для монолитного объема (без использования метавокселей). При начале отслеживания лучей на задней поверхности каждого метавокселя образовывались видимые швы на границах метавокселей.
На рис. 3 видно, как выборки оказываются в разных метавокселях. Серые элементы находятся вне метавокселей. Красные, зеленые и синие элементы оказываются в разных метавокселях.
Тестирование глубины
Шейдер отслеживания лучей учитывает буфер глубины, обрезая лучи в соответствии с буфером глубины. Для повышения эффективности отслеживание лучей начинается с первого шага луча, проходящего проверку глубины.
Рисунок 4. Взаимосвязь между глубиной и индексомs
Взаимосвязь между значениями глубины и индексами отслеживания лучей показана на рис. 4. Код прочитывает значение Z из Z-буфера и вычисляет соответствующее значение глубины (т. е. расстояние от наблюдателя). Величина индексов изменяется пропорционально от 0 до totalRaymarchCount в соответствии с изменением глубины от zMin до zMax. Из этого кода получаем (из EyeViewRayMarch.fx):
float depthBuffer = DepthBuffer.Sample( SAMPLER0, screenPosUV ).r; float div = Near/(Near-Far); float depth = (Far*div)/(div-depthBuffer); uint indexAtDepth = uint(totalRaymarchCount * (depth-zMax)/(zMin-zMax));
Здесь zMin и zMax — значения глубины с точки зрения отслеживания лучей. Значения zMax и zMin соответствуют самой дальней точке от наблюдателя и самой ближней к нему точке.
Сортировка.
При рендеринге метавокселей поддерживаются два порядка сортировки: один для света, другой для наблюдателя. Распространение света начинается в метавокселях, ближайших к источнику света, затем переходит к более дальним метавокселям. Метавоксели полупрозрачны, поэтому для получения правильного результата необходима сортировка с точки зрения наблюдателя. С точки зрения наблюдателя существует два способа сортировки: от заднего плана к переднему с «верхним» альфа-смешением и от переднего плана к заднему с «нижним» альфа-смешением.
Рисунок 5. Порядок сортировки метавокселей
На рис. 5 показано простое расположение трех метавокселей, камеры и источника света. Для распространения света требуется порядок 1, 2, 3; мы должны распространить свет через метавоксель 1, чтобы узнать, сколько света дойдет до метавокселя 2. Затем нужно провести свет через метавоксель 2, чтобы выяснить, сколько света достигнет метавокселя 3.
Также нужно сортировать метавоксели из точки зрения глаза. При рендеринге от переднего плана к заднему мы сначала отрисовали бы метавоксель 2. Синяя и фиолетовая линии показывают, почему расстояние до метавокселей 1 и 3 больше, чем до метавокселя 2. Нам нужно распространить свет через метавоксели 1 и 2 перед рендерингом метавокселя 3. В наихудшем случае требуется распространять свет через весь столбец перед рендерингом любого из его элементов. При рендеринге в порядке от заднего плана к переднему мы сможем отрисовать каждый метавоксель сразу же после распространения в нем света.
В образце сочетается сортировка от переднего плана к заднему и от заднего к переднему, чтобы поддерживать возможность рендеринга метавокселей сразу же после распространения света. Метавоксели над перпендикуляром (зеленая линия) отрисовываются от заднего плана к переднему с «верхним» смешением, а метавоксели под перпендикуляром — от переднего плана к заднему с «нижним» смешением. Такая процедура упорядочения всегда выдает правильные результаты, не требуя огромного объема памяти для размещения всего столбца метавокселей. Обратите внимание, что, если приложение способно выделить достаточно памяти, этот алгоритм всегда может сортировать от переднего плана к заднему с «нижним» смешением.
Альфа-смешение
В образце используется «верхнее» смешение для метавокселей, упорядоченных от заднего плана к переднему (т. е. сначала отрисовывается самый дальний метавоксель, затем следующий по порядку и т. д.). Для метавокселей, упорядоченных от переднего плана к заднему (т. е. сначала отрисовывается ближайший метавоксель, затем следующий и т. д.), используется «нижнее» смешение.
«Верхнее» смешение: Цветназначение = Цветназначение * Альфаисточник + Цветисточник
«Нижнее» смешение: Цветназначение = Цветисточник * Альфаназначение + Цветназначение
При этом альфа-смешение в обоих случаях работает одинаково: альфа-значение назначения увеличивается на альфа-значение пиксельного шейдера.
Альфаназначение = Альфаназначение * Альфаисточник
Ниже приведены состояния рендеринга для «верхнего» и «нижнего» смешения.
Состояния рендеринга «верхнего» смешения (из EyeViewRayMarchOver.rs)
SrcBlend = D3D11_BLEND_ONE DestBlend = D3D11_BLEND_SRC_ALPHA BlendOp = D3D11_BLEND_OP_ADD SrcBlendAlpha = D3D11_BLEND_ZERO DestBlendAlpha = D3D11_BLEND_SRC_ALPHA BlendOpAlpha = D3D11_BLEND_OP_ADD
Состояния рендеринга «нижнего» смешения (из EyeViewRayMarchUnder.rs)
SrcBlend = D3D11_BLEND_DEST_ALPHA DestBlend = D3D11_BLEND_ONE BlendOp = D3D11_BLEND_OP_ADD SrcBlendAlpha = D3D11_BLEND_ZERO DestBlendAlpha = D3D11_BLEND_SRC_ALPHA BlendOpAlpha = D3D11_BLEND_OP_ADD
Совмещение
Результатом отслеживания лучей из точки наблюдения является текстура с заранее умноженным значением альфа-канала. Мы отображаем полноэкранный спрайт с альфа-смешением для составления изображения с обратным буфером.
Цветназначение = Цветназначение * Альфаисточник + Цветисточник
Состояния рендеринга:
SrcBlend = D3D11_BLEND_ONE
DestBlend = D3D11_BLEND_SRC_ALPHA
В образце кода цель рендеринга с точки зрения наблюдателя может иметь меньшее разрешение, чем обратный буфер. При уменьшении цели рендеринга значительно повышается производительность, поскольку требуется отслеживать меньше лучей. Тем не менее, если цель рендеринга меньше обратного буфера, на этапе композиции будет выполнено увеличение масштаба, из-за чего вокруг краев силуэта могут образоваться трещины. Это распространенная проблема, решение которой мы оставим на будущее. Решить ее, в частности, можно путем масштабирования на этапе композиции.
Известные проблемы
- Если цель рендеринга с точки зрения наблюдателя меньше обратного буфера, при составлении возникают трещины.
- При несовпадении разрешения между текстурой распространения света и картой теней возникают трещины.
Заключение
Образец кода отображает пространственные эффекты с помощью заполнения разреженного объема примитивами и отслеживания лучей для получившихся объектов с точки зрения наблюдателя. Это пример, демонстрирующий полную поддержку интеграции пространственных эффектов с существующими сценами.
Разное
Некоторые слайды SPVR из презентации на конференции SIGGRAPH 2014:
https://software.intel.com/sites/default/files/managed/64/3b/VolumeRendering.25.pdf
См. пример в действии на Youtube*:
http://www.youtube.com/watch?v=50GEvbOGUks
http://www.youtube.com/watch?v=cEHY9nVD23o
http://www.youtube.com/watch?v=5yKBukDhH80
http://www.youtube.com/watch?v=SKSPZFM2G60
Дополнительные сведения об оптимизации для платформ Intel:
Whitepaper: Compute Architecture of Intel Processor Graphics Gen 8
IDF Presentation: Compute Architecture of Intel Processor Graphics Gen 8
Whitepaper: Compute Architecture of Intel Processor Graphics Gen 7.5
Intel Processor Graphics Public Developer Guides & Architecture Docs (Multiple Generations)
Благодарности
Благодарю за участие: Марка Фоконно-Дюфрена, Томера Барона, Джона Кеннеди, Джефферсона Монтгомера, Рэндалла Раувендала, Майка Берроуса, Фила Тэйлора, Аарона Кодэй, Егора Юсова, Филипа Стругара, Раджу Бала и Квернита Фремке.
Справочные материалы
1. M. Wrenninge и N. B. Zafar, SIGGRAPH 2010 и 2011 Production Volume Rendering, заметки к курсу: http://magnuswrenninge.com/content/pubs/ProductionVolumeRenderingFundamentals2011.pdf
2. M. Ikits, J. Kniss, A. Lefohn и C. Hansen, Volume Rendering Techniques (технологии объемной отрисовки), GPU Gems, http://http.developer.nvidia.com/GPUGems/gpugems_ch39.html
Дополнительные сведения об оптимизации компиляторов см. в нашем уведомлении об оптимизации.