Опубликован: 05.08.2010 | Уровень: специалист | Доступ: платный
Самостоятельная работа 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();
        }

Теперь создадим обработчики нужных событий и заполним их кодом.

Алексей Бабушкин
Алексей Бабушкин

При выполнении в лабораторной работе упражнения №1 , а именно при выполнении нижеследующего кода:

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.Graphics;

   

namespace Application1

{

    public partial class MainForm : Form

    {

        // Объявим поле графического устройства для видимости в методах

        GraphicsDevice device;

   

        public MainForm()

        {

            InitializeComponent();

   

            // Подпишемся на событие Load формы

            this.Load += new EventHandler(MainForm_Load);

   

            // Попишемся на событие FormClosed формы

            this.FormClosed += new FormClosedEventHandler(MainForm_FormClosed);

        }

   

        void MainForm_FormClosed(object sender, FormClosedEventArgs e)

        {

            //  Удаляем (освобождаем) устройство

            device.Dispose();

            // На всякий случай присваиваем ссылке на устройство значение null

            device = null;       

        }

   

        void MainForm_Load(object sender, EventArgs e)

        {

            // Создаем объект представления для настройки графического устройства

            PresentationParameters presentParams = new PresentationParameters();

            // Настраиваем объект представления через его свойства

            presentParams.IsFullScreen = false; // Включаем оконный режим

            presentParams.BackBufferCount = 1;  // Включаем задний буфер

                                                // для двойной буферизации

            // Переключение переднего и заднего буферов

            // должно осуществляться с максимальной эффективностью

            presentParams.SwapEffect = SwapEffect.Discard;

            // Устанавливаем размеры заднего буфера по клиентской области окна формы

            presentParams.BackBufferWidth = this.ClientSize.Width;

            presentParams.BackBufferHeight = this.ClientSize.Height;

   

            // Создадим графическое устройство с заданными настройками

            device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware,

                this.Handle, presentParams);

        }

   

        protected override void OnPaint(PaintEventArgs e)

        {

            device.Clear(Microsoft.Xna.Framework.Graphics.Color.CornflowerBlue);

   

            base.OnPaint(e);

        }

    }

}

Выбрасывается исключение:

Невозможно загрузить файл или сборку "Microsoft.Xna.Framework, Version=3.0.0.0, Culture=neutral, PublicKeyToken=6d5c3888ef60e27d" или один из зависимых от них компонентов. Не удается найти указанный файл.

Делаю все пунктуально. В чем может быть проблема?

Dmitriy Ivanchenko
Dmitriy Ivanchenko
Украина, Кировоград, Виктория-П, 2011
Татьяна Ковалюк
Татьяна Ковалюк
Украина, Киев, Киевский политехнический институт, 1974