| исключение в лабораторной работе № 3 |
Windows Forms и XNA 3.0
Функции и семантики HLSL
Данные в HLSL упаковываются в функции для выполнения определенных действий, например, можно объявить такую функцию
float4 MainVS(float3 pos)
{
return float4(pos, 1.0);
}Но она не является полноценным векторным шейдером VS. С точки зрения DirectX это всего лишь простая функция, принимающая в качестве параметра трехмерный вектор и возвращающая четырехмерный вектор. Описание вершины может содержать множество разнородных данных: цвет, геометрические координаты и т.п. Поэтому мы должны указать, какой именно вид данных нужно обрабатывать в функции. В HLSL для этой цели используются так называемые семантики (semantics), предназначенные для указания именно тех данных, которые будут проходить через различные ступени обработки графического конвейера.
Для более сложных данных в названии семантики требуется указывать целочисленный индекс. При отсутствии индекса в названии семантики он полагается равным 0. Вот пример некоторых семантик с индексом n:
| Семантика | Описание |
|---|---|
| POSITION[n] | Координаты вершины |
| COLOR[n] | Цвет вершины |
| PSIZE[n] | Размер точки (при визуализации набора точек) |
Уточняющая семантика пишется через двоеточие после объявления обрабатываемого данного: для входного параметра функции - после его объявления в списке параметров, для выходного - после заголовка функции. Таким образом, в нашем примере для связи входного (выходного) параметра pos функции MainVS() с координатами вершины необходимо использовать семантику POSITION:
float4 MainVS(float3 pos : POSITION) : POSITION
{
return float4(pos, 1.0);
}Вот теперь мы наконец-то получили полноценный вершинный шейдер VS.
Следующий этап - написание пиксельного шейдера PS. Наш пиксельный шейдер
будет просто закрашивать все пикселы цветом морской волны (
Aqua = {0,
255, 255} ). Но в HLSL для определения цветов приняты вещественные значения
и яркость соответствующего цвета модели RGB задается в диапазоне (0.0,
1.0). Поэтому для цвета Aqua и непрозрачного альфа-канала окончательный
код пиксельного шейдера будет таким
float4 MainPS() : COLOR
{
return float4(0.0, 1.0, 1.0, 1.0); // RGB hex=(0x00FFFF) и непрозрачный альфа-канал 0xFF
}Техники, проходы и профили HLSL
Мы получили функции для вершинного и пиксельного процессора - вершинный и пиксельный шейдеры. Заключительный этап написания эффекта - создание техники (technique), использующей эти шейдеры. Ниже приведено определение техники с названием Fill, использующей вершинный шейдер MainVS() и пиксельный шейдер MainPS():
technique Fill
{
pass p0
{
VertexShader = compile vs_1_1 MainVS();
PixelShader = compile ps_1_1 MainPS();
}
}Как видно, техника определяется с использованием ключевого слова technique. Каждая техника содержит один или несколько проходов, объявляемых с использованием ключевого слова pass. В свою очередь, каждому проходу ставится в соответствие пиксельный и вершинный шейдер. Наша техника Fill содержит единственный проход с названием p0, внутри которого используется синтаксис:
VertexShader = compile {используемый профиль} {вершинный шейдер};
PixelShader = compile {используемый профиль} {пиксельный шейдер};Профиль шейдера (shader profile) определяет версию языка HLSL, на котором будет скомпилирован шейдер. Профиль учитывает архитектурные особенности целевого графического процессора при генерации промежуточного ассемблерного кода. В большинстве случаев каждой версии HLSL соответствует один профиль. Например, языку 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. Ниже приведено соответствие между профилями и версиями HLSL.
| Профиль | Версия вершинных шейдеров |
|---|---|
| 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 |
| Профиль | Версия пиксельных шейдеров |
| 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 и выше. Все эти видеокарты поддерживают профили вершинных шейдеров 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.
Если мы хотим поступиться эффективностью в пользу масштабируемости приложения, следует использовать минимальную версию профилей, необходимую для нормальной компиляции шейдеров, например, профили vs_1_1 и ps_1_1. Это позволит работать нашему приложению даже на стареньких видеокартах семейства GeForce3 (NV20).
Добавление эффекта в приложение
Пришла пора создать эффект для нашего упражнения - файл с расширением .fx, содержащий инструкции для графических ускорителей видеокарты. Эффект будет ориентирован на минимальную версию профилей.
-
В панели Solution
Explorer вызовите контекстное меню для узла Application2 и командой Add/New Folder создайте папку с именем Data
-
В панели Solution
Explorer вызовите контекстное меню для папки Data и командой Add/New Item добавьте
в нее текстовый файл с именем ColorFill.fx
-
В панели Solution Explorer
выделите файл ColorFill.fx и в панели Properties установите для него
свойство Copy to Output Directory в значение Copy
if newer (копировать в каталог сборки свежую версию файла) -
Заполните файл с эффектом
следующим кодом
struct VertexInput
{
float4 pos : POSITION;
float4 color : COLOR;
};
struct VertexOutput
{
float4 pos : POSITION;
float4 color : COLOR;
};
VertexOutput MainVS(VertexInput input)
{
return input;
}
float4 MainPS(VertexOutput input):COLOR
{
return input.color;
}
technique Fill
{
pass p0
{
VertexShader = compile vs_1_1 MainVS();
PixelShader = compile ps_1_1 MainPS();
}
}Для использования созданного эффекта его нужно загрузить из файла эффекта в приложение и откомпилировать в байт-код. Для этого применяется одна из перегрузок статического метода класса Effect, в том числе
public static Microsoft.Xna.Framework.Graphics.CompiledEffect CompileEffectFromFile( string effectFile, Microsoft.Xna.Framework.Graphics.CompilerMacro[] preprocessorDefines, Microsoft.Xna.Framework.Graphics.CompilerIncludeHandler includeHandler, Microsoft.Xna.Framework.Graphics.CompilerOptions options, Microsoft.Xna.Framework.TargetPlatform platform )
- effectFile – имя загружаемого файла с эффектом
- preprocessorDefines – массив макроопределений (аналогов директивы #define ), используемых при компиляции эффекта. Мы будем использовать значение null
- includeHandler – объект, используемый для обработки директив #include в fx-файле. Так как наш файл не содержит директив #include, мы будем использовать значение null
- options – опции компилятора HLSL, задаваемые с использованием перечислимого типа CompilerOptions. Члены типа CompilerOptions являются битовыми флагами, что позволяет комбинировать их с использованием побитовой операции OR. В качестве этого параметра, как правило, передается значение CompilerOptions.None
- platform – значение перечислимого типа TargetPlatform, указывающее платформу, для которой компилируется эффект. Мы будем использовать значение TargetPlatform.Windows
Результат компиляции, возвращаемый методом CompileEffectFromFile(), нужно сохранить в экземпляре compiledEffect структуры типа CompiledEffect. Если компиляция прошла успешно, то булево свойство compiledEffect.Success принимает значение true. Если возникли проблемы с открытием fx -файла (например, файл не найден), то будет сгенерировано одно из исключений, производных от System.IO.IOException (например, System.IO.FileNotFoundException или System.IO.DirectoryNotFoundException ).
В дальнейшем объект compiledEffect нам понадобится, чтобы передать с помощью вызова метода compiledEffect.GetEffectCode() скомпилированный байт-код в одну из перегрузок конструктора класса Effect при создании объекта эффекта
public Effect(
Microsoft.Xna.Framework.Graphics.GraphicsDevice graphicsDevice,
byte[] effectCode,
Microsoft.Xna.Framework.Graphics.CompilerOptions options,
Microsoft.Xna.Framework.Graphics.EffectPool pool)- graphicsDevice - устройство Direct3D, которое будет использоваться для работы с эффектом
- effectCode - код эффекта, предварительно скомпилированный при помощи метода CompileEffectFromFile()
- options - опции компилятора, определяемые перечислимым типом CompilerOptions. Довольно часто в качестве этого параметра передается значение CompilerOptions.NotCloneable. Оно запрещает клонирование (создание копии) эффекта при помощи метод Clone(). Это уменьшает объем используемой памяти, так как в оперативной памяти видеокарты не хранится информация, необходимая для клонирования эффекта. При этом экономия оперативной памяти достигает 50%
- pool - экземпляр класса EffectPool, позволяющий нескольким эффектам использовать общие параметры. Мы используем один fx-файл и этот параметр будет равен null
Имена всех техник, указанные в fx -файле, при создании объекта effect, помещаются в его свойство-коллекцию Techniques типа EffectTechniqueCollection. Коллекция содержит объекты техник effectTechnique типа EffectTechnique. Перебрав все объекты этой коллекции можно проверить с помощью метода CurrentTechnique.Validate(), поддерживаются ли указанные техники видеокартой текущего компьютера.
Если эффект содержит лишь единственную технику, как в нашем случае, то задача упрощается. Дело в том, что конструктор класса Effect при создании экземпляра эффекта автоматически находит первую попавшуюся технику и присваивает ее свойству CurrentTechnique. Поэтому приложению для получения информации об этой технике достаточно обратиться к свойству CurrentTechnique и с помощью метода CurrentTechnique.Validate() проверить возвращаемый им булев флаг, поддерживает GPU видеокарты данную технику или нет.
-
В панели Solution
Explorer вызовите контекстное меню для узла References проекта Application2 и
командой Add Reference добавьте через одноименное окно из вкладки Recent окна
(недавно выбранные) библиотеку Microsoft.Xna.Framework.dll
-
Через контекстное меню
переведите форму в режим редактирования View Code и добавьте
в файл MainForm.cs следующий код
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using XNAGraphics = Microsoft.Xna.Framework.Graphics;
using System.IO;
namespace Application2
{
public partial class MainForm : Form
{
// Вспомогательные поля
const string effectFileName = "Data\\ColorFill.fx";
GraphicsDevice device = null;
PresentationParameters presentParams = new PresentationParameters();
Effect effect = null;
VertexDeclaration decl = null;
VertexPositionColor[] vertices = new VertexPositionColor[3];
bool closing = false;
public MainForm()
{
InitializeComponent();
}
}
}-
Добавьте в класс MainForm метод
с именем CreateDeviceAndEffect() и заполните его следующим кодом
void CreateDeviceAndEffect()
{
// Отключить у панели автоматическую очистку фона и задать
// автоматический вызов события Paint при изменении размеров
xnaPanel1.SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
// Настраиваем объект представления через его свойства
presentParams.IsFullScreen = false; // Включаем оконный режим
presentParams.BackBufferCount = 1; // Включаем задний буфер
// для двойной буферизации
// Переключение переднего и заднего буферов
// должно осуществляться с максимальной эффективностью
presentParams.SwapEffect = SwapEffect.Discard;
// Устанавливаем размеры заднего буфера по клиентской области панели
presentParams.BackBufferWidth = xnaPanel1.ClientSize.Width;
presentParams.BackBufferHeight = xnaPanel1.ClientSize.Height;
// Создадим графическое устройство с заданными настройками
device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware,
xnaPanel1.Handle, presentParams);
// Создаем декларацию о формате применяемого хранилища вершин
decl = new VertexDeclaration(device, VertexPositionColor.VertexElements);
// Загружаем и компилируем эффект из файла
CompiledEffect compiledEffect;
try
{
compiledEffect = Effect.CompileEffectFromFile(effectFileName,
null, null, CompilerOptions.None, TargetPlatform.Windows);
}
catch (IOException ex)
{
closing = true;
MessageBox.Show(ex.Message, "Ошибка при загрузке эффекта",
MessageBoxButtons.OK, MessageBoxIcon.Error);
Application.Idle += new EventHandler(Application_Idle);
return;
}
// Проверяем, нормально ли откомпилирован эффект
if (!compiledEffect.Success)
{
closing = true;
MessageBox.Show(String.Format(
"Ошибка при компиляции эффекта:\n{0}", compiledEffect.ErrorsAndWarnings),
"Критическая ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
Application.Idle += new EventHandler(Application_Idle);
return;
}
// Создаем объект эффекта
effect = new Effect(device, compiledEffect.GetEffectCode(),
CompilerOptions.NotCloneable, null);
// Проверяем, поддерживается ли видеокартой указанная в эффекте техника
if (!effect.CurrentTechnique.Validate())
{
closing = true;
MessageBox.Show(String.Format("Ошибка при проверке техники \"{0}\" эффекта \"{1}\"\n" +
"Скорее всего, функциональность шейдера превышает возможности GPU",
effect.CurrentTechnique.Name, effectFileName),
"Критическая ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
Application.Idle += new EventHandler(Application_Idle);
return;
}
}
void Application_Idle(object sender, EventArgs e)
{
this.Close();
}-
Поместите
вызов метода CreateDeviceAndEffect() в конструктор класса после кода
инициализации компонентов формы
public MainForm()
{
InitializeComponent();
// Создание устройства и эффекта
CreateDeviceAndEffect();
}Теперь создадим обработчики нужных событий и заполним их кодом.

