Опубликован: 28.04.2009 | Доступ: свободный | Студентов: 1840 / 107 | Оценка: 4.36 / 4.40 | Длительность: 16:40:00
Специальности: Программист
Лекция 2:

Визуализация примитивов

2.4.2. Управление размером точек

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

public RenderState RenderState {get; }

Это свойство возвращает экземпляр класса RenderState, содержащий множество свойств, влияющих на процесс визуализации. В частности, свойство RenderState.PointSize отвечает за размер точек:

// По умолчанию значение этого свойства равно 1.0f
float PointSize { get; set; }

Так, присвоив свойству PointSize значение 10, мы увеличите размер визуализируемых точек до 10x10 пикселей (рисунок 2.9):

// Фрагмент обработчика события Paint формы device.RenderState.PointSize = 10.0f;
 Точка размером 10x10 пикселей

Рис. 2.9. Точка размером 10x10 пикселей

Однако мы не можем просто так взять и присвоить свойству GraphicsDevice.RenderState.PointSize произвольное значение. Ведь никто не может гарантировать, что ваши программы будут запускаться исключительно на тех видеокартах, которые умеют работать с большими точками размером 10x10. Следовательно, необходимо предусмотреть поведение приложения в ситуации, когда видеокарта не удовлетворяет минимальным требованиям к размеру точек: наиболее логичное действие приложения в подобной ситуации – выдача соответствующего сообщение об ошибке с последующим завершением работы.

В разделе 2.4.1 упоминалось, что в XNA Framework имеется класс GraphicsDeviceCapabilities с информацией о возможностях графического устройства. В частности, свойство PointSize содержит максимальный размер точки в пикселях, поддерживаемый указанным графическим устройством:

public float MaxPointSize { get; }

Дополнительная информация

Для быстрого получения информации о возможностях текущей видеокарты я обычно пользуюсь тестовым пакетом D3D RightMark, инсталлятор которого находится в example.zip в каталоге RightMark D3D. Достаточно запустить D3D RightMark, щелкнуть левой кнопкой мыши на узле D3D RightMark | Direct3D 9.0 Information (вкладка Available Tests ) и в правой части экрана появится древовидный список возможностей видеокарты. В частности на рисунке 2.10 видно, что видеокарта ATI Radeon 9800 XT может визуализировать точки размером не более 256x256 пикселей. К сожалению D3D RightMark имеет одну нехорошую особенность – он всегда загружает процессор на 100%. Не забывайте закрывать D3D RightMark, когда он вам больше не нужен; в противном случае вы рискуете столкнуться с резким падением производительности других приложений.

 Тестовый пакет D3D RightMark

увеличить изображение
Рис. 2.10. Тестовый пакет D3D RightMark

Думаю, вам не составит труда написать код, проверяющий аппаратную поддержку видеокартой точек размером 10x10 пикселей (Ex02). Для этого достаточно вставить в обработчик события Load после создания графического устройства командой new GraphicsDevice следующий код:

// Если устройство не поддерживает точки размером 10x10 пикселей
if (device.GraphicsDeviceCapabilities.MaxPointSize < 10)
{
// Выводим сообщение об ошибке
MessageBox.Show("Устройство не поддерживает точки размером 10
 пикселей",
 "Критическая ошибка", MessageBoxButtons.OK, 
 MessageBoxIcon.Error); 
// Устанавливаем флаг завершения работы приложения
closing = true; 
// Задаем обработчик события Idle, выполняющий закрытие формы 
(вызов метода Close внутри 
// обработчика Load может привести к утечке ресурсов)
Application.Idle += new EventHandler(ApplicationIdle); 
// Выходим из обработчика события Load
return; }

В таблице 2.6 приведены значения свойства MaxPointSize для некоторых графических процессоров с аппаратной поддержкой пиксельных шейдеров. Обратите внимание, что все они поддерживают точки размером не менее 64-х пикселей. Следовательно, так как XNA Framework требует от видеокарты обязательной поддержки пиксельных шейдеров, приложению, использующему XNA Framework вовсе не обязательно проверять поддержку пикселей размером менее 64-х пикселей. Это обстоятельство позволит нам несколько сократить код некоторых примеров без ущерба надежности.

Таблица 2.6. Максимальные размеры точек для некоторых GPU
GPU Максимальный размер точки (в пикселях)
NV20 (NVIDIA GeForce3) 64
NV25 (NVIDIA GeForce4) 8192
NV3x (NVIDIA GeForce FX) 8192
R2xx – R5xx (ATI Radeon) 256
GMA 900 (Intel 915G) 256
GMA 950 (Intel 945G) 256
2.4.3. Визуализация набора точек

В этом разделе мы доработаем нашу программу, включив в нее возможность добавления новых точек путем простых щелчков левой кнопкой мыши на поверхности формы. Для этого мы добавим в программу обработчик события MouseDown, который при нажатии левой кнопки мыши будет добавлять в массив вершин новые точки с координатами курсора мыши. Ну и, разумеется, немного подправим обработчик события Paint. Основные фрагменты кода полученного приложения приведены в листинге 2.8 (Ex04).

public partial class MainForm : Form {
// Массив вершин
VertexPositionColor[] vertices = null;
 // Количество вершин
int pointCount = 0;
private void MainFormLoad(object sender, EventArgs e) 
{
// Вычисляем максимальное количество вершин, которые видеокарта 
может визуализировать за один 
// вызов метода DrawUserPrimitives
maxVertexCount = Math.Min(device.GraphicsDeviceCapabilities.
MaxPrimitiveCount, device.GraphicsDeviceCapabilities.MaxVertexIndex); 
// Создаем массив вершин, рассчитанный на хранение 16-ти вершин
vertices = new VertexTransformedPositionColor[16]; 
}
private void MainFormPaint(object sender, PaintEventArgs e) 
{
// Очищаем экран
device.Clear(XnaGraphics.Color.CornflowerBlue);
// Если количество точек больше нуля (метод DrawUserPrimitives 
некорректно работает с 
// массивами нулевого размера) if (pointCount > 0)
{
device.VertexDeclaration = decl; device.RenderState.PointSize = 10.0f;
// Рисуем набор точек
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin() ;
device.DrawUserPrimitives(PrimitiveType.PointList, vertices,
 0, pointCount);
pass.End() ; 
}
effect.End(); 
}
device.Present();
}
private void MainFormMouseDown(object sender, MouseEventArgs e) 
{
// Если нажата левая кнопка мыши
if (e.Button == MouseButtons.Left) 
{
// Если количество вершин достигло предельной величины if 
(pointCount == maxVertexCount) 
{
// Выводим предупреждение и выходим из обработчика события
MessageBox.Show(String.Format("Количество точек достигло 
максимального"+ 
 "значения для данного GPU: {0}.", maxVertexCount),
"Внимание", MessageBoxButtons.OK, Ч> MessageBoxIcon.Warning); return; 
}
// Если массив вершин полностью заполнен
if (pointCount == vertices.Length) 
{
// Вычисляем новый размер массива (удваиваем размер массива)
int newSize = vertices.Length * 2; 
// Размер массива не может превышать предельного значения 
if (newSize > maxVertexCount) newSize = maxVertexCount;
// Создаем новый массив увеличенного размера
VertexPositionColor[] newVertices = new VertexPositionColor[newSize];
// Копируем в него первоначальный массив
vertices.CopyTo(newVertices, 0); 
// Присваиваем полю vertices ссылку на новый массив
vertices = newVertices; 
}
// Заносим в массив информацию о новой точки, формируемой 
на основе текущих координат 
// указателя мыши. Для перевода координат указателя мыши в 
логическую систему координат XNA 
// Framework используется "самодельный" метод 
MouseToLogicalCoords.
vertices[pointCount] = new VertexPositionColor( 
 Helper.MouseToLogicalCoords(e.Location, ClientSize),
 XnaGraphics.Color.Aqua);
// Увеличиваем счетчик количества точек
pointCount++;
// Перерисовываем экран Invalidate(); 
}
}
}
Листинг 2.8.

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

Примечание

На первый взгляд, может показаться, что информацию о вершинах было бы рациональнее хранить в Genetic -коллекции list<>. Однако у подобного подхода есть один неочевидный недостаток. Дело в том, что метод DrawUserPrimitives умеет работать исключительно с классическими массивами System.Array, в результате чего нам придется постоянно преобразовывать список в массив посредством метода ToArray(), неявно создающим новый массив и копирующим в него содержимое списка. Таким образом, использование класса list<> снизит производительность приложения за счет неявного копирования информации из списка в массив, и, что еще хуже, повысит интенсивность вызовов сборщика мусора для удаления предыдущих массивов.

Другой очень полезный прием, используемый в программе - вывод всех точек одним вызовом метода GraphicsDevice.DrawUserPrimitives. Дело в том, что метод GraphicsDevice.DrawUserPrimitives тратит относительно много времени центрального процессора на подготовку графического ускорителя к визуализации примитивов, при этом собственно процесс визуализации выполняется графическим ускорителем и практически не требует вмешательства со стороны центрального процессора. Таким образом, рисуя все точки за один присест, мы значительно снижаем нагрузку на центральный процессор, распараллеливая работу между CPU и GPU.

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

// Максимальное количество примитивов, которые можно визуализировать за один присест
public int MaxPrimitiveCount { get; }
// Максимальное количество вершин, которые можно визуализировать за один присест.
public int MaxVertexIndex { get; }

В таблицах 2.7 и 2.8 приведены значения этого свойства для наиболее распространенных моделей видеокарт. Например, интегрированная видеокарта Intel GMA 900 могут визуализировать не более 65535 примитивов и не более 65534 вершины. При запуске приложения на данной видеокарте оно будет упираться в максимальное количество вершин (65534). А вот на видеокартах корпорации ATI наше приложение будет упираться в максимальное количество визуализируемых примитивов. Таким образом, при оценке максимального количества точек, которые приложение может вывести на экран, необходимо учитывать как значение свойства MaxPrimitiveCount, так и MaxVertexIndex:

maxVertexCount = Math.Min(device.GraphicsDeviceCapabilities.MaxVertexIndex,
 device.GraphicsDeviceCapabilities.MaxPrimitiveCount)
Таблица 2.7. Значение свойства MaxPrimitiveCount для некоторых GPU
GPU Значение
NVIDIA NV2x - NV3x 1.048.575
ATI R2xx - R5xx 1.048.575
Intel GMA 9xx 65.535
Intel GMA 3000 65.535 – 1.048.575
Таблица 2.8. Значение свойства MaxVertexIndex для некоторых GPU
GPU Значение
ATI R2xx - R5xx 16.777.215
NVIDIA NV2x - NV3x 1.048.575
Intel GMA 9xx 65.534
Intel GMA 3000 65.534 – 16.777.215

Внимание!

Если количество визуализируемых примитивов превысит допустимый лимит, на некоторых компьютерах могут начать происходить странные вещи вплоть до полного краха системы и "синего экрана смерти" (blue screen of death). Эта особенность является обратной стороной медали высокой производительности XNA Framework -любое некорректно написанной XNA -приложение теоретически может нарушить работу всей системы.

Итак, теоретически приложение вполне может столкнуться с видеокартой, способной выводить не более 65534 примитивов за один присест. Много это и ли мало? Например, если пользователь будет каждую секунду добавлять на экран по точке, то через 18 часов он достигнет лимита для Intel GMA 900. Иными словами, это довольно внушительное значение для нашего приложения, но вполне достижимое. Поэтому в приложение на всякий случай встроена проверка: при достижении предала на количество визуализируемых примитивов, точки просто перестают добавляться в массив. Как говорится, дешево и сердито10Более практичным подходом является автоматическая генерация дополнительных массивов по мере достижения размера предыдущего массива значения MaxPrimitiveCount. Однако эта функциональность заметно усложнит приложение, а ее полезность в данном случае весьма сомнительна .

Так же стоит обратить внимание на проверку размера массива на неравенство нулю перед тем, как вывести его на экран. Дело в том, что метод DrawUserPrimitives при попытке визуализации массива генерирует исключение System.IndexOutOfRangeException. Хотя подобное поведение метода нельзя назвать безупречным, эту особенность приходится учитывать.

В заключении следует обратить внимание на преобразование координат указателя мыши из системы координат клиентской области окна в логическую систему XNA Framework, в которой координаты компонентов вершин лежат в диапазоне от -1 .. +1. Кроме того, следует учитывать, что в Windows положительное направление оси Y направленно вниз, а в XNA Framework - вверх. Так подобные преобразования будут довольно часто применяться в наших приложениях, они были вынесены в отдельный класс Helper, расположенный в файле Helper.cs (листинг 2.9). В дальнейшем мы продолжим размещать в этом классе различные вспомогательные методы, облегчающие работу с XNA Framework.

using System;
using System.Collections.Generic; using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
namespace GSP.XNA 
{
class Helper
{ 
// Принимает в качестве параметров координаты указателя мыши и
 размер клиентской области окна. Возвращает координаты указателя мыши 
 раз в оконной системе координат.
public static Vector3 MouseToLogicalCoords(System.Drawing.Point 
location, System.Drawing.Size clientSize)
{
Vector3 v; 
// Приводим координаты указателя мыши к диапазону [-1, +1]. 
Для предотвращения деления на 0 
// используется метод Math.Max, не дающий знаменателю дроби 
стать меньше 1.
v.X = (float)location.X / (float)Math.Max(clientSize.Width - 1, 1)
 * 2.0f - 1.0f;
v.Y = 1.0f - (float)location.Y / (float)Math.Max(clientSize.Height 
- 1, 1)*2.0f;
v.Z = 0.0f;
return v; 
  } 
 } 
}
Листинг 2.9.
2.4.4. Управление цветом точек средствами HLSL

В этом, заключительно разделе, посвященном точкам, мы добавим в наше приложение возможность визуализации разноцветных пикселей. В принципе, это довольно тривиальная операция, если бы не одно но: в настоящее время наше приложение визуализирует точки исключительно фиксированного цвета морской волны ( aqua ), который жестко задан в файле эффекта (в нашем случае это SimpleEffect.fx ) и не может быть изменен C# -приложением. К примеру, если вы исправите код

vertices[pointCount] = new VertexPositionColor(
Helper.MouseToLogicalCoords(e.Location, ClientSize),
 XnaGraphics.Color.Aqua);

На

vertices[pointCount] = new VertexPositionColor(
4> Helper.MouseToLogicalCoords(e.Location,
 ClientSize), XnaGraphics.Color.Red);

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

Входные и выходные параметры функций языка HLSL

Начнем модификацию эффекта с вершинного шейдера. Теперь на вход шейдера будут подаются два параметра: координаты вершины ( iPos ) и цвет вершины ( iColor ). Результаты выполнения шейдера -однородные координаты вершины ( oPos ) и цвет вершины ( oColor ). Для указания компилятору связи входного параметра iColor с цветом вершины используется семантика color (листинг 2.10). Семантика color выходного параметра oColor указывает компилятору на то, что в этом параметре хранится результирующий цвет вершины.

void MainVS(in float3 iPos:POSITION, in float4 
iColor:COLOR, out float4 oPos:POSITION, 4>
out float4 oColor:COLOR)
{
oPos = float4(iPos, 1);
// Просто копируем параметр Color без изменения.
oColor = iColor; 
}
Листинг 2.10.

Обратите внимание на использование новых ключевых слов: in и out. Ключевое слово in используется для указания входных параметров, передающихся по значению. Ключевое слово out указывает на то, что параметр является возвращаемым: по завершению работы функции значение out-параметра копируется в вызывающий метод. Если параметр является одновременно и входным и выходным, то для указания этого факта используется ключевое слово inout. Например, мы можем объединить параметры iColor и oColor в один параметр Color, что позволит немного упростить код шейдера (листинг 2.11).

// Цвет вершины (параметр color) проходит через 
вершинный шейдер без изменений
void MainVS(in float3 iPos:POSITION, inout float4 
color:COLOR, out float4 oPos:POSITION)
{
oPos = float4(iPos, 1); 
}
Листинг 2.11.

Если не указан тип параметра функции ( in, out или inout ), HLSL делает этот параметр входящим ( in ). Соответственно, ключевое слово in указывать не обязательно. Кстати, мы активно использовали эту возможность в прошлом разделе.

Андрей Леонов
Андрей Леонов

Reference = add reference, в висуал студия 2010 не могу найти в вкладке Solution Explorer, Microsoft.Xna.Framework. Его нету.