Любому разработчику периодически приходится решать множество типовых задач визуализации:
Ответы на все эти вопросы будут даны в этой лекции. Начнем с решения первой проблемы.
До сих пор все наши приложения осуществляли монопольный вывод графической информации на поверхность формы. Такой подход не позволяет размещать на форме элементы управления .NET, что значительно ограничивает свободу разработчика. Конечно, можно попробовать использовать различные вспомогательные диалоговые окна и плавающие панели инструментов, но это не очень красивое решение проблемы, хотя и вполне Panel ). Это позволило бы нам легко ограничить область вывода XNA Framework определенной областью формы, а освободившееся пространство использовать для размещения различных элементов управления Windows Forms.
Вывод на поверхность элемента управления практически не отличается от вывода на поверхность формы, ведь в Windows Forms форма является частным случаем элементом управления. Более, форму можно поместить на другую форму или компонент в качестве элемента Control. Как следствие, любой элемент управления Windows Forms обладает свойствами Handle, Width, Height, методами SetWidth, Show, Hide, обработчиками событий Paint, Click, Resize и так
Примечание
Класс Control является полноценным элементом управления, по функциональности отдаленно напоминающий элемент Panel с немного ограниченными возможностями.
Но вот незадача, некоторые члены класса Control объявлены как protected. Это не создает никаких проблем при создании новой формы путем наследования от класса Form, так как в этом случае вы автоматически получаете полный доступ к защищенным (protected) членам класса. Однако при использовании готовых компонентов, помещаемых на форму средствами визуального редактора форм Visual Studio, все намного серьезнее. Например, вы не сможете получить доступ к такому важному методу как SetStyle, чтобы установить стиль ControlStyles.Opaque. А ведь без этого невозможно запретить самовольную перерисовку элемента управления, приводящую к мерцанию изображения в XNA -приложениях (раздел 1.2.3).
Наиболее красивое решение этой проблемы - создание на базе класса Control собственного элемента управления, изменяющего статус метода SetStyle с protected на public. Для этого запустите Visual Studio и создайте новый проект библиотеки классов XnaPanel (File | New Project… | Class Library, в текстовом поле Name введите XnaPane и нажмите OK). Установите флажок Create directory for solution, так как в решении будет еще один проект, предназначенный для тестирования созданного компонента. Подключите к проекту сборку System.Windows. Forms и введите код из листинга 3.1.
using System;
using System.Collections.Generic; using System.Text;
using System.Windows. Forms;
namespace GSP.XNA
{
выделялся среди других элементов формы
// Объявляем класс (элемент управления) XnaPanel,
наследуемый непосредственно public class
XnaPanel : Control
{
// Конструктор элемента управления public XnaPanel()
{
// Изменяем цвет элемента управления, чтобы он
1x1 пикселей
BackColor = Color.CornflowerBlue;
// Размер элемента управления не должен быть меньше MinimumSize = new
Size(1, 1);
}
SetStyle метода Control как public SetStyle(ControlStyles flag,
bool value)
//
//
Переопределяем метод
public new void
{
Вызываем оригинальный метод класса Control base.SetStyle(flag, value);
}
}
}
Важно
Для предотвращения неконтролируемого уменьшения размера компонента xnaPanel до размеров меньше одного пикселя, его свойству MinimumSize необходимо присвоить значение Size(1, 1) . Если этого не сделать, площадь области визуализации теоретически может достигнуть нуля, что приведет к генерации исключения при сбросе устройства
После компиляции проекта Visual Studi o самостоятельно создаст в окне Components и добавит в нее полученный элемент управления (рисунок 3.1).
Toolbox группу XnaPanel
(рис 3.1) Панель Toolbox с компонентом XnaPanel Для тестирования нашего компонента мы создадим простое приложение, визуализирующее треугольник. В правой части формы будет расположена панель с элементами управления, позволяющими изменять цвета вершин треугольника (рисунок 3.2).
Чтобы добавить к решению еще один проект щелкните правой кнопкой мыши на названии решения в окне Solution Explorer и выберите в контекстом меню пункт Add | New Project… (рисунок 3.3). В появившемся диалоговом окне выберите элемент Windows Application, введите в поле Name название приложения (например, Test ) и нажмите кнопку Ok. Как всегда, подключите к проекту необходимые сборки XNA Framework и добавьте необходимые директивы using.

(рис 3.3) Внешний вид тестового приложения, использующего компонент XnaPanel (рис 3.2) Добавление к решению (Solution) нового проекта Поместите на форму компонент SplitContainer, расширьте его на всю форму путем присвоения параметру Dock значения Fill и зафиксируйте размер правой панели, присвоив свойству FixedPanel значение Panel2. В правой панели компонента SplitContainer разместите группу ( Group ) "Параметры " и тоже расширьте ее на всю панель при помощи свойства Dock. В группе создайте три метки ( Label ) расположенные друг под другом: Цвет вершины №1, Цвет вершины №2, Цвет вершины №3. Поместите напротив этих меток три панели ( Panel ) и присвойте им имена vertex1Panel, vertex2Panel и vertex3Panel. Чтобы создать черные рамки вокруг панелей, установите свойство BorderStyle в значение FixedSingle. Поместите в левой панели компонента SplitContainer наш компонент XnaPanel, назовите его xnaPanel и расширьте его на всю свободную
область формы, присвоив свойству Dock значение Fill. В заключение, поместите на форму невизуальный компонент ColorDialog.
Следующий этап - "оживление " формы путем создания обработчиков событий. Для начала мы добавим в форму несколько вспомогательных полей и обработчики событий Load/ и FormClosed (листинг 3.2).
// Различные вспомогательные поля GraphicsDevice device = null;
PresentationParameters presentParams; Effect effect = null;
VertexDeclaration decl = null; VertexPositionColor[] vertices = null;
bool closing = false;
new XnaGraphics.Color(vertex2Panel.BackColor.R, vertex2Panel.BackColor.G,
vertex2Panel.BackColor.B));
vertices[2] = new VertexPositionColor(new Vector3(-0.4f, -0.4f, 0.0f),
new XnaGraphics.Color(vertex3Panel.BackColor.R,
vertex3Panel.BackColor.G,
vertex3Panel.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)
{
}
}
private void xnaPanelResize(object sender, EventArgs e)
{
// Если форма не минимизирована
if (WindowState != FormWindowState.Minimized)
{
// Обновляем размер формы
presentParams.BackBufferWidth = xnaPanel.ClientSize. Width;
presentParams.BackBufferHeight = xnaPanel.ClientSize.Height;
// Сбрасываем устройство
device.Reset(presentParams);
}
}
В заключении мы создадим обработчик щелчка мышью на панели выбора цвета вершины: при щелчке левой кнопкой мыши будет открываться стандартное диалоговое окно Windows выбора цвета, после чего панель окрасится цветом, выбранным пользователем, а изображение в компоненте xnaPanel будет перерисовано с учетом нового цвета вершины. Для этого выделите панель vertex1Panel и создайте следующий обработчик события Click:
private void vertex1Panel_Click(object sender, EventArgs e)
{
// Преобразовываем параметр sender к Panel
Panel panel = (Panel)sender;
// Выбираем в диалоговом окне текущий цвет вершины
colorDialog1.Color = panel.BackColor;
// Показываем стандартный диалог выбора цвета
if (colorDialog1.ShowDialog() == DialogResult.OK)
{
// Если пользователь выбрал цвет, изменяем цвет панели на выбраный
panel.BackColor = colorDialog1.Color;
// Обновляем содержимое нашего компонента xnaPanel
panelDX.Invalidate();
}
}
Как видно, обработчик получает информацию о нажатой панели из параметра sender, что позволяет без изменений использовать этот обработчик для панелей vertex2Panel и vertex3Panel: просто выделите эти панели и назначьте в качестве обработчика события Click метод vertex1Panel_Click.
Остается лишь откомпилировать программу и посмотреть результат. Готовое приложение можно найти в ../1/example.zip в каталоге Examples\Ch03\Ex01.
Примечание
В Visual Studio выбор активного Set as StartUp Project (рисунок 3.4).
(рис 3.4) Выбор активного проекта с использованием контекстного меню Практическое упражнение №3.1.
Напишите приложение, отображающее круг. В правой части окна должны быть расположены элементы управления, позволяющие задавать различные параметры изображения: радиус круга, количество секторов в круге, режим визуализации круга (поточечный, каркасный или с закраской) и цвет круга (рисунок 3.5).
(рис 3.5) Иллюстрация к практическому упражнению №3.1Для ограничения области визуализации XNA Framework воспользуйтесь компонентом XnaPanel из примера Ch03\Ex02. Существует два способа добавления компонента XnaPanel в окно Toolbox:
XnaPanel из примера Ch03\Ex01. Для этого во вкладке Solution Explorer щелкнете правой кнопкой мыши на узле с названием решения ( Solution ) и выберете в контекстном меню пункт Add | Existing Project… . Укажите в открывшемся окне файл проекта компонента PanelDX (Examples\Ch03\Ex01 - XnaPanel\XnaPanel\XnaPanel.csproj) и нажмите Ok. После компиляции подключенного проекта (Ctrl + Shift + B) компонент Xna Panel автоматически появится в окне Toolbox.ToolBox компонент из сборки примера Ex01. Для этого щелкните правой кнопкой мыши на поверхности окна ToolBox и выберите в контекстном меню пункт Choose Items…. Откроется диалоговое окно Choose Toolbox Items…. Щелкните на кнопку Browse… и укажите сборку с компонентом PanelDX (Examples\Ch03\Ex01 - XnaPanel\XnaPanel\bin\Release\XnaPanel.dll) . Наконец, нажмите кнопку Ok, после чего на панели Toolbox появится компонент XnaPanel.Какой из этих двух вариантов выбрать – решать вам, но лично мне больше симпатизирует первый подход.
Как известно, подавляющее большинство игровых приложений работают в полноэкранном режиме. На это есть несколько веских причин:
Windows вроде панели задач ( Taskbar ).back ) буферы без необходимости копирования информации между буферами: при показе итогового изображения XNA Framework просто делает задний буфер экранным, а экранный задним.К недостаткам полноэкранного режима можно отнести несколько усложнение кода приложения и невозможность использования элементов управления Windows Forms, что затрудняет применение полноэкранного режима в различных утилитах.
Переход полноэкранный режим осуществляется путем создания графического устройства ( GraphicsDevice ) с соответствующим образом настроенной структурой PresentationParameters. Легко догадаться, что свойству IsFullScreen необходимо присвоить значение true:
presentParams = new PresentationParameters(); presentParams.IsFullScreen = true;
Кроме того, приложение должно задать параметры используемого видеорежима, а именно:
back ) буферов задается свойствами BackBufferWidth и BackBufferHeight структуры PresentationParameters. Например:
// Используем видеорежим с разрешением 640 x 480 presentParams.BackBufferWidth = 640; presentParams.BackBufferHeight = 480;
Любой видеорежим характеризуется определенным форматом пикселей, используемым для хранения информации о цвете пикселей. Формат пикселей определяет, сколько бит отводится для хранения каждого цвета и как распределены биты между различными цветовыми каналами. Формат пикселей заднего буфера задается свойством BackBufferFormat структуры PresentationParameters:
public SurfaceFormat BackBufferFormat { get; set; }
где
SurfaceFormat - перечислимый тип, используемый для задания формата пикселей (таблица 3.1).В качестве формата экранного буфера автоматически используется формат, наиболее близкий к формату заднего буфера. Например, если вы укажете для заднего буфера формат SurfaceFormat.Color, в качестве формата экранного буфера будет выбран формат SurfaceFormat.Bgr32. Обычно это не существенно, ведь альфа-канал все равно не оказывает никакого влияния на отображаемом на экране изображении; тем не менее, в некоторых немногочисленных ситуациях этот нюанс все же приходится учитывать.
| Значение | Может использоваться в качестве формата экранного буфера | Размер в битах | ||||
|---|---|---|---|---|---|---|
| Весь пиксель | Красный канал | Зеленый канал | Синий канал | Альфа канал | ||
Unknown |
? | ? | ? | ? | ? | |
Rgba1010102 |
X | 32 | 10 | 10 | 10 | 2 |
Color |
32 | 8 | 8 | 8 | 8 | |
Bgr32 |
X | 32 | 8 | 8 | 8 | - |
Bgr565 |
X | 16 | 5 | 6 | 5 | - |
Bgra5551 |
16 | 5 | 5 | 5 | 1 | |
По умолчанию свойство SurfaceFormat равно SurfaceFormat.Unknown. В оконном режиме это значение указывает на необходимость использования в качестве формата заднего буфера такой же формат пикселей, как у рабочего стола. Например, если вы используете в Windows 32-х битный видеорежим, задний буфер будет использоваться формат SurfaceFormat.Bgr32. Эта особенность позволила нам не утруждать себя выбором формата заднего буфера для оконных приложений первой и второй глав книги.
Однако применение формата SurfaceFormat.Unknown для полноэкранного графического устройства приводит к генерации исключения System.InvalidOperationException. Так что при создании устройства, использующего полноэкранный режим, приложение обязано явно указывать формат пикселей заднего буфера. В нашем первом приложении мы, не мудрствуя лукаво, будем использовать формат SurfaceFormat.Bgr32, поддерживаемый всеми видеокартами, совместимыми с XNA Framework: presentParams.BackBufferFormat = SurfaceFormat.Bgr32 ;
Примечание
Должно быть, вы заметили, что пиксели формата SurfaceFormat.Bgr32 имея размер 32 бита, содержат всего 24 бита полезной информации (для красного, синего и зеленого каналов отведено по 8 бит). Такая избыточность обусловлена тем, что современные процессоры умеют работать только с типами данных разрядностью 8, 16, 32 и 64 бит. Соответственно, 24-х битные типы данных пришлось бы эмулировать посредством 8, 16 и 32-х разрядных типов, что неминуемо увеличило количество операций, требуемых для считывания и записи значения пикселя и, соответственно, ощутимо снизило производительность. Например, для записи 24-х битного значения необходимо загрузить регистр 32 бита, модифицировать 24 бита, и записать полученное 32-х битное значение обратно в память.
И, наконец, последней характеристикой любого графического режима является частота обновления экрана. Для современных электронно-лучевых трубок нормой является частота обновления порядка 85 герц и выше, так как при более низких частотах становится заметным мерцание монитора. Мониторы менее критичны к низкой частоте обновления экрана, однако редкий современный -монитор поддерживает частоту смены кадров более 60 Гц. Частота обновления экрана задается свойством FullScreenRefreshRateInHz структуры PresentationParameters:
public int FullScreenRefreshRateInHz { get; set; }
В оконном режиме этому свойству присевается нулевое значение, ведь окно все равно может обновляться с частотой, отличной от частоты обновления, используемой рабочим столом. В полноэкранном режиме нулевое значение параметра FullScreenRefreshRateInHz, означает, что приложение отдает выбор частоты обновления экрана на откуп Windows и драйверам видеокарты/монитора. Так как при указании частоты обновления неподдерживаемой текущей видеоподсистемой может быть сгенерировано исключение System. InvalidOperationException, мы пока не будем пытаться самостоятельно выбирать частоту обновления экрана.
Таким образом, код создания графического устройства должен выглядеть аналогично листингу 3.4.
presentParams = new PresentationParameters(); presentParams.BackBufferCount = 1; presentParams.SwapEffect = SwapEffect.Discard; // Используем полноэкранный режим 640Ч480, 32 бита на пиксель, частота обновления выбирается // автоматически presentParams.IsFullScreen = true; presentParams.BackBufferWidth = 640; presentParams.BackBufferHeight = 480; presentParams.BackBufferFormat = SurfaceFormat.Bgr32; presentParams.FullScreenRefreshRateInHz = 0; 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);
Так же необходимо подправить код обработчика событий Resize. Дело в том, что даже в полноэкранном alt + tab ) Windows может самостоятельно изменять размер формы, что, разумеется, приводит к генерации события Resize. Но так как в полноэкранном режиме размер формы некоим образом не связан с разрешением экрана, приложение не должно изменять размер заднего ( back ) буфера:
private void MainForm_Resize(object sender, EventArgs e)
{
// Если приложение находится в полноэкранном режиме,
оно не должно отслеживать изменение
// размера формы
if ((!presentParams.IsFullScreen)
(WindowState != FormWindowState.Minimized))
{
presentParams.BackBufferWidth = ClientSize.Width;
presentParams.BackBufferHeight = ClientSize.Height;
device.Reset(presentParams);
}
}
Визуализация сцены осуществляется аналогично оконному режиму:
private void MainFormPaint(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.Viewport = Helper.FullScreenViewport presentParams);
// Очищаем весь экран
device.Clear(XnaGraphics.Color.CornflowerBlue);
// Используем квадратную область визуализации
(во избежание геометрических искажений
// изображения)
device.Viewport = Helper.SquareViewport(presentParams);
// Рисуем CD-диск (см. раздел 2.6.3)
}
catch (DeviceNotResetException)
{
Invalidate() ;
}
catch (DeviceLostException)
{
}
}
Стоит отметить, что в связи с переходом к полноэкранному режиму при вычислении квадратной области в центре экрана используются не размеры клиентской формы, а информация о размере заднего буфера из структуры PresentationParameters presentParams. Собственно вычисление размеров области визуализации осуществляется двумя дополнительными перегруженными методами:
// Вычисление квадратной области визуализации
public static Viewport SquareViewport(PresentationParameters
presentParams)
{
return SquareViewport(new System.Drawing.Size(presentParams.
BackBufferWidth,
presentParams.BackBufferHeight)); }
// Вычисление области визуализации размеров во весь экран
public static Viewport FullScreenViewport(PresentationParameters
presentParams)
{
return FullScreenViewport(new System.Drawing.Size(presentParams.
BackBufferWidth,
presentParams.BackBufferHeight));
}
Готовое приложение, визуализирующее изображение CD-диска в полноэкранном режиме (рисунок 3.6), находится в каталоге Examples\Ch03\Ex03.
(рис 3.6) CD-диск, визуализированный полноэкранном режиме (640x480x32bpp) В примере Ch03\Ex03 (листинг 3.4) мы неявно предполагаем, что абсолютно все современные и будущие видеоподсистемы будут поддерживать видеорежим 640Ч480Ч32 System.InvalidOperationException. Поэтому было бы логичным на всякий случай предусмотреть поведение приложение при отсутствии поддержки требуемого видеорежима. Например, при неудачной попытке создания полноэкранного графического устройства приложение могло бы попытаться создать графическое устройство, осуществляющее визуализацию в окне:
presentParams = new PresentationParameters();
presentParams.BackBufferCount = 1;
presentParams.SwapEffect = SwapEffect.Discard;
presentParams.IsFullScreen = true;
// Используем полноэкранный режим 640Ч480, 32 бита на пиксель, 60 герц
presentParams.BackBufferWidth = 640;
presentParams.BackBufferHeight = 480;
presentParams.BackBufferFormat = SurfaceFormat.Bgr32;
presentParams.FullScreenRefreshRateInHz = 60;
try {
device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
DeviceType.Hardware, this.Handle,
options, presentParams);
}
catch(InvalidOperationException)
{
// Если попытка использования полноэкранного режима
потерпела фиаско, используем
// визуализацию в окне
presentParams.IsFullScreen = false;
presentParams.BackBufferWidth = 0;
presentParams.BackBufferHeight = 0;
presentParams.BackBufferFormat = SurfaceFormat.Unknown;
presentParams.FullScreenRefreshRateInHz = 0;
// После выполнения этой команды интерфейс формы будет
заблокирован: форму нельзя
// будет перемещать по экрану, изменять ее размер,
минимизировать и т.п.
device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
DeviceType.Hardware,
this.Handle, options, presentParams);
}
Хотя этот не содержит ничего противоестественного, на практике он не будет нормально функционировать: из-за ошибки в XNA Framework после неудачной попытки перехода в полноэкранный режим приложение уже не может использовать оконный режим. В следующих версиях XNA Framework эта проблема по видимости будет исправлена, ну а пока нам придется, как и всем настоящим героям, идти в
И так, при создании полноэкранного графического устройства необходимо подобрать видеорежим, гарантированно поддерживаемый данной видеокартой и монитором. А почему бы нам вместо гадания на кофейной гуще не использовать текущий видеорежим рабочего стола, по определению поддерживаемый видеоподсистемой компьютера? Ведь, как известно, в качестве видеорежима рабочего стола обычно используется видеорежим, наиболее оптимальный для текущего монитора компьютера. Это особенно актуально для -мониторов, оптимизированных для работы каком-то одном разрешении экрана (как правило, 1024x768 или 1280x1024). Информация о текущем видеорежиме доступна посредством свойства CurrentDisplayMode класса GraphicsAdapter:
public DisplayMode CurrentDisplayMode { get; }
Вся информация о видеорежиме хранится в структуре DisplayMode:
public struct DisplayMode
{
// Количество пикселей по горизонтали
public int Width { get; }
// Количество пикселей по вертикали
public int Height { get; }
// Формат пикселей
public SurfaceFormat Format { get; }
// Частота обновления экрана
public int RefreshRate { get; }
}
Соответственно, код создания графического устройства примет следующий вид:
// Получаем описание текущего видеорежима DisplayMode displayMode = GraphicsAdapter.DefaultAdapter. CurrentDisplayMode; presentParams = new PresentationParameters(); presentParams.IsFullScreen = true; presentParams.BackBufferCount = 1; presentParams.SwapEffect = SwapEffect.Discard; // Использует такие же параметры визуализации, как у текущего видеорежима presentParams.BackBufferWidth = displayMode.Width; presentParams.BackBufferHeight = displayMode.Height; presentParams.BackBufferFormat = displayMode.Format; presentParams.FullScreenRefreshRateInHz = displayMode.RefreshRate; 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);
Хотя использование видеорежима рабочего стола дает вполне нормальные результаты, в ряде случаев подобный подход оказывается неприемлемым. В основном это касается дешевых видеокарт, демонстрирующих довольно низкую производительность в современных трехмерных приложениях, в результате чего приемлемая частота смены кадров достигается лишь в самых низких разрешениях вроде 640Ч480 или 800Ч600. Поэтому было логичным предоставить пользователю возможность самостоятельно выбирать параметры видеорежима в зависимости от своих потребностей.
Наше следующее приложение будет отображать на экране перечень всех видеорежимов, поддерживаемых видеокартой и затем переключаться в видеорежим, выбранный пользователем, и визуализировать изображение диска (рисунок 3.7). Эту функциональность достаточно легко реализовать, так как разработчики XNA Framework снабдили класс GraphicsAdapter коллекцией SupportedDisplayModes, содержащей набор структур DisplayMode с информацией обо всех видеорежимах, поддерживаемых указанной связкой видеокарта – монитор.
(рис 3.7) Диалоговое окно выбора видеорежима Итак, создайте новое приложение Windows Forms и добавьте в него новую форму. Присвойте свойствам формы значения согласно таблице 3.2. Поместите на форму компонент ListBox и назовите его displayModeListBox. В заключение, поместите на форму кнопку Ok и присвойте ее свойству DialogResult значение OK.
| Свойство | Значение |
|---|---|
Name |
DisplayModeForm |
Text |
Выберите видеорежим |
FormBorderStyle |
FixedDialog |
MaximizeBox |
False |
MinimizeBox |
False |
Теперь реализуем логику работы формы, а именно: конструктор формы и свойство SelectedDisplayMode, возвращающее информацию о выбранном видеорежиме (листинг 3.9).
public partial class DisplayModeForm : Form
{
// Конструктор формы. В качестве параметра
принимает объект графического адаптера, для
// которого необходимо выбрать графический режим. Public
DisplayModeForm(GraphicsAdapter adapter)
{
InitializeComponent();
// Перебираем все графические режимы, поддерживаемые
графическим адаптером и добавляем их на
// панель displayModeListBox
foreach (DisplayMode diplayMode in adapter.SupportedDisplayModes)
displayModeListBox.Items.Add(diplayMode);
// Выбираем самый первый элемент списка видеорежимов
displayModeListBox.SelectedIndex = 0;
}
// Возвращает информацию о графическом режиме, выбранном пользователем
public DisplayMode SelectedDisplayMode
{
get
{
return (DisplayMode)displayModeListBox.SelectedItem;
}
}
}
И наконец, в обработчик события Load главной формы необходимо вставить код взаимодействия с диалоговым окном выбора видеорежима:
private void MainFormLoad(object sender, EventArgs e)
{
SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
MinimumSize = SizeFromClientSize(new Size(1, 1));
presentParams = new PresentationParameters();
presentParams.IsFullScreen = true;
presentParams.BackBufferCount = 1;
presentParams.SwapEffect = SwapEffect.Discard;
// Создаем диалоговое окно выбора видеорежима
using (DisplayModeForm displayModeForm = new
DisplayModeForm(GraphicsAdapter.DefaultAdapter))
{
// Отображаем диалоговое окно
displayModeForm.ShowDialog();
// Получаем видеорежим, выбранный пользователем
DisplayMode mode = displayModeForm.SelectedDisplayMode;
// Задаем требуемый видеорежим
presentParams.BackBufferWidth = mode.Width;
presentParams.BackBufferHeight = mode.Height;
presentParams.BackBufferFormat = mode.Format;
presentParams.FullScreenRefreshRateInHz = mode.RefreshRate;
}
// Создаем графическое устройство
device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
DeviceType.Hardware,
4> this.Handle, options, presentParams);
}
Оставшийся код приложения не содержит чего-либо интересного, поэтому мы не будем на нем останавливаться. Готовое приложение находится в каталоге Examples\Ch03\Ex05.
Запустите полученное приложение и попробуйте поэкспериментировать с различными разрешениями экрана. Очень скоро вы заметите одну важную особенность: в некоторых видеорежимах диск вместо круглой формы растягивается/сплющивается в эллипсовидную фигуру. После внимательного изучения соотношения сторон разрешений экрана мы прейдем к выводу, что диск корректно отображается лишь при условии совпадения отношения сторон монитора с отношением соответствующих размерностей разрешения экрана. Например, отношение сторон моего монитора NEC MultiSync FE991SB равно 4:3 = 1.33, поэтому в разрешениях 320Ч240, 640Ч480, 800Ч600, 1024Ч768, 1280Ч960, 1600Ч1200, 1792Ч1344 изображение отображается без искажений. В разрешении 1280Ч1024 (соотношение сторон 5:4=1.25) круг едва заметно сплющивается вдоль оси Y, а вот в разрешении 1360Ч768 (соотношение сторон 16:9=1.77) сплющивание круга вдоль оси X становится более чем заметным. С другой стороны, на широкоэкранных мониторах с соотношением сторон 16:9 именно разрешения 1088Ч612, 1280Ч768, 1360Ч768 и 1600Ч900 (т.е. с соотношением сторон 16:9) будут давать изображение без искажений.
(рис 3.8) Искажение формы круглого диска, визуализированного в разрешении 1360Ч768, при отображение на мониторе с соотношение сторон 4:3Чем обусловлено данное явление? Для борьбы с искажениями наше приложение использует область визуализации квадратной формы. Однако если хорошо подумать, равенство ширины и высоты области визуализации еще не гарантирует квадратной области визуализации, так как необходимо еще одно условие: соотношение сторон разрешение экрана должно совпадать с соотношением сторон экрана монитора. Если эта не так, квадратная область визуализации в действительности окажется неквадратной и форма изображения будет искажена.
Примечание
Обратите внимание на отсутствие в списке видеорежимов с форматами пикселей с поддержкой альфа канала. Дело в том, что коллекция SupportedDisplayModes содержит только видеорежимы экранного буфера, а экранный буфер не поддерживает альфа-канал по причине банальной ненужности (см. таблицу 3.1). Так что отсутствие поддержки альфа-канала экранным буфером вовсе не означает отсутствие поддержки форматов с альфа-каналом задним буфером. Более того, если современная видеокарта поддерживает формат SurfaceFormat.Bgr32, то она с вероятность 99.9% поддерживает и формат SurfaceFormat.Color. Впрочем, эти нюансы для нас пока не актуальны, так как наши текущие приложения все равно не используют альфа-канал.
Диалоговое окно выбора видеорежима из примера Ch03\Ex05 хотя и выполняет свою задачу, однако имеет при этом ряд существенных недостатков, затрудняющих его применение в реальных приложениях:
Ну что ж, настало время разработать новую версию диалогового окна, свободную от этих недостатков (рисунок 3.9). Запустите Visual Studio 2005 и создайте приложение Windows Application. Мы начнем с создания класса, инкапсулирующего работу с файлами конфигурации приложения. Наш файл конфигурации будет содержать 7 полей:
| Поле | Тип данных | Описание | Значение по умолчанию |
|---|---|---|---|
Width |
int |
размер заднего буфера вдоль оси X |
0 |
Height |
int |
размер заднего буфера вдоль оси Y |
0 |
Format |
SurfaceFormat |
формат пикселей заднего буфера | SurfaceFormat.Unknown |
RefreshRate |
int |
частота обновления экрана | 0 |
FullScreen |
bool |
работает ли приложение в полноэкранном режиме ( false – нет, true – да) |
true |
Init |
bool |
равен true, если информация о конфигурации приложения была загружена из файла |
false |
ShowSettingForm |
bool |
надо ли отображать при запуске приложения диалоговое окно выбора видеорежима | true |
Так как наш файл конфигурации не будет представлять собой нечего экстравагантного, все операции по созданию класса вполне можно выполнить в дизайнере Visual Studio. Для этого в окне Solution Explorer раскройте узел Properties, и сделайте двойной щелчок левой кнопкой мыши на узле Setting.settings, после чего слева откроется дизайнер файла конфигурации

(рис 3.10) Новая версия диалогового окна выбора видеорежима (рис 3.9) Настройка параметров конфигурации приложения Теперь приступим к созданию диалогового окна настройки параметров приложения. Добавьте приложение в новую форму, разместите на ней элементы управления аналогично рисунку 3.9 и задайте их свойства согласно таблице 3.4.
| Класс | Свойство | Значение |
|---|---|---|
Form (диалоговое окно) |
Name |
SettingsForm |
Text |
Параметры | |
FormatBorderStyle |
FixedDialog |
|
MinimizeBox |
false |
|
MaximizeBox |
false |
|
CheckBox |
Name |
inWindowCheckBox |
Text |
Визуализировать в окне | |
GroupBox |
Text |
Видеорежим |
Label |
Text |
Соотношение сторон |
ComboBox (фильтр отображаемых видеорежимов по соотношению сторон) |
Name |
aspectRatioComboBox |
DropDownStyle |
DropDownList |
|
Label |
Text |
Разрешение |
ComboBox (список разрешений экрана) |
Name |
resolutionComboBox |
DropDownStyle |
DropDownList |
|
Label |
Text |
Глубина цвета: |
ComboBox (список форматов пикселей) |
Name |
colorDepthComboBox |
DropDownStyle |
DropDownList |
|
Label |
Text |
Частота обновления |
ComboBox (список частот обновления экрана) |
Name |
refreshRateComboBox |
DropDownStyle |
DropDownList |
|
Button |
Name |
okButton |
Text |
Ok |
|
Button |
Text |
Cancel |
DialogResult |
Cancel |
Как видно, диалоговое окно содержит списки соотношений сторон экрана, разрешений, глубины цвета (формата пикселей) и частот обновления экрана. Информация в этих списках должна отображаться в дружелюбной форме, ведь неподготовленный пользователь вряд ли сможет без подсказки понять разницу между форматами Bgr32 и Bgr565. Кроме того, информация в списках должна быть отсортирована по некоторому критерию: например, видеорежимы должны перечислять в порядке увеличения разрешения экрана. Чтобы реализовать эти требования придется создать четыре структуры (по одной на каждый список), инкапсулирующие элементы списков. Для этого необходимо добавить в файл диалогового окна код из листинга 3.11.
// Инкапсулирует элемент списка соотношений сторон
public struct AspectItem
{
// Коэффициент отношения сторон
public float aspect;
public AspectItem(float aspect)
{
this.aspect = aspect;
}
// Проверяет указанный элемент списка разрешений
(ResolutionItem) на соответствие текущему
// соотношению сторон. В случае равенства метод
возвращает true, иначе - false public bool
Compare(float aspect)
{
// Нулевое соотношение сторон является зарезервированным
значением ( "любое соотношение
// сторон "), поэтому возвращаем true if
(this.aspect == 0) return true;
// Сравниваем коэффициенты отношения сторон,
закладывая небольшой "запас " в
один процент.
// Дело в том, что некоторые режимы имеют не совсем
"академически " правильное
соотношение
// сторон. Например, соотношение сторон разрешения
1360 x 768 равно 85/48=1.771, в
// как 16/9=1.778.
if (Math.Abs(this.aspect - aspect) < this.aspect * 0.01f)
return true;
else
return false;
}
// Возвращает текущее соотношение сторон в удобочитаемом виде
public override string ToString()
{
if (aspect == 0)
return "Любое";
if (Compare(4.0f / 3.0f))
return "4 : 3";
if (Compare(5.0f / 4.0f))
return "5 : 4";
if (Compare(16.0f / 9.0f)) return "16 : 9";
return aspect.ToString();
}
}
// Инкапсулирует элемент списка разрешений экрана
public struct ResolutionItem : IComparable<ResolutionItem>
{
// Разрешение экрана
public int width;
public int height;
public ResolutionItem(int width, int height)
{
this.width = width;
this.height = height;
}
// Отношение количества пикселей вдоль осей X и Y для
текущего разрешения экрана
public float Aspect
{
get
{
return (float)width / (float)height;
}
}
// Возвращает в удобочитаемом виде текст элемента
списка public override string ToString()
{
return width.ToString() + " x " + height.ToString();
}
// Реализация интерфейса IComparable<ResolutionItem>,
используемого при сортировке элементов
// списка
public int CompareTo(ResolutionItem other)
{
// Сначала сравнивает ширина разрешений if (width > other.width)
return 1;
if (width < other.width)
return -1;
// Если ширина одинаковая, сравнивается высота разрешений.
В результате разрешения сначала
// сортируются по ширине, а затем по высоте. if (height >
other.height) return 1;
if (height < other.height) return -1;
return 0;
}
}
// Инкапсулирует элемент списка форматов пикселей
public struct ColorDepthItem : IComparable<ColorDepthItem>
{
// Формат пикселей
public SurfaceFormat value;
public ColorDepthItem(SurfaceFormat value)
{
this.value = value;
}
// Возвращает строку с описанием формата пикселя
public override string ToString()
{
string str;
switch (value)
{
case SurfaceFormat.Bgr32:
str = "32 бита ({0})";
break;
case SurfaceFormat.Bgr565:
str = "16 бит ({0})";
break; case
SurfaceFormat.Rgba1010102:
str = "32 бит ({0})";
break; case
SurfaceFormat.Bgr555:
str = "16 бит {0}";
break; default:
str = "{0}";
break;
}
return string.Format(str, value);
}
// Реализация интерфейса IComparable<ColorDepthItem>,
используемого при сортировке элементов
// списка
public int CompareTo(ColorDepthItem other)
{
if (value > other.value)
return -1;
if (value < other.value)
return 1;
return 0;
}
}
// Инкапсулирует элемент списка частот обновлений экрана public
struct RefreshRateItem : IComparable<RefreshRateItem>
{
// Частота обновления экрана
public int value;
public RefreshRateItem(int value)
{
this.value = value;
}
// Возвращает строку с частотой обновления экрана
public override string ToString()
{
return value.ToString() + " Гц";
}
// Реализация интерфейса IComparable< RefreshRateItem>,
используемого при сортировке
// элементов списка
public int CompareTo(RefreshRateItem other)
{
if (value > other.value)
return 1;
if (value < other.value)
return -1;
return 0;
}
}
Следующий шаг - создание конструктора формы, принимающего в качестве параметров текущие настройки приложения и объект графического адаптера, используемого приложением (листинг 3.12).
public partial class SettingsForm : Form
{
// Набор ассоциативных сортированных массив с
информаций о поддерживаемых видеорежимах
SortedDictionary<ResolutionItem, SortedDictionary<ColorDepthItem,
SortedDictionary<RefreshRateItem, bool>>> modeCollection;
// Текущие настройки приложения. Класс Properties.Settings
автоматически генерируется
// дизайнером настроек приложения (рисунок 3.10).
Properties.Settings settings;
// Конструктор формы
internal SettingsForm(Properties.Settings settings,
GraphicsAdapter adapter)
{
InitializeComponent();
// Сохраняем ссылку на настройки приложения
this.settings = settings;
// Если файл с конфигурацией приложения не был найден,
устанавливаем в качестве параметров по
// умолчанию настройки рабочего стола if (!settings.Init)
{
settings.Width = adapter.CurrentDisplayMode.Width;
settings.Height = adapter.CurrentDisplayMode.Height;
settings.Format = adapter.CurrentDisplayMode.Format;
settings.RefreshRate = adapter.CurrentDisplayMode.RefreshRate;
settings.FullScreen = true;
}
modeCollection = new SortedDictionary<ResolutionItem,
4> SortedDictionary<ColorDepthItem, SortedDictionary<RefreshRateItem,
bool>>>();
// Перебираем все видеорежимы, поддерживаемые видеокартой и
заполняем ассоциативный массив
// modeCollection
foreach (DisplayMode mode in adapter.SupportedDisplayModes)
{
ResolutionItem res = new ResolutionItem(mode.Width, mode.Height);
if (!modeCollection.ContainsKey(res))
modeCollection.Add(res, new SortedDictionary<ColorDepthItem,
SortedDictionary<RefreshRateItem, bool>>());
ColorDepthItem depth = new ColorDepthItem(mode.Format); if
(!modeCollection[res].ContainsKey(depth))
modeCollection[res].Add(depth, new SortedDictionary<RefreshRateItem,
bool>());
RefreshRateItem refresh = new RefreshRateItem(mode.RefreshRate); if
(!modeCollection[res][depth].ContainsKey(refresh))
modeCollection[res][depth].Add(refresh, true); }
// Задаем состояние флага "визуализировать в окне "
inWindowCheckBox.Checked = !settings.FullScreen;
// Добавляем в список "соотношение сторон " типовые фильтры
видеорежимов aspectRatioComboBox.Items.Add(new AspectItem());
aspectRatioComboBox.Items.Add(new AspectItem(4.0f / 3.0f));
aspectRatioComboBox.Items.Add(new AspectItem(5.0f / 4.0f));
aspectRatioComboBox.Items.Add(new AspectItem(16.0f / 9.0f));
// Выбираем самый первый элемент списка ( "Любое ")
aspectRatioComboBox.SelectedIndex = 0; }
}
Как видно, информация о поддерживаемых видеорежимах хранится в многомерном ассоциативном массиве, упрощающем поиск информации о требуемых видеорежимах. Например, для проверки существования видеорежима с разрешением 1024x768x32bpp:@85Hz приложение должно проверить существование элемента modeCollection[ "1024x768 "][ SurfaceFormat.Brg32][85] . Последний оператор конструктора выбирает нулевой элемент списка, генерируя событие SelectedIndexChanged, обработчик которого приведен в листинге 3.13.
// Обновляет список поддерживаемых разрешений экрана с
учетом нового фильтра соотношения
// сторон
private void aspectRatioComboBoxSelectedIndexChanged(object
sender, EventArgs e)
{
// Получаем текущий элемент списка "соотношение сторон "
AspectItem aspect = (AspectItem)aspectRatioComboBox.SelectedItem;
// Текущее разрешение экрана
ResolutionItem currentResolution;
// Используем в качестве текущего разрешения экрана элемент
из списка "Разрешение ". Если же
// элемент в списке не выбран, используем разрешение
экрана из текущих настроек приложения if
(resolutionComboBox.SelectedIndex != -1)
currentResolution = (ResolutionItem)resolutionComboBox.
SelectedItem; else
currentResolution = new ResolutionItem(settings.Width,
settings.Height);
// Очищаем список разрешений экрана
resolutionComboBox.Items.Clear();
// Перебираем все разрешения экрана
foreach (ResolutionItem res in modeCollection.Keys)
// Если разрешение экрана удовлетворяет заданному соотношению сторон
if (aspect.Compare(res.Aspect))
// Добавляем его в список разрешений
resolutionComboBox.Items.Add(res);
// Если было найдено хотя бы одно разрешение экрана,
соответствующее заданному соотношению
// сторон
if (resolutionComboBox.Items.Count != 0)
{
// В списке разрешений экрана пытаемся выбрать текущее разрешение
resolutionComboBox.SelectedItem = currentResolution;
// Если это не удалось, выбираем самый первый элемент списка if
(resolutionComboBox.SelectedIndex == -1)
resolutionComboBox.SelectedIndex = 0; }
// Вызываем обработчик события изменения состояния
переключателя "Визуализировать в окне ",
// который при необходимости активирует/деактивирует
определенные элементы формы вроде кнопки
// Ok
inWindowCheckBox_CheckedChanged(inWindowCheckBox, null);
}
// Обработчик события CheckedChange флага "Визуализировать
в окне ",
private void inWindowCheckBox_CheckedChanged(object sender, EventArgs e)
{
// Если флаг включен
if (inWindowCheckBox.Checked)
{
// Деактивируем все списки, связанные с параметрами
полноэкранного режима
aspectRatioComboBox.Enabled = false; resolutionComboBox.Enabled =
false; colorDepthComboBox.Enabled = false; refreshRateComboBox.Enabled
= false; okButton.Enabled = true;
}
else
{
// Активируем список "Соотношение сторон "
aspectRatioComboBox.Enabled = true;
// Если список доступных разрешений экрана не пустой if
(resolutionComboBox.Items.Count > 0)
{
// Активируем оставшиеся три списка и кнопку Ok
resolutionComboBox.Enabled = true;
colorDepthComboBox.Enabled = true;
refreshRateComboBox.Enabled = true;
okButton.Enabled = true;
}
else
{
// В противном случае блокируем эти элементы управления
resolutionComboBox.Enabled = false;
colorDepthComboBox.Enabled = false;
refreshRateComboBox.Enabled = false; okButton.Enabled =
false;
}
}
}
Так как выбранное разрешение оказывает влияние на доступные форматы пикселей списка "Глубина цвета ", необходимо определить обработчик события SelectedIndexChanged списка разрешений (листинг 3.14).
private void resolutionComboBoxSelectedIndexChanged(object sender, EventArgs e)
{
// Получаем выбранный элемент списка разрешений экрана
ResolutionItem res = (ResolutionItem) resolutionComboBox.SelectedItem;
// Текущая глубина цвета
ColorDepthItem currentColorDepth;
// Используем в качестве текущей глубины цвета
элемент из списка "Глубина цвета ".
Если же
// элемент в списке не выбран, используем глубину
цвета из текущих настроек приложения
if (colorDepthComboBox.SelectedIndex != -1)
currentColorDepth = (ColorDepthItem)colorDepthComboBox.SelectedItem;
else
currentColorDepth = new ColorDepthItem(settings.Format);
// Очищаем список "Глубина цвета "
colorDepthComboBox.Items.Clear();
// Перебираем все форматы пикселей выбранного
разрешения экрана и добавляем их в список
// "Глубина цвета "
foreach (ColorDepthItem colorDepth in modeCollection[res].Keys)
colorDepthComboBox.Items.Add(colorDepth);
// Пытаемся выбрать глубину цвета, как у текущих настроек приложения
colorDepthComboBox.SelectedItem = currentColorDepth;
// В случае неудачи выбираем первый элемент списка
if (colorDepthComboBox.SelectedIndex == -1)
colorDepthComboBox.SelectedIndex = 0;
}
Список поддерживаемых частот экрана, разумеется, тоже зависит от выбранной глубины цвета и разрешения экрана, соответственно, необходимо реализовать и обработчик события SelectedIndexChanged списка глубины цвета (листинг 3.15).
private void colorDepthComboBox_SelectedIndex
Changed(object sender, EventArgs e)
{
// Получаем выбранное разрешение и глубину цвета (формат пикселей)
ResolutionItem res = (ResolutionItem)resolutionComboBox.SelectedItem;
ColorDepthItem colorDepth = (ColorDepthItem)
colorDepthComboBox.SelectedItem;
// Текущая частота обновления экрана
RefreshRateItem currentRefreshRate;
// Используем в качестве текущей частоты обновления
экрана элемент из списка. Если же элемент
// в списке не выбран, используем частоту обновления
экрана из текущих настроек приложения if
(refreshRateComboBox.SelectedIndex != -1)
currentRefreshRate = (RefreshRateItem)refreshRate
ComboBox.SelectedItem; else
currentRefreshRate = new RefreshRateItem(settings.RefreshRate);
// Очищаем список "Частота обновления "
refreshRateComboBox.Items.Clear();
// Перебираем частоты обновления экрана, поддерживаемые
выбранным разрешением с указанной
// глубиной цвета
foreach (RefreshRateItem refreshRate in modeCollection[res]
[colorDepth].Keys)
refreshRateComboBox.Items.Add(refreshRate);
// Выбираем в списке "Частота обновления "
текущую частоту обновления экрана
refreshRateComboBox.SelectedItem = currentRefreshRate;
// Если такого элемента нет, выбираем самый первый элемент списка
if (refreshRateComboBox.SelectedIndex == -1)
refreshRateComboBox.SelectedIndex = 0;
}
Завершая создание диалогового окна, мы должны реализовать обработчик нажатия кнопки Ok:
private void okButton_Click(object sender, EventArgs e)
{
// Обновляем настройки приложения на основе текущего
состояния элементов управления
// формы окна
settings.FullScreen = !inWindowCheckBox.Checked;
if (!inWindowCheckBox.Checked)
{
settings.Width = ((ResolutionItem)resolutionComboBox.SelectedItem).width;
settings.Height = ((ResolutionItem)resolutionComboBox.SelectedItem).height;
settings.Format = ((ColorDepthItem)colorDepthComboBox.SelectedItem).value;
settings.RefreshRate = ((RefreshRateItem)refreshRate
ComboBox.SelectedItem).value;
}
// Настройки были инициализированы. При следующем показа
диалогового окна все элементы
// управления будут инициализированы согласно значениями
сохраненных настроек
settings.Init = true;
// Пользователь настроил приложение. Показывать диалоговое
окно больше нет необходимости.
settings.ShowSettingForm = false;
DialogResult = DialogResult.OK;
}
И так, у нас есть класс для работы с настройками приложения и диалоговое окно для визуального управления этими настройками. Теперь самое время интегрировать эту функциональность в наше приложение. Для начала мы должны определиться со стратегией поведения приложения, а именно, в каких случаях оно должно отображать диалоговое окно:
Init класса Settings равному true.ShowSettingForm конфигурации приложения значение true и завершить работу. А при следующем запуске приложение обнаружит, что свойство ShowSettingForm равно true, и отобразит диалоговое окно с настройками приложения.Load главной формы приложения распознавание ключа /config в параметрах командной строки приложения. Таким образом, инсталлятор приложения наряду с ярлыком запуска приложения может создать дополнительный ярлык "Настройка приложения ", вызывающий это же приложение с параметром /config.Код, реализующий всю вышеперечисленную функциональность, будет иметь достаточно большой размер, поэтому его логично будет инкапсулировать в отдельный метод, чтобы не захламлять обработчик события Load (листинг 3.17).
using System.Diagnostics;
void InitGraphivsDevice()
{
SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
MinimumSize = SizeFromClientSize(new Size(1, 1));
// Загружаем настройки приложения из файла
settings = new Properties.Settings();
// Определяем, содержит ли командная строка параметр "/config
" string[] args = Environment.GetCommandLineArgs(); bool
configParam = false;
if ((args.Length == 2) (args[1].ToUpper() == "/CONFIG"))
configParam = true;
// Если выполняется одно из трех вышеописанных условий
if ((configParam) || (settings.ShowSettingForm) || (!settings.Init))
{
// Создаем диалоговое окно
using (SettingsForm settingsForm = new SettingsForm(settings,
4> GraphicsAdapter.DefaultAdapter))
{
// Отображаем диалоговое
окно
settingsForm.ShowDialog();
// Если командная строка содержит "/config
" if (configParam)
{
// Завершаем работу приложения closing =true;
Application.Idle += new EventHandler(ApplicationIdle); return;
}
}
}
presentParams = new PresentationParameters();
presentParams.BackBufferCount = 1;
presentParams.SwapEffect = SwapEffect.Discard;
// Настраиваем параметры визуализации согласно настройкам приложения
presentParams.IsFullScreen = settings.FullScreen;
if (settings.FullScreen)
{
presentParams.BackBufferWidth = settings.Width;
presentParams.BackBufferHeight = settings.Height;
presentParams.BackBufferFormat = settings.Format;
presentParams.FullScreenRefreshRateInHz = settings.RefreshRate;
}
else
{
presentParams.BackBufferWidth = 0;
presentParams.BackBufferHeight = 0;
presentParams.BackBufferFormat = SurfaceFormat.Unknown;
presentParams.FullScreenRefreshRateInHz = 0;
}
// Проверяем наличие аппаратных вершинных процессоров
GraphicsDeviceCapabilities caps = GraphicsAdapter.
DefaultAdapter.GetCapabilities(
DeviceType.Hardware);
CreateOptions options = CreateOptions.SingleThreaded;
if (caps.DeviceCapabilities.SupportsHardwareTransformAndLight)
options |= CreateOptions.HardwareVertexProcessing; else
options |= CreateOptions.SoftwareVertexProcessing;
try
{
// Создаем графическое устройство
device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
DeviceType.Hardware,
this.Handle,
options, presentParams);
}
catch (InvalidOperationException)
{
// Если при создании графического устройства возникли проблемы
closing = true;
Application.Idle += new EventHandler(ApplicationIdle);
// Отображаем диалоговое окно с предложением изменить параметры
визуализации
if (MessageBox.Show("Ошибка при создании графического устройства.
Изменить " +
"параметры визуализации (разрешение экрана и т.п.)?", "Ошибка",
MessageBoxButtons.YesNo,
MessageBoxIcon.Error) == DialogResult.Yes)
{
// При положительном ответе устанавливаем параметр ShowSettingForm
в значение true,
// указывающий на необходимость показать окно с настройками
приложения при следующем запуске
settings.ShowSettingForm = true;
// Сохраняем настройки приложения в файл
settings.Save();
// Перезапускаем приложение
Process.Start(Application.ExecutablePath);
}
return;
}
}
private void MainFormLoad(object sender, EventArgs e)
{
// Загружаем настройки из файла и создаем графическое устройство
InitGraphivsDevice();
// Если приложение завершает работу, выходим из обработчика
события Load
if (closing)
return;
// Остальные действие (создание декларации вершины, заполнение
массива вершин, загрузка
// эффекта и т.п.
...
}
Остальные фрагменты приложения не содержат чего-либо заслуживающего внимания, и вы сможете легко их реализовать самостоятельно. Готовое приложение находится в example.zip в каталоге Examples\Ch03\Ex03.
Местоположение файла настроек приложения
.NET Framework 2.0 сохраняет пользовательские настройки в XML -файле user.config, который располагается по достаточно запутанному пути:
<Profile Directory>\<Company Name>\<App Name><Evidence Type><Evidence Hash>\<Version>\user.config
где
Profile Directory - каталог локального профиля приложения, обычно имеющий название вроде c:\Documents and Settings\<Имя Пользователя>\Local Settings\Application DataCompany Name - строка, формируемая на основе названия компании, заданного атрибутом AssemblyCompany в файле Properties\AssemblyInfo.cs.App Name - строка, формируемая на основе названия приложения, заданного атрибутом AssemblyProduct в файле Properties\AssemblyInfo.cs.Evidence Type, Evidence Hash - вычисляются на основе информации о Version - строка, формируемая на основе версии приложения, заданной атрибутом AssemblyVersion в файле Properties\AssemblyInfo.cs.Например, на моем компьютере пример Сh03\Ex06 хранит информацию о конфигурации приложения в файле C:\Documents and Settings\Administrator\Local Settings\Application Data\GSPInc\Ex06-FullscreenDialog.Url31gvfqvzegievbxah0w1qmgu2s2siyo3\1.0.0.0\user.config. Сам файл имеет простую структуру и легко может быть проанализирован любым продвинутым пользователем:
<?xml version="1.0" encoding="utf-8"?> <configuration> <userSettings> <GSP.XNA.Book.Ch03.Ex06.Properties.Settings> <setting name="Width" serializeAs="String"> <value>1280</value> </setting> <setting name="Height" serializeAs="String"> <value>960</value> </setting> <setting name="Format" erializeAs="String"> <value>Bgr32</value> </setting> <setting name="RefreshRate" serializeAs="String"> <value>85</value> </setting> <setting name="FullScreen" serializeAs="String"> <value>True</value> </setting> <setting name="Init" serializeAs="String"> <value>True</value> </setting> <setting name="ShowSettingForm" serializeAs="String"> <value>False</value> </setting> </GSP.XNA.Book.Ch03.Ex06.Properties.Settings> </userSettings> </configuration>
Это обстоятельство позволяет редактировать файл в обычном текстовом редакторе, что очень полезно при отладке приложения. Например, в целях повышения "дуракоустройчивости " можно легко протестировать поведение приложения при некорректном значении параметров файла конфигурации.
До сих пор мы визуализировали исключительно статичные изображение, в то время как XNA Framework в первую очередь предназначен для визуализация динамичных сцен с движущимися объектами. Как создать анимированную сцену? Обратимся к
И так, для создания анимации мы должны отображать визуализировать различные фазы движения изображения с частотой 25 кадров в секунду. Наше первое приложение будет визуализировать шарик (точнее диск), летающий по форме и отскакивающий от ее стенок (рисунок 3.11). Анимация будет моделироваться посредством таймера (компонент Timer ), тикающего с интервалом 40 миллисекунд (то есть 25 раз в секунду). После каждого тика таймера мы будем прибавлять к координатам шарика значение вектора скорости и перерисовывать сцену. При пролете шарика сквозь стенку он будет отскакивать обратно, при этом вектор скорости будет изменяться на противоположный. Для придания движениям шарика некоторой неопределенности, модуль вектора скорости будет изменяться на незначительную случайную величину. Основные фрагменты приложения приведены в листинге 3.18. Исходный код примера находится в example.zip в каталоге Examples\Ch03\Ex07.

(рис 3.18) Прыгающий шарик (рис 3.11) public partial class MainForm : Form { // Файл эффекта, используемый для визуализации изображения const string effectFileName = "Data\\ColorFill.fx"; // Минимальная скорость диска ( "шарика ") const float diskMinSpeed = 0.5f; // Максимальная скорость диска const float diskMaxSpeed = 1.3f; // Количество сегментов в диске const int diskSlices = 32; // Радиус диска const float diskRadius = 0.1f; // Цвет центра диска readonly static XnaGraphics.Color diskInnerColor = XnaGraphics.Color.White; // Цвет края диска readonly static XnaGraphics.Color diskOuterColor = XnaGraphics.Color.Green; // Толщина стенки вдоль границы экрана const float borderSize = 0.1f; // Цвет внутренней границы стенки readonly static XnaGraphics.Color borderInnerColor = XnaGraphics.Color.DarkBlue; // Цвет внешней границы стенки readonly static XnaGraphics.Color borderOuterColor = XnaGraphics.Color.CornflowerBlue; GraphicsDevice device = null; PresentationParameters presentParams; Effect effect = null; VertexDeclaration decl = null; // Массив вершин стены вдоль края формы VertexPositionColor[] borderVerts = null; // Массив вершин диска с центром в начале системы координат VertexPositionColor[] baseDiskVerts = null; // Массив вершин диска, перемещаемого по поверхности экрана VertexPositionColor[] diskVerts = null; FillMode fillMode = FillMode.Solid; // Скорость диска вдоль оси X float speedX; // Скорость диска вдоль оси Y float speedY; // Координата X центра диска float posX = 0; // Координата Y центра диска float posY = 0; // Генератор случайных чисел Random rnd = new Random(); // Конфигурация приложения (разрешение экрана и т.п.) Properties.Settings settings; bool closing = false; // Вычисляет случайную скорость диска, лежащую в диапазоне diskMinSpeed .. diskMaxSpeed float RndSpeed() { return diskMinSpeed + (float)rnd.NextDouble() * (diskMaxSpeed - diskMinSpeed); } // Обработчик события Load главной формы private void MainForm_Load(object sender, EventArgs e) { // Чтение файла конфигурации и создание графического устройства InitGraphivsDevice(); if (closing) return; // Создание декларации вершины decl = new VertexDeclaration(device, VertexPositionColor.VertexElements); // Создание и заполнение массива вершин, визуализируемого с использованием списка // треугольников (PrimitiveType.TriangleStrip) borderVerts = new VertexPositionColor[10]; borderVerts[0] = new VertexPositionColor(new Vector3(-1.0f, -1.0f, 0.0f), borderOuterColor); borderVerts[1] = new VertexPositionColor(new Vector3(-1.0f + borderSize, -1.0f + borderSize, 0.0f), borderInnerColor); borderVerts[2] = new VertexPositionColor(new Vector3(-1.0f, 1.0f, 0.0f), borderOuterColor); borderVerts[3] = new VertexPositionColor(new Vector3(-1.0f + borderSize, 1.0f -borderSize, 0.0f), borderInnerColor); borderVerts[4] = new VertexPositionColor(new Vector3(1.0f, 1.0f, 0.0f), borderOuterColor); borderVerts[5] = new VertexPositionColor(new Vector3(1.0f - borderSize, 1.0f -borderSize, 0.0f), borderInnerColor); borderVerts[6] = new VertexPositionColor(new Vector3(1.0f, -1.0f, 0.0f), borderOuterColor); borderVerts[7] = new VertexPositionColor(new Vector3(1.0f - borderSize, -1.0f + borderSize, 0.0f), borderInnerColor); borderVerts[8] = new VertexPositionColor(new Vector3(-1.0f, -1.0f, 0.0f), borderOuterColor); borderVerts[9] = new VertexPositionColor(new Vector3(-1.0f + borderSize, -1.0f + borderSize, 0.0f), borderInnerColor); // Создание диска с центром в начале координат. Диск визуализируется с использованием веера // треугольников (PrimitiveType.TriangleFan) baseDiskVerts = new VertexPositionColor[diskSlices + 2]; baseDiskVerts[0] = new VertexPositionColor(new Vector3(0.0f, 0.0f, 0.0f), XnaGraphics.Color.White); for (int i = 0; i <= diskSlices; i++) { float angle = (float)i / (float)diskSlices * 2.0f * (float)Math.PI; float x = diskRadius * (float)Math.Sin(angle); float y = diskRadius * (float)Math.Cos(angle); baseDiskVerts[i + 1] = new VertexPositionColor(new Vector3(x, y, 0.0f), diskOuterColor); }; // Создаем массив вершин диска путем клонирования. Таким образом, при старте приложения диск // расположен в начале системы координат. diskVerts = (VertexPositionColor[]) baseDiskVerts.Clone(); // Задаем начальную скорость диска вдоль осей X и Y speedX = RndSpeed(); speedY = RndSpeed(); } // Обработчик события Paint главной формы private void MainForm_Paint(object sender, PaintEventArgs e) { ... // Визуализируем сцену effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); device.DrawUserPrimitives(PrimitiveType.TriangleStrip, borderVerts, 0, borderVerts.Length - 2); device.DrawUserPrimitives(PrimitiveType.TriangleFan, diskVerts, 0, diskVerts.Length - 2); pass.End(); } effect.End(); device.Present(); ... } // Обработчик события Tick таймера, свойству Interval которого присвоено значение 40 private void timer_Tick(object sender, EventArgs e) { // Изменяем координаты центра диска на расстояние, которое он должен пройти за время между // двумя тиками таймера posX += speedX * (float)timer.Interval * 0.001f; posY += speedY * (float)timer.Interval * 0.001f; // Если диск столкнулся с правым краем границы if (posX >= 1 - diskRadius - borderSize) { // Диск не должен перелетать за границу posX = 1 - diskRadius - borderSize; // Изменяем направление движения диска на противоположное и вычисляем новую скорость. speedX = -Math.Sign(speedX) * RndSpeed(); } // Если диск столкнулся с верхним краем границы if (posY >= 1 - diskRadius - borderSize) { posY = 1 - diskRadius - borderSize; speedY = -Math.Sign(speedY) * RndSpeed(); } // Если диск столкнулся с левым краем границы if (posX <= -1 + diskRadius + borderSize) posX = -1 + diskRadius + borderSize; speedX = -Math.Sign(speedX) * RndSpeed(); } // Если диск столкнулся с нижним краем границы if (posY <= -1 + diskRadius + borderSize) { posY = -1 + diskRadius + borderSize; speedY = -Math.Sign(speedY) * RndSpeed(); } // Вычисляем новые координаты диска на основе "эталонного " диска с центром в начале // координат. for (int i = 0; i < baseDiskVerts.Length; i++) { diskVerts[i].Position.X = baseDiskVerts[i].Position.X + posX; diskVerts[i].Position.Y = baseDiskVerts[i].Position.Y + posY; } // Перерисовываем изображение Invalidate(); } }
Расчет координат вершин диска, использующий тригонометрические функции, является весьма ресурсоемкой операцией, поэтому в приложении используется небольшая хитрость. Вместо многократного расчета вершин диска приложение рассчитывает только координаты "эталонного " диска с центром в начале системы координат, которые заносятся в массив baseDiskVerts. После этого для получения вершин заданной окружности необходимо сместить все вершины "эталонной " окружности на расстояние заданной окружности от центра.
Запустите приложение на выполнение. Первое что бросится в глаза – это движение окружности рывками. Но ведь этого не может быть! Фильмы то при частоте 25 fps идут очень плавно. В чем же принципиальная разница между фильмом и нашим приложением? Все очень просто. В кинематографе кадры снимается с некоторой FPS в кинофильме и 25 FPS в нашем приложении – это немного разные FPS. В частности, в компьютерных играх при частоте кадров 25-30 FPS ощущаются некие "подергивания " в динамичных сценах – объекты движутся как бы рывками. Эту проблему решают методом грубой силы, то есть простым увеличением частоты кадров – практический опыт показыва
ет, что в большинстве случаев вполне достаточно частоты кадров 60 FPS.
Дополнительная информация
Для визуализации быстродвижущихся объектов вроде спортивного автомобиля, пролетающего мимо наблюдателя со скоростью 300 км/ч, даже частота кадров 60 FPS оказывается недостаточной. С другой стороны, при попытке наращивания FPS мы упираемся в возможности аппаратуры – максимальная частота вертикальной развертки современных мониторов (то есть частота смены кадров) редко превышает 60-100 Hz. Поэтому для повышения реалистичности движений применяют технологии Motion
Ну что ж, попробуем увеличить частоту смены кадров. Как известно, компонент таймер может тикать до 64-х раз в секунду, т.е. минимальный интервал между двумя тиками равен 0.015 миллисекунд. Присвойте свойству Interval таймера значение 15 и снова запустите приложение. Шарик будет двигаться ощутимо плавнее, однако скорость не будет постоянной: периодически шарик будет ни с того то замедляться, то ускоряться.
Почему это происходит? Для ответа на этот вопрос необходимо внимательно прочитать описания таймера Windows в MSDN. Хотя таймер и может тикать с интервалом 15 миллисекунд, точность каждого тика составляет 55 миллисекунд. Таким образом, по мере уменьшения интервала между тиками, точность таймера катастрофически падает, что приводит к неравномерности движения объектов. Но есть еще один немаловажный фактор – события от таймера имеют самый низкий приоритет среди всех сообщений. Например, если пользователь в это время нажимает клавишу клавиатуры, а в очереди сообщений находится необработанное сообщение WM_TIMER, то сообщения WM_KEYDOWN, WM_CHAR и WM_KEYUP будут вставлены в очередь перед сообщением WM_TIMER. По сути, сообщение WM_TIMER обрабатывается только при условии пустой очереди событий потока, а так как в очереди может находиться не более одного сообщения wmtimer, таймер может "терять " тики. Чтобы убедиться в этом, попробуйте ухватиться указателем мыши за край формы и поизменять ее размер, что тут же приведет к потере сообщений и, соответственно, замедлению шарика.
Эти недостатки компонента Timer затрудняют его применение в реальных игровых приложениях. Впрочем, данный компонент предназначен для реализации задач, не критичных к точности
Итак, использование обычного компонента Timer не увенчалось успехом из-за ряда ограничений Windows. Кроме того, использование таймера для визуализации сцены обладает еще одним фундаментальным ограничением. Дело в том, что, задавая определенную частоту визуализации кадров, мы делаем неявное предположение, что абсолютно любой компьютер может визуализировать сцену с требуемой частотой кадров. Если это вдруг окажется не так, приложение будет работать в "замедленном " режиме. Учитывая, что производительность видеоподсистем различных компьютеров может отличаться в десятки раз, возникновение подобной проблемы более чем вероятно.
Что же делать? Раз визуализация сцены с фиксированной частотой кадров чревата возникновением множества проблем, надо просто визуализировать кадры с максимально возможной частотой, для чего достаточно поместить в обработчик события Idle вызов метода Invalidate. Для измерения временных интервалов между вызовами обработчика события Idle можно воспользоваться свойством System.Enviroment.TickCount, возвращающим количество миллисекунд, прошедших с момента загрузки операционной системы. Основные фрагменты нового варианта кода приведены в листинге 3.19.
// Пример Examples\Ch03\Ex08
public partial class MainForm : Form
{
// Значение свойства System.Enviroment.TickCount
во время последнего вызова обработчика
// события Idle.
int lastTick;
private void MainForm_Load(object sender, EventArgs e)
{ ...
// Запоминаем текущее значение свойства System.Enviroment.TickCount
lastTick = Environment.TickCount;
Application.Idle += new EventHandler(Application_Idle);
}
void Application_Idle(object sender, EventArgs e)
{
// Если приложение завершает работу, закрываем
главную форму. if (closing)
{
Close(); return; }
int currentTick = Environment.TickCount;
// Вычисляем время (в секундах), прошедшее между
двумя вызовами обработчика Idle float
delta = (float)(currentTick - lastTick) * 0.001f;
// Изменяем положение диска
posX += speedX * delta; posY += speedY * delta;
// Обрабатываем столкновение с стеной вдоль границы экрана
...
// Запоминаем текущее время с момента загрузки операционной системы
lastTick = currentTick;
// Перерисовываем экран
Invalidate();
}
...
}
Шарик примера Ch03\Ex08 движется ощутимо плавнее, однако небольшие, еле заметные рывки все же остались. Для определения причины рывков вставьте в обработчик события Idle команду Trace.WriteLine(delta), запустите приложение, вернитесь в Visual Studio и посмотрите содержимое окна Output. Скорее всего, его содержимое будет выглядеть примерно следующим образом:
delta = 0,078 delta = 0 delta = 0 delta = 0,016 delta = 0,015 delta = 0 delta = 0,016 delta = 0,015 delta = 0,016 delta = 0 delta = 0,016
Обратите внимание на множество нулевых значений параметра delta, соответствующих полной остановке шарика на месте, которая воспринимается пользователем как небольшое подергивание. Почему между некоторыми вызовами события Idle проходит 15-16 миллисекунд, а между другими – меньше одной миллисекунды. Подобный разброс производительности не выглядит правдоподобным, значит дело в чем-то ином. Для выяснения причин этой аномалии придется обратиться к MSDN. Метод Environment.TickCount использует функцию Win32 GeTickCount, точность которой порядка 10 миллисекунд. Соответственно, при попытке измерить временной интервал близкий к 10 миллисекундам метод Environment.TickCount может вернуть два одинаковых значения, что мы и наблюдаем.
Так как точность измерения времени посредством свойства Environment.TickCount зачастую оказывается недостаточной, Microsoft по многочисленным просьбам трудящихся добавил в .NET Framework 2.0 класс System.Diagnostics.Stopwatch, предназначенный для высокоточного измерения временных интервалов. Данный класс весьма прост в использовании. Сначала приложение должно создать экземпляр класса Stopwatch и запустить таймер методом Start. Затем по мере необходимости приложение посредством свойства ElapsedMilliseconds получает количество миллисекунд, прошедшее с момента запуска таймера. Когда же все измерения времени выполнены, приложение останавливает таймер командой Stop.
Для оценки точности таймера Stopwatch можно воспользоваться статическим свойством Frequency, возвращающее количество тиков высокоточного таймера, используемого классом Stopwatch для измерения временных интервалов, за 1 секунду. На моем компьютере свойство Frequency равно 1.870.000.0000, что соответствует фантастической точности $$1/frac18700000000 = 5?10^{-10}сек$$.
Так как метод ElapsedMilliseconds по определению не может изменять интервалы меньше 1 миллисекунды, в классе Stopwatch имеется один метод ElapsedTicks, возвращающий количество тиков таймера. Для перевода тиков в секунды значение свойства ElapsedTicks надо поделить на Stopwatch.Frequency. При этом следует всегда помнить о том, что при измерении сверхкоротких интервалов появляются иные погрешности, вызванные переключением задач, кэш промахами, Stopwatch редко достижима на практике.
Код приложения, переписанный с использованием таймера Stopwatch приведен в листинге 3.20.
// Пример Examples\Ch03\Ex09
public partial class MainForm : Form
{
// Высокоточный таймер
Stopwatch stopwatch;
// Время, прошедшее с момента запуска таймера при
последнем вызове обработчика события
// Idle
long lastTime;
private void MainForm_Load(object sender, EventArgs e)
{
...
// Запускаем таймер
stopwatch = new Stopwatch();
stopwatch.Start();
lastTime = 0;
// Выводим в окно Output точность таймера
Trace.WriteLine("Accuracy = " + 1.0 / Stopwatch.Frequency + " sec");
Application.Idle += new EventHandler(Application_Idle);
}
void Application_Idle(object sender, EventArgs e)
{
// Если приложение завершает работу, закрываем главную форму. If
(closing)
{
Close(); return; }
// Получаем количество миллисекунд, прошедших с
момента запуска таймера
double currentTime = (double)stopwatch.ElapsedTicks /
(double)Stopwatch.Frequency;
// Вычисляем время (в секундах), прошедшее между двумя
вызовами обработчика события Idle
` float delta = (float)(currentTick - lastTick);
// Изменяем положение диска
posX += speedX * delta; posy
+= speedY * delta;
// Обрабатываем столкновение с краями экрана
...
// Запоминаем текущее показание таймера
lastTime = currentTime;
// Перерисовываем экран
Invalidate();
}
...
}
Запустив приложение, вы убедитесь, что в окне Output пропали нулевые значения. В полноэкранном режиме содержимое окна Output скорее всего будет выгладить примерно следующим образом:
Accuracy = 5,3475935828877E-10 sec delta = 0,035 delta = 0,006 delta = 0,003 delta = 0,002 delta = 0,011 delta = 0,012 delta = 0,012 delta = 0,012 delta = 0,011 delta = 0,013 delta = 0,011
Примечание
Обратите внимание на ощутимую задержку при визуализации первого кадра, обусловленную накладными расходами JIT -компилятора при компиляции метода Paint.
В результате шарик наконец-то стал двигаться по-настоящему плавно. Однако в оконном режиме рывки по-прежнему остались, при этом время между визуализацией соседних кадров скачкообразно меняется почти в два раза:
Accuracy = 5,3475935828877E-10 sec delta = 0,035 delta = 0,022 delta = 0,013 delta = 0,032 delta = 0,015 delta = 0,032 delta = 0,014 delta = 0,032 delta = 0,016 delta = 0,032 delta = 0,014 delta = 0,016 delta = 0,019
Все дело в вертикальной синхронизации, которая будет рассмотрена в следующем разделе.
Как известно, CRT -мониторы формируют изображение посредством пучка электронов, который построчно пробегает по поверхности экрана сверху вниз, заставляя ее светиться. Поскольку свечение экрана быстро тускнеет, этот процесс повторяется снова и снова с частотой вертикальной развертки текущего видеорежима режима.
Теперь давайте представим, что произойдет, если изображение изменится в процессе обратного хода луча. И так, часть верхняя изображения уже отображена на экране, но прервать процесс формирования изображения не возможно, поэтому электронный луч продолжит сканировать поверхность экрана, формируя новое изображение. В результате на экране кратковременно будут находиться новое и старое изображение, что воспринимается пользователем как странный артефакт.
Для борьбы с этим недоразумением XNA Framework предоставляет программисту возможность управления синхронизацией переключения заднего и экранного буферов с моментом окончания формирования электронным лучом изображения на экране монитора. Эта функциональность доступна посредством свойства PresentationInterval класса PresentationParameters:
public PresentInterval PresentationInterval { get; set; }
В подавляющем большинстве случаев свойству PresentationInterval присваивают одно из следующих трех значений перечислимого типа PresentInterval:
PresentInterval.Default - значение по умолчанию, аналогичное PresentInterval.One.PresentInterval.One - буферы переключаются только по окончанию обновления электронным лучом изображения на экране монитора.PresentInterval.Immediate - кадровые буферы переключаются так быстро, насколько это возможно.Хотя значения PresentInterval.Default и PresentInterval.One формально являются братьями-близнецами, между ними есть одна очень тонкая разница. При использовании PresentInterval.Default синхронизации с обратным ходом луча осуществляется посредством обычного таймера Windows, который, как вы помните, обладает очень низкой точностью. Из-за особенностей организации оконной подсистемы Windows низкая точность таймера приводит к частым задержкам при переключении буферов и, соответственно, "рывкам " при анимации. В полноэкранном режиме последствия, как правило, не столь серьезны.
В отличии от PresentInterval.Default, значение PresentInterval.One использует высокоточный таймер, что позволяет избавиться от рывков. Тем не менее, в некоторых случаях незначительные рывки все же могут остаться: так как видеокарта перед переключением кадровых буферов ожидает завершение визуализации текущего кадра, нетрудно догадаться, что при самом не благоприятном стечении обстоятельств частота смены кадров может оказаться в два раза меньше частоты вертикальной развертки монитора. Да и драйверы видеокарты зачастую оказываются не такими совершенными, как хотелось бы. При возникновении подобных проблем пользователь может попробовать отключить вертикальную синхронизацию, так как резкие скачки частоты смены кадров раздражают гораздо сильнее артефактов из-за отсутствия вертикальной синхронизации.
Итак, однозначно правильного режима смены кадров не существует, поэтому было бы разумным предоставить этот выбор пользователю. Для этой цели я поместил на форму диалогового окна Параметры дополнительный флажок Вертикальная синхронизация ( vsynchCheckBox ), установка которого соответствует использованию режима PresentInterval.One, а снятие - PresentInterval.Immediate (рисунок 3.12). Для сохранения значения режима вертикальной синхронизации в настройки приложения было добавлено дополнительное поле PresentationInterval типа Mcrosoft.Xna.Framework.Graphics.PresentInterval, а сам код приложения подвергся косметической доработке (листинг 3.21). Готовое приложение находится в example.zip
в каталоге Examples\Ch03\Ex10.

(рис 3.21) Диалоговое окно Параметры с новым флажком Вертикальная синхронизация (рис 3.12) // Класс главной формы приложения public partial class MainForm : Form { void InitGraphivsDevice() { ... presentParams = new PresentationParameters(); presentParams.BackBufferCount = 1; presentParams.SwapEffect = SwapEffect.Discard; presentParams.IsFullScreen = settings.FullScreen; // Устанавливаем режим вертикальной синхронизации согласно настройкам приложения presentParams.PresentationInterval = settings.PresentationInterval; ... } ... } // Класс диалогового окна "Параметры " public partial class SettingsForm : Form { // Конструктор диалогового окна internal SettingsForm(Properties.Settings settings, GraphicsAdapter adapter) { ... // Устанавливает значение флажка "Вертикальная синхронизация " согласно настройкам приложения if (settings.PresentationInterval == PresentInterval.One) vsynchCheckBox.Checked = true; else vsynchCheckBox.Checked = false; } // Обработчик нажатия кнопки Ok private void okButton_Click(object sender, EventArgs e) { ... // Сохраняем в настройках приложения информацию о режиме кадровой синхронизации if (vsynchCheckBox.Checked) settings.PresentationInterval = PresentInterval.One; else settings.PresentationInterval = PresentInterval.Immediate; ... } }
Запустите приложение и попробуйте поэкспериментировать с настройками кадровой синхронизации, наблюдая за окном Output. При включенной вертикальной синхронизации интервал между визуализацией кадров будет примерно равен константе 1.0/{частота обновления экрана текущего видеорежима}. При отключении вертикальной синхронизации частота интервал смены кадров уменьшится до величины сопоставимой с 1 мс, что соответствует частоте порядка 1000 fps. При этом и в том и в другом случае движения шарика будут плавными.
При визуализации диска проходы ( Passes ) эффекта перебираются посредством цикла foreach.
Достоинствами цикла foreach является простота кода и защита от потенциальных ошибок вроде использования неправильного индекса при обращении к элементу коллекции. Обратной стороной является неявное использование циклом foreach специального типа, реализующего интерфейс Enumerator. Например, код
List<int> numbers = new List<int>(3); ... int s = 0; foreach (int v in numbers) s += v;
неявно заменяется компилятором следующим аналогом:
List<int> numbers = new List<int>(3); ... int s = 0; // Создается объект enumerator IEnumerator enumerator = numbers.GetEnumerator(); // Перебирает элементы коллекции while (enumerator.MoveNext()) // Получает доступ к текущему элементу коллекции s += (int)enumerator.Current;
На первый взгляд, данный код является неэффективным. Но в действительности все не так уж и плохо. Во-первых, метод GetEnumerator возвращает структуру:
public class List<T> : IList<T>,
ICollection<T>, IEnumerable<T>, IList,
ICollection,
IEnumerable
{
public Enumerator<T>
GetEnumerator() {
return new Enumerator<T>((List<T>)
this); }
...
public struct Enumerator : IEnumerator<T>, IDisposable,
IEnumerator
{
private List<T> list;
private int index;
private int version;
private T current;
internal Enumerator(List<T> list);
public void Dispose();
public bool MoveNext();
public T Current { get; }
object IEnumerator.Current { get; }
void IEnumerator.Reset(); }
}
Соответственно, вызов метода GetEnumerator не приводит к выделению памяти в управляемой куче и учащению вызовов сборщика мусора. Во-вторых, компилятор C# вызывает метод MoveNext и свойство Current структуры Enumerator напрямую без приведения к ссылке на интерфейс IEnumerator, так что боксирование ( boxing ) структуры Enumerator не происходит. И, наконец, компилятор может встроить ( inline ) код метода MoveNext и свойства Current непосредственно в код цикла, избежав накладных расходов на вызовы методов. Так что при использовании массивов или коллекций наподобие List разница в производительности циклов foreach и for будет исключающее мала, и в подавляющем большинстве случаев на нее можно не обращать внимания.
Однако при использовании других коллекций не все так просто. Например, у некоторых объектов метод GetEnumerator возвращает объект, размещаемых в управляемой куче. Подобный подход имеет два недостатка:
foreach для перебора элементов такой коллекции приведет к созданию множества объектов enumerator, и, соответственно, частому вызову сборщика мусора, что привет к падению производительности.MoveNext является виртуальным, то компилятор не сможет встроить его непосредственно в код цикла, что тоже негативно скажется на производительности.Таким образом, в критичных к производительности приложениях при переборе элементов коллекций, возвращающих объект enumerator, имеет смысл избегать циклов foreach.
Но давайте вернемся к коллекции Passes класса Effect. Данная коллекция реализуется классом EffectPassCollection, объявленным следующим образом:
public sealed class EffectPassCollection :
IEnumerable<EffectPass>
{
// Список для хранения информации о проходах
private List<EffectPass> pPass;
// Возвращает объект (полученный путем боксирования
структуры), реализующий интерфейс
// IEnumerator<EffectPass>
public IEnumerator<EffectPass> GetEnumerator()
{
return (IEnumerator<EffectPass>) this.pPass.GetEnumerator();
}
}
Давайте внимательно рассмотрим этот код. Класс EffectPassCollection в действительности хранит информацию о проходах в коллекции List. Соответственно метод this.pPass. GetEnumerator возвращает ссылку на структуру Enumerator класса List, определение которой было приведено в начале раздела.
И все бы было просто замечательно, если бы не один маленький нюанс - структура IEnumerator приводится к интерфейсу IEnumerator<EffectPass>, что приводит к боксированию и выделению памяти в управляемой куче. Чтобы оценить влияние этой особенности на производительность приложения, мы встроим в обработчик события Paint код, перебирающий элементы коллекции 10.000.000 раз посредством циклов for и foreach с измерением времени их выполнения (листинг 3.22).
// Пример Examples\Ch03\Ex11 #define TEST
#if TEST
bool testing = true;
#endif
private void MainForm_Paint(object sender, PaintEventArgs e)
{
// Измерение выполняется только при условии определения
идентификатора TEST
#if TEST
// Тест выполняется только один раз
if (testing)
{
// Число итераций
const int n = 10000000;
int sum;
// Выводим количество сборок мусора, выполненных на момент
начала эксперимента
Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
Stopwatch timer = new Stopwatch();
// Запускаем таймер
timer.Start();
// Перебираем элементы коллекции passes и суммируем коды первой буквы
sum = 0;
EffectPassCollection passes = effect.CurrentTechnique.Passes;
// Выполняем n итераций
for (int i = 0; i < n; i++)
// Перебираем элементы коллекции passes посредством
for и суммируем коды первой буквы for
(int j = 0; j < passes.Count; j++) sum += (int)passes[j].Name[0];
// Вычисляем суммарное время, затраченное на интеграции
double time1 = (double)timer.ElapsedTicks / (double)Stopwatch.Frequency;
// Отображаем отчет
Trace.WriteLine(" for ");
Trace.WriteLine("Sum : " + sum.ToString());
Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
Trace.WriteLine("Time1 : " + time1);
Trace.WriteLine("");
// Перезапускаем таймер
timer.Reset(); timer.Start();
sum = 0;
// Снова выполняем итерации, но уже посредством цикла foreach for
(int i = 0; i < n; i++)
foreach (EffectPass pass in effect.CurrentTechnique.Passes) sum
+= (int)pass.Name[0];
// Вычисляем суммарное время, потраченное на итерации
double time2 = (double)timer.ElapsedTicks /
(double)Stopwatch.Frequency;
// Отображаем отчет
Trace.WriteLine(" foreach ");
Trace.WriteLine("Sum : " + sum.ToString());
Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
Trace.WriteLine("Time2 : " + time2);
// Определяем разницу в
производительности
Trace.WriteLine("Time2 / Time1 = " + time2 / time1);
Trace.WriteLine("");
testing = false; }
#endif
}
Запустив модифицированное приложение на своем компьютере с процессором Intel Pentium-4 2.8C я получил следующие результаты:
Accuracy = 3,5730747380043E-10 sec Gen 0 collection count: 3 for Sum : 1120000000 Gen 0 collection count: 3 Time1 : 0,554915015846586 foreach Sum : 1120000000 Gen 0 collection count: 915 Time2 : 1,84470303389776 Time2 / Time1 = 3,32429828211344
Как видно, в процессе работы цикла foreach сборщик мусора был вызван 915 Intel Core2 Due E6300 при выполнении циклов foreach сборщик мусора был вызван 229 раз. В целом же, частота вызовов сборщика мусора в первую очередь зависит от размера кэша второго уровня CPU. Например, размер кэша процессора Intel Core2 Due E6300 (2MB) в четыре раза больше, чем у Pentium-4 2.8C (512KB). Соответственно, сборщик мусора на Intel Core2 Due E6300 вызывается в 4 раза реже по сравнению с Pentium-4 2.8C (915/229 = 3.996).for и foreach достигла трехкратной величины. Таким образом, перебор элементов коллекции foreach в методе Paint, вызываемом около сотни раз в секунду, является не самой лучшей идеей. Особенно если учесть, что реальные приложения зачастую содержат десятки
эффектов, а внезапная сборка мусора при визуализации кадра может привести заметному провалу производительности.
Примечание
На самом деле Microsoft здорово поработала над эффективностью сборщика мусора, и сейчас сборка мусора в поколении 0 обычно занимает порядка 1 мс. В частности, пренебрегая накладными затратами цикла foreach, можно прикинуть, что в нашем эксперименте каждая сборка мусора выполнялась в среднем не более чем за (1.84 - 0.55) / 915 = 0.0014 секунд. Тем не менее, рано или поздно частые сборки мусора могут спровоцировать сбор мусора в старших поколениях, который будет воспринят пользователем как внезапное "подтормаживание " приложения.
Итак, после всех наших трудов движения диска стали по-настоящему плавными. Тем не менее, наше приложение все еще далеко от совершенства. Давайте попробуем представить, что произойдет, если приложение вдруг внезапно приостановит свою работу на несколько секунд, например, из-за повысившейся активности работы с файлом подкачки, плохо читаемого сектора на диске или какой-либо проблемы в драйвере устройства. После возобновления выполнения приложения оно экстраполирует прямолинейное движение диска на несколько секунд вперед и обнаружит, что он уже далеко вылетел за пределы ограничивающей стены. После чего шарик будет автоматически возращен обратно в один из углов прямоугольной границы. Разумеется, траектория движения диска при этом окажется нарушенной, ведь за это время диск должен был уже несколько раз отскочить от стены. Подобный эффект в меньшей степени проявляется и при незначительных провалах производительности, что в конечном счете, приводит несколько различному поведению приложения на разных компьютерах. Вообще данная особенность не является большим недостатком для нашего приложения, в конце концов, диск ведь движется по случайной траектории. Однако в реальных игровых приложениях такая реакция на внезапные провалы производительности неминуемо приведет к разнообразным "глюкам " игровой логики (проход сквозь препятствия и т.п.). Причем чем ниже производительность компьютера, тем вероятнее возникновение проблем.
В принципе для ликвидации данного недостатка можно было бы найти аналитическое выражение зависимости координат центра диска от времени. Однако такое решение будет пригодно лишь для самых простых случаев. Поэтому мы реализуем более универсальный подход: моделирование движений диска с некоторым фиксированным шагом, например, 0.005 секунды. Реализация данной технологии потребует лишь косметической правки обработчика события Idle (листинг 3.23).
// Дискретный шаг времени (в секундах), с которым выполняются расчеты
const float timeStep = 0.005f;
// Максимальный временной интервал между двумя
вызовами обработчика события Idle (в секундах)
const float maxDelta = 5.0f;
void Application_Idle(object sender, EventArgs e)
{
...
// Определяем текущее время
double currentTime = (double)stopwatch.ElapsedTicks /
(double)Stopwatch.Frequency;
// Если время между двумя вызовами обработчика Idle
превышает maxDelta, корректируем lastTime
// во избежание слишком длинной работы обработчика события
Idle if (currentTime - lastTime > maxDelta)
lastTime = currentTime - maxDelta;
// Моделируем движения шарика дискретным шагом времени timeStep
while (lastTime + timeStep < currentTime)
{
posX += speedX * timeStep;
posY += speedY * timeStep;
lastTime += timeStep; }
Invalidate();
}
В самых запущенных случаях задержка между визуализацией кадров может достигать минуты и даже больше. Так как моделирование игровой логики интервала времни в несколько минут может занять достаточно существенно время, в приложении используется простой прием - игровая логика рассчитывается не более чем за 5 секунд игрового времени (константа maxDelta ). Для пользователя подобный трюк является незаметным, ведь он все равно не сможет спрогнозировать поведение приложения на значительный временной интервал.
Примечание
В интерактивных приложениях вроде автосимуляторов при возникновении длительной задержки между кадрами имеет смысл приостановить игровой процесс, ведь в течение задержки в несколько секунд автомобиль игрока наверняка слетит с трассы или столкнется с препятствием. Эффекта автоматической приостановки игрового процесса можно достичь путем присвоения константе maxDelta небольшого значения порядка 0.2 - 0.5 секунд.
В компьютерной графике довольно часто приходится моделировать различные полупрозрачные объекты вроде стекла, воды, тумана и т.п. Полупрозрачность - это характеристика объекта, показывающая, какую часть света пропускает среда (объект) без изменения направления его распространения. Так полностью прозрачный объект пропускает через себя весь свет, в результате чего не оказывает никакого влияния на находящиеся позади него объекты (иными словами, он является невидимкой). Непрозрачный объект напротив не пропускает через себя свет, и соответственно полностью перекрывает находящиеся позади него объекты. К слову, все наши объекты до сих пор являлись полностью не прозрачными.
Коэффициент поглощения ? определенной среды есть количественная мера, позволяющая оценить, какая доля светового, падающего на поверхность раздела сред, проходит дальше, а какая поглощается. Например, если стекло имеет коэффициент поглощения 0.2, то результирующий цвет будет представлять собой объединение 20% цвета стекла и 80% цвета объектов позади стекла. То есть наблюдатель будет видеть как сам объект, так и предметы позади объекта.
В общем случае итоговый цвет полупрозрачного объекта может быть определен по формуле:
$$C=c_s-\alpha+c_d-(1-\alpha) $$где
$$c$$ - результирующий цвет
$$c_s$$ - цвет полупрозрачного объекта
$$c_d$$ - цвет фона позади объекта
$$\alpha$$ - коэффициент поглощения, равный 0 для полностью прозрачных объектов, и 1 для непрозрачных.
Перейдем к моделированию эффекта полупрозрачности в XNA Framework. Как вы помните, в XNA Framework любой объект визуализируется с использованием простых примитивов, которые проходят через ряд стадий графического конвейера. В конечном счете, каждый примитив преобразуется в массив пикселей, которые заносятся в кадровый буфер. По сути, итоговый массив пикселей и есть сам объект, а кадровый буфер - его фон. Когда объект является непрозрачным, то пиксели просто копируются в кадровый буфер, затирая старые значения (собственно этим мы до сих пор и занимались). Если же объект является полупрозрачным, то разумно предположить, что приложение должно скомбинировать цвет пикселей объекта с пикселями кадрового буфера по формуле 3.1.
На первый взгляд операцию смешивания пикселей было бы логичным осуществлять в пиксельном шейдере. Но, к сожалению, это не возможно – пиксельный шейдер не может считывать информацию из кадрового буфера, так как реализация данной функциональности ощутимо бы усложнила бы пиксельные процессоры. Соответственно, в современных процессорах эта функциональность реализуется посредством специализированных блоков
(рис 3.13) Принцип работы блоков ROP Примечание
В общем случае количество блоков не обязательно совпадает с числом пиксельных процессоров. Например, G70 содержит 24 пиксельных процессора и 16 блоков . Подобная асимметрия обусловлена меньшей ресурсоемкостью операций смешения пикселей по сравнению со среднестатистическим пиксельным шейдером, поэтому при равном количестве пиксельных процессоров и часть последних будет в основном простаивать.
Блоки имеют очень ограниченную функциональность и программируются с использованием весьма запутанного синтаксиса. Блок может выполнить над двумя аргументами одну из пяти векторных операций вида:
где
$$\qquard d'$$ - новый цвет пикселя кадрового буфера $$[\qquard d'_r,\qquard d'_g,\qquard d'_b,\qquard d'_a] $$
$$op$$ - операция (см. таблица 3.5)
$$s$$ - цвет пикселя $$[s_r, s_g, s_b, s_a ] $$
$$d$$ - текущий цвет пикселя кадрового буфера $$[d_r,d_g,d_b,d_a]$$
$$b, с$$ - векторные коэффициенты $$[b_r,b_g,b_b,b_a]$$ и $$[c_r,c_g,c_b,c_a] $$
Примечание
Наряду с операциями смешения блоки могут выполнять множество других полезных простых операций над пикселями вроде отсечения невидимых областей объектов при визуализации трехмерных цен, реализовывать полноэкранное сглаживание ( FSAA - Full Scene Anti Aliasing ) и т.д.
Управление смешением пикселей осуществляется посредством свойств свойства RenderState экземпляра класса GraphicsDevice. Активация режима смешения осуществляется путем присвоения значения true свойству AlphaBlendEnable.
public bool AlphaBlendEnable { get; set; }
Операция, выполняемая над цветами графического примитива и кадрового буфера задается путем присвоения соответствующего значения перечислимого типа BlendFunction (таблица 3.5) свойству RenderState.BlendFunction.
public BlendFunction BlendFunction { get; set; }
| Значение | Описание |
|---|---|
Add (значение по умолчанию) |
Покомпонентно складывает два аргумента |
Subtract |
Покомпонентно вычитает из первого аргумента (цвет пикселя) второй (цвет кадрового буфера). |
ReverseSubtract |
Покомпонентно вычитает из второго аргумента (цвет кадрового буфера) первый (цвет пикселя). |
Max |
Покомпонентно сравнивает оба аргумента и возвращает компоненты с наибольшим значением |
Min |
Покомпонентно сравнивает оба аргумента и возвращает компоненты с наименьшим значением |
Каждый из Blen (таблица 3.6). При этом коэффициент $$b$$ задается свойством RenderState.SourceBlend, а коэффициент c – свойством RenderState.DestinationBlend:
public Blend SourceBlend { get; set; } public
Blend DestinationBlend { get; set; }
| Значение | Описание |
|---|---|
BlendFactor |
В качестве коэффициента используется вектор, присвоенный свойству RenderState.BlendFactor |
InverseBlendFactor |
Коэффициент получается путем вычитания из единичного вектора (1, 1, 1, 1) значения свойства RenderState.BlendFactor. |
Zero |
Коэффициент равен вектору (0, 0, 0, 0). |
One |
Коэффициент равен вектору (1, 1, 1, 1). |
SourceColor |
В качестве коэффициента используются цвета примитива $$[s_r , s_g ,s_b, s_a ] $$ |
InverseSourceColor |
В качестве коэффициента используется вектор $$[1-s_r,1-s_g,1-s_b,1-s_a] $$ |
SourceAlpha |
В качестве коэффициента используется вектор альфа каналов примитива $$[s_a ,s_a ,s_a ,s_a ] $$. |
InverseSourceAlpha |
В качестве коэффициента используется вектор $$[1-s_a,1-s_a,1-s_a,1-s_a] $$. |
DestinationColor |
В качестве коэффициента используется цвет пикселя кадрового буфера $$[d_r ,d_g ,d_b,d_a ] /$$. |
InverseDestinationColor |
В качестве коэффициента используется вектор [1-d_r,1-d_g,1-d_b,1-d_a]. |
DestinationAlpha |
В качестве коэффициента используется вектор альфа-каналов пикселя кадрового буфера $$[d_a,d_a,d_a,d_a ] $$. |
InverseDestinationAlpha |
В качестве коэффициента используется вектор $$[1-d_a,1-d_a ,1-d_a,1-d_a] $$. |
SourceAlphaSaturation |
Вектор коэффициентов равен [f,f,f,1], где $$f = min(s_a,d_a) $$. |
Хотя первое время от разнообразия параметров рябит в глазах, в действительности все очень просто. Допустим, нам необходимо скомбинировать цвет пикселей примитива с цветом кадрового буфера с использованием следующего экзотического выражения:
$$\qquard d’_r=(1-s_r)*s_r-s_a*d_r\\ \qquard d’_g=(1-s_g)*s_g-s_a*d_g\\ \qquard d’_b=(1-s_b)*s_b-s_a*d_b $$Значение альфа канала нас не интересует, так как он все равно не учитывается при выводе изображения на экран. Вдобавок, часто используемые форматы пикселей SurfaceFormat.Bgr565 и SurfaceFormat.Bgr32 не содержат альфа канала.
Для начала давайте определим, какой операцией связаны между собой цвет примитива и цвет кадрового буфера. Во всех трех выражениях из произведения с множителями $$s_r ,s_g ,s_b$$ вычитается произведение, содержащее множители $$d_r ,d_g ,d_b,$$ следовательно для реализации этой формулы необходимо использовать операцию вычитания BlendFunction.Subtract.
Перейдем к коэффициентам. Сопоставив коэффициенты перед $$s_r ,s_g ,s_b$$ с таблицей 3.6 мы придем к выводу, что они соответствуют значению Blend.InverseSourceColor. Все коэффициенты перед $$d_r ,d_g ,d_b$$ равны $$s_a,$$ следовательно они могут быть заданы с использованием константы Blend.SourceAlpha.
Таким образом, программирование блоков для смешения цветов по формуле 3.3 реализуется посредством следующих четырех строчек кода:
device.RenderState.AlphaBlendEnable = true; device.RenderState.BlendFunction = BlendFunction.Subtract; device.RenderState.SourceBlend = Blend.InverseSourceColor device.RenderState.DestinationBlend = Blend.SourceAlpha;
Дополнительная информация
Хотя по умолчанию XNA Framework применяет для смешения всех цветовых компонентов общие выражения, он так же позволяет использовать раздельные выражения для смешивания RGB и Alpha компонентов цвета. Эта функциональность активируется посредством присвоения свойству RenderState.SeparateAlphaBlendEnabled значения true, после чего вышеописанные свойства RenderState.BlendFunction, RenderState.SourceBlend и RenderState.DestinationBlend будут влиять исключительно на R, G и B составляющие цвета. Режим смешения альфа-компоненты цвета управляется аналогичной тройкой свойств: RenderState.AlphaBlendOperation, RenderState.AlphaSourceBlend,
RenderState.AlphaDestinationBlend. Так как раздельное смешения каналов применяется достаточно редко, мы пока не будет акцентировать на нем внимание.
Для реализации визуализации полупрозрачных примитивов, мы должны переложить выражение 3.1 на причудливый язык программирования блоков из XNA Framework. Для простоты мы положим, что весь примитив имеет одну и ту же прозрачность, что позволит нам использовать для задания коэффициента непрозрачности параметр RenderState.BlendFactor. Преимуществом такого подхода является возможность изменения прозрачности всех вершин примитива посредством коррекции одного единственного параметра. Само выражение 3.1 содержит операцию сложения и два коэффициента непрозрачности, так что оно легко реализуется средствами XNA Framework:
// Задаем коэффициент непрозрачности (0 – абсолютно прозрачен, 255 – полностью непрозрачен). // При присвоении свойству RenderState.BlendFactor он автоматически приводится к диапазону // 0..1 путем деления на 255. const byte opacity = 50; // Включаем режим смешивания цветов device.RenderState.AlphaBlendEnable = true; // Используем операцию сложения device.RenderState.BlendFunction = BlendFunction.Add; // Коэффициент смешения всех трех компонентов цвета равен opacity (точнее opacity / 255) device.RenderState.BlendFactor = new XnaGraphics.Color (opacity, opacity, opacity, 0); // Коэффициент, на который умножается цвет примитива, равен opacity / 255 device.RenderState.SourceBlend = Blend.BlendFactor; // Коэффициент, на который умножается цвет кадрового буфера, равен 1 - opacity / 255 device.RenderState.DestinationBlend = Blend.InverseBlendFactor;
Для демонстрации использования эффекта полупрозрачности мы строим в решение практического упражнения 3.1 ( Examples\Ch03\Ex02 ) возможность управления прозрачностью круга (рисунок 3.14). Для этого необходимо добавить в группу Параметры ползунок ( TrackBar ) и две метки, свойства которых перечислены в таблице 3.7.
(рис 3.14) Приложение, визуализирующее полупрозрачный круг | Класс | Свойство | Значение |
|---|---|---|
TrackBar |
Name |
opacityTrackBar |
Minimum |
0 | |
Maximum |
255 | |
TickFrequency |
0 | |
Label |
Name |
opacityLabel |
Label |
Text |
Непрозрачность |
Так же мне необходимо реализовать обработчик события Scrol ползунка и немного подправить обработчик события Paint (листинг 3.24). Готовый проект приложения находится в example.zip в каталоге Examples\Ch03\Ex13.
private void opacityTrackBarScroll(object sender, EventArgs e)
{
// Отображаем текущий коэффициент непрозрачности
opacityLabel.Text = ((float)opacityTrackBar.Value / 255.0f).
ToString("0.00");
// Перерисовывает компонент xnaPanel
xnaPanel.Invalidate();
}
private void xnaPanel_Paint(object sender, PaintEventArgs e)
{
...
// Рисуем шахматную доску (код визуализации шахматной доски
взят из пример Ch01\Ex03
// (см. раздел 1.2).
...
device.RenderState.CullMode = CullMode.None;
device.RenderState.PointSize = 3;
device.RenderState.AlphaBlendEnable = true;
device.RenderState.BlendFunction = BlendFunction.Add;
// Значение параметра BlendFactor вычисляется на основе
текущего положения ползунка прокрутки
device.RenderState.BlendFactor = new XnaGraphics.
Color((byte)opacityTrackBar.Value,
(byte)opacityTrackBar.Value, (byte)opacityTrackBar.Value, 0);
device.RenderState.SourceBlend = Blend.BlendFactor;
device.RenderState.DestinationBlend = Blend.InverseBlendFactor;
// Визуализируем круг
...
}
В следующем примере мы создадим приложение, анимирующее построение фигуры Лиссажу, заданной выражением
$$x = \sin(2-a) y =\cos(3-a) $$где
x, y - координаты текущей точкиа - угол, пробегающий с определенным шагом значения от 0 до 360 градусов (0…2·?) Вначале все пиксели фигуры будут прозрачными. Затем мы начнем постепенно увеличивать непрозрачность вершин фигуры, при этом непрозрачность всех вершин будет расти неравномерно: первыми непрозрачными станут вершины, соответствующие углу а равному 0, а последними - а равному 360 градусов. Соответственно, фигура Лиссажу будет как бы рисоваться как бы невидимым пером (рисунок 3.15).
Так как на этот раз вершины примитива имеют различную прозрачность, задание коэффициента непрозрачности посредством свойства RenderState.BlendFactor вряд ли будет разумным решением. Поэтому мы будем хранить информацию о непрозрачности в альфа-канале цвета вершины, задавая режим смешения пикселей посредством следующего кода:
device. RenderState.AlphaBlendEnable = true; device.RenderState.BlendFunction = BlendFunction.Add; device.RenderState.SourceBlend = Blend.SourceAlpha; device.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
Чтобы облегчить задачу мы воспользуемся в качестве отправной точки решением практического
упражнения 2.5. Все что от нас требуется - переписать обработчик события Idle и немного подправить
обработчики событий Load и Paint формы (листинг 3.25).

(рис 3.25) Построение фигуры Лиссажу (рис 3.15) // Пример Examples\Ch03\Ex14 public partial class MainForm : Form { // Количество сегментов в кривой const int QuadStrips = 200; // Количество треугольников в кривой * 2; const int TriStrips = QuadStrips ... // Массив вершин фигуры Лиссажу VertexPositionColor[] vertices = null; // Таймер Stopwatch stopwatch = null; ... private void MainForm_Load(object sender, EventArgse) { // Создаем массив вершин объекта vertices = new VertexPositionColor[TriStrips + 2]; // Перебираем все вершины фигуры Лиссажу for (int i = 0; i <= QuadStrips; i++) { // Вычисляем текущий угол float angle = 2.0f * (float)Math.PI * (float)i // Рассчитываем координаты текущей точки фигуры Лиссажу float cos = (float)Math.Cos(angle); float x = 0.85f * (float)Math.Sin(2 * angle); / (float)QuadStrips; float y = 0.85f * (float)Math.Cos(3 * angle); // Вычисляем цвет точки byte green = (byte)(Math.Pow(0.5f + cos * 0.5f, 0.3f) * 255.0f); byte red = (byte)(Math.Pow(0.5f - cos * 0.5f, 0.3f) * 255.0f); /// Рассчитываем вектор, перпендикулярный графику фигуры Лиссажу длиной 0.015 float sx = -2.55f * (float)Math.Sin(3 * angle); float sy =- 1.7f *(float)Math.Cos(2 * angle); float length = (float)Math.Sqrt(sx * sx + sy * sy); float nx = sx / length * 0.015f; float ny = sy / length * 0.015f; // Заносим в массив информацию о двух вершинах фигуры Лиссажу, смещенных на 0.015 в // направление, перпендикулярном фигуре. Таким образом, кривая фигуры Лиссажу будет иметь // ширину 0.03 единицы. vertices[i * 2] = new VertexPositionColor(new Vector3(x - nx, y - ny, 0.0f), new XnaGraphics.Color(red, green, 0)); vertices [i * 2 + 1] = new VertexPositionColor(new Vector3(x + nx, y + ny, 0.0f), new XnaGraphics. Color(red, green, 0)); }; ... // Запускаем таймер stopwatch = new Stopwatch(); stopwatch.Start(); // Задаем обработчик события Idle, выполняющий коррекцию прозрачности вершин Application.Idle+=new EventHandler(Application_Idle); // Рассчитываем текущие коэффициенты непрозрачности вершин Application_Idle(this, null); } // Рассчитывает текущие коэффициенты непрозрачности вершин и перерисовывает изображение void Application_Idle(object sender, EventArgs e) { if (closing) Close(); // Получаем текущее время float currentTime = (float)stopwatch.ElapsedTicks / (float)Stopwatch.Frequency; // Перебираем все вершины фигуры Лиссажу for (int i = 0; i <= QuadStrips; i++) { // Вычисляем коэффициент непрозрачности для текущей пары вершин byte opacity = (byte)Math.Max(Math.Min((currentTime - 15.0f * (float)i / (float)QuadStrips) * 255.0f, 255f), 0.0f); // Изменяем коэффициент непрозрачности вершин. К сожалению, структура Color не позволяет // изменять отдельные компоненты цвета, поэтому приходится создавать новую структуру и // указывать значения всех цветовых компонентов. vertices[i * 2].Color = new XnaGraphics.Color(vertices[i * 2].Color.R, vertices[i * 2].Color.G, vertices[i * 2].Color.B, opacity); vertices[i * 2 + 1].Color = new XnaGraphics.Color (vertices[i * 2 + 1].Color.R, vertices[i * 2 + 1].Color.G, vertices[i * 2 + 1].Color.B, opacity); }; // Если фигура Лиссажу полностью визуализирована (она визуализируется ровно 16 секунд) if (currentTime > 16.0f) { // Прекращаем анимацию, дабы не загружать центральный процессор и видеокарту бесполезной // работой. Application.Idle -= new EventHandler(Application_Idle); // Выключаем таймер stopwatch.Stop(); } // Перерисовываем форму Invalidate(); } private void MainForm_Paint(object sender, PaintEventArgs e) { ... device.RenderState.CullMode = CullMode.None; device.RenderState.FillMode = fillMode; // Задаем режим смешения пикселей device.RenderState.AlphaBlendEnable = true; device.RenderState.BlendFunction = BlendFunction.Add; device.RenderState.SourceBlend = Blend.SourceAlpha; device.RenderState.DestinationBlend = Blend.InverseSourceAlpha; device.VertexDeclaration = decl; // Визуализируем фигуру Лиссажу effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); device.DrawUserPrimitives(PrimitiveType.TriangleStrip, vertices, 0, vertices.Length - 2); pass.End(); } effect.End(); device.Present(); } }
В этой лекции мы научились осуществлять визуализацию как на поверхность компонентов Windows Forms, так и на целый экран с выбором требуемого видеорежима, и даже на поверхность элементов Win32. Так же мы научились реализовать плавную анимацию и моделировать полупрозрачность посредством смешивания с учетом альфа канала цвета
Любому разработчику периодически приходится решать множество типовых задач визуализации:
Ответы на все эти вопросы будут даны в этой лекции. Начнем с решения первой проблемы.
До сих пор все наши приложения осуществляли монопольный вывод графической информации на поверхность формы. Такой подход не позволяет размещать на форме элементы управления .NET, что значительно ограничивает свободу разработчика. Конечно, можно попробовать использовать различные вспомогательные диалоговые окна и плавающие панели инструментов, но это не очень красивое решение проблемы, хотя и вполне Panel ). Это позволило бы нам легко ограничить область вывода XNA Framework определенной областью формы, а освободившееся пространство использовать для размещения различных элементов управления Windows Forms.
Вывод на поверхность элемента управления практически не отличается от вывода на поверхность формы, ведь в Windows Forms форма является частным случаем элементом управления. Более, форму можно поместить на другую форму или компонент в качестве элемента Control. Как следствие, любой элемент управления Windows Forms обладает свойствами Handle, Width, Height, методами SetWidth, Show, Hide, обработчиками событий Paint, Click, Resize и так
Примечание
Класс Control является полноценным элементом управления, по функциональности отдаленно напоминающий элемент Panel с немного ограниченными возможностями.
Но вот незадача, некоторые члены класса Control объявлены как protected. Это не создает никаких проблем при создании новой формы путем наследования от класса Form, так как в этом случае вы автоматически получаете полный доступ к защищенным (protected) членам класса. Однако при использовании готовых компонентов, помещаемых на форму средствами визуального редактора форм Visual Studio, все намного серьезнее. Например, вы не сможете получить доступ к такому важному методу как SetStyle, чтобы установить стиль ControlStyles.Opaque. А ведь без этого невозможно запретить самовольную перерисовку элемента управления, приводящую к мерцанию изображения в XNA -приложениях (раздел 1.2.3).
Наиболее красивое решение этой проблемы - создание на базе класса Control собственного элемента управления, изменяющего статус метода SetStyle с protected на public. Для этого запустите Visual Studio и создайте новый проект библиотеки классов XnaPanel (File | New Project… | Class Library, в текстовом поле Name введите XnaPane и нажмите OK). Установите флажок Create directory for solution, так как в решении будет еще один проект, предназначенный для тестирования созданного компонента. Подключите к проекту сборку System.Windows. Forms и введите код из листинга 3.1.
using System;
using System.Collections.Generic; using System.Text;
using System.Windows. Forms;
namespace GSP.XNA
{
выделялся среди других элементов формы
// Объявляем класс (элемент управления) XnaPanel,
наследуемый непосредственно public class
XnaPanel : Control
{
// Конструктор элемента управления public XnaPanel()
{
// Изменяем цвет элемента управления, чтобы он
1x1 пикселей
BackColor = Color.CornflowerBlue;
// Размер элемента управления не должен быть меньше MinimumSize = new
Size(1, 1);
}
SetStyle метода Control как public SetStyle(ControlStyles flag,
bool value)
//
//
Переопределяем метод
public new void
{
Вызываем оригинальный метод класса Control base.SetStyle(flag, value);
}
}
}
Важно
Для предотвращения неконтролируемого уменьшения размера компонента xnaPanel до размеров меньше одного пикселя, его свойству MinimumSize необходимо присвоить значение Size(1, 1) . Если этого не сделать, площадь области визуализации теоретически может достигнуть нуля, что приведет к генерации исключения при сбросе устройства
После компиляции проекта Visual Studi o самостоятельно создаст в окне Components и добавит в нее полученный элемент управления (рисунок 3.1).
Toolbox группу XnaPanel
(рис 3.1) Панель Toolbox с компонентом XnaPanel Для тестирования нашего компонента мы создадим простое приложение, визуализирующее треугольник. В правой части формы будет расположена панель с элементами управления, позволяющими изменять цвета вершин треугольника (рисунок 3.2).
Чтобы добавить к решению еще один проект щелкните правой кнопкой мыши на названии решения в окне Solution Explorer и выберите в контекстом меню пункт Add | New Project… (рисунок 3.3). В появившемся диалоговом окне выберите элемент Windows Application, введите в поле Name название приложения (например, Test ) и нажмите кнопку Ok. Как всегда, подключите к проекту необходимые сборки XNA Framework и добавьте необходимые директивы using.

(рис 3.3) Внешний вид тестового приложения, использующего компонент XnaPanel (рис 3.2) Добавление к решению (Solution) нового проекта Поместите на форму компонент SplitContainer, расширьте его на всю форму путем присвоения параметру Dock значения Fill и зафиксируйте размер правой панели, присвоив свойству FixedPanel значение Panel2. В правой панели компонента SplitContainer разместите группу ( Group ) "Параметры " и тоже расширьте ее на всю панель при помощи свойства Dock. В группе создайте три метки ( Label ) расположенные друг под другом: Цвет вершины №1, Цвет вершины №2, Цвет вершины №3. Поместите напротив этих меток три панели ( Panel ) и присвойте им имена vertex1Panel, vertex2Panel и vertex3Panel. Чтобы создать черные рамки вокруг панелей, установите свойство BorderStyle в значение FixedSingle. Поместите в левой панели компонента SplitContainer наш компонент XnaPanel, назовите его xnaPanel и расширьте его на всю свободную
область формы, присвоив свойству Dock значение Fill. В заключение, поместите на форму невизуальный компонент ColorDialog.
Следующий этап - "оживление " формы путем создания обработчиков событий. Для начала мы добавим в форму несколько вспомогательных полей и обработчики событий Load/ и FormClosed (листинг 3.2).
// Различные вспомогательные поля GraphicsDevice device = null;
PresentationParameters presentParams; Effect effect = null;
VertexDeclaration decl = null; VertexPositionColor[] vertices = null;
bool closing = false;
new XnaGraphics.Color(vertex2Panel.BackColor.R, vertex2Panel.BackColor.G,
vertex2Panel.BackColor.B));
vertices[2] = new VertexPositionColor(new Vector3(-0.4f, -0.4f, 0.0f),
new XnaGraphics.Color(vertex3Panel.BackColor.R,
vertex3Panel.BackColor.G,
vertex3Panel.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)
{
}
}
private void xnaPanelResize(object sender, EventArgs e)
{
// Если форма не минимизирована
if (WindowState != FormWindowState.Minimized)
{
// Обновляем размер формы
presentParams.BackBufferWidth = xnaPanel.ClientSize. Width;
presentParams.BackBufferHeight = xnaPanel.ClientSize.Height;
// Сбрасываем устройство
device.Reset(presentParams);
}
}
В заключении мы создадим обработчик щелчка мышью на панели выбора цвета вершины: при щелчке левой кнопкой мыши будет открываться стандартное диалоговое окно Windows выбора цвета, после чего панель окрасится цветом, выбранным пользователем, а изображение в компоненте xnaPanel будет перерисовано с учетом нового цвета вершины. Для этого выделите панель vertex1Panel и создайте следующий обработчик события Click:
private void vertex1Panel_Click(object sender, EventArgs e)
{
// Преобразовываем параметр sender к Panel
Panel panel = (Panel)sender;
// Выбираем в диалоговом окне текущий цвет вершины
colorDialog1.Color = panel.BackColor;
// Показываем стандартный диалог выбора цвета
if (colorDialog1.ShowDialog() == DialogResult.OK)
{
// Если пользователь выбрал цвет, изменяем цвет панели на выбраный
panel.BackColor = colorDialog1.Color;
// Обновляем содержимое нашего компонента xnaPanel
panelDX.Invalidate();
}
}
Как видно, обработчик получает информацию о нажатой панели из параметра sender, что позволяет без изменений использовать этот обработчик для панелей vertex2Panel и vertex3Panel: просто выделите эти панели и назначьте в качестве обработчика события Click метод vertex1Panel_Click.
Остается лишь откомпилировать программу и посмотреть результат. Готовое приложение можно найти в ../1/example.zip в каталоге Examples\Ch03\Ex01.
Примечание
В Visual Studio выбор активного Set as StartUp Project (рисунок 3.4).
(рис 3.4) Выбор активного проекта с использованием контекстного меню Практическое упражнение №3.1.
Напишите приложение, отображающее круг. В правой части окна должны быть расположены элементы управления, позволяющие задавать различные параметры изображения: радиус круга, количество секторов в круге, режим визуализации круга (поточечный, каркасный или с закраской) и цвет круга (рисунок 3.5).
(рис 3.5) Иллюстрация к практическому упражнению №3.1Для ограничения области визуализации XNA Framework воспользуйтесь компонентом XnaPanel из примера Ch03\Ex02. Существует два способа добавления компонента XnaPanel в окно Toolbox:
XnaPanel из примера Ch03\Ex01. Для этого во вкладке Solution Explorer щелкнете правой кнопкой мыши на узле с названием решения ( Solution ) и выберете в контекстном меню пункт Add | Existing Project… . Укажите в открывшемся окне файл проекта компонента PanelDX (Examples\Ch03\Ex01 - XnaPanel\XnaPanel\XnaPanel.csproj) и нажмите Ok. После компиляции подключенного проекта (Ctrl + Shift + B) компонент Xna Panel автоматически появится в окне Toolbox.ToolBox компонент из сборки примера Ex01. Для этого щелкните правой кнопкой мыши на поверхности окна ToolBox и выберите в контекстном меню пункт Choose Items…. Откроется диалоговое окно Choose Toolbox Items…. Щелкните на кнопку Browse… и укажите сборку с компонентом PanelDX (Examples\Ch03\Ex01 - XnaPanel\XnaPanel\bin\Release\XnaPanel.dll) . Наконец, нажмите кнопку Ok, после чего на панели Toolbox появится компонент XnaPanel.Какой из этих двух вариантов выбрать – решать вам, но лично мне больше симпатизирует первый подход.
Как известно, подавляющее большинство игровых приложений работают в полноэкранном режиме. На это есть несколько веских причин:
Windows вроде панели задач ( Taskbar ).back ) буферы без необходимости копирования информации между буферами: при показе итогового изображения XNA Framework просто делает задний буфер экранным, а экранный задним.К недостаткам полноэкранного режима можно отнести несколько усложнение кода приложения и невозможность использования элементов управления Windows Forms, что затрудняет применение полноэкранного режима в различных утилитах.
Переход полноэкранный режим осуществляется путем создания графического устройства ( GraphicsDevice ) с соответствующим образом настроенной структурой PresentationParameters. Легко догадаться, что свойству IsFullScreen необходимо присвоить значение true:
presentParams = new PresentationParameters(); presentParams.IsFullScreen = true;
Кроме того, приложение должно задать параметры используемого видеорежима, а именно:
back ) буферов задается свойствами BackBufferWidth и BackBufferHeight структуры PresentationParameters. Например:
// Используем видеорежим с разрешением 640 x 480 presentParams.BackBufferWidth = 640; presentParams.BackBufferHeight = 480;
Любой видеорежим характеризуется определенным форматом пикселей, используемым для хранения информации о цвете пикселей. Формат пикселей определяет, сколько бит отводится для хранения каждого цвета и как распределены биты между различными цветовыми каналами. Формат пикселей заднего буфера задается свойством BackBufferFormat структуры PresentationParameters:
public SurfaceFormat BackBufferFormat { get; set; }
где
SurfaceFormat - перечислимый тип, используемый для задания формата пикселей (таблица 3.1).В качестве формата экранного буфера автоматически используется формат, наиболее близкий к формату заднего буфера. Например, если вы укажете для заднего буфера формат SurfaceFormat.Color, в качестве формата экранного буфера будет выбран формат SurfaceFormat.Bgr32. Обычно это не существенно, ведь альфа-канал все равно не оказывает никакого влияния на отображаемом на экране изображении; тем не менее, в некоторых немногочисленных ситуациях этот нюанс все же приходится учитывать.
| Значение | Может использоваться в качестве формата экранного буфера | Размер в битах | ||||
|---|---|---|---|---|---|---|
| Весь пиксель | Красный канал | Зеленый канал | Синий канал | Альфа канал | ||
Unknown |
? | ? | ? | ? | ? | |
Rgba1010102 |
X | 32 | 10 | 10 | 10 | 2 |
Color |
32 | 8 | 8 | 8 | 8 | |
Bgr32 |
X | 32 | 8 | 8 | 8 | - |
Bgr565 |
X | 16 | 5 | 6 | 5 | - |
Bgra5551 |
16 | 5 | 5 | 5 | 1 | |
По умолчанию свойство SurfaceFormat равно SurfaceFormat.Unknown. В оконном режиме это значение указывает на необходимость использования в качестве формата заднего буфера такой же формат пикселей, как у рабочего стола. Например, если вы используете в Windows 32-х битный видеорежим, задний буфер будет использоваться формат SurfaceFormat.Bgr32. Эта особенность позволила нам не утруждать себя выбором формата заднего буфера для оконных приложений первой и второй глав книги.
Однако применение формата SurfaceFormat.Unknown для полноэкранного графического устройства приводит к генерации исключения System.InvalidOperationException. Так что при создании устройства, использующего полноэкранный режим, приложение обязано явно указывать формат пикселей заднего буфера. В нашем первом приложении мы, не мудрствуя лукаво, будем использовать формат SurfaceFormat.Bgr32, поддерживаемый всеми видеокартами, совместимыми с XNA Framework: presentParams.BackBufferFormat = SurfaceFormat.Bgr32 ;
Примечание
Должно быть, вы заметили, что пиксели формата SurfaceFormat.Bgr32 имея размер 32 бита, содержат всего 24 бита полезной информации (для красного, синего и зеленого каналов отведено по 8 бит). Такая избыточность обусловлена тем, что современные процессоры умеют работать только с типами данных разрядностью 8, 16, 32 и 64 бит. Соответственно, 24-х битные типы данных пришлось бы эмулировать посредством 8, 16 и 32-х разрядных типов, что неминуемо увеличило количество операций, требуемых для считывания и записи значения пикселя и, соответственно, ощутимо снизило производительность. Например, для записи 24-х битного значения необходимо загрузить регистр 32 бита, модифицировать 24 бита, и записать полученное 32-х битное значение обратно в память.
И, наконец, последней характеристикой любого графического режима является частота обновления экрана. Для современных электронно-лучевых трубок нормой является частота обновления порядка 85 герц и выше, так как при более низких частотах становится заметным мерцание монитора. Мониторы менее критичны к низкой частоте обновления экрана, однако редкий современный -монитор поддерживает частоту смены кадров более 60 Гц. Частота обновления экрана задается свойством FullScreenRefreshRateInHz структуры PresentationParameters:
public int FullScreenRefreshRateInHz { get; set; }
В оконном режиме этому свойству присевается нулевое значение, ведь окно все равно может обновляться с частотой, отличной от частоты обновления, используемой рабочим столом. В полноэкранном режиме нулевое значение параметра FullScreenRefreshRateInHz, означает, что приложение отдает выбор частоты обновления экрана на откуп Windows и драйверам видеокарты/монитора. Так как при указании частоты обновления неподдерживаемой текущей видеоподсистемой может быть сгенерировано исключение System. InvalidOperationException, мы пока не будем пытаться самостоятельно выбирать частоту обновления экрана.
Таким образом, код создания графического устройства должен выглядеть аналогично листингу 3.4.
presentParams = new PresentationParameters(); presentParams.BackBufferCount = 1; presentParams.SwapEffect = SwapEffect.Discard; // Используем полноэкранный режим 640Ч480, 32 бита на пиксель, частота обновления выбирается // автоматически presentParams.IsFullScreen = true; presentParams.BackBufferWidth = 640; presentParams.BackBufferHeight = 480; presentParams.BackBufferFormat = SurfaceFormat.Bgr32; presentParams.FullScreenRefreshRateInHz = 0; 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);
Так же необходимо подправить код обработчика событий Resize. Дело в том, что даже в полноэкранном alt + tab ) Windows может самостоятельно изменять размер формы, что, разумеется, приводит к генерации события Resize. Но так как в полноэкранном режиме размер формы некоим образом не связан с разрешением экрана, приложение не должно изменять размер заднего ( back ) буфера:
private void MainForm_Resize(object sender, EventArgs e)
{
// Если приложение находится в полноэкранном режиме,
оно не должно отслеживать изменение
// размера формы
if ((!presentParams.IsFullScreen)
(WindowState != FormWindowState.Minimized))
{
presentParams.BackBufferWidth = ClientSize.Width;
presentParams.BackBufferHeight = ClientSize.Height;
device.Reset(presentParams);
}
}
Визуализация сцены осуществляется аналогично оконному режиму:
private void MainFormPaint(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.Viewport = Helper.FullScreenViewport presentParams);
// Очищаем весь экран
device.Clear(XnaGraphics.Color.CornflowerBlue);
// Используем квадратную область визуализации
(во избежание геометрических искажений
// изображения)
device.Viewport = Helper.SquareViewport(presentParams);
// Рисуем CD-диск (см. раздел 2.6.3)
}
catch (DeviceNotResetException)
{
Invalidate() ;
}
catch (DeviceLostException)
{
}
}
Стоит отметить, что в связи с переходом к полноэкранному режиму при вычислении квадратной области в центре экрана используются не размеры клиентской формы, а информация о размере заднего буфера из структуры PresentationParameters presentParams. Собственно вычисление размеров области визуализации осуществляется двумя дополнительными перегруженными методами:
// Вычисление квадратной области визуализации
public static Viewport SquareViewport(PresentationParameters
presentParams)
{
return SquareViewport(new System.Drawing.Size(presentParams.
BackBufferWidth,
presentParams.BackBufferHeight)); }
// Вычисление области визуализации размеров во весь экран
public static Viewport FullScreenViewport(PresentationParameters
presentParams)
{
return FullScreenViewport(new System.Drawing.Size(presentParams.
BackBufferWidth,
presentParams.BackBufferHeight));
}
Готовое приложение, визуализирующее изображение CD-диска в полноэкранном режиме (рисунок 3.6), находится в каталоге Examples\Ch03\Ex03.
(рис 3.6) CD-диск, визуализированный полноэкранном режиме (640x480x32bpp)В примере Ch03\Ex03 (листинг 3.4) мы неявно предполагаем, что абсолютно все современные и будущие видеоподсистемы будут поддерживать видеорежим 640Ч480Ч32 System.InvalidOperationException. Поэтому было бы логичным на всякий случай предусмотреть поведение приложение при отсутствии поддержки требуемого видеорежима. Например, при неудачной попытке создания полноэкранного графического устройства приложение могло бы попытаться создать графическое устройство, осуществляющее визуализацию в окне:
presentParams = new PresentationParameters();
presentParams.BackBufferCount = 1;
presentParams.SwapEffect = SwapEffect.Discard;
presentParams.IsFullScreen = true;
// Используем полноэкранный режим 640Ч480, 32 бита на пиксель, 60 герц
presentParams.BackBufferWidth = 640;
presentParams.BackBufferHeight = 480;
presentParams.BackBufferFormat = SurfaceFormat.Bgr32;
presentParams.FullScreenRefreshRateInHz = 60;
try {
device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
DeviceType.Hardware, this.Handle,
options, presentParams);
}
catch(InvalidOperationException)
{
// Если попытка использования полноэкранного режима
потерпела фиаско, используем
// визуализацию в окне
presentParams.IsFullScreen = false;
presentParams.BackBufferWidth = 0;
presentParams.BackBufferHeight = 0;
presentParams.BackBufferFormat = SurfaceFormat.Unknown;
presentParams.FullScreenRefreshRateInHz = 0;
// После выполнения этой команды интерфейс формы будет
заблокирован: форму нельзя
// будет перемещать по экрану, изменять ее размер,
минимизировать и т.п.
device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
DeviceType.Hardware,
this.Handle, options, presentParams);
}
Хотя этот не содержит ничего противоестественного, на практике он не будет нормально функционировать: из-за ошибки в XNA Framework после неудачной попытки перехода в полноэкранный режим приложение уже не может использовать оконный режим. В следующих версиях XNA Framework эта проблема по видимости будет исправлена, ну а пока нам придется, как и всем настоящим героям, идти в
И так, при создании полноэкранного графического устройства необходимо подобрать видеорежим, гарантированно поддерживаемый данной видеокартой и монитором. А почему бы нам вместо гадания на кофейной гуще не использовать текущий видеорежим рабочего стола, по определению поддерживаемый видеоподсистемой компьютера? Ведь, как известно, в качестве видеорежима рабочего стола обычно используется видеорежим, наиболее оптимальный для текущего монитора компьютера. Это особенно актуально для -мониторов, оптимизированных для работы каком-то одном разрешении экрана (как правило, 1024x768 или 1280x1024). Информация о текущем видеорежиме доступна посредством свойства CurrentDisplayMode класса GraphicsAdapter:
public DisplayMode CurrentDisplayMode { get; }
Вся информация о видеорежиме хранится в структуре DisplayMode:
public struct DisplayMode
{
// Количество пикселей по горизонтали
public int Width { get; }
// Количество пикселей по вертикали
public int Height { get; }
// Формат пикселей
public SurfaceFormat Format { get; }
// Частота обновления экрана
public int RefreshRate { get; }
}
Соответственно, код создания графического устройства примет следующий вид:
// Получаем описание текущего видеорежима DisplayMode displayMode = GraphicsAdapter.DefaultAdapter. CurrentDisplayMode; presentParams = new PresentationParameters(); presentParams.IsFullScreen = true; presentParams.BackBufferCount = 1; presentParams.SwapEffect = SwapEffect.Discard; // Использует такие же параметры визуализации, как у текущего видеорежима presentParams.BackBufferWidth = displayMode.Width; presentParams.BackBufferHeight = displayMode.Height; presentParams.BackBufferFormat = displayMode.Format; presentParams.FullScreenRefreshRateInHz = displayMode.RefreshRate; 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);
Хотя использование видеорежима рабочего стола дает вполне нормальные результаты, в ряде случаев подобный подход оказывается неприемлемым. В основном это касается дешевых видеокарт, демонстрирующих довольно низкую производительность в современных трехмерных приложениях, в результате чего приемлемая частота смены кадров достигается лишь в самых низких разрешениях вроде 640Ч480 или 800Ч600. Поэтому было логичным предоставить пользователю возможность самостоятельно выбирать параметры видеорежима в зависимости от своих потребностей.
Наше следующее приложение будет отображать на экране перечень всех видеорежимов, поддерживаемых видеокартой и затем переключаться в видеорежим, выбранный пользователем, и визуализировать изображение диска (рисунок 3.7). Эту функциональность достаточно легко реализовать, так как разработчики XNA Framework снабдили класс GraphicsAdapter коллекцией SupportedDisplayModes, содержащей набор структур DisplayMode с информацией обо всех видеорежимах, поддерживаемых указанной связкой видеокарта – монитор.
(рис 3.7) Диалоговое окно выбора видеорежима Итак, создайте новое приложение Windows Forms и добавьте в него новую форму. Присвойте свойствам формы значения согласно таблице 3.2. Поместите на форму компонент ListBox и назовите его displayModeListBox. В заключение, поместите на форму кнопку Ok и присвойте ее свойству DialogResult значение OK.
| Свойство | Значение |
|---|---|
Name |
DisplayModeForm |
Text |
Выберите видеорежим |
FormBorderStyle |
FixedDialog |
MaximizeBox |
False |
MinimizeBox |
False |
Теперь реализуем логику работы формы, а именно: конструктор формы и свойство SelectedDisplayMode, возвращающее информацию о выбранном видеорежиме (листинг 3.9).
public partial class DisplayModeForm : Form
{
// Конструктор формы. В качестве параметра
принимает объект графического адаптера, для
// которого необходимо выбрать графический режим. Public
DisplayModeForm(GraphicsAdapter adapter)
{
InitializeComponent();
// Перебираем все графические режимы, поддерживаемые
графическим адаптером и добавляем их на
// панель displayModeListBox
foreach (DisplayMode diplayMode in adapter.SupportedDisplayModes)
displayModeListBox.Items.Add(diplayMode);
// Выбираем самый первый элемент списка видеорежимов
displayModeListBox.SelectedIndex = 0;
}
// Возвращает информацию о графическом режиме, выбранном пользователем
public DisplayMode SelectedDisplayMode
{
get
{
return (DisplayMode)displayModeListBox.SelectedItem;
}
}
}
И наконец, в обработчик события Load главной формы необходимо вставить код взаимодействия с диалоговым окном выбора видеорежима:
private void MainFormLoad(object sender, EventArgs e)
{
SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
MinimumSize = SizeFromClientSize(new Size(1, 1));
presentParams = new PresentationParameters();
presentParams.IsFullScreen = true;
presentParams.BackBufferCount = 1;
presentParams.SwapEffect = SwapEffect.Discard;
// Создаем диалоговое окно выбора видеорежима
using (DisplayModeForm displayModeForm = new
DisplayModeForm(GraphicsAdapter.DefaultAdapter))
{
// Отображаем диалоговое окно
displayModeForm.ShowDialog();
// Получаем видеорежим, выбранный пользователем
DisplayMode mode = displayModeForm.SelectedDisplayMode;
// Задаем требуемый видеорежим
presentParams.BackBufferWidth = mode.Width;
presentParams.BackBufferHeight = mode.Height;
presentParams.BackBufferFormat = mode.Format;
presentParams.FullScreenRefreshRateInHz = mode.RefreshRate;
}
// Создаем графическое устройство
device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
DeviceType.Hardware,
4> this.Handle, options, presentParams);
}
Оставшийся код приложения не содержит чего-либо интересного, поэтому мы не будем на нем останавливаться. Готовое приложение находится в каталоге Examples\Ch03\Ex05.
Запустите полученное приложение и попробуйте поэкспериментировать с различными разрешениями экрана. Очень скоро вы заметите одну важную особенность: в некоторых видеорежимах диск вместо круглой формы растягивается/сплющивается в эллипсовидную фигуру. После внимательного изучения соотношения сторон разрешений экрана мы прейдем к выводу, что диск корректно отображается лишь при условии совпадения отношения сторон монитора с отношением соответствующих размерностей разрешения экрана. Например, отношение сторон моего монитора NEC MultiSync FE991SB равно 4:3 = 1.33, поэтому в разрешениях 320Ч240, 640Ч480, 800Ч600, 1024Ч768, 1280Ч960, 1600Ч1200, 1792Ч1344 изображение отображается без искажений. В разрешении 1280Ч1024 (соотношение сторон 5:4=1.25) круг едва заметно сплющивается вдоль оси Y, а вот в разрешении 1360Ч768 (соотношение сторон 16:9=1.77) сплющивание круга вдоль оси X становится более чем заметным. С другой стороны, на широкоэкранных мониторах с соотношением сторон 16:9 именно разрешения 1088Ч612, 1280Ч768, 1360Ч768 и 1600Ч900 (т.е. с соотношением сторон 16:9) будут давать изображение без искажений.
(рис 3.8) Искажение формы круглого диска, визуализированного в разрешении 1360Ч768, при отображение на мониторе с соотношение сторон 4:3Чем обусловлено данное явление? Для борьбы с искажениями наше приложение использует область визуализации квадратной формы. Однако если хорошо подумать, равенство ширины и высоты области визуализации еще не гарантирует квадратной области визуализации, так как необходимо еще одно условие: соотношение сторон разрешение экрана должно совпадать с соотношением сторон экрана монитора. Если эта не так, квадратная область визуализации в действительности окажется неквадратной и форма изображения будет искажена.
Примечание
Обратите внимание на отсутствие в списке видеорежимов с форматами пикселей с поддержкой альфа канала. Дело в том, что коллекция SupportedDisplayModes содержит только видеорежимы экранного буфера, а экранный буфер не поддерживает альфа-канал по причине банальной ненужности (см. таблицу 3.1). Так что отсутствие поддержки альфа-канала экранным буфером вовсе не означает отсутствие поддержки форматов с альфа-каналом задним буфером. Более того, если современная видеокарта поддерживает формат SurfaceFormat.Bgr32, то она с вероятность 99.9% поддерживает и формат SurfaceFormat.Color. Впрочем, эти нюансы для нас пока не актуальны, так как наши текущие приложения все равно не используют альфа-канал.
Диалоговое окно выбора видеорежима из примера Ch03\Ex05 хотя и выполняет свою задачу, однако имеет при этом ряд существенных недостатков, затрудняющих его применение в реальных приложениях:
Ну что ж, настало время разработать новую версию диалогового окна, свободную от этих недостатков (рисунок 3.9). Запустите Visual Studio 2005 и создайте приложение Windows Application. Мы начнем с создания класса, инкапсулирующего работу с файлами конфигурации приложения. Наш файл конфигурации будет содержать 7 полей:
| Поле | Тип данных | Описание | Значение по умолчанию |
|---|---|---|---|
Width |
int |
размер заднего буфера вдоль оси X |
0 |
Height |
int |
размер заднего буфера вдоль оси Y |
0 |
Format |
SurfaceFormat |
формат пикселей заднего буфера | SurfaceFormat.Unknown |
RefreshRate |
int |
частота обновления экрана | 0 |
FullScreen |
bool |
работает ли приложение в полноэкранном режиме ( false – нет, true – да) |
true |
Init |
bool |
равен true, если информация о конфигурации приложения была загружена из файла |
false |
ShowSettingForm |
bool |
надо ли отображать при запуске приложения диалоговое окно выбора видеорежима | true |
Так как наш файл конфигурации не будет представлять собой нечего экстравагантного, все операции по созданию класса вполне можно выполнить в дизайнере Visual Studio. Для этого в окне Solution Explorer раскройте узел Properties, и сделайте двойной щелчок левой кнопкой мыши на узле Setting.settings, после чего слева откроется дизайнер файла конфигурации

(рис 3.10) Новая версия диалогового окна выбора видеорежима (рис 3.9) Настройка параметров конфигурации приложения Теперь приступим к созданию диалогового окна настройки параметров приложения. Добавьте приложение в новую форму, разместите на ней элементы управления аналогично рисунку 3.9 и задайте их свойства согласно таблице 3.4.
| Класс | Свойство | Значение |
|---|---|---|
Form (диалоговое окно) |
Name |
SettingsForm |
Text |
Параметры | |
FormatBorderStyle |
FixedDialog |
|
MinimizeBox |
false |
|
MaximizeBox |
false |
|
CheckBox |
Name |
inWindowCheckBox |
Text |
Визуализировать в окне | |
GroupBox |
Text |
Видеорежим |
Label |
Text |
Соотношение сторон |
ComboBox (фильтр отображаемых видеорежимов по соотношению сторон) |
Name |
aspectRatioComboBox |
DropDownStyle |
DropDownList |
|
Label |
Text |
Разрешение |
ComboBox (список разрешений экрана) |
Name |
resolutionComboBox |
DropDownStyle |
DropDownList |
|
Label |
Text |
Глубина цвета: |
ComboBox (список форматов пикселей) |
Name |
colorDepthComboBox |
DropDownStyle |
DropDownList |
|
Label |
Text |
Частота обновления |
ComboBox (список частот обновления экрана) |
Name |
refreshRateComboBox |
DropDownStyle |
DropDownList |
|
Button |
Name |
okButton |
Text |
Ok |
|
Button |
Text |
Cancel |
DialogResult |
Cancel |
Как видно, диалоговое окно содержит списки соотношений сторон экрана, разрешений, глубины цвета (формата пикселей) и частот обновления экрана. Информация в этих списках должна отображаться в дружелюбной форме, ведь неподготовленный пользователь вряд ли сможет без подсказки понять разницу между форматами Bgr32 и Bgr565. Кроме того, информация в списках должна быть отсортирована по некоторому критерию: например, видеорежимы должны перечислять в порядке увеличения разрешения экрана. Чтобы реализовать эти требования придется создать четыре структуры (по одной на каждый список), инкапсулирующие элементы списков. Для этого необходимо добавить в файл диалогового окна код из листинга 3.11.
// Инкапсулирует элемент списка соотношений сторон
public struct AspectItem
{
// Коэффициент отношения сторон
public float aspect;
public AspectItem(float aspect)
{
this.aspect = aspect;
}
// Проверяет указанный элемент списка разрешений
(ResolutionItem) на соответствие текущему
// соотношению сторон. В случае равенства метод
возвращает true, иначе - false public bool
Compare(float aspect)
{
// Нулевое соотношение сторон является зарезервированным
значением ( "любое соотношение
// сторон "), поэтому возвращаем true if
(this.aspect == 0) return true;
// Сравниваем коэффициенты отношения сторон,
закладывая небольшой "запас " в
один процент.
// Дело в том, что некоторые режимы имеют не совсем
"академически " правильное
соотношение
// сторон. Например, соотношение сторон разрешения
1360 x 768 равно 85/48=1.771, в
// как 16/9=1.778.
if (Math.Abs(this.aspect - aspect) < this.aspect * 0.01f)
return true;
else
return false;
}
// Возвращает текущее соотношение сторон в удобочитаемом виде
public override string ToString()
{
if (aspect == 0)
return "Любое";
if (Compare(4.0f / 3.0f))
return "4 : 3";
if (Compare(5.0f / 4.0f))
return "5 : 4";
if (Compare(16.0f / 9.0f)) return "16 : 9";
return aspect.ToString();
}
}
// Инкапсулирует элемент списка разрешений экрана
public struct ResolutionItem : IComparable<ResolutionItem>
{
// Разрешение экрана
public int width;
public int height;
public ResolutionItem(int width, int height)
{
this.width = width;
this.height = height;
}
// Отношение количества пикселей вдоль осей X и Y для
текущего разрешения экрана
public float Aspect
{
get
{
return (float)width / (float)height;
}
}
// Возвращает в удобочитаемом виде текст элемента
списка public override string ToString()
{
return width.ToString() + " x " + height.ToString();
}
// Реализация интерфейса IComparable<ResolutionItem>,
используемого при сортировке элементов
// списка
public int CompareTo(ResolutionItem other)
{
// Сначала сравнивает ширина разрешений if (width > other.width)
return 1;
if (width < other.width)
return -1;
// Если ширина одинаковая, сравнивается высота разрешений.
В результате разрешения сначала
// сортируются по ширине, а затем по высоте. if (height >
other.height) return 1;
if (height < other.height) return -1;
return 0;
}
}
// Инкапсулирует элемент списка форматов пикселей
public struct ColorDepthItem : IComparable<ColorDepthItem>
{
// Формат пикселей
public SurfaceFormat value;
public ColorDepthItem(SurfaceFormat value)
{
this.value = value;
}
// Возвращает строку с описанием формата пикселя
public override string ToString()
{
string str;
switch (value)
{
case SurfaceFormat.Bgr32:
str = "32 бита ({0})";
break;
case SurfaceFormat.Bgr565:
str = "16 бит ({0})";
break; case
SurfaceFormat.Rgba1010102:
str = "32 бит ({0})";
break; case
SurfaceFormat.Bgr555:
str = "16 бит {0}";
break; default:
str = "{0}";
break;
}
return string.Format(str, value);
}
// Реализация интерфейса IComparable<ColorDepthItem>,
используемого при сортировке элементов
// списка
public int CompareTo(ColorDepthItem other)
{
if (value > other.value)
return -1;
if (value < other.value)
return 1;
return 0;
}
}
// Инкапсулирует элемент списка частот обновлений экрана public
struct RefreshRateItem : IComparable<RefreshRateItem>
{
// Частота обновления экрана
public int value;
public RefreshRateItem(int value)
{
this.value = value;
}
// Возвращает строку с частотой обновления экрана
public override string ToString()
{
return value.ToString() + " Гц";
}
// Реализация интерфейса IComparable< RefreshRateItem>,
используемого при сортировке
// элементов списка
public int CompareTo(RefreshRateItem other)
{
if (value > other.value)
return 1;
if (value < other.value)
return -1;
return 0;
}
}
Следующий шаг - создание конструктора формы, принимающего в качестве параметров текущие настройки приложения и объект графического адаптера, используемого приложением (листинг 3.12).
public partial class SettingsForm : Form
{
// Набор ассоциативных сортированных массив с
информаций о поддерживаемых видеорежимах
SortedDictionary<ResolutionItem, SortedDictionary<ColorDepthItem,
SortedDictionary<RefreshRateItem, bool>>> modeCollection;
// Текущие настройки приложения. Класс Properties.Settings
автоматически генерируется
// дизайнером настроек приложения (рисунок 3.10).
Properties.Settings settings;
// Конструктор формы
internal SettingsForm(Properties.Settings settings,
GraphicsAdapter adapter)
{
InitializeComponent();
// Сохраняем ссылку на настройки приложения
this.settings = settings;
// Если файл с конфигурацией приложения не был найден,
устанавливаем в качестве параметров по
// умолчанию настройки рабочего стола if (!settings.Init)
{
settings.Width = adapter.CurrentDisplayMode.Width;
settings.Height = adapter.CurrentDisplayMode.Height;
settings.Format = adapter.CurrentDisplayMode.Format;
settings.RefreshRate = adapter.CurrentDisplayMode.RefreshRate;
settings.FullScreen = true;
}
modeCollection = new SortedDictionary<ResolutionItem,
4> SortedDictionary<ColorDepthItem, SortedDictionary<RefreshRateItem,
bool>>>();
// Перебираем все видеорежимы, поддерживаемые видеокартой и
заполняем ассоциативный массив
// modeCollection
foreach (DisplayMode mode in adapter.SupportedDisplayModes)
{
ResolutionItem res = new ResolutionItem(mode.Width, mode.Height);
if (!modeCollection.ContainsKey(res))
modeCollection.Add(res, new SortedDictionary<ColorDepthItem,
SortedDictionary<RefreshRateItem, bool>>());
ColorDepthItem depth = new ColorDepthItem(mode.Format); if
(!modeCollection[res].ContainsKey(depth))
modeCollection[res].Add(depth, new SortedDictionary<RefreshRateItem,
bool>());
RefreshRateItem refresh = new RefreshRateItem(mode.RefreshRate); if
(!modeCollection[res][depth].ContainsKey(refresh))
modeCollection[res][depth].Add(refresh, true); }
// Задаем состояние флага "визуализировать в окне "
inWindowCheckBox.Checked = !settings.FullScreen;
// Добавляем в список "соотношение сторон " типовые фильтры
видеорежимов aspectRatioComboBox.Items.Add(new AspectItem());
aspectRatioComboBox.Items.Add(new AspectItem(4.0f / 3.0f));
aspectRatioComboBox.Items.Add(new AspectItem(5.0f / 4.0f));
aspectRatioComboBox.Items.Add(new AspectItem(16.0f / 9.0f));
// Выбираем самый первый элемент списка ( "Любое ")
aspectRatioComboBox.SelectedIndex = 0; }
}
Как видно, информация о поддерживаемых видеорежимах хранится в многомерном ассоциативном массиве, упрощающем поиск информации о требуемых видеорежимах. Например, для проверки существования видеорежима с разрешением 1024x768x32bpp:@85Hz приложение должно проверить существование элемента modeCollection[ "1024x768 "][ SurfaceFormat.Brg32][85] . Последний оператор конструктора выбирает нулевой элемент списка, генерируя событие SelectedIndexChanged, обработчик которого приведен в листинге 3.13.
// Обновляет список поддерживаемых разрешений экрана с
учетом нового фильтра соотношения
// сторон
private void aspectRatioComboBoxSelectedIndexChanged(object
sender, EventArgs e)
{
// Получаем текущий элемент списка "соотношение сторон "
AspectItem aspect = (AspectItem)aspectRatioComboBox.SelectedItem;
// Текущее разрешение экрана
ResolutionItem currentResolution;
// Используем в качестве текущего разрешения экрана элемент
из списка "Разрешение ". Если же
// элемент в списке не выбран, используем разрешение
экрана из текущих настроек приложения if
(resolutionComboBox.SelectedIndex != -1)
currentResolution = (ResolutionItem)resolutionComboBox.
SelectedItem; else
currentResolution = new ResolutionItem(settings.Width,
settings.Height);
// Очищаем список разрешений экрана
resolutionComboBox.Items.Clear();
// Перебираем все разрешения экрана
foreach (ResolutionItem res in modeCollection.Keys)
// Если разрешение экрана удовлетворяет заданному соотношению сторон
if (aspect.Compare(res.Aspect))
// Добавляем его в список разрешений
resolutionComboBox.Items.Add(res);
// Если было найдено хотя бы одно разрешение экрана,
соответствующее заданному соотношению
// сторон
if (resolutionComboBox.Items.Count != 0)
{
// В списке разрешений экрана пытаемся выбрать текущее разрешение
resolutionComboBox.SelectedItem = currentResolution;
// Если это не удалось, выбираем самый первый элемент списка if
(resolutionComboBox.SelectedIndex == -1)
resolutionComboBox.SelectedIndex = 0; }
// Вызываем обработчик события изменения состояния
переключателя "Визуализировать в окне ",
// который при необходимости активирует/деактивирует
определенные элементы формы вроде кнопки
// Ok
inWindowCheckBox_CheckedChanged(inWindowCheckBox, null);
}
// Обработчик события CheckedChange флага "Визуализировать
в окне ",
private void inWindowCheckBox_CheckedChanged(object sender, EventArgs e)
{
// Если флаг включен
if (inWindowCheckBox.Checked)
{
// Деактивируем все списки, связанные с параметрами
полноэкранного режима
aspectRatioComboBox.Enabled = false; resolutionComboBox.Enabled =
false; colorDepthComboBox.Enabled = false; refreshRateComboBox.Enabled
= false; okButton.Enabled = true;
}
else
{
// Активируем список "Соотношение сторон "
aspectRatioComboBox.Enabled = true;
// Если список доступных разрешений экрана не пустой if
(resolutionComboBox.Items.Count > 0)
{
// Активируем оставшиеся три списка и кнопку Ok
resolutionComboBox.Enabled = true;
colorDepthComboBox.Enabled = true;
refreshRateComboBox.Enabled = true;
okButton.Enabled = true;
}
else
{
// В противном случае блокируем эти элементы управления
resolutionComboBox.Enabled = false;
colorDepthComboBox.Enabled = false;
refreshRateComboBox.Enabled = false; okButton.Enabled =
false;
}
}
}
Так как выбранное разрешение оказывает влияние на доступные форматы пикселей списка "Глубина цвета ", необходимо определить обработчик события SelectedIndexChanged списка разрешений (листинг 3.14).
private void resolutionComboBoxSelectedIndexChanged(object sender, EventArgs e)
{
// Получаем выбранный элемент списка разрешений экрана
ResolutionItem res = (ResolutionItem) resolutionComboBox.SelectedItem;
// Текущая глубина цвета
ColorDepthItem currentColorDepth;
// Используем в качестве текущей глубины цвета
элемент из списка "Глубина цвета ".
Если же
// элемент в списке не выбран, используем глубину
цвета из текущих настроек приложения
if (colorDepthComboBox.SelectedIndex != -1)
currentColorDepth = (ColorDepthItem)colorDepthComboBox.SelectedItem;
else
currentColorDepth = new ColorDepthItem(settings.Format);
// Очищаем список "Глубина цвета "
colorDepthComboBox.Items.Clear();
// Перебираем все форматы пикселей выбранного
разрешения экрана и добавляем их в список
// "Глубина цвета "
foreach (ColorDepthItem colorDepth in modeCollection[res].Keys)
colorDepthComboBox.Items.Add(colorDepth);
// Пытаемся выбрать глубину цвета, как у текущих настроек приложения
colorDepthComboBox.SelectedItem = currentColorDepth;
// В случае неудачи выбираем первый элемент списка
if (colorDepthComboBox.SelectedIndex == -1)
colorDepthComboBox.SelectedIndex = 0;
}
Список поддерживаемых частот экрана, разумеется, тоже зависит от выбранной глубины цвета и разрешения экрана, соответственно, необходимо реализовать и обработчик события SelectedIndexChanged списка глубины цвета (листинг 3.15).
private void colorDepthComboBox_SelectedIndex
Changed(object sender, EventArgs e)
{
// Получаем выбранное разрешение и глубину цвета (формат пикселей)
ResolutionItem res = (ResolutionItem)resolutionComboBox.SelectedItem;
ColorDepthItem colorDepth = (ColorDepthItem)
colorDepthComboBox.SelectedItem;
// Текущая частота обновления экрана
RefreshRateItem currentRefreshRate;
// Используем в качестве текущей частоты обновления
экрана элемент из списка. Если же элемент
// в списке не выбран, используем частоту обновления
экрана из текущих настроек приложения if
(refreshRateComboBox.SelectedIndex != -1)
currentRefreshRate = (RefreshRateItem)refreshRate
ComboBox.SelectedItem; else
currentRefreshRate = new RefreshRateItem(settings.RefreshRate);
// Очищаем список "Частота обновления "
refreshRateComboBox.Items.Clear();
// Перебираем частоты обновления экрана, поддерживаемые
выбранным разрешением с указанной
// глубиной цвета
foreach (RefreshRateItem refreshRate in modeCollection[res]
[colorDepth].Keys)
refreshRateComboBox.Items.Add(refreshRate);
// Выбираем в списке "Частота обновления "
текущую частоту обновления экрана
refreshRateComboBox.SelectedItem = currentRefreshRate;
// Если такого элемента нет, выбираем самый первый элемент списка
if (refreshRateComboBox.SelectedIndex == -1)
refreshRateComboBox.SelectedIndex = 0;
}
Завершая создание диалогового окна, мы должны реализовать обработчик нажатия кнопки Ok:
private void okButton_Click(object sender, EventArgs e)
{
// Обновляем настройки приложения на основе текущего
состояния элементов управления
// формы окна
settings.FullScreen = !inWindowCheckBox.Checked;
if (!inWindowCheckBox.Checked)
{
settings.Width = ((ResolutionItem)resolutionComboBox.SelectedItem).width;
settings.Height = ((ResolutionItem)resolutionComboBox.SelectedItem).height;
settings.Format = ((ColorDepthItem)colorDepthComboBox.SelectedItem).value;
settings.RefreshRate = ((RefreshRateItem)refreshRate
ComboBox.SelectedItem).value;
}
// Настройки были инициализированы. При следующем показа
диалогового окна все элементы
// управления будут инициализированы согласно значениями
сохраненных настроек
settings.Init = true;
// Пользователь настроил приложение. Показывать диалоговое
окно больше нет необходимости.
settings.ShowSettingForm = false;
DialogResult = DialogResult.OK;
}
И так, у нас есть класс для работы с настройками приложения и диалоговое окно для визуального управления этими настройками. Теперь самое время интегрировать эту функциональность в наше приложение. Для начала мы должны определиться со стратегией поведения приложения, а именно, в каких случаях оно должно отображать диалоговое окно:
Init класса Settings равному true.ShowSettingForm конфигурации приложения значение true и завершить работу. А при следующем запуске приложение обнаружит, что свойство ShowSettingForm равно true, и отобразит диалоговое окно с настройками приложения.Load главной формы приложения распознавание ключа /config в параметрах командной строки приложения. Таким образом, инсталлятор приложения наряду с ярлыком запуска приложения может создать дополнительный ярлык "Настройка приложения ", вызывающий это же приложение с параметром /config.Код, реализующий всю вышеперечисленную функциональность, будет иметь достаточно большой размер, поэтому его логично будет инкапсулировать в отдельный метод, чтобы не захламлять обработчик события Load (листинг 3.17).
using System.Diagnostics;
void InitGraphivsDevice()
{
SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
MinimumSize = SizeFromClientSize(new Size(1, 1));
// Загружаем настройки приложения из файла
settings = new Properties.Settings();
// Определяем, содержит ли командная строка параметр "/config
" string[] args = Environment.GetCommandLineArgs(); bool
configParam = false;
if ((args.Length == 2) (args[1].ToUpper() == "/CONFIG"))
configParam = true;
// Если выполняется одно из трех вышеописанных условий
if ((configParam) || (settings.ShowSettingForm) || (!settings.Init))
{
// Создаем диалоговое окно
using (SettingsForm settingsForm = new SettingsForm(settings,
4> GraphicsAdapter.DefaultAdapter))
{
// Отображаем диалоговое
окно
settingsForm.ShowDialog();
// Если командная строка содержит "/config
" if (configParam)
{
// Завершаем работу приложения closing =true;
Application.Idle += new EventHandler(ApplicationIdle); return;
}
}
}
presentParams = new PresentationParameters();
presentParams.BackBufferCount = 1;
presentParams.SwapEffect = SwapEffect.Discard;
// Настраиваем параметры визуализации согласно настройкам приложения
presentParams.IsFullScreen = settings.FullScreen;
if (settings.FullScreen)
{
presentParams.BackBufferWidth = settings.Width;
presentParams.BackBufferHeight = settings.Height;
presentParams.BackBufferFormat = settings.Format;
presentParams.FullScreenRefreshRateInHz = settings.RefreshRate;
}
else
{
presentParams.BackBufferWidth = 0;
presentParams.BackBufferHeight = 0;
presentParams.BackBufferFormat = SurfaceFormat.Unknown;
presentParams.FullScreenRefreshRateInHz = 0;
}
// Проверяем наличие аппаратных вершинных процессоров
GraphicsDeviceCapabilities caps = GraphicsAdapter.
DefaultAdapter.GetCapabilities(
DeviceType.Hardware);
CreateOptions options = CreateOptions.SingleThreaded;
if (caps.DeviceCapabilities.SupportsHardwareTransformAndLight)
options |= CreateOptions.HardwareVertexProcessing; else
options |= CreateOptions.SoftwareVertexProcessing;
try
{
// Создаем графическое устройство
device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
DeviceType.Hardware,
this.Handle,
options, presentParams);
}
catch (InvalidOperationException)
{
// Если при создании графического устройства возникли проблемы
closing = true;
Application.Idle += new EventHandler(ApplicationIdle);
// Отображаем диалоговое окно с предложением изменить параметры
визуализации
if (MessageBox.Show("Ошибка при создании графического устройства.
Изменить " +
"параметры визуализации (разрешение экрана и т.п.)?", "Ошибка",
MessageBoxButtons.YesNo,
MessageBoxIcon.Error) == DialogResult.Yes)
{
// При положительном ответе устанавливаем параметр ShowSettingForm
в значение true,
// указывающий на необходимость показать окно с настройками
приложения при следующем запуске
settings.ShowSettingForm = true;
// Сохраняем настройки приложения в файл
settings.Save();
// Перезапускаем приложение
Process.Start(Application.ExecutablePath);
}
return;
}
}
private void MainFormLoad(object sender, EventArgs e)
{
// Загружаем настройки из файла и создаем графическое устройство
InitGraphivsDevice();
// Если приложение завершает работу, выходим из обработчика
события Load
if (closing)
return;
// Остальные действие (создание декларации вершины, заполнение
массива вершин, загрузка
// эффекта и т.п.
...
}
Остальные фрагменты приложения не содержат чего-либо заслуживающего внимания, и вы сможете легко их реализовать самостоятельно. Готовое приложение находится в example.zip в каталоге Examples\Ch03\Ex03.
Местоположение файла настроек приложения
.NET Framework 2.0 сохраняет пользовательские настройки в XML -файле user.config, который располагается по достаточно запутанному пути:
<Profile Directory>\<Company Name>\<App Name><Evidence Type><Evidence Hash>\<Version>\user.config
где
Profile Directory - каталог локального профиля приложения, обычно имеющий название вроде c:\Documents and Settings\<Имя Пользователя>\Local Settings\Application DataCompany Name - строка, формируемая на основе названия компании, заданного атрибутом AssemblyCompany в файле Properties\AssemblyInfo.cs.App Name - строка, формируемая на основе названия приложения, заданного атрибутом AssemblyProduct в файле Properties\AssemblyInfo.cs.Evidence Type, Evidence Hash - вычисляются на основе информации о Version - строка, формируемая на основе версии приложения, заданной атрибутом AssemblyVersion в файле Properties\AssemblyInfo.cs.Например, на моем компьютере пример Сh03\Ex06 хранит информацию о конфигурации приложения в файле C:\Documents and Settings\Administrator\Local Settings\Application Data\GSPInc\Ex06-FullscreenDialog.Url31gvfqvzegievbxah0w1qmgu2s2siyo3\1.0.0.0\user.config. Сам файл имеет простую структуру и легко может быть проанализирован любым продвинутым пользователем:
<?xml version="1.0" encoding="utf-8"?> <configuration> <userSettings> <GSP.XNA.Book.Ch03.Ex06.Properties.Settings> <setting name="Width" serializeAs="String"> <value>1280</value> </setting> <setting name="Height" serializeAs="String"> <value>960</value> </setting> <setting name="Format" erializeAs="String"> <value>Bgr32</value> </setting> <setting name="RefreshRate" serializeAs="String"> <value>85</value> </setting> <setting name="FullScreen" serializeAs="String"> <value>True</value> </setting> <setting name="Init" serializeAs="String"> <value>True</value> </setting> <setting name="ShowSettingForm" serializeAs="String"> <value>False</value> </setting> </GSP.XNA.Book.Ch03.Ex06.Properties.Settings> </userSettings> </configuration>
Это обстоятельство позволяет редактировать файл в обычном текстовом редакторе, что очень полезно при отладке приложения. Например, в целях повышения "дуракоустройчивости " можно легко протестировать поведение приложения при некорректном значении параметров файла конфигурации.
До сих пор мы визуализировали исключительно статичные изображение, в то время как XNA Framework в первую очередь предназначен для визуализация динамичных сцен с движущимися объектами. Как создать анимированную сцену? Обратимся к
И так, для создания анимации мы должны отображать визуализировать различные фазы движения изображения с частотой 25 кадров в секунду. Наше первое приложение будет визуализировать шарик (точнее диск), летающий по форме и отскакивающий от ее стенок (рисунок 3.11). Анимация будет моделироваться посредством таймера (компонент Timer ), тикающего с интервалом 40 миллисекунд (то есть 25 раз в секунду). После каждого тика таймера мы будем прибавлять к координатам шарика значение вектора скорости и перерисовывать сцену. При пролете шарика сквозь стенку он будет отскакивать обратно, при этом вектор скорости будет изменяться на противоположный. Для придания движениям шарика некоторой неопределенности, модуль вектора скорости будет изменяться на незначительную случайную величину. Основные фрагменты приложения приведены в листинге 3.18. Исходный код примера находится в example.zip в каталоге Examples\Ch03\Ex07.

(рис 3.18) Прыгающий шарик (рис 3.11) public partial class MainForm : Form { // Файл эффекта, используемый для визуализации изображения const string effectFileName = "Data\\ColorFill.fx"; // Минимальная скорость диска ( "шарика ") const float diskMinSpeed = 0.5f; // Максимальная скорость диска const float diskMaxSpeed = 1.3f; // Количество сегментов в диске const int diskSlices = 32; // Радиус диска const float diskRadius = 0.1f; // Цвет центра диска readonly static XnaGraphics.Color diskInnerColor = XnaGraphics.Color.White; // Цвет края диска readonly static XnaGraphics.Color diskOuterColor = XnaGraphics.Color.Green; // Толщина стенки вдоль границы экрана const float borderSize = 0.1f; // Цвет внутренней границы стенки readonly static XnaGraphics.Color borderInnerColor = XnaGraphics.Color.DarkBlue; // Цвет внешней границы стенки readonly static XnaGraphics.Color borderOuterColor = XnaGraphics.Color.CornflowerBlue; GraphicsDevice device = null; PresentationParameters presentParams; Effect effect = null; VertexDeclaration decl = null; // Массив вершин стены вдоль края формы VertexPositionColor[] borderVerts = null; // Массив вершин диска с центром в начале системы координат VertexPositionColor[] baseDiskVerts = null; // Массив вершин диска, перемещаемого по поверхности экрана VertexPositionColor[] diskVerts = null; FillMode fillMode = FillMode.Solid; // Скорость диска вдоль оси X float speedX; // Скорость диска вдоль оси Y float speedY; // Координата X центра диска float posX = 0; // Координата Y центра диска float posY = 0; // Генератор случайных чисел Random rnd = new Random(); // Конфигурация приложения (разрешение экрана и т.п.) Properties.Settings settings; bool closing = false; // Вычисляет случайную скорость диска, лежащую в диапазоне diskMinSpeed .. diskMaxSpeed float RndSpeed() { return diskMinSpeed + (float)rnd.NextDouble() * (diskMaxSpeed - diskMinSpeed); } // Обработчик события Load главной формы private void MainForm_Load(object sender, EventArgs e) { // Чтение файла конфигурации и создание графического устройства InitGraphivsDevice(); if (closing) return; // Создание декларации вершины decl = new VertexDeclaration(device, VertexPositionColor.VertexElements); // Создание и заполнение массива вершин, визуализируемого с использованием списка // треугольников (PrimitiveType.TriangleStrip) borderVerts = new VertexPositionColor[10]; borderVerts[0] = new VertexPositionColor(new Vector3(-1.0f, -1.0f, 0.0f), borderOuterColor); borderVerts[1] = new VertexPositionColor(new Vector3(-1.0f + borderSize, -1.0f + borderSize, 0.0f), borderInnerColor); borderVerts[2] = new VertexPositionColor(new Vector3(-1.0f, 1.0f, 0.0f), borderOuterColor); borderVerts[3] = new VertexPositionColor(new Vector3(-1.0f + borderSize, 1.0f -borderSize, 0.0f), borderInnerColor); borderVerts[4] = new VertexPositionColor(new Vector3(1.0f, 1.0f, 0.0f), borderOuterColor); borderVerts[5] = new VertexPositionColor(new Vector3(1.0f - borderSize, 1.0f -borderSize, 0.0f), borderInnerColor); borderVerts[6] = new VertexPositionColor(new Vector3(1.0f, -1.0f, 0.0f), borderOuterColor); borderVerts[7] = new VertexPositionColor(new Vector3(1.0f - borderSize, -1.0f + borderSize, 0.0f), borderInnerColor); borderVerts[8] = new VertexPositionColor(new Vector3(-1.0f, -1.0f, 0.0f), borderOuterColor); borderVerts[9] = new VertexPositionColor(new Vector3(-1.0f + borderSize, -1.0f + borderSize, 0.0f), borderInnerColor); // Создание диска с центром в начале координат. Диск визуализируется с использованием веера // треугольников (PrimitiveType.TriangleFan) baseDiskVerts = new VertexPositionColor[diskSlices + 2]; baseDiskVerts[0] = new VertexPositionColor(new Vector3(0.0f, 0.0f, 0.0f), XnaGraphics.Color.White); for (int i = 0; i <= diskSlices; i++) { float angle = (float)i / (float)diskSlices * 2.0f * (float)Math.PI; float x = diskRadius * (float)Math.Sin(angle); float y = diskRadius * (float)Math.Cos(angle); baseDiskVerts[i + 1] = new VertexPositionColor(new Vector3(x, y, 0.0f), diskOuterColor); }; // Создаем массив вершин диска путем клонирования. Таким образом, при старте приложения диск // расположен в начале системы координат. diskVerts = (VertexPositionColor[]) baseDiskVerts.Clone(); // Задаем начальную скорость диска вдоль осей X и Y speedX = RndSpeed(); speedY = RndSpeed(); } // Обработчик события Paint главной формы private void MainForm_Paint(object sender, PaintEventArgs e) { ... // Визуализируем сцену effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); device.DrawUserPrimitives(PrimitiveType.TriangleStrip, borderVerts, 0, borderVerts.Length - 2); device.DrawUserPrimitives(PrimitiveType.TriangleFan, diskVerts, 0, diskVerts.Length - 2); pass.End(); } effect.End(); device.Present(); ... } // Обработчик события Tick таймера, свойству Interval которого присвоено значение 40 private void timer_Tick(object sender, EventArgs e) { // Изменяем координаты центра диска на расстояние, которое он должен пройти за время между // двумя тиками таймера posX += speedX * (float)timer.Interval * 0.001f; posY += speedY * (float)timer.Interval * 0.001f; // Если диск столкнулся с правым краем границы if (posX >= 1 - diskRadius - borderSize) { // Диск не должен перелетать за границу posX = 1 - diskRadius - borderSize; // Изменяем направление движения диска на противоположное и вычисляем новую скорость. speedX = -Math.Sign(speedX) * RndSpeed(); } // Если диск столкнулся с верхним краем границы if (posY >= 1 - diskRadius - borderSize) { posY = 1 - diskRadius - borderSize; speedY = -Math.Sign(speedY) * RndSpeed(); } // Если диск столкнулся с левым краем границы if (posX <= -1 + diskRadius + borderSize) posX = -1 + diskRadius + borderSize; speedX = -Math.Sign(speedX) * RndSpeed(); } // Если диск столкнулся с нижним краем границы if (posY <= -1 + diskRadius + borderSize) { posY = -1 + diskRadius + borderSize; speedY = -Math.Sign(speedY) * RndSpeed(); } // Вычисляем новые координаты диска на основе "эталонного " диска с центром в начале // координат. for (int i = 0; i < baseDiskVerts.Length; i++) { diskVerts[i].Position.X = baseDiskVerts[i].Position.X + posX; diskVerts[i].Position.Y = baseDiskVerts[i].Position.Y + posY; } // Перерисовываем изображение Invalidate(); } }
Расчет координат вершин диска, использующий тригонометрические функции, является весьма ресурсоемкой операцией, поэтому в приложении используется небольшая хитрость. Вместо многократного расчета вершин диска приложение рассчитывает только координаты "эталонного " диска с центром в начале системы координат, которые заносятся в массив baseDiskVerts. После этого для получения вершин заданной окружности необходимо сместить все вершины "эталонной " окружности на расстояние заданной окружности от центра.
Запустите приложение на выполнение. Первое что бросится в глаза – это движение окружности рывками. Но ведь этого не может быть! Фильмы то при частоте 25 fps идут очень плавно. В чем же принципиальная разница между фильмом и нашим приложением? Все очень просто. В кинематографе кадры снимается с некоторой FPS в кинофильме и 25 FPS в нашем приложении – это немного разные FPS. В частности, в компьютерных играх при частоте кадров 25-30 FPS ощущаются некие "подергивания " в динамичных сценах – объекты движутся как бы рывками. Эту проблему решают методом грубой силы, то есть простым увеличением частоты кадров – практический опыт показыва
ет, что в большинстве случаев вполне достаточно частоты кадров 60 FPS.
Дополнительная информация
Для визуализации быстродвижущихся объектов вроде спортивного автомобиля, пролетающего мимо наблюдателя со скоростью 300 км/ч, даже частота кадров 60 FPS оказывается недостаточной. С другой стороны, при попытке наращивания FPS мы упираемся в возможности аппаратуры – максимальная частота вертикальной развертки современных мониторов (то есть частота смены кадров) редко превышает 60-100 Hz. Поэтому для повышения реалистичности движений применяют технологии Motion
Ну что ж, попробуем увеличить частоту смены кадров. Как известно, компонент таймер может тикать до 64-х раз в секунду, т.е. минимальный интервал между двумя тиками равен 0.015 миллисекунд. Присвойте свойству Interval таймера значение 15 и снова запустите приложение. Шарик будет двигаться ощутимо плавнее, однако скорость не будет постоянной: периодически шарик будет ни с того то замедляться, то ускоряться.
Почему это происходит? Для ответа на этот вопрос необходимо внимательно прочитать описания таймера Windows в MSDN. Хотя таймер и может тикать с интервалом 15 миллисекунд, точность каждого тика составляет 55 миллисекунд. Таким образом, по мере уменьшения интервала между тиками, точность таймера катастрофически падает, что приводит к неравномерности движения объектов. Но есть еще один немаловажный фактор – события от таймера имеют самый низкий приоритет среди всех сообщений. Например, если пользователь в это время нажимает клавишу клавиатуры, а в очереди сообщений находится необработанное сообщение WM_TIMER, то сообщения WM_KEYDOWN, WM_CHAR и WM_KEYUP будут вставлены в очередь перед сообщением WM_TIMER. По сути, сообщение WM_TIMER обрабатывается только при условии пустой очереди событий потока, а так как в очереди может находиться не более одного сообщения wmtimer, таймер может "терять " тики. Чтобы убедиться в этом, попробуйте ухватиться указателем мыши за край формы и поизменять ее размер, что тут же приведет к потере сообщений и, соответственно, замедлению шарика.
Эти недостатки компонента Timer затрудняют его применение в реальных игровых приложениях. Впрочем, данный компонент предназначен для реализации задач, не критичных к точности
Итак, использование обычного компонента Timer не увенчалось успехом из-за ряда ограничений Windows. Кроме того, использование таймера для визуализации сцены обладает еще одним фундаментальным ограничением. Дело в том, что, задавая определенную частоту визуализации кадров, мы делаем неявное предположение, что абсолютно любой компьютер может визуализировать сцену с требуемой частотой кадров. Если это вдруг окажется не так, приложение будет работать в "замедленном " режиме. Учитывая, что производительность видеоподсистем различных компьютеров может отличаться в десятки раз, возникновение подобной проблемы более чем вероятно.
Что же делать? Раз визуализация сцены с фиксированной частотой кадров чревата возникновением множества проблем, надо просто визуализировать кадры с максимально возможной частотой, для чего достаточно поместить в обработчик события Idle вызов метода Invalidate. Для измерения временных интервалов между вызовами обработчика события Idle можно воспользоваться свойством System.Enviroment.TickCount, возвращающим количество миллисекунд, прошедших с момента загрузки операционной системы. Основные фрагменты нового варианта кода приведены в листинге 3.19.
// Пример Examples\Ch03\Ex08
public partial class MainForm : Form
{
// Значение свойства System.Enviroment.TickCount
во время последнего вызова обработчика
// события Idle.
int lastTick;
private void MainForm_Load(object sender, EventArgs e)
{ ...
// Запоминаем текущее значение свойства System.Enviroment.TickCount
lastTick = Environment.TickCount;
Application.Idle += new EventHandler(Application_Idle);
}
void Application_Idle(object sender, EventArgs e)
{
// Если приложение завершает работу, закрываем
главную форму. if (closing)
{
Close(); return; }
int currentTick = Environment.TickCount;
// Вычисляем время (в секундах), прошедшее между
двумя вызовами обработчика Idle float
delta = (float)(currentTick - lastTick) * 0.001f;
// Изменяем положение диска
posX += speedX * delta; posY += speedY * delta;
// Обрабатываем столкновение с стеной вдоль границы экрана
...
// Запоминаем текущее время с момента загрузки операционной системы
lastTick = currentTick;
// Перерисовываем экран
Invalidate();
}
...
}
Шарик примера Ch03\Ex08 движется ощутимо плавнее, однако небольшие, еле заметные рывки все же остались. Для определения причины рывков вставьте в обработчик события Idle команду Trace.WriteLine(delta), запустите приложение, вернитесь в Visual Studio и посмотрите содержимое окна Output. Скорее всего, его содержимое будет выглядеть примерно следующим образом:
delta = 0,078 delta = 0 delta = 0 delta = 0,016 delta = 0,015 delta = 0 delta = 0,016 delta = 0,015 delta = 0,016 delta = 0 delta = 0,016
Обратите внимание на множество нулевых значений параметра delta, соответствующих полной остановке шарика на месте, которая воспринимается пользователем как небольшое подергивание. Почему между некоторыми вызовами события Idle проходит 15-16 миллисекунд, а между другими – меньше одной миллисекунды. Подобный разброс производительности не выглядит правдоподобным, значит дело в чем-то ином. Для выяснения причин этой аномалии придется обратиться к MSDN. Метод Environment.TickCount использует функцию Win32 GeTickCount, точность которой порядка 10 миллисекунд. Соответственно, при попытке измерить временной интервал близкий к 10 миллисекундам метод Environment.TickCount может вернуть два одинаковых значения, что мы и наблюдаем.
Так как точность измерения времени посредством свойства Environment.TickCount зачастую оказывается недостаточной, Microsoft по многочисленным просьбам трудящихся добавил в .NET Framework 2.0 класс System.Diagnostics.Stopwatch, предназначенный для высокоточного измерения временных интервалов. Данный класс весьма прост в использовании. Сначала приложение должно создать экземпляр класса Stopwatch и запустить таймер методом Start. Затем по мере необходимости приложение посредством свойства ElapsedMilliseconds получает количество миллисекунд, прошедшее с момента запуска таймера. Когда же все измерения времени выполнены, приложение останавливает таймер командой Stop.
Для оценки точности таймера Stopwatch можно воспользоваться статическим свойством Frequency, возвращающее количество тиков высокоточного таймера, используемого классом Stopwatch для измерения временных интервалов, за 1 секунду. На моем компьютере свойство Frequency равно 1.870.000.0000, что соответствует фантастической точности $$1/frac18700000000 = 5?10^{-10}сек$$.
Так как метод ElapsedMilliseconds по определению не может изменять интервалы меньше 1 миллисекунды, в классе Stopwatch имеется один метод ElapsedTicks, возвращающий количество тиков таймера. Для перевода тиков в секунды значение свойства ElapsedTicks надо поделить на Stopwatch.Frequency. При этом следует всегда помнить о том, что при измерении сверхкоротких интервалов появляются иные погрешности, вызванные переключением задач, кэш промахами, Stopwatch редко достижима на практике.
Код приложения, переписанный с использованием таймера Stopwatch приведен в листинге 3.20.
// Пример Examples\Ch03\Ex09
public partial class MainForm : Form
{
// Высокоточный таймер
Stopwatch stopwatch;
// Время, прошедшее с момента запуска таймера при
последнем вызове обработчика события
// Idle
long lastTime;
private void MainForm_Load(object sender, EventArgs e)
{
...
// Запускаем таймер
stopwatch = new Stopwatch();
stopwatch.Start();
lastTime = 0;
// Выводим в окно Output точность таймера
Trace.WriteLine("Accuracy = " + 1.0 / Stopwatch.Frequency + " sec");
Application.Idle += new EventHandler(Application_Idle);
}
void Application_Idle(object sender, EventArgs e)
{
// Если приложение завершает работу, закрываем главную форму. If
(closing)
{
Close(); return; }
// Получаем количество миллисекунд, прошедших с
момента запуска таймера
double currentTime = (double)stopwatch.ElapsedTicks /
(double)Stopwatch.Frequency;
// Вычисляем время (в секундах), прошедшее между двумя
вызовами обработчика события Idle
` float delta = (float)(currentTick - lastTick);
// Изменяем положение диска
posX += speedX * delta; posy
+= speedY * delta;
// Обрабатываем столкновение с краями экрана
...
// Запоминаем текущее показание таймера
lastTime = currentTime;
// Перерисовываем экран
Invalidate();
}
...
}
Запустив приложение, вы убедитесь, что в окне Output пропали нулевые значения. В полноэкранном режиме содержимое окна Output скорее всего будет выгладить примерно следующим образом:
Accuracy = 5,3475935828877E-10 sec delta = 0,035 delta = 0,006 delta = 0,003 delta = 0,002 delta = 0,011 delta = 0,012 delta = 0,012 delta = 0,012 delta = 0,011 delta = 0,013 delta = 0,011
Примечание
Обратите внимание на ощутимую задержку при визуализации первого кадра, обусловленную накладными расходами JIT -компилятора при компиляции метода Paint.
В результате шарик наконец-то стал двигаться по-настоящему плавно. Однако в оконном режиме рывки по-прежнему остались, при этом время между визуализацией соседних кадров скачкообразно меняется почти в два раза:
Accuracy = 5,3475935828877E-10 sec delta = 0,035 delta = 0,022 delta = 0,013 delta = 0,032 delta = 0,015 delta = 0,032 delta = 0,014 delta = 0,032 delta = 0,016 delta = 0,032 delta = 0,014 delta = 0,016 delta = 0,019
Все дело в вертикальной синхронизации, которая будет рассмотрена в следующем разделе.
Как известно, CRT -мониторы формируют изображение посредством пучка электронов, который построчно пробегает по поверхности экрана сверху вниз, заставляя ее светиться. Поскольку свечение экрана быстро тускнеет, этот процесс повторяется снова и снова с частотой вертикальной развертки текущего видеорежима режима.
Теперь давайте представим, что произойдет, если изображение изменится в процессе обратного хода луча. И так, часть верхняя изображения уже отображена на экране, но прервать процесс формирования изображения не возможно, поэтому электронный луч продолжит сканировать поверхность экрана, формируя новое изображение. В результате на экране кратковременно будут находиться новое и старое изображение, что воспринимается пользователем как странный артефакт.
Для борьбы с этим недоразумением XNA Framework предоставляет программисту возможность управления синхронизацией переключения заднего и экранного буферов с моментом окончания формирования электронным лучом изображения на экране монитора. Эта функциональность доступна посредством свойства PresentationInterval класса PresentationParameters:
public PresentInterval PresentationInterval { get; set; }
В подавляющем большинстве случаев свойству PresentationInterval присваивают одно из следующих трех значений перечислимого типа PresentInterval:
PresentInterval.Default - значение по умолчанию, аналогичное PresentInterval.One.PresentInterval.One - буферы переключаются только по окончанию обновления электронным лучом изображения на экране монитора.PresentInterval.Immediate - кадровые буферы переключаются так быстро, насколько это возможно.Хотя значения PresentInterval.Default и PresentInterval.One формально являются братьями-близнецами, между ними есть одна очень тонкая разница. При использовании PresentInterval.Default синхронизации с обратным ходом луча осуществляется посредством обычного таймера Windows, который, как вы помните, обладает очень низкой точностью. Из-за особенностей организации оконной подсистемы Windows низкая точность таймера приводит к частым задержкам при переключении буферов и, соответственно, "рывкам " при анимации. В полноэкранном режиме последствия, как правило, не столь серьезны.
В отличии от PresentInterval.Default, значение PresentInterval.One использует высокоточный таймер, что позволяет избавиться от рывков. Тем не менее, в некоторых случаях незначительные рывки все же могут остаться: так как видеокарта перед переключением кадровых буферов ожидает завершение визуализации текущего кадра, нетрудно догадаться, что при самом не благоприятном стечении обстоятельств частота смены кадров может оказаться в два раза меньше частоты вертикальной развертки монитора. Да и драйверы видеокарты зачастую оказываются не такими совершенными, как хотелось бы. При возникновении подобных проблем пользователь может попробовать отключить вертикальную синхронизацию, так как резкие скачки частоты смены кадров раздражают гораздо сильнее артефактов из-за отсутствия вертикальной синхронизации.
Итак, однозначно правильного режима смены кадров не существует, поэтому было бы разумным предоставить этот выбор пользователю. Для этой цели я поместил на форму диалогового окна Параметры дополнительный флажок Вертикальная синхронизация ( vsynchCheckBox ), установка которого соответствует использованию режима PresentInterval.One, а снятие - PresentInterval.Immediate (рисунок 3.12). Для сохранения значения режима вертикальной синхронизации в настройки приложения было добавлено дополнительное поле PresentationInterval типа Mcrosoft.Xna.Framework.Graphics.PresentInterval, а сам код приложения подвергся косметической доработке (листинг 3.21). Готовое приложение находится в example.zip
в каталоге Examples\Ch03\Ex10.

(рис 3.21) Диалоговое окно Параметры с новым флажком Вертикальная синхронизация (рис 3.12) // Класс главной формы приложения public partial class MainForm : Form { void InitGraphivsDevice() { ... presentParams = new PresentationParameters(); presentParams.BackBufferCount = 1; presentParams.SwapEffect = SwapEffect.Discard; presentParams.IsFullScreen = settings.FullScreen; // Устанавливаем режим вертикальной синхронизации согласно настройкам приложения presentParams.PresentationInterval = settings.PresentationInterval; ... } ... } // Класс диалогового окна "Параметры " public partial class SettingsForm : Form { // Конструктор диалогового окна internal SettingsForm(Properties.Settings settings, GraphicsAdapter adapter) { ... // Устанавливает значение флажка "Вертикальная синхронизация " согласно настройкам приложения if (settings.PresentationInterval == PresentInterval.One) vsynchCheckBox.Checked = true; else vsynchCheckBox.Checked = false; } // Обработчик нажатия кнопки Ok private void okButton_Click(object sender, EventArgs e) { ... // Сохраняем в настройках приложения информацию о режиме кадровой синхронизации if (vsynchCheckBox.Checked) settings.PresentationInterval = PresentInterval.One; else settings.PresentationInterval = PresentInterval.Immediate; ... } }
Запустите приложение и попробуйте поэкспериментировать с настройками кадровой синхронизации, наблюдая за окном Output. При включенной вертикальной синхронизации интервал между визуализацией кадров будет примерно равен константе 1.0/{частота обновления экрана текущего видеорежима}. При отключении вертикальной синхронизации частота интервал смены кадров уменьшится до величины сопоставимой с 1 мс, что соответствует частоте порядка 1000 fps. При этом и в том и в другом случае движения шарика будут плавными.
При визуализации диска проходы ( Passes ) эффекта перебираются посредством цикла foreach.
Достоинствами цикла foreach является простота кода и защита от потенциальных ошибок вроде использования неправильного индекса при обращении к элементу коллекции. Обратной стороной является неявное использование циклом foreach специального типа, реализующего интерфейс Enumerator. Например, код
List<int> numbers = new List<int>(3); ... int s = 0; foreach (int v in numbers) s += v;
неявно заменяется компилятором следующим аналогом:
List<int> numbers = new List<int>(3); ... int s = 0; // Создается объект enumerator IEnumerator enumerator = numbers.GetEnumerator(); // Перебирает элементы коллекции while (enumerator.MoveNext()) // Получает доступ к текущему элементу коллекции s += (int)enumerator.Current;
На первый взгляд, данный код является неэффективным. Но в действительности все не так уж и плохо. Во-первых, метод GetEnumerator возвращает структуру:
public class List<T> : IList<T>,
ICollection<T>, IEnumerable<T>, IList,
ICollection,
IEnumerable
{
public Enumerator<T>
GetEnumerator() {
return new Enumerator<T>((List<T>)
this); }
...
public struct Enumerator : IEnumerator<T>, IDisposable,
IEnumerator
{
private List<T> list;
private int index;
private int version;
private T current;
internal Enumerator(List<T> list);
public void Dispose();
public bool MoveNext();
public T Current { get; }
object IEnumerator.Current { get; }
void IEnumerator.Reset(); }
}
Соответственно, вызов метода GetEnumerator не приводит к выделению памяти в управляемой куче и учащению вызовов сборщика мусора. Во-вторых, компилятор C# вызывает метод MoveNext и свойство Current структуры Enumerator напрямую без приведения к ссылке на интерфейс IEnumerator, так что боксирование ( boxing ) структуры Enumerator не происходит. И, наконец, компилятор может встроить ( inline ) код метода MoveNext и свойства Current непосредственно в код цикла, избежав накладных расходов на вызовы методов. Так что при использовании массивов или коллекций наподобие List разница в производительности циклов foreach и for будет исключающее мала, и в подавляющем большинстве случаев на нее можно не обращать внимания.
Однако при использовании других коллекций не все так просто. Например, у некоторых объектов метод GetEnumerator возвращает объект, размещаемых в управляемой куче. Подобный подход имеет два недостатка:
foreach для перебора элементов такой коллекции приведет к созданию множества объектов enumerator, и, соответственно, частому вызову сборщика мусора, что привет к падению производительности.MoveNext является виртуальным, то компилятор не сможет встроить его непосредственно в код цикла, что тоже негативно скажется на производительности.Таким образом, в критичных к производительности приложениях при переборе элементов коллекций, возвращающих объект enumerator, имеет смысл избегать циклов foreach.
Но давайте вернемся к коллекции Passes класса Effect. Данная коллекция реализуется классом EffectPassCollection, объявленным следующим образом:
public sealed class EffectPassCollection :
IEnumerable<EffectPass>
{
// Список для хранения информации о проходах
private List<EffectPass> pPass;
// Возвращает объект (полученный путем боксирования
структуры), реализующий интерфейс
// IEnumerator<EffectPass>
public IEnumerator<EffectPass> GetEnumerator()
{
return (IEnumerator<EffectPass>) this.pPass.GetEnumerator();
}
}
Давайте внимательно рассмотрим этот код. Класс EffectPassCollection в действительности хранит информацию о проходах в коллекции List. Соответственно метод this.pPass. GetEnumerator возвращает ссылку на структуру Enumerator класса List, определение которой было приведено в начале раздела.
И все бы было просто замечательно, если бы не один маленький нюанс - структура IEnumerator приводится к интерфейсу IEnumerator<EffectPass>, что приводит к боксированию и выделению памяти в управляемой куче. Чтобы оценить влияние этой особенности на производительность приложения, мы встроим в обработчик события Paint код, перебирающий элементы коллекции 10.000.000 раз посредством циклов for и foreach с измерением времени их выполнения (листинг 3.22).
// Пример Examples\Ch03\Ex11 #define TEST
#if TEST
bool testing = true;
#endif
private void MainForm_Paint(object sender, PaintEventArgs e)
{
// Измерение выполняется только при условии определения
идентификатора TEST
#if TEST
// Тест выполняется только один раз
if (testing)
{
// Число итераций
const int n = 10000000;
int sum;
// Выводим количество сборок мусора, выполненных на момент
начала эксперимента
Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
Stopwatch timer = new Stopwatch();
// Запускаем таймер
timer.Start();
// Перебираем элементы коллекции passes и суммируем коды первой буквы
sum = 0;
EffectPassCollection passes = effect.CurrentTechnique.Passes;
// Выполняем n итераций
for (int i = 0; i < n; i++)
// Перебираем элементы коллекции passes посредством
for и суммируем коды первой буквы for
(int j = 0; j < passes.Count; j++) sum += (int)passes[j].Name[0];
// Вычисляем суммарное время, затраченное на интеграции
double time1 = (double)timer.ElapsedTicks / (double)Stopwatch.Frequency;
// Отображаем отчет
Trace.WriteLine(" for ");
Trace.WriteLine("Sum : " + sum.ToString());
Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
Trace.WriteLine("Time1 : " + time1);
Trace.WriteLine("");
// Перезапускаем таймер
timer.Reset(); timer.Start();
sum = 0;
// Снова выполняем итерации, но уже посредством цикла foreach for
(int i = 0; i < n; i++)
foreach (EffectPass pass in effect.CurrentTechnique.Passes) sum
+= (int)pass.Name[0];
// Вычисляем суммарное время, потраченное на итерации
double time2 = (double)timer.ElapsedTicks /
(double)Stopwatch.Frequency;
// Отображаем отчет
Trace.WriteLine(" foreach ");
Trace.WriteLine("Sum : " + sum.ToString());
Trace.WriteLine("Gen 0 collection count: " + GC.CollectionCount(0));
Trace.WriteLine("Time2 : " + time2);
// Определяем разницу в
производительности
Trace.WriteLine("Time2 / Time1 = " + time2 / time1);
Trace.WriteLine("");
testing = false; }
#endif
}
Запустив модифицированное приложение на своем компьютере с процессором Intel Pentium-4 2.8C я получил следующие результаты:
Accuracy = 3,5730747380043E-10 sec Gen 0 collection count: 3 for Sum : 1120000000 Gen 0 collection count: 3 Time1 : 0,554915015846586 foreach Sum : 1120000000 Gen 0 collection count: 915 Time2 : 1,84470303389776 Time2 / Time1 = 3,32429828211344
Как видно, в процессе работы цикла foreach сборщик мусора был вызван 915 Intel Core2 Due E6300 при выполнении циклов foreach сборщик мусора был вызван 229 раз. В целом же, частота вызовов сборщика мусора в первую очередь зависит от размера кэша второго уровня CPU. Например, размер кэша процессора Intel Core2 Due E6300 (2MB) в четыре раза больше, чем у Pentium-4 2.8C (512KB). Соответственно, сборщик мусора на Intel Core2 Due E6300 вызывается в 4 раза реже по сравнению с Pentium-4 2.8C (915/229 = 3.996).for и foreach достигла трехкратной величины. Таким образом, перебор элементов коллекции foreach в методе Paint, вызываемом около сотни раз в секунду, является не самой лучшей идеей. Особенно если учесть, что реальные приложения зачастую содержат десятки
эффектов, а внезапная сборка мусора при визуализации кадра может привести заметному провалу производительности.
Примечание
На самом деле Microsoft здорово поработала над эффективностью сборщика мусора, и сейчас сборка мусора в поколении 0 обычно занимает порядка 1 мс. В частности, пренебрегая накладными затратами цикла foreach, можно прикинуть, что в нашем эксперименте каждая сборка мусора выполнялась в среднем не более чем за (1.84 - 0.55) / 915 = 0.0014 секунд. Тем не менее, рано или поздно частые сборки мусора могут спровоцировать сбор мусора в старших поколениях, который будет воспринят пользователем как внезапное "подтормаживание " приложения.
Итак, после всех наших трудов движения диска стали по-настоящему плавными. Тем не менее, наше приложение все еще далеко от совершенства. Давайте попробуем представить, что произойдет, если приложение вдруг внезапно приостановит свою работу на несколько секунд, например, из-за повысившейся активности работы с файлом подкачки, плохо читаемого сектора на диске или какой-либо проблемы в драйвере устройства. После возобновления выполнения приложения оно экстраполирует прямолинейное движение диска на несколько секунд вперед и обнаружит, что он уже далеко вылетел за пределы ограничивающей стены. После чего шарик будет автоматически возращен обратно в один из углов прямоугольной границы. Разумеется, траектория движения диска при этом окажется нарушенной, ведь за это время диск должен был уже несколько раз отскочить от стены. Подобный эффект в меньшей степени проявляется и при незначительных провалах производительности, что в конечном счете, приводит несколько различному поведению приложения на разных компьютерах. Вообще данная особенность не является большим недостатком для нашего приложения, в конце концов, диск ведь движется по случайной траектории. Однако в реальных игровых приложениях такая реакция на внезапные провалы производительности неминуемо приведет к разнообразным "глюкам " игровой логики (проход сквозь препятствия и т.п.). Причем чем ниже производительность компьютера, тем вероятнее возникновение проблем.
В принципе для ликвидации данного недостатка можно было бы найти аналитическое выражение зависимости координат центра диска от времени. Однако такое решение будет пригодно лишь для самых простых случаев. Поэтому мы реализуем более универсальный подход: моделирование движений диска с некоторым фиксированным шагом, например, 0.005 секунды. Реализация данной технологии потребует лишь косметической правки обработчика события Idle (листинг 3.23).
// Дискретный шаг времени (в секундах), с которым выполняются расчеты
const float timeStep = 0.005f;
// Максимальный временной интервал между двумя
вызовами обработчика события Idle (в секундах)
const float maxDelta = 5.0f;
void Application_Idle(object sender, EventArgs e)
{
...
// Определяем текущее время
double currentTime = (double)stopwatch.ElapsedTicks /
(double)Stopwatch.Frequency;
// Если время между двумя вызовами обработчика Idle
превышает maxDelta, корректируем lastTime
// во избежание слишком длинной работы обработчика события
Idle if (currentTime - lastTime > maxDelta)
lastTime = currentTime - maxDelta;
// Моделируем движения шарика дискретным шагом времени timeStep
while (lastTime + timeStep < currentTime)
{
posX += speedX * timeStep;
posY += speedY * timeStep;
lastTime += timeStep; }
Invalidate();
}
В самых запущенных случаях задержка между визуализацией кадров может достигать минуты и даже больше. Так как моделирование игровой логики интервала времни в несколько минут может занять достаточно существенно время, в приложении используется простой прием - игровая логика рассчитывается не более чем за 5 секунд игрового времени (константа maxDelta ). Для пользователя подобный трюк является незаметным, ведь он все равно не сможет спрогнозировать поведение приложения на значительный временной интервал.
Примечание
В интерактивных приложениях вроде автосимуляторов при возникновении длительной задержки между кадрами имеет смысл приостановить игровой процесс, ведь в течение задержки в несколько секунд автомобиль игрока наверняка слетит с трассы или столкнется с препятствием. Эффекта автоматической приостановки игрового процесса можно достичь путем присвоения константе maxDelta небольшого значения порядка 0.2 - 0.5 секунд.
В компьютерной графике довольно часто приходится моделировать различные полупрозрачные объекты вроде стекла, воды, тумана и т.п. Полупрозрачность - это характеристика объекта, показывающая, какую часть света пропускает среда (объект) без изменения направления его распространения. Так полностью прозрачный объект пропускает через себя весь свет, в результате чего не оказывает никакого влияния на находящиеся позади него объекты (иными словами, он является невидимкой). Непрозрачный объект напротив не пропускает через себя свет, и соответственно полностью перекрывает находящиеся позади него объекты. К слову, все наши объекты до сих пор являлись полностью не прозрачными.
Коэффициент поглощения ? определенной среды есть количественная мера, позволяющая оценить, какая доля светового, падающего на поверхность раздела сред, проходит дальше, а какая поглощается. Например, если стекло имеет коэффициент поглощения 0.2, то результирующий цвет будет представлять собой объединение 20% цвета стекла и 80% цвета объектов позади стекла. То есть наблюдатель будет видеть как сам объект, так и предметы позади объекта.
В общем случае итоговый цвет полупрозрачного объекта может быть определен по формуле:
$$C=c_s-\alpha+c_d-(1-\alpha) $$где
$$c$$ - результирующий цвет
$$c_s$$ - цвет полупрозрачного объекта
$$c_d$$ - цвет фона позади объекта
$$\alpha$$ - коэффициент поглощения, равный 0 для полностью прозрачных объектов, и 1 для непрозрачных.
Перейдем к моделированию эффекта полупрозрачности в XNA Framework. Как вы помните, в XNA Framework любой объект визуализируется с использованием простых примитивов, которые проходят через ряд стадий графического конвейера. В конечном счете, каждый примитив преобразуется в массив пикселей, которые заносятся в кадровый буфер. По сути, итоговый массив пикселей и есть сам объект, а кадровый буфер - его фон. Когда объект является непрозрачным, то пиксели просто копируются в кадровый буфер, затирая старые значения (собственно этим мы до сих пор и занимались). Если же объект является полупрозрачным, то разумно предположить, что приложение должно скомбинировать цвет пикселей объекта с пикселями кадрового буфера по формуле 3.1.
На первый взгляд операцию смешивания пикселей было бы логичным осуществлять в пиксельном шейдере. Но, к сожалению, это не возможно – пиксельный шейдер не может считывать информацию из кадрового буфера, так как реализация данной функциональности ощутимо бы усложнила бы пиксельные процессоры. Соответственно, в современных процессорах эта функциональность реализуется посредством специализированных блоков
(рис 3.13) Принцип работы блоков ROP Примечание
В общем случае количество блоков не обязательно совпадает с числом пиксельных процессоров. Например, G70 содержит 24 пиксельных процессора и 16 блоков . Подобная асимметрия обусловлена меньшей ресурсоемкостью операций смешения пикселей по сравнению со среднестатистическим пиксельным шейдером, поэтому при равном количестве пиксельных процессоров и часть последних будет в основном простаивать.
Блоки имеют очень ограниченную функциональность и программируются с использованием весьма запутанного синтаксиса. Блок может выполнить над двумя аргументами одну из пяти векторных операций вида:
где
$$\qquard d'$$ - новый цвет пикселя кадрового буфера $$[\qquard d'_r,\qquard d'_g,\qquard d'_b,\qquard d'_a] $$
$$op$$ - операция (см. таблица 3.5)
$$s$$ - цвет пикселя $$[s_r, s_g, s_b, s_a ] $$
$$d$$ - текущий цвет пикселя кадрового буфера $$[d_r,d_g,d_b,d_a]$$
$$b, с$$ - векторные коэффициенты $$[b_r,b_g,b_b,b_a]$$ и $$[c_r,c_g,c_b,c_a] $$
Примечание
Наряду с операциями смешения блоки могут выполнять множество других полезных простых операций над пикселями вроде отсечения невидимых областей объектов при визуализации трехмерных цен, реализовывать полноэкранное сглаживание ( FSAA - Full Scene Anti Aliasing ) и т.д.
Управление смешением пикселей осуществляется посредством свойств свойства RenderState экземпляра класса GraphicsDevice. Активация режима смешения осуществляется путем присвоения значения true свойству AlphaBlendEnable.
public bool AlphaBlendEnable { get; set; }
Операция, выполняемая над цветами графического примитива и кадрового буфера задается путем присвоения соответствующего значения перечислимого типа BlendFunction (таблица 3.5) свойству RenderState.BlendFunction.
public BlendFunction BlendFunction { get; set; }
| Значение | Описание |
|---|---|
Add (значение по умолчанию) |
Покомпонентно складывает два аргумента |
Subtract |
Покомпонентно вычитает из первого аргумента (цвет пикселя) второй (цвет кадрового буфера). |
ReverseSubtract |
Покомпонентно вычитает из второго аргумента (цвет кадрового буфера) первый (цвет пикселя). |
Max |
Покомпонентно сравнивает оба аргумента и возвращает компоненты с наибольшим значением |
Min |
Покомпонентно сравнивает оба аргумента и возвращает компоненты с наименьшим значением |
Каждый из Blen (таблица 3.6). При этом коэффициент $$b$$ задается свойством RenderState.SourceBlend, а коэффициент c – свойством RenderState.DestinationBlend:
public Blend SourceBlend { get; set; } public
Blend DestinationBlend { get; set; }
| Значение | Описание |
|---|---|
BlendFactor |
В качестве коэффициента используется вектор, присвоенный свойству RenderState.BlendFactor |
InverseBlendFactor |
Коэффициент получается путем вычитания из единичного вектора (1, 1, 1, 1) значения свойства RenderState.BlendFactor. |
Zero |
Коэффициент равен вектору (0, 0, 0, 0). |
One |
Коэффициент равен вектору (1, 1, 1, 1). |
SourceColor |
В качестве коэффициента используются цвета примитива $$[s_r , s_g ,s_b, s_a ] $$ |
InverseSourceColor |
В качестве коэффициента используется вектор $$[1-s_r,1-s_g,1-s_b,1-s_a] $$ |
SourceAlpha |
В качестве коэффициента используется вектор альфа каналов примитива $$[s_a ,s_a ,s_a ,s_a ] $$. |
InverseSourceAlpha |
В качестве коэффициента используется вектор $$[1-s_a,1-s_a,1-s_a,1-s_a] $$. |
DestinationColor |
В качестве коэффициента используется цвет пикселя кадрового буфера $$[d_r ,d_g ,d_b,d_a ] /$$. |
InverseDestinationColor |
В качестве коэффициента используется вектор [1-d_r,1-d_g,1-d_b,1-d_a]. |
DestinationAlpha |
В качестве коэффициента используется вектор альфа-каналов пикселя кадрового буфера $$[d_a,d_a,d_a,d_a ] $$. |
InverseDestinationAlpha |
В качестве коэффициента используется вектор $$[1-d_a,1-d_a ,1-d_a,1-d_a] $$. |
SourceAlphaSaturation |
Вектор коэффициентов равен [f,f,f,1], где $$f = min(s_a,d_a) $$. |
Хотя первое время от разнообразия параметров рябит в глазах, в действительности все очень просто. Допустим, нам необходимо скомбинировать цвет пикселей примитива с цветом кадрового буфера с использованием следующего экзотического выражения:
$$\qquard d’_r=(1-s_r)*s_r-s_a*d_r\\ \qquard d’_g=(1-s_g)*s_g-s_a*d_g\\ \qquard d’_b=(1-s_b)*s_b-s_a*d_b $$Значение альфа канала нас не интересует, так как он все равно не учитывается при выводе изображения на экран. Вдобавок, часто используемые форматы пикселей SurfaceFormat.Bgr565 и SurfaceFormat.Bgr32 не содержат альфа канала.
Для начала давайте определим, какой операцией связаны между собой цвет примитива и цвет кадрового буфера. Во всех трех выражениях из произведения с множителями $$s_r ,s_g ,s_b$$ вычитается произведение, содержащее множители $$d_r ,d_g ,d_b,$$ следовательно для реализации этой формулы необходимо использовать операцию вычитания BlendFunction.Subtract.
Перейдем к коэффициентам. Сопоставив коэффициенты перед $$s_r ,s_g ,s_b$$ с таблицей 3.6 мы придем к выводу, что они соответствуют значению Blend.InverseSourceColor. Все коэффициенты перед $$d_r ,d_g ,d_b$$ равны $$s_a,$$ следовательно они могут быть заданы с использованием константы Blend.SourceAlpha.
Таким образом, программирование блоков для смешения цветов по формуле 3.3 реализуется посредством следующих четырех строчек кода:
device.RenderState.AlphaBlendEnable = true; device.RenderState.BlendFunction = BlendFunction.Subtract; device.RenderState.SourceBlend = Blend.InverseSourceColor device.RenderState.DestinationBlend = Blend.SourceAlpha;
Дополнительная информация
Хотя по умолчанию XNA Framework применяет для смешения всех цветовых компонентов общие выражения, он так же позволяет использовать раздельные выражения для смешивания RGB и Alpha компонентов цвета. Эта функциональность активируется посредством присвоения свойству RenderState.SeparateAlphaBlendEnabled значения true, после чего вышеописанные свойства RenderState.BlendFunction, RenderState.SourceBlend и RenderState.DestinationBlend будут влиять исключительно на R, G и B составляющие цвета. Режим смешения альфа-компоненты цвета управляется аналогичной тройкой свойств: RenderState.AlphaBlendOperation, RenderState.AlphaSourceBlend,
RenderState.AlphaDestinationBlend. Так как раздельное смешения каналов применяется достаточно редко, мы пока не будет акцентировать на нем внимание.
Для реализации визуализации полупрозрачных примитивов, мы должны переложить выражение 3.1 на причудливый язык программирования блоков из XNA Framework. Для простоты мы положим, что весь примитив имеет одну и ту же прозрачность, что позволит нам использовать для задания коэффициента непрозрачности параметр RenderState.BlendFactor. Преимуществом такого подхода является возможность изменения прозрачности всех вершин примитива посредством коррекции одного единственного параметра. Само выражение 3.1 содержит операцию сложения и два коэффициента непрозрачности, так что оно легко реализуется средствами XNA Framework:
// Задаем коэффициент непрозрачности (0 – абсолютно прозрачен, 255 – полностью непрозрачен). // При присвоении свойству RenderState.BlendFactor он автоматически приводится к диапазону // 0..1 путем деления на 255. const byte opacity = 50; // Включаем режим смешивания цветов device.RenderState.AlphaBlendEnable = true; // Используем операцию сложения device.RenderState.BlendFunction = BlendFunction.Add; // Коэффициент смешения всех трех компонентов цвета равен opacity (точнее opacity / 255) device.RenderState.BlendFactor = new XnaGraphics.Color (opacity, opacity, opacity, 0); // Коэффициент, на который умножается цвет примитива, равен opacity / 255 device.RenderState.SourceBlend = Blend.BlendFactor; // Коэффициент, на который умножается цвет кадрового буфера, равен 1 - opacity / 255 device.RenderState.DestinationBlend = Blend.InverseBlendFactor;
Для демонстрации использования эффекта полупрозрачности мы строим в решение практического упражнения 3.1 ( Examples\Ch03\Ex02 ) возможность управления прозрачностью круга (рисунок 3.14). Для этого необходимо добавить в группу Параметры ползунок ( TrackBar ) и две метки, свойства которых перечислены в таблице 3.7.
(рис 3.14) Приложение, визуализирующее полупрозрачный круг | Класс | Свойство | Значение |
|---|---|---|
TrackBar |
Name |
opacityTrackBar |
Minimum |
0 | |
Maximum |
255 | |
TickFrequency |
0 | |
Label |
Name |
opacityLabel |
Label |
Text |
Непрозрачность |
Так же мне необходимо реализовать обработчик события Scrol ползунка и немного подправить обработчик события Paint (листинг 3.24). Готовый проект приложения находится в example.zip в каталоге Examples\Ch03\Ex13.
private void opacityTrackBarScroll(object sender, EventArgs e)
{
// Отображаем текущий коэффициент непрозрачности
opacityLabel.Text = ((float)opacityTrackBar.Value / 255.0f).
ToString("0.00");
// Перерисовывает компонент xnaPanel
xnaPanel.Invalidate();
}
private void xnaPanel_Paint(object sender, PaintEventArgs e)
{
...
// Рисуем шахматную доску (код визуализации шахматной доски
взят из пример Ch01\Ex03
// (см. раздел 1.2).
...
device.RenderState.CullMode = CullMode.None;
device.RenderState.PointSize = 3;
device.RenderState.AlphaBlendEnable = true;
device.RenderState.BlendFunction = BlendFunction.Add;
// Значение параметра BlendFactor вычисляется на основе
текущего положения ползунка прокрутки
device.RenderState.BlendFactor = new XnaGraphics.
Color((byte)opacityTrackBar.Value,
(byte)opacityTrackBar.Value, (byte)opacityTrackBar.Value, 0);
device.RenderState.SourceBlend = Blend.BlendFactor;
device.RenderState.DestinationBlend = Blend.InverseBlendFactor;
// Визуализируем круг
...
}
В следующем примере мы создадим приложение, анимирующее построение фигуры Лиссажу, заданной выражением
$$x = \sin(2-a) y =\cos(3-a) $$где
x, y - координаты текущей точкиа - угол, пробегающий с определенным шагом значения от 0 до 360 градусов (0…2·?) Вначале все пиксели фигуры будут прозрачными. Затем мы начнем постепенно увеличивать непрозрачность вершин фигуры, при этом непрозрачность всех вершин будет расти неравномерно: первыми непрозрачными станут вершины, соответствующие углу а равному 0, а последними - а равному 360 градусов. Соответственно, фигура Лиссажу будет как бы рисоваться как бы невидимым пером (рисунок 3.15).
Так как на этот раз вершины примитива имеют различную прозрачность, задание коэффициента непрозрачности посредством свойства RenderState.BlendFactor вряд ли будет разумным решением. Поэтому мы будем хранить информацию о непрозрачности в альфа-канале цвета вершины, задавая режим смешения пикселей посредством следующего кода:
device. RenderState.AlphaBlendEnable = true; device.RenderState.BlendFunction = BlendFunction.Add; device.RenderState.SourceBlend = Blend.SourceAlpha; device.RenderState.DestinationBlend = Blend.InverseSourceAlpha;
Чтобы облегчить задачу мы воспользуемся в качестве отправной точки решением практического
упражнения 2.5. Все что от нас требуется - переписать обработчик события Idle и немного подправить
обработчики событий Load и Paint формы (листинг 3.25).

(рис 3.25) Построение фигуры Лиссажу (рис 3.15) // Пример Examples\Ch03\Ex14 public partial class MainForm : Form { // Количество сегментов в кривой const int QuadStrips = 200; // Количество треугольников в кривой * 2; const int TriStrips = QuadStrips ... // Массив вершин фигуры Лиссажу VertexPositionColor[] vertices = null; // Таймер Stopwatch stopwatch = null; ... private void MainForm_Load(object sender, EventArgse) { // Создаем массив вершин объекта vertices = new VertexPositionColor[TriStrips + 2]; // Перебираем все вершины фигуры Лиссажу for (int i = 0; i <= QuadStrips; i++) { // Вычисляем текущий угол float angle = 2.0f * (float)Math.PI * (float)i // Рассчитываем координаты текущей точки фигуры Лиссажу float cos = (float)Math.Cos(angle); float x = 0.85f * (float)Math.Sin(2 * angle); / (float)QuadStrips; float y = 0.85f * (float)Math.Cos(3 * angle); // Вычисляем цвет точки byte green = (byte)(Math.Pow(0.5f + cos * 0.5f, 0.3f) * 255.0f); byte red = (byte)(Math.Pow(0.5f - cos * 0.5f, 0.3f) * 255.0f); /// Рассчитываем вектор, перпендикулярный графику фигуры Лиссажу длиной 0.015 float sx = -2.55f * (float)Math.Sin(3 * angle); float sy =- 1.7f *(float)Math.Cos(2 * angle); float length = (float)Math.Sqrt(sx * sx + sy * sy); float nx = sx / length * 0.015f; float ny = sy / length * 0.015f; // Заносим в массив информацию о двух вершинах фигуры Лиссажу, смещенных на 0.015 в // направление, перпендикулярном фигуре. Таким образом, кривая фигуры Лиссажу будет иметь // ширину 0.03 единицы. vertices[i * 2] = new VertexPositionColor(new Vector3(x - nx, y - ny, 0.0f), new XnaGraphics.Color(red, green, 0)); vertices [i * 2 + 1] = new VertexPositionColor(new Vector3(x + nx, y + ny, 0.0f), new XnaGraphics. Color(red, green, 0)); }; ... // Запускаем таймер stopwatch = new Stopwatch(); stopwatch.Start(); // Задаем обработчик события Idle, выполняющий коррекцию прозрачности вершин Application.Idle+=new EventHandler(Application_Idle); // Рассчитываем текущие коэффициенты непрозрачности вершин Application_Idle(this, null); } // Рассчитывает текущие коэффициенты непрозрачности вершин и перерисовывает изображение void Application_Idle(object sender, EventArgs e) { if (closing) Close(); // Получаем текущее время float currentTime = (float)stopwatch.ElapsedTicks / (float)Stopwatch.Frequency; // Перебираем все вершины фигуры Лиссажу for (int i = 0; i <= QuadStrips; i++) { // Вычисляем коэффициент непрозрачности для текущей пары вершин byte opacity = (byte)Math.Max(Math.Min((currentTime - 15.0f * (float)i / (float)QuadStrips) * 255.0f, 255f), 0.0f); // Изменяем коэффициент непрозрачности вершин. К сожалению, структура Color не позволяет // изменять отдельные компоненты цвета, поэтому приходится создавать новую структуру и // указывать значения всех цветовых компонентов. vertices[i * 2].Color = new XnaGraphics.Color(vertices[i * 2].Color.R, vertices[i * 2].Color.G, vertices[i * 2].Color.B, opacity); vertices[i * 2 + 1].Color = new XnaGraphics.Color (vertices[i * 2 + 1].Color.R, vertices[i * 2 + 1].Color.G, vertices[i * 2 + 1].Color.B, opacity); }; // Если фигура Лиссажу полностью визуализирована (она визуализируется ровно 16 секунд) if (currentTime > 16.0f) { // Прекращаем анимацию, дабы не загружать центральный процессор и видеокарту бесполезной // работой. Application.Idle -= new EventHandler(Application_Idle); // Выключаем таймер stopwatch.Stop(); } // Перерисовываем форму Invalidate(); } private void MainForm_Paint(object sender, PaintEventArgs e) { ... device.RenderState.CullMode = CullMode.None; device.RenderState.FillMode = fillMode; // Задаем режим смешения пикселей device.RenderState.AlphaBlendEnable = true; device.RenderState.BlendFunction = BlendFunction.Add; device.RenderState.SourceBlend = Blend.SourceAlpha; device.RenderState.DestinationBlend = Blend.InverseSourceAlpha; device.VertexDeclaration = decl; // Визуализируем фигуру Лиссажу effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); device.DrawUserPrimitives(PrimitiveType.TriangleStrip, vertices, 0, vertices.Length - 2); pass.End(); } effect.End(); device.Present(); } }
В этой лекции мы научились осуществлять визуализацию как на поверхность компонентов Windows Forms, так и на целый экран с выбором требуемого видеорежима, и даже на поверхность элементов Win32. Так же мы научились реализовать плавную анимацию и моделировать полупрозрачность посредством смешивания с учетом альфа канала цвета
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.