Опубликован: 28.04.2009 | Доступ: свободный | Студентов: 1840 / 107 | Оценка: 4.36 / 4.40 | Длительность: 16:40:00
Специальности: Программист
Лекция 2:

Визуализация примитивов

Типы данных

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

Приступим. Мы начнем с написания программы для вершинного процессора: вершинного шейдера. Наш шейдер будет принимать в качестве параметра координаты вершины в обычных декартовых координатах, а возвращать координаты вершины уже в однородных координатах. Все преобразование будет сводиться к добавлению к координатам вершины четвертого компонента ( w ), равного 1 (листинг 2.1).

float4 MainVS(float3 pos) 
{
return float4(pos, 1.0); 
}
Листинг 2.1.

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

Скалярные типы

В HLSL все встроенные типы делятся на две большие группы: скалярные и векторные. Скалярные типы данных являются аналогами встроенных типов данных языка C (таблица 2.1).

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

Задавая тип переменной, вы просто указываете компилятору, что вы хотели бы использовать переменную этого типа. Если текущий ускоритель не поддерживает некоторые типы данных, используемые в программе, то при компиляции шейдера в машинный код они будут заменены ближайшими аналогами. Например, тип 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;
// 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. Единственным надежным решением является внимательный анализ кода ассемблерного кода шейдера. Поэтому рекомендуется, по возможности, избегать использования типа 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;

Язык HLSL позволяет инициализировать вектор двумя способами. Первый способ - перечислить значения вектора в фигурных скобках на манер инициализации массивов в языке C. Ниже приведен пример, присвоения четырехмерному вектору v начального значения (\overline{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).

float4 MainVS(float3
pos:POSITION) 
{
return float4(pos, 1.0);
}
Листинг 2.2.

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

float4 MainVS(float3
pos:POSITION):POSITION
 {
return float4(pos, 1.0); 
}
Листинг 2.3.

Вот теперь мы наконец-то получили полноценный вершинный шейдер. Следующий этап - написание пиксельного шейдера. Наш первый пиксельный шейдер будет просто закрашивать все пиксели цветом морской волны ( aqua ) (листинг 2.4).

float4 MainPS() : COLOR 
{
return float4(0.0, 1.0, 1.0, 1.0); 
}
Листинг 2.4.

Примечание

В HLSL минимальной яркости цветового канала соответствует значение 0.0, а максимальной 1.0.

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

Техники, проходы и профили

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

technique Fill 
{
pass p0 
{
VertexShader = compile vs_1_1 MainVS(); 
PixelShader = compile ps_1_1 MainPS(); 
} 
}
Листинг 2.5.

Как видно, техника определяется с использованием ключевого слова 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

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

Допустим, необходимо, чтобы наша программа могла работать на видеокартах класса 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. Если для вас актуальна производительность вашего приложения на этих 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).

// Вершинный шейдер. Принимает координаты 
вершины (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();
} 
}
Листинг 2.6.

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

Андрей Леонов
Андрей Леонов

Reference = add reference, в висуал студия 2010 не могу найти в вкладке Solution Explorer, Microsoft.Xna.Framework. Его нету.