Введение в программирование трехмерных игр с DX9

         

Объекты с затенением выполненным


Рисунок 17.2. (а) Объекты с затенением выполненным по мультипликационной технологии (обратите внимание на резкие переходы между оттенками) (б) Усиление эффекта достигается путем обведения силуэта объекта (в) Объекты, затеняемые с использованием стандартного рассеянного освещения


ПРИМЕЧАНИЕ

Мультипликационная визуализация (сartoon rendering) — это один из видов нефотореалистичной визуализации (non-photorealistic rendering) иногда называемой стилистической визуализацией (stylistic rendering).

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

Мы разделим мультипликационную визуализацию на два этапа.

Мультипликационные рисунки обычно имеют несколько уровней интенсивности затенения с резкими переходами от одного уровня к другому; мы будем ссылаться на такой способ как на мультипликационное затенение (cartoon shading). На Рисунок  17.2(а) видно, что для затенения сеток используются всего три уровня интенсивности (яркий, средний и темный) и переходы между оттенками явно выражены в отличие от Рисунок  17.2(в), где показан плавный переход от темного оттенка к светлому.

Также в мультфильмах обычно обводится силуэт объектов, как показано на Рисунок  17.2(б).

Оба этапа требуют собственных вершинных шейдеров.



Генерация краев


17.5.3.3. Генерация краев

Генерация краев сетки очень проста; мы перебираем грани сетки и для каждой стороны грани формируем квадрат (вырожденный, как показано на Рисунок  17.6).

ПРИМЕЧАНИЕ

У каждой грани три стороны, так что для каждого треугольника формируется три края.

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



Знакомство с вершинными шейдерами


Вершинный шейдер (vertex shader)— это программа, выполняемая процессором видеокарты и заменяющая этапы преобразования и освещения в фиксированном конвейере. (Это описание не является абсолютно верным, поскольку если вершинные шейдеры не поддерживаются аппаратно, они могут эмулироваться программно библиотекой времени выполнения Direct3D.) На Рисунок  17.1 изображена та часть конвейера визуализации, которую заменяют вершинные шейдеры.



Использование данных вершин

Рассмотрим следующее объявление вершины:

D3DVERTEXELEMENT9 decl[] = { {0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0}, {0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0}, {0, 24, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 1}, {0, 36, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 2}, D3DDECL_END() };



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

struct VS_INPUT { vector position : POSITION; vector normal : NORMAL0; vector faceNormal1 : NORMAL1; vector faceNormal2 : NORMAL2; };

ПРИМЕЧАНИЕ

Если мы не указываем индекс использования, подразумевается, что он равен 0. Таким образом запись POSITION означает то же самое, что и POSITION0.

Здесь элемент 0 в decl, идентифицируемый по типу использования POSITION и индексу использования 0, отображается на элемент входной структуры position. Элемент 1 в decl, идентифицируемый по типу использования NORMAL и индексу использования 0, отображается на элемент входной структуры normal. Элемент 2 в decl, идентифицируемый по типу использования NORMAL и индексу использования 1, отображается на элемент входной структуры faceNormal1. Элемент 3 в decl, идентифицируемый по типу использования NORMAL и индексу использования 2, отображается на элемент входной структуры faceNormal2.

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

POSITION[n] — Местоположение.

BLENDWEIGHTS[n] — Веса смешивания.

BLENDINDICES[n] — Индексы смешивания.

NORMAL[n] — Вектор нормали.

PSIZE[n] — Размер точки.

DIFFUSE[n] — Рассеиваемый цвет.

SPECULAR[n] — Отражаемый цвет.

TEXCOORD[n] — Координаты текстуры.

TANGENT[n] — Тангенциальный вектор.

BINORMAL[n] — Бинормальный вектор.

TESSFACTOR[n] — Мозаичный коэффициент.

Здесь n — это необязательное целое число в диапазоне [0, 15].

ПРИМЕЧАНИЕ

Еще раз напоминаем, что некоторые типы использования, такие как BLENDWEIGHTS, TANGENT, BINORMAL, BLENDINDICES и TESSFACTOR, в этой книге не рассматриваются. Кроме того, мы должны указать способ использования каждого члена данных и для выходной структуры. Должен ли член данных интерпретироваться как вектор местоположения, или как цвет, или как координаты текстуры? Видеокарта не знает, пока вы явно не укажете ей это. Синтаксис здесь тот же самый, что и во входной структуре:

struct VS_OUTPUT { vector position : POSITION; vector diffuse : COLOR0; vector specular : COLOR1; };

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

POSITION — Местоположение.

PSIZE — Размер точки.

FOG — Значение смешивания тумана.

COLOR[n] — Цвет вершины. Обратите внимание, что можно возвращать несколько значений цветов. Для получения итогового цвета все эти цвета смешиваются вместе.

TEXCOORD[n] — Координтаы текстуры. Обратите внимание, что можно возвращать несколько координат текстуры.

Здесь n — это необязательное целое число в диапазоне [0, 15].


Используемый оттенок зависит от интервала, в который попадает координата текстуры



Рисунок 17.4. Используемый оттенок зависит от интервала, в который попадает координата текстуры

Тогда если значение s находится в диапазоне [0,0.33] для затенения используется оттенок 0, если значение s находится в диапазоне (0.33, 0.66] — используется оттенок 1, и для значений s из диапазона (0.66, 1] используется оттенок 2. Естественно, переходы от одного оттенка к другому будут резкими, что нам и требуется.

ПРИМЕЧАНИЕ

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

Вершинные шейдеры позволяют заменять этапы


Вершинные шейдеры позволяют заменять этапы преобразования и освещения в фиксированном конвейере функций. Заменяя фиксированный процесс на нашу собственную программу (вершинный шейдер) мы получаем огромное количество возможностей для реализации различных графических эффектов.
Объявление вершин используется для описания формата данных наших вершин. Оно похоже на настраиваемый формат вершин (FVF), но является более гибким и позволяет описывать форматы вершин, которые не могут быть представлены посредством FVF. Обратите внимание, что если наша вершина может быть представлена с помощью FVF, мы можем продолжать использовать этот формат; внутри программы он будет автоматически преобразован в объявление вершин.
Для входных данных семантика использования определяет каким образом будут связаны компоненты вершины из объявления вершин и переменные в программе на HLSL. Для выходных данных семантика использования определяет для чего будет использоваться компонент вершины (т.е. для указания местоположения, цвета, координат текстуры и т.д.).

Этапы работы с вершинным шейдером


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

Написать и скомпилировать вершинный шейдер.

Создать представляющий вершинный шейдер интерфейс IDirect3DVertexShader9 на основе скомпилированного кода.

Установить вершинный шейдер с помощью метода IDirect3DDevice9::SetVertexShader.

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



Код вершинного шейдера для мультипликационного затенения



17.5.2. Код вершинного шейдера для мультипликационного затенения

Теперь мы представляем вершинный шейдер для мультипликационного затенения. Главной задачей шейдера является простое вычисление коэффициента s = 



Код вершинного шейдера обводки силуэта



17.5.4. Код вершинного шейдера обводки силуэта

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

// Файл: outline.txt // Описание: Вершинный шейдер, отображающий силуэт

// // Глобальные переменные //

extern matrix WorldViewMatrix; extern matrix ProjMatrix;

static vector Black = {0.0f, 0.0f, 0.0f, 0.0f};

// // Структуры //

struct VS_INPUT { vector position : POSITION; vector normal : NORMAL0; vector faceNormal1 : NORMAL1; vector faceNormal2 : NORMAL2; };

struct VS_OUTPUT { vector position : POSITION; vector diffuse : COLOR; };

// // Точка входа //

VS_OUTPUT Main(VS_INPUT input) { // Обнуляем члены выходной структуры VS_OUTPUT output = (VS_OUTPUT)0;

// Преобразуем местоположение в пространство вида input.position = mul(input.position, WorldViewMatrix);

// Вычисляем вектор направления взгляда на вершину. // Вспомните, что в пространстве вида зритель находится // в начале координат (зритель это то же самое, что и камера). vector eyeToVertex = input.position;

// Преобразуем нормали в пространство вида. Компоненте w // присваиваем нуль, поскольку преобразуем векторы. // Предполагается, что в мировой матрице нет масштабирования input.normal.w = 0.0f; input.faceNormal1.w = 0.0f; input.faceNormal2.w = 0.0f;

input.normal = mul(input.normal, WorldViewMatrix); input.faceNormal1 = mul(input.faceNormal1, WorldViewMatrix); input.faceNormal2 = mul(input.faceNormal2, WorldViewMatrix);

// Вычисляем косинус угла между вектором eyeToVertex // и нормалями граней float dot0 = dot(eyeToVertex, input.faceNormal1); float dot1 = dot(eyeToVertex, input.faceNormal2);

// Если у косинусов разные знаки (один положительный, // а другой отрицательный) значит край является частью // силуэта if((dot0 * dot1) < 0.0f) { // Знаки разные, значит вершина является частью // края силуэта, смещаем позиции вершин на заданный // скаляр в направлении нормали вершины input.position += 0.1f * input.normal; }

// Преобразование в однородное пространство отсечения output.position = mul(input.position, ProjMatrix);

// Устанавливаем цвет силуэта output.diffuse = Black;

return output; }



Край, определенный вершинами vи vиспользуется только одной гранью



Рисунок 17.9. Край, определенный вершинами v0 и v1 используется только одной гранью

Мы считаем, что такие края всегда являются частью силуэта. Чтобы вершинный шейдер обрабатывал такие грани как часть силуэта, мы устанавливаем что faceNormal2= -faceNormal1. таким образом, нормали граней будут направлены в разные стороны и формула (1) будет истинна, указывая, что край является частью силуэта.



Квадрат, представляющий край



Рисунок 17.5. Квадрат, представляющий край

<
Мы выбрали квадрат по двум причинам: во-первых можно легко изменять толщину края просто меняя размеры квадрата, и во-вторых мы можем визуализировать вырожденные квадраты для скрытия отдельных краев, например, тех краев, которые не являются частью силуэта. В Direct3D мы создаем квадрат из двух треугольников. Вырожденный квадрат (degenerate quad) — это квадрат, созданный из двух вырожденных треугольников. Вырожденный треугольник (degenerate triangle) — это треугольник с нулевой площадью или, другими словами, треугольник у которого все три вершины лежат на одной прямой. Если передать вырожденный треугольник в конвейер визуализации, то ничего отображено не будет. Это очень полезно, поскольку если мы хотим скрыть какой-нибудь треугольник, достаточно просто сделать его вырожденным без действительного удаления из списка треугольников (буфера вершин). Вспомните, что нам надо отображать только края силуэта, а не все края сетки.

Когда мы впервые создаем край, то указываем его четыре вершины таким образом, чтобы квадрат был вырожденный (Рисунок  17.6), а это значит, что данный край будет скрыт (не будет отображаться при визуализации).



Обводка силуэта



17.5.3. Обводка силуэта

Чтобы усилить мультипликационный эффект, нам надо обвести силуэты объектов. Сделать это несколько сложнее, чем реализовать мультипликационное затенение.



Описание объявления вершин



17.1.1. Описание объявления вершин

Мы описываем объявление вершин в виде массива структур D3DVERTEXELEMENT9. Каждый элемент массива D3DVERTEXELEMENT9 описывает один компонент данных вершины. Таким образом, если структура данных вершины содержит три компонента (например, местоположение, нормаль и цвет), соответствующее ей объявление вершины будет описано массивом из трех структур D3DVERTEXELEMENT9. Определение структуры D3DVERTEXELEMENT9 выглядит следующим образом:

typedef struct _D3DVERTEXELEMENT9 { BYTE Stream; BYTE Offset; BYTE Type; BYTE Method; BYTE Usage; BYTE UsageIndex; } D3DVERTEXELEMENT9;

Stream— Указывает поток с которым связан данный компонент данных вершины.

Offset — Смещение в байтах от начала структуры данных вершины до начала данных компонента. Например, если структура данных вершины объявлена следующим образом:

struct Vertex { D3DXVECTOR3 pos; D3DXVECTOR3 normal; };

Смещение компонента pos равно 0, поскольку этот компонент первый в структуре. Смещение компонента normal равно 12 потому что sizeof(pos) == 12. Другими словами, компонент normal начинается с 12 байта, считая от начала структуры Vertex.

Type — Указывает тип данных. Здесь можно использовать любой член из перечисления D3DDECLTYPE; чтобы посмотреть полный список типов, обратитесь к документации. Вот наиболее часто используемые типы:

D3DDECLTYPE_FLOAT1 — Скаляр с плавающей точкой.

D3DDECLTYPE_FLOAT2 — Двухмерный вектор с плавающей точкой.

D3DDECLTYPE_FLOAT3 — Трехмерный вектор с плавающей точкой.

D3DDECLTYPE_FLOAT4 — Четырехмерный вектор с плавающей точкой.

D3DDECLTYPE_D3DCOLOR — Значение типа D3DCOLOR, которое расширяется до цветового вектора RGBA с плавающей точкой (r, g, b, a), в котором каждая компонента нормализована в интервале [0, 1].

Method — Задает мозаичный метод триангуляции. Мы считаем, что это достаточно сложная тема и поэтому будем всегда использоватть метод по умолчанию, задаваемый идентификатором D3DDECLMETHOD_DEFAULT.

Usage — Указывает предполагаемый способ использования данного компонента. То есть позволяет определить, чем является данный компонент — вектором местоположения, вектором нормали, координатами текстуры и т.д. В качестве значений используются члены перечисления D3DDECLUSAGE:
typedef enum _D3DDECLUSAGE { D3DDECLUSAGE_POSITION = 0, // Местоположение D3DDECLUSAGE_BLENDWEIGHTS = 1, // Веса смешивания D3DDECLUSAGE_BLENDINDICES = 2, // Индексы смешивания D3DDECLUSAGE_NORMAL = 3, // Вектор нормали D3DDECLUSAGE_PSIZE = 4, // Размер точки D3DDECLUSAGE_TEXCOORD = 5, // Координаты текстуры D3DDECLUSAGE_TANGENT = 6, // Тангенциальный вектор D3DDECLUSAGE_BINORMAL = 7, // Бинормальный вектор D3DDECLUSAGE_TESSFACTOR = 8, // Мозаичный коэффициент D3DDECLUSAGE_POSITIONT = 9, // Преобразованная позиция D3DDECLUSAGE_COLOR = 10, // Цвет D3DDECLUSAGE_FOG = 11, // Значение смешивания тумана D3DDECLUSAGE_DEPTH = 12, // Значение глубины D3DDECLUSAGE_SAMPLE = 13 // Данные выборки } D3DDECLUSAGE;
Тип D3DDECLUSAGE_PSIZE используется для задания размеров точек. Обычно он применяется для точечных спрайтов, чтобы можно было управлять их размером для каждой вершины. Объявление вершины с флагом D3DDECLUSAGE_POSITIONT явно указывает, что эта вершина уже преобразована и, следовательно, процессор видеокарты не должен отправлять ее на этапы обработки вершин (преобразование и освещение).
ПРИМЕЧАНИЕ


UsageIndex — Используется для идентификации нескольких компонентов вершины с одинаковым типом использования. Индекс использования представляет собой целое число в диапазоне [0, 15]. Предположим, у нас есть три компонента вершины с флагом типа использования D3DDECLUSAGE_NORMAL. Тогда для первого из них мы должны указать индекс использования 0, для второго — индекс использования 1, и для третьего — индекс использования 2.Благодаря этому мы сможем идентифицировать отдельную нормаль по ее индексу использования.
Рассмотрим пример описания объявления вершины. Предположим, что описываемый формат содержит вектор местоположения и три вектора нормалей. В этом случае описание объявления вершины будет выглядеть так:
D3DVERTEXELEMENT9 decl[] = { {0, 0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0}, {0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 0}, {0, 24, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 1}, {0, 36, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL, 2}, D3DDECL_END() };
Макрос D3DDECL_END применяется для инициализации последнего элемента в массиве D3DVERTEXELEMENT9. Кроме того, обратите внимание на применение индекса использования для отметки векторов нормалей.

Представление краев



17.5.3.1. Представление краев

Мы представляем край сетки в виде квадрата, образованного из двух треугольников (Рисунок 17.5).




Мультипликационное затенение



17.5.1. Мультипликационное затенение

Для реализации мультипликационного затенения мы воспользуемся методикой, описанной Ландером в статье «Shades of Disney: Opaquing a 3D World», опубликованной в выпуске журнала Game Developer Magazine за март 2000 года. Работает она следующим образом: мы создаем состоящую из оттенков серого текстуру, которая будет управлять яркостью и должна состоять из требуемого нам количества оттенков. На Рисунок 17.3 показана текстура, которая будет использоваться в данном примере.



На Рисунок (a) одна из граней



Рисунок 17.8. На Рисунок (a) одна из граней, совместно использующих край, образованный вершинами v0 и v1 является фронтальной, а другая — обратной, следовательно край является частью силуэта. На Рисунок (б) обе грани, совместно использующие край, образованный вершинами v0 и v1 являются фронтальными и, следовательно, край не является частью силуэта

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

struct VS_INPUT { vector position : POSITION; vector normal : NORMAL0; vector faceNormal1 : NORMAL1; vector faceNormal2 : NORMAL2; };

Первые два компонента мы уже обсуждали, но сейчас настало время взглянуть на два дополнительных вектора нормали — faceNormal1 и faceNormal2. Эти векторы являются нормалями тех двух граней на стыке которых находится рассматриваемый край, а именно face0 и face1.

Математическая часть проверки, является ли вершина частью силуэта, заключается в следующем. Предположим, мы находимся в пространстве вида. Пусть v — это вектор, направленный от начала координат до проверяемой вершины (Рисунок  17.8). Пусть n0 — это нормаль грани face0, а n1 — нормаль грани face1. Тогда вершина является частью силуэта, если следующее сравнение истино:


Написание и компиляция вершинного шейдера



17.3.1. Написание и компиляция вершинного шейдера

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



Объявление вершин

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

ПРИМЕЧАНИЕ

Мы можем продолжать использовать FVF для программируемого конвейера, если предоставляемых им возможностей достаточно для описания структур наших вершин. Но это лишь вопрос удобства, поскольку FVF автоматически преобразуется в объявление вершин. Некоторые из типов использования, такие как BLENDWEIGHTS, BLENDINDICES, TANGENT, BINORMAL и TESSFACTOR, в этой книге не рассматриваются.

bool Setup() { HRESULT hr = 0;
// // Создание геометрии: //
D3DXCreateTeapot(Device, &Teapot, 0);
// // Компиляция шейдера //
ID3DXBuffer* shader = 0; ID3DXBuffer* errorBuffer = 0;
hr = D3DXCompileShaderFromFile( "diffuse.txt", 0, 0, "Main", // имя точки входа "vs_1_1", D3DXSHADER_DEBUG, &shader, &errorBuffer, &DiffuseConstTable);
// Выводим сообщения об ошибках if(errorBuffer) { ::MessageBox(0, (char*)errorBuffer->GetBufferPointer(), 0, 0); d3d::Release<ID3DXBuffer*>(errorBuffer); }
if(FAILED(hr)) { ::MessageBox(0, "D3DXCompileShaderFromFile() - FAILED", 0, 0); return false; }
// // Создаем шейдер //
hr = Device->CreateVertexShader( (DWORD*)shader->GetBufferPointer(), &DiffuseShader);
if(FAILED(hr)) { ::MessageBox(0, "CreateVertexShader - FAILED", 0, 0); return false; }
d3d::Release<ID3DXBuffer*>(shader);
// // Получаем дескрипторы // ViewMatrixHandle = DiffuseConstTable->GetConstantByName( 0, "ViewMatrix"); ViewProjMatrixHandle = DiffuseConstTable->GetConstantByName( 0, "ViewProjMatrix"); AmbientMtrlHandle = DiffuseConstTable->GetConstantByName( 0, "AmbientMtrl"); DiffuseMtrlHandle = DiffuseConstTable->GetConstantByName( 0, "DiffuseMtrl"); LightDirHandle = DiffuseConstTable->GetConstantByName( 0, "LightDirection");


// // Устанавливаем константы шейдера: //
// Направление на источник света: D3DXVECTOR4 directionToLight(-0.57f, 0.57f, -0.57f, 0.0f); DiffuseConstTable->SetVector(Device, LightDirHandle, &directionToLight);
// Материалы: D3DXVECTOR4 ambientMtrl(0.0f, 0.0f, 1.0f, 1.0f); D3DXVECTOR4 diffuseMtrl(0.0f, 0.0f, 1.0f, 1.0f); DiffuseConstTable->SetVector(Device, AmbientMtrlHandle, &ambientMtrl); DiffuseConstTable->SetVector(Device, DiffuseMtrlHandle, &diffuseMtrl); DiffuseConstTable->SetDefaults(Device);
// Вычисляем матрицу проекции D3DXMatrixPerspectiveFovLH( &Proj, D3DX_PI * 0.25f, (float)Width / (float)Height, 1.0f, 1000.0f);
return true; }
Функция Display достаточно простая. Она проверяет какие клавиши нажал пользователь и соотвествующим образом изменяет матрицу вида. Однако, поскольку преобразование вида мы выполняем в шейдере, нам необходимо обновить значение переменной шейдера, которая хранит матрицу вида. Мы делаем это через таблицу констант:
bool Display(float timeDelta) { if(Device) { // // Код обновления матрицы вида пропущен... //
D3DXMATRIX V; D3DXMatrixLookAtLH(&V, &position, &target, &up);
DiffuseConstTable->SetMatrix(Device, ViewMatrixHandle, &V);
D3DXMATRIX ViewProj = V * Proj; DiffuseConstTable->SetMatrix(Device, ViewProjMatrixHandle, &ViewProj);
// // Визуализация //
Device->Clear(0, 0, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, 0xffffffff, 1.0f, 0); Device->BeginScene();
Device->SetVertexShader(DiffuseShader);
Teapot->DrawSubset(0);
Device->EndScene(); Device->Present(0, 0, 0, 0); } return true; }
Обратите внимание, что мы включаем вершинный шейдер, который хотим использовать, перед вызовом DrawSubset.
Очистка выполняется как обычно; мы просто освобождаем все запрошенные интерфейсы:
void Cleanup() { d3d::Release<ID3DXMesh*>(Teapot); d3d::Release<IDirect3DVertexShader9*>(DiffuseShader); d3d::Release<ID3DXConstantTable*>(DiffuseConstTable); }

Проверка для краев силуэта



17.5.3.2. Проверка для краев силуэта

Край является частью силуэта если он находится на стыке двух граней face0 и face1, и эти грани ориентированы в различных направлениях относительно вектора взгляда. То есть, если одна из граней является фронтальной, а другая— обратной, то край между ними является частью силуэта. На Рисунок  17.8 приведены примеры краев являющихся и не являющихся частью силуэта.



Пример приложения: мультипликационная визуализация


В качестве второго примера давайте напишем два вершинных шейдера, которые будут выполнять затенение и обвод контуров рисунка в мультипликационном стиле (Рисунок 17.2).



Пример приложения: рассеянный свет


В качестве разминки напишем вершинный шейдер, который будет реализовать для вершин обычное рассеянное освещение для направленного (параллельного) источника света. Напомним, что для рассеянного света количество получаемого вершиной света вычисляется на основании угла между нормалью вершины и вектором света (который указывает в направлении на источник света). Чем меньше угол, тем больше света получает вершина, и чем больше угол, тем меньше света получает вершина. Если угол больше или равен 90 градусам, вершина вообще не освещена. Подробное описание алгоритма рассеянного освещения приводилось в разделе 13.4.1.

Начнем с исследования кода вершинного шейдера.

// Файл: diffuse.txt // Описание: Вершинный шейдер, реализующий рассеянное освещение.

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

matrix ViewMatrix; matrix ViewProjMatrix;

vector AmbientMtrl; vector DiffuseMtrl;

vector LightDirection;

// // Глобальные переменные, используемые для хранения // интенсивности фонового света (фоновая составляющая // испускаемого источником света) и интенсивности рассеиваемого // света (рассеиваемая составляющая испускаемого источником света). // Эти переменные инициализируются в шейдере. //

vector DiffuseLightIntensity = {0.0f, 0.0f, 1.0f, 1.0f}; vector AmbientLightIntensity = {0.0f, 0.0f, 0.2f, 1.0f};

// // Входная и выходная структуры //

struct VS_INPUT { vector position : POSITION; vector normal : NORMAL; }; struct VS_OUTPUT { vector position : POSITION; vector diffuse : COLOR; };

// // Точка входа //

VS_OUTPUT Main(VS_INPUT input) { // Обнуляем все члены экземпляра выходной структуры VS_OUTPUT output = (VS_OUTPUT)0;

// // Преобразуем местоположение вершины в однородное пространство // отсечения и сохраняем его в члене output.position // output.position = mul(input.position, ViewProjMatrix);


// // Преобразуем вектор освещения и нормаль в пространство вида. // Присваиваем компоненте w значение 0, поскольку мы преобразуем // векторы, а не точки. // LightDirection.w = 0.0f; input.normal.w = 0.0f; LightDirection = mul(LightDirection, ViewMatrix); input.normal = mul(input.normal, ViewMatrix);
// // Вычисляем косинус угла между вектором света и нормалью // float s = dot(LightDirection, input.normal);
// // Вспомните, что если угол между нормалью поверхности // и вектором освещения больше 90 градусов, поверхность // не получает света. Следовательно, если угол больше // 90 градусов, мы присваиваем s ноль, сообщая тем самым, // что поверхность не освещена. // if(s < 0.0f) s = 0.0f;
// // Отраженный фоновый свет вычисляется путем покомпонентного // умножения вектора фоновой составляющей материала и вектора // интенсивности фонового света. // // Отраженный рассеиваемый свет вычисляется путем покомпонентного // умножения вектора рассеиваемой составляющей материала на вектор // интенсивности рассеиваемого света. Затем мы масштабируем полученный // вектор, умножая каждую его компоненту на коэффициент затенения s, // чтобы затемнить цвет в зависимости от того, сколько света получает // вершина от источника. // // Сумма фоновой и рассеиваемой компонент дает нам // итоговый цвет вершины. // output.diffuse = (AmbientMtrl * AmbientLightIntensity) + (s * (DiffuseLightIntensity * DiffuseMtrl));
return output; }
Теперь, когда мы посмотрели на код вершинного шейдера, давайте переключим передачу и взглянем на код приложения. В приложении используются следующие, относящиеся к рассматриваемой теме, глобальные переменные:
IDirect3DVertexShader9* DiffuseShader = 0; ID3DXConstantTable* DiffuseConstTable = 0;
ID3DXMesh* Teapot = 0;
D3DXHANDLE ViewMatrixHandle = 0; D3DXHANDLE ViewProjMatrixHandle = 0; D3DXHANDLE AmbientMtrlHandle = 0; D3DXHANDLE DiffuseMtrlHandle = 0; D3DXHANDLE LightDirHandle = 0;
D3DXMATRIX Proj;
У нас есть переменные, представляющие сам вершинный шейдер и его таблицу констант.

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



Текстура затенения содержит используемые



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



Разрешение использования объявлений вершин



17.1.3. Разрешение использования объявлений вершин

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

Device->SetFVF(fvf);

Вместо него мы используем вызов:

Device->SetVertexDeclaration(_decl);

где _decl — это указатель на интерфейс IDirect3DVertexDeclaration9.



Создание объявления вершин



17.1.2. Создание объявления вершин

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

HRESULT IDirect3DDevice9::CreateVertexDeclaration( CONST D3DVERTEXELEMENT9* pVertexElements, IDirect3DVertexDeclaration9** ppDecl );

pVertexElements — Массив структур D3DVERTEXELEMENT9, описывающий объявление вершины, которое мы создаем.

ppDecl — Используется для возврата указателя на созданный интерфейс IDirect3DVertexDeclaration9.

Вот пример вызова, где decl — это массив структур D3DVERTEXELEMENT9:

IDirect3DVertexDeclaration9* _decl = 0; hr = _device->CreateVertexDeclaration(decl, &_decl);



Создание вершинного шейдера


17.3.2. Создание вершинного шейдера

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

HRESULT IDirect3DDevice9::CreateVertexShader( const DWORD *pFunction, IDirect3DVertexShader9** ppShader );

pFunction — Указатель на скомпилированный код шейдера.

ppShader — Возвращает указатель на интерфейс IDirect3DVertexShader9.

Предположим, переменная shader — это указатель на интерфейс ID3DXBuffer, содержащий скомпилированный код шейдера. Тогда для получения указателя на интерфейс IDirect3DVertexShader9 следует написать:

IDirect3DVertexShader9* ToonShader = 0; hr = Device->CreateVertexShader( (DWORD*)shader->GetBufferPointer(), &ToonShader);

ПРИМЕЧАНИЕ

Повторим еще раз, D3DXCompileShaderFromFile — это функция, которая возвращает скомпилированный код шейдера (shader).

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



Уничтожение вершинного шейдера



17.3.4. Уничтожение вершинного шейдера

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

d3d::Release<IDirect3DVertexShader9*>(ToonShader);



Установка вершинного шейдера



17.3.3. Установка вершинного шейдера

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

HRESULT IDirect3DDevice9::SetVertexShader( IDirect3DVertexShader9* pShader );

Метод получает единственный параметр в котором мы передаем указатель на тот вершинный шейдер, который должен быть включен. Чтобы включить шейдер, созданный в разделе17.3.2, следует написать:

Device->SetVertexShader(ToonShader);



Вершинный шейдер заменяет этапы преобразования и освещения в фиксированном конвейере



Рисунок 17.1. Вершинный шейдер заменяет этапы преобразования и освещения в фиксированном конвейере


На Рисунок  17.1 видно, что на вход вершинного шейдера поступает вершина в локальной системе координат и шейдер должен вернуть освещенную (окрашенную) вершину в однородном пространстве отсечения (homogeneous clip space). (В этой книге, чтобы не усложнять материал, мы не будем углубляться в детали преобразования проекции. Отметим, что пространство, в которое матрица проекции преобразует вершины называется однородным пространством отсечения. Следовательно, для того, чтобы преобразовать вершину из локального пространства в однородное пространство отсечения, необходимо выполнить следующую последовательность преобразований: мировое преобразование, преобразование вида и преобразование проекции выполняемые соответственно с помощью мировой матрицы, матрицы вида и матрицы проекции.) Для примитивов точек вершинные шейдеры могут использоваться также для управления размером.

Поскольку вершинные шейдеры — это программы, которые вы пишете сами (на HLSL) открывается огромный круг возможностей, позволяющих реализовать разнообразные графические эффекты. Например, благодаря вершинным шейдерам можно применять любой алгоритм освещения, реализуемый в коде шейдера. Мы больше не ограничены набором фиксированных алгоритмов освещения, реализованных в Direct3D. Более того, возможность управлять местоположением вершин также может применяться во многих задачах, таких как моделирование одежды, управление размером точек для систем частиц, смешивание вершин и морфинг. Помимо этого, в программируемом конвейере структура данных вершин более гибкая и может содержать больше данных, чем в фиксированном конвейере.

Вершинные шейдеры остаются достаточно новой возможностью и многие видеокарты не поддерживают их, особенно новые версии шейдеров, реализованные в DirectX 9. Чтобы проверить, какую версию шейдеров поддерживает установленная видеокарта, проверьте значение члена VertexShaderVersion структуры D3DCAPS9 воспользовавшись макросом D3DVS_VERSION, как показано в приведенном ниже фрагменте кода:

// Если поддерживаемая устройством версия шейдеров меньше 2.0 if(caps.VertexShaderVersion < D3DVS_VERSION(2, 0)) // Значит видеокарта не поддерживает шейдеры версии 2.0

Как видите, в двух параметрах D3DVS_VERSION передаются старший и младший номер версии соответственно. На данный момент функция D3DXCompileShaderFromFile поддерживает вершинные шейдеры версий 1.1 и 2.0.

Цели

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

Узнать о различных способах использования компонентов вершины.

Узнать как создать, установить и уничтожить вершинный шейдер.

Изучить реализацию эффекта мультипликационной визуализации с помощью вершинных шейдеров.



Вершины vи vкрая силуэта смещаются



Рисунок 17.7. Вершины v2 и v3 края силуэта смещаются в направлении их нормалей вершин n2 и n3 соответственно. Обратите внимание, что вершины v0 и v1 остаются на фиксированных позициях и никуда не смещаются, поскольку их векторы нормалей вершин — нулевые векторы. Благодаря этому происходит успешное восстановление квадрата, представляющего край силуэта

ПРИМЕЧАНИЕ

Если векторы нормалей для вершин v0 и v1 будут ненулевыми, то эти вершины также будут смещаться. Но если мы сместим все четыре вершины, описывающие край силуэта, то просто переместим вырожденный квадрат. Зафиксировав вершины v0 и v1 и смещая только вершины v2 и v3 мы восстанавливаем квадрат.

Вырожденный квадрат, описывающий край, разделенный на два треугольника



Рисунок 17.6. Вырожденный квадрат, описывающий край, разделенный на два треугольника

Обратите внимание, что для двух вершин, v0 и v1 на Рисунок 17.6, мы указываем в качестве вектора нормали вершины нулевой вектор. Затем, когда мы передаем вершины края в вершинный шейдер, он проверяет, является ли данный край частью силуэта. Если да, вершинный шейдер смещает позицию вершин вдоль вектора нормали вершины на заданный скаляр. Обратите внимание, что те вершины, для которых указан нулевой вектор нормали, не смещаются. Таким образом мы получаем невырожденный квадрат, представляющий край силуэта, как показано на Рисунок  17.7.