netlib.narod.ru< Назад | Оглавление | Далее >

Займемся фальсификацией!

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

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

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

Второй компонент — размытие находящихся вне фокуса областей. Размытие рассматривалось в главе 5, «Смотрим через фильтр», и в этой главе мы воспользуемся тем, что изучили раньше.

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

Сперва мы более подробно рассмотрим размывание изображения, которое является общим для всех техник.

ПРИМЕЧАНИЕ
Имейте в виду, что существует гораздо больше способов реализации DOF, чем представлено в этой главе. Я рассмотрю только самый общий подход, который хорошо работает на современном оборудовании, и предоставлю несколько идей, которые стоит учесть с расчетом на будущие версии видеокарт.

Размытые объекты — дайте два

Поскольку эффект глубины резкости требует комбинирования вашей цели визуализации с размытой версией того же изображения, следует снова взглянуть на обсуждавшиеся в главе 5 техники размытия: фильтр коробки и фильтр Гаусса.

Заметьте, что на рис. 6.1 объекты, расположенные близко к камере сильно размыты. Фильтр коробки и фильтр Гаусса не обеспечивают такой степени размытия. Здесь вы узнаете, как усовершенствовать ваши фильтры, чтобы они лучше соответствовали потребностям эффекта DOF.

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

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

Поскольку фильтр коробки слишком прост, а фильтр Гаусса требует двух проходов, мы попробуем новое ядро фильтра размытия, которое действует лучше, чем фильтр коробки, но остается достаточно простым, чтобы оно выполнялось за один проход и могло быть повторено несколько раз.

Новый фильтр по существу является простым ядром фильтра с 9-точечной выборкой. Этот фильтр проще, чем обычный фильтр Гаусса и может быть выполнен за один проход. Как видно на рис. 6.5, по существу этот фильтр является упрощенной формой фильтра Гаусса для квадрата 3 × 3.


Рис. 6.5. Ядро фильтра размытия с 9 выборками

Рис. 6.5. Ядро фильтра размытия с 9 выборками


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

float fInverseViewportWidth;
float fInverseViewportHeight;
sampler Texture0;
const float4 samples[9] = {
    -1.0, -1.0, 0, 1.0/16.0,
    -1.0,  1.0, 0, 1.0/16.0,
     1.0, -1.0, 0, 1.0/16.0,
     1.0,  1.0, 0, 1.0/16.0,
    -1.0,  0.0, 0, 2.0/16.0,
     1.0,  0.0, 0, 2.0/16.0,
     0.0, -1.0, 0, 2.0/16.0,
     0.0,  1.0, 0, 2.0/16.0,
     0.0,  0.0, 0, 4.0/16.0
};

float4 ps_main(float2 texCoord: TEXCOORD0) : COLOR
{
    float4 col = float4(0,0,0,0);

    // Выборка и вычисление среднего цвета
    for(int i = 0; i < 9; i++)
        col += samples[i].w * tex2D(Texture0, texCoord +
                  float2(samples[i].x * fInverseViewportWidth,
                         samples[i].y * fInverseViewportHeight));

    return col;
}

Этот шейдер находится в файле shader_1.rfx на CD-ROM в папке с исходными кодами к данной главе.

Надо еще определить, какое размытие изображения будет достаточным. Для этого я разработал многопроходную версию данного шейдера, в которой вы можете контролировать количество итераций, включая и отключая проходы визуализации. В разработанном шейдере скомбинированы три прохода обычного размывания и заключительный проход размывания с фильтром Гаусса. Для вашего удобства и экспериментов я поместил этот шейдер в файл shader_2.rfx на CD-ROM.

Результаты экспериментов показаны на рис. 6.6. Если вы сравните рис. 6.6 с рис. 6.1, то заметите, что три итерации и фильтр Гаусса позволяют достичь аналогичной степени размытия, и их должно быть достаточно для эффекта глубины резкости.


Рис. 6.6. Результаты для разного количества итераций размытия

Рис. 6.6. Результаты для разного количества итераций размытия


Мы рассмотрели один из двух аспектов эффекта глубины резкости. В оставшейся части этой главы в качестве базовой реализации я буду использовать три итерации и фильтр Гаусса. Для вашего удобства я разработал шейдер, который содержит шаблон сцены, которую я буду использовать с фильтром размывания изображения. Этот шейдер находится в файле shader_3.rfx на CD-ROM.

ПРИМЕЧАНИЕ
Сейчас вы уже должны были заметить использование в предыдущем шейдере второй текстуры цели визуализации с именем Blur2. Причина появления второй текстуры заключается в том, что если вы хотите выполнить проход через несколько фильтров, то вам придется выполнять визуализацию в ту же текстуру, которая использовалась в качестве источника, что приведет к затруднениям при визуализации. Способ избежать этого — чередование двух или более визуализируемых текстур.

Имитаторы глубины

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

В этом разделе вы познакомитесь с техникой, называемой имитаторы глубины (depth impostors), которая позволяет реализовать DOF без необходимости вручную учитывать глубину каждого пикселя. Подход прост; вместо определения коэффициента размытия для глубины каждого пикселя, вы можете визуализировать полноэкранный полигон с вашей сценой на глубине, соответствующей ближней и дальней плоскостям области фокусировки. Полигон служит как имитатор (impostor), отображая размытый вариант сцены на заданной глубине. Подход тот же самый, что и при копировании визуализируемой текстуры на экран, за исключением того, что у геометрии есть набор значений глубины для взаимодействия с существующей сценой. Весь процесс показан на рис. 6.7.


Рис. 6.7. Процесс, используемый в технике имитаторов глубины

Рис. 6.7. Процесс, используемый в технике имитаторов глубины


Начнем с шаблона из файла shader_3.rfx, к которому нам потребуется добавить несколько переменных. Сначала к узлу эффекта добавляются две скалярных переменных с именами Near_Dist и Far_Dist. Они используются для хранения значений глубины с которых начинаются ближняя и дальняя области расфокусировки. Для началя присвойте Near_Dist значение 0.4, а Far_Dist — 0.75.

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

ПРИМЕЧАНИЕ
Размещение в сцене вручную управляемых камер позволяет контролировать их параметры, такие как поле зрения и дальняя и ближняя плоскости отсечения. Кроме того, камеры позволяют отображать сцену с разных точек зрения. Помните, что когда вы не создаете управляемую вручную камеру, для вас создается камера по умолчанию, которая не отображается в рабочем пространстве.
Узлы камеры будут подробно рассмотрены в этой книге чуть позже.

При визуализации сцены каждому проходу необходимо знать, какую камеру он должен использовать. В наших предыдущих шейдерах была только одна камера — камера по умолчанию. Сейчас, поскольку вы создали управляемую вручную камеру, для каждого прохода, которому требуется матрица проекции, необходимо создать узел Camera Reference, сообщающий, какая камера используется. В вашем шейдере необходимо добавить узел ссылки на камеру ко всем проходам визуализации объектов. Как и для других узлов ссылок, вы также должны выбрать используемую камеру через вызываемое правой кнопкой мыши контекстное меню.

Прежде чем вы сможете начать визуализацию имитаторов, необходимо внести небольшие изменения в узлы проходов для обеих моделей. В данном шейдере вместо Z-буферизации я использую W-буферизацию. Причины этого я объясню в следующем разделе, а сейчас просто добавьте к проходам визуализации обеих моделей узлы Render State и измените значение режима ZENABLE на USEW.

Теперь у вас есть шейдер, который визуализирует две ваших модели в цель визуализации с именем RenderTarget, размывает эту цель визуализации, используя текстуры с именами Blur1 и Blur2, и показывает RenderTarget пользователю. Этого достаточно для создания имитаторов. Техника, используемая при визуализации имитаторов, уже применялась нами в прошлом, когда мы копировали цель визуализации. Код вершинного шейдера, обрабатывающий местоположение вершины, выглядел так:

Out.Pos = float4(Pos.xy, 0, 1);

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

// Устанавливаем позицию имитатора, чтобы он занимал весь экран.
// Компоненте Z присваивается либо значение Far_Dist либо Near_Dist,
// а компонента W равна 1, чтобы не было перспективных искажений
// геометрии.
Out.Pos = float4(Pos.xy, Far_Dist, 1);

// Или для ближней плоскости
Out.Pos = float4(Pos.xy, Near_Dist, 1);

Визуализация имитатора глубины — это просто повторное использование вашего обычного прохода копирования цели визуализации и модификация кода его вершинного шейдера для установки требуемой глубины вместо принудительного расположения объекта прямо перед камерой. С помощью команд Copy и Paste из вызываемого правой кнопкой мыши контекстного меню скопируйте два прохода из процесса размывания и переименуйте их в Impostor_Near и Impostor_Far. Эти проходы просто применяют полноэкранную визуализацию к текстуре цели визуализации, когда получен окончательный результат размывания, а это почти то, что необходимо для визуализации имитатора глубины. Вам также необходимо переместить эти проходы, чтобы они располагались между последним проходом размытия и проходом отображения итогового результата. Это можно сделать через контекстное меню или просто перетащив узлы мышью.

Начнем с рассмотрения прохода Impostos_Far. Первое, что вам необходимо, — добавить узел Render State и изменить параметры Z-буферизации. Поскольку вы хотите, чтобы дальний имитатор визуализировал только то, что находится позади него, он должен привлекаться если его глубина меньше, чем значение в Z-буфере. Теперь вы должны разрешить Z-буферизацию, присвоив режиму ZENABLE значение USEW, а ZFUNC — LESSEQUAL. Вы также должны гарантировать, что имитатор не будет изменять буфер глубины, присвоив режиму визуализации ZWRITEENABLE значение FALSE; это не позволит имитатору перезаписывать значения глубины вашей сцены.

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

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

float4x4 matViewProjection: register(c0);
float Near_Dist;
float fInverseViewportWidth;
float fInverseViewportHeight;
struct VS_OUTPUT
{
    float4 Pos: POSITION;
    float2 texCoord: TEXCOORD0;
};

VS_OUTPUT vs_main(float4 Pos: POSITION)
{
    VS_OUTPUT Out;

    // Возвращаем местоположение без преобразования
    Out.Pos = float4(Pos.xy, Far_Dist, 1);

    // Устанавливаем координаты текстуры таким образом, чтобы
    // вся текстура полностью отображалась на экран
    Out.texCoord.x = 0.5 * (1 + Pos.x + fInverseViewportWidth);
    Out.texCoord.y = 0.5 * (1 - Pos.y + fInverseViewportHeight);

    return Out;
}

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

float fInverseViewportWidth;
float fInverseViewportHeight;
sampler Texture0;

float4 ps_main(float2 texCoord: TEXCOORD0) : COLOR
{
    return tex2D(Texture0, texCoord);
}

После установки прохода Impostor_Far, создание прохода Impostor_Near заключается в выполнении тех же самых шагов, которые я только что описал, но в вершинном шейдере Far_Dist заменяется на Near_Dist. Кроме того, необходимо внести изменения в узел Render State, установив для режима ZFUNC значение GREATOREQUAL, чтобы размытыми были те части изображения, которые находятся перед имитатором.

На рис. 6.8 показано итоговое рабочее пространство и окно предварительного просмотра для данного эффекта. Поиграйтесь с камерой и вы увидите как объекты фокусируются и расфокусируются при изменении их глубины.


Рис. 6.8. Итоговое рабочее пространство и результат визуализации для техники имитаторов глубины

Рис. 6.8. Итоговое рабочее пространство и результат визуализации для техники имитаторов глубины. Обратите внимание на резкие переходы между сфокусированными и расфокусированными областями


Шейдер находится в файле shader_4.rfx на CD-ROM в папке с исходными кодами для данной главы.

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

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

Замечание о Z-буфере

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

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

В случае вашего шейдера с имитаторами глубины это вызывает трудности при определении значений Near_Dist и Far_Dist для конкретной сцены без дополнительных вычислений. С другой стороны, в схеме W-буферизации используется значение w, получаемое из матрицы проекции и являющееся линейным, что упрощает определение верных значений глубины. С W-буфером для определения правильной глубины просто используется следующая формула:


W-глубина = глубина/Z задней плоскости отсечения


Не все видеокарты поддерживают W-буферизацию; если это произошло, вы получите сообщение об ошибке и придется вернуться к Z-буферизации.

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

VS_OUTPUT vs_main(float4 Pos: POSITION)
{
    VS_OUTPUT Out;

    // Вычисляем правильную глубину Z-буфера проецируя
    // местоположение имитатора в трехмерном пространстве
    // и извлекая полученное значение Z
    Out.Pos = float4(Pos.xy, 0, 1);
    Out.Pos.z = mul(matViewProjection,
             vViewPosition + Near_Dist * vViewDirection).z;

    // Устанавливаем координаты текстуры таким образом, чтобы
    // вся текстура полностью отображалась на экран
    Out.texCoord.x = 0.5 * (1 + Pos.x + fInverseViewportWidth);
    Out.texCoord.y = 0.5 * (1 - Pos.y + fInverseViewportHeight);

    return Out;
}

Использование альфа-канала

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

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

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

Если мы решаем, что ближнее и дальнее фокусные расстояния определяют точки в которых сцена полностью расфокусирована, то можно определить небольшие области перед этими плоскостями в которых степень размывания изображения постепенно увеличивается. Этот процесс поясняется на рис. 6.9. В вашем шейдере такие области, или диапазоны, будут храниться в переменных с именами Near_Range и Far_Range.


Рис. 6.9. Определение областей постепенной расфокусировки

Рис. 6.9. Определение областей постепенной расфокусировки


Приведенный ниже код оценивает коэффициент размывания для эффекта глубины резкости на основании значений Near_Dist, Near_Range, Far_Dist и Far_Range.

float Blur = max(clamp(0, 1, 1 - (Depth - Near_Dist) / Near_Range),
                 clamp(0, 1, (Depth - (Far_Dist - Far_Range)) / Far_Range));

Функция clamp в приведенном выше коде является встроенной функцией HLSL, получает три параметра и обеспечивает попадание третьего параметра в диапазон, определенный первыми двумя параметрами.

Вооружившись этой информацией можно приступать к разработке новой техники. Сперва необходимо внести несколько изменений в вершинные шейдеры, а именно в проходы, визуализирующие геометрию сцены; другими словами, в проходы Teapot и Elephant. Первая вещь, которую необходимо сделать, — это вычислить глубину для каждой вершины и передать ее в пиксельный шейдер. На самом деле, эта информация всегда вычисляется для каждой вершины во время процесса проецирования и сохраняется как Out.Pos.z и Out.Pos.w. Взяв значение w, полученное при проецировании местоположения вершины, и разделив его на расстояние до дальней плоскости отсечения, вы получите значение глубины, находящееся в диапазоне от нуля до единицы. Это означает, что нам понадобится переменная far_clip, в которой будет храниться расстояние до дальней плоскости отсечения вашей камеры. Чтобы сделать все вышеперечисленное, вам потребуется добавить к вершинному шейдеру выходную переменную и передавать значение глубины в пиксельный шейдер через нее. Применив эти модификации мы получим следующий код вершинного шейдера:

float4x4 matViewProjection: register(c0);
float far_clip;
struct VS_OUTPUT
{
    float4 Pos: POSITION;
    float2 Txr1: TEXCOORD0;
    float1 Depth: TEXCOORD1;
};
VS_OUTPUT vs_main(float4 inPos: POSITION,
                  float2 Txr1: TEXCOORD0)
{
    VS_OUTPUT Out;
    float4 OutPos;

    // Вычисляем местоположение вершины
    Out.Pos = OutPos = mul(view_proj_matrix, inPos);
    Out.Txr1 = Txr1;

    // Отправляем глубину в пиксельный шейдер для кодирования
    Out.Depth = OutPos.w/far_clip;

    return Out;
}

Изменив вершинный шейдер, вы должны сделать соответствующие изменения и в вашем пиксельном шейдере. Вам необходимо добавить к пиксельному шейдеру дополнительный входной параметр, и использовать описанную ранее формулу для преобразования глубины в коэффициент размытия. Не забудьте добавить необходимые для этого переменные Near_Dist, Near_Range, Far_Dist и Far_Range к узлу эффекта и в код пиксельного шейдера.

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

float Near_Range;
float Far_Range;
float Near_Dist;
float Far_Dist;
sampler Texture0;
float4 ps_main(float4 inDiffuse: COLOR0,
               float2 inTxr1: TEXCOORD0,
               float1 Depth: TEXCOORD1) : COLOR0
{
    float Blur = max(clamp(0,1, 1 - (Depth-Near_Dist)/Near_Range),
    clamp(0,1, (Depth-(Far_Dist-Far_Range))/Far_Range));

    // Возвращаем цвет пикселя вместе с коэффициентом размытия.
    // Коэффициент размытия хранится в альфа-канале выходного цвета,
    // чтобы его можно было использовать в заключительном проходе DOF
    return float4(tex2D(Texture0,inTxr1).rgb,Blur);
}

 

ПРИМЕЧАНИЕ
Главным недостатком использования альфа-канала является то, что из-за этого вы лишаетесь прозрачных объектов. Это вызвано тем, что теперь альфа-значения используются для представления размытия, а не прозрачности. Это означает, что вы не сможете корректно выполнять смешивание, поскольку на него будет влиять глубина резкости.

Теперь альфа-канал вашей цели визуализации содержит коэффициенты размывания, которые сообщают насколько надо размывать каждый пиксель. На рис. 6.10 показано содержимое альфа-канала после вычисления коэффициентов размытия.


Рис. 6.10. Коэффициенты размытия, сгенерированные для вашей сцены в альфа-канале цели визуализации

Рис. 6.10. Коэффициенты размытия, сгенерированные для вашей сцены в альфа-канале цели визуализации


Теперь вам необходимо адаптировать проход смешивания, чтобы он брал в качестве текстур размытую и оригинальную цели визуализации, Это легко реализуется с помощью функции HLSL lerp. Вы добавляете обе текстуры к вашему проходу визуализации и изменяете пиксельный шейдер, чтобы он комбинировал их. Это приводит к следующему коду шейдера:

sampler Texture0;
sampler Texture1;

float4 ps_main(float2 texCoord: TEXCOORD0) : COLOR
{
    // Выборка из обычной и размытой сцены
    float4 BlurColor = tex2D(Texture1,texCoord);
    float4 SceneColor = tex2D(Texture0,texCoord);

    // Интерполяция между двумя текстурами на основе
    // коэффициента размытия DOF, хранящегося в SceneColor.a
    return lerp(SceneColor,BlurColor,SceneColor.a);
}

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


Рис. 6.11. Итоговое рабочее пространство и результат визуализации для шейдера, использующего альфа-канал для эффекта глубины резкости

Рис. 6.11. Итоговое рабочее пространство и результат визуализации для шейдера, использующего альфа-канал для эффекта глубины резкости


Завершенный шейдер для данного раздела находится на CD-ROM в файле shader_5.rfx. Во втором упражнении в конце данной главы я предложу вам упростить этот шейдер путем использования текстуры преобразования, содержащей взаимоотношения между глубиной и коэффициентом размытия.

Замечание о нескольких целях визуализации

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

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

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

Сделаем это дважды

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

Другой подход, работающий похожим образом, заключается в визуализации вашей сцены, или, по крайней мере, тех объектов, к которым вы хотите применить эффект, дважды. Сперва вы заполняете Z-буфер и цель визуализации, а во второй раз заполняете другую цель визуализации информацией о глубине, закодированной в RGB-компонентах цели визуализации. Потом вы можете декодировать эту информацию о глубине в вашем проходе DOF, и использовать ее для вычисления коэффициента размытия для каждого пикселя.

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

Кодируем глубину в радугу цветов

Несколькими абзацами выше я упоминал о кодировании информации о глубине в виде цветов цели визуализации. Вашей первой реакцией будет вопрос: «Хорошо, глубина — это значение в диапазоне от нуля до единицы, и это же верно для цветового компонента. Почему я не могу поместить глубину в одну цветовую компоненту?»

И можно и нельзя. Вспомните, что чаще всего используется Z-буфер с 24-разрядной точностью, а в цветовой компоненте только 8 бит. Вы можете спросить о новых 16- и 32-разрядных форматах текстур с плавающей точкой, доступных в новых видеокартах. К сожалению, на момент написания книги их поддержка встречается редко, а производительность ниже всякой критики. На современном оборудовании вы не можете планировать их использование в любых приложениях, работающих в реальном времени, но эту возможность следует держать в уме, поскольку ее поддержка и производительность улучшаются.

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

Например, возьмем полное значение глубины, находящееся в диапазоне от нуля до единицы, и просто поместим его в красную компоненту текстуры. Поскольку текстура обеспечивает только восьмиразрядную точность, аппаратура позаботится об округлении для удаления излишней точности, что эквивалентно выполнению Color.Red = round(Depth * 256). Как видите, аппаратура заботится об автоматическом получении восьми старших разрядов из вашего значения глубины. Чтобы получить следующие 8 бит значения вы просто должны удалить 8 старших разрядов и повторить тот же процесс. Это эквивалентно коду Color.Green = round((Depth * 256 - Color.Red) * 256).

Весь процесс, записанный на HLSL, приводит к следующему коду:

float4 Depth;

Depth.w = 1.0;
Depth.x = floor(inDepth.x * 127) / 127;
Depth.y = floor((inDepth.x - Depth.x) * 127 * 127) / 127;
Depth.z = 0;

В данном примере кода inDepth — это глубина пикселя в диапазоне от нуля до единицы. Также обратите внимания, что в качестве множителя я использую число 127, и занимаю только две цветовых компоненты. Использование числа 127 вместо 255 позволяет вам принять меры по защите от переполнения, а также оставляет возможность выполнения других операций, таких как сложение глубин, чем я воспользуюсь в этой книге позже. Я также ограничил использование цветовых компонент только красной и зеленой, что дает 14 разрядов точности, что достаточно для эффекта DOF и уменьшает объем вычислений. Обратный процесс преобразования закодированной глубины в число с плавающей точкой выполняется так:

float Depth = DepthValue.r + DepthValue.g / 127 +
                             DepthValue.b / (127 * 127);

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


Рис. 6.12. Визуализация тестовой сцены, использующей технику кодирования глубины

Рис. 6.12. Визуализация тестовой сцены, использующей технику кодирования глубины


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

Создание двухпроходного шейдера DOF

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

Для первой фазы нам требуется новая цель визуализации в которой будет сохраняться информация о глубине. Щелкните правой кнопкой мыши по узлу эффекта, выберите в контекстном меню команду Add Render Target и переименуйте созданную цель визуализации в Depth. Вам также нужно создать копии узлов проходов визуализации Teapot и Elephant. Для этого воспользуйтесь командами копирования и вставки из контекстного меню. Вам также следует с помощью мыши или контекстного меню переместить эти узлы проходов визуализации, чтобы они располагались сразу же за своими оригиналами.

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

float4x4 matViewProjection;
float far_clip;
struct VS_OUTPUT
{
    float4 Pos:   POSITION;
    float1 Depth: TEXCOORD0;
};

VS_OUTPUT vs_main(float4 inPos: POSITION,
                  float2 Txr1: TEXCOORD0)
{
    VS_OUTPUT Out;
    float4 OutPos;

    // Вычисление местоположения вершины
    Out.Pos = OutPos = mul(matViewProjection, inPos);

    // Передаем глубину в пиксельный шейдер для кодирования
    Out.Depth = OutPos.w/far_clip;

    return Out;
}

Пиксельный шейдер будет выглядеть так:

float4 ps_main(float4 inDepth: TEXCOORD0) : COLOR0
{
    // Возвращаем глубину, вычисленную на основании
    // полученных от вершинного шейдера значений
    float4 Depth;

    Depth.w = 1.0;
    Depth.x = floor(inDepth.x * 127) / 127;
    Depth.y = floor((inDepth.x - Depth.x) * 127 * 127) / 127;
    Depth.z = 0;

    return Depth;
}

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

Out.Depth = OutPos.w/far_clip;

Причина, по которой я делю значение w на far_clip в том, что значение глубины, вычисленное в процессе проецирования, является реальной глубиной сцены. Поскольку при кодировании вы ожидаете значение из диапазона от нуля до единицы, необходимо масштабировать значение глубины, используя глубину дальней плоскости отсечения, поскольку вы знаете, что глубина объектов сцены будет находиться где-нибудь в диапазоне от нуля до far_clip. Обратите внимание, что вам необходимо создать эту переменную и гарантировать, что ее значение будет соответствовать глубине дальней плоскости отсечения используемой камеры.

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

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

float Near_Dist;
float Far_Dist;
float Near_Range;
float Far_Range;
sampler Texture0;
sampler Texture1;
sampler Texture2;

float4 ps_main(float2 texCoord: TEXCOORD0) : COLOR
{
    // Получаем и декодируем значение глубины
    float4 DepthValue = tex2D(Texture2, texCoord);
    float Depth = DepthValue.r + DepthValue.g / 127
                  + DepthValue.b / (127 * 127);

    // Делаем выборку для обычной и размытой сцены
    float4 BlurColor  = tex2D(Texture1, texCoord);
    float4 SceneColor = tex2D(Texture0, texCoord);

    // Используем заданные диапазоны для вычисления
    // требуемой комбинации обоих целей визуализации
    // на основании расстояния
    float Blur = max(clamp(0, 1, 1 - (Depth - Near_Dist) / Near_Range),
                     clamp(0, 1, (Depth - (Far_Dist - Far_Range)) / Far_Range));

    return lerp(SceneColor, BlurColor, clamp(0, 1, Blur));
}

Закройте окно редактирования, скомпилируйте шейдер и вы получите вашу двухпроходную реализацию эффекта глубины резкости. На рис. 6.13 показано рабочее пространство и окно предварительного просмотра для данного шейдера. Полный код шейдера находится на CD-ROM в файле shader_6.rfx.


Рис. 6.13. Итоговое рабочее пространство и результат визуализации для двухпроходной техники DOF

Рис. 6.13. Итоговое рабочее пространство и результат визуализации для двухпроходной техники DOF


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

А что насчет Z-буфера?

Читая эту главу вы, возможно, удивлялись, зачем я так мучаюсь, пытаясь определить глубину объектов сцены. Вы можете спросить «Хорошо, мы визуализируем нашу сцену и значения глубины помещаются в Z-буфер, так почему бы не использовать их?» Это правильный вопрос и я должен показать, почему я так не поступаю.

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

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

Тем временем, если только вы не занимаетесь разработкой для Microsoft Xbox, то будете вынуждены придерживаться подходов, рассмотренных в данной главе.

Особые замечания

Последняя заслуживающая рассмотрения вещь, о которой я еще не сказал, — способ, которым DOF определяет переходы между сфокусированными и расфокусированными областями. Если вы еще раз внимательно посмотрите на рис. 6.1, то заметите, что нерезкость краев находящихся вне фокуса объектов распространяется и на находящиеся в фокусе предметы.

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

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

Если вы желаете воссоздать такое размытие границ, этого можно достигнуть используя попиксельные техники DOF. Хотя я и не буду разрабатывать такой шейдер в этой главе, думаю вам все же будет интересно узнать, как это можно сделать.

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


Рис. 6.14. Учитываем размытие границ, рассматривая глубину соседних пикселей

Рис. 6.14. Учитываем размытие границ, рассматривая глубину соседних пикселей


Ясно, что стоимость и сложность этого подхода значительно больше, да и его использование не слишком нужно, поскольку не дает явно видимого улучшения результата визуализации. Однако, некоторые приложения могут извлечь пользу из такого увеличения реализма.


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz