Единое окно доступа к образовательным ресурсам

Введение в XNA

Голосов: 5

Книга адресована студентам и начинающим разработчикам, которые хотят использовать в своих проектах высокопроизводительную графику. Книга рассчитана на читателей, уже знакомых с основами C# и платформы .NET. Первая глава знакомит читателя с XNA Framework. Вторая глава посвящена визуализации базовых примитивов XNA Framework. В третьей главе рассматриваются более сложные вопросы визуализации: использование полноэкранного режима, плавная анимация примитивов и имитация прозрачности. В четвертой главе весь ранее изученный материал сводится воедино на примере создания полноценного хранителя экрана с дистрибутивом, при необходимости автоматически инсталлирующим на компьютер пользователя XNA Framework. В пятой, заключительной главе затрагивается тема программируемого графического конвейера, но уже на значительно более глубоком уровне; рассматриваются язык HLSL, основы ассемблеро-подобного языка Vertex Shader 1.1 и интегрированная среда разработки FX Composer 2.0, значительно облегчающая разработку и отладку шейдеров. Данная книга входит в состав <a target=_blank href="http://www.microsoft.com/Rus/Msdnaa/Curricula/">"Библиотеки учебных курсов"</a>, формирование которой ведется в рамках программы академического сотрудничества <a target=_blank href="http://www.microsoft.com/rus/msdnaa/">MSDN Academic Alliance (MSDN AA)</a>.

Приведенный ниже текст получен путем автоматического извлечения из оригинального PDF-документа и предназначен для предварительного просмотра.
Изображения (картинки, формулы, графики) отсутствуют.
    Первым действительно массовым GPU с вершинными и пиксельными процессорами стал NV20 (NVIDIA
GeForce3), появившийся в 2001 году. Для программирования вершинных и пиксельных процессоров NV20
корпорация Microsoft совместно с NVIDIA разработали языки Vertex Shader 1.1 и Pixel Shader 1.1,
являющиеся расширенными версиями Vertex Shader 1.0 и Pixel Shader 1.0. Вскоре после NV20 вышел NV25
(GeForce4), функциональность пиксельных процессоров которого была несколько расширена.
Соответственно язык Pixel Shader 1.1 был обновлѐн до версии 1.328. Потом появился процессор GPU R200
(Radeon 8500) корпорации ATI и язык Pixel Shader 1.4, затем R300 (Radeon 9700 Pro) с Vertex Shader 2.0 и
Pixel Shader 2.0 и так далее (см. приложение 1).
В итоге к началу 2002-го года на рынке творилась полная неразбериха среди языков программирования
шейдеров. К счастью Microsoft предвидела подобный поворот, и поэтому заранее сделала языки Vertex
Shader и Pixel Shader независимыми от системы команд графического процессора. Фактически каждая
версия языка Vertex/Pixel Shader является языком программирования для некоторого виртуального
процессора, приближенного к некоторому реальному прототипу. Компиляция шейдера в систему команд
физического процессора происходит непосредственно перед загрузкой шейдера в GPU. Таким образом,
языки Vertex Shader и Pixel Shader являются аналогами языка IL в .NET.
Независимость языков Vertex Shader и Pixel Shader от системы команд физического процессора
теоретически позволяет GPU выполнять любой ассемблерный код, независимо о версии шейдера.
Например, GPU R200 корпорации ATI наряду с родными Pixel Shader 1.4 может выполнять Pixel Shader 1.0,
Pixel Shader 1.1, Pixel Shader 1.2 и Pixel Shader 1.3. Это достигается путѐм перекомпиляции чужеродных
шейдеров в родной код. К сожалению, обратное преобразование не всегда возможно. Например, R200 не
может выполнять Pixel Shader 2.0, так как программа, использующая продвинутые возможности этой версии
шейдеров не может быть втиснута в прокрустово ложе архитектуры R200.
По мере роста возможностей GPU программы для вершинных и пиксельных процессоров становились всѐ
сложение и сложнее. Например, если в Pixel Shader 1.1 длина программы не могла превышать 16
ассемблерных команд, то в Pixel Shader 2.0 максимально возможное число ассемблерных инструкций
превысило сотню. Соответственно возрастала трудоѐмкость разработки и поддержки шейдеров с
использованием ассемблера-подобного языка. Таким образом, возникла реальная потребность в переходе на
языки программирования шейдеров высокого уровня.
В 2002 году Microsoft выпустила высокоуровневый язык программирования шейдеров High Level Shader
Language (HLSL). HLSL – это язык программирования высокого уровня, предназначенный для написания
программ (шейдеров) для вершинных и пиксельных процессоров. HLSL является C-подобным языком
программирования с многочисленными заимствованиями из C++ и C#. В тоже время в HLSL имеется ряд
важных расширений, полезных при программировании графического процессора. Программа, написанная на
HLSL, компилируется в один из ассемблеро-подобных языков DirectX. Таким образом, процесс компиляции
HLSL программы очень напоминает компиляцию C#-программы сначала на промежуточный язык (IL), а
затем в машинный для конкретного центрального процессора (рисунок 2.4).

                                           Ассемблеро-
                                                                       Машинный код
     Программа на                         подобный язык
                                                                      для графического
         HLSL                              (Vertex/Pixel
                                                                         процессора
                                             Shader)

Рисунок 2.4. Компиляция HLSL-программы.

Самой крупной логической единицей HLSL является эффект (Effect), хранящийся в отдельном текстовом
файле с расширением .fx. В принципе, эффект можно считать аналогом материала в 3DS MAX. Каждый
эффект состоит из одной или нескольких техник (technique). Техника – это способ визуализации материала.
Например, эффект визуализации мраморного материала может содержать три техники для различных
графических процессоров: технику High для ускорителей класса High End, Medium для ускорителей
среднего класса, и Low – максимальная производительность при низком качестве изображения 29. Каждой
технике сопоставлен пиксельный и вершинный шейдер, при этом несколько техник могут использовать
общий шейдер.




28
   Для программирования пиксельных процессоров NV25 (GeForce4) планировалось использовать язык Pixel
Shader 1.2. Однако после выхода NV25 оказалась, что его функциональность несколько шире, чем
предполагалось. Соответственно язык Pixel Shader 1.2 оказался не удел, и вскоре был обновлѐн до версии
1.3.
29
   Количество техник и их названия могут быть произвольными.


Типы данных
Как известно, лучший способ изучить новый язык программирования – написать на нѐм несколько
программ. Так мы и поступим. Для начала мы создадим простейший эффект, закрашивающий примитив
цветом морской волны (aqua). Эффект будет содержать одну технику, которую мы назовѐм Fill.
И так, приступим. Мы начнѐм с написания программы для вершинного процессора: вершинного шейдера.
Наш шейдер будет принимать в качестве параметра координаты вершины в обычных декартовых
координатах, а возвращать координаты вершины уже в однородных координатах. Всѐ преобразование будет
сводиться к добавлению к координатам вершины четвѐртого компонента (w), равного 1 (листинг 2.1).

 Листинг 2.1.

float4 MainVS(float3 pos)
{
       return float4(pos, 1.0);
}

Как видно, программа, написанная на HLSL, очень напоминает обычную C-программу: мы объявляем
функцию MainVS, которая принимает в качестве параметра переменную типа float3, а возвращает
значение типа float4. Что это за такие странные типы float3 и float4, которых нет ни в C, ни C++, ни в
C#? Чтобы ответить на этот вопрос мы рассмотрим встроенные типы HLSL.
Скалярные типы.
В HLSL все встроенные типы делятся на две большие группы: скалярные и векторные. Скалярные типы
данных являются аналогами встроенных типов данных языка C (таблица 1.1).
                                                                               Таблица 1.1. Скалярные типы

Тип                                                  Описание
bool                                                 Логический тип, который может принимать значения true
                                                     или false
int                                                  32-х битное целое число
half                                                 16-ти битное число с плавающей точкой
float                                                32-х битное число с плавающей точкой
double                                               64-х битное число с плавающей точкой


Задавая тип переменной, вы просто указываете компилятору, что вы хотели бы использовать переменную
этого типа. Если текущий ускоритель не поддерживает некоторые типы данных, используемые в программе,
то при компиляции шейдера в машинный код они будут заменены ближайшими аналогами30. Например, тип
                                                                                         31
double может быть заменѐн на тип float, half или какой-нибудь иной внутренний тип . Поэтому
программист должен стараться избегать жѐсткой привязки к точности и допустимому диапазону значений
используемого типа данных. Особенно это актуально для типа int, так как подавляющее большинство
современных ускорителей не поддерживают тип int, в результате чего он эмулируется посредством одного
из вещественных типов. Допустим, у нас имеется следующий код:
// a присваивается значение 5
int a = 5;
// b должно быть присвоено значение 1
int b = a / 3;
// c должно стать равно 2
int c = b * 2;
Какой код будет сгенерирован компилятором? Трудно дать однозначный ответ. В большинстве случаев
компилятор просто заменяет типы int, к примеру, на float:
// a присваивается значение 5.0
float a = 5.0;

30
  Это лучше, чем прерывать работу программы с сообщением об ошибке.
31
  Например, GPU семейства NV3x (GeForce FX) в дополнение к half и float поддерживают 12-битный
вещественный формат с диапазоном значений от -2 до 2. А GPU ATI R3xx/R4xx поддерживают 24-х битный
формат с плавающей запятой. Подробную информацию о типах данных, поддерживаемых GPU корпораций
ATI, NVIDIA и Intel, можно найти в приложении 5.


// b будет присвоено значение 1.66667
float b = a / 3.0;
// c станет равно 3.33334
float c = b * 2.0;
Думаю, это совершенно не то результат, который вы ожидали. Однако в ряде случаев компилятор HLSL всѐ
же может начать скрупулезно эмулировать тип int посредством float:
// a присваивается значение 5.0
float a = 5.0;

// Значение b вычисляется посредством целочисленного деления
float b;

// Выполняем обычно вещественное деление
float fd = a / 3.0;
// Находим дробную часть от деления
float ff = frac(fd);
// Получаем целую часть
b = fd - ff;

// Если частное меньше нуля, а дробная часть не равна 0, корректируем результат. Это
// обусловлено тем, что frac(2.3) = 0.3, но frac(-2.3) = 0.7
if ((fd < 0) && (ff > 0))
       b = b + 1;

// c станет равно 2.0
float c = b * 2.0;
Нетрудно заметить, что обратной стороной подобной эмуляции является существенно падение
производительности шейдера.
Из-за множества нюансов, заранее достаточно трудно предугадать, какой из двух подходов будет выбран
компилятором HLSL. Единственным надежным решением является внимательный анализ кода
ассемблерного кода шейдера32. Поэтому рекомендуется, по возможности, избегать использования типа int в
коде шейдера за исключением переменных счетчиков цикла и индексов массивов.
Векторные типы.
Большинство данных, используемых в трѐхмерной графике, является многомерными векторами,
размерность которых редко превышает 4. Так, координаты точки в трѐхмерном пространстве задаются
трѐхмерным вектором, цвет пикселя – четырѐхмерным вектором (три цвета и альфа-канал) и так далее.
Соответственно, все современные GPU являются векторными процессорами, способными одновременно
выполнять одну операцию сразу над набором из четырѐх чисел (четырѐхмерным вектором).
В HLSL имеется множество типов для работы с векторами размерностью от 2-х до 4-х. Вектор из N
элементов типа type задаѐтся с использованием синтаксиса, отдалѐнно напоминающего обобщенные
(Generic) классы из C#:
vector<type, size>
где
     type – имя базового типа: bool, int, half, float или double;
     size – размерность вектора, которая может быть равна 1, 2, 3 или 4.
Ниже приведѐн пример объявления переменной v, являющейся вектором из четырѐх чисел типа float.
vector<float, 4> v;
Однако на практике обычно используется сокращѐнная запись по схеме:
{type}{N}
где
     type – имя базового типа
     N – размерность вектора.
Таким образом, вышеприведѐнное определение переменной v можно переписать следующим образом:
float4 v;

32
     Основы ассемблероподобных языков Vertex Shader и Pixel Shader будут рассмотрены в четвертой главе.


Язык HLSL позволяет инициализировать вектор двумя способами. Первый способ – перечислить значения
вектора в фигурных скобках на манер инициализации массивов в языке C. Ниже приведѐн пример,
присвоения четырѐхмерному вектору v начального значения       (0.2, 0.4, 0.6, 0.8) .
float4 v={0.2, 0.4, 0.6, 0.8};
Другой способ – создать новый вектор с использованием конструктора и присвоить его вектору v:
float4 v=float4(0.2, 0.4, 0.6, 0.8);
Любой N мерный вектор имеет множество конструкторов, которые могут принимать в качестве параметров
как скалярные типы, так и векторы.. Единственное ограничение: общее количество всех компонентов
векторов и скалярных типов должно быть равно N. Подобное многообразие конструкторов даѐт
программисту потрясающую гибкость при инициализации векторов:
// Создаѐм двухмерный вектор и присваиваем ему значение (0.1, 0.2)
float2 a={0.1, 0.2};
// Создаѐм ещѐ один двухмерный вектор и присваиваем ему значение (0.3, 0.4)
float2 b=float2(0.3, 0.4);
// Создаѐм трѐхмерный вектор. Конструктору в качестве параметра передаѐтся вектор “b” и число
// 1.0. Соответственно вектору c будет присвоено значение (0.3, 0.4, 1.0)
float3 c=float3(b, 1.0);
// Создаѐм четырѐхмерный вектор на основе скалярного типа и трѐхмерного вектора. Итоговое
// значение вектора d будет равно (0.7, 0.3. 0.4, 1.0)
float4 d=float4(0.7, c);
// Создаѐм четырѐхмерный вектор на основе двух двухмерных. В результате вектору “d” будет
// присвоено значение (0.1, 0.2. 0.3, 0.4)
float4 e=float4(a, b);

Семантики
Думаю, после такого небольшого экскурса в HLSL вы без труда сможете разобраться в тексте вершинного
шейдера из листинга 2.1. Однако если быть более точным, функция, приведѐнная в этом листинге, не
является полноценным шейдером. С точки зрения DirectX это всего лишь простая функция, принимающая в
качестве параметра трѐхмерный вектор и возвращающая четырѐхмерный вектор. Чтобы превратить эту
функцию в вершинный шейдер, мы должны связать параметр Pos с координатами вершины, а результаты
функции – с итоговыми координатами вершины. В HLSL для этой цели используются так называемые
семантики (semantics), предназначенные для связи между собой данных, проходящих через различные
ступени графического конвейера. В таблице 2.2 приведены некоторые семантики для входящих данных
вершинного шейдера. Описание всех семантик HLSL можно найти в приложении 3.


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

                                       Таблица 2.2. Некоторые семантики входных данных вершинного шейдера

Семантика                                               Описание
POSITION[n]                                             Координаты вершины
COLOR[n]                                                Цвет вершины
PSIZE[n]                                                Размер точки (при визуализации набора точек)


Для связи параметра функции с входными данными шейдера, после объявления параметра укажите знак
двоеточия и название соответствующей семантики. Таким образом, для связи параметра pos функции
MainVS с координатами вершины необходимо использовать семантику POSITION (листинг 2.2).

Листинг 2.2.

float4 MainVS(float3 pos:POSITION)
{
       return float4(pos, 1.0);
}


Теперь нам надо указать, что функция MainVS возвращает трансформированные координаты вершины. Для
этого в HLSL используются семантики выходных данных вершинного шейдера. В частности, для указания
того факта, что шейдер возвращает трансформированные координаты вершины используется семантика
POSITION (листинг 2.3).

Листинг 2.3.

float4 MainVS(float3 pos:POSITION):POSITION
{
       return float4(pos, 1.0);
}
Вот теперь мы наконец-то получили полноценный вершинный шейдер. Следующий этап – написание
пиксельного шейдера. Наш первый пиксельный шейдер будет просто закрашивать все пиксели цветом
морской волны (aqua) (листинг 2.4).

Листинг 2.4.

float4 MainPS() : COLOR
{
       return float4(0.0, 1.0, 1.0, 1.0);
}
      П р им еч а н ие
      В HLSL минимальной яркости цветового канала соответствует значение 0.0, а максимальной 1.0.

Так как этот шейдер будет выполняться для каждого пикселя визуализируемого примитива, все пиксели
примитива окрасятся в цвет морской волны. Семантика COLOR указывает DirectX, что результат работы
пиксельного шейдера MainPS является итоговым цветом пикселя.

Техники, проходы и профили
И так, у нас имеются программы для вершинного и пиксельного процессора – вершинный и пиксельный
шейдеры. Заключительный этап написания эффекта – создание техники (technique), использующей этот
шейдеры. Ниже приведено определение техники с названием Fill, использующей вершинный шейдер
MainVS и пиксельный шейдер MainPS (листинг 2.5).

Листинг 2.5.

technique Fill
{
       pass p0
       {
               VertexShader = compile vs_1_1 MainVS();
               PixelShader = compile ps_1_1 MainPS();
       }
}


Как видно, техника определяется с использованием ключевого слова technique. Каждая техника содержит
один или несколько проходов, объявляемых с использованием ключевого слова pass. В свою очередь
каждому проходу ставится в соответствие пиксельный и вершинный шейдер. Наша техника Fill содержит
единственный проход с названием p0.


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



Вершинный шейдер для каждого прохода (pass) задаѐтся с использованием следующего синтаксиса:
VertexShader = compile {используемый профиль} {вершинный шейдер};
Пиксельный шейдер задаѐтся аналогично:
PixelShader = compile {используемый профиль} {пиксельный шейдер};


Профиль шейдера (shader profile) задаѐт промежуточный ассемблеро-подобный язык, на который будет
скомпилирован шейдер. Кроме того, профиль задаѐт некоторые архитектурные особенности целевого
графического процессора, которые будут учтены компилятором при генерации промежуточного
ассемблерного кода. В большинстве случаев каждой версии шейдеров соответствует один профиль.
Например, языку Vertex Shader 1.1 соответствует профиль vs_1_1; Pixel Shader 1.4 – профиль ps_1_4, Pixel
Shader 2.0 – профиль ps_2_0 и так далее. Однако некоторым языкам вроде Pixel Shader 2.x соответствует два
профиля: в данном случае это ps_2_a и ps_2_b, при этом первый профиль генерирует код Pixel Shader 2.x,
оптимизированный под архитектуру NV3x, а второй – для R4xx. В таблицах 2.3 и 2.4 приведено
соответствие между профилями и соответствующими версиями шейдеров.
                                                                     Таблица 2.3. Профили вершинных шейдеров

Профиль                                                  Версия вершинных шейдеров
vs_1_0                                                   1.0
vs_1_1                                                   1.1
vs_2_0                                                   2.0
vs_2_a                                                   2.x
vs_3_0                                                   3.0


                                                                     Таблица 2.4. Профили пиксельных шейдеров

Профиль                                                  Версия пиксельных шейдеров
ps_1_0                                                   1.0
ps_1_1                                                   1.1
ps_1_2                                                   1.2
ps_1_3                                                   1.3
ps_1_4                                                   1.4
ps_2_0                                                   2.0
ps_2_a                                                   2.x (оптимизация для NV3x)
ps_2_b                                                   2.x (оптимизация для R4xx)
ps_3_0                                                   3.0


Большинство видеокарт поддерживает несколько профилей вершинных и пиксельных шейдеров (см.
приложение 2). В результате каждый разработчик сталкивается с проблемой выбора используемого
профиля. В большинстве случаев выбор версии шейдеров определяется минимальными требованиями к
приложению.
Допустим, необходимо, чтобы наша программа могла работать на видеокартах класса ATI Radeon 9500
(R3xx) и выше, NVIDIA GeForce FX 5200 (NV3x) и выше, а так же Intel GMA 900 и выше. Изучив
приложение 2, мы увидим, что все видеокарты, удовлетворяющие этому критерию, поддерживают профили
вершинных шейдеров vs_1_0, vs_1_1, vs_2_0 и профили пиксельные шейдеров ps_1_0, ps_1_1, ps_1_2,
ps_1_3, ps_1_4 и ps_2_0. Таким образом, мы можем смело использовать профили vs_2_0 и ps_2_0 для всех
шейдеров. При этом для некоторых эффектов можно предусмотреть дополнительные техники (technique)
для видеокарт класса High End, использующих профили vs_3_0 и ps_3_0.
         П р им еч а н ие
         GPU семейства NV3x демонстрируют очень низкую производительность при использовании профилей
         пиксельных шейдеров ps_2_0 и ps_2_a ([С.5], [С.6]). Если для вас актуальна производительность вашего
         приложения на этих GPU, то имеет смысл стараться по возможности использовать профиль ps_1_4 вместо
         ps_2_0. Другой вариант – предусмотреть отдельные упрощѐнные техники для NV3x, использующие профили
         ps_1_4.

В примерах этой книги я буду стараться использовать минимальную версию профилей, необходимую для
нормальной компиляции шейдеров. В частности, именно по этой причине, наш эффект Fill использует
профили vs_1_1 и ps_1_1: это позволит работать нашему эффекту даже на стареньких видеокартах
семейства GeForce3 (NV20).


И так, у нас есть вершинный и пиксельный шейдеры, а так же техника Fill, использующая эти шейдеры.
Для получения готового эффекта осталось только помесить их в файл с расширением *.fx, например, в
SimpleEffect.fx (листинг 2.6).

 Листинг 2.6.

// Вершинный шейдер. Принимает координаты вершины (x, y, z). Возвращает – координаты вершины
// в однородных координатах (x, y, z, 1.0)
float4 MainVS(float3 pos:POSITION):POSITION
{
       return float4(pos, 1.0);
}

// Пиксельный шейдер. Закрашивает все пиксели примитива цветом морской волны.
float4 MainPS():COLOR
{
       return float4(0.0, 1.0, 1.0, 1.0);
}

// Техника Fill
technique Fill
{
// Первый проход
       pass p0
       {
// Задаѐм вершинный шейдер для техники. Для компиляции шейдера используется профиль vs_1_1
               VertexShader = compile vs_1_1 MainVS();
// Задаѐм пиксельный шейдер. для компиляции шейдера используется профиль ps_1_1
               PixelShader = compile ps_1_1 MainPS();
       }
}


Теперь мы должны научиться использовать этот эффект в наших C#-приложениях.

2.3.3. Использование эффектов в XNA Framework
Одним из основных классов XNA Framework, предназначенным для работы с эффектами, является класс
Effect. Класс Effect является довольно сложным классом, содержащим ряд коллекций, отражающих
структуру файла эффекта (рисунок 2.5). Как говорилось в прошлом разделе, в каждом эффекте HLSL
имеется несколько техник, которые в свою очередь содержат несколько проходов. При этом минимально
возможный эффект включает хотя бы одну технику и один проход. Соответственно, класс Effect содержит
коллекцию Techniques с экземплярами классов EffectTechnique, инкапсулирующих техники. В свою очередь,
каждая техника содержит коллекцию Passes экземпляров класса EffectPass с информацией об эффекте.

              Effect



                Techniques
             (EffectTechnique)


                         Passes
                       (EffectPass)
Рисунок 2.5. Коллекции класса Effect.


Загрузка и компиляция файла эффекта.
Загрузка эффекта из файла *.fx с последующей компиляцией осуществляется при помощи статического
метода Effect.CompiledEffect:
public static CompiledEffect CompileEffectFromFile(string effectFile, CompilerMacro[]
preprocessorDefines, CompilerIncludeHandler includeHandler, CompilerOptions options,
TargetPlatform platform);


где
     effectFile – имя файла с эффектом.
     preprocessorDefines – массив макроопределений (аналогов директивы #define в C#), используемых
      при компиляции эффекта. Мы будем использовать значение null.
     includeHandler – объект, используемый для обработки директив #include в fx-файле. Так как наш
      файл не содержит директив #include, мы будем использовать значение null.
     options – опции компилятора HLSL, которые задаваемые с использованием перечислимого типа
      CompilerOptions (таблица 2.5.). Члены типа CompilerOptions являются битовыми флагами, что
      позволяет комбинировать их с использованием оператора |. В качестве этого параметра, как правило,
      передаѐтся значение CompilerOptions.None.
     platform – значение перечислимого типа TargetPlatform, указывающее платформу, для которой
      компилируется    эффект.     В       XNA   Framework      1.0    поддерживаются       две     платформы:
      TargetPlatform.Windows и TargetPlatform.Xbox360, названия которых говорят за себя. Все
      примеры этой книги будут использовать значение TargetPlatform.Windows.
                                             Таблица 2.5. Некоторые члены перечислимого типа CompilerOptions.

Член перечисления                                        Значение
None                                                     Нет никаких опций
Debug                                                    Вставляет в ассемблерный код отладочную информацию
NotCloneable(*)                                          Запрещает клонирование (создании копии) эффекта при
                                                         помощи метод Clone. Эта опция уменьшает объѐм
                                                         используемой памяти, так как в оперативной памяти не
                                                         хранится информация, необходимая для клонирования
                                                         эффекта. При этом экономия оперативной памяти
                                                         достигает 50%.
ForceVertexShaderSoftwareNoOptimizations                 Форсирует компиляцию вершинного шейдера с
                                                         использованием максимально возможной версии Pixel
                                                         Shader (на момент написания книги это 3.0), не взирая на
                                                         возможности текущего графического устройства.
ForcePixelShaderSoftwareNoOptimizations                  Форсирует компиляцию пиксельного шейдера с
                                                         использованием максимально возможной версии Pixel
                                                         Shader (на момент написания книги это 3.0), не взирая на
                                                         возможности текущего графического устройства.
PartialPrecision                                         Использовать минимальную точность вычислений,
                                                         поддерживаемую текущим графическим устройством. Как
                                                         правило, при использовании этой опции типы double и
                                                         float заменяются на half.
SkipOptimization                                         Отключает оптимизацию кода.
SkipValidation                                           Отключает проверку соответствия сгенерированного кода
                                                         возможностям текущего ускорителя (не превышено ли
                                                         ограничение на максимальную длину программы и т.д.)
                                                         перед отправкой откомпилированного кода шейдера в
                                                         драйвер. Этот флаг полезен в тех случаях, когда драйверу
                                                         всѐ же удаѐтся оптимизировать слишком длинный
                                                         ассемблеро-подобный код таким образом, чтобы
                                                         уложиться в ограничения архитектуры графического
                                                         процессора.
(*) – не поддерживается методом CompileEffectFromFile.


Если метод Effect.CompileEffectFromFile не сможет открыть fx-файл (например, из-за его отсутствия),
то будет сгенерировано одно из исключений производных от System.IO.IOException вроде
System.IO.FileNotFoundException или System.IO.DirectoryNotFoundException.
Метод     CompileEffectFromFile       возвращает     структуру     CompiledEffect,      содержащую
откомпилированный код эффекта, а так же отчет о компиляции эффекта (возникли ли какие-либо проблемы
при компиляции эффекта и т.п.).
public struct CompiledEffect
{


// Сообщения о проблемах, возникших при компиляции эффекта
          public string ErrorsAndWarnings { get; }
// Был ли эффект откомпилирован удачно
          public bool Success { get; }
// Если свойство Success равное true, содержит откомпилированный код эффекта
          public byte[] GetEffectCode();
...
}
Стоит отметить, что метод GetEffectCode возвращает байт-код промежуточного языка наподобие того, что
содержится в exe-файлах для платформы .NET. Соответственно, этот код с точки зрения человека является
лишь бессмысленным набором байт. Тем не менее, как мы увидим далее, при необходимости этот байт-код
может быть легко дизассемблирован удобочитаемый текстовый вид.
         П р им еч а н ие
         При желании приложение может сохранить откомпилированный байт-код в каком-нибудь файле, и при
         следующих запусках считывать из файла уже готовый откомпилированный байт-код. Кстати, Visual C# 2005
         Express33 при компиляции проектов, использующих Content Pipeline, автоматически выполняет компиляцию fx-
         файлов проекта и сохраняет полученный промежуточный код в файлах с расширением nvb. Таким образом,
         приложениям, использующим Content Pipeline, нет нужды самостоятельно компилировать fx-файлы.

Следующий этап – компиляция байт-кода промежуточного языка в машинный код вершинных и
пиксельных процессоров текущей видеокарты. Эта операция автоматически осуществляется конструктором
класса Effect:
public Effect(GraphicsDevice graphicsDevice, byte[] effectCode, CompilerOptions options,
EffectPool pool);
где
     graphicsDevice – устройство Direct3D, которое будет использоваться для работы с эффектом
     byte[] effectCode –код           эффекта, предварительно скомпилированный при помощи метода
      CompileEffectFromFile.
     options – опции компилятора, задающиеся использованием перечислимого типа CompilerOptions
      (таблица    2.5.).    Довольно    часто  в   качестве  этого  параметра  передаѐтся значение
      CompilerOptions.NotCloneable,         что позволяет несколько сэкономить объем используемой
      оперативной памяти.
     pool – экземпляр класса EffectPool, позволяющий нескольким эффектам использовать общие
      параметры. В наших первых примерах мы будем использовать не более одного fx-файла, этот параметр
      будет равен null.
После вызова конструктора класса Effect мы наконец-то получим готовую технику. Теперь нам
необходимо выбрать одну из техник эффекта и проверить еѐ поддержку текущей видеокартой. Техники
эффекта хранятся в коллекции Techniques эффекта:
public EffectTechniqueCollection Techniques { get; }
Однако XNA-приложения достаточно редко обращаются к этой коллекции. Дело в том, что конструктор
класса Effect автоматически находит первую попавшуюся технику эффекта и присваивает еѐ свойству
CurrentTechnique.
public EffectTechnique CurrentTechnique { get; set; }
Соответственно, если эффект содержит лишь единственную технику, приложению для получения
информации об этой техники достаточно обратиться к свойству CurrentTechnique, возвращающему
экземпляр класса EffectTechnique, инкапсулирующий технику эффекта. Ниже приведено сокращенное
определение класса EffectTechnique:
public sealed class EffectTechnique
{
// Название техники
       public string Name { get; }
// Коллекция проходов техники
       public EffectPassCollection Passes { get; }
// Выполняет валидацию техники
       public bool Validate();

33
     В Visual Studio 2005 Pro эта функциональность в настоящее время не доступна.


...
}
И так каждый эффект может содержать несколько техник. При этом некоторые техники эффекта могут
нормально работать на текущем GPU, а некоторые (наиболее продвинутые) нет. Если требования техники
(technique) превышают возможности текущего GPU (например, пользователь пытается запустить эффект
использующий профиль ps_1_4 на NV2x), XNA Framework проигнорирует технику. В результате
примитивы, использующие эту технику, будут отображаться некорректно 34. Во избежание подобных
неприятностей необходимо заранее проверить возможность выполнения данной техники средствами
текущего графического устройства. Для этой цели в классе EffectTechnique предусмотрен метод
Validate. Если техника может быть выполнена на текущем устройстве, метод Validate возвращает
значение true, иначе – false. Во втором случае, приложение может попытаться подобрать альтернативную
технику с меньшими системными требованиями или завершить приложение с сообщением о недостаточной
“мощности” текущей видеокарты.
Резюмируя всѐ вышесказанное можно предположить, что код для загрузки эффекта и выбора техники, как
правило, имеет следующую структуру:

GraphicsDevice device;
// Флаг, устанавливаемый в значение true при аварийном завершении работы приложения из-за
// проблем в обработчике события Load
closing = false;
...


// Этот код обычно размещается в обработчике события Load.
CompiledEffect compiledEffect;
try
{
// Загружаем эффект из файла и компилируем в промежуточный код
       compiledEffect = Effect.CompileEffectFromFile(effectFileName, null, null,
CompilerOptions.None, TargetPlatform.Windows);
}
// Если при загрузке файла эффекта возникли проблемы
catch (IOException ex)
{
// Выводим сообщение об ошибке
       MessageBox.Show(ex.Message, "Критическая ошибка", MessageBoxButtons.OK,
 MessageBoxIcon.Error);
// Завершаем работу приложения. Так как метод Close() нельзя вызвать из обработчика
// события Load, приходится идти на хитрости (использовать обработчик события
// Application.Idle, вызывающий внутри себя метод Close главной формы приложения, если
// флаг closing равен true).
       closing = true;
       Application.Idle += new EventHandler(Application_Idle);


       return;
}


// Если эффект был скомпилирован с ошибками
if (!compiledEffect.Success)
{
// Выдаем сообщение об ошибке
       MessageBox.Show(String.Format("Ошибка при компиляции эффекта: \r\n{0}",
            compiledEffect.ErrorsAndWarnings), "Критическая ошибка", MessageBoxButtons.OK,

34
     Как правило, такие примитивы просто закрашиваются чѐрным цветом.



    
Яндекс цитирования Яндекс.Метрика