Reference = add reference, в висуал студия 2010 не могу найти в вкладке Solution Explorer, Microsoft.Xna.Framework. Его нету. |
Визуализация примитивов
2.4. Точки (PrimitiveType.PointList)
Как известно, иногда лучше один раз увидеть, чем сто раз услышать. Эта простая истина как никогда подходит к XNA Framework с весьма запутанной технологией визуализации примитивов. Поэтому мы начнем изучение материала с разбора приложения, рисующего в центре экрана одну точку цвета морской волны (листинг 2.7).
// Пример Examples\Ch02\Ex01 // Стандартные директивы C# using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; // При обработке исключений, связанных с открытием файла эффекта, нам понадобится // пространство имен System.IO using System.IO; // Включаем в приложение пространства имен XNA Framework using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Graphics; using XnaGraphics = Microsoft.Xna.Framework.Graphics; namespace GSP.XNA.Book.Ch02.Ex01 { public partial class MainForm : Form { // Устройство XNA Framework GraphicsDevice device = null; // Параметры представления данных на экране PresentationParameters presentParams; // Графический буфер для хранения вершин (то есть координат нашей точки) VertexPositionColor[] vertices = null; // Декларация формата вершины VertexDeclaration decl = null; // Эффект, используемый при визуализации точки Effect effect = null; // Флаг, устанавливаемый в true при подготовке к завершении работы приложения bool closing = false; public MainForm() { InitializeComponent(); } private void MainFormLoad(object sender, EventArgs e) { // Стандартная процедура настройки параметров формы и создание графического устройства SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true); MinimumSize = SizeFromClientSize(new Size(1, 1)); presentParams = new PresentationParameters(); presentParams.IsFullScreen = false; presentParams.BackBufferCount = 1; presentParams.SwapEffect = SwapEffect.Discard; presentParams.BackBufferWidth = ClientSize. Width; presentParams.BackBufferHeight = ClientSize.Height; device = new GraphicsDevice(GraphicsAdapter. DefaultAdapter, DeviceType.Hardware, this.Handle, CreateOptions.SoftwareVertexProcessing | CreateOptions.SingleThreaded, presentParams); // Создаем массив, предназначенный для хранения координат одной точки vertices = new VertexPositionColor[1]; // Создаем декларацию формата вершины decl = new VertexDeclaration(device, VertexPositionColor.VertexElements); // Задаем координаты точки (вершины) таким образом, чтобы она всегда была в центре экрана. // Цвет точки устанавливаем в морской волны, но в действительности он не влияет на цвет // точки, так как используемый эффект игнорирует информацию о цвете вершины vertices[0] = new VertexPositionColor (new Vector3(0.0f, 0.0f, 0.0f), XnaGraphics.Color.Aqua); // Структура для хранения кода откомпилированного эффекта CompiledEffect compiledEffect; try { // Пытаемся загрузить эффект из файла и откомпилировать его в промежуточный байт-код compiledEffect = Effect.CompileEffectFromFile (effectFileName, null, null, CompilerOptions.None, TargetPlatform.Windows); } // Если файл с эффектом не был найден catch (IOException ex) { // Выводим сообщение об ошибке и завершаем работу приложения MessageBox.Show(ex.Message, "Критическая ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); closing = true; Application.Idle += new EventHandler(ApplicationIdle); return; } // Если эффект не был удачно откомпилирован if (!compiledEffect.Success) { // Выводим сообщение об ошибках и предупреждениях из свойства ErrorsAndWarnings и завершаем // работу приложения MessageBox.Show(String.Format("Ошибка при компиляции эффекта: \r\n{0}", compiledEffect.ErrorsAndWarnings), "Критическая ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error); closing = true; Application.Idle += new EventHandler(ApplicationIdle); return; } // Создаем эффект на базе скомпилированного байт-кода. Обратите на использование флага // CompilerOptions.NotCloneable,который позволяет ощутимо сократить объем оперативной // памяти, используемой эффектом effect = new Effect(device, compiledEffect.GetEffectCode(), CompilerOptions.NotCloneable, null); // Выполняем валидацию текущей техники (проверяем, может ли текущая техника выполнится на // данном GPU) if (!effect.CurrentTechnique.Validate()) { // Если функциональность текущего GPU недостаточна, выводим сообщение об ошибке MessageBox.Show(String.Format("Ошибка при валидации техники \"{0}\" эффекта \"{1}\"\n\r" + "Скорее всего, функциональность шейдера превышает возможности GPU", effect.CurrentTechnique.Name, effectFileName), "Критическая ошибка", MessageBoxButtons.OK, > MessageBoxIcon.Error); closing = true; Application.Idle += new EventHandler(ApplicationIdle); return; } } private void MainFormPaint(object sender, PaintEventArgs e) { // Если приложение завершает работу из-за проблем в обработчике события Load, выходим из // обработчика события Paint (эффект effect может быть не корректно инициализирован, поэтому // попытка визуализации сцены может спровоцировать исключение) 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.VertexDeclaration = decl; // Начинаем визуализацию эффекта. effect.Begin(); // Перебираем все проходы эффекта foreach (EffectPass pass in effect.CurrentTechnique.Passes) { // Начинаем визуализацию текущего прохода pass.Begin() ; // Рисуем точку device.DrawUserPrimitives(PrimitiveType.PointList, vertices, 0, Ч> vertices.Length); // Заканчиваем визуализацию прохода pass.End(); } // Оканчиваем визуализацию эффекта effect.End(); // Завершаем визуализацию примитивов // Выводим полученное изображение на экран device.Present(); } // Обработка потери устройства catch (DeviceNotResetException) { Invalidate(); } catch (DeviceLostException) { } } // Обработчик события Idle. Завершает работу приложения. void Application_Idle(object sender, EventArgs e) { Close(); } // Сброс устройства при изменении размеров окна private void MainForm_Resize(object sender, EventArgs e) { if (WindowState != FormWindowState.Minimized) { presentParams.BackBufferWidth = ClientSize.Width; presentParams.BackBufferHeight = ClientSize.Height; device.Reset(presentParams); } } // Удаление устройства при завершении программы private void MainForm_FormClosed(object sender, FormClosedEventArgs e) { if (device != null) { device.Dispose(); device = null; } } } }Листинг 2.7.
Рассмотрим наиболее интересные фрагменты программы. Вначале мы объявляем массив для хранения вершин (то есть координат нашей точки) и декларацию вершины, для хранения описания формата элементов массива:
VertexPositionColor[] vertices = null; VertexDeclaration decl = null;
Ниже объявляется эффект, который будет использоваться для визуализации точки:
Effect effect = null;
Инициализация всех этих объектов выполняется в обработчике события Load формы. После создания графического устройства, обработчик события Load создает массив с информацией о единственной вершине сцены и декларацию формата этой вершины:
vertices = new VertexPositionColor[1]; vertices[0] = new VertexPositionColor(new Vector3 (0.0f, 0.0f, 0.0f), XnaGraphics.Color. Aqua); // Описание формата вершины берется из поля VertexPositionColor decl = new VertexDeclaration(device, VertexPositionColor.VertexElements);
Далее обработчик события Load выполняет компиляцию fx -файла, после использует полученный байт-код для создания объекта эффекта:
// Для сокращения объема кода из него исключена обработка исключительных ситуаций. В реальных // приложениях так поступать категорически не рекомендуется, так как это значительно снизит // "дуракоустойчивость" вашего приложения. Поэтому настоятельно рекомендую ознакомится с // полной версией кода из листинга 2.7. CompiledEffect compiledEffect; // Компилируем fx-файл в байт код compiledEffect = Effect.CompileEffectFromFile (effectFileName, null, null, CompilerOptions.None, TargetPlatform.Windows); // Используем полученный байт-код для создания объекта эффекта. effect = new Effect(device, compiledEffect. GetEffectCode(), CompilerOptions.NotCloneable, null);
При возникновении ошибок при загрузке или компиляции эффекта обработчик не завершает работу приложения путем вызова метода Close формы, так как, если верить MSDN, это может вызвать утечку ресурсов. Вместо этого он регистрирует собственный обработчик события Idle, автоматически вызывающий метод Close. Но здесь есть один подводный камень: метод Idle будет вызван по завершении обработки всех событий, в том числе Paint. Таким образом, если не принять особых мер, не исключен вызов метода Idle с не полностью сформированным эффектом, что с большой долей вероятности приведет к краху приложения. Для борьбы с этим недоразумением в начале обработчика события Paint осуществляется проверка, не готовится ли приложение к завершению работы: если это так, то обработчик события Paint не выполняет визуализацию сцены.
Переходим к обработчику события Paint, выполняющего визуализацию изображения. Первым делом данный обработчик выполняет стандартные проверки потери устройства, после чего очищает экран. Далее он присваивает свойству VertexDeclaration графического устройства декларацию вершины, созданную в обработчике события Load: device.VertexDeclaration = decl;.
На первый взгляд эту операцию было бы рациональнее вынести в обработчик события Load. Однако это не самая лучшая идея, так как информация о параметрах графического устройства теряется при сбросе методом Reset. Следовательно, такое приложение перестало бы нормально функционировать после первой же потери устройства. И, наконец, главная изюминка программы: визуализация точки на экране с использованием эффекта:
effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); device.DrawUserPrimitives(PrimitiveType.PointList, vertices, 0, vertices.Length); pass.End() ; } effect.End();
Как видно, несмотря на обилие кода, приложение имеет достаточно простую структуру. Как говорится, у страха глаза велики. Теперь давайте попробуем создать это приложение в Visual Studio 2005. Для начала запустите Visual Studio 2005, создайте проект нового приложения Windows Forms и подключите сборку Microsoft.Xna.Framework.dll. В окне Solution Explorer щелкните правой кнопкой мыши на узле проекта и выберите в контекстном меню команду Add | New Folder, и создайте папку Data, в которой мы
будем хранить различные вспомогательные эффекты (рисунок 2.6,). Затем добавьте в папку Data файл эффекта SimpleEffect.fx (рисунок 2.7), например, при помощи команды контекстного меню Add | New Item... . Поместите в файл SimpleEffect.fx текст эффекта из листинга 2.6.
После этих действий в каталоге проекта появится каталог Data, содержащий файл эффекта SimpleEffect.fx. Однако подобное расположение файла не совсем удобно, ведь при компиляции Debug -версии приложения Visual Studio копирует исполняемый exe -файл в подкаталог проекта bin\Debug, а при компиляции Release версии соответственно в каталог bin\Release. Соответственно, было бы логичным, если бы файл эффекта размещался вместе с исполняемым файлом приложения, что облегчило бы создание инсталлятора финальной версии приложения. К счастью, это достаточно легко организовать: просто выделите в окне Solution Explorer файл SimpleEffect.fx и в окне Properties присвойте свойству Copy to Output Directory значение Copy if newer (рисунок 2.8). После этого при каждой компиляции приложения Visual Studio будет автоматически создавать в подкаталоге bin\Debug или bin\Release подкаталог bin\Debug\Data или bin\Release\Data и копировать в него файл SimpleEffect.fx.
В заключении остается создать необходимые обработчики сообщений в соответствии с листингом 2.7. Полную версию приложения можно найти в example.zip в каталоге Ch02\Ex01.
2.4.1. Проверка аппаратной поддержки вершинных шейдеров
Наше приложение, визуализирующее точку в центре экрана, всегда создает графическое устройство с использованием флага CreateOptions.SoftwareVertexProcessing, то есть вершинные шейдеры всегда выполняются средствами центрального процессора ( CPU ). Учитывая, что подавляющее большинство современных графических процессоров имеют аппаратную поддержку вершинных шейдеров, этот недочет приводит к неоптимальному использованию ресурсов GPU. Использование флага CreateOptions.HardwareVertexProcessing тоже не является хорошей идей, так это сделает невозможной работу приложения на видеокартах без аппаратных вершинных процессоров (например, Intel GMA 900 и Intel GMA 950 ).
Так что же делать? Наиболее красивое решение проблемы - проверка возможностей текущего GPU. Если текущий GPU имеет аппаратные вершинные процессоры, приложение должно создать устройство с использованием флага CreateOptions.HardwareVertexProcessing, в противном случае - CreateOptions.SoftwareVertexProcessing.
Таким образом, нам необходимо научиться анализировать возможности текущего GPU. В XNA Framework информация обо всех возможностях графического устройства инкапсулируются в классе GraphicsDeviceCapabilities, каждое свойство которого соответствует одной из характеристик графического устройства. Учитывая многообразие характеристик устройства, разработчики сгруппировали часть свойств в логические группы (структуры), то есть некоторые свойства класса GraphicsDeviceCapabilities в свою очередь тоже содержат набор свойств по некоторой тематике:
// Некоторые фрагменты определения класса GraphicsDeviceCapabilities public sealed class GraphicsDeviceCapabilities : IDisposable { // Группа свойств, описывающих возможности графического устройства по визуализации примитивов public GraphicsDeviceCapabilities.Primitive Caps PrimitiveCapabilities { get; } // Группа свойств с информацией о возможностях декларации вершин public GraphicsDeviceCapabilities. DeclarationTypeCaps DeclarationTypeCapabilities { ^ get; } // Группа свойств с информацией о вершинных шейдерах public GraphicsDeviceCapabilities. VertexShaderCaps VertexShaderCapabilities { get; } // Группа свойств с информацией о пиксельных шейдерах public GraphicsDeviceCapabilities. PixelShaderCaps PixelShaderCapabilities { get; } // Группа свойств с информацией о драйвере устройства public GraphicsDeviceCapabilities.DriverCaps DriverCapabilities { get; } // Группа свойств с информацией об устройстве, которая может пригодится при создании // устройства public GraphicsDeviceCapabilities.DeviceCaps DeviceCapabilities { get; }3 ... // Свойства без подсвойств: // Максимальная версия языка Vertex Shader, поддерживаемая графическим устройством public Version VertexShaderVersion { get; } // Максимальная версия языка Pixel Shader, поддерживаемая графическим устройством public Version PixelShaderVersion { get; } // Максимальный размер точки, которую способно отображать графическое устройство public float MaxPointSize { get; } // Максимальное количество примитивов, которое способно отобразить графическое устройство за // один вызов метода DrawUserPrimitives public int MaxPrimitiveCount { get; } // Остальные свойства ... }
Информация, которая может понадобиться при создании графического устройства, сосредоточена в свойствах свойства GraphicsDeviceCapabilities.DeviceCaps DeviceCapabilities:
// Некоторые фрагменты определения структуры DeviceCaps public struct DeviceCaps { // Поддерживает ли графическое устройство метод DrawUserPrimitives на аппаратном уровне public bool SupportsDrawPrimitives2Ex { get; } // Поддерживает ли графическое устройство аппаратную растеризацию примитивов (при отсутствии // подобной поддержки визуализация будет выполняться с неприемлемо низкой // производительностью) public bool SupportsHardwareRasterization { get; } // Имеет ли графическое устройство аппаратные вершинные процессоры public bool SupportsHardwareTransformAndLight { get; } ... }
Как видно, информация о наличии аппаратных вершинных процессоров содержится в свойстве SupportsHardwareTransformAndLight. Таким образом, нашему приложению необходимо просто проверить значение свойства GraphicsDeviceCapabilities.DeviceCapabilities.SupportsHardwareTransformAndLight. Если оно равно true, приложение может создать графическое устройство с использованием флага CreateOptions.HardwareVertexProcessing, в противном случае должен использоваться флаг CreateOptions.SoftwareVertexProcessing.
XNA Framework предоставляет разработчику два способа получения доступа к экземпляру объекта GraphicsDeviceCapabilities. Наиболее простым из них является использование свойства GraphicsDeviceCapabilities экземпляра класса графического устройства:
public GraphicsDeviceCapabilities GraphicsDeviceCapabilities { get; }
Не смотря на простоту данный способ обладает существенным недостатком: для получения доступа к свойству GraphicsDeviceCapabilities приложение должно создать графическое устройство. Получается замкнутый круг: чтобы получить информацию, необходимую для создания графического устройства, приложение должно создать это устройство. В принципе, мы можем попробовать написать что-то вроде:
// Создаем графическое устройство без аппаратной поддержки вершинных шейдеров. device = new GraphicsDevice(GraphicsAdapter. DefaultAdapter, DeviceType.Hardware, this.Handle, CreateOptions.SoftwareVertexProcessing | CreateOptions.SingleThreaded, presentParams); // Если GPU имеет аппаратные вершинные процессоры if (device.GraphicsDeviceCapabilities.DeviceCapabilities. SupportsHardwareTransformAndLight) { // Уничтожаем устройство device.Dispose(); // Снова создаем устройство, но уже с аппаратной поддержкой вершинных шейдеров device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware, this.Handle, CreateOptions.HardwareVertexProcessing | CreateOptions.SingleThreaded, presentParams); }
Хотя данная технология и работает, все это напоминает поездку из Киева в Харьков через Жмеринку. Поэтому разработчики XNA Framework предусмотрели альтернативный способ получения экземпляра класса GraphicsDeviceCapabilities без создания графического устройства. Как вы знаете, конструктор класса GraphicsDevice принимает в качестве первого параметра экземпляр класса GraphicsAdapter, описывающий используемую видеокарту. Так вот, заботливые разработчики XNA Framework снабдили этот класс методом GetCapabilities, возвращающем экземпляр класса GraphicsDeviceCapabilities, соответствующий этому устройству:
public GraphicsDeviceCapabilities GetCapabilities(DeviceType deviceType);
где
- deviceType - тип устройства, задаваемый с использованием перечислимого типа DeviceType.
Зачем нужен параметр deviceType? Дело в том, что метод GetCapabilitie не может предугадать, какой тип устройства вы собираетесь создать ( DeviceType.Hardware, DeviceType.Reference или DeviceType.NullReference ), в то время как все эти типы устройств имеют совершенно разные характеристики. Соответственно, при помощи параметра deviceType вы указываете методу GetCapabilities, какое значение вы планируете передать параметру deviceType конструктора класса графического устройства ( GraphicsDevice/ ).
Таким образом, проверку наличия аппаратных вершинных процессоров можно организовать с использованием следующего фрагмента кода:
GraphicsDeviceCapabilities caps = GraphicsAdapter.DefaultAdapter.GetCapabilities(DeviceType.Hardware); CreateOptions options = CreateOptions.SingleThreaded; if (caps.DeviceCapabilities.SupportsHardwareTransformAndLight) options |= CreateOptions.HardwareVertexProcessing; else options |= CreateOptions.SoftwareVertexProcessing; device = new GraphicsDevice(GraphicsAdapter. DefaultAdapter, DeviceType.Hardware, this.Handle, options, presentParams)