Практикум прикладного программирования на C# в среде VS.NET 2008

Windows Forms и XNA 3.0

Разбить на страницы
Показывать лекцию целиком

Все необходимые для выполнения данной работы программы можно найти в прилагаемом каталоге.

Работа выполнена на основе материалов С.А. Гайдукова, приведенных в источнике http://www.intuit.ru/department/se/intxna

Упражнение 1. Вывод графической информации XNA на стандартную форму

Создание заготовки приложения

  • Создайте новое решение и новый проект командой File/New/Project, заполнив окно мастера так
  • Переименуйте файл формы Form1.cs в MainForm.cs
  • Выделите форму в конструкторе форм и в панели Properties в свойстве Text установите заголовок окна " Упражнение 1 "
  • Командой Project/Add Reference вызовите окно добавления ссылок и на вкладке .NET выберите библиотечную сборку Microsoft.Xna.Framework.dll версии 3.0
  • В этой библиотеке находится множество компонентов 3D -графики, размещенные в нескольких пространствах имен, в том числе в интересующем нас пространстве имен Microsoft.Xna.Framework.Graphics

    Прежде всего воспользуемся классом GraphicsDevice, инкапсулирующим средства управления графическим устройством - видеокартой компьютера. Конструктор этого класса описывается так

    public GraphicsDevice(Microsoft.Xna.Framework.Graphics.GraphicsAdapter adapter,
      Microsoft.Xna.Framework.Graphics.DeviceType deviceType, System.IntPtr renderWindowHandle,
    Microsoft.Xna.Framework.Graphics.PresentationParameters presentationParameters)
  • adapter - статическое свойство класса GraphicsAdapter, соответствующее выбранной для работы видеокарте. Современные компьютеры содержат несколько видеокарт, поэтому проще всего выбрать свойства GraphicsAdapter.DefaultAdapter
  • deviceType - тип устройства, задаваемый перечислением DeviceType, которое имеет значения
  • Hardware - реальное аппаратное устройство
  • Reference - реальное устройство эмулируется средствами DirectX SDK. Обычно используется для эмуляции функциональности, не поддерживаемой текущей видеокартой. Такую эмуляцию реально использовать лишь в отладочных целях, так как производительность приложений в этом режиме редко превышает один кадр в секунду
  • NullReference - игнорируются все поступающие команды и используется для оценки производительности приложения с имитацией бесконечно быстрой видеокарты
  • renderWindowHandle - дескриптор окна или элемента управления (например, Panel ), который будет использоваться для вывода графических изображений
  • presentationParameters - экземпляр класса PresentationParameters, свойства которого определяют настройки представления изображения на экране
  • Для создания и настройки графического устройства нужно объявить в классе MainForm поле, зарегистрировать для события Load формы обработчик, в котором создать экземпляр устройства и настроить его с помощью свойств соответствующих объектов.

  • Вызовите контекстное меню формы MainForm, щелкнув на ней правой кнопкой мыши, и выполните команду View Code
  • Подключите иснструкцией using к файлу MainForm.cs пространство имен Microsoft.Xna.Framework.Graphics
  • Объявите в классе MainForm поле типа GraphicsDevice с именем device
  • 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);
            }
        
            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);
            }
        }
    }

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

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

  • Устройство будет использовать видеоадаптер по умолчанию
  • Устройство будет аппаратным
  • Вывод будет осуществляться в оконном режиме на клиентскую поверхность текущей формы
  • Обработка вершин будет осуществляться средствами GPU - графического процессора видеокарты с двойной буферизацией
  • Очистка буферов графического устройства

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

    public void Clear(Microsoft.Xna.Framework.Graphics.Color color)

    Куда же вставить вызов функции очистки графического устройства? Когда происходит разрушение экрана за счет сворачивания окна или заслонения другими окнами, то при необходимости его восстановления операционная система генерирует событие Paint унаследованного формой класса Control и мы можем либо подписать обработчик на это событие и туда вставить очистку, либо перекрыть виртуальный метод диспетчеризации OnPaint() формы и в него вставить вызов очистки. Воспользуемся вторым вариантом...

  • В классе MainForm как член класса наберите ключевое слово override, после введения символа пробела в открывшемся окне подсказчика кода IntalliSense выберите метод диспетчеризации OnPaint() и завершите набор нажатием клавиши Enter
  • В перекрытом методе диспетчеризации OnPaint() введите код вызова метода очистки устройства
  • 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);
            }
        
            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);
            }
        }
    }

    Обратите внимание, что мы были вынуждены прописать полное имя структуры Color во избежании конфликта имен, поскольку точно такая же структура находится в подключенном к файлу пространстве имен System.Drawing.

    При завершении работы нашего приложения сборщик мусора GC среды исполнения CLR попытается автоматически освободить графическое устройство. Но сделать это корректно ему не удастся. Дело в том, классы XNA являются тонкой оболочкой API DirectX, а сам DirectX освобождает только те объекты, которые были созданы в его потоке. Сборщик мусора же для своей работы создает новый отдельный поток, который не сможет корректно удалить объекты DirectX, в том числе созданный нами объект device. В связи с этим нам нужно самим позаботиться об удалении таких критичных объектов в нашем приложении.

  • В конструкторе MainForm() одноименного класса подпишитесь на обработчик события FormClosed и заполните обработчик кодом удаления устройства device
  • 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);
            }
        }
    }
  • Запустите приложение на выполнение
  • Пока ничего не произошло и мы не видим графического окна в клиентской области формы, хотя событие Paint исправно срабатывает, в чем можно убедиться, если установить точку останова (Breakpoint) в перекрытом методе диспетчеризации OnPaint().

    Всe дело в том, что мы используем двойную буферизацию, то есть наше приложение выполняет все графические построения в заднем невидимом вспомогательном буфере. После окончания визуализации необходимо скопировать информацию из этого вспомогательного буфера на форму. Эту операцию выполняет метод Present() класса GraphicsDevice.

  • Добавьте вызов метода Present() в перекрытый метод диспетчеризации OnPaint()
  • protected override void OnPaint(PaintEventArgs e)
    {
        device.Clear(Microsoft.Xna.Framework.Graphics.Color.CornflowerBlue);
        
        // Копируем задний буфер на экран
        device.Present();
        
        base.OnPaint(e);
    }
  • Снова запустите программу на выполнение - на экране появится окно, клиентская область которого закрашена синим цветом, что нам и требовалось
  • Избирательная очистка буферов

    Теперь попробуем применить более сложную перегрузку метода Clear() для избирательной очистки экрана и нарисовать с ее помощью шахматную доску:

    public void Clear(Microsoft.Xna.Framework.Graphics.ClearOptions options, Microsoft.Xna.Framework.Graphics.Color
    color, float depth, int stencil, Microsoft.Xna.Framework.Rectangle[] regions)
  • options - набор битовых флагов, указывающих, какие буферы необходимо очистить. Для очистки экранного буфера используется флаг Target перечисления ClearOptions. Остальные флаги DepthBuffer и Stencil этого перечисления используются для очистки соответственно буфера глубины и буфера шаблона, и будут рассмотрены далее
  • color - цвет, которым будет закрашен буфер. Задаeтся с использованием структуры Microsoft.Xna.Framework.Graphics.Color, являющейся аналогом структуры System.Drawing.Color. Необходимость дублирования обусловлено стремлением разработчиков Microsoft сделать XNA Framework переносимой на другие платформы
  • depth - значение, которым будет заполнен буфер глубины
  • stencil - значение, которым будет заполнен буфер шаблона
  • regions - массив структур Microsoft.Xna.Framework.Rectangle, задающих прямоугольные области экрана, которые должны быть очищены. Области экрана задаются в оконных координатах клиентской области формы - начало координат расположено в ее левом верхнем углу
  • Для рисования шахматной доски вначале мы очистим экран белым цветом, затем создим массив областей экрана, соответствующих клеткам доски коричневого цвета и ещe раз очистим экран, но уже коричневым цветом.

  • Модифицируйте перекрытый метод OnPaint() следующим образом
  • protected override void OnPaint(PaintEventArgs e)
    {
        // Очищаем экран белым цветом
        device.Clear(Microsoft.Xna.Framework.Graphics.Color.WhiteSmoke);
        
        // Создаeм массив областей закраски, соответствующих коричневым клеткам
        Microsoft.Xna.Framework.Rectangle[] rects = new Microsoft.Xna.Framework.Rectangle[32];
        int k = 0; // Счетчик элементов массива 
        // Перебираем коричневые клетки шахматной доски 
        for (int j = 0; j < 8; j++) // Строки шахматной доски
            for (int i = j % 2; i < 8; i += 2)  // Столбцы шахматной доски
            {
                // Заносим в массив параметры рисования очередной клетки
                rects[k] = new Microsoft.Xna.Framework.Rectangle(
                   i * this.ClientSize.Width / 8,  // Отступ по горизонтали
                   j * this.ClientSize.Height / 8, // Отступ по вертикали
                   this.ClientSize.Width / 8,      // Ширина клетки
                   this.ClientSize.Height / 8);    // Высота клетки
        
                k++;    // Увеличиваем счетчик
            }
        
            // Закрашиваем все области из массива rects коричневым цветом
            device.Clear(ClearOptions.Target, Microsoft.Xna.Framework.
               Graphics.Color.Brown, 0.0f, 0, rects);
        
            // Копируем задний буфер на экран
            device.Present();
        
            base.OnPaint(e);
    }
  • Запустите приложение - на экране появится окно, раскрашенное как шахматная доска
  • Устранение скрытых дефектов приложения

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

    Начнем с того, что при уменьшении окна необходимости в дорисовке экрана нет и событие Paint наследуемого формой класса Control не срабатывает. В принципе, для борьбы с этим недоразумением мы могли бы перекрыть метод диспетчеризации OnResize() формы (или подписаться на событие Resize ) и вставить туда вызов метода Invalidate(), принудительно генерирующего событие Paint. Однако существует гораздо более элегантное решение: если установить у формы стиль ResizeRedraw, то при изменении размера формы будет автоматически генерироваться событие Paint.

  • Добавьте в обработчик события Load установку стиля формы ResizeRedraw
  • 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);
        
        // Перерисовывать форму при изменении размеров 
        this.SetStyle(ControlStyles.ResizeRedraw, true);
    }
  • Запустите приложение и поизменяйте размеры окна
  • Теперь экран при уменьшении размеров стал перерисовываться. Но здесь стала очевидной другая проблема: при изменении размеров появились заметные мигания окна. Чтобы эти мигания стали еще заметнее, выполните следующее:

  • В рабочей области редактора кода вызовите контекстное меню и выполните команду View Designer (или просто выполните команду меню оболочки View/Designer )
  • Выделите форму щелчком мыши и в панели Properties установите свойство BackColor формы в значение Green
  • Запустите приложение и поизменяйте размеры окна - сейчас мигания зеленого цвета на фоне шахматной доски стали более заметны
  • Дело здесь в том, что перед каждым вызовом события Paint автоматически вызывается виртуальный метод OnPaintBackground() наследуемого формой класса Control, который очищает экран цветом BackColor. Эта функциональность позволяет разработчику, использующему обычный GDI+, не заботится об очистке экрана, однако в нашем случае такая "самовольная" очистка формы стандартными средствами Windows приводит лишь к мерцанию. Следовательно, нам необходимо запретить форме закрашивать экран перед вызовом обработчика события Paint. Это делается установкой для формы стиля Opaque.

  • Добавьте в обработчик события Load установку стиля формы Opaque
  • 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);
        
        // Перерисовывать форму при изменении размеров 
        this.SetStyle(ControlStyles.ResizeRedraw, true);
        // Запретить очистку экрана средствами GDI+ 
        this.SetStyle(ControlStyles.Opaque, true);
    }
  • Запустите приложение - теперь мигания формы при изменении ее размеров исчезли
  • И так, первую проблему мы решили, но осталась вторая, гораздо более неприятная - некорректное масштабирование шахматной доски при изменении размера окна. Дело в том, что при создании графического устройства мы задаeм размер вспомогательного буфера, используемого при двойной буферизации, равным размеру клиентской области окна и к тому же только один раз (в обработчике события Load формы). Когда мы изменяем размер окна, его клиентская область так же изменяется. А вот размер вспомогательного буфера графического устройства остаeтся неизменным. Получается, что при изменении размеров окна происходит рассинхронизация между размерами клиентской области окна просмотра и вспомогательного буфера-источника, в котором рисуется изображение. В результате, приложение начинает работать некорректно.

    Чтобы синхронно подстраивать параметры графического устройства под новые размеры окна, нужно перекрыть метод диспетчеризации OnResize() формы, включить в него новые настройки объекта presentParams и перезапустить объект device его методом Reset(). Но прежде всего нужно ссылку на объект presentParams сделать видимой в методе диспетчеризации OnResize(), т.е. вынести ее объявление в поле класса.

  • На основе вышесказанного внесите следующие изменения в класс MainForm
  • 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;
            // Объявим объект настроек графического устройства 
            PresentationParameters presentParams;
        
            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 = 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);
        
                // Перерисовывать форму при изменении размеров 
                this.SetStyle(ControlStyles.ResizeRedraw, true);
                // Запретить очистку экрана средствами GDI+ 
                this.SetStyle(ControlStyles.Opaque, true);
            }
        
            protected override void OnResize(EventArgs e)
            {
                // Устанавливаем размеры заднего буфера по клиентской области окна формы
                presentParams.BackBufferWidth = this.ClientSize.Width;
                presentParams.BackBufferHeight = this.ClientSize.Height;
                // Сбрасываем устройство и применяем к нему новые параметры        
                device.Reset(presentParams);
        
                base.OnResize(e);
            }
        
            protected override void OnPaint(PaintEventArgs e)
            {
                // Очищаем экран белым цветом
                device.Clear(Microsoft.Xna.Framework.Graphics.Color.WhiteSmoke);
        
                // Создаeм массив областей закраски, соответствующих коричневым клеткам
                Microsoft.Xna.Framework.Rectangle[] rects = new Microsoft.Xna.Framework.Rectangle[32];
                int k = 0; // Счетчик элементов массива 
                // Перебираем коричневые клетки шахматной доски 
                for (int j = 0; j < 8; j++) // Строки шахматной доски
                    for (int i = j % 2; i < 8; i += 2)  // Столбцы шахматной доски
                    {
                        // Заносим в массив параметры рисования очередной клетки
                        rects[k] = new Microsoft.Xna.Framework.Rectangle(
                            i * this.ClientSize.Width / 8,  // Отступ по горизонтали
                            j * this.ClientSize.Height / 8, // Отступ по вертикали
                            this.ClientSize.Width / 8,      // Ширина клетки
                            this.ClientSize.Height / 8);    // Высота клетки
        
                        k++;    // Увеличиваем счетчик
                    }
        
                // Закрашиваем все области из массива rects коричневым цветом
                device.Clear(ClearOptions.Target, Microsoft.Xna.Framework.
                    Graphics.Color.Brown, 0.0f, 0, rects);
        
                // Копируем задний буфер на экран
                device.Present();
        
                base.OnPaint(e);
            }
        }
    }

    Операция сброса устройства, выполняемая при вызове метода Reset(), является очень медленной операция. Никогда не вставляйте еe без причины в обработчик события Paint или перекрытый метод диспетчеризации OnPaint(). Они вызываются очень часто и это приведeт к заметному падению производительности. В нашем случае эта операция вставлена в перекрытый метод диспетчеризации OnResize() и выполняется только при изменении пользователем размеров формы, что бывает редко.

  • Выполните приложение - теперь рисование при изменениях окна работает нормально
  • Но здесь скрыта еще одна проблема. Если уменьшить вертикальный размер клиентской области формы до нуля (уменьшить горизонтальный размер до нуля мешают системные кнопки и заголовок окна), то приложение завершится аварийно именно при попытке установки нулевых размеров заднего буфера в графическом устройстве. Для решения этой проблемы нужно ограничить минимальный размер клиентской области хотя бы одним пикселом. Это легко можно сделать при помощи свойства MinimumSize формы, которое задает минимальные размеры окна (но не клиентской области!).

    Задание размеров окна в целом (вместе с заголовком и рамкой) в режиме проектирования через панель Properties будет не точным. Размер клиентской области, который отведет система при заданном размере окна в целом, зависит от множества факторов: установленной операционной системы, пользовательских настроек и т.п. Правилнее устанавливать значение свойства MinimumSize программно посредством метода формы SizeFromClientSize(), который возвращает размер окна при заданном размере именно клиентской области.
  • Добавьте в обработчик события Load следующий код, устанавливающий минимально допустимый для изменения размер окна при заданном размере клиентской области 1x1 пикселов
  • void MainForm_Load(object sender, EventArgs e)
    {
        // Создаем объект представления для настройки графического устройства
        //PresentationParameters presentParams = new 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);
        
        // Перерисовывать форму при изменении размеров 
        this.SetStyle(ControlStyles.ResizeRedraw, true);
        // Запретить очистку экрана средствами GDI+ 
        this.SetStyle(ControlStyles.Opaque, true);
        
        // Вычисляем размер окна при размерах клиентской области 1x1 пикселов.
        // Полученное значение присваиваем свойству MinimumSize 
        this.MinimumSize = this.SizeFromClientSize(new Size(1, 1));
    }

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

  • Добавьте в перекрытый метод диспетчеризации OnResize() следующую проверку состояния окна
  • protected override void OnResize(EventArgs e)
    {
        // Устанавливаем размеры заднего буфера по клиентской области окна формы
        presentParams.BackBufferWidth = this.ClientSize.Width;
        presentParams.BackBufferHeight = this.ClientSize.Height;
      
        // Сбрасываем устройство и применяем к нему новые параметры  
        // Если окно не минимизировано
        if (this.WindowState != FormWindowState.Minimized)  
            device.Reset(presentParams);
        
        base.OnResize(e);
    }
  • Запустите приложение, минимизируйте окно - приложение продолжает устойчиво работать!
  • Потеря информации графическим устройством

    Операционная система Windows является многозадачной системой, в которой одновременно могут выполняться несколько задач и делить между собой одни и те же ресурсы. Существует опасность, что настройки графического устройства, установленные нашим приложением XNA, в любой момент могут сбиться другим подобным приложением. Такая ситуация называется потерей устройства (Device Lost). Это легко проверить, если при запущенном приложении XNA попытаться изменить разрешение экрана - приложение завершится аварийно.

    Отсюда возникает задача о восстановлении потерянной информации, рассмотрением которой мы сейчас и займемся. Информацию о состоянии, в котором в данный момент находится устройство, можно получить из свойства GraphicsDeviceStatus нашего объекта (экземпляра класса GraphicsDevice ), которое имеет одно из значений одноименного перечисления GraphicsDeviceStatus:

  • Normal - графическое устройство работает нормально
  • NotReset - графическое устройство потеряно, но может быть восстановлено методом Reset() объекта устройства
  • Lost - графическое устройство потеряно и пока не может быть восстановлено
  • При восстановимой потере устройства приложение выдает исключение Microsoft.Xna.Framework.Graphics.DeviceNotResetException, а при невосстановимой - Microsoft.Xna.Framework.Graphics.DeviceLostException. Если сгенерировано исключение DeviceNotResetException, то приложение должно восстановить устройство методом device.Reset() и снова перерисовать изображение путeм вызова метода Invalidate(), в противном случае просто не реагировать на событие Paint, требующее обновить экран.

  • На основе вышесказанного модифицируйте обработку события Paint следующим образом
  • bool closing = false;
            protected override void OnPaint(PaintEventArgs e)
            {
    
                if (closing) return;
        
                try     // Попытка
                {
                    // Очищаем экран белым цветом
                    device.Clear(Microsoft.Xna.Framework.Graphics.Color.WhiteSmoke);
        
                    // Создаeм массив областей закраски, соответствующих коричневым клеткам
                    Microsoft.Xna.Framework.Rectangle[] rects = new Microsoft.Xna.Framework.Rectangle[32];
                    int k = 0; // Счетчик элементов массива 
                    // Перебираем коричневые клетки шахматной доски 
                    for (int j = 0; j < 8; j++) // Строки шахматной доски
                        for (int i = j % 2; i < 8; i += 2)  // Столбцы шахматной доски
                        {
                            // Заносим в массив параметры рисования очередной клетки
                            rects[k] = new Microsoft.Xna.Framework.Rectangle(
                                i * this.ClientSize.Width / 8,  // Отступ по горизонтали
                                j * this.ClientSize.Height / 8, // Отступ по вертикали
                                this.ClientSize.Width / 8,      // Ширина клетки
                                this.ClientSize.Height / 8);    // Высота клетки
        
                            k++;    // Увеличиваем счетчик
                        }
        
                    // Закрашиваем все области из массива rects коричневым цветом
                    device.Clear(ClearOptions.Target, Microsoft.Xna.Framework.
                        Graphics.Color.Brown, 0.0f, 0, rects);
        
                    // Копируем задний буфер на экран
                    device.Present();
        
                    base.OnPaint(e);
                }
                // Откаты
                catch (DeviceNotResetException) // Устройство можно восстановить
                {
                    device.Reset(presentParams);
                    this.Invalidate();
                }
                catch (DeviceLostException)     // Устройство нельзя восстановить
                {
                    closing = true;     // Больше не выполнять OnPaint()
                    string title = "Сбой графического устройства";
                    string message = "Работа программы будет завершена.\n"
                                   + "Закройте все ненужные программы\n"
                                   + "и повторите запуск этого приложения";
                    MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    Application.Idle += new EventHandler(Application_Idle);
                    return;
                }
                catch (Exception ext)           // Все другие исключения
                {
                    closing = true;
                    MessageBox.Show(ext.Message);
                    Application.Idle += Application_Idle1;
                }
            }
        
            void Application_Idle(object sender, EventArgs e)
            {
                this.Close();   // Закрывает окно, у нас оно же и главное окно приложения
            }
        
            void Application_Idle1(object sender, EventArgs e)
            {
                Application.Exit(); // Завершает работу приложения (приведено для разнообразия)
            }
  • Запустите приложение и попробуйте изменить разрешение экрана, чтобы поймать исключение невосстановимой потери устройства
  • В этом случае будет выдано окно с сообщением

    Упражнение 2. Вывод графической информации XNA на пользовательский элемент управления

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

    Все элементы управления наследуют от класса Control, в том числе класс Form и Panel. Ранее при управлении экземпляром класса Form мы фактически использовали методы и свойства класса Control. В данном упражнении мы с таким же успехом можем управлять экземпляром класса Panel, помещенным на форму.

    Единственная сложность состоит в том, что при управлении экземпляром Form наш код размещался внутри его расширения - классе MainForm, и имел доступ ко всем общедоступным ( public ) и защищенным ( protected ) свойствам и методам, унаследованным, в конечном итоге, от класса Control. А если попытаться просто управлять экземпляром класса Panel, помещенным на форму, то видимыми из расширения класса Form в объекте Panel (и, соответственно, в Control ) будут только общедоступные члены. Чтобы получить полный доступ к нужным сервисам класса Control, нужно создать расширение класса Panel и упаковать в нем вызовы защищенных членов в общедоступные.

    Фактически, нам нужно создать новый элемент управления, которые называют пользовательскими (в отличии от стандартных библиотечных).

    Создание заготовки пользовательского элемента управления

  • Закройте в рабочей области оболочки все редактируемые ранее документы командой меню Window/Close All Documents
  • Командой File/Add/New Project вызовите мастер добавления в решение нового проекта с именем XNAPanel и заполните его окно так
  • Через панель Solution Explorer переименуйте автоматически созданный файл Class1.cs на XNAPanel.cs
  • Командой Project/Add Reference подключите к проекту библиотечные сборки System.Windows.Forms.dll и System.Drawing.dll, а в начало файла XNAPanel.cs вставьте инструкции:
    using System.Windows.Forms;   
      using System.Drawing;
  • Модифицируйте XNAPanel.cs следующим образом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    using System.Windows.Forms;
    using System.Drawing;
        
    namespace XNAPanel
    {
        public class XNAPanel : Panel
        {
            // Конструктор по умолчанию
            public XNAPanel()
            {
                // Изменим цвет фона панели, чтобы визуально отличить от других
                BackColor = Color.CornflowerBlue;
                // Установим минимальный размер, 
                // чтобы устранить возможное исключение
                MinimumSize = new Size(1, 1);
            }
        
            // Сделаем общедоступным нужный нам защищенный метод
            public new void SetStyle(ControlStyles flag, bool value)
            {
                base.SetStyle(flag, value);
            }
        }
    }
  • Командой меню Build/Build XNAPanel откомпилируйте данный проект
  • Мы создали библиотечный пользовательский элемент управления для вывода графической информации XNA, который теперь можно размещать на форме.

  • Командой File/Add/New Project вызовите мастер добавления в решение нового проекта с именем Application2 и заполните его окно так
  • После того, как мастер создаст новый проект и выведет визуальное представление формы в рабочую область, можно убедиться, что в панели инструментов Toolbox появилась новая вкладка XNAPanel Components с пиктограммой компонета XNAPanel.

  • Через Обозреватель решений переименуйте файл Form1.cs проекта Application2 в MainForm.cs, поменяйте заголовок окна Text="Упражнение 2" и из панели инструментов Toolbox поместите на форму экземпляр компонента SplitContainer (можно просто выполнить двойной щелчок мышью на компоненте). Убедитесь, что свойство Dock объекта splitContainer1 имеет значение Fill, т.е. что объект занимает всю клиентскую область формы
  • Установите для объекта splitContainer1 свойство BorderStyle в значение FixedSingle, чтобы создать черные рамки вокруг панелей элемента
  • Выделите курсором левую панель Panel1 объекта splitContainer1, поместите в нее из панели Toolbox экземпляр нашего компонента XNAPanel и установите его свойство Dock в значение Fill, чтобы он занял всю область объекта Panel1
  • Выделите курсором правую панель объекта splitContainer1, поместите на нее экземпляр компонента GroupBox и разверните его на всю панель установкой свойства Dock в значение Fill. Присвойте объекту groupBox1 заголовок " Параметры " через его свойство Text
  • Выделите объект groupBox1 и добавьте в него три текстовых метки Label с надписями
  • Цвет вершины №1
  • Цвет вершины №2
  • Цвет вершины №3
  • Поместите напротив этих меток три экземпляра компонента Button и присвойте им имена vertex1Color, vertex2 Color и vertex3Color
  • Выделите все три экземпляра компонента Button, очистите для них содержимое свойства Text и установите свойство Size равным ( 30 ; 20 )
  • Настройте для каждой кнопки уникальный цвет фона через свойство BackColor
  • Добавьте к форме экземпляр невизуального компонента ColorDialog, пиктограмма которого будет размещена в подвале рабочей области ( Workspace ) дизайнера формы
  • В результате форма в режиме проектирования примет следующий вид

    Средствами XNA мы нарисуем примитивный треугольник на экземпляре нашего компонента XNAPanel, цвет вершин которого мы будем выбирать из диалоговой панели. Для этого, как и ранее, нужно добавить в класс формы несколько вспомогательных полей, создать обработчики событий Load, FormClosed, Paint и т.д. и заполнить их соответствующим кодом. Но прежде, чем приступить к программированию обработчиков, познакомимся с новой техникой, которую мы будем использовать в этом упражнении для управления видеокартой на низком уровне.

    Общие сведения о примитивах

    Примитивом называется графический объект - точка, прямая линия, треугольник, которые используются для построения более сложных объектов. В XNA Framework поддерживает шесть типов примитивов, заданных структурой PrimitiveType: список точек PointList, список линий LineList, полоса линий LineStrip, список треугольников TriangleList, полоса треугольников TriangleStrip и веер треугольников TriangleFan.

    Примитивы описываются координатами точек вершин в мировой системе координат. В XNA и OpenGL используется правая система мировых координат, а в DirectX - левая. Начало мировой системы координат находится на поверхности экрана в его центре. Для наблюдателя, смотрящего на экран, ось Ox направлена по горизонтали вправо, а ось Oy - по вертикали вверх. Системы отличаются только направлением оси Oz: в правой системе координат она направлена из глубины экрана к пользователю, а в левой - от пользователя в глубину экрана.

    Совокупность трехмерных объектов в мировой системе координат называется сценой. Визуализация сцены выполняется при помощи установки камеры, размещение которой также задается относительно мировой системы координат. Положение камеры ассоциируется с положением глаз наблюдателя и с математической точки зрения определяет матрицу вида. Для корректного отображения объектов сцены на экране каждый из них предварительно преобразуется матрицей вида. Обычно камера отодвигается от экрана в положительном направлении оси Oz и несколько приподнимается вверх в положительном направлении оси Oy. Это позволяет установить взгляд наблюдателя на сцену как с балкона театра.

    В пространстве имен Microsoft.Xna.Framework.Graphics имеется ряд структур для хранения информации о вершинах примитива. В данном упражнении мы будем использовать структуру VertexPositionColor, инкапсулирующую информацию о координатах и цвете вершины.

    Напомним, что любая структура, как и класс, содержит члены-методы и члены-данные и относится к значимому типу (в противоположность к ссылочномы типу). Члены-данные струтуры (как и класса) называются полями. В структурах C# конструктор по умолчанию является предопределенным и его задавать нельзя. Это сделано для того, что объявление структурной переменной одновременно означает и создание экземпляра структуры с неявным вызовом конструктора по умолчанию. Зато параметризованных конструкторов может быть сколько угодно при условии, что каждый из них имеет уникальную в рамках структуры сигнатуру. В этом случае применение оператора new обязательно.

    Библиотечная структура VertexPositionColor имеет один параметризованный конструктор вида:

    public VertexPositionColor(Microsoft.Xna.Framework.Vector3 position,
          Microsoft.Xna.Framework.Graphics.Color color)
  • position - координаты вершины
  • color - цвет вершины
  • Информацию обо всех вершинах примитива можно хранить в массиве, например:

    VertexPositionColor[] vertices;

    Но здесь есть один нюанс. Дело в том, что при визуализации примитивов информация о вершинах напрямую передается в графический процессор видеокарты GPU (Graphics Processor Unit), который не знает, что используется массив структур именно VertexPositionColor. Для разъяснения графическому процессору формата отдельных полей структуры применяются декларации формата вершины. В XNA Framework декларация вершины задается классом VertexDeclaration, конструктор которого имеет вид:

    public VertexDeclaration(Microsoft.Xna.Framework.Graphics.GraphicsDevice graphicsDevice,
          Microsoft.Xna.Framework.Graphics.VertexElement[] elements)
  • graphicsDevice - графическое устройство, используемое для работы с вершинами
  • elements - массив элементов c описанием формата
  • Массив elements содержит информацию о формате данных, с которыми будет работать графическое устройство:

  • Адрес описываемого поля структуры (смещение от начала структуры)
  • Тип поля структуры (скаляр, вектор, упакованный вектор)
  • Информация, содержащаяся в данном поле (координаты вершины, цвет вершины, текстурные координаты и т.п.)
  • Некоторая другая служебная информация
  • Если для хранения данных применяется структура VertexPositionColor, то описание ее формата хранится в статическом поле только для чтения VertexPositionColor.VertexElements. Пользователю (программисту) остается только передать это поле конструктору класса VertexDeclaration в качестве второго параметра.

    Состав элементов сцены описывается один раз при загрузке приложения. Код загрузки описания примитивов можно разместить, например, в обработчике события Load формы. Изменение позиции объектов и их цвет, расчитываемые для каждого кадра визуализации сцены с помощью мировых матриц смещения, поворота и масштабирования, матриц проекций и вида, помещают в блок кода Update(), который выполняется каждый раз в обработчике события Paint.

    Визуализация массива примитивов осуществляется с помощью метода класса GraphicsDevice, имеющего следующее описание

    public void DrawUserPrimitives<T>(Microsoft.Xna.Framework.Graphics.PrimitiveType primitiveType, T[] vertexData,
        int vertexOffset, int primitiveCount)
  • T - шаблон типа
  • primitiveType - тип примитива, задаваемый с использованием перечисления PrimitiveType
  • vertexData - массив вершин примитива
  • vertexOffset - смещение от начала массива. Данный параметр обычно равен нулю. Ненулевые значения применяется, когда визуализируемый примитив использует примитивы лишь из части массива (например, вершины разных примитивов хранятся в одном большом общем массиве)
  • primitiveCount - количество примитивов, которые нужно выбрать для визуализации
  • Таким образом, для визуализации примитивов приложение должно выполнить примерно следующие шаги:

    В обработчике события Load:

  • Создать экземпляр presentParams класса PresentationParameters и определить в его свойствах настройки будущего графического устройства device
  • Создать само графическое устройство device типа GraphicsDevice и настроить его через свойства экземпляра presentParams
  • Создать и заполнить массив vertices структур типа VertexPositionColor[], где каждый элемент этого массива задает координаты вершины типа Vector3 и ее цвет типа Color
  • Создать декларацию формата вершины - экземпляр decl структуры VertexDeclaration, передав ее конструктору статическое поле-массив VertexPositionColor.VertexElements
  • В обработчике события Paint:

  • Передать ссылку на объект decl свойству VertexDeclaration объекта device для корректной обработки вершин при отображении сцены на экране
  • Привести размеры заднего буфера в соответствие размерам окна вывода
  • Очисть экран методом device.Clear()
  • Нарисовать набор примитивов вызовом метода device.DrawUserPrimitives()
  • Показать полученное изображение на экране, переключив буферы методом device.Present()
  • На первый взгляд операцию передачи ссылки на объект decl свойству VertexDeclaration объекта device было бы рациональнее вынести в обработчик события графического устройства теряются при сбросе методом Reset() и нужно восстанавливать информацией из decl и presentParams. И это нужно делать сразу в обработчике события Paint.

    Но это еще не все. Дело в том, что все современные видеокарты содержат специализированные GPU - векторные и пиксельные процессоры, используемые для ускорения операций преобразования вершин и закраски примитивов. Так как эти процессоры принимают участие при визуализации любых примитивов, приложение должно запрограммировать их на выполнения требуемых преобразований. Если этого не сделать, то результат вызова метода device.DrawUserPrimitives() будет непредсказуемым. Отсюда возникает необходимость программирования вершинных и пиксельных процессоров графического ускорителя видеокарты.

    Введение в HLSL

    Для программирования вершинных и пиксельных процессоров GPU служит язык HLSL (High Level Shader Language - язык высокого уровня для программирования шейдеров). Его разработала Microsoft в 2002 году. Программа для вершинного процессора называется вершинным шейдером ( VS ), а для пиксельного процессора - пиксельным шейдером ( PS ) . Классы библиотеки XNA Framework при работе с шейдерами на платформе Windows в значительной степени опирается на функциональность DirectX.

    Язык HLSL хотя и тесно связан с архитектурой графического процессора, но является управляемым C -подобным языком программирования для некоторого виртуального процессора, приближенного к некоторому реальному прототипу. Такой подход напоминает применение промежуточного языка IL в .NET. Компиляция шейдера в систему команд физического процессора происходит непосредственно перед загрузкой шейдера в GPU. Это позволяет несколько абстрагироваться от аппаратной части процессора, генерируя при компиляции промежуточный байт-код, и таким образом расширить семейство видеокарт, на которых может работать графическое приложение.

    На первой ступени вершины обрабатываются вершинным процессором по программе, называемой вершинным шейдером. На выходе из вершинного процессора получаются так называемые трансформированные (преобразованные) вершины. К вершинам могут быть "привязаны" различные параметры: цвет вершины, текстурные координаты и так далее. Координаты трансформированных вершин задаются в логической системе однородных координат, называемой clip space.

    Однородные координаты вершины определяются четырьмя числами: (x, y, z, w). Перевод однородных координат в обычные геометрические осуществляется путем деления первых трех компонентов на четвертый компонент w: (x/w, y/w, z/w). Например, вершине с однородными координатами (1, 2, 3, 4) в трехмерном пространстве соответствует точка с координатами (1/4, 2/4, 3/4). Использование четвертого компонента обусловлено рядом особенностей алгоритмов визуализации трехмерных изображений, используемых в 3D -графике.

    При визуализации двухмерных изображений компонент w обычно полагают равным 1. В этом случае нижнему левому углу клиентской области формы соответствует точка с координатами (-1, -1, 0, 1), правому верхнему углу клиентской области - (1, 1, 0, 1), а центру клиентской области - соответственно (0, 0, 0, 1).

    На второй ступени графического конвейера видеокарта производит преобразование координат вершины из логической системы координат в оконную. По умолчанию координаты трансформируются таким образом, чтобы растянуть изображение на всю поверхность элемента управления. Managed DirectX через код XNA позволяет программисту задавать координаты вершин в оконных координатах. В этом случае, при вызове метода device.DrawUserPrimitives() вершины сразу поступают на третью стадию графического конвейера, минуя первую и вторую стадии.

    На третьей ступени идет сборка примитивов. На этой стадии вершины объединяются в примитивы. Тип примитивов определяется первым параметром метода device.DrawUserPrimitives(). Так при использовании параметра PrimitiveType.TriangleStrip вершины трактуются, как опорные точки полосы треугольников. При этом каждый треугольник из полосы является независимым примитивов и обрабатывается независимо от других треугольников этой полосы.

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

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

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

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

    Известным инструментом для разработки шейдеров является RenderMonkey компании AMD. Этот продукт можно скачать по адресу (http://developer.amd.com/gpu/rendermonkey/Pages/default.aspx).

    Типы данных HLSL

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

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

    Задавая тип скаларной переменной, мы просто указываем компилятору свое пожелание. Если текущий ускоритель не поддерживает некоторые типы данных, используемые в программе, то при компиляции шейдера в машинный код они будут заменены ближайшими аналогами. Например, тип double может быть заменен на тип float, half или какой-нибудь иной внутренний тип. Поэтому программист должен стараться избегать жесткой привязки к точности и допустимому диапазону значений используемого типа данных. Особенно это актуально для типа int, так как подавляющее большинство современных ускорителей не поддерживают тип int, в результате чего он эмулируется посредством одного из вещественных типов. Подобная эмуляция приводит к существенному падению производительности шейдера.

    Большинство данных, используемых в трехмерной графике, является векторами размерности от 2 до 4 скаларных переменных. Так, координаты точки в трехмерном пространстве задаются трехмерным вектором, цвет пиксела - четырехмерным вектором (три цвета и альфа-канал). Соответственно, все современные GPU являются векторными процессорами, способными одновременно выполнять одну операцию сразу над набором из четырех чисел (четырехмерным вектором).

    Для описание векторных данных в HLSL имеются векторные типы размерностью от 2 до 4. Если сказать обобщенно, то векторный тип из N элементов скалярного типа type задается с использованием синтаксиса, отдаленно напоминающего шаблоны из C#:

    vector<type, size>
  • type - имя скалярного типа: bool, int, half, float или double
  • size - размерность вектора: 1, 2, 3 или 4
  • Например, объявление переменной v, являющейся вектором из четырех чисел типа float, выполняется так:

    vector<float, 4> v;

    Но на практике обычно используется сокращенная запись

    float4 v;

    Язык HLSL позволяет инициализировать вектор двумя способами. Первый способ - перечислить значения вектора в фигурных скобках, например, присвоение четырехмерному вектору v начального значения выглядит так

    float4 v = {0.2, 0.4, 0.6, 0.8};

    Другой способ - создать новый вектор с использованием конструктора и присвоить его вектору v:

    float4 v = float4(0.2, 0.4, 0.6, 0.8);

    Любой вектор имеет множество конструкторов, которые могут принимать в качестве параметров как переменные скалярных типов, так и - векторных. Единственное ограничение: общее количество всех компонентов векторных и скалярных типов должно быть равно N. Подобное многообразие конструкторов дает программисту большую гибкость при инициализации векторов. Например, вот как последовательно можно создать векторы разных размерностей:

    float2 a = {0.1, 0.2}; // Создаем двухмерный вектор  
        float2 b = float2(0.3, 0.4); // Создаем еще один двухмерный  
        float3 c = float3(b, 1.0); // Создаем трехмерный вектор   
        float4 d = float4(0.7, c); // Создаем четырехмерный вектор   
        float4 e = float4(a, b); // Создаем еще один четырехмерный вектор

    Функции и семантики 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();
            }

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

    Визуализация объекта, использующего эффект

    Визуализация примитивов, использующих эффект, начинается с вызова метода effect.Begin() объекта эффекта. Далее приложение должно перебрать все проходы текущей техники (коллекция effect.CurrentTechnique.Passes ) и вызвать метод pass.Begin() для каждого прохода. Внутри прохода нужно визуализировать примитивы с использованием метода device.DrawUserPrimitives(). Визуализацию прохода нужно закончить вызовом метода pass.End(). По окончанию визуализации всего эффекта приложение должно вызвать метод effect.End().

  • Через контекстное меню перейдите в режим View Designer формы, выделите на ней объект xnaPanel1 и откройте панель Properties в режиме Events
  • Найдите событие Paint объекта xnaPanel1 и двойным щелчком на его поле справа создайте обработчик, который заполните так
  • private void xnaPanel1_Paint(object sender, PaintEventArgs e)
            {
                if (closing)
                    return;
        
                try
                {
                    if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.Lost)
                        throw new DeviceLostException();
        
                    if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.NotReset)
                        device.Reset(presentParams);
        
                    // Очищаем область экрана
                    device.Clear(XNAGraphics.Color.CornflowerBlue);
        
                    device.RenderState.CullMode = CullMode.None;
                    device.VertexDeclaration = decl;
        
                    // Задаем координаты вершин из установленного в группе панелей цвета
                    vertices[0] = new VertexPositionColor(new Vector3(0.0f, 0.8f, 0.0f), new XNAGraphics.Color(
                        vertex1Color.BackColor.R, vertex1Color.BackColor.G, vertex1Color.BackColor.B));
                    vertices[1] = new VertexPositionColor(new Vector3(0.8f, -0.8f, 0.0f), new XNAGraphics.Color(
                        vertex2Color.BackColor.R, vertex2Color.BackColor.G, vertex2Color.BackColor.B));
                    vertices[2] = new VertexPositionColor(new Vector3(-0.8f, -0.8f, 0.0f), new XNAGraphics.Color(
                        vertex3Color.BackColor.R, vertex3Color.BackColor.G, vertex3Color.BackColor.B));
        
                    effect.Begin();
                    foreach(EffectPass pass in effect.CurrentTechnique.Passes)
                    {
                        pass.Begin();
                        device.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
                        pass.End();
                    }
                    effect.End();
        
                    device.Present();
                }
                catch (DeviceNotResetException)
                {
                    Invalidate();
                }
                catch (DeviceLostException)
                {
                    closing = true;     // Больше не выполнять OnPaint()
                    string title = "Сбой графического устройства";
                    string message = "Работа программы будет завершена.\n"
                                   + "Закройте все ненужные программы\n"
                                   + "и повторите запуск этого приложения";
                    MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    Application.Idle += new EventHandler(Application_Idle);
                    return;
                }
                catch (Exception ext)           // Все другие исключения
                {
                    closing = true;
                    MessageBox.Show(ext.Message);
                    Application.Idle += Application_Idle;
                }
            }
  • Выделите на форме объект xnaPanel1 и через панель Properties в режиме Events создайте обработчик события Resize, который заполните так
  • private void xnaPanel1_Resize(object sender, EventArgs e)
            {
                // Устанавливаем размеры заднего буфера по клиентской области панели
                presentParams.BackBufferWidth = xnaPanel1.ClientSize.Width;
                presentParams.BackBufferHeight = xnaPanel1.ClientSize.Height;
                // Сбрасываем устройство и применяем к нему новые параметры  
                // Если окно не минимизировано
                if (this.WindowState != FormWindowState.Minimized)
                    device.Reset(presentParams); 
            }
  • Выделите саму форму щелчком на ее заголовке и через панель Properties в режиме Events создайте обработчик события FormClosed, который заполните так
  • private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
            {
                //  Удаляем (освобождаем) устройство
                device.Dispose();
                // На всякий случай присваиваем ссылке на устройство значение null 
                device = null;
            }
  • Осторожно (чтобы не создать обработчик) выделите все три кнопки на форме (удерживая клавишу Ctrl ) и через панель Properties в режиме Events создайте обработчик события Click с именем vertexColor_Click, который заполните так
  • private void vertexColor_Click(object sender, EventArgs e)
            {
                Button button = (Button)sender;
                colorDialog1.Color = button.BackColor;
                if (colorDialog1.ShowDialog() == DialogResult.OK)
                {
                    button.BackColor = colorDialog1.Color;
                    xnaPanel1.Invalidate();
                }
            }
  • В панели Solution Explorer выделите узел проекта Application2 и через контекстное меню сделайте проект стартовым. Запустите приложение - должен получиться такой результат
  • Замечание. Если приложение выдаст сообщение "Ошибка при компиляции эффекта...", то добавьте к проекту файл с эффектом ColorFill.fx из прилагаемой к работе папки Source, не редактируя его. Незабудьте в этом случае изменить свойство файла Copy to Output Directory на значение Copy if newer.

    Запускающее приложение

    Если вы все делали так как описано, то исполнимые сборки для упражнений 1 и 2 должны находиться относительно папки решений FormsXNA в подпапках:

    FormsXNA\Application1\bin\Debug\Application1.exe  
      FormsXNA\Application2\bin\Debug\Application2.exe

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

  • Добавьте к решению новый проект с именем StartForm и сделайте его стартовым
  • Переименуйте файл формы Form1.cs в StartForm.cs и откройте его в режиме View Designer и поместите на форму два экземпляра компонента LinkLabel
  • Создайте для них обработчики события LinkClicked и заполните их следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    using System.IO;
        
    namespace StartApp
    {
        public partial class StartForm : Form
        {
            System.Diagnostics.Process exe = new System.Diagnostics.Process();
            String path; // Путь к папке с решением
        
            public StartForm()
            {
                InitializeComponent();
        
                path = Directory.GetCurrentDirectory();// Получаем путь к сборке StartForm 
                int pos = path.LastIndexOf(@"\bin\");
                path = path.Substring(0, pos);// Убираем 2 последних секции пути
                pos = path.LastIndexOf(@"\");
                path = path.Substring(0, pos);// Убираем еще одну секцию
            }
        
            private void linkLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                Directory.SetCurrentDirectory(path + @"\Application1\bin\Debug");// Путь к сборке
                exe.StartInfo.FileName = "Application1.exe";
                System.Diagnostics.Debug.WriteLine(path); // Для отладки в окно Output
                exe.Start();
            }
        
            private void linkLabel2_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                Directory.SetCurrentDirectory(path + @"\Application2\bin\Debug");// Путь к сборке
                exe.StartInfo.FileName = "Application2.exe";
                System.Diagnostics.Debug.WriteLine(path); // Для отладки в окно Output
                exe.Start();
            }
        }
    }

    Код будет работать при условии, что сборка стартовой формы находится в своем каталоге bin дерева решения.

    Страницы:

    Все необходимые для выполнения данной работы программы можно найти в прилагаемом каталоге.

    Работа выполнена на основе материалов С.А. Гайдукова, приведенных в источнике http://www.intuit.ru/department/se/intxna

    Упражнение 1. Вывод графической информации XNA на стандартную форму

    Создание заготовки приложения

  • Создайте новое решение и новый проект командой File/New/Project, заполнив окно мастера так
  • Переименуйте файл формы Form1.cs в MainForm.cs
  • Выделите форму в конструкторе форм и в панели Properties в свойстве Text установите заголовок окна " Упражнение 1 "
  • Командой Project/Add Reference вызовите окно добавления ссылок и на вкладке .NET выберите библиотечную сборку Microsoft.Xna.Framework.dll версии 3.0
  • В этой библиотеке находится множество компонентов 3D -графики, размещенные в нескольких пространствах имен, в том числе в интересующем нас пространстве имен Microsoft.Xna.Framework.Graphics

    Прежде всего воспользуемся классом GraphicsDevice, инкапсулирующим средства управления графическим устройством - видеокартой компьютера. Конструктор этого класса описывается так

    public GraphicsDevice(Microsoft.Xna.Framework.Graphics.GraphicsAdapter adapter,
      Microsoft.Xna.Framework.Graphics.DeviceType deviceType, System.IntPtr renderWindowHandle,
    Microsoft.Xna.Framework.Graphics.PresentationParameters presentationParameters)
  • adapter - статическое свойство класса GraphicsAdapter, соответствующее выбранной для работы видеокарте. Современные компьютеры содержат несколько видеокарт, поэтому проще всего выбрать свойства GraphicsAdapter.DefaultAdapter
  • deviceType - тип устройства, задаваемый перечислением DeviceType, которое имеет значения
  • Hardware - реальное аппаратное устройство
  • Reference - реальное устройство эмулируется средствами DirectX SDK. Обычно используется для эмуляции функциональности, не поддерживаемой текущей видеокартой. Такую эмуляцию реально использовать лишь в отладочных целях, так как производительность приложений в этом режиме редко превышает один кадр в секунду
  • NullReference - игнорируются все поступающие команды и используется для оценки производительности приложения с имитацией бесконечно быстрой видеокарты
  • renderWindowHandle - дескриптор окна или элемента управления (например, Panel ), который будет использоваться для вывода графических изображений
  • presentationParameters - экземпляр класса PresentationParameters, свойства которого определяют настройки представления изображения на экране
  • Для создания и настройки графического устройства нужно объявить в классе MainForm поле, зарегистрировать для события Load формы обработчик, в котором создать экземпляр устройства и настроить его с помощью свойств соответствующих объектов.

  • Вызовите контекстное меню формы MainForm, щелкнув на ней правой кнопкой мыши, и выполните команду View Code
  • Подключите иснструкцией using к файлу MainForm.cs пространство имен Microsoft.Xna.Framework.Graphics
  • Объявите в классе MainForm поле типа GraphicsDevice с именем device
  • 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);
            }
        
            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);
            }
        }
    }

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

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

  • Устройство будет использовать видеоадаптер по умолчанию
  • Устройство будет аппаратным
  • Вывод будет осуществляться в оконном режиме на клиентскую поверхность текущей формы
  • Обработка вершин будет осуществляться средствами GPU - графического процессора видеокарты с двойной буферизацией
  • Очистка буферов графического устройства

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

    public void Clear(Microsoft.Xna.Framework.Graphics.Color color)

    Куда же вставить вызов функции очистки графического устройства? Когда происходит разрушение экрана за счет сворачивания окна или заслонения другими окнами, то при необходимости его восстановления операционная система генерирует событие Paint унаследованного формой класса Control и мы можем либо подписать обработчик на это событие и туда вставить очистку, либо перекрыть виртуальный метод диспетчеризации OnPaint() формы и в него вставить вызов очистки. Воспользуемся вторым вариантом...

  • В классе MainForm как член класса наберите ключевое слово override, после введения символа пробела в открывшемся окне подсказчика кода IntalliSense выберите метод диспетчеризации OnPaint() и завершите набор нажатием клавиши Enter
  • В перекрытом методе диспетчеризации OnPaint() введите код вызова метода очистки устройства
  • 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);
            }
        
            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);
            }
        }
    }

    Обратите внимание, что мы были вынуждены прописать полное имя структуры Color во избежании конфликта имен, поскольку точно такая же структура находится в подключенном к файлу пространстве имен System.Drawing.

    При завершении работы нашего приложения сборщик мусора GC среды исполнения CLR попытается автоматически освободить графическое устройство. Но сделать это корректно ему не удастся. Дело в том, классы XNA являются тонкой оболочкой API DirectX, а сам DirectX освобождает только те объекты, которые были созданы в его потоке. Сборщик мусора же для своей работы создает новый отдельный поток, который не сможет корректно удалить объекты DirectX, в том числе созданный нами объект device. В связи с этим нам нужно самим позаботиться об удалении таких критичных объектов в нашем приложении.

  • В конструкторе MainForm() одноименного класса подпишитесь на обработчик события FormClosed и заполните обработчик кодом удаления устройства device
  • 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);
            }
        }
    }
  • Запустите приложение на выполнение
  • Пока ничего не произошло и мы не видим графического окна в клиентской области формы, хотя событие Paint исправно срабатывает, в чем можно убедиться, если установить точку останова (Breakpoint) в перекрытом методе диспетчеризации OnPaint().

    Всe дело в том, что мы используем двойную буферизацию, то есть наше приложение выполняет все графические построения в заднем невидимом вспомогательном буфере. После окончания визуализации необходимо скопировать информацию из этого вспомогательного буфера на форму. Эту операцию выполняет метод Present() класса GraphicsDevice.

  • Добавьте вызов метода Present() в перекрытый метод диспетчеризации OnPaint()
  • protected override void OnPaint(PaintEventArgs e)
    {
        device.Clear(Microsoft.Xna.Framework.Graphics.Color.CornflowerBlue);
        
        // Копируем задний буфер на экран
        device.Present();
        
        base.OnPaint(e);
    }
  • Снова запустите программу на выполнение - на экране появится окно, клиентская область которого закрашена синим цветом, что нам и требовалось
  • Избирательная очистка буферов

    Теперь попробуем применить более сложную перегрузку метода Clear() для избирательной очистки экрана и нарисовать с ее помощью шахматную доску:

    public void Clear(Microsoft.Xna.Framework.Graphics.ClearOptions options, Microsoft.Xna.Framework.Graphics.Color
    color, float depth, int stencil, Microsoft.Xna.Framework.Rectangle[] regions)
  • options - набор битовых флагов, указывающих, какие буферы необходимо очистить. Для очистки экранного буфера используется флаг Target перечисления ClearOptions. Остальные флаги DepthBuffer и Stencil этого перечисления используются для очистки соответственно буфера глубины и буфера шаблона, и будут рассмотрены далее
  • color - цвет, которым будет закрашен буфер. Задаeтся с использованием структуры Microsoft.Xna.Framework.Graphics.Color, являющейся аналогом структуры System.Drawing.Color. Необходимость дублирования обусловлено стремлением разработчиков Microsoft сделать XNA Framework переносимой на другие платформы
  • depth - значение, которым будет заполнен буфер глубины
  • stencil - значение, которым будет заполнен буфер шаблона
  • regions - массив структур Microsoft.Xna.Framework.Rectangle, задающих прямоугольные области экрана, которые должны быть очищены. Области экрана задаются в оконных координатах клиентской области формы - начало координат расположено в ее левом верхнем углу
  • Для рисования шахматной доски вначале мы очистим экран белым цветом, затем создим массив областей экрана, соответствующих клеткам доски коричневого цвета и ещe раз очистим экран, но уже коричневым цветом.

  • Модифицируйте перекрытый метод OnPaint() следующим образом
  • protected override void OnPaint(PaintEventArgs e)
    {
        // Очищаем экран белым цветом
        device.Clear(Microsoft.Xna.Framework.Graphics.Color.WhiteSmoke);
        
        // Создаeм массив областей закраски, соответствующих коричневым клеткам
        Microsoft.Xna.Framework.Rectangle[] rects = new Microsoft.Xna.Framework.Rectangle[32];
        int k = 0; // Счетчик элементов массива 
        // Перебираем коричневые клетки шахматной доски 
        for (int j = 0; j < 8; j++) // Строки шахматной доски
            for (int i = j % 2; i < 8; i += 2)  // Столбцы шахматной доски
            {
                // Заносим в массив параметры рисования очередной клетки
                rects[k] = new Microsoft.Xna.Framework.Rectangle(
                   i * this.ClientSize.Width / 8,  // Отступ по горизонтали
                   j * this.ClientSize.Height / 8, // Отступ по вертикали
                   this.ClientSize.Width / 8,      // Ширина клетки
                   this.ClientSize.Height / 8);    // Высота клетки
        
                k++;    // Увеличиваем счетчик
            }
        
            // Закрашиваем все области из массива rects коричневым цветом
            device.Clear(ClearOptions.Target, Microsoft.Xna.Framework.
               Graphics.Color.Brown, 0.0f, 0, rects);
        
            // Копируем задний буфер на экран
            device.Present();
        
            base.OnPaint(e);
    }
  • Запустите приложение - на экране появится окно, раскрашенное как шахматная доска
  • Устранение скрытых дефектов приложения

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

    Начнем с того, что при уменьшении окна необходимости в дорисовке экрана нет и событие Paint наследуемого формой класса Control не срабатывает. В принципе, для борьбы с этим недоразумением мы могли бы перекрыть метод диспетчеризации OnResize() формы (или подписаться на событие Resize ) и вставить туда вызов метода Invalidate(), принудительно генерирующего событие Paint. Однако существует гораздо более элегантное решение: если установить у формы стиль ResizeRedraw, то при изменении размера формы будет автоматически генерироваться событие Paint.

  • Добавьте в обработчик события Load установку стиля формы ResizeRedraw
  • 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);
        
        // Перерисовывать форму при изменении размеров 
        this.SetStyle(ControlStyles.ResizeRedraw, true);
    }
  • Запустите приложение и поизменяйте размеры окна
  • Теперь экран при уменьшении размеров стал перерисовываться. Но здесь стала очевидной другая проблема: при изменении размеров появились заметные мигания окна. Чтобы эти мигания стали еще заметнее, выполните следующее:

  • В рабочей области редактора кода вызовите контекстное меню и выполните команду View Designer (или просто выполните команду меню оболочки View/Designer )
  • Выделите форму щелчком мыши и в панели Properties установите свойство BackColor формы в значение Green
  • Запустите приложение и поизменяйте размеры окна - сейчас мигания зеленого цвета на фоне шахматной доски стали более заметны
  • Дело здесь в том, что перед каждым вызовом события Paint автоматически вызывается виртуальный метод OnPaintBackground() наследуемого формой класса Control, который очищает экран цветом BackColor. Эта функциональность позволяет разработчику, использующему обычный GDI+, не заботится об очистке экрана, однако в нашем случае такая "самовольная" очистка формы стандартными средствами Windows приводит лишь к мерцанию. Следовательно, нам необходимо запретить форме закрашивать экран перед вызовом обработчика события Paint. Это делается установкой для формы стиля Opaque.

  • Добавьте в обработчик события Load установку стиля формы Opaque
  • 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);
        
        // Перерисовывать форму при изменении размеров 
        this.SetStyle(ControlStyles.ResizeRedraw, true);
        // Запретить очистку экрана средствами GDI+ 
        this.SetStyle(ControlStyles.Opaque, true);
    }
  • Запустите приложение - теперь мигания формы при изменении ее размеров исчезли
  • И так, первую проблему мы решили, но осталась вторая, гораздо более неприятная - некорректное масштабирование шахматной доски при изменении размера окна. Дело в том, что при создании графического устройства мы задаeм размер вспомогательного буфера, используемого при двойной буферизации, равным размеру клиентской области окна и к тому же только один раз (в обработчике события Load формы). Когда мы изменяем размер окна, его клиентская область так же изменяется. А вот размер вспомогательного буфера графического устройства остаeтся неизменным. Получается, что при изменении размеров окна происходит рассинхронизация между размерами клиентской области окна просмотра и вспомогательного буфера-источника, в котором рисуется изображение. В результате, приложение начинает работать некорректно.

    Чтобы синхронно подстраивать параметры графического устройства под новые размеры окна, нужно перекрыть метод диспетчеризации OnResize() формы, включить в него новые настройки объекта presentParams и перезапустить объект device его методом Reset(). Но прежде всего нужно ссылку на объект presentParams сделать видимой в методе диспетчеризации OnResize(), т.е. вынести ее объявление в поле класса.

  • На основе вышесказанного внесите следующие изменения в класс MainForm
  • 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;
            // Объявим объект настроек графического устройства 
            PresentationParameters presentParams;
        
            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 = 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);
        
                // Перерисовывать форму при изменении размеров 
                this.SetStyle(ControlStyles.ResizeRedraw, true);
                // Запретить очистку экрана средствами GDI+ 
                this.SetStyle(ControlStyles.Opaque, true);
            }
        
            protected override void OnResize(EventArgs e)
            {
                // Устанавливаем размеры заднего буфера по клиентской области окна формы
                presentParams.BackBufferWidth = this.ClientSize.Width;
                presentParams.BackBufferHeight = this.ClientSize.Height;
                // Сбрасываем устройство и применяем к нему новые параметры        
                device.Reset(presentParams);
        
                base.OnResize(e);
            }
        
            protected override void OnPaint(PaintEventArgs e)
            {
                // Очищаем экран белым цветом
                device.Clear(Microsoft.Xna.Framework.Graphics.Color.WhiteSmoke);
        
                // Создаeм массив областей закраски, соответствующих коричневым клеткам
                Microsoft.Xna.Framework.Rectangle[] rects = new Microsoft.Xna.Framework.Rectangle[32];
                int k = 0; // Счетчик элементов массива 
                // Перебираем коричневые клетки шахматной доски 
                for (int j = 0; j < 8; j++) // Строки шахматной доски
                    for (int i = j % 2; i < 8; i += 2)  // Столбцы шахматной доски
                    {
                        // Заносим в массив параметры рисования очередной клетки
                        rects[k] = new Microsoft.Xna.Framework.Rectangle(
                            i * this.ClientSize.Width / 8,  // Отступ по горизонтали
                            j * this.ClientSize.Height / 8, // Отступ по вертикали
                            this.ClientSize.Width / 8,      // Ширина клетки
                            this.ClientSize.Height / 8);    // Высота клетки
        
                        k++;    // Увеличиваем счетчик
                    }
        
                // Закрашиваем все области из массива rects коричневым цветом
                device.Clear(ClearOptions.Target, Microsoft.Xna.Framework.
                    Graphics.Color.Brown, 0.0f, 0, rects);
        
                // Копируем задний буфер на экран
                device.Present();
        
                base.OnPaint(e);
            }
        }
    }

    Операция сброса устройства, выполняемая при вызове метода Reset(), является очень медленной операция. Никогда не вставляйте еe без причины в обработчик события Paint или перекрытый метод диспетчеризации OnPaint(). Они вызываются очень часто и это приведeт к заметному падению производительности. В нашем случае эта операция вставлена в перекрытый метод диспетчеризации OnResize() и выполняется только при изменении пользователем размеров формы, что бывает редко.

  • Выполните приложение - теперь рисование при изменениях окна работает нормально
  • Но здесь скрыта еще одна проблема. Если уменьшить вертикальный размер клиентской области формы до нуля (уменьшить горизонтальный размер до нуля мешают системные кнопки и заголовок окна), то приложение завершится аварийно именно при попытке установки нулевых размеров заднего буфера в графическом устройстве. Для решения этой проблемы нужно ограничить минимальный размер клиентской области хотя бы одним пикселом. Это легко можно сделать при помощи свойства MinimumSize формы, которое задает минимальные размеры окна (но не клиентской области!).

    Задание размеров окна в целом (вместе с заголовком и рамкой) в режиме проектирования через панель Properties будет не точным. Размер клиентской области, который отведет система при заданном размере окна в целом, зависит от множества факторов: установленной операционной системы, пользовательских настроек и т.п. Правилнее устанавливать значение свойства MinimumSize программно посредством метода формы SizeFromClientSize(), который возвращает размер окна при заданном размере именно клиентской области.
  • Добавьте в обработчик события Load следующий код, устанавливающий минимально допустимый для изменения размер окна при заданном размере клиентской области 1x1 пикселов
  • void MainForm_Load(object sender, EventArgs e)
    {
        // Создаем объект представления для настройки графического устройства
        //PresentationParameters presentParams = new 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);
        
        // Перерисовывать форму при изменении размеров 
        this.SetStyle(ControlStyles.ResizeRedraw, true);
        // Запретить очистку экрана средствами GDI+ 
        this.SetStyle(ControlStyles.Opaque, true);
        
        // Вычисляем размер окна при размерах клиентской области 1x1 пикселов.
        // Полученное значение присваиваем свойству MinimumSize 
        this.MinimumSize = this.SizeFromClientSize(new Size(1, 1));
    }

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

  • Добавьте в перекрытый метод диспетчеризации OnResize() следующую проверку состояния окна
  • protected override void OnResize(EventArgs e)
    {
        // Устанавливаем размеры заднего буфера по клиентской области окна формы
        presentParams.BackBufferWidth = this.ClientSize.Width;
        presentParams.BackBufferHeight = this.ClientSize.Height;
      
        // Сбрасываем устройство и применяем к нему новые параметры  
        // Если окно не минимизировано
        if (this.WindowState != FormWindowState.Minimized)  
            device.Reset(presentParams);
        
        base.OnResize(e);
    }
  • Запустите приложение, минимизируйте окно - приложение продолжает устойчиво работать!
  • Потеря информации графическим устройством

    Операционная система Windows является многозадачной системой, в которой одновременно могут выполняться несколько задач и делить между собой одни и те же ресурсы. Существует опасность, что настройки графического устройства, установленные нашим приложением XNA, в любой момент могут сбиться другим подобным приложением. Такая ситуация называется потерей устройства (Device Lost). Это легко проверить, если при запущенном приложении XNA попытаться изменить разрешение экрана - приложение завершится аварийно.

    Отсюда возникает задача о восстановлении потерянной информации, рассмотрением которой мы сейчас и займемся. Информацию о состоянии, в котором в данный момент находится устройство, можно получить из свойства GraphicsDeviceStatus нашего объекта (экземпляра класса GraphicsDevice ), которое имеет одно из значений одноименного перечисления GraphicsDeviceStatus:

  • Normal - графическое устройство работает нормально
  • NotReset - графическое устройство потеряно, но может быть восстановлено методом Reset() объекта устройства
  • Lost - графическое устройство потеряно и пока не может быть восстановлено
  • При восстановимой потере устройства приложение выдает исключение Microsoft.Xna.Framework.Graphics.DeviceNotResetException, а при невосстановимой - Microsoft.Xna.Framework.Graphics.DeviceLostException. Если сгенерировано исключение DeviceNotResetException, то приложение должно восстановить устройство методом device.Reset() и снова перерисовать изображение путeм вызова метода Invalidate(), в противном случае просто не реагировать на событие Paint, требующее обновить экран.

  • На основе вышесказанного модифицируйте обработку события Paint следующим образом
  • bool closing = false;
            protected override void OnPaint(PaintEventArgs e)
            {
    
                if (closing) return;
        
                try     // Попытка
                {
                    // Очищаем экран белым цветом
                    device.Clear(Microsoft.Xna.Framework.Graphics.Color.WhiteSmoke);
        
                    // Создаeм массив областей закраски, соответствующих коричневым клеткам
                    Microsoft.Xna.Framework.Rectangle[] rects = new Microsoft.Xna.Framework.Rectangle[32];
                    int k = 0; // Счетчик элементов массива 
                    // Перебираем коричневые клетки шахматной доски 
                    for (int j = 0; j < 8; j++) // Строки шахматной доски
                        for (int i = j % 2; i < 8; i += 2)  // Столбцы шахматной доски
                        {
                            // Заносим в массив параметры рисования очередной клетки
                            rects[k] = new Microsoft.Xna.Framework.Rectangle(
                                i * this.ClientSize.Width / 8,  // Отступ по горизонтали
                                j * this.ClientSize.Height / 8, // Отступ по вертикали
                                this.ClientSize.Width / 8,      // Ширина клетки
                                this.ClientSize.Height / 8);    // Высота клетки
        
                            k++;    // Увеличиваем счетчик
                        }
        
                    // Закрашиваем все области из массива rects коричневым цветом
                    device.Clear(ClearOptions.Target, Microsoft.Xna.Framework.
                        Graphics.Color.Brown, 0.0f, 0, rects);
        
                    // Копируем задний буфер на экран
                    device.Present();
        
                    base.OnPaint(e);
                }
                // Откаты
                catch (DeviceNotResetException) // Устройство можно восстановить
                {
                    device.Reset(presentParams);
                    this.Invalidate();
                }
                catch (DeviceLostException)     // Устройство нельзя восстановить
                {
                    closing = true;     // Больше не выполнять OnPaint()
                    string title = "Сбой графического устройства";
                    string message = "Работа программы будет завершена.\n"
                                   + "Закройте все ненужные программы\n"
                                   + "и повторите запуск этого приложения";
                    MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    Application.Idle += new EventHandler(Application_Idle);
                    return;
                }
                catch (Exception ext)           // Все другие исключения
                {
                    closing = true;
                    MessageBox.Show(ext.Message);
                    Application.Idle += Application_Idle1;
                }
            }
        
            void Application_Idle(object sender, EventArgs e)
            {
                this.Close();   // Закрывает окно, у нас оно же и главное окно приложения
            }
        
            void Application_Idle1(object sender, EventArgs e)
            {
                Application.Exit(); // Завершает работу приложения (приведено для разнообразия)
            }
  • Запустите приложение и попробуйте изменить разрешение экрана, чтобы поймать исключение невосстановимой потери устройства
  • В этом случае будет выдано окно с сообщением

    Упражнение 2. Вывод графической информации XNA на пользовательский элемент управления

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

    Все элементы управления наследуют от класса Control, в том числе класс Form и Panel. Ранее при управлении экземпляром класса Form мы фактически использовали методы и свойства класса Control. В данном упражнении мы с таким же успехом можем управлять экземпляром класса Panel, помещенным на форму.

    Единственная сложность состоит в том, что при управлении экземпляром Form наш код размещался внутри его расширения - классе MainForm, и имел доступ ко всем общедоступным ( public ) и защищенным ( protected ) свойствам и методам, унаследованным, в конечном итоге, от класса Control. А если попытаться просто управлять экземпляром класса Panel, помещенным на форму, то видимыми из расширения класса Form в объекте Panel (и, соответственно, в Control ) будут только общедоступные члены. Чтобы получить полный доступ к нужным сервисам класса Control, нужно создать расширение класса Panel и упаковать в нем вызовы защищенных членов в общедоступные.

    Фактически, нам нужно создать новый элемент управления, которые называют пользовательскими (в отличии от стандартных библиотечных).

    Создание заготовки пользовательского элемента управления

  • Закройте в рабочей области оболочки все редактируемые ранее документы командой меню Window/Close All Documents
  • Командой File/Add/New Project вызовите мастер добавления в решение нового проекта с именем XNAPanel и заполните его окно так
  • Через панель Solution Explorer переименуйте автоматически созданный файл Class1.cs на XNAPanel.cs
  • Командой Project/Add Reference подключите к проекту библиотечные сборки System.Windows.Forms.dll и System.Drawing.dll, а в начало файла XNAPanel.cs вставьте инструкции:
    using System.Windows.Forms;   
      using System.Drawing;
  • Модифицируйте XNAPanel.cs следующим образом
  • using System;
    using System.Collections.Generic;
    using System.Text;
        
    using System.Windows.Forms;
    using System.Drawing;
        
    namespace XNAPanel
    {
        public class XNAPanel : Panel
        {
            // Конструктор по умолчанию
            public XNAPanel()
            {
                // Изменим цвет фона панели, чтобы визуально отличить от других
                BackColor = Color.CornflowerBlue;
                // Установим минимальный размер, 
                // чтобы устранить возможное исключение
                MinimumSize = new Size(1, 1);
            }
        
            // Сделаем общедоступным нужный нам защищенный метод
            public new void SetStyle(ControlStyles flag, bool value)
            {
                base.SetStyle(flag, value);
            }
        }
    }
  • Командой меню Build/Build XNAPanel откомпилируйте данный проект
  • Мы создали библиотечный пользовательский элемент управления для вывода графической информации XNA, который теперь можно размещать на форме.

  • Командой File/Add/New Project вызовите мастер добавления в решение нового проекта с именем Application2 и заполните его окно так
  • После того, как мастер создаст новый проект и выведет визуальное представление формы в рабочую область, можно убедиться, что в панели инструментов Toolbox появилась новая вкладка XNAPanel Components с пиктограммой компонета XNAPanel.

  • Через Обозреватель решений переименуйте файл Form1.cs проекта Application2 в MainForm.cs, поменяйте заголовок окна Text="Упражнение 2" и из панели инструментов Toolbox поместите на форму экземпляр компонента SplitContainer (можно просто выполнить двойной щелчок мышью на компоненте). Убедитесь, что свойство Dock объекта splitContainer1 имеет значение Fill, т.е. что объект занимает всю клиентскую область формы
  • Установите для объекта splitContainer1 свойство BorderStyle в значение FixedSingle, чтобы создать черные рамки вокруг панелей элемента
  • Выделите курсором левую панель Panel1 объекта splitContainer1, поместите в нее из панели Toolbox экземпляр нашего компонента XNAPanel и установите его свойство Dock в значение Fill, чтобы он занял всю область объекта Panel1
  • Выделите курсором правую панель объекта splitContainer1, поместите на нее экземпляр компонента GroupBox и разверните его на всю панель установкой свойства Dock в значение Fill. Присвойте объекту groupBox1 заголовок " Параметры " через его свойство Text
  • Выделите объект groupBox1 и добавьте в него три текстовых метки Label с надписями
  • Цвет вершины №1
  • Цвет вершины №2
  • Цвет вершины №3
  • Поместите напротив этих меток три экземпляра компонента Button и присвойте им имена vertex1Color, vertex2 Color и vertex3Color
  • Выделите все три экземпляра компонента Button, очистите для них содержимое свойства Text и установите свойство Size равным ( 30 ; 20 )
  • Настройте для каждой кнопки уникальный цвет фона через свойство BackColor
  • Добавьте к форме экземпляр невизуального компонента ColorDialog, пиктограмма которого будет размещена в подвале рабочей области ( Workspace ) дизайнера формы
  • В результате форма в режиме проектирования примет следующий вид

    Средствами XNA мы нарисуем примитивный треугольник на экземпляре нашего компонента XNAPanel, цвет вершин которого мы будем выбирать из диалоговой панели. Для этого, как и ранее, нужно добавить в класс формы несколько вспомогательных полей, создать обработчики событий Load, FormClosed, Paint и т.д. и заполнить их соответствующим кодом. Но прежде, чем приступить к программированию обработчиков, познакомимся с новой техникой, которую мы будем использовать в этом упражнении для управления видеокартой на низком уровне.

    Общие сведения о примитивах

    Примитивом называется графический объект - точка, прямая линия, треугольник, которые используются для построения более сложных объектов. В XNA Framework поддерживает шесть типов примитивов, заданных структурой PrimitiveType: список точек PointList, список линий LineList, полоса линий LineStrip, список треугольников TriangleList, полоса треугольников TriangleStrip и веер треугольников TriangleFan.

    Примитивы описываются координатами точек вершин в мировой системе координат. В XNA и OpenGL используется правая система мировых координат, а в DirectX - левая. Начало мировой системы координат находится на поверхности экрана в его центре. Для наблюдателя, смотрящего на экран, ось Ox направлена по горизонтали вправо, а ось Oy - по вертикали вверх. Системы отличаются только направлением оси Oz: в правой системе координат она направлена из глубины экрана к пользователю, а в левой - от пользователя в глубину экрана.

    Совокупность трехмерных объектов в мировой системе координат называется сценой. Визуализация сцены выполняется при помощи установки камеры, размещение которой также задается относительно мировой системы координат. Положение камеры ассоциируется с положением глаз наблюдателя и с математической точки зрения определяет матрицу вида. Для корректного отображения объектов сцены на экране каждый из них предварительно преобразуется матрицей вида. Обычно камера отодвигается от экрана в положительном направлении оси Oz и несколько приподнимается вверх в положительном направлении оси Oy. Это позволяет установить взгляд наблюдателя на сцену как с балкона театра.

    В пространстве имен Microsoft.Xna.Framework.Graphics имеется ряд структур для хранения информации о вершинах примитива. В данном упражнении мы будем использовать структуру VertexPositionColor, инкапсулирующую информацию о координатах и цвете вершины.

    Напомним, что любая структура, как и класс, содержит члены-методы и члены-данные и относится к значимому типу (в противоположность к ссылочномы типу). Члены-данные струтуры (как и класса) называются полями. В структурах C# конструктор по умолчанию является предопределенным и его задавать нельзя. Это сделано для того, что объявление структурной переменной одновременно означает и создание экземпляра структуры с неявным вызовом конструктора по умолчанию. Зато параметризованных конструкторов может быть сколько угодно при условии, что каждый из них имеет уникальную в рамках структуры сигнатуру. В этом случае применение оператора new обязательно.

    Библиотечная структура VertexPositionColor имеет один параметризованный конструктор вида:

    public VertexPositionColor(Microsoft.Xna.Framework.Vector3 position,
          Microsoft.Xna.Framework.Graphics.Color color)
  • position - координаты вершины
  • color - цвет вершины
  • Информацию обо всех вершинах примитива можно хранить в массиве, например:

    VertexPositionColor[] vertices;

    Но здесь есть один нюанс. Дело в том, что при визуализации примитивов информация о вершинах напрямую передается в графический процессор видеокарты GPU (Graphics Processor Unit), который не знает, что используется массив структур именно VertexPositionColor. Для разъяснения графическому процессору формата отдельных полей структуры применяются декларации формата вершины. В XNA Framework декларация вершины задается классом VertexDeclaration, конструктор которого имеет вид:

    public VertexDeclaration(Microsoft.Xna.Framework.Graphics.GraphicsDevice graphicsDevice,
          Microsoft.Xna.Framework.Graphics.VertexElement[] elements)
  • graphicsDevice - графическое устройство, используемое для работы с вершинами
  • elements - массив элементов c описанием формата
  • Массив elements содержит информацию о формате данных, с которыми будет работать графическое устройство:

  • Адрес описываемого поля структуры (смещение от начала структуры)
  • Тип поля структуры (скаляр, вектор, упакованный вектор)
  • Информация, содержащаяся в данном поле (координаты вершины, цвет вершины, текстурные координаты и т.п.)
  • Некоторая другая служебная информация
  • Если для хранения данных применяется структура VertexPositionColor, то описание ее формата хранится в статическом поле только для чтения VertexPositionColor.VertexElements. Пользователю (программисту) остается только передать это поле конструктору класса VertexDeclaration в качестве второго параметра.

    Состав элементов сцены описывается один раз при загрузке приложения. Код загрузки описания примитивов можно разместить, например, в обработчике события Load формы. Изменение позиции объектов и их цвет, расчитываемые для каждого кадра визуализации сцены с помощью мировых матриц смещения, поворота и масштабирования, матриц проекций и вида, помещают в блок кода Update(), который выполняется каждый раз в обработчике события Paint.

    Визуализация массива примитивов осуществляется с помощью метода класса GraphicsDevice, имеющего следующее описание

    public void DrawUserPrimitives<T>(Microsoft.Xna.Framework.Graphics.PrimitiveType primitiveType, T[] vertexData,
        int vertexOffset, int primitiveCount)
  • T - шаблон типа
  • primitiveType - тип примитива, задаваемый с использованием перечисления PrimitiveType
  • vertexData - массив вершин примитива
  • vertexOffset - смещение от начала массива. Данный параметр обычно равен нулю. Ненулевые значения применяется, когда визуализируемый примитив использует примитивы лишь из части массива (например, вершины разных примитивов хранятся в одном большом общем массиве)
  • primitiveCount - количество примитивов, которые нужно выбрать для визуализации
  • Таким образом, для визуализации примитивов приложение должно выполнить примерно следующие шаги:

    В обработчике события Load:

  • Создать экземпляр presentParams класса PresentationParameters и определить в его свойствах настройки будущего графического устройства device
  • Создать само графическое устройство device типа GraphicsDevice и настроить его через свойства экземпляра presentParams
  • Создать и заполнить массив vertices структур типа VertexPositionColor[], где каждый элемент этого массива задает координаты вершины типа Vector3 и ее цвет типа Color
  • Создать декларацию формата вершины - экземпляр decl структуры VertexDeclaration, передав ее конструктору статическое поле-массив VertexPositionColor.VertexElements
  • В обработчике события Paint:

  • Передать ссылку на объект decl свойству VertexDeclaration объекта device для корректной обработки вершин при отображении сцены на экране
  • Привести размеры заднего буфера в соответствие размерам окна вывода
  • Очисть экран методом device.Clear()
  • Нарисовать набор примитивов вызовом метода device.DrawUserPrimitives()
  • Показать полученное изображение на экране, переключив буферы методом device.Present()
  • На первый взгляд операцию передачи ссылки на объект decl свойству VertexDeclaration объекта device было бы рациональнее вынести в обработчик события графического устройства теряются при сбросе методом Reset() и нужно восстанавливать информацией из decl и presentParams. И это нужно делать сразу в обработчике события Paint.

    Но это еще не все. Дело в том, что все современные видеокарты содержат специализированные GPU - векторные и пиксельные процессоры, используемые для ускорения операций преобразования вершин и закраски примитивов. Так как эти процессоры принимают участие при визуализации любых примитивов, приложение должно запрограммировать их на выполнения требуемых преобразований. Если этого не сделать, то результат вызова метода device.DrawUserPrimitives() будет непредсказуемым. Отсюда возникает необходимость программирования вершинных и пиксельных процессоров графического ускорителя видеокарты.

    Введение в HLSL

    Для программирования вершинных и пиксельных процессоров GPU служит язык HLSL (High Level Shader Language - язык высокого уровня для программирования шейдеров). Его разработала Microsoft в 2002 году. Программа для вершинного процессора называется вершинным шейдером ( VS ), а для пиксельного процессора - пиксельным шейдером ( PS ) . Классы библиотеки XNA Framework при работе с шейдерами на платформе Windows в значительной степени опирается на функциональность DirectX.

    Язык HLSL хотя и тесно связан с архитектурой графического процессора, но является управляемым C -подобным языком программирования для некоторого виртуального процессора, приближенного к некоторому реальному прототипу. Такой подход напоминает применение промежуточного языка IL в .NET. Компиляция шейдера в систему команд физического процессора происходит непосредственно перед загрузкой шейдера в GPU. Это позволяет несколько абстрагироваться от аппаратной части процессора, генерируя при компиляции промежуточный байт-код, и таким образом расширить семейство видеокарт, на которых может работать графическое приложение.

    На первой ступени вершины обрабатываются вершинным процессором по программе, называемой вершинным шейдером. На выходе из вершинного процессора получаются так называемые трансформированные (преобразованные) вершины. К вершинам могут быть "привязаны" различные параметры: цвет вершины, текстурные координаты и так далее. Координаты трансформированных вершин задаются в логической системе однородных координат, называемой clip space.

    Однородные координаты вершины определяются четырьмя числами: (x, y, z, w). Перевод однородных координат в обычные геометрические осуществляется путем деления первых трех компонентов на четвертый компонент w: (x/w, y/w, z/w). Например, вершине с однородными координатами (1, 2, 3, 4) в трехмерном пространстве соответствует точка с координатами (1/4, 2/4, 3/4). Использование четвертого компонента обусловлено рядом особенностей алгоритмов визуализации трехмерных изображений, используемых в 3D -графике.

    При визуализации двухмерных изображений компонент w обычно полагают равным 1. В этом случае нижнему левому углу клиентской области формы соответствует точка с координатами (-1, -1, 0, 1), правому верхнему углу клиентской области - (1, 1, 0, 1), а центру клиентской области - соответственно (0, 0, 0, 1).

    На второй ступени графического конвейера видеокарта производит преобразование координат вершины из логической системы координат в оконную. По умолчанию координаты трансформируются таким образом, чтобы растянуть изображение на всю поверхность элемента управления. Managed DirectX через код XNA позволяет программисту задавать координаты вершин в оконных координатах. В этом случае, при вызове метода device.DrawUserPrimitives() вершины сразу поступают на третью стадию графического конвейера, минуя первую и вторую стадии.

    На третьей ступени идет сборка примитивов. На этой стадии вершины объединяются в примитивы. Тип примитивов определяется первым параметром метода device.DrawUserPrimitives(). Так при использовании параметра PrimitiveType.TriangleStrip вершины трактуются, как опорные точки полосы треугольников. При этом каждый треугольник из полосы является независимым примитивов и обрабатывается независимо от других треугольников этой полосы.

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

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

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

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

    Известным инструментом для разработки шейдеров является RenderMonkey компании AMD. Этот продукт можно скачать по адресу (http://developer.amd.com/gpu/rendermonkey/Pages/default.aspx).

    Типы данных HLSL

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

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

    Задавая тип скаларной переменной, мы просто указываем компилятору свое пожелание. Если текущий ускоритель не поддерживает некоторые типы данных, используемые в программе, то при компиляции шейдера в машинный код они будут заменены ближайшими аналогами. Например, тип double может быть заменен на тип float, half или какой-нибудь иной внутренний тип. Поэтому программист должен стараться избегать жесткой привязки к точности и допустимому диапазону значений используемого типа данных. Особенно это актуально для типа int, так как подавляющее большинство современных ускорителей не поддерживают тип int, в результате чего он эмулируется посредством одного из вещественных типов. Подобная эмуляция приводит к существенному падению производительности шейдера.

    Большинство данных, используемых в трехмерной графике, является векторами размерности от 2 до 4 скаларных переменных. Так, координаты точки в трехмерном пространстве задаются трехмерным вектором, цвет пиксела - четырехмерным вектором (три цвета и альфа-канал). Соответственно, все современные GPU являются векторными процессорами, способными одновременно выполнять одну операцию сразу над набором из четырех чисел (четырехмерным вектором).

    Для описание векторных данных в HLSL имеются векторные типы размерностью от 2 до 4. Если сказать обобщенно, то векторный тип из N элементов скалярного типа type задается с использованием синтаксиса, отдаленно напоминающего шаблоны из C#:

    vector<type, size>
  • type - имя скалярного типа: bool, int, half, float или double
  • size - размерность вектора: 1, 2, 3 или 4
  • Например, объявление переменной v, являющейся вектором из четырех чисел типа float, выполняется так:

    vector<float, 4> v;

    Но на практике обычно используется сокращенная запись

    float4 v;

    Язык HLSL позволяет инициализировать вектор двумя способами. Первый способ - перечислить значения вектора в фигурных скобках, например, присвоение четырехмерному вектору v начального значения выглядит так

    float4 v = {0.2, 0.4, 0.6, 0.8};

    Другой способ - создать новый вектор с использованием конструктора и присвоить его вектору v:

    float4 v = float4(0.2, 0.4, 0.6, 0.8);

    Любой вектор имеет множество конструкторов, которые могут принимать в качестве параметров как переменные скалярных типов, так и - векторных. Единственное ограничение: общее количество всех компонентов векторных и скалярных типов должно быть равно N. Подобное многообразие конструкторов дает программисту большую гибкость при инициализации векторов. Например, вот как последовательно можно создать векторы разных размерностей:

    float2 a = {0.1, 0.2}; // Создаем двухмерный вектор  
        float2 b = float2(0.3, 0.4); // Создаем еще один двухмерный  
        float3 c = float3(b, 1.0); // Создаем трехмерный вектор   
        float4 d = float4(0.7, c); // Создаем четырехмерный вектор   
        float4 e = float4(a, b); // Создаем еще один четырехмерный вектор

    Функции и семантики 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();
            }

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

    Визуализация объекта, использующего эффект

    Визуализация примитивов, использующих эффект, начинается с вызова метода effect.Begin() объекта эффекта. Далее приложение должно перебрать все проходы текущей техники (коллекция effect.CurrentTechnique.Passes ) и вызвать метод pass.Begin() для каждого прохода. Внутри прохода нужно визуализировать примитивы с использованием метода device.DrawUserPrimitives(). Визуализацию прохода нужно закончить вызовом метода pass.End(). По окончанию визуализации всего эффекта приложение должно вызвать метод effect.End().

  • Через контекстное меню перейдите в режим View Designer формы, выделите на ней объект xnaPanel1 и откройте панель Properties в режиме Events
  • Найдите событие Paint объекта xnaPanel1 и двойным щелчком на его поле справа создайте обработчик, который заполните так
  • private void xnaPanel1_Paint(object sender, PaintEventArgs e)
            {
                if (closing)
                    return;
        
                try
                {
                    if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.Lost)
                        throw new DeviceLostException();
        
                    if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.NotReset)
                        device.Reset(presentParams);
        
                    // Очищаем область экрана
                    device.Clear(XNAGraphics.Color.CornflowerBlue);
        
                    device.RenderState.CullMode = CullMode.None;
                    device.VertexDeclaration = decl;
        
                    // Задаем координаты вершин из установленного в группе панелей цвета
                    vertices[0] = new VertexPositionColor(new Vector3(0.0f, 0.8f, 0.0f), new XNAGraphics.Color(
                        vertex1Color.BackColor.R, vertex1Color.BackColor.G, vertex1Color.BackColor.B));
                    vertices[1] = new VertexPositionColor(new Vector3(0.8f, -0.8f, 0.0f), new XNAGraphics.Color(
                        vertex2Color.BackColor.R, vertex2Color.BackColor.G, vertex2Color.BackColor.B));
                    vertices[2] = new VertexPositionColor(new Vector3(-0.8f, -0.8f, 0.0f), new XNAGraphics.Color(
                        vertex3Color.BackColor.R, vertex3Color.BackColor.G, vertex3Color.BackColor.B));
        
                    effect.Begin();
                    foreach(EffectPass pass in effect.CurrentTechnique.Passes)
                    {
                        pass.Begin();
                        device.DrawUserPrimitives(PrimitiveType.TriangleList, vertices, 0, vertices.Length / 3);
                        pass.End();
                    }
                    effect.End();
        
                    device.Present();
                }
                catch (DeviceNotResetException)
                {
                    Invalidate();
                }
                catch (DeviceLostException)
                {
                    closing = true;     // Больше не выполнять OnPaint()
                    string title = "Сбой графического устройства";
                    string message = "Работа программы будет завершена.\n"
                                   + "Закройте все ненужные программы\n"
                                   + "и повторите запуск этого приложения";
                    MessageBox.Show(message, title, MessageBoxButtons.OK, MessageBoxIcon.Stop);
                    Application.Idle += new EventHandler(Application_Idle);
                    return;
                }
                catch (Exception ext)           // Все другие исключения
                {
                    closing = true;
                    MessageBox.Show(ext.Message);
                    Application.Idle += Application_Idle;
                }
            }
  • Выделите на форме объект xnaPanel1 и через панель Properties в режиме Events создайте обработчик события Resize, который заполните так
  • private void xnaPanel1_Resize(object sender, EventArgs e)
            {
                // Устанавливаем размеры заднего буфера по клиентской области панели
                presentParams.BackBufferWidth = xnaPanel1.ClientSize.Width;
                presentParams.BackBufferHeight = xnaPanel1.ClientSize.Height;
                // Сбрасываем устройство и применяем к нему новые параметры  
                // Если окно не минимизировано
                if (this.WindowState != FormWindowState.Minimized)
                    device.Reset(presentParams); 
            }
  • Выделите саму форму щелчком на ее заголовке и через панель Properties в режиме Events создайте обработчик события FormClosed, который заполните так
  • private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
            {
                //  Удаляем (освобождаем) устройство
                device.Dispose();
                // На всякий случай присваиваем ссылке на устройство значение null 
                device = null;
            }
  • Осторожно (чтобы не создать обработчик) выделите все три кнопки на форме (удерживая клавишу Ctrl ) и через панель Properties в режиме Events создайте обработчик события Click с именем vertexColor_Click, который заполните так
  • private void vertexColor_Click(object sender, EventArgs e)
            {
                Button button = (Button)sender;
                colorDialog1.Color = button.BackColor;
                if (colorDialog1.ShowDialog() == DialogResult.OK)
                {
                    button.BackColor = colorDialog1.Color;
                    xnaPanel1.Invalidate();
                }
            }
  • В панели Solution Explorer выделите узел проекта Application2 и через контекстное меню сделайте проект стартовым. Запустите приложение - должен получиться такой результат
  • Замечание. Если приложение выдаст сообщение "Ошибка при компиляции эффекта...", то добавьте к проекту файл с эффектом ColorFill.fx из прилагаемой к работе папки Source, не редактируя его. Незабудьте в этом случае изменить свойство файла Copy to Output Directory на значение Copy if newer.

    Запускающее приложение

    Если вы все делали так как описано, то исполнимые сборки для упражнений 1 и 2 должны находиться относительно папки решений FormsXNA в подпапках:

    FormsXNA\Application1\bin\Debug\Application1.exe  
      FormsXNA\Application2\bin\Debug\Application2.exe

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

  • Добавьте к решению новый проект с именем StartForm и сделайте его стартовым
  • Переименуйте файл формы Form1.cs в StartForm.cs и откройте его в режиме View Designer и поместите на форму два экземпляра компонента LinkLabel
  • Создайте для них обработчики события LinkClicked и заполните их следующим кодом
  • using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
        
    using System.IO;
        
    namespace StartApp
    {
        public partial class StartForm : Form
        {
            System.Diagnostics.Process exe = new System.Diagnostics.Process();
            String path; // Путь к папке с решением
        
            public StartForm()
            {
                InitializeComponent();
        
                path = Directory.GetCurrentDirectory();// Получаем путь к сборке StartForm 
                int pos = path.LastIndexOf(@"\bin\");
                path = path.Substring(0, pos);// Убираем 2 последних секции пути
                pos = path.LastIndexOf(@"\");
                path = path.Substring(0, pos);// Убираем еще одну секцию
            }
        
            private void linkLabel1_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                Directory.SetCurrentDirectory(path + @"\Application1\bin\Debug");// Путь к сборке
                exe.StartInfo.FileName = "Application1.exe";
                System.Diagnostics.Debug.WriteLine(path); // Для отладки в окно Output
                exe.Start();
            }
        
            private void linkLabel2_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
            {
                Directory.SetCurrentDirectory(path + @"\Application2\bin\Debug");// Путь к сборке
                exe.StartInfo.FileName = "Application2.exe";
                System.Diagnostics.Debug.WriteLine(path); // Для отладки в окно Output
                exe.Start();
            }
        }
    }

    Код будет работать при условии, что сборка стартовой формы находится в своем каталоге bin дерева решения.

    Вернуться к учебному плану