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

Введение в XNA

Голосов: 5

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

Приведенный ниже текст получен путем автоматического извлечения из оригинального PDF-документа и предназначен для предварительного просмотра.
Изображения (картинки, формулы, графики) отсутствуют.
    Рисунок 2.12. Поточечный график функции y=f(x), визуализированный с использованием двухсот точек.


2.5. Отрезки
В XNA Framework имеется два типа отрезков: независимые отрезки (PrimitiveType.LineList) и
связанные отрезки (PrimitiveType.LineStrip). При указании независимого типа отрезков метод
Device.DrawUserPrimitives рисует набор несвязанных между собой отрезков прямых линий. Первый
отрезок рисуется между нулевой и первой вершиной набора вершин, второй отрезок – между второй и
третьей, и т.д. (рисунок 2.13). Данный тип примитивов обычно применяется для рисования отдельных
отрезков. Связанные отрезки (PrimitiveType.LineStrip) используются для построения ломаной линии,
проходящей через вершины. Первый сегмент линии рисуется между нулевой и первой вершиной, второй –
между первой и второй вершиной и т.д. (рисунок 2.14).
                              v1             v5
                                                                 v4

         v0
                         v2


                                              v3
Рисунок 2.13. Независимые отрезки (Direct3D.PrimitiveType.LineList)

                              v1             v5
                                                                v4

         v0
                         v2


                                              v3
Рисунок 2.14. Связанные отрезки (Direct3D.PrimitiveType.LineStrip)


2.5.1. Независимые отрезки (PrimitiveType.LineList).
Для демонстрации практического использования примитивов PrimitiveType.LineList мы перепишем
пример Ex04. Первая точка отрезка будет задаваться нажатием левой кнопки, а вторая – при отпускании
левой кнопки мыши. Таким образом, процесс рисования линии будет аналогичен редактору Paint –
пользователь помещает указатель мышь в начало отрезка, зажимает левую кнопку, и ведѐт указатель мыши
до конца отрезка, после чего отпускает левую кнопку мыши. В листинге 2.16 приведены основные
фрагменты исходного кода полученного приложения (Ex07):

 Листинг 2.16

public partial class MainForm : Form
{
...


// Массив вершин отрезков
    VertexPositionColor[] vertices = null;
// Количество отрезков
    int lineCount = 0;
// Максимальное количество отрезков, которые текущая видеокарта может визуализировать одним
// вызовом метода DrawUserPrimitives
int maxLineCount;
// Флаг, показывающий, находится ли программа в режиме добавления нового отрезка (когда
// пользователь уже указал начало отрезка, но ещѐ не отжал левую кнопку мыши)
    bool AddingLine = false;

...

      private void MainForm_Load(object sender, EventArgs e)
      {
...
// Определяем максимальное количество отрезков, которое видеокарта может визуализировать за
// один вызов метода DrawUserPrimitives
        maxLineCount = Math.Min(device.GraphicsDeviceCapabilities.MaxPrimitiveCount,
 device.GraphicsDeviceCapabilities.MaxVertexIndex / 2);

// Создаѐм массив, рассчитанный на хранение вершин восьми отрезков
        vertices = new VertexPositionColor[16];
    }

      private void MainForm_Paint(object sender, PaintEventArgs e)
      {
...
// Очищаем экран
            device.Clear(XnaGraphics.Color.CornflowerBlue);

// Если количество отрезков больше нуля
            if (lineCount > 0)
            {
                device.VertexDeclaration = decl;
// Визуализируем отрезки
                effect.Begin();
                foreach (EffectPass pass in effect.CurrentTechnique.Passes)
                {
                    pass.Begin();
                    device.DrawUserPrimitives(PrimitiveType.LineList, vertices, 0,
lineCount);
                    pass.End();
                }
                effect.End();
            }

              device.Present();
...
      }

...

    private void MainForm_MouseDown(object sender, MouseEventArgs e)
    {
// Если нажата левая кнопка мыши
        if (e.Button == MouseButtons.Left)
        {
// Если количество линий достигло предельно возможной величины, нечего не делаем
            if (lineCount == maxLineCount)


            {
               MessageBox.Show(String.Format("Количество отрезков достигло максимального” +
 “значения для данного GPU: {0}.", maxLineCount), "Внимание", MessageBoxButtons.OK,
 MessageBoxIcon.Warning);
               return;
           }

// Переходим в режим добавления отрезка
            AddingLine = true;

// Если размер массива вершин не достаточен вставки нового отрезка, то создаем массив
// удвоенного размера и копируем в него содержимое старого массива.
            if (lineCount * 2 >= vertices.Length)
            {
                int newLineCount = lineCount * 2;
// Размер массива не должен превышать предельно лимит текущей видеокарты
                if (newLineCount > maxLineCount)
                    newLineCount = maxLineCount;

                VertexPositionColor[] newVertices = new VertexPositionColor[newLineCount*2];
                vertices.CopyTo(newVertices, 0);
                vertices = newVertices;            }

// Заносим в массив вершин координаты начала и конца нового отрезка. Для перевода координат
// указателя мыши к диапазону [-1, +1] используется метод MouseToLogicalCoords, созданный
// нами в разделе 2.4.3.
            vertices[lineCount * 2] =
 new VertexPositionColor(Helper.MouseToLogicalCoords(e.Location, ClientSize),
 XnaGraphics.Color.Aqua);
            vertices[lineCount * 2 + 1] = vertices[lineCount * 2];
// Увеличиваем счетчик количества отрезков на 1
            lineCount++;

// Перерисовываем форму
            Invalidate();
        }
    }

    private void MainForm_MouseMove(object sender, MouseEventArgs e)
    {
// Если программа находится в режиме добавления нового отрезка
        if (AddingLine == true)
        {
// Обновляем координаты конца отрезка
            vertices[lineCount * 2 - 1].Position = Helper.MouseToLogicalCoords(e.Location,
 ClientSize);
// Перерисовываем экран
            Invalidate();
        }
    }

    private void MainForm_MouseUp(object sender, MouseEventArgs e)
    {
// Если была отжата левая кнопка мыши
        if (e.Button==MouseButtons.Left)
// Выходим из режима добавления нового отрезка
            AddingLine = false;
    }
}


Небольшого внимания заслуживает код, вычисляющий максимальное количество линий, которое может
визуализировать видеокарта за один вызов метода DrawUserPrimitives. Как вы знаете из раздела 2.4.3,
значение максимального количества примитивов, которые может визуализировать видеокарта за один
присест,       определяется    свойствами   GraphicsDeviceCapabilities.MaxPrimitiveCount         и
GraphicsDeviceCapabilities.MaxVertexIndex.         Но    так  как    каждый     примитив      типа
PrimitiveType.LineList содержит две вершины, при оценке максимального количества отрезков,
которые может визуализировать видеокарта за один присест, приложение должно поделить значение
GraphicsDeviceCapabilities.MaxVertexIndex на 2.
Чтобы сделать работу с программой более комфортной, мы встроим в неѐ возможность отмены изменений
при помощи комбинации клавиш Ctrl+Z, что позволит пользователю легко откатываться назад после
ошибочно нарисованных отрезков и т.д. Код обработчика, выполняющего откат изменений, приведѐн в
листинге 2.17. После такой доработки нашу программу вполне можно будет использовать как простенький
графический редактор (рисунок 2.15).

 Листинг 2.17.

private void MainForm_KeyDown(object sender, KeyEventArgs e)
{
     if ((e.KeyCode==Keys.Z) && (e.Control==true))
          if (AddingLine == false)
          {
                 if (lineCount > 0)
                     lineCount--;


                 Invalidate();
          }
}




Рисунок 2.15. Изображение, нарисованное при помощи нашего самодельного графического редактора (Ex07)


2.5.2. Связанные отрезки (PrimitiveType.LineStrip).
Перейдѐм к следующему типу примитивов – PrimitiveType.LineStrip. Как говорилось выше, этот тип
примитивов применяется для рисования ломаных линий, которые часто используются при построении
контуров различных поверхностей или графиков функций. Чтобы опробовать примитивы типа
PrimitiveType.LineStrip на практике, мы напишем приложение, рисующее в центре формы окружность
радиусом 0.8 единиц (Ex08). Окружность будет нарисована с использованием ломаной линии, содержащей
тридцать два сегмента. Каждая вершина ломанной будет иметь свой цвет, благодаря чему окружность будет
переливаться различными цветами (рисунок 2.16). Для вычисления координат вершин окружности мы
воспользуемся простой формулой из школьного курса аналитической геометрии:


  0..360
x  x0  r  sin( )                                                                                   (2.2)
y  y0  r  cos( )
где
     x и y – координаты текущей вершины окружности
     x0 и y0 – координаты центра окружности
      – угол, пробегающий с некоторым шагом значения от 0° до 360°.
     r – радиус окружности
Наиболее важные фрагменты приложения приведѐны в листинге 2.18.




Рисунок 2.16. Окружность, нарисованная с использованием примитивов Direct3D.PrimitiveType.LineStrip.


    Листинг 2.18.

public partial class MainForm : Form
{
    GraphicsDevice device = null;
    PresentParameters presentParams;
    VertexDeclaration decl;
    VertexPositionColor[] vertices = null;
// Количество сегментов в ломанной линии, аппроксимирующей окружность.
    const int LineStripCount = 32;

...

       private void MainForm_Load(object sender, EventArgs e)
       {
...
        decl = new VertexDeclaration(device, VertexPositionColor.VertexElements);
// Создаѐм графический буфер, для хранения вершин окружности
        vertices = new VertexPositionColor[LineStripCount + 1];
    }

       private void MainForm_Paint(object sender, PaintEventArgs e)
       {
...
// Очищаем экран

                    device.Clear(XnaGraphics.Color.CornflowerBlue);

                    device.VertexDeclaration = decl;


// Перебираем все вершины
            for (int i = 0; i <= LineStripCount; i++)
            {
// Вычисляем координаты текущей вершины окружности по формуле 2.2
                float angle = (float)i / (float)LineStripCount * 2.0f * (float)Math.PI;
// Окружность имеет радиус 0.8 единиц и расположена в начале системы координат
                float x = 0.8f * (float)Math.Sin(angle);
                float y = 0.8f * (float)Math.Cos(angle);
// Вычисляем цвет вершины
                int red=(int) (255 * Math.Abs(Math.Sin(angle * 3)));
                int green = (int)(255 * Math.Abs(Math.Cos(angle * 2)));
// Заносим информацию о вершине в графический буфер
                vertices[i] = new VertexPositionColor(new Vector3(x, y, 1.0f),
  new XnaGraphics.Color(red, green, 0));
            };

// Рисуем ломанную, аппроксимирующую окружность. Ломанная состоит из vertices.Length - 1
// сегментов.
            effect.Begin();
            foreach (EffectPass pass in effect.CurrentTechnique.Passes)
            {
                pass.Begin();
                device.DrawUserPrimitives(PrimitiveType.LineStrip, vertices, 0,
 vertices.Length - 1);
                pass.End();
            }
            effect.End();

             device.Present();
      }
...
}
Приложение устроено достаточно просто: сначала в обработчике события Load по формуле 2.2
вычисляются вершины, через которые будет построена ломаная, аппроксимирующая окружность.
Визуализация полученной ломанной выполняется в обработчике события Paint.
Стоит отметить, что с ростом числа сегментов ломанная все сильнее начинает походить на настоящую
окружность; при количестве сегментов порядка сотни вряд ли кто сможет найти визуальные различия между
окружностью, визуализированной поточено средствами GDI+, и еѐ аппроксимацией ломанной линией. А вот
разница в производительности будет более чем заметна.

Управление видовым преобразованием.
Так как мы используем логическую систему координат, в которой ширина и высота формы всегда равна
двум, непропорциональное растяжение формы приводит к искажению изображения (рисунок 2.17).
Существует два метода борьбы с этим явлением:
17. “Ручное” масштабирование примитивов таким образом, чтобы они всегда корректно отображалось
    независимо от размеров формы.
18. Запретить пользователю делать окно неквадратным, то есть действовать по принципу “нет неквадратной
    формы – нет и проблемы”.


Рисунок 2.17. Искажения формы круга при непропорциональном изменении размеров окна.

У каждого из этих подходов есть недостатки: реализация масштабирования примитивов сцены неминуемо
сделает код визуализации более запутанным, а ограничение на размеры окна будет сковывать действия
пользователя и создаст чувство дискомфорта. Однако существует и третий вариант:
19. Автоматическое   изменение     размера             визуализируемого         изображения   для   компенсации
    непропорционального размера формы.
Этот метод имеет важный нюанс относительно первого варианта: в данном случае трансформируются не
координаты вершин примитива, а итоговое изображение40. В разделе 2.3.1 на рисунке 2.2 была приведена
схема графического конвейера. Как вы знаете, на выходе из вершинного шейдера получаются вершины в
логической системе координат, в которой координаты вершин лежат в диапазоне [-1, +1]. Затем эти
логические координаты трансформируются в систему координат окна посредством видового
преобразование. Параметры этого преобразования задаются свойством Viewport класса GraphicsDevice:
public Viewport Viewport { get; set; }
Одноименная структура Viewport, инкапсулирующая параметры видового преобразования, определяется
следующим образом:
public struct Viewport
{
// Ширина области, в которую осуществляется визуализация
    public int Width { get; set; }
// Высота области, в которую осуществляется визуализация
    public int Height { get; set; }
// Координата X левого верхнего угла области визуализация
    public int X { get; set; }
// Координата Y левого верхнего угла области визуализация
    public int Y { get; set; }
...
}
Как видно, структура Viewport определяет в клиентской области окна прямоугольную область,
используемую для визуализации изображения. По умолчанию при создании и сбросе устройства XNA
Framework автоматически присваивает полям X и Y нулевое значение, а Width и Height – ширину и высоту
клиентской области окна. Таким образом, по умолчанию клиентская область заполняет всю клиентскую
область окна.
В следующем примере демонстрируется использование пользовательской области визуализации размером
100×100, расположенной в левом верхнем углу приложения (листинг 2.19).

 Листинг 2.19.


40
  Физически этот метод всѐ же сводится к неявной модификации координат вершин после трансформации
вершинным шейдером.


// Пример Examples\Ch02\Ex09
private void MainForm_Load(object sender, EventArgs e)
{
// Важно! Размер формы не может быть меньше области визуализации, в противном случае при
// попытке визуализации в такую форму будет сгенерировано исключение
// System.InvalidOperationException
    MinimumSize = SizeFromClientSize(new Size(100, 100));
...
}

private void MainForm_Paint(object sender, PaintEventArgs e)
{
    if (closing)
        return;

    try
    {
          if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.Lost)
              throw new DeviceLostException();

          if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.NotReset)
              device.Reset();

// Задание параметров области визуализации.
        Viewport viewport = new Viewport();
        viewport.Width = 100;
        viewport.Height = 100;
// Присваиваем информацию об области визуализации структуре Viewport
        device.Viewport = viewport;

// Очищаем экран
        device.Clear(XnaGraphics.Color.CornflowerBlue);
// Рисуем окружность
...
    }
}
Результат визуализации приведен на рисунке 2.18. Что же изменилось в работе приложения?
20. Размер визуализируемого изображения теперь не зависит от размера окна. Соответственно,
    непропорциональный размер формы больше не искажает изображение.
21. Клиентская область формы за пределами области визуализации заполнена “мусором”. В этом не ничего
    удивительного, ведь наше приложение оставляет содержимое формы за пределами области визуализации
    “как есть”.


Рисунок 2.18. Использование области визуализации 100×100, расположенной в левом верхнем углу окна.

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

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

 Листинг 2.20.

// Сборник вспомогательных методов, полезных в хозяйстве
class Helper
{
...
// Принимает размеры клиентской области формы. Возвращает квадратную область визуализации
// максимально возможного размера, расположенную в центре формы.
    public static Viewport SquareViewport(System.Drawing.Size clientSize)
    {
        Viewport viewport = new Viewport();

           viewport.Width = Math.Min(clientSize.Width, clientSize.Height);
           viewport.Height = viewport.Width;
           viewport.X = (clientSize.Width- viewport.Width) / 2;
           viewport.Y = (clientSize.Height - viewport.Height) / 2;

           return viewport;
     }

// Принимает размеры клиентской области формы. Возвращает область визуализации размером во
// всю клиентскую область формы
    public static Viewport FullScreenViewport(System.Drawing.Size clientSize)
    {
        Viewport viewport = new Viewport();
        viewport.Width = clientSize.Width;
        viewport.Height = clientSize.Height;
        return viewport;


     }
}
Код модифицированного приложения, использующего новые методы класса Helper, приведен в листинге
2.21.

 Листинг 2.21.

// Пример Ch02\Ex10
private void MainForm_Paint(object sender, PaintEventArgs e)
{
    if (closing)
        return;

     try
     {
           if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.Lost)
               throw new DeviceLostException();

           if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.NotReset)
               device.Reset();

// Используем для визуализации всю клиентскую область окна
        device.Viewport = Helper.FullScreenViewport(ClientSize);

// Очищаем экран
        device.Clear(XnaGraphics.Color.CornflowerBlue);

// Используем квадратную область визуализации
        device.Viewport = Helper.SquareViewport(ClientSize);
...
    }
}
Результат визуализации круга в окне с неквадратной клиентской областью приведен на рисунке 2.19. Как
видно, несмотря на непропорциональное изменение сторон окна, круг остался кругом.




Рисунок 2.19. Круг, визуализированный с использованием квадратной области визуализации, расположенной в центре формы.


Практическое упражнение №2.2.
Доработайте приложение, визуализирующее график косинуса из практического упражнения №2.1, заменив
примитивы PrimitiveType.PointList на PrimitiveType.LineStrip. Так же добавьте в приложение
высотную закраску: когда функция принимает значение 1, график должен окрашиваться зелѐным цветом, а
при -1 – красным. Остальные точки графика принимают промежуточные цвета (рисунок 2.20). Для
вычисления цвета промежуточных точек можно воспользоваться следующими формулами:



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