Процесс визуализации изображений в XNA Framework ощутимо отличаются от подхода, используемого в "классических" двухмерных библиотеках вроде GDI/GDI+. В XNA Framework все графические построения осуществляются с использованием простых фигур, называемых графическими примитивами. XNA Framework поддерживает три вида графических примитивов: точки, отрезки и треугольники. Эти примитивы очень просты в отображении, поэтому все современные NVIDIA GeForce 8800 GTX может визуализировать порядка 400 миллионов треугольников в
Каждый примитив задается набором вершин: точка – одной в центре точки, отрезок двумя – вершинами на концах отрезка, а треугольник – тремя вершинами в углах треугольника. В XNA Framework координаты вершин обычно задаются тремя координатами x, y и z. Центр используемой системы координат расположен в центре клиентской области формы, ось положительное направление оси X направлено вправо, ось Y – вверх, а ось Z – из экрана монитора на наблюдателя (рисунок 2.1). Левый нижний угол формы имеет координаты (-1, -1, 0), верхний правый (+1, +1, 0).
(рис 2.1) Система координат клиентской области формы при использовании XNA Framework В пространстве имен Microsoft.Xna.Framework.Graphics имеется ряд структур для хранения информации о вершинах примитива. В настоящее время для нас наиболее интересна структура VertexPositionColor, инкапсулирующая информацию о координатах и цвете вершины. Начнем рассмотрение этой структуры с конструктора:
public VertexPositionColor(Vector3 position, Color color);
где
position - координаты вершины;color - цвет вершины.В процессе создания новой вершины ширина и высота автоматически заносятся конструктором в поля Position и Color структуры VertexPositionColor:
public Color Color; public Vector3 Position;
Здесь мы впервые встречаемся с новой для нас структурой Microsoft.XNA.Framework.Vector3, инкапсулирующей трехмерный вектор. Наряду с Vector3 в XNA Framework определены структуры Vector2 и Vector4, которые, как нетрудно догадаться, предназначены для работы с двухмерными и четырехмерными векторами. Структуры Vector2, Vector3, Vector4 широко используются в XNA Framework для хранения координат вершин, а так же выполнения различных математических векторных операций. Так как большая часть функциональности данных структур нам пока не нужна, мы отложим их подробное изучение до пятой лекции. В конец концов, с точки зрения логики работы наших первых приложений структуры Vector2, Vector3 и Vector4 представляют собой всего лишь расширенную версию структуры System.Drawing.PointF.
Информация обо всех вершинах примитива хранится в массиве. Например:
// Вершин примитива VertexPositionColor[] vertices;
Казалось бы, все должно быть очень просто, если бы не один нюанс. Дело в том, что при визуализации
примитивов информация о вершинах напрямую передается в графический процессор видеокарты ( - ), который не имеет ни малейшего понятия об управляемом коде и, соответственно,
формате структуры. Для разъяснения графическому процессору формата отдельных полей структуры
применяются декларации формата вершины. В XNA Framework декларация вершины инкапсулируется
классом VertexDeclaration, конструктор которого приведен ниже:
public VertexDeclaration(GraphicsDevice graphicsDevice, VertexElement[] elements);
где
graphicsDevice - графическое устройство, используемое для работы с вершинамиelements - массив элементов (структур VertexElement ) c описанием формата структуры.Описание формата структуры задается массивом elements, каждый элемент которого описывает одно поле структуры. Соответственно, количество элементов в массиве elements всегда равно количеству полей структуры. Какая информация содержится в каждом элементе массива elements? Это:
Создание массива, описывающего структуру, является довольно монотонной и утомительной операцией. К счастью, разработчики XNA Framework встроили в структуру VertexPositionColor (а так же во все аналогичные структуры) статическое поле только для чтения, содержащее массив с описанием этой структуры:
public static readonly VertexElement[] VertexElements;
Соответственно, для создания декларации вершины приложению достаточно лишь передать это поле в качестве второго параметра конструктора класса VertexDeclaration.
Рассмотрим основные этапы визуализации примитивов, информация о вершинах которых хранится в
массиве структур VertexPositionColor. Сначала приложение должно создать декларацию вершины на основе описания, содержащегося в нашей структуре VertexTransformedPositionColor:
VertexDeclaration decl; … decl = new VertexDeclaration(device, VertexTransformedPositionColor.vertexElements);
Эту операцию достаточно выполнять один раз при запуске приложения, например, где-нибудь в обработчике события Load формы.
Код визуализации примитива следует поместить в обработчик события Paint. Перед тем, как приступить к визуализации примитивов, необходимо задать формат вершин примитива, присвоив свойству VertexDeclaration класса Device декларацию формата вершины, созданную в обработчике события Load.
Собственно визуализация примитивов выполняется методом DrawUserPrimitives:
DrawUserPrimitives<T>(PrimitiveType primitiveType, T[] vertexData, int vertexOffset, int primitiveCount);
где
primitiveType - тип примитива, задаваемый с использованием перечислимого типа PrimitiveType. Различные типы примитивов будут подробно рассмотрены в разделах 1.2.1, 1.2.2 и 1.2.3. Пока же отметим, что в XNA Framework поддерживает шесть типов примитивов: список точек ( PrimitiveType.PointList ), список линий ( PrimitiveType.LineList ), полоса линий ( PrimitiveType.LineStrip ), список треугольников ( PrimitiveType.TriangleList ), полоса треугольников ( PrimitiveType.TriangleStrip ) и веер треугольников ( PrimitiveType.TriangleFan ).vertexData - массив вершин примитива.vertexOffset - смещение от начала массива. Данный параметр обычно равен нулю. Ненулевые значения применяется, когда визуализируемый примитив использует примитивы лишь из части массива (например, вершины разных примитивов хранятся в одном большом общем массиве).primitiveCount - количество примитивов, которые будут визуализированы.Резюмируем все вышесказанное. Для визуализации примитива приложение должно выполнить следующие шаги:
Load.Paint первым делом необходимо очисть экран методомGraphicsDevice.Clear.Load ).GraphicsDevice. VertexDeclaration декларацию, созданную на втором этапе.GraphicsDevice.DrawUserPrimitives.Device.Present.Однако, это еще не все. Дело в том, что все современные содержан специализированные DrawUserPrimitives будет не DrawUserPrimitives без явного задания вершинных и пиксельных шейдеров привет к генерации исключения System.InvalidOperationException с сообщением Both a valid
В этом разделе мы познакомимся с языком High Level (язык высокого уровня для программирования шейдеров), или сокращенно HLSL. HLSL используется для программирования вершинных и пиксельных процессоров DirectX. Правда шейдеры DirectX 8 имели множество ограничений и программировались на низкоуровневом ассемблеро-подобном языке, однако в 9-й версии DirectX возможности шейдеров значительно возросли, что привело к появлению надобности в языках высокого уровня. Было создано несколько языков высокого уровня для написания шейдеров HLSL, входящий в состав DirectX 9. В XNA Framework шейдеры так же пишутся на языке HLSL, а сам XNA Framework при работе с шейдерами на платформе Windows в значительной степени опирается на функциональность DirectX.
Так язык HLSL тесно связан с архитектурой графического процессора, мы начнем этот раздел с знакомства с основами архитектуры современного графического процессора.
В разделе 2.2 вы получили представлении о визуализации примитивов средствами XNA Framework. При этом собственно процесс визуализации изображения (метод GraphicsDevice.DrawUserPrimitives ) оставался для нас черным ящиком. Настало время наверстать упущенное. Итак, при вызове метода GraphicsDevice.DrawUserPrimitives вершины из графического буфера поступают на обработку в графический конвейер XNA Framework, представляющий собой последовательность ступеней (простых операций), выполняемых над вершинами в определенном порядке (рисунок 2.2). Рассмотрим эти ступени в порядке выполнения:
(рис 2.2) Упрощенная схема графического конвейера clip space. Однородные координаты вершины определяются четырьмя числами: (x, y, z, w). Перевод однородных координат в обычные геометрические осуществляется путем деления первых трех компонентов на четвертый компонент w: (x/w, y/w, z/w. Например, вершине с однородными координатами (1, 2, 3, 4) в трехмерном пространстве соответствует точка с координатами (1/4, 2/4, 3/4) = (0.25,0.5,0.75) Использование четвертого компонента обусловлено рядом особенностей алгоритмов визуализации трехмерных изображений, используемых в 3D графике. При визуализации двухмерных изображений компонент w обычно полагают равным 1. В этом случае нижнему левому углу клиентской области формы соответствует точка с координатами (-1, -1, 0, 1), правому верхнему углу клиентской области - (1, 1, 0, 1), а центру клиентской области - соответственно (0, 0, 0, 1).На следующей ступени графического конвейера видеокарта производит преобразование координат вершины из логической системы координат в оконную. По-умолчанию координаты трансформируются таким образом, чтобы растянуть изображение на всю поверхность элемента управления. В большинстве случаев этот процесс полностью прозрачен для приложения.
Примечание
DirectX позволяет программисту задавать координаты вершин в оконных координатах. В этом случае, при вызове метода Device.DrawUserPrimitives вершины сразу поступают на третью стадию графического конвейера, минуя первую и вторую стадии. Managed DirectX и XNA Framework Beta 1 позволяют задавать координаты в оконной системе координат, однако начиная с XNA Framework Beta 2 эта функциональность почему-то пропала. По видимости, это обусловлено стремлением сделать XNA Framework как можно более платформо-независимым.
GraphicsDevice.DrawUserPrimitives. Так при использовании параметра PrimitiveType.TriangleStrip вершины трактуются, как опорные точки (вершины) полосы треугольников. При этом каждый треугольник из полосы является независимым примитивов и обрабатывается независимо от других треугольников этой полосы. Полосы треугольников подробно будут рассмотрены в разделе 2.6.3.Следующий этап – обработка пикселей пиксельным процессором с использованием программы, называемой пиксельным шейдером. На вход пиксельному процессору подаются параметры пикселя (цвет, текстурные координаты и т.д.), полученные путем интерполяции соответствующих вершинных параметров вдоль поверхности примитива. После обработки входных параметров, пиксельный процессор возвращает цвет пикселя.
Технические подробности
В современных графических процессорах имеется массив вершинных и пиксельных процессоров, что позволяет им одновременно обрабатывать несколько вершин и пикселей. Так графический процессор NV40 корпорации NVIDIA, используемый в видеокартах семейства GeForce 6800, имеет 6 вершинных и 16 пиксельных процессоров, соответственно, он может параллельно обрабатывать до 6-ти вершин и 16-ти пикселей.
В заключении стоит отметить, что этот логический конвейер DirectX не обязательно соответствует физической организации видеокарты. К примеру, видеокарта NVIDIA GeForce 8800 GTX, основанная на , содержит 8 универсальных блоков, которые могут выполнять как вершинные, так и пиксельные шейдеры .
После прочтения этого раздела у вас, возможно, сложились несколько сумбурные представления о графическом конвейере. Ничего страшного – в следующем разделе вы познакомитесь с языком HLSL и напишете несколько шейдеров, после чего все встанет на свои места.
Дополнительная информация Все современные графические подсистемы построены по принципу конвейера. Идея конвейера, впервые реализованная Генри Фордом, заключается в следующем: если сложный процесс разбить на
последовательность простых операций (конвейер), то на выходе конвейера мы получим производительность равную производительности самой медленной операции в этой цепочке. В качестве примера конвейера рассмотрим процесс производства популярных процессоров Стороннему наблюдателю может показаться, что за день один конвейер производит около 7-ми процессоров (400/60). Но в реальности, между поступлением заготовки процессора и выходом готового процессора по-прежнему проходит два месяца. Это явление получило название латентность конвейера. При нормальном функционировании конвейера на это обстоятельство можно не обращать внимания; однако в случае неполадок латентность конвейера не замедлит проявиться. Предположим, что была обнаружена и исправлена очень опасная ошибка в архитектуре процессора, после чего исправленная версия процессора немедленно поступила в производство. Но, не смотря на всю оперативность исправления ошибки, первые исправленные образцы процессоров выйдут с конвейера лишь через два месяца. А ведь подобная задержка может принести фирме заметные убытки… Другое следствие латентности – низкая эффективность конвейера при производстве небольших партий процессоров. К примеру, при производстве одного процессора темп производства будет равен 0.017 процессоров в день (один процессор за 60 дней), при производстве 28 процессоров – 0.44 процессора в день, при 100 процессорах - уже 1.33 процессоров в день и т.д. Более-менее, нормальный темп будет достигнут только при производстве партии из нескольких тысяч процессоров (рисунок 2.3). К слову, графический конвейер не является исключением из правил. Он также малоэффективен при визуализации небольшого количества примитивов. Поэтому для эффективного использования графического конвейера программист должен стараться минимизировать количество вызовов метода |
(рис 2.3) Зависимость производительности конвейера от количества выпускаемых процессоров. Производительность оценивается по числу процессоров, выпускаемых в среднем за сутки
В начале XXI века корпорация работала над революционным , имеющим на борту массив вершинных и пиксельных процессоров. Для программирования этих процессоров Microsoft в тесном сотрудничестве с разработала два похожих ассемблеро-подобных языка, которые были включены в DirectX 8. Язык для программирования вершинных процессоров получил название , а язык для программирования пиксельных процессоров – . Соответственно, программы, написанные на этих языках, стали называться вершинными и пиксельными ) обусловлено применением первых вершинных и пиксельных процессоров преимущественно для более точной передачи игры света и тени ( ) на поверхности объектов Rampage так и не поступил в массовое производство по финансовым причинам: компания была объявлена банкротом и вскоре куплена NVIDIA, а проект Rampage закрыт.
Примечание
Если быть более точным, зачатки пиксельных (1999 год). Однако по ряду причин Microsoft не захотела включить поддержку этих шейдеров в DirectX. В результате, с точки зрения DirectX -программиста, в NV10 отсутствует какая-либо поддержка шейдеров. Единственная возможность задействовать шейдеры NV10 – воспользоваться API .
Первым действительно массовым с вершинными и пиксельными процессорами стал NV20 (NVIDIA GeForce3), появившийся в 2001 году. Для программирования вершинных и пиксельных процессоров NV20 корпорация Microsoft совместно с NVIDIA разработала языки и , являющиеся расширенными версиями и . Вскоре после NV20 вышел NV25 (GeForce4) , функциональность пиксельных процессоров которого была несколько расширена.
Соответственно язык был обновлен до версии 1.328. Потом появился процессор корпорации и язык , затем R300 (Radeon 9700 Pro) с и и так далее.
В итоге к началу 2002-го года на рынке творилась полная неразбериха среди языков программирования шейдеров. К счастью Microsoft предвидела подобный поворот, и поэтому заранее сделала языки и независимыми от системы команд графического процессора. Фактически каждая версия языка Vertex/ является языком программирования для некоторого виртуального процессора, приближенного к некоторому реальному прототипу. Компиляция шейдера в систему команд физического процессора происходит непосредственно перед загрузкой шейдера в . Таким образом, языки и являются аналогами языка IL в .NET.
Независимость языков и от системы команд физического процессора теоретически позволяет выполнять любой ассемблерный код, независимо о версии шейдера.
Например, корпорации наряду с родными может выполнять и . Это достигается путем перекомпиляции чужеродных шейдеров в родной код. К сожалению, обратное преобразование не всегда возможно. Например, R200 не может выполнять , так как программа, использующая продвинутые возможности этой версии шейдеров не может быть втиснута в прокрустово ложе архитектуры R200.
По мере роста возможностей программы для вершинных и пиксельных процессоров становились все сложение и сложнее. Например, если в длина программы не могла превышать 16 ассемблерных команд, то в максимально возможное число ассемблерных инструкций превысило сотню. Соответственно возрастала трудоемкость разработки и поддержки шейдеров с использованием ассемблера-подобного языка. Таким образом, возникла реальная потребность в переходе на языки программирования шейдеров высокого уровня.
В 2002 году Microsoft выпустила высокоуровневый язык программирования шейдеров High Level . HLSL – это язык программирования высокого уровня, предназначенный для написания программ (шейдеров) для вершинных и пиксельных процессоров. HLSL является C-подобным языком программирования с многочисленными заимствованиями из C++ и C#. В тоже время в HLSL имеется ряд важных расширений, полезных при программировании графического процессора. Программа, написанная на HLSL, компилируется в один из ассемблеро-подобных языков DirectX. Таким образом, процесс компиляции HLSL программы очень напоминает компиляцию C# -программы сначала на промежуточный язык ( IL ), а затем в машинный для конкретного центрального процессора (рисунок 2.4).
(рис 2.4) Компиляция HLSL-программыСамой крупной логической единицей HLSL является эффект ( Effect ), хранящийся в отдельном текстовом файле с расширением .fx. В принципе, эффект можно считать аналогом материала в 3DS MAX. Каждый эффект состоит из одной или нескольких техник ( technique ). Техника – это способ визуализации материала. Например, эффект визуализации мраморного материала может содержать три техники для различных графических процессоров: технику High для ускорителей класса High End, Medium для ускорителей среднего класса, и Low – максимальная производительность при низком качестве NV25 (GeForce4) планировалось использовать язык . Однако после выхода NV25 оказалась, что его функциональность несколько шире, чем предполагалось. Соответственно язык оказался не удел, и вскоре был обновлен до версии 1.3.
Количество техник и их названия могут быть произвольными.
Типы данных
Как известно, лучший способ изучить новый язык программирования - написать на нем несколько программ. Так мы и поступим. Для начала мы создадим простейший эффект, закрашивающий примитив цветом морской волны ( aqua ). Эффект будет содержать одну технику, которую мы назовем Fill.
Приступим. Мы начнем с написания программы для вершинного процессора: вершинного шейдера. Наш шейдер будет принимать в качестве параметра координаты вершины в обычных декартовых координатах, а возвращать координаты вершины уже в однородных координатах. Все преобразование будет сводиться к добавлению к координатам вершины четвертого компонента ( w ), равного 1 (листинг 2.1).
float4 MainVS(float3 pos)
{
return float4(pos, 1.0);
}
Как видно, программа, написанная на HLSL, очень напоминает обычную C-программу: мы объявляем функцию MainVS, которая принимает в качестве параметра переменную типа float3, а возвращает значение типа float4. Что это за такие странные типы float3 и float4, которых нет ни в C, ни C++, ни в C#? Чтобы ответить на этот вопрос мы рассмотрим встроенные типы HLSL.
Скалярные типы
В HLSL все встроенные типы делятся на две большие группы: скалярные и векторные. C (таблица 2.1).
| Тип | Описание |
|---|---|
| bool | Логический тип, который может принимать значения true или false |
int |
32-х битное целое число |
half |
16-ти битное число с плавающей точкой |
float |
32-х битное число с плавающей точкой |
double |
64-х битное число с плавающей точкой |
Задавая тип переменной, вы просто указываете компилятору, что вы хотели бы использовать переменную этого типа. Если текущий ускоритель не поддерживает некоторые типы данных, используемые в программе, то при компиляции шейдера в машинный код они будут заменены ближайшими аналогами. Например, тип double может быть заменен на тип float, half или какой-нибудь иной внутренний тип. Поэтому программист должен стараться избегать жесткой привязки к точности и допустимому диапазону значений используемого типа данных. Особенно это актуально для типа int, так как подавляющее большинство современных ускорителей не поддерживают тип int, в результате чего он эмулируется посредством одного из вещественных типов. Допустим, у нас имеется следующий код:
// a присваивается значение 5 int a = 5; // b должно быть присвоено значение 1 int b = a / 3; // c должно стать равно 2 int c = b * 2;
Какой код будет сгенерирован компилятором? Трудно дать однозначный ответ. В большинстве случаев
компилятор просто заменяет типы int, к примеру, на float:
// a присваивается значение 5.0 float a = 5.0; // b будет присвоено значение 1.66667 float b = a / 3.0; // c станет равно 3.33334 float c = b * 2.0;
Думаю, это совершенно не тот результат, который вы ожидали. Однако в ряде случаев компилятор HLSL все же может начать скрупулезно эмулировать тип int посредством float:
// a присваивается значение 5.0 float a = 5.0; // Значение b вычисляется посредством целочисленного деления float b; // Выполняем обычно вещественное деление float fd = a / 3.0; // Находим дробную часть от деления float ff = frac(fd); // Получаем целую часть b = fd - ff; // Если частное меньше нуля, а дробная часть не равна 0, корректируем результат. Это // обусловлено тем, что frac(2.3) = 0.3, но frac(-2.3) = 0.7 if ((fd < 0) (ff > 0) ) b = b + 1; // c станет равно 2.0 float c = b * 2.0;
Нетрудно заметить, что обратной стороной подобной эмуляции является существенно падение производительности шейдера.
Из-за множества нюансов, заранее достаточно трудно предугадать, какой из двух подходов будет выбран компилятором HLSL. Единственным надежным решением является внимательный анализ кода ассемблерного кода шейдера. Поэтому рекомендуется, по возможности, избегать использования типа int в коде шейдера за исключением переменных счетчиков цикла и индексов массивов.
Векторные типы
Большинство данных, используемых в трехмерной графике, является многомерными векторами, размерность которых редко превышает 4. Так, координаты точки в трехмерном пространстве задаются трехмерным вектором, цвет пикселя - четырехмерным вектором (три цвета и альфа-канал) и так далее. Соответственно, все современные являются
В HLSL имеется множество типов для работы с векторами размерностью от 2-х до 4-х. Вектор из N
элементов типа type задается с использованием синтаксиса, отдаленно напоминающего обобщенные
( Generic ) классы из C#:
vector<type, size>
где
type - имя базового типа: bool, int, half, float или double ;size - размерность вектора, которая может быть равна 1, 2, 3 или 4.Ниже приведен пример объявления переменной v, являющейся вектором из четырех чисел типа float.
vector<float, 4> v;
Однако на практике обычно используется сокращенная запись по схеме:
{type}{N}
где
type - имя базового типаN - размерность вектора.Таким образом, вышеприведенное определение переменной v можно переписать следующим образом:
float4 v;
Язык HLSL позволяет инициализировать вектор двумя способами. Первый способ - перечислить значения вектора в фигурных скобках на манер инициализации массивов в языке C. Ниже приведен пример,
присвоения четырехмерному вектору v начального значения $$(\overline{0.2,0.4,0.6,0.8})$$.
float4 v={0.2, 0.4, 0.6, 0.8};
Другой способ - создать новый вектор с использованием конструктора и присвоить его вектору v:
float4 v=float4(0.2, 0.4, 0.6, 0.8);
Любой N мерный вектор имеет множество конструкторов, которые могут принимать в качестве параметров
как N. Подобное многообразие конструкторов дает
программисту потрясающую гибкость при инициализации векторов:
// Создаем двухмерный вектор и присваиваем ему значение (0.1, 0.2)
float2 a={0.1, 0.2};
// Создаем еще один двухмерный вектор и
присваиваем ему значение (0.3, 0.4)
float2 b=float2(0.3, 0.4);
// Создаем трехмерный вектор. Конструктору в
качестве параметра передается вектор
"b" и число
// 1.0. Соответственно вектору c будет присвоено
значение (0.3, 0.4, 1.0)
float3 c=float3(b, 1.0);
// Создаем четырехмерный вектор на основе
скалярного типа и трехмерного вектора.
Итоговое
// значение вектора d будет равно (0.7, 0.3. 0.4, 1.0)
float4 d=float4(0.7, c);
// Создаем четырехмерный вектор на основе двух
двухмерных. В результате вектору
"d" будет
// присвоено значение (0.1, 0.2. 0.3, 0.4)
float4 e=float4(a, b);
Семантики
Думаю, после такого небольшого экскурса в HLSL вы без труда сможете разобраться в тексте вершинного шейдера из листинга 2.1. Однако если быть более точным, функция, приведенная в этом листинге, не является полноценным шейдером. С точки зрения DirectX это всего лишь простая функция, принимающая в качестве параметра трехмерный вектор и возвращающая четырехмерный вектор. Чтобы превратить эту функцию в вершинный шейдер, мы должны связать параметр Pos с координатами вершины, а результаты функции - с итоговыми координатами вершины. В HLSL для этой цели используются так называемые семантики ( semantics ), предназначенные для связи между собой данных, проходящих через различные ступени графического конвейера. В таблице 2.2 приведены некоторые семантики для входящих данных вершинного шейдера. Описание всех семантик HLSL можно найти в приложении 3.
Примечание
Теоретически вершина может содержать несколько цветов, геометрических координат и т.п. Чтобы различать их в названии семантики требуется указывать целочисленный индекс. При отсутствии индекса в названии семантики он полагается равным 0. Применение семантик с индексами будет рассмотрено в пятой лекции.
| Семантика | Описание |
|---|---|
POSITION[n] |
Координаты вершины |
COLOR[n] |
Цвет вершины |
PSIZE[n] |
Размер точки (при визуализации набора точек) |
Для связи параметра функции с входными данными шейдера, после объявления параметра укажите знак двоеточия и название соответствующей семантики. Таким образом, для связи параметра pos функции MainVS с координатами вершины необходимо использовать семантику POSITION (листинг 2.2).
float4 MainVS(float3
pos:POSITION)
{
return float4(pos, 1.0);
}
Теперь нам надо указать, что функция MainVS возвращает трансформированные координаты вершины. Для этого в HLSL используются семантики выходных данных вершинного шейдера. В частности, для указания того факта, что шейдер возвращает трансформированные координаты вершины используется семантика POSITION (листинг 2.3).
float4 MainVS(float3
pos:POSITION):POSITION
{
return float4(pos, 1.0);
}
Вот теперь мы наконец-то получили полноценный вершинный шейдер. Следующий этап - написание пиксельного шейдера. Наш первый пиксельный шейдер будет просто закрашивать все пиксели цветом морской волны ( aqua ) (листинг 2.4).
float4 MainPS() : COLOR
{
return float4(0.0, 1.0, 1.0, 1.0);
}
Примечание
В HLSL минимальной яркости цветового канала соответствует значение 0.0, а максимальной 1.0.
Так как этот шейдер будет выполняться для каждого пикселя визуализируемого примитива, все пиксели примитива окрасятся в цвет морской волны. Семантика color указывает DirectX, что результат работы пиксельного шейдера MainPS является итоговым цветом пикселя.
Техники, проходы и профили
И так, у нас имеются программы для вершинного и пиксельного процессора - вершинный и пиксельный шейдеры. Заключительный этап написания эффекта - создание техники ( technique ), использующей этот шейдеры. Ниже приведено определение техники с названием Fill, использующей вершинный шейдер MainVS и пиксельный шейдер MainPS (листинг 2.5).
technique Fill
{
pass p0
{
VertexShader = compile vs_1_1 MainVS();
PixelShader = compile ps_1_1 MainPS();
}
}
Как видно, техника определяется с использованием ключевого слова technique. Каждая техника содержит один или несколько проходов, объявляемых с использованием ключевого слова pass. В свою очередь каждому проходу ставится в соответствие пиксельный и вершинный шейдер. Наша техника Fill содержит единственный проход с названием p0.
Примечание
Многопроходные техники используются для создания сложных спецэффектов, которые не могут быть визуализированы за один проход графического конвейера.
Вершинный шейдер для каждого прохода ( pass ) задается с использованием следующего синтаксиса:
VertexShader = compile {используемый профиль} {вершинный шейдер};
Пиксельный шейдер задается аналогично:
PixelShader = compile {используемый профиль} {пиксельный шейдер};
Профиль шейдера ( ) задает промежуточный ассемблеро-подобный язык, на который будет скомпилирован шейдер. Кроме того, профиль задает некоторые архитектурные особенности целевого графического процессора, которые будут учтены компилятором при генерации промежуточного ассемблерного кода. В большинстве случаев каждой версии шейдеров соответствует один профиль. Например, языку соответствует профиль vs_1_1; – профиль ps_1_4, – профиль ps_2_0 и так далее. Однако некоторым языкам вроде соответствует два профиля: в данном случае это ps_2_a и ps_2_b, при этом первый профиль генерирует код , оптимизированный под архитектуру NV3x, а второй – для R4xx. В таблицах 2.3 и 2.4 приведено соответствие между профилями и
соответствующими версиями шейдеров.
| Профиль | Версия вершинных шейдеров |
|---|---|
| vs_1_0 | 1.0 |
| vs_1_1 | 1.1 |
| vs_2_0 | 2.0 |
| vs_2_a | 2.x |
| vs_3_0 | 3.0 |
| Профиль | Версия пиксельных шейдеров |
|---|---|
| ps_1_0 | 1.0 |
| ps_1_1 | 1.1 |
| ps_1_2 | 1.2 |
| ps_1_3 | 1.3 |
| ps_1_4 | 1.4 |
| ps_2_0 | 2.0 |
| ps_2_a | 2.x (оптимизация для NV3x ) |
| ps_2_b | 2.x (оптимизация для R4xx ) |
| ps_3_0 | 3.0 |
Большинство видеокарт поддерживает несколько профилей вершинных и пиксельных шейдеров. В результате каждый разработчик сталкивается с проблемой выбора используемого профиля. В большинстве случаев выбор версии шейдеров определяется минимальными требованиями к приложению.
Допустим, необходимо, чтобы наша программа могла работать на видеокартах класса и выше, NVIDIA GeForce FX 5200 (NV3x) и выше, а так же Intel GMA 900 и выше. Изучив приложение 2, мы увидим, что все видеокарты, удовлетворяющие этому критерию, поддерживают профили вершинных шейдеров vs_1_0, vs_1_1, vs_2_0 и профили пиксельные шейдеров ps_1_0, ps_1_1, ps_1_2, ps_1_3, ps_1_4 и ps_2_0. Таким образом, мы можем смело использовать профили vs_2_0 и ps_2_0 для всех шейдеров. При этом для некоторых эффектов можно предусмотреть дополнительные техники ( technique ) для видеокарт класса High End, использующих профили vs_3_0 и ps_3_0.
Примечание
семейства NV3x демонстрируют очень низкую производительность при использовании профилей пиксельных шейдеров ps_2_0 и ps_2_a. Если для вас актуальна производительность вашего приложения на этих , то имеет смысл стараться по возможности использовать профиль ps_1_4 вместо ps_2_0. Другой вариант – предусмотреть отдельные упрощенные техники для NV3x, использующие профили ps_1_4.
В примерах этого курса я буду стараться использовать минимальную версию профилей, необходимую для нормальной компиляции шейдеров. В частности, именно по этой причине, наш эффект Fill использует профили vs_1_1 и ps_1_1: это позволит работать нашему эффекту даже на стареньких видеокартах семейства GeForce3 (NV20).
И так, у нас есть вершинный и пиксельный шейдеры, а так же техника Fill, использующая эти шейдеры. Для получения готового эффекта осталось только помесить их в файл с расширением *.fx, например, в SimpleEffect.fx (листинг 2.6).
// Вершинный шейдер. Принимает координаты
вершины (x, y, z). Возвращает – координаты вершины
// в однородных координатах (x, y, z, 1.0)
float4 MainVS(float3 pos:POSITION):POSITION
{
return float4(pos, 1.0);
}
// Пиксельный шейдер. Закрашивает все пиксели
примитива цветом морской волны.
float4 MainPS():COLOR
{
return float4(0.0, 1.0, 1.0, 1.0);
}
// Техника Fill
technique Fill
{
// Первый проход
pass p0
{
// задаем вершинный шейдер для техники. Для
компиляции шейдера используется профиль vs_1_1
VertexShader = compile vs_1_1 MainVS();
// задаем пиксельный шейдер. для компиляции
шейдера используется профиль
ps_1_1 PixelShader = compile ps_1_1 MainPS();
}
}
Теперь мы должны научиться использовать этот эффект в наших C# -приложениях.
Одним из основных классов XNA Framework, предназначенным для работы с эффектами, является класс Effect. Класс Effect является довольно сложным классом, содержащим ряд коллекций, отражающих структуру файла эффекта (рисунок 2.5). Как говорилось в прошлом разделе, в каждом эффекте HLSL имеется несколько техник, которые в свою очередь содержат несколько проходов. При этом минимально возможный эффект включает хотя бы одну технику и один проход. Соответственно, класс Effect содержит коллекцию Techniques с экземплярами классов EffectTechnique, инкапсулирующих техники. В свою очередь, каждая техника содержит коллекцию Passes экземпляров класса EffectPass с информацией об эффекте.
(рис 2.5) Коллекции класса EffectЗагрузка и компиляция файла эффекта
Загрузка эффекта из файла *.fx с последующей компиляцией осуществляется при помощи статического
метода Effect.CompileEffectFromFile:
public static CompiledEffect CompileEffectFromFile (string effectFile, CompilerMacro[] preprocessorDefines, CompilerIncludeHandler includeHandler, CompilerOptions options, TargetPlatform platform);
где
effectFile – имя файла с эффектом.preprocessorDefines – массив макроопределений (аналогов директивы #define в C# ), используемых при компиляции эффекта. Мы будем использовать значение null.includeHandler – объект, используемый для обработки директив #include в fx -файле. Так как наш файл не содержит директив #include, мы будем использовать значение null.options – опции компилятора HLSL, задаваемые с использованием перечислимого типа CompilerOptions (таблица 2.5). Члены типа CompilerOptions являются битовыми флагами, что позволяет комбинировать их с использованием оператора OR. В качестве этого параметра, как правило, передается значение CompilerOptions.None.platform – значение перечислимого типа TargetPlatform, указывающее платформу, для которой компилируется эффект. В XNA Framework 1.0 поддерживаются две платформы: TargetPlatform.Windows и TargetPlatform.Xbox360, названия которых говорят за себя. Все примеры этого курса будут использовать значение TargetPlatform.Windows.| Член перечисления | Значение |
|---|---|
None |
Нет никаких опций |
Debug |
Вставляет в ассемблерный код отладочную информацию |
NotCloneable(*) |
Запрещает клонирование (создании копии) эффекта при помощи метода Clone. Эта опция уменьшает объем используемой памяти, так как в оперативной памяти не хранится информация, необходимая для клонирования эффекта. При этом экономия оперативной памяти достигает 50%. |
ForceVertexShaderSoftwareNoOptimizations |
Форсирует компиляцию вершинного шейдера с использованием максимально возможной версии (на момент написания курса это 3.0), не взирая на возможности текущего графического устройства. |
ForcePixelShaderSoftwareNoOptimizations |
Форсирует компиляцию пиксельного шейдера с использованием максимально возможной версии (на момент написания курса это 3.0), не взирая на возможности текущего графического устройства. |
PartialPrecision |
Использовать минимальную точность вычислений, поддерживаемую текущим графическим устройством. Как правило, при использовании этой опции типы double и float заменяются на half. |
SkipOptimization |
Отключает оптимизацию кода |
SkipValidation |
Отключает проверку соответствия сгенерированного кода возможностям текущего ускорителя (не превышено ли ограничение на максимальную длину программы и т.д.) перед отправкой откомпилированного кода шейдера в драйвер. Этот флаг полезен в тех случаях, когда драйверу все же удается оптимизировать слишком длинный ассемблеро-подобный код таким образом, чтобы уложиться в ограничения архитектуры графического процессора. |
(*) – не поддерживается методом CompileEffectFromFile.
Если метод Effect.CompileEffectFromFile не сможет открыть fx -файл (например, из-за его отсутствия), то будет сгенерировано одно из исключений производных от System.IO.IOException вроде System.IO.FileNotFoundException или System.IO.DirectoryNotFoundException.
Метод CompileEffectFromFile возвращает структуру CompiledEffect, содержащую откомпилированный код эффекта, а так же отчет о компиляции эффекта (возникли ли какие-либо проблемы при компиляции эффекта и т.п.).
public struct CompiledEffect
{
// Сообщения о проблемах, возникших при компиляции эффекта
public string ErrorsAndWarnings { get; }
// Был ли эффект откомпилирован удачно
public bool Success { get; }
// Если свойство Success равное true,
содержит откомпилированный код эффекта
public byte[] GetEffectCode();
...
}
Стоит отметить, что метод GetEffectCode возвращает байт-код промежуточного языка наподобие того, что содержится в exe -файлах для платформы .NET. Соответственно, этот код с точки зрения человека является лишь бессмысленным набором байт. Тем не менее, как мы увидим далее, при необходимости этот байт-код может быть легко дизассемблирован удобочитаемый текстовый вид.
Примечание
При желании приложение может сохранить откомпилированный байт-код в каком-нибудь файле, и при следующих запусках считывать из файла уже готовый откомпилированный байт-код. Кстати, Visual C# 2005 Express при компиляции проектов, использующих Content Pipeline, автоматически выполняет компиляцию fx -файлов проекта и сохраняет полученный промежуточный код в файлах с расширением nvb. Таким образом, приложениям, использующим Content Pipeline, нет нужды самостоятельно компилировать fx -файлы.
Следующий этап - компиляция байт-кода промежуточного языка в машинный код вершинных и пиксельных процессоров текущей видеокарты. Эта операция автоматически осуществляется конструктором класса Effect:
public Effect(GraphicsDevice graphicsDevice, byte[] effectCode, CompilerOptions options, EffectPool pool);
где
graphicsDevice - устройство Direct3D, которое будет использоваться для работы с эффектомbyte[] effectCode -код эффекта, предварительно скомпилированный при помощи метода CompileEffectFromFile.options - опции компилятора, задающиеся использованием перечислимого типа CompilerOptions (таблица 2.5.). Довольно часто в качестве этого параметра передается значение CompilerOptions.NotCloneable, что позволяет несколько сэкономить объем используемой оперативной памяти.pool - экземпляр класса EffectPool, позволяющий нескольким эффектам использовать общие параметры. В наших первых примерах мы будем использовать не более одного fx -файла, этот параметр будет равен null.После вызова конструктора класса Effect мы наконец-то получим готовый эффект. Теперь нам необходимо выбрать одну из техник эффекта и проверить ее поддержку текущей видеокартой. Техники эффекта хранятся в коллекции Techniques эффекта:
public EffectTechniqueCollection Techniques { get; }
Однако XNA -приложения достаточно редко обращаются к этой коллекции. Дело в том, что конструктор класса Effect автоматически находит первую попавшуюся технику эффекта и присваивает ее свойству CurrentTechnique.
public EffectTechnique CurrentTechnique { get; set; }
Соответственно, если эффект содержит лишь единственную технику, приложению для получения
информации об этой техники достаточно обратиться к свойству CurrentTechnique, возвращающему
экземпляр класса EffectTechnique, инкапсулирующий технику эффекта. Ниже приведено сокращенное
определение класса EffectTechnique:
public sealed class EffectTechnique
{
// Название техники
public string Name { get; }
// Коллекция проходов техники
public EffectPassCollection Passes { get; }
// Выполняет валидацию техники
public bool Validate();
...
}
И так, каждый эффект может содержать несколько техник. При этом некоторые техники эффекта могут нормально работать на текущем , а некоторые (наиболее продвинутые) нет. Если требования техники ( technique ) превышают возможности текущего (например, пользователь пытается запустить эффект использующий профиль ps_1_4 на NV2x ), XNA Framework проигнорирует технику. В результате примитивы, использующие эту технику, будут отображаться некорректно. Во избежание подобных неприятностей необходимо заранее проверить возможность выполнения данной техники средствами текущего графического устройства. Для этой цели в классе EffectTechnique предусмотрен метод Validate. Если техника может быть выполнена на текущем устройстве, метод Validate возвращает значение true, иначе - false. Во втором случае, приложение может попытаться подобрать альтернативную технику с меньшими системными требованиями или завершить приложение с сообщением о недостаточной "мощности" текущей видеокарты.
Резюмируя все вышесказанное можно предположить, что код для загрузки эффекта и выбора техники, как правило, имеет следующую структуру:
GraphicsDevice device;
// Флаг, устанавливаемый в значение true при
аварийном завершении работы приложения из-за
// проблем в обработчике события Load
closing = false;
...
// Этот код обычно размещается в обработчике события Load.
CompiledEffect compiledEffect;
try
{
// Загружаем эффект из файла и компилируем в промежуточный код
compiledEffect = Effect.CompileEffectFromFile
(effectFileName, null, null,
CompilerOptions.None, TargetPlatform.Windows);
}
// Если при загрузке файла эффекта возникли проблемы
catch (IOException ex)
{
// Выводим сообщение об ошибке
MessageBox.Show(ex.Message, "Критическая ошибка",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
// Завершаем работу приложения. Так как метод Close()
нельзя вызвать из обработчика
// события Load, приходится идти на хитрости
(использовать обработчик события
// Application.Idle, вызывающий внутри себя метод
Close главной формы приложения, если
// флаг closing равен true).
closing = true;
Application.Idle += new EventHandler(ApplicationIdle);
return; }
// Если эффект был скомпилирован с ошибками
if (!compiledEffect.Success)
{
// Выдаем сообщение об ошибке
MessageBox.Show(String.Format("Ошибка при компиляции
эффекта: \r\n{0}",
compiledEffect.ErrorsAndWarnings), "Критическая ошибка",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
closing = true;
Application.Idle += new EventHandler(ApplicationIdle); return; }
// Компилируем байт-код промежуточного языка и создаем объект эффекта.
effect = new Effect(device, compiledEffect.GetEffectCode(),
CompilerOptions.NotCloneable, null);
// Если текущая техника не может быть выполнена на текущем графическом устройстве.
if (!effect.CurrentTechnique.Validate())
{
// Выводим сообщение об ошибке и завершаем работу приложения
MessageBox.Show(String.Format("Ошибка при
валидации техники \"{0}\" эффекта \"{1}\"\n\r" +
"Скорее всего, функциональность шейдера
превышает возможности GPU",
effect.CurrentTechnique.Name, effectFileName),
"Критическая ошибка", MessageBoxButtons.OK,
MessageBoxIcon.Error);
closing = true;
Application.Idle += new EventHandler(ApplicationIdle);
return;
}
Визуализация объекта, использующего эффект
Визуализация примитивов, использующих эффект, начинается с вызова метода Effect.Begin:
public void Begin();
Далее приложение должно перебрать все проходы (коллекция passes) текущей техники ( CurrentTechnique ) и для каждой техники:
Pass текущего эффекта.GraphicsDevice .DrawUserPrimitives.End текущего эффекта.По окончанию визуализации эффекта приложение должно вызвать метод Effect.End. В итоге код визуализации примитива выглядит следующим образом:
Effect effect;
...
// Фрагмент типового обработчика события Paint
...
// Начинаем визуализацию примитивов с использованием эффекта effect.
effect.Begin();
// Перебираем все проходы визуализации текущей техники
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
// Начинаем визуализацию текущего прохода
pass.Begin() ;
// Визуализируем примитивы
device.DrawUserPrimitives(...);
...
device.DrawUserPrimitives(...);
// Завершаем проход
pass.End();
}
// Заканчиваем визуализацию эффекта
effect.End();
Ну что ж, этих знаний вполне достаточно, для того, чтобы попробовать свои силы в визуализации простых примитивов.
Примечание
Как известно, оператор foreach, используемый нами для перебора коллекции проходов ( effectCurrentTechnique.Passes ), обладает несколько более низкой производительностью по сравнению с классическим оператором for. Однако при небольшом количестве итераций эта особенность не является сколь либо заметным недостатком.
Как известно, иногда лучше один раз увидеть, чем сто раз услышать. Эта простая истина как никогда подходит к XNA Framework с весьма запутанной технологией визуализации примитивов. Поэтому мы начнем изучение материала с разбора приложения, рисующего в центре экрана одну точку цвета морской волны (листинг 2.7).
// Пример Examples\Ch02\Ex01
// Стандартные директивы C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
// При обработке исключений, связанных с
открытием файла эффекта, нам понадобится
// пространство имен System.IO
using System.IO;
// Включаем в приложение пространства имен XNA Framework
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using XnaGraphics = Microsoft.Xna.Framework.Graphics;
namespace GSP.XNA.Book.Ch02.Ex01
{
public partial class MainForm : Form
{
// Устройство XNA Framework
GraphicsDevice device = null;
// Параметры представления данных на экране
PresentationParameters presentParams;
// Графический буфер для хранения вершин
(то есть координат нашей точки)
VertexPositionColor[] vertices = null;
// Декларация формата вершины
VertexDeclaration decl = null;
// Эффект, используемый при визуализации точки
Effect effect = null;
// Флаг, устанавливаемый в true при подготовке
к завершении работы приложения
bool closing = false;
public MainForm()
{
InitializeComponent();
}
private void MainFormLoad(object sender, EventArgs e)
{
// Стандартная процедура настройки параметров
формы и создание графического устройства
SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
MinimumSize = SizeFromClientSize(new Size(1, 1));
presentParams = new PresentationParameters();
presentParams.IsFullScreen = false;
presentParams.BackBufferCount = 1;
presentParams.SwapEffect = SwapEffect.Discard;
presentParams.BackBufferWidth = ClientSize.
Width; presentParams.BackBufferHeight = ClientSize.Height;
device = new GraphicsDevice(GraphicsAdapter.
DefaultAdapter, DeviceType.Hardware,
this.Handle, CreateOptions.SoftwareVertexProcessing |
CreateOptions.SingleThreaded,
presentParams);
// Создаем массив, предназначенный для хранения
координат одной точки
vertices = new VertexPositionColor[1];
// Создаем декларацию формата вершины
decl = new VertexDeclaration(device,
VertexPositionColor.VertexElements);
// Задаем координаты точки (вершины)
таким образом, чтобы она всегда была в центре экрана.
// Цвет точки устанавливаем в морской волны, но
в действительности он не влияет на цвет
// точки, так как используемый эффект игнорирует
информацию о цвете вершины
vertices[0] = new VertexPositionColor
(new Vector3(0.0f, 0.0f, 0.0f),
XnaGraphics.Color.Aqua);
// Структура для хранения кода откомпилированного эффекта
CompiledEffect compiledEffect; try
{
// Пытаемся загрузить эффект из файла и
откомпилировать его в промежуточный байт-код
compiledEffect = Effect.CompileEffectFromFile
(effectFileName, null, null, CompilerOptions.None, TargetPlatform.Windows);
}
// Если файл с эффектом не был найден catch (IOException ex)
{
// Выводим сообщение об ошибке и завершаем работу приложения
MessageBox.Show(ex.Message, "Критическая
ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
closing = true;
Application.Idle += new EventHandler(ApplicationIdle);
return; }
// Если эффект не был удачно откомпилирован if
(!compiledEffect.Success)
{
// Выводим сообщение об ошибках и предупреждениях
из свойства ErrorsAndWarnings и завершаем
// работу приложения
MessageBox.Show(String.Format("Ошибка при компиляции
эффекта: \r\n{0}", compiledEffect.ErrorsAndWarnings),
"Критическая ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
closing = true;
Application.Idle += new EventHandler(ApplicationIdle); return;
}
// Создаем эффект на базе скомпилированного
байт-кода. Обратите на использование флага
// CompilerOptions.NotCloneable,который позволяет
ощутимо сократить объем оперативной
// памяти, используемой эффектом
effect = new Effect(device, compiledEffect.GetEffectCode(), CompilerOptions.NotCloneable, null);
// Выполняем валидацию текущей техники (проверяем,
может ли текущая техника выполнится на
// данном GPU)
if (!effect.CurrentTechnique.Validate())
{
// Если функциональность текущего GPU недостаточна,
выводим сообщение об ошибке
MessageBox.Show(String.Format("Ошибка при валидации
техники \"{0}\" эффекта
\"{1}\"\n\r" + "Скорее всего, функциональность
шейдера превышает возможности GPU",
effect.CurrentTechnique.Name, effectFileName),
"Критическая ошибка", MessageBoxButtons.OK,
> MessageBoxIcon.Error);
closing = true;
Application.Idle += new EventHandler(ApplicationIdle);
return;
}
}
private void MainFormPaint(object sender, PaintEventArgs e)
{
// Если приложение завершает работу из-за проблем в
обработчике события Load, выходим из
// обработчика события Paint (эффект effect может
быть не корректно инициализирован, поэтому
// попытка визуализации сцены может спровоцировать исключение)
if (closing)
return;
try
{
// Проверяем, не потеряно ли устройство
if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.Lost)
throw new DeviceLostException();
if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.NotReset)
device.Reset(presentParams);
// Очищаем форму
device.Clear(XnaGraphics.Color.CornflowerBlue);
// Устанавливаем формат вершины
device.VertexDeclaration = decl;
// Начинаем визуализацию эффекта.
effect.Begin();
// Перебираем все проходы эффекта
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
// Начинаем визуализацию текущего прохода
pass.Begin() ;
// Рисуем точку
device.DrawUserPrimitives(PrimitiveType.PointList, vertices, 0,
Ч> vertices.Length);
// Заканчиваем визуализацию прохода
pass.End();
}
// Оканчиваем визуализацию эффекта
effect.End();
// Завершаем визуализацию примитивов
// Выводим полученное изображение на экран device.Present(); }
// Обработка потери устройства
catch (DeviceNotResetException)
{
Invalidate();
}
catch (DeviceLostException)
{
}
}
// Обработчик события Idle. Завершает работу приложения.
void Application_Idle(object sender, EventArgs e)
{
Close();
}
// Сброс устройства при изменении размеров окна
private void MainForm_Resize(object sender, EventArgs e)
{
if (WindowState != FormWindowState.Minimized)
{
presentParams.BackBufferWidth = ClientSize.Width;
presentParams.BackBufferHeight = ClientSize.Height;
device.Reset(presentParams);
}
}
// Удаление устройства при завершении программы
private void MainForm_FormClosed(object sender,
FormClosedEventArgs e)
{
if (device != null)
{
device.Dispose(); device = null;
}
}
}
}
Рассмотрим наиболее интересные фрагменты программы. Вначале мы объявляем массив для хранения вершин (то есть координат нашей точки) и декларацию вершины, для хранения описания формата элементов массива:
VertexPositionColor[] vertices = null; VertexDeclaration decl = null;
Ниже объявляется эффект, который будет использоваться для визуализации точки:
Effect effect = null;
Инициализация всех этих объектов выполняется в обработчике события Load формы. После создания графического устройства, обработчик события Load создает массив с информацией о единственной вершине сцены и декларацию формата этой вершины:
vertices = new VertexPositionColor[1]; vertices[0] = new VertexPositionColor(new Vector3 (0.0f, 0.0f, 0.0f), XnaGraphics.Color. Aqua); // Описание формата вершины берется из поля VertexPositionColor decl = new VertexDeclaration(device, VertexPositionColor.VertexElements);
Далее обработчик события Load выполняет компиляцию fx -файла, после использует полученный байт-код для создания объекта эффекта:
// Для сокращения объема кода из него исключена обработка исключительных ситуаций. В реальных // приложениях так поступать категорически не рекомендуется, так как это значительно снизит // "дуракоустойчивость" вашего приложения. Поэтому настоятельно рекомендую ознакомится с // полной версией кода из листинга 2.7. CompiledEffect compiledEffect; // Компилируем fx-файл в байт код compiledEffect = Effect.CompileEffectFromFile (effectFileName, null, null, CompilerOptions.None, TargetPlatform.Windows); // Используем полученный байт-код для создания объекта эффекта. effect = new Effect(device, compiledEffect. GetEffectCode(), CompilerOptions.NotCloneable, null);
При возникновении ошибок при загрузке или компиляции эффекта обработчик не завершает работу приложения путем вызова метода Close формы, так как, если верить MSDN, это может вызвать утечку ресурсов. Вместо этого он регистрирует собственный обработчик события Idle, автоматически вызывающий метод Close. Но здесь есть один подводный камень: метод Idle будет вызван по завершении обработки всех событий, в том числе Paint. Таким образом, если не принять особых мер, не исключен вызов метода Idle с не полностью сформированным эффектом, что с большой долей вероятности приведет к краху приложения. Для борьбы с этим недоразумением в начале обработчика события Paint осуществляется проверка, не готовится ли приложение к завершению работы: если это так, то обработчик события Paint не выполняет визуализацию сцены.
Переходим к обработчику события Paint, выполняющего визуализацию изображения. Первым делом данный обработчик выполняет стандартные проверки потери устройства, после чего очищает экран. Далее он присваивает свойству VertexDeclaration графического устройства декларацию вершины, созданную в обработчике события Load: device.VertexDeclaration = decl;.
На первый взгляд эту операцию было бы рациональнее вынести в обработчик события Load. Однако это не самая лучшая идея, так как информация о параметрах графического устройства теряется при сбросе методом Reset. Следовательно, такое приложение перестало бы нормально функционировать после первой же потери устройства.
И, наконец, главная изюминка программы: визуализация точки на экране с использованием эффекта:
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
device.DrawUserPrimitives(PrimitiveType.PointList,
vertices, 0, vertices.Length);
pass.End() ;
}
effect.End();
Как видно, несмотря на обилие кода, приложение имеет достаточно простую структуру. Как говорится, у страха глаза велики. Теперь давайте попробуем создать это приложение в Visual Studio 2005. Для начала запустите Visual Studio 2005, создайте проект нового приложения Windows Forms и подключите сборку Microsoft.Xna.Framework.dll. В окне Solution Explorer щелкните правой кнопкой мыши на узле проекта и выберите в контекстном меню команду Add | New Folder, и создайте папку Data, в которой мы
(рис 2.6) Создание новой папкибудем хранить различные вспомогательные эффекты (рисунок 2.6,). Затем добавьте в папку Data файл эффекта SimpleEffect.fx (рисунок 2.7), например, при помощи команды контекстного меню Add | New Item... . Поместите в файл SimpleEffect.fx текст эффекта из листинга 2.6.
(рис 2.7) Файл SimpleEffect.fxПосле этих действий в каталоге проекта появится каталог Data, содержащий файл эффекта SimpleEffect.fx. Однако подобное расположение файла не совсем удобно, ведь при компиляции Debug -версии приложения Visual Studio копирует исполняемый exe -файл в подкаталог проекта bin\Debug, а при компиляции Release версии соответственно в каталог bin\Release. Соответственно, было бы логичным, если бы файл эффекта размещался вместе с исполняемым файлом приложения, что облегчило бы создание инсталлятора финальной версии приложения. К счастью, это достаточно легко организовать: просто выделите в окне Solution Explorer файл SimpleEffect.fx и в окне Properties присвойте свойству Copy to Output Directory значение Copy if newer (рисунок 2.8). После этого при каждой компиляции приложения Visual Studio будет автоматически создавать в подкаталоге bin\Debug или bin\Release подкаталог bin\Debug\Data или bin\Release\Data и копировать в него файл SimpleEffect.fx.
В заключении остается создать необходимые обработчики сообщений в соответствии с листингом 2.7. Полную версию приложения можно найти в example.zip в каталоге Ch02\Ex01.
(рис 2.8) Свойства файла SimpleEffect.fxНаше приложение, визуализирующее точку в центре экрана, всегда создает графическое устройство с использованием флага CreateOptions.SoftwareVertexProcessing, то есть вершинные шейдеры всегда выполняются средствами центрального процессора ( CPU ). Учитывая, что подавляющее большинство современных графических процессоров имеют аппаратную поддержку вершинных шейдеров, этот недочет приводит к неоптимальному использованию ресурсов . Использование флага CreateOptions.HardwareVertexProcessing тоже не является хорошей идей, так это сделает невозможной работу приложения на видеокартах без аппаратных вершинных процессоров (например, Intel GMA 900 и Intel GMA 950 ).
Так что же делать? Наиболее красивое решение проблемы - проверка возможностей текущего . Если текущий имеет аппаратные вершинные процессоры, приложение должно создать устройство с использованием флага CreateOptions.HardwareVertexProcessing, в противном случае - CreateOptions.SoftwareVertexProcessing.
Таким образом, нам необходимо научиться анализировать возможности текущего . В XNA Framework информация обо всех возможностях графического устройства инкапсулируются в классе GraphicsDeviceCapabilities, каждое свойство которого соответствует одной из характеристик графического устройства. Учитывая многообразие характеристик устройства, разработчики сгруппировали часть свойств в логические группы (структуры), то есть некоторые свойства класса GraphicsDeviceCapabilities в свою очередь тоже содержат набор свойств по некоторой тематике:
// Некоторые фрагменты определения класса
GraphicsDeviceCapabilities
public sealed class GraphicsDeviceCapabilities : IDisposable
{
// Группа свойств, описывающих возможности
графического устройства по визуализации примитивов
public GraphicsDeviceCapabilities.Primitive
Caps PrimitiveCapabilities { get; }
// Группа свойств с информацией о возможностях декларации вершин
public GraphicsDeviceCapabilities.
DeclarationTypeCaps DeclarationTypeCapabilities { ^ get; }
// Группа свойств с информацией о вершинных шейдерах
public GraphicsDeviceCapabilities.
VertexShaderCaps VertexShaderCapabilities { get; }
// Группа свойств с информацией о пиксельных шейдерах
public GraphicsDeviceCapabilities.
PixelShaderCaps PixelShaderCapabilities { get; }
// Группа свойств с информацией о драйвере устройства
public GraphicsDeviceCapabilities.DriverCaps DriverCapabilities { get; }
// Группа свойств с информацией об устройстве,
которая может пригодится при создании
// устройства
public GraphicsDeviceCapabilities.DeviceCaps
DeviceCapabilities { get; }3
...
// Свойства без подсвойств:
// Максимальная версия языка Vertex Shader,
поддерживаемая графическим устройством
public Version VertexShaderVersion { get; }
// Максимальная версия языка Pixel Shader,
поддерживаемая графическим устройством
public Version PixelShaderVersion { get; }
// Максимальный размер точки, которую способно
отображать графическое устройство
public float MaxPointSize { get; }
// Максимальное количество примитивов,
которое способно отобразить графическое устройство за
// один вызов метода DrawUserPrimitives
public int MaxPrimitiveCount { get; }
// Остальные свойства
...
}
Информация, которая может понадобиться при создании графического устройства, сосредоточена в свойствах свойства GraphicsDeviceCapabilities.DeviceCaps DeviceCapabilities:
// Некоторые фрагменты определения структуры
DeviceCaps public struct DeviceCaps
{
// Поддерживает ли графическое устройство
метод DrawUserPrimitives на аппаратном уровне
public bool SupportsDrawPrimitives2Ex { get; }
// Поддерживает ли графическое устройство
аппаратную растеризацию примитивов (при отсутствии
// подобной поддержки визуализация будет
выполняться с неприемлемо низкой
// производительностью)
public bool SupportsHardwareRasterization { get; }
// Имеет ли графическое устройство аппаратные вершинные процессоры
public bool SupportsHardwareTransformAndLight { get; }
...
}
Как видно, информация о наличии аппаратных вершинных процессоров содержится в свойстве SupportsHardwareTransformAndLight. Таким образом, нашему приложению необходимо просто проверить значение свойства GraphicsDeviceCapabilities.DeviceCapabilities.SupportsHardwareTransformAndLight. Если оно равно true, приложение может создать графическое устройство с использованием флага CreateOptions.HardwareVertexProcessing, в противном случае должен использоваться флаг CreateOptions.SoftwareVertexProcessing.
XNA Framework предоставляет разработчику два способа получения доступа к экземпляру объекта GraphicsDeviceCapabilities. Наиболее простым из них является использование свойства GraphicsDeviceCapabilities экземпляра класса графического устройства:
public GraphicsDeviceCapabilities GraphicsDeviceCapabilities { get; }
Не смотря на простоту данный способ обладает существенным недостатком: для получения доступа к свойству GraphicsDeviceCapabilities приложение должно создать графическое устройство. Получается замкнутый круг: чтобы получить информацию, необходимую для создания графического устройства, приложение должно создать это устройство. В принципе, мы можем попробовать написать что-то вроде:
// Создаем графическое устройство без
аппаратной поддержки вершинных шейдеров.
device = new GraphicsDevice(GraphicsAdapter.
DefaultAdapter, DeviceType.Hardware, this.Handle,
CreateOptions.SoftwareVertexProcessing |
CreateOptions.SingleThreaded, presentParams);
// Если GPU имеет аппаратные вершинные процессоры
if (device.GraphicsDeviceCapabilities.DeviceCapabilities.
SupportsHardwareTransformAndLight)
{
// Уничтожаем устройство device.Dispose();
// Снова создаем устройство, но уже с аппаратной
поддержкой вершинных шейдеров
device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
DeviceType.Hardware, this.Handle,
CreateOptions.HardwareVertexProcessing |
CreateOptions.SingleThreaded, presentParams);
}
Хотя данная технология и работает, все это напоминает поездку из Киева в Харьков через Жмеринку. Поэтому разработчики XNA Framework предусмотрели альтернативный способ получения экземпляра класса GraphicsDeviceCapabilities без создания графического устройства. Как вы знаете, конструктор класса GraphicsDevice принимает в качестве первого параметра экземпляр класса GraphicsAdapter, описывающий используемую видеокарту. Так вот, заботливые разработчики XNA Framework снабдили этот класс методом GetCapabilities, возвращающем экземпляр класса GraphicsDeviceCapabilities, соответствующий этому устройству:
public GraphicsDeviceCapabilities GetCapabilities(DeviceType deviceType);
где
deviceType - тип устройства, задаваемый с использованием перечислимого типа DeviceType.Зачем нужен параметр deviceType? Дело в том, что метод GetCapabilitie не может предугадать, какой тип устройства вы собираетесь создать ( DeviceType.Hardware, DeviceType.Reference или DeviceType.NullReference ), в то время как все эти типы устройств имеют совершенно разные характеристики. Соответственно, при помощи параметра deviceType вы указываете методу GetCapabilities, какое значение вы планируете передать параметру deviceType конструктора класса графического устройства ( GraphicsDevice/ ).
Таким образом, проверку наличия аппаратных вершинных процессоров можно организовать с использованием следующего фрагмента кода:
GraphicsDeviceCapabilities caps = GraphicsAdapter.DefaultAdapter.GetCapabilities(DeviceType.Hardware); CreateOptions options = CreateOptions.SingleThreaded; if (caps.DeviceCapabilities.SupportsHardwareTransformAndLight) options |= CreateOptions.HardwareVertexProcessing; else options |= CreateOptions.SoftwareVertexProcessing; device = new GraphicsDevice(GraphicsAdapter. DefaultAdapter, DeviceType.Hardware, this.Handle, options, presentParams)
Точка, визуализируемая нашим приложением ( 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;
(рис 2.9) Точка размером 10x10 пикселей Однако мы не можем просто так взять и присвоить свойству GraphicsDevice.RenderState.PointSize произвольное значение. Ведь никто не может гарантировать, что ваши программы будут запускаться исключительно на тех видеокартах, которые умеют работать с большими точками размером 10x10. Следовательно, необходимо предусмотреть поведение приложения в ситуации, когда видеокарта не удовлетворяет минимальным требованиям к размеру точек: наиболее логичное действие приложения в подобной ситуации – выдача соответствующего сообщение об ошибке с последующим завершением работы.
В разделе 2.4.1 упоминалось, что в XNA Framework имеется класс GraphicsDeviceCapabilities с информацией о возможностях графического устройства. В частности, свойство PointSize содержит максимальный размер точки в пикселях, поддерживаемый указанным графическим устройством:
public float MaxPointSize { get; }
Дополнительная информация
Для быстрого получения информации о возможностях текущей видеокарты я обычно пользуюсь тестовым пакетом , инсталлятор которого находится в example.zip в каталоге RightMark . Достаточно запустить , щелкнуть левой кнопкой мыши на узле (вкладка Available Tests ) и в правой части экрана появится древовидный список возможностей видеокарты. В частности на рисунке 2.10 видно, что видеокарта может визуализировать точки размером не более 256x256 пикселей.
К сожалению имеет одну нехорошую особенность – он всегда загружает процессор на 100%. Не забывайте закрывать , когда он вам больше не нужен; в противном случае вы рискуете столкнуться с резким падением производительности других приложений.
(рис 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-х пикселей. Это обстоятельство позволит нам несколько сократить код некоторых примеров без ущерба надежности.
| |
Максимальный размер точки (в пикселях) |
|---|---|
| NV20 (NVIDIA GeForce3) | 64 |
| NV25 (NVIDIA GeForce4) | 8192 |
| NV3x (NVIDIA GeForce FX) | 8192 |
| R2xx – R5xx ( |
256 |
| GMA 900 (Intel 915G) | 256 |
| GMA 950 (Intel 945G) | 256 |
В этом разделе мы доработаем нашу программу, включив в нее возможность добавления новых точек путем простых щелчков левой кнопкой мыши на поверхности формы. Для этого мы добавим в программу обработчик события 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();
}
}
}
Пройдемся по наиболее интересным особенностям приложения. Как известно, массивы .NET Framework не умеют изменять свой размер, поэтому при добавлении элемента в массив необходимо создать массив увеличенного размера, скопировать в него первоначальный массив и занести в последний элемент массива информацию о новой точке. Однако это не совсем оптимальный вариант, так как каждое добавление новой вершины сопряжено с довольно затратными операциями копирования массива, включая, возможную сборку мусора. В нашей программе используется более агрессивный подход: при каждом увеличении размера массива его размер увеличивается с неким запасом (размер массива увеличивается не на один элемент, а сразу удваивается), чтобы уменьшить вероятность повторного выделения памяти при добавлении следующих точек и уменьшить частоту запуска сборщика мусора.
Примечание
На первый взгляд, может показаться, что информацию о вершинах было бы рациональнее хранить в -коллекции list<>. Однако у подобного подхода есть один неочевидный недостаток. Дело в том, что метод DrawUserPrimitives умеет работать исключительно с классическими массивами System.Array, в результате чего нам придется постоянно преобразовывать список в массив посредством метода ToArray(), неявно создающим новый массив и копирующим в него содержимое списка. Таким образом, использование класса list<> снизит производительность приложения за счет неявного копирования информации из списка в массив, и, что еще хуже, повысит интенсивность вызовов сборщика мусора для удаления предыдущих массивов.
Другой очень полезный прием, используемый в программе - вывод всех точек одним вызовом метода GraphicsDevice.DrawUserPrimitives. Дело в том, что метод GraphicsDevice.DrawUserPrimitives тратит относительно много времени центрального процессора на подготовку CPU и .
Однако метод DrawUserPrimitives имеет ограничения на максимальное количество примитивов, которые можно визуализировать за один вызов этого метода. Количество вершин, которые можно вывести за один присест, тоже далеко не бесконечно. Информация о возможностях текущей видеокарты по визуализации примитивов хранится в двух свойствах класса GraphicsDeviceCapabilities:
// Максимальное количество примитивов, которые можно визуализировать за один присест
public int MaxPrimitiveCount { get; }
// Максимальное количество вершин, которые можно визуализировать за один присест.
public int MaxVertexIndex { get; }
В таблицах 2.7 и 2.8 приведены значения этого свойства для наиболее распространенных моделей видеокарт. Например, интегрированная видеокарта Intel GMA 900 могут визуализировать не более 65535
примитивов и не более 65534 вершины. При запуске приложения на данной видеокарте оно будет
упираться в максимальное количество вершин (65534). А вот на видеокартах корпорации наше приложение будет упираться в максимальное количество визуализируемых примитивов. Таким образом, при оценке максимального количества точек, которые приложение может вывести на экран, необходимо
учитывать как значение свойства MaxPrimitiveCount, так и MaxVertexIndex:
maxVertexCount = Math.Min(device.GraphicsDeviceCapabilities.MaxVertexIndex, device.GraphicsDeviceCapabilities.MaxPrimitiveCount)
|
Значение |
|---|---|
NVIDIA NV2x - NV3x |
1.048.575 |
|
1.048.575 |
Intel GMA 9xx |
65.535 |
Intel GMA 3000 |
65.535 – 1.048.575 |
|
Значение |
|---|---|
|
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. Иными словами, это довольно внушительное значение для нашего приложения, но вполне достижимое. Поэтому в приложение на всякий случай встроена проверка: при достижении предала на количество визуализируемых примитивов, точки просто перестают добавляться в массив. Как говорится, дешево и 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;
}
}
}
В этом, заключительно разделе, посвященном точкам, мы добавим в наше приложение возможность визуализации разноцветных пикселей. В принципе, это довольно тривиальная операция, если бы не одно но: в настоящее время наше приложение визуализирует точки исключительно фиксированного цвета морской волны ( 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
Начнем модификацию эффекта с вершинного шейдера. Теперь на вход шейдера будут подаются два параметра: координаты вершины ( ) и цвет вершины ( iColor ). Результаты выполнения шейдера -однородные координаты вершины ( ) и цвет вершины ( 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;
}
Обратите внимание на использование новых ключевых слов: 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);
}
Если не указан тип параметра функции ( in, out или inout ), HLSL делает этот параметр входящим ( in ). Соответственно, ключевое слово in указывать не обязательно. Кстати, мы активно использовали эту возможность в прошлом разделе.
Структуры
Как известно, передача в функцию большого количества параметров делает код HLSL аналогично языку C (листинг 2.12).
// Объявляем структуру входных данных шейдера.
Обратите внимание на возможность назначения
// каждому полю структуры семантики struct VertexInput
{
float3 pos : POSITION;
float4 color : COLOR; };
// Объявляем структуру выходных данных шейдера
struct VertexOutput
{
float4 pos : POSITION;
float4 color : COLOR; };
void MainVS(in VertexInput input, out VertexOutput output)
{
output.pos = float4(input.pos, 1.0f);
output.color = input.color;
}
Как видно, использование структур делает код значительно более понятным: для определения формата входных данных вершинного шейдера, достаточно лишь беглого взгляда на определение структуры VertexInput. После этой модификации наш шейдер MainVS возвращает в качестве результата лишь один параметр ( output ). Следовательно, процедуру MainVS можно заменить функцией, что сделает код программы еще более интуитивно понятным ( листинг 2.13).
VertexOutput MainVS(VertexInput input)
{
// Создаем структуру output
VertexOutput output;
output.pos = float4(input.pos, 1.0f);
output.color = input.color;
// Возвращаем результаты работы шейдера
return output;
}
Пиксельный шейдер
После обработки вершинным процессором вершины объединяются в примитивы, которые разбиваются на отдельные пиксели (то есть растеризуются). При этом параметры вершины, рассчитанные вершинным шейдером, интерполируются вдоль поверхности примитива. В нашем случае, вдоль поверхности примитива интерполируется цвет вершины. Иными словами, каждому пикселю примитива ставится в соответствие интерполированный цвет (при визуализации точек вдоль поверхности точки интерполируется константный цвет). Наш пиксельный шейдер будет просто принимать интерполированный цвет и выводить его на экран (листинг 2.14).
float4 MainPS(float4 color:COLOR):COLOR
{
return color;
}
Для привязки входных данных пиксельного шейдера к интерполированным выходным данным из вершинного шейдера используется семантика color. Хочу обратить ваше внимание на то, что семантики выходных данных вершинного шейдера и входных данных пиксельного шейдера ничего не говорят о смысле этих ps_3_0 семантики иногда все же могут оказывать незначительное влияние на работу шейдера. Эта тема подробно будет рассмотрена в разделе 4.x.color на texcoord некоим образом не повлияет на работу приложения (листинг 2.14). Главное, чтобы выходные параметры вершинного шейдера и входные параметры пиксельного шейдера использовали одинаковые семантики.
Примечание
Так как профили семейства ps_1_x не позволяют использовать четырех компонентные текстурные координаты, нам пришлось применить профиль ps_2_0. Использование текстурных координат будет рассмотрено в разделе 2.6.
struct VertexInput {
float3 pos : POSITION;
float4 color : COLOR;
};
struct VertexOutput {
float4 pos : POSITION;
// Рассчитанный цвет вершины, передается как текстурные координаты
float4 color : TEXCOORD;
};
VertexOutput MainVS(VertexInput input)
{
VertexOutput output;
output.pos = float4(input.pos, 1.0f);
output.color = input.color;
return output;
}
// Пиксельный шейдер получает входные параметры из
интерполированных текстурных координат
float4 MainPS(float4 color:TEXCOORD):COLOR
{
return color;
}
technique Fill
{
pass p0
{
VertexShader = compile vs20 MainVS(); PixelShader =
compile ps20 MainPS();
}
}
Доработка C#-приложения
С кодом эффекта мы вполне разобрались и, следовательно, можем приступать к модификации C#-кода нашего приложения: теперь при каждом щелчке левой кнопкой мыши на форму будут добавляться разноцветные точки случайного цвета. Для этого достаточно лишь немного подправить обработчик события MouseDown (листинг 2.15).
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 = newVertices;
}
XnaGraphics.Color color; double delta; do
{
// Вычисляем случайные значение компонентов R, G, B
цвета точки byte[] bytes = new byte[3]; rnd.NextBytes(bytes) ;
// Формируем цвет
color = new XnaGraphics.Color(bytes[0], bytes[1], bytes[2]);
// Вычисляем квадрат "расстояния"
между рассчитанным случайным цветом и цветом фона формы
delta = Math.Pow((color.R - XnaGraphics.Color.CornflowerBlue.R), 2) + Math.Pow((color.G - XnaGraphics.Color.CornflowerBlue.G), 2) +
Math.Pow((color.B - XnaGraphics.Color.CornflowerBlue.B), 2);
}
// Если цвет точки слабо отличается от цвета фона,
повторяем вычисления. while(delta < 1000);
// Заносим информацию о точке в массив вершин
vertices[pointCount] = new VertexPositionColor(Helper.
MouseToLogicalCoords(
e.Location, ClientSize), color);
pointCount++;
}
Invalidate();
}
При генерации случайного цвета точки приложение проверяет, не сольется ли полученный цвет с цветом фона. Так как в компьютерной графике цвет задается яркостью трех компонентов, мы можем трактовать значения этих трех компонентов как координаты цвета в некотором цветовом пространстве (рисунок 2.11). Соответственно, в качестве критерия похожести двух цветов можно использовать расстояние между этими цветами:
$$R=\sqrt{(c1_r-c2_r)^2+(c1_g-c2_g)^2+(c1_b-c2_b)^2} $$где
Однако учитывая высокую ресурсоемкость операции вычисления квадратного корня, в качестве критерия похожести цветов рациональнее использовать не само расстояние, а его квадрат. Полная версия приложения находится в example.zip в каталоге Ch02\Ex05.
(рис 2.11) Цветовое пространство Практическое упражнение №2.1
Создайте приложение, рисующее поточечный график функции y=cos(x) , где x находится в диапазоне 0°…720° (рисунок 2.12). Если у вас возникнут трудности при выполнении упражнения, посмотрите готовое приложение в example.zip ( Ch02\Ex06 ).
(рис 2.12) Поточечный график функции y=f(x), визуализированный с использованием двухсот точек
В XNA имеется два типа отрезков: независимые отрезки ( PrimitiveType.LineList ) и связанные отрезки ( PrimitiveType.LineStrip ). При указании независимого типа отрезков метод Device.DrawUserPrimitives рисует набор несвязанных между собой отрезков прямых линий. Первый отрезок рисуется между нулевой и первой вершиной набора вершин, второй отрезок – между второй и третьей, и т.д. (рисунок 2.13). Данный тип примитивов обычно применяется для рисования отдельных отрезков. Связанные отрезки ( PrimitiveType.LineStrip ) используются для построения ломаной линии, проходящей через вершины. Первый сегмент линии рисуется между нулевой и первой вершиной, второй – между первой и второй вершиной и т.д. (рисунок 2.14).

(рис 2.14) Независимые отрезки (рис 2.13) Связанные отрезки (Direct3D.PrimitiveType.LineStrip)Для демонстрации практического использования примитивов PrimitiveType.LineList мы перепишем пример Ex04. Первая точка отрезка будет задаваться нажатием левой кнопки, а вторая – при отпускании левой кнопки мыши. Таким образом, процесс рисования линии будет аналогичен редактору Paint – пользователь помещает указатель мышь в начало отрезка, зажимает левую кнопку, и ведет указатель мыши до конца отрезка, после чего отпускает левую кнопку мыши. В листинге 2.16 приведены основные фрагменты исходного кода полученного приложения (Ex07):
public partial class MainForm : Form
{
...
// Массив вершин отрезков
VertexPositionColor[] vertices = null;
// Количество отрезков
int lineCount = 0;
// Максимальное количество отрезков, которые текущая
видеокарта может визуализировать одним
// вызовом метода DrawUserPrimitives int maxLineCount;
// Флаг, показывающий, находится ли программа в
режиме добавления нового отрезка (когда
// пользователь уже указал начало отрезка, но еще
не отжал левую кнопку мыши)
bool AddingLine = false;
private void MainFormLoad(object sender, EventArgs e)
{
// Определяем максимальное количество отрезков,
которое видеокарта может визуализировать за
// один вызов метода DrawUserPrimitives
maxLineCount = Math.Min(device.GraphicsDeviceCapabilities.
MaxPrimitiveCount,
device.GraphicsDeviceCapabilities.MaxVertexIndex / 2);
// Создаем массив, рассчитанный на хранение вершин
восьми отрезков vertices = new VertexPositionColor[16];
}
private void MainFormPaint(object sender, PaintEventArgs e)
{
// Очищаем экран
device.Clear(XnaGraphics.Color.CornflowerBlue);
// Если количество отрезков больше нуля if (lineCount > 0)
{
device.VertexDeclaration = decl;
// Визуализируем отрезки
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin() ;
device.DrawUserPrimitives(PrimitiveType.LineList,
vertices, 0, lineCount);
pass.End() ;
}
effect.End();
}
device.Present();
}
private void MainFormMouseDown(object sender, MouseEventArgs e)
{
// Если нажата левая кнопка мыши
if (e.Button == MouseButtons.Left)
{
// Если количество линий достигло предельно возможной величины,
нечего не делаем if (lineCount == maxLineCount)
{
MessageBox.Show(String.Format("Количество отрезков достигло
максимального" +
"значения для данного GPU: {0}.", maxLineCount),
"Внимание", MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return; }
// Переходим в режим добавления отрезка AddingLine = true;
// Если размер массива вершин не достаточен
вставки нового отрезка, то создаем массив
// удвоенного размера и копируем в него
содержимое старого массива. if (lineCount * 2 >= vertices.Length)
{
int newLineCount = lineCount * 2;
// Размер массива не должен превышать предельно
лимит текущей видеокарты if
(newLineCount > maxLineCount) newLineCount = maxLineCount;
VertexPositionColor[] newVertices = new VertexPositionColor
[newLineCount*2];
vertices.CopyTo(newVertices, 0);
vertices = newVertices;
}
// Заносим в массив вершин координаты начала и конца
нового отрезка. Для перевода координат
// указателя мыши к диапазону [-1, +1] используется
метод MouseToLogicalCoords, созданный
// нами в разделе 2.4.3.
vertices[lineCount * 2] =
new VertexPositionColor(Helper.MouseToLogicalCoords
(e.Location, ClientSize),
XnaGraphics.Color.Aqua);
vertices[lineCount * 2 + 1] = vertices[lineCount * 2];
// Увеличиваем счетчик количества отрезков на 1
lineCount++;
// Перерисовываем форму Invalidate();
}
}
private void MainFormMouseMove(object sender, MouseEventArgs e)
{
// Если программа находится в режиме добавления нового отрезка
if AddingLine == true)
{
// Обновляем координаты конца отрезка
vertices[lineCount * 2 - 1].Position =
Helper.MouseToLogicalCoords(e.Location,
ClientSize) ; // Перерисовываем экран Invalidate() ;
}
}
private void MainFormMouseUp(object sender, MouseEventArgs e)
{
// Если была отжата левая кнопка мыши
if (e.Button==MouseButtons.Left)
// Выходим из режима добавления нового отрезка AddingLine = false;
}
}
Небольшого внимания заслуживает код, вычисляющий максимальное количество линий, которое может визуализировать видеокарта за один вызов метода DrawUserPrimitives. Как вы знаете из раздела 2.4.3, значение максимального количества примитивов, которые может визуализировать видеокарта за один присест, определяется свойствами GraphicsDeviceCapabilities.MaxPrimitiveCount и GraphicsDeviceCapabilities.MaxVertexIndex. Но так как каждый примитив типа PrimitiveType.LineList содержит две вершины, при оценке максимального количества отрезков, которые может визуализировать видеокарта за один присест, приложение должно поделить значение GraphicsDeviceCapabilities.MaxVertexIndex на 2.
Чтобы сделать работу с программой более комфортной, мы встроим в нее возможность отмены изменений при помощи комбинации клавиш Ctrl+Z, что позволит пользователю легко откатываться назад после ошибочно нарисованных отрезков и т.д. Код обработчика, выполняющего откат изменений, приведен в листинге 2.17 После такой доработки нашу программу вполне можно будет использовать как простенький графический редактор (рисунок 2.15).
private void MainForm_KeyDown(object sender, KeyEventArgs e)
{
if ((e.KeyCode==Keys.Z) (e.Control==true))
if (AddingLine == false)
{
if (lineCount > 0) lineCount--;
Invalidate();
}
}
(рис 2.15) Изображение, нарисованное при помощи нашего самодельного графического редактора (Ex07)
Перейдем к следующему типу примитивов – PrimitiveType.LineStrip. Как говорилось выше, этот тип примитивов применяется для рисования ломаных линий, которые часто используются при построении контуров различных поверхностей или графиков функций. Чтобы опробовать примитивы типа PrimitiveType.LineStrip на практике, мы напишем приложение, рисующее в центре формы окружность радиусом 0.8 единиц ( Ex08 ). Окружность будет нарисована с использованием ломаной линии, содержащей тридцать два сегмента. Каждая вершина ломанной будет иметь свой цвет, благодаря чему окружность будет переливаться различными цветами (рисунок 2.16). Для вычисления координат вершин окружности мы воспользуемся простой формулой из школьного курса аналитической геометрии:
где
y - координаты текущей вершины окружностиy0 - координаты центра окружностиНаиболее важные фрагменты приложения приведены в листинге 2.18.

(рис 2.18) Окружность, нарисованная с использованием примитивов Direct3D.PrimitiveType.LineStrip(рис 2.16) public partial class MainForm : Form { GraphicsDevice device = null; PresentParameters presentParams; VertexDeclaration decl; VertexPositionColor[] vertices = null; // Количество сегментов в ломанной линии, аппроксимирующей окружность. const int LineStripCount = 32; ... private void MainForm_Load(object sender, EventArgs e) { ... decl = new VertexDeclaration(device, VertexPositionColor.VertexElements); // Создаем графический буфер, для хранения вершин окружности vertices = new VertexPositionColor[LineStripCount + 1]; } private void MainForm_Paint(object sender, PaintEventArgs e) { ... // Очищаем экран device.Clear(XnaGraphics.Color.CornflowerBlue); device.VertexDeclaration = decl; // Перебираем все вершины for (int i = 0; i <= LineStripCount; i++) { // Вычисляем координаты текущей вершины окружности по формуле 2.2 float angle = (float)i / (float)LineStripCount * 2.0f * (float)Math.PI; // Окружность имеет радиус 0.8 единиц и расположена в начале системы координат float x = 0.8f * (float)Math.Sin(angle); float y = 0.8f * (float)Math.Cos(angle); // Вычисляем цвет вершины int red=(int) (255 * Math.Abs(Math.Sin(angle * 3))); int green = (int)(255 * Math.Abs(Math.Cos(angle * 2))); // Заносим информацию о вершине в графический буфер vertices[i] = new VertexPositionColor(new Vector3(x, y, 1.0f), new XnaGraphics.Color (red, green, 0) ) ; }; // Рисуем ломанную, аппроксимирующую окружность. Ломанная состоит из vertices.Length - 1 // сегментов. effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin() ; device.DrawUserPrimitives(PrimitiveType.LineStrip, vertices, 0, 4> vertices.Length - 1) ; pass.End() ; } effect.End(); device.Present(); } }
Приложение устроено достаточно просто: сначала в обработчике события Load по формуле 2.2 вычисляются вершины, через которые будет построена ломаная, аппроксимирующая окружность. Визуализация полученной ломанной выполняется в обработчике события Paint.
Стоит отметить, что с ростом числа сегментов ломанная все сильнее начинает походить на настоящую окружность; при количестве сегментов порядка сотни вряд ли кто сможет найти визуальные различия между окружностью, визуализированной поточено средствами GDI+, и ее аппроксимацией ломанной линией. А вот разница в производительности будет более чем заметна.
Управление видовым преобразованием
Так как мы используем логическую систему координат, в которой ширина и высота формы всегда равна двум, непропорциональное растяжение формы приводит к искажению изображения (рисунок 2.17). Существует два метода борьбы с этим явлением:
(рис 2.17) Искажения формы круга при непропорциональном изменении размеров окна У каждого из этих подходов есть недостатки: реализация масштабирования примитивов сцены неминуемо сделает код визуализации более запутанным, а ограничение на размеры окна будет сковывать действия пользователя и создаст чувство дискомфорта. Однако существует и третий вариант:
Этот метод имеет важный нюанс относительно первого варианта: в данном случае трансформируются не координаты вершин примитива, а итоговое Viewport класса GraphicsDevice: public Viewport Viewport { get; set; }.
Одноименная структура Viewport, инкапсулирующая параметры видового преобразования, определяется следующим образом:
public struct Viewport
{
// Ширина области, в которую осуществляется визуализация
public int Width { get; set; }
// Высота области, в которую осуществляется визуализация
public int Height { get; set; }
// Координата X левого верхнего угла области визуализация
public int X { get; set; }
// Координата Y левого верхнего угла области визуализация
public int Y { get; set; }
…
}
Как видно, структура Viewport определяет в клиентской области окна прямоугольную область, используемую для визуализации изображения. По умолчанию при создании и сбросе устройства XNA Framework автоматически присваивает полям X и Y нулевое значение, a Width и Height - ширину и высоту клиентской области окна. Таким образом, по умолчанию клиентская область заполняет всю клиентскую область окна.
В следующем примере демонстрируется использование пользовательской области визуализации размером 100x100, расположенной в левом верхнем углу приложения (листинг 2.19).
// Пример Examples\Ch02\Ex09
private void MainForm_Load(object sender, EventArgs e)
{
// Важно! Размер формы не может быть меньше области
визуализации, в противном случае при
// попытке визуализации в такую форму будет сгенерировано исключение
// System.InvalidOperationException
MinimumSize = SizeFromClientSize(new Size(100, 100)); ...
}
private void MainForm_Paint(object sender, PaintEventArgs e)
{
if (closing) return;
try
{
if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.Lost)
throw new DeviceLostException();
if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.NotReset)
device.Reset();
// Задание параметров области визуализации.
Viewport viewport = new Viewport();
viewport.Width = 100;
viewport.Height = 100;
// Присваиваем информацию об области визуализации структуре Viewport
device.Viewport = viewport;
// Очищаем экран
device.Clear(XnaGraphics.Color.CornflowerBlue);
// Рисуем окружность
...
}
}
Результат визуализации приведен на рисунке 2.18. Что же изменилось в работе приложения?
(рис 2.18) Использование области визуализации 100x100, расположенной в левом верхнем углу окна Примечание
Отладочная версия DirectX перед началом визуализации кадра автоматически закрашивает его случайным цветом, что позволяет разработчику легко обнаружить области окна, игнорируемые приложением.
Теперь мы уже можем определиться со стратегией борьбы с геометрическими искажениями в примере Ex08: перед визуализацией изображения приложение должно задать квадратную область визуализации максимально возможного размера, расположенную в клиентской области формы. При этом, во избежание артефактов по краям окна, во время очистке окна методом Clear должна применяться область визуализации размеров во всю клиентскую область формы. Учитывая, универсальность подобной технологии, методы рассчитывающие параметры области визуализации будет разумно поместить в наш класс Helper.cs (листинг 2.20).
// Сборник вспомогательных методов, полезных в хозяйстве
class Helper
{
...
// Принимает размеры клиентской области формы. Возвращает
квадратную область визуализации
// максимально возможного размера, расположенную в центре формы.
public static Viewport SquareViewport(System.Drawing.Size clientSize)
{
Viewport viewport = new Viewport();
viewport.Width = Math.Min(clientSize.Width, clientSize.Height);
viewport.Height = viewport.Width;
viewport.X = (clientSize.Width- viewport.Width) / 2;
viewport.Y = (clientSize.Height - viewport.Height) / 2;
return viewport;
}
// Принимает размеры клиентской области формы. Возвращает
область визуализации размером во
// всю клиентскую область формы
public static Viewport FullScreenViewport(System.Drawing.
Size clientSize)
{
Viewport viewport = new Viewport(); viewport.Width =
clientSize.Width; viewport.Height = clientSize.Height; return viewport;
}
}
Код модифицированного приложения, использующего новые методы класса Helper, приведен в листинге 2.21.
// Пример Ch02\Ex10
private void MainForm_Paint(object sender, PaintEventArgs e)
{
if (closing) return;
try
{
if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.
Lost) throw new DeviceLostException();
if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.
NotReset) device.Reset();
// Используем для визуализации всю клиентскую область окна
device.Viewport = Helper.FullScreenViewport(ClientSize);
// Очищаем экран
device.Clear(XnaGraphics.Color.CornflowerBlue);
// Используем квадратную область визуализации
device.Viewport = Helper.SquareViewport(ClientSize);
...
}
}
Результат визуализации круга в окне с неквадратной клиентской областью приведен на рисунке 2.19. Как видно, несмотря на непропорциональное изменение сторон окна, круг остался кругом.
(рис 2.19) Круг, визуализированный с использованием квадратной области визуализации, расположенной в центре формы Практическое упражнение №2.2
Доработайте приложение, визуализирующее график косинуса из практического упражнения №2.1, заменив примитивы PrimitiveType.PointList на PrimitiveType.LineStrip. Так же добавьте в приложение высотную закраску: когда функция принимает значение 1, график должен окрашиваться зеленым цветом, а при -1 – красным. Остальные точки графика принимают промежуточные цвета (рисунок 2.20). Для вычисления цвета промежуточных точек можно воспользоваться следующими формулами:
где
red - красная составляющая цветаgreen - зеленая составляющая цветаПримечание
Если же убрать операцию возведения в степень, то график функции будет заметно терять яркость в окрестностях y равного 0, что смотрится не особо красиво.
(рис 2.20) График синуса с высотной закраской
Для визуализации наборов треугольников с различной топологией в XNA Framework имеется три типа примитивов: PrimitiveType.TriangleList, PrimitiveType.TriangleFan и PrimitiveType.TriangleStrip.Начнем с самого простого примитива, PrimitiveType.TriangleList.
Этот примитив предназначен для визуализации набора несвязанных треугольников: первый треугольник строится с использованием 0-й, 1-й и 2-й вершин, второй треугольник – 3-й, 4-й и 5-й вершин, третий треугольник – 6-й, 7-й и 8-й вершин и т.д. (рисунок 2.21).
(рис 2.21) Треугольники, нарисованные с использованием примитива PrimitiveType.TriangleList По умолчанию XNA Framework отображает на экране только те треугольники, вершины которых расположены на экране по часовой стрелке. К примеру, при визуализации треугольников, изображенных на рисунке 2.21 на экране отобразятся только крайние треугольники ( v0, v1, v2 ) и ( v6, v7, v8 ). А вот средний треугольник ( v3, v4, v5 ) будет отброшен, так как его вершины перечисляются против часовой стрелки. Такое на первый взгляд странное поведение XNA Framework обусловлено особенностью отсечения невидимых треугольников в трехмерных сценах. Однако при визуализации двухмерных изображений эта функциональность оказывается не только излишней, но и вредной. Поэтому разработчики XNA Framework заботливо предусмотрели свойство GraphicsDevice.RenderState.CullMode,
public CullMode CullMode { get; set; }
Это свойство может принимать следующие значения перечислимого типа CullMode:
CullMode.None - отсечение выключеноCullMode.Clockwise - отсекаются треугольники, вершины которых расположены на экране по часовой стрелкеCullMode.CounterClockwise - отсекаются треугольники, у которых вершины расположены на экране против часовой стрелки.По умолчанию свойству GraphicsDevice.RenderState.CullMode присваивается значение CullMode.CounterClockwise, то есть видеокарта отбрасывает все треугольники, у которых вершины расположены против часовой стрелки. Для отключения этой функциональности достаточно присвоить этому свойству значения CullMode.None.
В листинге 2.22 приведен исходный код основных фрагментов программы (Ex12), рисующей в центре экрана треугольник (рисунок 2.22).

(рис 2.22) Треугольник с разноцветными вершинами (рис 2.22) public partial class MainForm : Form { const string effectFileName = "Data \\ColorFill.fx"; GraphicsDevice device = null; PresentationParameters presentParams; Effect effect = null; VertexDeclaration decl = null; VertexPositionColor[] vertices = null; FillMode fillMode=FillMode.Solid; bool closing = false; … private void MainFormLoad(object sender, EventArgs e) … { // Создаем массив для хранения трех вершин треугольника vertices = new GraphicsBuffer<TransformedColored>(3); // Задаем вершины треугольника vertices[0] = new VertexPositionColor(new Vector3(0.0f, 0.4f, 0.0f), 4> XnaGraphics.Color.Coral); vertices[1] = new VertexPositionColor(new Vector3(0.4f, -0.4f, 0.0f), 4> XnaGraphics.Color.LightGreen); vertices[2] = new VertexPositionColor(new Vector3(-0.4f, -0.4f, 0.0f), 4> XnaGraphics.Color.Yellow); } private void MainFormPaint(object sender, PaintEventArgs e) { device.Clear(XnaGraphics.Color.CornflowerBlue); // Выключаем отсечение треугольников device.RenderState.CullMode = Cull.None; device.VertexDeclaration = decl; // Рисуем треугольник 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(); }
Как видно, листинг программы мало чем отличается от предыдущих примеров. Единственное разница заключается в отключении режима отсечения треугольников и использовании примитивов типа PrimitiveType.TriangleList.
Режимы закраски
Как известно, многие приложения 3D моделирования вроде 3ds Max или Maya позволяют отображать сцену в режиме проволочного каркаса ( ). Благодаря этому разработчик может ясно видеть топологию сцены, в частности, взаимное расположение всех треугольников на сцене. XNA Framework тоже поддерживает подобную функциональность, позволяя отображать вместо закрашенных треугольников их проволочный каркас. Управление этой функциональностью осуществляется при помощи свойства RenderState.FillMode класса GraphicsDevice:
FillMode FillMode { get; set; }
Свойство может принимать следующие значения перечислимого типа FillMode:
FillMode.Point - визуализируются только точки, расположенные на вершинах треугольника. Визуализируемые точки являются полноценными точками XNA Framework: к примеру, их размер можно изменять при помощи свойства device.RenderState. PointSize.FillMode.WireFrame - визуализирует каркас треугольника, который рисуется с использование oбычных линий вроде TrianglePrimitive.LineList или TrianglePrimitive.LineStrip.FillMode.Solid - закрашивает внутреннюю область треугольника.По умолчанию свойству RenderState.FillMode присвоено значение FillMode.Solid, то есть треугольники рисуются закрашенными.
Для демонстрации практического использования свойства FillMode мы добавим в нашу программу ( Ex12 ) возможность циклической смены режимов отображения треугольников при помощи клавиши пробел (листинг 2.23).
public partial class MainForm : Form
{
// Режим отображения треугольников
FillMode fillMode=FillMode.Solid;
...
private void MainForm_Paint(object sender, PaintEventArgs e)
{
...
// Выключаем отсечение треугольников
device.RenderState.CullMode = Cull.None;
// Задаем режим отображения треугольников
device.RenderState.FillMode = fillMode;
// Зазаем размер точек
device.RenderState.PointSize = 3.0f;
...
// Рисуем треугольник
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
device.DrawUserPrimitives(PrimitiveType.TriangleList,
vertices, 0, verteces.Length / 3);
pass.End();
}
effect.End();
...
}
private void MainForm_KeyDown(object sender, KeyEventArgs e)
{
// Если нажата клавиша пробел
if (e.KeyCode == Keys.Space)
{
// Изменяем режим отображения switch (fillMode)
{
case FillMode.Point:
fillMode = FillMode.WireFrame; break; case FillMode.WireFrame:
fillMode = FillMode.Solid; break; case FillMode.Solid:
fillMode = FillMode.Point;
break;
}
// Перерисовываем экран
Invalidate();
}
}
}
Узор Серпинского
(рис 2.23) Построение узора Серпинского. Базовый треугольник Перейдем к более сложному примеру. Наше следующее приложение будет строить узор Серпинского путем рекурсивного разбиения треугольника, визуализируемого в каркасном режиме. Построение узора начинается с базового треугольника (рисунок 2.23). На первой интеграции в данный большой треугольник вписывается другой треугольник меньшего размера, вершины которого расположены в середине сторон большого треугольника (рисунок 2.24). В результате большой треугольник оказывается как бы разбит на 4 треугольника. На втором этапе данные в три треугольника, примыкающие к вершинам исходного большого треугольника, вписываются по три треугольника (рисунок 2.25). На третьем этапе в образовавшиеся девять треугольников вписываются уже девять треугольников (рисунок 2.26), на четвертом этапе вписывается уже 27 треугольников и так далее. В идеале процесс должен продолжаться до бесконечности, одна ко на практике вполне можно ограничиться десятком итераций (рисунок 2.27), так как размер треугольников, генерируемых в последующих итерациях, будет уже меньше размера пикселей экрана.

(рис 2.25) Построение узора Серпинского. Первая итерация (рис 2.24) Построение узора Серпинского. Вторая итерация 
(рис 2.27) Построение узора Серпинского. Третья итерация (рис 2.26) Узор Серпинского, 11 итераций Так как приложение будет визуализировать десятки или даже сотни тысяч треугольников, очень важно поместить их в единый массив и вывести одним вызовом метода DrawUserPrimitives. Однако для создания такого массива очень полезно заранее знать количество треугольников, которые будут визуализированы за n итераций. Это поможет нам избежать многочисленных изменений размера массива по мере генерации треугольников. Давайте попробуем найти зависимость числа визуализируемых треугольников от количества интеграций. И так, при нулевом количестве итераций мы визуализируем 1 треугольник. При одной итерации число треугольников становится 1 + 1 = 2. При двух итерациях количество треугольников будет равно 1 + 1 + 3 = 5, при трех 1 + 1 + 3 + 9 = 14. Таким образом, мы можем вывести некоторую общую закономерность для n итераций:
где
tc - количество треугольников, визуализируемых при n итераций.В принципе, это выражение вполне приемлемо, однако знак суммы смотрится не особо красиво. Однако открыв учебник высшей математики вроде можно найти весьма интересное соотношение:
$$\sum_{i-0}^n x^i=\frac{1-x^{n+1}}{1-x} $$Соответственно, выражение 2.4 можно переписать без использования n элементов:
Гораздо более наглядное выражение, не так ли? Однако так как разные видеокарты могут визуализировать разное число треугольников, не исключено, что приложению придется решать и образную задачу. Допустим, мы определим в приложении число итераций ( n ) равным 11, то есть узор Серпинского будет содержать $$\frac{3^{11}+1}2=88574$$ треугольников с общим количеством вершин 88574 ? 3 = 265722. Но ведь некоторые видеокарты могут оказаться не способными визуализировать такое количество треугольников за один присест. Как приложение должно повести себя в подобном случае? Наиболее простое решение – сократить количество интеграций до максимально приемлемого. А для этого нам придется определять максимальное количество итераций ( n ), при котором количество треугольников не превышает заданное значение tc. Для этого выражение (2.6) достаточно переписать как
после чего взять от обоих частей выражения логарифм по основанию 3:
$$n=floor(\log_3(2-tc-1)) $$где
x (то аналог метода Math.Floor из C#).К слову
Согласно выражению 2.8 на компьютере с Intel GMA 900 приложение может выполнить до 9-ти итераций, на NVIDIA NV 2x-3x до 12-ти итераций, а на до 13-ти итераций.
После такого небольшого математического экскурса можно приступать реализации нашего приложения. Визуализация треугольников будет осуществляться в два этапа:
DrawUserPrimitives.Вычисление координат треугольников мы организуем с использованием рекурсивной функции DrawTriangle принимающей в качестве параметров координаты треугольника и количество оставшихся итераций. Эта функция будет помещать в массив вершин координаты текущего треугольника, после чего выполнять деление этого треугольника на три части и вызывать саму себя для этих частей, но уже с уменьшенным количеством оставшихся итераций на 1. Процесс повторяется до тех пор, пока количество оставшихся итераций не достигнет 0.
Исходный код основных фрагментов программы с подробными комментариями приведен в листинге 2.24.
// Примем Examples\Ch02\Ex13
public partial class MainForm : Form
{
// Число итераций для визуализации треугольника Серпинского.
Если видеокарта не способна
// визуализировать такое количество треугольников,
число итераций автоматически уменьшается
// до приемлемого значения
const int n = 15;
const string effectFileName = "Data
\\ColorFill.fx";
GraphicsDevice device = null; PresentParameters presentParams;
Effect effect = null; VertexDeclaration decl = null;
// Массив вершин узора Серпинского
VertexPositionColor[] vertices = null;
// Индекс текущей вершины (глобальная переменная,
используемая при рекурсивном формировании
// узора Серпинского)
int currentVertex;
// Рекурсивная функция, заносящая в массив
vertices информацию о вершинах узора.
// a, b, c - координаты текущего треугольника
// pass - число оставшихся итераций
void DrawTriangle(Vector2 a, Vector2 b, Vector2 c, int pass)
{
// Если это последняя итерация, выходим из функции
if (pass <= 0)
return;
// Уменьшаем количество оставшихся итераций pass -= 1;
// Помещаем в массив вершины треугольника
вписанного в текущий "большой" треугольник
vertices[currentVertex] = new VertexPositionColor
(new Vector3(ab.X, ab.Y, 0.0f),
XnaGraphics.Color.Black);
vertices[currentVertex + 1] = new VertexPositionColor
(new Vector3(ac.X, ac.Y, 0.0f),
XnaGraphics.Color.Black);
vertices[currentVertex + 2] = new VertexPositionColor
(new Vector3(bc.X, bc.Y, 0.0f),
XnaGraphics.Color.Black);
// Увеличиваем индекс текущей вершины currentVertex += 3;
// Вычисляем координаты середины сторон треугольника
Vector2 ab = new Vector2((a.X + b.X) / 2.0f,(a.Y+b.Y)/2.0f);
Vector2 ac = new Vector2((a.X + c.X) / 2.0f,(a.Y+c.Y)/2.0f);
Vector2 bc = new Vector2((b.X + c.X) / 2.0f,(b.Y+c.Y)/2.0f);
// Вызываем этот рекурсивный метод для
образовавшихся трех крайних треугольников, примыкающих
// к углам текущего треугольника
DrawTriangle(a, ab, ac, pass);
DrawTriangle(b, ab, bc, pass);
DrawTriangle(c, ac, bc, pass);
}
private void MainForm_Load(object sender, EventArgs e)
{
...
// Определяем максимальное количество треугольников,
которое текущая видеокарта может
// визуализировать за один присест
int maxTriangleCount = Math.Min(device.GraphicsDevice
Capabilities.MaxPrimitiveCount,
device.GraphicsDeviceCapabilities.MaxVertexIndex / 3);
// Вычисляем по формуле 2.8 максимальное количество
итераций визуализации узора, которые
// можно выполнить на текущей видеокарте
int maxPass = (int) Math.Floor(Math.Log(2 * maxTriangleCount - 1, 3));
// При необходимости уменьшаем количество интеграций,
которое задаются константой n
int passes = Math.Min(n, maxPass);
// Вычисляем по формуле 2.6 количество треугольников,
формирующих данный узор Серпинского.
int triangleCount = ((int)Math.Pow(3, passes) + 1) / 2;
// Выделяем память для хранения информации о вершинах
треугольниках vertices = new VertexPositionColor[3 * triangleCount];
Text += " Количество итераций: " + passes.ToString();
// Вершины начального треугольника
Vector2 a = new Vector2(0.0f, 0.9f); Vector2 b = new
Vector2(-0.9f, -0.9f); Vector2 c = new Vector2(0.9f, -0.9f);
// Обнуляем индекс текущей вершины
currentVertex = 0;
// Заносим в массив вершины самого большого треугольника
vertices[currentVertex] = new
VertexPositionColor(new Vector3(a.X, a.Y, 0.0f),
XnaGraphics.Color.Black); vertices[currentVertex + 1] = new
VertexPositionColor(new Vector3(b.X, b.Y, 0.0f),
XnaGraphics.Color.Black); vertices[currentVertex + 2] = new
VertexPositionColor(new Vector3(c.X, c.Y, 0.0f),
XnaGraphics.Color.Black); currentVertex += 3;
// Выполняет рекурсивное деление треугольника в течении
pass итераций CreateTriangle(a, b, c, passes);
}
private void MainForm_Paint(object sender, PaintEventArgs e)
{
...
// Очищаем экран
device.Clear(ClearFlags.Target, Color.White, 0.0f, 0);
device.BeginScene();
// Отключаем отсечение треугольников
device.RenderState.CullMode = Cull.None;
// Используем каркасную визуализацию треугольников
device.RenderState.FillMode = FillMode.WireFrame;
device.VertexFormat = TransformedColored.Format;
// Рисуем треугольники
device.DrawUserPrimitives(PrimitiveType.TriangleList,
verteces.NumberElements/3, verteces);
device.EndScene();
device.Present();
}
}
}
Практическое упражнение №2.3
Напишите приложение, рисующее обыкновенный деревянный забор, покрашенный (рисунок 2.28). Готовое приложение находится в example.zip в каталоге Ch02\Ex14.
(рис 2.28) Покрашенный деревянный забор
Следующий тип примитивов, PrimitiveType.TriangleFan, используется для рисования вееров треугольников. Первые три вершины (0-я, 1-я и 2-я) задают первый треугольник. Второй треугольник задается 0-й, 2-й и 3-й вершинами, третий – 0-й, 3-й и 4-й вершинами и т.д. (рисунок 2.29). Данный тип примитивов идеально подходит для рисования эллипсов, окружностей, секторов окружностей и аналогичных фигур.
(рис 2.29) Веер треугольников, нарисованный с использованием примитивов PrimitiveType.TriangleFan Чтобы опробовать этот тип примитива на практике, мы модифицируем пример Ex09, заставив его рисовать на экране закрашенный круг вместо окружности (рисунки 2.31). Для этого придется внести три небольших изменения в обработчики событий Load и Paint:
LineList на TriangleFan
(рис 2.30) . Круг, нарисованный при помощи веера из 64-х треугольников (PrimitiveType.TriangleFan)Так же мы добавим в программу возможность переключения между каркасным и закрашенным режимами отображения треугольников при помощи клавиши пробел ( Space ). Эта функциональность, позволяющая просматривать топологию сцены, неоценима при отладке приложения (рисунок 2.31).
(рис 2.31) Круг (веер из 18-ти треугольников), визуализированный в каркасном режиме Основные фрагменты исходного кода полученного приложения ( Ex15 ) приведены в листинге 2.25.
// Количество сегментов в круге
const int slices = 64;
// Режим закраски круга
FillMode fillMode = FillMode.Solid;
private void MainForm_Load(object sender, EventArgs e)
{
...
// Создаем графический буфер
vertices = new VertexPositionColor[slices + 2];
// Помещаем в начало графического буфера
вершину, расположенную в центре экрана
vertices[0] = new VertexPositionColor(new Vector3(0.0f, 0.0f, 0.0f),
XnaGraphics.Color.White);
// Перебираем все вершины окружности for (int i = 0;
i <= slices; i++)
{
// Определяем координаты текущей вершины
float angle = (float)i /
(float)slices * 2.0f * (float)Math.PI; float x = 0.7f *
(float)Math.Sin(angle); float y = 0.7f * (float)Math.Cos(angle);
// Вычисляем цвет вершины
byte red = (byte)(255 * Math.Abs(Math.Sin(angle * 3))); byte green =
(byte)(255 * Math.Abs(Math.Cos(angle * 2)));
// Помещаем информацию о вершине в массив вершин
vertices[i + 1] = new VertexPositionColor(new Vector3(x, y, 0.0f), new
XnaGraphics.Color(red, green, 0)); }; ... }
private void MainForm_Paint(object sender, PaintEventArgs e)
{
...
// Задаем область визуализации размером во весь
экран. Для вычисления параметров видового
// преобразования используется метод
FullScreenViewport нашего вспомогательного класса Helper
device.Viewport = Helper.FullScreenViewport(ClientSize);
// Закрашиваем поверхность формы
device.Clear(XnaGraphics.Color.CornflowerBlue);
// Задаем квадратную область закраски максимально
возможного размера device.Viewport = Helper.SquareViewport(ClientSize);
// Выключаем отсечение треугольников (см. предыдущий раздел)
device.RenderState.CullMode = CullMode.None;
// Задаемрежим визуализации треугольников
device.RenderState.FillMode = fillMode;
device.VertexDeclaration = decl;;
// Рисуем круг
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
device.DrawUserPrimitives(PrimitiveType.
TriangleFan, vertices, 0, vertices.Length - 2);
pass.End(); } effect.End();
// Переключаем вспомогательные буферы
device.Present(); }
// Обработчик события нажатия клавиш
private void MainForm_KeyDown(object sender, KeyEventArgs e)
{
// Если нажата клавиша пробел
if (e.KeyCode == Keys.Space)
{
// Меняем режим отображения
if (fillMode == FillMode.Solid)
fillMode = FillMode.WireFrame; else
fillMode = FillMode.Solid;
// Обновляем изображение Invalidate();
}
}
Последний тип примитивов, PrimitiveType.TriangleStrip, предназначен для рисования полосы из связанных треугольников. При этом первый треугольник проходит через 0-ю, 1-ю 2-ю вершины, второй треугольник – через 3-ю, 2-ю и 1-ю вершины, третий треугольник – через 2-ю, 3-ю и 4-ю вершины, четвертый через 5-ю, 4-ю и 3-ю, и т.д (рисунок 2.32). Обратите внимание на порядок перечисления вершин в треугольниках: все треугольники в полосе имеют одинаковую ориентацию относительно часовой стрелки – например, если вершины первого треугольника располагаются по часовой стрелке, то и вершины других треугольников так же будут перечисляться по часовой стрелке. Эта особенность используется при отсечении невидимых треугольников с использованием поля RenderState.CullMode.
(рис 2.32) Визуализация полосы связанных треугольников Данный тип примитивов очень удобно использовать для визуализации ломаных линий шириной больше одного пикселя, то есть в качестве продвинутой версии примитива PrimitiveType.LineStrip. В частности, на рисунке 2.32 в качестве иллюстрации приведена ломаная линия переменной ширины, состоящая из пяти сегментов.
Визуализация графика функции y=cos(x)
Чтобы попрактиковаться в рисовании ломаных линий при помощи примитива PrimitiveType.TriangleStrip, мы напишем приложение, визуализирующее график косинуса в интервале с $$0°…720° (0.4 \pi \ в \ радианах)$$ использованием ломаной толщиной 10 пикселей. Хотя на первый взгляд эта задача не намного сложнее практический упражнений №2.1 и №2.2, она все же имеет несколько подвохов.
Для начала сформулируем задачу более четко. Нам необходимо построить полосу из связанных треугольников, аппроксимирующую график косинуса, центр которой совпадает с графиком косинуса (рисунок 2.33).
(рис 2.33) Полоса из связанных треугольников, аппроксимирующая график косинуса (тонкая линию, проходящая по центру полосы).Для построения синусоиды мы будем перебирать точки графика косинуса с определенным шагом. На рисунке 2.30 эти точки обозначены как $$p_0, p_1, p_2$$ и т.д. Отступив симметрично по обе стороны от точки p0 на некоторое расстояние, например, на 0.05 единиц, мы получим две вершины $$v_0$$ и $$v_1$$, расстояние межу которыми равно 10 пикселей. Проделав аналогичную операцию над остальными точками, мы получим пары вершин ( $$v_2–v_3, v_4–v_5, v_6–v_7, …, v_{2n}n–v_{2n+1}$$ ), расстояние между которыми равно 0.1 единиц. И, наконец, построив полосу из треугольников, опирающуюся на вершины $$v_0, v_1, v_2, v_3$$, мы получим ломаную линию толщиной 0.1 пикселей, точно аппроксимирующую график косинуса (рисунок 2.34).
(рис 2.34) Построение полосы из треугольников По ширине график косинуса будет вписан в клиентскую область окна, а по высоте наш график будет немного меньше высоты окна. Таким образом, в действительности наше приложение будет визуализировать не сам график $$y=\cos(x)$$, а несколько другую функцию, полученную путем масштабирования графика $$y=\cos(x)$$ вдоль осей X и Y:
где
[-1, +1] . Фактически это координата x графика, пробегающая с определенным шагом значения от левого до правого краев экрана, то есть от -1 до +1. Соответственно аргумент функции косинуса пробегает значения от 0 до 4 • 71 (0°...720°).[-0.7, +0.7] .Перебирая с определенным шагом значения координаты X от -1 до +1 и подставляя их в выражение (2.9), мы получим координаты набора точек $$p_0, p_1, … , p_n$$ (рисунок 2.34). Как говорилось выше, для получения координат вершин полосы треугольников $$v_0, v_1, …, v_{2n}, v_{2n+1}$$ необходимо симметрично отупить от точек $$p_0 … p_n$$ на 0.05 единиц.
Вроде бы все просто и понятно, если не считать одной мелочи: мы пока не еще определились, каким образом должны быть сориентированы отрезки $$v_0-v_1, v_2-v_3, …, v_{2n}-v_{2n+1}$$ относительно точек $$p_0, p_1, … , p_n$$. Не мудрствуя лукаво, мы сделаем эти отрезки параллельными оси Y и посмотрим, что из этого выйдет:
и т.д.
Основные фрагменты приложения (Ex16) приведены в листинге 2.26.
public partial class MainForm : Form
{
// Количество сегментов в ломаной линии,
аппроксимирующей график косинуса
const int QuadStrips = 100;
// Число треугольников в ломанной линии
const int TriStrips = QuadStrips * 2;
// Массив вершин
VertexPositionColor[] vertices = null;
// Режим закраски треугольников
FillMode fillMode = FillMode.Solid;
private void MainForm_Load(object sender, EventArgs e)
{ ...
// Создаем массив вершин для хранения вершин
полоски из треугольников vertices = new
пVertexPositionColor[TriStrips+2];
// Перебираем вершины полоски из треугольников
for (int i = 0; i <= QuadStrips; i++)
{
// Определяем текущее значение координаты x вершины
float x = -1.0f + 2.0f * (float) i / (float) QuadStrips;
// Вычисляем значение косинуса, соответствующее координате x
float angle = 2.0f * (float)Math.PI * x;
float cos = (float)Math.Cos(angle);
// Вычисляем значение координаты y вершины по формуле 2.9
float y = 0.6f * cos;
// Вычисляем красную и зеленую составляющую
цвета по формулам 2.3 (см. практическое
// упражнение 2.2)
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);
// Заносим в массив координаты вершины v[i*2] (см. выражение 2.10)
vertices[i * 2] = new VertexPositionColor(new
Vector3(x, y - 0.05f, 0.0f), new
XnaGraphics.Color(red, green, 0));
// Заносим в массив координаты вершины v[i*2+1]
vertices[i * 2 + 1] = new VertexPositionColor
(new Vector3(x, y + 0.05f, 0.0f), new
XnaGraphics.Color(red, green, 0));
};
}
private void MainForm_Paint(object sender, PaintEventArgs e) {
device.Clear(XnaGraphics.Color.DarkSlateGray);
// Выключаем отсечение невидимых треугольников
device.RenderState.CullMode = CullMode.None;
// Задаем режим показа треугольников
device.RenderState.FillMode = fillMode;
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();
}
}
Скомпилируйте и запустите приложение на выполнение. Как и ожидалось, на экране появится график функции y=cos(x) , однако толщина графика будет переменной, причем максимальная толщина графика будет достигаться в окрестностях точек, в которых функция cos(x) принимает значения -1 или 1. (рисунок 2.35).
Примечание
Следующий материал этого раздела содержит довольно много математических выкладок, поэтому если вы не в ладах с математикой, можете смело пропустить оставшуюся часть раздела 2.6.3.
(рис 2.35) График функции y=cos(x) переменной толщины График y=cos(x) постоянной толщины
И так, попытка использования отрезков $$v_0–v_1, …, v_{2n}–v_{2n+1}$$ параллельных оси Y закончилась неудачей. Ну что ж, отрицательный результат, это тоже результат. Попробуем поэкспериментировать ориентацией отрезков $$v_0–v_1, …, v_{2n}–v_{2n+1}$$, например, развернув их под углом 45° (рисунок 2.36):
// Вычисляем вектор смещения вершин v[2*i+1] относительно p[n] float nx = (float) (5.0 / Math.Sqrt(2.0)); float ny = nx; // Симметрично смещаем вершины на 5 пикселей в направлении векторов (-1, -1) и (+1, +1) 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));
(рис 2.36) График функции y=cos(x) переменной толщины. Вершины смещаются в направлении векторов (-1, -1) и (+1, +1)Проведя несколько экспериментов, мы придем к выводу, что график косинуса имеет необходимую толщину только в там, где отрезки $$v_{2i} –v_{2*i+1}$$ перпендикулярны графику косинуса. Следовательно, чтобы график функции имел постоянную толщину 0.1 единиц, все отрезки $$v_{2i} –v_{2*i+1}$$ должны быть перпендикулярны графику косинуса.
Для нахождения координат вершин отрезка $$v_{2i} –v_{2*i+1}$$ длиной 0.1 единиц, проходящего через точку $$p_i$$, перпендикулярно графику функции необходимо выполнить следующие действия:
s перпендикулярный графику функции в точке pi.pi вектора $$\ovepline n $$ и $$- \ovepline n $$.Рассмотрим эти шаги более подробно. Как вы знаете из курса аналитической геометрии, вектор, перпендикулярного графику функции, определяются по формуле:
$$\overline S=(\frac{df(x,y)}{dx}*\frac{df(x,y)}{dy}) $$где
Чтобы определить значение вектора s для нашей функции (2.9), перепишем ее в неявной форме $$f(x, y)=0$$:
$$y-0.6*\cos(2*\pi*x)=0 $$Теперь найдем частные производные, являющиеся координатами вектора $$s:$$
$$\overline S=\overline{(S_x,S_y)}// S_x=\frac {d(y-0.6*\cos(2*\pi*x))}{dx}=1.2*\pi*\sin(2*\pi*x)// S_x=\frac {d(y-0.6*\cos(2*\pi*x))}{dy}=1 $$Нахождение производных в среде MathCAD
Если вы немного подзабыли высшую математику, не огорчайтесь. Для нахождения производных можно воспользоваться, к примеру, математическим пакетом MathCAD.
Для вычисления значения производной средствами символьной математики пакета MatCAD просто наберите выражение производной, которую выходите вычислить. Затем введите специальный символ ? (Ctrl + .) и нажмите Enter, после чего справа от выражения появится вычисленное значение производной (рисунок 2.37)
(рис 2.37) Математический пакет MathCAD Зная вектор $$\overline s$$ можно легко найти вектор $$\overline n$$ заданной длины, параллельный вектору $$\overline s$$:
$$\overline n=\alpha*\frac{\overline S}{|\overline S|} $$где
После этого определить координаты точек $$v_2i$$ и $$v_{2i+1}$$ не составит труда:
$$V_{2-i}=P_i-\overline n// V_{2-i+1}=P_i+\overline n $$Имея под рукой формулы 2.13, 2.14 и 2.15 мы можем легко исправить ошибку в примере Ex16 (визуализация графика функций переменной толщины вместо постоянной) путем небольшой модификации фрагмента обработчика события Paint (листинг 2.27). Полная версия приложения находится в example.zip в каталоге Ex02\Ex17
for (int i = 0; i <= QuadStrips; i++)
{
float x = -1.0f + 2.0f * (float) i / (float) QuadStrips;
float angle = 2.0f * (float)Math.PI * x;
float cos = (float)Math.Cos(angle);
float y = 0.6f * cos;
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);
// Вычисляем вектор s
float sx = (float)(1.2 * Math.PI * Math.Sin(2.0 * Math.PI * x));
float sy = 1.0f;
// Вычисляем длину вектора s
float length = (float)Math.Sqrt(sx * sx + sy * sy);
// Вычисляем вектор nx
float nx = sx / length * 0.05f;
float ny = sy / length * 0.05f;
// Заносим в графический буфер координаты
вершин v[i*2] и v[i*2+1] (вычисляются по формуле
// 2.15)
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));
};
(рис 2.38) График функции y=cos(x) постоянной толщины Все изменения обработчика события Paint сводятся к добавлению пяти новых строк кода и косметической правке двух строк. Как говорится, дело в количестве строк, а в математических формулах, которые заложены в эти строки. Результат работы приложения приведен на рисунке 2.38.
Визуализация CD-диска
Область применения полос из треугольников не ограничивается визуализацией полосок определенной толщины, ведь примитивы PrimitiveType.TriangleStrip активно используются для визуализации самых разнообразных геометрических объектов. В листинге 2.28 приведены основные фрагменты исходного кода примера, визуализирующий на экране CD диск (рисунок 2.39).

(рис 2.28) CD-диск, нарисованный с использованием полосы из 200 треугольников (рис 2.39) public partial class MainForm : Form { // fx-файл const string effectFileName = "Data\\ColorFill.fx"; // Количество сегментов в диске const int slices = 100; // Радиус внутренней границы CD-диска const float innerRadius = 0.2f; // Радиус внешней границы CD-диска const float outerRadius = 0.7f; // Режим закраски треугольников FillMode fillMode = FillMode.Solid; ... private void MainForm_Load(object sender, EventArgs e) { ... // Выделяем память для хранения вершин диска vertices = new VertexPositionColor[slices * 2 + 2]; // Перебираем вершины CD диска for (int i = 0; i <= slices; i++) { // Вычисляем текущий угол ? float angle = (float)i / (float)slices * 2.0f * (float)Math.PI; // Вычисляем вспомогательные переменные float sin = (float)Math.Sin(angle); float cos = (float)Math.Cos(angle); // Вычисляем координаты вершины внутренней границы CD диска float x = innerRadius * sin; float y = innerRadius * cos; // Добавляем вершину внутренней границы диска в массив вершин vertices[i * 2] = new VertexPositionColor(new Vector3(x, y, 0.0f), XnaGraphics.Color.White); // Вычисляем координаты вершины внешней границы CD диска x = outerRadius * sin; y = outerRadius * cos; // Вычисляем цвет вершины byte red = (byte)(255 * Math.Abs(Math.Sin(angle * 3))); byte green = (byte)(255 * Math.Abs(Math.Cos(angle * 2))); // Добавляем вершину внешней границы диска в массив вершин vertices[i * 2 + 1] = new VertexPositionColor(new Vector3(x, y, 0.0f), new XnaGraphics.Color(red, green, 0)); }; } private void MainForm_Paint(object sender, PaintEventArgs e) { ... device.Viewport = Helper.FullScreenViewport(ClientSize); device.Clear(ClearFlags.Target, Color.CornflowerBlue, 0.0f, 0); device.Viewport = Helper.SquareViewport(ClientSize); device.RenderState.CullMode = CullMode.None; device.RenderState.FillMode = fillMode; device.VertexDeclaration = decl; // Рисуем CD диск 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(); } ... } // Обработчик событий от клавиатуры, переключающий режимы закраски при нажатии клавиши Space // (Пробел) private void MainForm_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Space) { if (fillMode == FillMode.Solid) fillMode = FillMode.WireFrame; else fillMode = FillMode.Solid; Invalidate(); } } }
Нетрудно заметить, что пример Ex18 является тривиальной модификацией примера Ex09 из раздела 2.5.2 (круг, переливающийся разнообразными цветами). CD-диск рисуется при помощи замкнутой полосы из треугольников,
(рис 2.40) CD-диск, состоящий из 18-ти сегментов (36 треугольников), визуализированный в каркасном режиме Визуализация квадрата с круглым отверстием
В следующем примере ( Ex19 ) мы нарисуем более интересное изображение: квадрат, внутри которого вырезана дырка в виде окружности (рисунок 2.41). Для упрощения задачи в качестве отправной точки будет использоваться исходный код примера Ex18.
(рис 2.41) Квадрат с круглым отверстием На первый взгляд между квадратом и кругом практически нет ничего общего: граница круга ограничивается окружностью, а граница квадрата – четырьмя отрезками равной длины. Однако вспомним аналитическую геометрию. Граница квадрата, стороны которого расположены под углом 45° к осям X и Y, может быть описана следующими формулами:
где
Math.Sign в .NET.Таким образом, для визуализации повернутого на 45° квадрата, в центре которого вырезано круглое отверстие, необходимо всего лишь немного подправить пример Ex19, подкорректировав формулу для расчета внешней границы CD диска (листинг 2.39).
for (int i = 0; i <= slices; i++)
{
float angle = (float)i / (float)slices * 2.0f * (float)Math.PI;
float sin = (float)Math.Sin(angle);
float cos = (float)Math.Cos(angle);
// Вычисляем вспомогательные переменные,
используемые при расчете координат вершин границы
// квадрата
float sin2 = sin * sin * Math.Sign(sin);
float cos2 = cos * cos * Math.Sign(cos);
// Вычисляем координаты вершин внутреннего
круглого отверстия и заносим в массив вершин
float x = innerRadius * sin;
float y = innerRadius * cos;
vertices[i * 2] = new VertexPositionColor(new
Vector3(x, y, 0.0f), XnaGraphics.Color.White);
// Вычисляем координаты внешней границы квадрата по формуле 2.16.
x = outerRadius * sin2;
y = outerRadius * cos2; // Вычисляем цвет вершины
byte red = (byte)(255 * Math.Abs(Math.Sin(angle * 3)));
byte green = (byte)(255 * Math.Abs(Math.Cos(angle * 2)));
// Заносим в информацию о вершине в массив вершин
vertices[i * 2 + 1] = new VertexPositionColor(new
Vector3(x, y, 0.0f), new
XnaGraphics.Color(red, green, 0));
};
(рис 2.42) Повернутый на 45° квадрат с отверстием, визуализированный в каркасном режиме. Фигура построена с использованием полосы из 40-ка треугольников Результат работы примера Ex21 приведен на рисунке 2.42. Теперь, нам необходимо развернуть эту фигуру на 45° относительно геометрического центра фигуры. Из курса аналитической геометрии вам должно быть известно, что при повороте изображения на угол $$\varphi$$ относительно точки с координатами x0, y0, координаты всех точек изображения трансформируются по следующим формулам:
где
$$x_0, y_0$$ - координаты точки, относительно которой выполняется поворот
$$\qquard x',\qquard y'$$ - старые координаты точки
$$\varphi$$ - угол поворота
Подставив вместо $$\qquard x'$$ и $$\qquard y'$$ выражения 2.16, а так же положив угол ( $$р$$ равным 45 $$\deg$$, мы получим следующие формулы, задающие координаты точек квадрата:
$$x=x_0+\frac{r}{\sqrt 2}*(\cos^2 \alpha*sign(\cos\alpha)+\sin^2*sign(\cos\alpha)) y=y_0+\frac{r}{\sqrt 2}*(\cos^2 \alpha*sign(\cos\alpha)-\sin^2*sign(\cos\alpha)) $$Модифицируем пример Ex19, изменив вычисление координат вершин внешней границы квадрата согласно выражению 2.18, и запустим программу на выполнение. В результате мы получим любопытную картину: в процессе поворота квадрата на 45° полоса треугольников как бы завинчивается по спирали. (рисунок 2.39) Это обусловлено тем, что мы повернули только внешнюю сторону полосы треугольников, забыв о внутренней стороне, имеющую форму круга. Для поворота круглого отверстия в квадрате на 45 $$\deg$$ мы скомбинируем выражения 2.17 и 2.2:
(рис 2.39) Квадрат, повернутый относительно внутреннего отверстия. Каркасный режим После доработки приложения согласно выражению 2.19 изображение наконец-то примет нормальный вид (рисунки 2.37 и 2.40). Исправленный код с учетом выражений 2.18 и 2.19 приведен в листинге 2.40 (Ex20):

(рис 2.40) Квадрат с круглым отверстием, визуализированный в каркасном режиме (рис 2.44) // Несколько увеличиваем размер квадрата и отверстия const float innerRadius = 0.285f; const float outerRadius = 1.0f; // Константа, используемая при расчете вершин квадрата readonly float sqrt2 = (float)Math.Sqrt(2.0); for (int i = 0; i <= slices; i++) { float angle = (float)i / (float)slices * 2.0f * (float)Math.PI; float sin = (float)Math.Sin(angle); float cos = (float)Math.Cos(angle); float sin2 = sin * sin * Math.Sign(sin); float cos2 = cos * cos * Math.Sign(cos); float x = innerRadius /sqrt2 * (cos + sin); float y = innerRadius / sqrt2 * (cos - sin) ; vertices[i * 2] = new VertexPositionColor(new Vector3(x, y, 0.0f), XnaGraphics.Color.White); x = outerRadius * outerRadius/sqrt2 * (cos2 + sin2); y = outerRadius * outerRadius / sqrt2 * (cos2 - sin2); byte red = (byte)(255 * Math.Abs(Math.Sin(angle * 3))); byte green = (byte)(255 * Math.Abs(Math.Cos(angle * 2))); vertices[i * 2 + 1] = new VertexPositionColor(new Vector3(x, y, 0.0f), new XnaGraphics.Color(red, green, 0)); };
Практическое упражнение №2.4
Выражения 2.18 и 2.19 являются частными случаями так называемой суперокружности, координаты точек которой задаются формулами:
$$x=x_0+\frac{r}{\sqrt2}*(|\cos\alpha|^n*sign(\cos\alpha)+|\sin\alpha|^n*sign(\cos\alpha)) y=y_0+\frac{r}{\sqrt2}*(|\cos\alpha|^n*sign(\cos\alpha)-|\sin\alpha|^n*sign(\cos\alpha)) $$где
$$x, y$$ - координаты текущей точки.
$$x_0, y_0$$ - координаты центра суперокружности.
$$r$$ - радиус суперокружности.
$$n$$ - степень суперокружности
$$\alpha$$ - угол, пробегающий с некоторым шагом все значения от 0° до 360°.
При n=1, суперокружность приобретает форму обычной окружности (формула 2.19), а при n=2 - квадрата (формула 2.18). При других n суперокружность приобретает различные интересные формы.
Напишите приложение, визуализирующую фигуру, имеющую форму суперокружности, степень n которой равна 2.5. При этом в центре этой фигуры должно быть вырезано отверстие в форме суперокружности степени 1.3 (рисунок 2.45).
Если быть более точным, формула 2.20 задает суперокружность, повернутую на 45 градусов
(рис 2.45) Фигура, границы которой задаются двумя суперокружностями. Внешняя суперокружность имеет степень 2.5, а внутренняя 1.3.Практическое упражнение №2.5
Создайте приложение, визуализирующее фигуру Лиссажу, координаты точек которой задаются следующей формулой:
$$x = \sin(2*\alpha) y = \cos(3*\alpha) $$где
$$x, y$$ - координаты текущей точки
$$\alpha$$ - угол, пробегающий с определенным шагом значения от 0° до 360° (0…2· $$\pi$$ )
(рис 2.46) Фигура Листажу График функции должен быть визуализирован с использованием линии толщиной 10 пикселей (рисунок 2.46).
На всякий случай, ниже приведены математические выкладки, которые могут облегчить написание программы.
Для построения фигуры Лиссажу, вписанной в форму с небольшими отступами по краям, выражение 2.21 необходимо преобразовать к следующему виду:
$$x = 0.85*\sin(2-\alpha)\\ y = 0.85*\cos(3-\alpha) $$Для нахождения вектора касательной в точке x, y достаточно найти производные по а:
где
$$k$$ - вектор касательной
$$k_x, k_y$$ - компоненты этого вектора
Зная вектор касательной, можно легко найти вектор, перпендикулярный графику функции:
$$\overline{s}=\overline{(k_y,-k_x)}\\ s_x=k_y=-2.55*\sin(3*\alpha)\\ s_y=-k_x=-1.7*\cos(2*\alpha) $$Имея перпендикуляр к графику функции ( s ), вы сможете легко найти координаты вершин полосы из треугольников (по аналогии примером построения графика синуса из начала раздела).
Имея перпендикуляр к графику функции ( s ), вы сможете легко найти координаты вершин полосы из треугольников (по аналогии примером построения графика синуса из начала раздела).
В отличие от универсальных графических библиотек вроде GDI, XNA Framework поддерживает визуализацию весьма ограниченного набора примитивов: точки, отрезки и треугольники. Любой визуализируемый объект, какой сложной формы он не был, всегда аппроксимируется набором этих примитивов. Хотя подобный подход заметно усложняет жизнь разработчика, он позволяет достичь беспрецедентного уровня производительности (порядка нескольких сотен миллионов визуализированных примитивов в секунду).
Примитивы в процессе визуализации обрабатываются специализированными вершинными и пиксельными процессорами видеокарты. Вершинные процессоры предназначены для преобразования вершин, а пиксельные – для закраски примитивов. В DirectX и XNA Framework вершинные и пиксельные процессоры программируются с использованием семейства ассемблеро-подобных языков и . Каждый из этих языков оптимизирован под определенный тип . Например, язык был специально разработан для программирования пиксельных процессоров NV25 ( GeForce4 ). При загрузке шейдера программа, написанная на ассемблеро-подобном языке автоматически компилируется в машинный код текущего ; таким образом, языки и очень похожи на промежуточный язык IL в .NET.
По мере роста функциональности вершинных и пиксельных процессоров появилась потребность в языке программирования высокого уровня. Таким языком стал HLSL ( High Level ) – C-подобный язык программирования, предназначенный для программирования вершинных и пиксельных процессоров. Программа, написанная на HLSL, компилируется в промежуточный ассемблеро-подобный язык с использованием профиля, указывающего под какой ускоритель необходимо оптимизировать генерируемый код. Так, профиль ps_2_a указывает, что шейдер будет транслироваться в язык и оптимизирован для семейства NV3x (GeForce FX). Как правило, чем выше номер профиля, тем большая функциональность доступна программисту, однако платой за эту гибкость является рост требований приложения к функциональности (см. приложение 2).
Процесс визуализации изображений в XNA Framework ощутимо отличаются от подхода, используемого в "классических" двухмерных библиотеках вроде GDI/GDI+. В XNA Framework все графические построения осуществляются с использованием простых фигур, называемых графическими примитивами. XNA Framework поддерживает три вида графических примитивов: точки, отрезки и треугольники. Эти примитивы очень просты в отображении, поэтому все современные NVIDIA GeForce 8800 GTX может визуализировать порядка 400 миллионов треугольников в
Каждый примитив задается набором вершин: точка – одной в центре точки, отрезок двумя – вершинами на концах отрезка, а треугольник – тремя вершинами в углах треугольника. В XNA Framework координаты вершин обычно задаются тремя координатами x, y и z. Центр используемой системы координат расположен в центре клиентской области формы, ось положительное направление оси X направлено вправо, ось Y – вверх, а ось Z – из экрана монитора на наблюдателя (рисунок 2.1). Левый нижний угол формы имеет координаты (-1, -1, 0), верхний правый (+1, +1, 0).
(рис 2.1) Система координат клиентской области формы при использовании XNA Framework В пространстве имен Microsoft.Xna.Framework.Graphics имеется ряд структур для хранения информации о вершинах примитива. В настоящее время для нас наиболее интересна структура VertexPositionColor, инкапсулирующая информацию о координатах и цвете вершины. Начнем рассмотрение этой структуры с конструктора:
public VertexPositionColor(Vector3 position, Color color);
где
position - координаты вершины;color - цвет вершины.В процессе создания новой вершины ширина и высота автоматически заносятся конструктором в поля Position и Color структуры VertexPositionColor:
public Color Color; public Vector3 Position;
Здесь мы впервые встречаемся с новой для нас структурой Microsoft.XNA.Framework.Vector3, инкапсулирующей трехмерный вектор. Наряду с Vector3 в XNA Framework определены структуры Vector2 и Vector4, которые, как нетрудно догадаться, предназначены для работы с двухмерными и четырехмерными векторами. Структуры Vector2, Vector3, Vector4 широко используются в XNA Framework для хранения координат вершин, а так же выполнения различных математических векторных операций. Так как большая часть функциональности данных структур нам пока не нужна, мы отложим их подробное изучение до пятой лекции. В конец концов, с точки зрения логики работы наших первых приложений структуры Vector2, Vector3 и Vector4 представляют собой всего лишь расширенную версию структуры System.Drawing.PointF.
Информация обо всех вершинах примитива хранится в массиве. Например:
// Вершин примитива VertexPositionColor[] vertices;
Казалось бы, все должно быть очень просто, если бы не один нюанс. Дело в том, что при визуализации
примитивов информация о вершинах напрямую передается в графический процессор видеокарты ( - ), который не имеет ни малейшего понятия об управляемом коде и, соответственно,
формате структуры. Для разъяснения графическому процессору формата отдельных полей структуры
применяются декларации формата вершины. В XNA Framework декларация вершины инкапсулируется
классом VertexDeclaration, конструктор которого приведен ниже:
public VertexDeclaration(GraphicsDevice graphicsDevice, VertexElement[] elements);
где
graphicsDevice - графическое устройство, используемое для работы с вершинамиelements - массив элементов (структур VertexElement ) c описанием формата структуры.Описание формата структуры задается массивом elements, каждый элемент которого описывает одно поле структуры. Соответственно, количество элементов в массиве elements всегда равно количеству полей структуры. Какая информация содержится в каждом элементе массива elements? Это:
Создание массива, описывающего структуру, является довольно монотонной и утомительной операцией. К счастью, разработчики XNA Framework встроили в структуру VertexPositionColor (а так же во все аналогичные структуры) статическое поле только для чтения, содержащее массив с описанием этой структуры:
public static readonly VertexElement[] VertexElements;
Соответственно, для создания декларации вершины приложению достаточно лишь передать это поле в качестве второго параметра конструктора класса VertexDeclaration.
Рассмотрим основные этапы визуализации примитивов, информация о вершинах которых хранится в
массиве структур VertexPositionColor. Сначала приложение должно создать декларацию вершины на основе описания, содержащегося в нашей структуре VertexTransformedPositionColor:
VertexDeclaration decl; … decl = new VertexDeclaration(device, VertexTransformedPositionColor.vertexElements);
Эту операцию достаточно выполнять один раз при запуске приложения, например, где-нибудь в обработчике события Load формы.
Код визуализации примитива следует поместить в обработчик события Paint. Перед тем, как приступить к визуализации примитивов, необходимо задать формат вершин примитива, присвоив свойству VertexDeclaration класса Device декларацию формата вершины, созданную в обработчике события Load.
Собственно визуализация примитивов выполняется методом DrawUserPrimitives:
DrawUserPrimitives<T>(PrimitiveType primitiveType, T[] vertexData, int vertexOffset, int primitiveCount);
где
primitiveType - тип примитива, задаваемый с использованием перечислимого типа PrimitiveType. Различные типы примитивов будут подробно рассмотрены в разделах 1.2.1, 1.2.2 и 1.2.3. Пока же отметим, что в XNA Framework поддерживает шесть типов примитивов: список точек ( PrimitiveType.PointList ), список линий ( PrimitiveType.LineList ), полоса линий ( PrimitiveType.LineStrip ), список треугольников ( PrimitiveType.TriangleList ), полоса треугольников ( PrimitiveType.TriangleStrip ) и веер треугольников ( PrimitiveType.TriangleFan ).vertexData - массив вершин примитива.vertexOffset - смещение от начала массива. Данный параметр обычно равен нулю. Ненулевые значения применяется, когда визуализируемый примитив использует примитивы лишь из части массива (например, вершины разных примитивов хранятся в одном большом общем массиве).primitiveCount - количество примитивов, которые будут визуализированы.Резюмируем все вышесказанное. Для визуализации примитива приложение должно выполнить следующие шаги:
Load.Paint первым делом необходимо очисть экран методомGraphicsDevice.Clear.Load ).GraphicsDevice. VertexDeclaration декларацию, созданную на втором этапе.GraphicsDevice.DrawUserPrimitives.Device.Present.Однако, это еще не все. Дело в том, что все современные содержан специализированные DrawUserPrimitives будет не DrawUserPrimitives без явного задания вершинных и пиксельных шейдеров привет к генерации исключения System.InvalidOperationException с сообщением Both a valid
В этом разделе мы познакомимся с языком High Level (язык высокого уровня для программирования шейдеров), или сокращенно HLSL. HLSL используется для программирования вершинных и пиксельных процессоров DirectX. Правда шейдеры DirectX 8 имели множество ограничений и программировались на низкоуровневом ассемблеро-подобном языке, однако в 9-й версии DirectX возможности шейдеров значительно возросли, что привело к появлению надобности в языках высокого уровня. Было создано несколько языков высокого уровня для написания шейдеров HLSL, входящий в состав DirectX 9. В XNA Framework шейдеры так же пишутся на языке HLSL, а сам XNA Framework при работе с шейдерами на платформе Windows в значительной степени опирается на функциональность DirectX.
Так язык HLSL тесно связан с архитектурой графического процессора, мы начнем этот раздел с знакомства с основами архитектуры современного графического процессора.
В разделе 2.2 вы получили представлении о визуализации примитивов средствами XNA Framework. При этом собственно процесс визуализации изображения (метод GraphicsDevice.DrawUserPrimitives ) оставался для нас черным ящиком. Настало время наверстать упущенное. Итак, при вызове метода GraphicsDevice.DrawUserPrimitives вершины из графического буфера поступают на обработку в графический конвейер XNA Framework, представляющий собой последовательность ступеней (простых операций), выполняемых над вершинами в определенном порядке (рисунок 2.2). Рассмотрим эти ступени в порядке выполнения:
(рис 2.2) Упрощенная схема графического конвейера clip space. Однородные координаты вершины определяются четырьмя числами: (x, y, z, w). Перевод однородных координат в обычные геометрические осуществляется путем деления первых трех компонентов на четвертый компонент w: (x/w, y/w, z/w. Например, вершине с однородными координатами (1, 2, 3, 4) в трехмерном пространстве соответствует точка с координатами (1/4, 2/4, 3/4) = (0.25,0.5,0.75) Использование четвертого компонента обусловлено рядом особенностей алгоритмов визуализации трехмерных изображений, используемых в 3D графике. При визуализации двухмерных изображений компонент w обычно полагают равным 1. В этом случае нижнему левому углу клиентской области формы соответствует точка с координатами (-1, -1, 0, 1), правому верхнему углу клиентской области - (1, 1, 0, 1), а центру клиентской области - соответственно (0, 0, 0, 1).На следующей ступени графического конвейера видеокарта производит преобразование координат вершины из логической системы координат в оконную. По-умолчанию координаты трансформируются таким образом, чтобы растянуть изображение на всю поверхность элемента управления. В большинстве случаев этот процесс полностью прозрачен для приложения.
Примечание
DirectX позволяет программисту задавать координаты вершин в оконных координатах. В этом случае, при вызове метода Device.DrawUserPrimitives вершины сразу поступают на третью стадию графического конвейера, минуя первую и вторую стадии. Managed DirectX и XNA Framework Beta 1 позволяют задавать координаты в оконной системе координат, однако начиная с XNA Framework Beta 2 эта функциональность почему-то пропала. По видимости, это обусловлено стремлением сделать XNA Framework как можно более платформо-независимым.
GraphicsDevice.DrawUserPrimitives. Так при использовании параметра PrimitiveType.TriangleStrip вершины трактуются, как опорные точки (вершины) полосы треугольников. При этом каждый треугольник из полосы является независимым примитивов и обрабатывается независимо от других треугольников этой полосы. Полосы треугольников подробно будут рассмотрены в разделе 2.6.3.Следующий этап – обработка пикселей пиксельным процессором с использованием программы, называемой пиксельным шейдером. На вход пиксельному процессору подаются параметры пикселя (цвет, текстурные координаты и т.д.), полученные путем интерполяции соответствующих вершинных параметров вдоль поверхности примитива. После обработки входных параметров, пиксельный процессор возвращает цвет пикселя.
Технические подробности
В современных графических процессорах имеется массив вершинных и пиксельных процессоров, что позволяет им одновременно обрабатывать несколько вершин и пикселей. Так графический процессор NV40 корпорации NVIDIA, используемый в видеокартах семейства GeForce 6800, имеет 6 вершинных и 16 пиксельных процессоров, соответственно, он может параллельно обрабатывать до 6-ти вершин и 16-ти пикселей.
В заключении стоит отметить, что этот логический конвейер DirectX не обязательно соответствует физической организации видеокарты. К примеру, видеокарта NVIDIA GeForce 8800 GTX, основанная на , содержит 8 универсальных блоков, которые могут выполнять как вершинные, так и пиксельные шейдеры .
После прочтения этого раздела у вас, возможно, сложились несколько сумбурные представления о графическом конвейере. Ничего страшного – в следующем разделе вы познакомитесь с языком HLSL и напишете несколько шейдеров, после чего все встанет на свои места.
Дополнительная информация Все современные графические подсистемы построены по принципу конвейера. Идея конвейера, впервые реализованная Генри Фордом, заключается в следующем: если сложный процесс разбить на
последовательность простых операций (конвейер), то на выходе конвейера мы получим производительность равную производительности самой медленной операции в этой цепочке. В качестве примера конвейера рассмотрим процесс производства популярных процессоров Стороннему наблюдателю может показаться, что за день один конвейер производит около 7-ми процессоров (400/60). Но в реальности, между поступлением заготовки процессора и выходом готового процессора по-прежнему проходит два месяца. Это явление получило название латентность конвейера. При нормальном функционировании конвейера на это обстоятельство можно не обращать внимания; однако в случае неполадок латентность конвейера не замедлит проявиться. Предположим, что была обнаружена и исправлена очень опасная ошибка в архитектуре процессора, после чего исправленная версия процессора немедленно поступила в производство. Но, не смотря на всю оперативность исправления ошибки, первые исправленные образцы процессоров выйдут с конвейера лишь через два месяца. А ведь подобная задержка может принести фирме заметные убытки… Другое следствие латентности – низкая эффективность конвейера при производстве небольших партий процессоров. К примеру, при производстве одного процессора темп производства будет равен 0.017 процессоров в день (один процессор за 60 дней), при производстве 28 процессоров – 0.44 процессора в день, при 100 процессорах - уже 1.33 процессоров в день и т.д. Более-менее, нормальный темп будет достигнут только при производстве партии из нескольких тысяч процессоров (рисунок 2.3). К слову, графический конвейер не является исключением из правил. Он также малоэффективен при визуализации небольшого количества примитивов. Поэтому для эффективного использования графического конвейера программист должен стараться минимизировать количество вызовов метода |
(рис 2.3) Зависимость производительности конвейера от количества выпускаемых процессоров. Производительность оценивается по числу процессоров, выпускаемых в среднем за сутки
В начале XXI века корпорация работала над революционным , имеющим на борту массив вершинных и пиксельных процессоров. Для программирования этих процессоров Microsoft в тесном сотрудничестве с разработала два похожих ассемблеро-подобных языка, которые были включены в DirectX 8. Язык для программирования вершинных процессоров получил название , а язык для программирования пиксельных процессоров – . Соответственно, программы, написанные на этих языках, стали называться вершинными и пиксельными ) обусловлено применением первых вершинных и пиксельных процессоров преимущественно для более точной передачи игры света и тени ( ) на поверхности объектов Rampage так и не поступил в массовое производство по финансовым причинам: компания была объявлена банкротом и вскоре куплена NVIDIA, а проект Rampage закрыт.
Примечание
Если быть более точным, зачатки пиксельных (1999 год). Однако по ряду причин Microsoft не захотела включить поддержку этих шейдеров в DirectX. В результате, с точки зрения DirectX -программиста, в NV10 отсутствует какая-либо поддержка шейдеров. Единственная возможность задействовать шейдеры NV10 – воспользоваться API .
Первым действительно массовым с вершинными и пиксельными процессорами стал NV20 (NVIDIA GeForce3), появившийся в 2001 году. Для программирования вершинных и пиксельных процессоров NV20 корпорация Microsoft совместно с NVIDIA разработала языки и , являющиеся расширенными версиями и . Вскоре после NV20 вышел NV25 (GeForce4) , функциональность пиксельных процессоров которого была несколько расширена.
Соответственно язык был обновлен до версии 1.328. Потом появился процессор корпорации и язык , затем R300 (Radeon 9700 Pro) с и и так далее.
В итоге к началу 2002-го года на рынке творилась полная неразбериха среди языков программирования шейдеров. К счастью Microsoft предвидела подобный поворот, и поэтому заранее сделала языки и независимыми от системы команд графического процессора. Фактически каждая версия языка Vertex/ является языком программирования для некоторого виртуального процессора, приближенного к некоторому реальному прототипу. Компиляция шейдера в систему команд физического процессора происходит непосредственно перед загрузкой шейдера в . Таким образом, языки и являются аналогами языка IL в .NET.
Независимость языков и от системы команд физического процессора теоретически позволяет выполнять любой ассемблерный код, независимо о версии шейдера.
Например, корпорации наряду с родными может выполнять и . Это достигается путем перекомпиляции чужеродных шейдеров в родной код. К сожалению, обратное преобразование не всегда возможно. Например, R200 не может выполнять , так как программа, использующая продвинутые возможности этой версии шейдеров не может быть втиснута в прокрустово ложе архитектуры R200.
По мере роста возможностей программы для вершинных и пиксельных процессоров становились все сложение и сложнее. Например, если в длина программы не могла превышать 16 ассемблерных команд, то в максимально возможное число ассемблерных инструкций превысило сотню. Соответственно возрастала трудоемкость разработки и поддержки шейдеров с использованием ассемблера-подобного языка. Таким образом, возникла реальная потребность в переходе на языки программирования шейдеров высокого уровня.
В 2002 году Microsoft выпустила высокоуровневый язык программирования шейдеров High Level . HLSL – это язык программирования высокого уровня, предназначенный для написания программ (шейдеров) для вершинных и пиксельных процессоров. HLSL является C-подобным языком программирования с многочисленными заимствованиями из C++ и C#. В тоже время в HLSL имеется ряд важных расширений, полезных при программировании графического процессора. Программа, написанная на HLSL, компилируется в один из ассемблеро-подобных языков DirectX. Таким образом, процесс компиляции HLSL программы очень напоминает компиляцию C# -программы сначала на промежуточный язык ( IL ), а затем в машинный для конкретного центрального процессора (рисунок 2.4).
(рис 2.4) Компиляция HLSL-программыСамой крупной логической единицей HLSL является эффект ( Effect ), хранящийся в отдельном текстовом файле с расширением .fx. В принципе, эффект можно считать аналогом материала в 3DS MAX. Каждый эффект состоит из одной или нескольких техник ( technique ). Техника – это способ визуализации материала. Например, эффект визуализации мраморного материала может содержать три техники для различных графических процессоров: технику High для ускорителей класса High End, Medium для ускорителей среднего класса, и Low – максимальная производительность при низком качестве NV25 (GeForce4) планировалось использовать язык . Однако после выхода NV25 оказалась, что его функциональность несколько шире, чем предполагалось. Соответственно язык оказался не удел, и вскоре был обновлен до версии 1.3.
Количество техник и их названия могут быть произвольными.
Типы данных
Как известно, лучший способ изучить новый язык программирования - написать на нем несколько программ. Так мы и поступим. Для начала мы создадим простейший эффект, закрашивающий примитив цветом морской волны ( aqua ). Эффект будет содержать одну технику, которую мы назовем Fill.
Приступим. Мы начнем с написания программы для вершинного процессора: вершинного шейдера. Наш шейдер будет принимать в качестве параметра координаты вершины в обычных декартовых координатах, а возвращать координаты вершины уже в однородных координатах. Все преобразование будет сводиться к добавлению к координатам вершины четвертого компонента ( w ), равного 1 (листинг 2.1).
float4 MainVS(float3 pos)
{
return float4(pos, 1.0);
}
Как видно, программа, написанная на HLSL, очень напоминает обычную C-программу: мы объявляем функцию MainVS, которая принимает в качестве параметра переменную типа float3, а возвращает значение типа float4. Что это за такие странные типы float3 и float4, которых нет ни в C, ни C++, ни в C#? Чтобы ответить на этот вопрос мы рассмотрим встроенные типы HLSL.
Скалярные типы
В HLSL все встроенные типы делятся на две большие группы: скалярные и векторные. C (таблица 2.1).
| Тип | Описание |
|---|---|
| bool | Логический тип, который может принимать значения true или false |
int |
32-х битное целое число |
half |
16-ти битное число с плавающей точкой |
float |
32-х битное число с плавающей точкой |
double |
64-х битное число с плавающей точкой |
Задавая тип переменной, вы просто указываете компилятору, что вы хотели бы использовать переменную этого типа. Если текущий ускоритель не поддерживает некоторые типы данных, используемые в программе, то при компиляции шейдера в машинный код они будут заменены ближайшими аналогами. Например, тип double может быть заменен на тип float, half или какой-нибудь иной внутренний тип. Поэтому программист должен стараться избегать жесткой привязки к точности и допустимому диапазону значений используемого типа данных. Особенно это актуально для типа int, так как подавляющее большинство современных ускорителей не поддерживают тип int, в результате чего он эмулируется посредством одного из вещественных типов. Допустим, у нас имеется следующий код:
// a присваивается значение 5 int a = 5; // b должно быть присвоено значение 1 int b = a / 3; // c должно стать равно 2 int c = b * 2;
Какой код будет сгенерирован компилятором? Трудно дать однозначный ответ. В большинстве случаев
компилятор просто заменяет типы int, к примеру, на float:
// a присваивается значение 5.0 float a = 5.0; // b будет присвоено значение 1.66667 float b = a / 3.0; // c станет равно 3.33334 float c = b * 2.0;
Думаю, это совершенно не тот результат, который вы ожидали. Однако в ряде случаев компилятор HLSL все же может начать скрупулезно эмулировать тип int посредством float:
// a присваивается значение 5.0 float a = 5.0; // Значение b вычисляется посредством целочисленного деления float b; // Выполняем обычно вещественное деление float fd = a / 3.0; // Находим дробную часть от деления float ff = frac(fd); // Получаем целую часть b = fd - ff; // Если частное меньше нуля, а дробная часть не равна 0, корректируем результат. Это // обусловлено тем, что frac(2.3) = 0.3, но frac(-2.3) = 0.7 if ((fd < 0) (ff > 0) ) b = b + 1; // c станет равно 2.0 float c = b * 2.0;
Нетрудно заметить, что обратной стороной подобной эмуляции является существенно падение производительности шейдера.
Из-за множества нюансов, заранее достаточно трудно предугадать, какой из двух подходов будет выбран компилятором HLSL. Единственным надежным решением является внимательный анализ кода ассемблерного кода шейдера. Поэтому рекомендуется, по возможности, избегать использования типа int в коде шейдера за исключением переменных счетчиков цикла и индексов массивов.
Векторные типы
Большинство данных, используемых в трехмерной графике, является многомерными векторами, размерность которых редко превышает 4. Так, координаты точки в трехмерном пространстве задаются трехмерным вектором, цвет пикселя - четырехмерным вектором (три цвета и альфа-канал) и так далее. Соответственно, все современные являются
В HLSL имеется множество типов для работы с векторами размерностью от 2-х до 4-х. Вектор из N
элементов типа type задается с использованием синтаксиса, отдаленно напоминающего обобщенные
( Generic ) классы из C#:
vector<type, size>
где
type - имя базового типа: bool, int, half, float или double ;size - размерность вектора, которая может быть равна 1, 2, 3 или 4.Ниже приведен пример объявления переменной v, являющейся вектором из четырех чисел типа float.
vector<float, 4> v;
Однако на практике обычно используется сокращенная запись по схеме:
{type}{N}
где
type - имя базового типаN - размерность вектора.Таким образом, вышеприведенное определение переменной v можно переписать следующим образом:
float4 v;
Язык HLSL позволяет инициализировать вектор двумя способами. Первый способ - перечислить значения вектора в фигурных скобках на манер инициализации массивов в языке C. Ниже приведен пример,
присвоения четырехмерному вектору v начального значения $$(\overline{0.2,0.4,0.6,0.8})$$.
float4 v={0.2, 0.4, 0.6, 0.8};
Другой способ - создать новый вектор с использованием конструктора и присвоить его вектору v:
float4 v=float4(0.2, 0.4, 0.6, 0.8);
Любой N мерный вектор имеет множество конструкторов, которые могут принимать в качестве параметров
как N. Подобное многообразие конструкторов дает
программисту потрясающую гибкость при инициализации векторов:
// Создаем двухмерный вектор и присваиваем ему значение (0.1, 0.2)
float2 a={0.1, 0.2};
// Создаем еще один двухмерный вектор и
присваиваем ему значение (0.3, 0.4)
float2 b=float2(0.3, 0.4);
// Создаем трехмерный вектор. Конструктору в
качестве параметра передается вектор
"b" и число
// 1.0. Соответственно вектору c будет присвоено
значение (0.3, 0.4, 1.0)
float3 c=float3(b, 1.0);
// Создаем четырехмерный вектор на основе
скалярного типа и трехмерного вектора.
Итоговое
// значение вектора d будет равно (0.7, 0.3. 0.4, 1.0)
float4 d=float4(0.7, c);
// Создаем четырехмерный вектор на основе двух
двухмерных. В результате вектору
"d" будет
// присвоено значение (0.1, 0.2. 0.3, 0.4)
float4 e=float4(a, b);
Семантики
Думаю, после такого небольшого экскурса в HLSL вы без труда сможете разобраться в тексте вершинного шейдера из листинга 2.1. Однако если быть более точным, функция, приведенная в этом листинге, не является полноценным шейдером. С точки зрения DirectX это всего лишь простая функция, принимающая в качестве параметра трехмерный вектор и возвращающая четырехмерный вектор. Чтобы превратить эту функцию в вершинный шейдер, мы должны связать параметр Pos с координатами вершины, а результаты функции - с итоговыми координатами вершины. В HLSL для этой цели используются так называемые семантики ( semantics ), предназначенные для связи между собой данных, проходящих через различные ступени графического конвейера. В таблице 2.2 приведены некоторые семантики для входящих данных вершинного шейдера. Описание всех семантик HLSL можно найти в приложении 3.
Примечание
Теоретически вершина может содержать несколько цветов, геометрических координат и т.п. Чтобы различать их в названии семантики требуется указывать целочисленный индекс. При отсутствии индекса в названии семантики он полагается равным 0. Применение семантик с индексами будет рассмотрено в пятой лекции.
| Семантика | Описание |
|---|---|
POSITION[n] |
Координаты вершины |
COLOR[n] |
Цвет вершины |
PSIZE[n] |
Размер точки (при визуализации набора точек) |
Для связи параметра функции с входными данными шейдера, после объявления параметра укажите знак двоеточия и название соответствующей семантики. Таким образом, для связи параметра pos функции MainVS с координатами вершины необходимо использовать семантику POSITION (листинг 2.2).
float4 MainVS(float3
pos:POSITION)
{
return float4(pos, 1.0);
}
Теперь нам надо указать, что функция MainVS возвращает трансформированные координаты вершины. Для этого в HLSL используются семантики выходных данных вершинного шейдера. В частности, для указания того факта, что шейдер возвращает трансформированные координаты вершины используется семантика POSITION (листинг 2.3).
float4 MainVS(float3
pos:POSITION):POSITION
{
return float4(pos, 1.0);
}
Вот теперь мы наконец-то получили полноценный вершинный шейдер. Следующий этап - написание пиксельного шейдера. Наш первый пиксельный шейдер будет просто закрашивать все пиксели цветом морской волны ( aqua ) (листинг 2.4).
float4 MainPS() : COLOR
{
return float4(0.0, 1.0, 1.0, 1.0);
}
Примечание
В HLSL минимальной яркости цветового канала соответствует значение 0.0, а максимальной 1.0.
Так как этот шейдер будет выполняться для каждого пикселя визуализируемого примитива, все пиксели примитива окрасятся в цвет морской волны. Семантика color указывает DirectX, что результат работы пиксельного шейдера MainPS является итоговым цветом пикселя.
Техники, проходы и профили
И так, у нас имеются программы для вершинного и пиксельного процессора - вершинный и пиксельный шейдеры. Заключительный этап написания эффекта - создание техники ( technique ), использующей этот шейдеры. Ниже приведено определение техники с названием Fill, использующей вершинный шейдер MainVS и пиксельный шейдер MainPS (листинг 2.5).
technique Fill
{
pass p0
{
VertexShader = compile vs_1_1 MainVS();
PixelShader = compile ps_1_1 MainPS();
}
}
Как видно, техника определяется с использованием ключевого слова technique. Каждая техника содержит один или несколько проходов, объявляемых с использованием ключевого слова pass. В свою очередь каждому проходу ставится в соответствие пиксельный и вершинный шейдер. Наша техника Fill содержит единственный проход с названием p0.
Примечание
Многопроходные техники используются для создания сложных спецэффектов, которые не могут быть визуализированы за один проход графического конвейера.
Вершинный шейдер для каждого прохода ( pass ) задается с использованием следующего синтаксиса:
VertexShader = compile {используемый профиль} {вершинный шейдер};
Пиксельный шейдер задается аналогично:
PixelShader = compile {используемый профиль} {пиксельный шейдер};
Профиль шейдера ( ) задает промежуточный ассемблеро-подобный язык, на который будет скомпилирован шейдер. Кроме того, профиль задает некоторые архитектурные особенности целевого графического процессора, которые будут учтены компилятором при генерации промежуточного ассемблерного кода. В большинстве случаев каждой версии шейдеров соответствует один профиль. Например, языку соответствует профиль vs_1_1; – профиль ps_1_4, – профиль ps_2_0 и так далее. Однако некоторым языкам вроде соответствует два профиля: в данном случае это ps_2_a и ps_2_b, при этом первый профиль генерирует код , оптимизированный под архитектуру NV3x, а второй – для R4xx. В таблицах 2.3 и 2.4 приведено соответствие между профилями и
соответствующими версиями шейдеров.
| Профиль | Версия вершинных шейдеров |
|---|---|
| vs_1_0 | 1.0 |
| vs_1_1 | 1.1 |
| vs_2_0 | 2.0 |
| vs_2_a | 2.x |
| vs_3_0 | 3.0 |
| Профиль | Версия пиксельных шейдеров |
|---|---|
| ps_1_0 | 1.0 |
| ps_1_1 | 1.1 |
| ps_1_2 | 1.2 |
| ps_1_3 | 1.3 |
| ps_1_4 | 1.4 |
| ps_2_0 | 2.0 |
| ps_2_a | 2.x (оптимизация для NV3x ) |
| ps_2_b | 2.x (оптимизация для R4xx ) |
| ps_3_0 | 3.0 |
Большинство видеокарт поддерживает несколько профилей вершинных и пиксельных шейдеров. В результате каждый разработчик сталкивается с проблемой выбора используемого профиля. В большинстве случаев выбор версии шейдеров определяется минимальными требованиями к приложению.
Допустим, необходимо, чтобы наша программа могла работать на видеокартах класса и выше, NVIDIA GeForce FX 5200 (NV3x) и выше, а так же Intel GMA 900 и выше. Изучив приложение 2, мы увидим, что все видеокарты, удовлетворяющие этому критерию, поддерживают профили вершинных шейдеров vs_1_0, vs_1_1, vs_2_0 и профили пиксельные шейдеров ps_1_0, ps_1_1, ps_1_2, ps_1_3, ps_1_4 и ps_2_0. Таким образом, мы можем смело использовать профили vs_2_0 и ps_2_0 для всех шейдеров. При этом для некоторых эффектов можно предусмотреть дополнительные техники ( technique ) для видеокарт класса High End, использующих профили vs_3_0 и ps_3_0.
Примечание
семейства NV3x демонстрируют очень низкую производительность при использовании профилей пиксельных шейдеров ps_2_0 и ps_2_a. Если для вас актуальна производительность вашего приложения на этих , то имеет смысл стараться по возможности использовать профиль ps_1_4 вместо ps_2_0. Другой вариант – предусмотреть отдельные упрощенные техники для NV3x, использующие профили ps_1_4.
В примерах этого курса я буду стараться использовать минимальную версию профилей, необходимую для нормальной компиляции шейдеров. В частности, именно по этой причине, наш эффект Fill использует профили vs_1_1 и ps_1_1: это позволит работать нашему эффекту даже на стареньких видеокартах семейства GeForce3 (NV20).
И так, у нас есть вершинный и пиксельный шейдеры, а так же техника Fill, использующая эти шейдеры. Для получения готового эффекта осталось только помесить их в файл с расширением *.fx, например, в SimpleEffect.fx (листинг 2.6).
// Вершинный шейдер. Принимает координаты
вершины (x, y, z). Возвращает – координаты вершины
// в однородных координатах (x, y, z, 1.0)
float4 MainVS(float3 pos:POSITION):POSITION
{
return float4(pos, 1.0);
}
// Пиксельный шейдер. Закрашивает все пиксели
примитива цветом морской волны.
float4 MainPS():COLOR
{
return float4(0.0, 1.0, 1.0, 1.0);
}
// Техника Fill
technique Fill
{
// Первый проход
pass p0
{
// задаем вершинный шейдер для техники. Для
компиляции шейдера используется профиль vs_1_1
VertexShader = compile vs_1_1 MainVS();
// задаем пиксельный шейдер. для компиляции
шейдера используется профиль
ps_1_1 PixelShader = compile ps_1_1 MainPS();
}
}
Теперь мы должны научиться использовать этот эффект в наших C# -приложениях.
Одним из основных классов XNA Framework, предназначенным для работы с эффектами, является класс Effect. Класс Effect является довольно сложным классом, содержащим ряд коллекций, отражающих структуру файла эффекта (рисунок 2.5). Как говорилось в прошлом разделе, в каждом эффекте HLSL имеется несколько техник, которые в свою очередь содержат несколько проходов. При этом минимально возможный эффект включает хотя бы одну технику и один проход. Соответственно, класс Effect содержит коллекцию Techniques с экземплярами классов EffectTechnique, инкапсулирующих техники. В свою очередь, каждая техника содержит коллекцию Passes экземпляров класса EffectPass с информацией об эффекте.
(рис 2.5) Коллекции класса EffectЗагрузка и компиляция файла эффекта
Загрузка эффекта из файла *.fx с последующей компиляцией осуществляется при помощи статического
метода Effect.CompileEffectFromFile:
public static CompiledEffect CompileEffectFromFile (string effectFile, CompilerMacro[] preprocessorDefines, CompilerIncludeHandler includeHandler, CompilerOptions options, TargetPlatform platform);
где
effectFile – имя файла с эффектом.preprocessorDefines – массив макроопределений (аналогов директивы #define в C# ), используемых при компиляции эффекта. Мы будем использовать значение null.includeHandler – объект, используемый для обработки директив #include в fx -файле. Так как наш файл не содержит директив #include, мы будем использовать значение null.options – опции компилятора HLSL, задаваемые с использованием перечислимого типа CompilerOptions (таблица 2.5). Члены типа CompilerOptions являются битовыми флагами, что позволяет комбинировать их с использованием оператора OR. В качестве этого параметра, как правило, передается значение CompilerOptions.None.platform – значение перечислимого типа TargetPlatform, указывающее платформу, для которой компилируется эффект. В XNA Framework 1.0 поддерживаются две платформы: TargetPlatform.Windows и TargetPlatform.Xbox360, названия которых говорят за себя. Все примеры этого курса будут использовать значение TargetPlatform.Windows.| Член перечисления | Значение |
|---|---|
None |
Нет никаких опций |
Debug |
Вставляет в ассемблерный код отладочную информацию |
NotCloneable(*) |
Запрещает клонирование (создании копии) эффекта при помощи метода Clone. Эта опция уменьшает объем используемой памяти, так как в оперативной памяти не хранится информация, необходимая для клонирования эффекта. При этом экономия оперативной памяти достигает 50%. |
ForceVertexShaderSoftwareNoOptimizations |
Форсирует компиляцию вершинного шейдера с использованием максимально возможной версии (на момент написания курса это 3.0), не взирая на возможности текущего графического устройства. |
ForcePixelShaderSoftwareNoOptimizations |
Форсирует компиляцию пиксельного шейдера с использованием максимально возможной версии (на момент написания курса это 3.0), не взирая на возможности текущего графического устройства. |
PartialPrecision |
Использовать минимальную точность вычислений, поддерживаемую текущим графическим устройством. Как правило, при использовании этой опции типы double и float заменяются на half. |
SkipOptimization |
Отключает оптимизацию кода |
SkipValidation |
Отключает проверку соответствия сгенерированного кода возможностям текущего ускорителя (не превышено ли ограничение на максимальную длину программы и т.д.) перед отправкой откомпилированного кода шейдера в драйвер. Этот флаг полезен в тех случаях, когда драйверу все же удается оптимизировать слишком длинный ассемблеро-подобный код таким образом, чтобы уложиться в ограничения архитектуры графического процессора. |
(*) – не поддерживается методом CompileEffectFromFile.
Если метод Effect.CompileEffectFromFile не сможет открыть fx -файл (например, из-за его отсутствия), то будет сгенерировано одно из исключений производных от System.IO.IOException вроде System.IO.FileNotFoundException или System.IO.DirectoryNotFoundException.
Метод CompileEffectFromFile возвращает структуру CompiledEffect, содержащую откомпилированный код эффекта, а так же отчет о компиляции эффекта (возникли ли какие-либо проблемы при компиляции эффекта и т.п.).
public struct CompiledEffect
{
// Сообщения о проблемах, возникших при компиляции эффекта
public string ErrorsAndWarnings { get; }
// Был ли эффект откомпилирован удачно
public bool Success { get; }
// Если свойство Success равное true,
содержит откомпилированный код эффекта
public byte[] GetEffectCode();
...
}
Стоит отметить, что метод GetEffectCode возвращает байт-код промежуточного языка наподобие того, что содержится в exe -файлах для платформы .NET. Соответственно, этот код с точки зрения человека является лишь бессмысленным набором байт. Тем не менее, как мы увидим далее, при необходимости этот байт-код может быть легко дизассемблирован удобочитаемый текстовый вид.
Примечание
При желании приложение может сохранить откомпилированный байт-код в каком-нибудь файле, и при следующих запусках считывать из файла уже готовый откомпилированный байт-код. Кстати, Visual C# 2005 Express при компиляции проектов, использующих Content Pipeline, автоматически выполняет компиляцию fx -файлов проекта и сохраняет полученный промежуточный код в файлах с расширением nvb. Таким образом, приложениям, использующим Content Pipeline, нет нужды самостоятельно компилировать fx -файлы.
Следующий этап - компиляция байт-кода промежуточного языка в машинный код вершинных и пиксельных процессоров текущей видеокарты. Эта операция автоматически осуществляется конструктором класса Effect:
public Effect(GraphicsDevice graphicsDevice, byte[] effectCode, CompilerOptions options, EffectPool pool);
где
graphicsDevice - устройство Direct3D, которое будет использоваться для работы с эффектомbyte[] effectCode -код эффекта, предварительно скомпилированный при помощи метода CompileEffectFromFile.options - опции компилятора, задающиеся использованием перечислимого типа CompilerOptions (таблица 2.5.). Довольно часто в качестве этого параметра передается значение CompilerOptions.NotCloneable, что позволяет несколько сэкономить объем используемой оперативной памяти.pool - экземпляр класса EffectPool, позволяющий нескольким эффектам использовать общие параметры. В наших первых примерах мы будем использовать не более одного fx -файла, этот параметр будет равен null.После вызова конструктора класса Effect мы наконец-то получим готовый эффект. Теперь нам необходимо выбрать одну из техник эффекта и проверить ее поддержку текущей видеокартой. Техники эффекта хранятся в коллекции Techniques эффекта:
public EffectTechniqueCollection Techniques { get; }
Однако XNA -приложения достаточно редко обращаются к этой коллекции. Дело в том, что конструктор класса Effect автоматически находит первую попавшуюся технику эффекта и присваивает ее свойству CurrentTechnique.
public EffectTechnique CurrentTechnique { get; set; }
Соответственно, если эффект содержит лишь единственную технику, приложению для получения
информации об этой техники достаточно обратиться к свойству CurrentTechnique, возвращающему
экземпляр класса EffectTechnique, инкапсулирующий технику эффекта. Ниже приведено сокращенное
определение класса EffectTechnique:
public sealed class EffectTechnique
{
// Название техники
public string Name { get; }
// Коллекция проходов техники
public EffectPassCollection Passes { get; }
// Выполняет валидацию техники
public bool Validate();
...
}
И так, каждый эффект может содержать несколько техник. При этом некоторые техники эффекта могут нормально работать на текущем , а некоторые (наиболее продвинутые) нет. Если требования техники ( technique ) превышают возможности текущего (например, пользователь пытается запустить эффект использующий профиль ps_1_4 на NV2x ), XNA Framework проигнорирует технику. В результате примитивы, использующие эту технику, будут отображаться некорректно. Во избежание подобных неприятностей необходимо заранее проверить возможность выполнения данной техники средствами текущего графического устройства. Для этой цели в классе EffectTechnique предусмотрен метод Validate. Если техника может быть выполнена на текущем устройстве, метод Validate возвращает значение true, иначе - false. Во втором случае, приложение может попытаться подобрать альтернативную технику с меньшими системными требованиями или завершить приложение с сообщением о недостаточной "мощности" текущей видеокарты.
Резюмируя все вышесказанное можно предположить, что код для загрузки эффекта и выбора техники, как правило, имеет следующую структуру:
GraphicsDevice device;
// Флаг, устанавливаемый в значение true при
аварийном завершении работы приложения из-за
// проблем в обработчике события Load
closing = false;
...
// Этот код обычно размещается в обработчике события Load.
CompiledEffect compiledEffect;
try
{
// Загружаем эффект из файла и компилируем в промежуточный код
compiledEffect = Effect.CompileEffectFromFile
(effectFileName, null, null,
CompilerOptions.None, TargetPlatform.Windows);
}
// Если при загрузке файла эффекта возникли проблемы
catch (IOException ex)
{
// Выводим сообщение об ошибке
MessageBox.Show(ex.Message, "Критическая ошибка",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
// Завершаем работу приложения. Так как метод Close()
нельзя вызвать из обработчика
// события Load, приходится идти на хитрости
(использовать обработчик события
// Application.Idle, вызывающий внутри себя метод
Close главной формы приложения, если
// флаг closing равен true).
closing = true;
Application.Idle += new EventHandler(ApplicationIdle);
return; }
// Если эффект был скомпилирован с ошибками
if (!compiledEffect.Success)
{
// Выдаем сообщение об ошибке
MessageBox.Show(String.Format("Ошибка при компиляции
эффекта: \r\n{0}",
compiledEffect.ErrorsAndWarnings), "Критическая ошибка",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
closing = true;
Application.Idle += new EventHandler(ApplicationIdle); return; }
// Компилируем байт-код промежуточного языка и создаем объект эффекта.
effect = new Effect(device, compiledEffect.GetEffectCode(),
CompilerOptions.NotCloneable, null);
// Если текущая техника не может быть выполнена на текущем графическом устройстве.
if (!effect.CurrentTechnique.Validate())
{
// Выводим сообщение об ошибке и завершаем работу приложения
MessageBox.Show(String.Format("Ошибка при
валидации техники \"{0}\" эффекта \"{1}\"\n\r" +
"Скорее всего, функциональность шейдера
превышает возможности GPU",
effect.CurrentTechnique.Name, effectFileName),
"Критическая ошибка", MessageBoxButtons.OK,
MessageBoxIcon.Error);
closing = true;
Application.Idle += new EventHandler(ApplicationIdle);
return;
}
Визуализация объекта, использующего эффект
Визуализация примитивов, использующих эффект, начинается с вызова метода Effect.Begin:
public void Begin();
Далее приложение должно перебрать все проходы (коллекция passes) текущей техники ( CurrentTechnique ) и для каждой техники:
Pass текущего эффекта.GraphicsDevice .DrawUserPrimitives.End текущего эффекта.По окончанию визуализации эффекта приложение должно вызвать метод Effect.End. В итоге код визуализации примитива выглядит следующим образом:
Effect effect;
...
// Фрагмент типового обработчика события Paint
...
// Начинаем визуализацию примитивов с использованием эффекта effect.
effect.Begin();
// Перебираем все проходы визуализации текущей техники
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
// Начинаем визуализацию текущего прохода
pass.Begin() ;
// Визуализируем примитивы
device.DrawUserPrimitives(...);
...
device.DrawUserPrimitives(...);
// Завершаем проход
pass.End();
}
// Заканчиваем визуализацию эффекта
effect.End();
Ну что ж, этих знаний вполне достаточно, для того, чтобы попробовать свои силы в визуализации простых примитивов.
Примечание
Как известно, оператор foreach, используемый нами для перебора коллекции проходов ( effectCurrentTechnique.Passes ), обладает несколько более низкой производительностью по сравнению с классическим оператором for. Однако при небольшом количестве итераций эта особенность не является сколь либо заметным недостатком.
Как известно, иногда лучше один раз увидеть, чем сто раз услышать. Эта простая истина как никогда подходит к XNA Framework с весьма запутанной технологией визуализации примитивов. Поэтому мы начнем изучение материала с разбора приложения, рисующего в центре экрана одну точку цвета морской волны (листинг 2.7).
// Пример Examples\Ch02\Ex01
// Стандартные директивы C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
// При обработке исключений, связанных с
открытием файла эффекта, нам понадобится
// пространство имен System.IO
using System.IO;
// Включаем в приложение пространства имен XNA Framework
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using XnaGraphics = Microsoft.Xna.Framework.Graphics;
namespace GSP.XNA.Book.Ch02.Ex01
{
public partial class MainForm : Form
{
// Устройство XNA Framework
GraphicsDevice device = null;
// Параметры представления данных на экране
PresentationParameters presentParams;
// Графический буфер для хранения вершин
(то есть координат нашей точки)
VertexPositionColor[] vertices = null;
// Декларация формата вершины
VertexDeclaration decl = null;
// Эффект, используемый при визуализации точки
Effect effect = null;
// Флаг, устанавливаемый в true при подготовке
к завершении работы приложения
bool closing = false;
public MainForm()
{
InitializeComponent();
}
private void MainFormLoad(object sender, EventArgs e)
{
// Стандартная процедура настройки параметров
формы и создание графического устройства
SetStyle(ControlStyles.Opaque | ControlStyles.ResizeRedraw, true);
MinimumSize = SizeFromClientSize(new Size(1, 1));
presentParams = new PresentationParameters();
presentParams.IsFullScreen = false;
presentParams.BackBufferCount = 1;
presentParams.SwapEffect = SwapEffect.Discard;
presentParams.BackBufferWidth = ClientSize.
Width; presentParams.BackBufferHeight = ClientSize.Height;
device = new GraphicsDevice(GraphicsAdapter.
DefaultAdapter, DeviceType.Hardware,
this.Handle, CreateOptions.SoftwareVertexProcessing |
CreateOptions.SingleThreaded,
presentParams);
// Создаем массив, предназначенный для хранения
координат одной точки
vertices = new VertexPositionColor[1];
// Создаем декларацию формата вершины
decl = new VertexDeclaration(device,
VertexPositionColor.VertexElements);
// Задаем координаты точки (вершины)
таким образом, чтобы она всегда была в центре экрана.
// Цвет точки устанавливаем в морской волны, но
в действительности он не влияет на цвет
// точки, так как используемый эффект игнорирует
информацию о цвете вершины
vertices[0] = new VertexPositionColor
(new Vector3(0.0f, 0.0f, 0.0f),
XnaGraphics.Color.Aqua);
// Структура для хранения кода откомпилированного эффекта
CompiledEffect compiledEffect; try
{
// Пытаемся загрузить эффект из файла и
откомпилировать его в промежуточный байт-код
compiledEffect = Effect.CompileEffectFromFile
(effectFileName, null, null, CompilerOptions.None, TargetPlatform.Windows);
}
// Если файл с эффектом не был найден catch (IOException ex)
{
// Выводим сообщение об ошибке и завершаем работу приложения
MessageBox.Show(ex.Message, "Критическая
ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
closing = true;
Application.Idle += new EventHandler(ApplicationIdle);
return; }
// Если эффект не был удачно откомпилирован if
(!compiledEffect.Success)
{
// Выводим сообщение об ошибках и предупреждениях
из свойства ErrorsAndWarnings и завершаем
// работу приложения
MessageBox.Show(String.Format("Ошибка при компиляции
эффекта: \r\n{0}", compiledEffect.ErrorsAndWarnings),
"Критическая ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);
closing = true;
Application.Idle += new EventHandler(ApplicationIdle); return;
}
// Создаем эффект на базе скомпилированного
байт-кода. Обратите на использование флага
// CompilerOptions.NotCloneable,который позволяет
ощутимо сократить объем оперативной
// памяти, используемой эффектом
effect = new Effect(device, compiledEffect.GetEffectCode(), CompilerOptions.NotCloneable, null);
// Выполняем валидацию текущей техники (проверяем,
может ли текущая техника выполнится на
// данном GPU)
if (!effect.CurrentTechnique.Validate())
{
// Если функциональность текущего GPU недостаточна,
выводим сообщение об ошибке
MessageBox.Show(String.Format("Ошибка при валидации
техники \"{0}\" эффекта
\"{1}\"\n\r" + "Скорее всего, функциональность
шейдера превышает возможности GPU",
effect.CurrentTechnique.Name, effectFileName),
"Критическая ошибка", MessageBoxButtons.OK,
> MessageBoxIcon.Error);
closing = true;
Application.Idle += new EventHandler(ApplicationIdle);
return;
}
}
private void MainFormPaint(object sender, PaintEventArgs e)
{
// Если приложение завершает работу из-за проблем в
обработчике события Load, выходим из
// обработчика события Paint (эффект effect может
быть не корректно инициализирован, поэтому
// попытка визуализации сцены может спровоцировать исключение)
if (closing)
return;
try
{
// Проверяем, не потеряно ли устройство
if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.Lost)
throw new DeviceLostException();
if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.NotReset)
device.Reset(presentParams);
// Очищаем форму
device.Clear(XnaGraphics.Color.CornflowerBlue);
// Устанавливаем формат вершины
device.VertexDeclaration = decl;
// Начинаем визуализацию эффекта.
effect.Begin();
// Перебираем все проходы эффекта
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
// Начинаем визуализацию текущего прохода
pass.Begin() ;
// Рисуем точку
device.DrawUserPrimitives(PrimitiveType.PointList, vertices, 0,
Ч> vertices.Length);
// Заканчиваем визуализацию прохода
pass.End();
}
// Оканчиваем визуализацию эффекта
effect.End();
// Завершаем визуализацию примитивов
// Выводим полученное изображение на экран device.Present(); }
// Обработка потери устройства
catch (DeviceNotResetException)
{
Invalidate();
}
catch (DeviceLostException)
{
}
}
// Обработчик события Idle. Завершает работу приложения.
void Application_Idle(object sender, EventArgs e)
{
Close();
}
// Сброс устройства при изменении размеров окна
private void MainForm_Resize(object sender, EventArgs e)
{
if (WindowState != FormWindowState.Minimized)
{
presentParams.BackBufferWidth = ClientSize.Width;
presentParams.BackBufferHeight = ClientSize.Height;
device.Reset(presentParams);
}
}
// Удаление устройства при завершении программы
private void MainForm_FormClosed(object sender,
FormClosedEventArgs e)
{
if (device != null)
{
device.Dispose(); device = null;
}
}
}
}
Рассмотрим наиболее интересные фрагменты программы. Вначале мы объявляем массив для хранения вершин (то есть координат нашей точки) и декларацию вершины, для хранения описания формата элементов массива:
VertexPositionColor[] vertices = null; VertexDeclaration decl = null;
Ниже объявляется эффект, который будет использоваться для визуализации точки:
Effect effect = null;
Инициализация всех этих объектов выполняется в обработчике события Load формы. После создания графического устройства, обработчик события Load создает массив с информацией о единственной вершине сцены и декларацию формата этой вершины:
vertices = new VertexPositionColor[1]; vertices[0] = new VertexPositionColor(new Vector3 (0.0f, 0.0f, 0.0f), XnaGraphics.Color. Aqua); // Описание формата вершины берется из поля VertexPositionColor decl = new VertexDeclaration(device, VertexPositionColor.VertexElements);
Далее обработчик события Load выполняет компиляцию fx -файла, после использует полученный байт-код для создания объекта эффекта:
// Для сокращения объема кода из него исключена обработка исключительных ситуаций. В реальных // приложениях так поступать категорически не рекомендуется, так как это значительно снизит // "дуракоустойчивость" вашего приложения. Поэтому настоятельно рекомендую ознакомится с // полной версией кода из листинга 2.7. CompiledEffect compiledEffect; // Компилируем fx-файл в байт код compiledEffect = Effect.CompileEffectFromFile (effectFileName, null, null, CompilerOptions.None, TargetPlatform.Windows); // Используем полученный байт-код для создания объекта эффекта. effect = new Effect(device, compiledEffect. GetEffectCode(), CompilerOptions.NotCloneable, null);
При возникновении ошибок при загрузке или компиляции эффекта обработчик не завершает работу приложения путем вызова метода Close формы, так как, если верить MSDN, это может вызвать утечку ресурсов. Вместо этого он регистрирует собственный обработчик события Idle, автоматически вызывающий метод Close. Но здесь есть один подводный камень: метод Idle будет вызван по завершении обработки всех событий, в том числе Paint. Таким образом, если не принять особых мер, не исключен вызов метода Idle с не полностью сформированным эффектом, что с большой долей вероятности приведет к краху приложения. Для борьбы с этим недоразумением в начале обработчика события Paint осуществляется проверка, не готовится ли приложение к завершению работы: если это так, то обработчик события Paint не выполняет визуализацию сцены.
Переходим к обработчику события Paint, выполняющего визуализацию изображения. Первым делом данный обработчик выполняет стандартные проверки потери устройства, после чего очищает экран. Далее он присваивает свойству VertexDeclaration графического устройства декларацию вершины, созданную в обработчике события Load: device.VertexDeclaration = decl;.
На первый взгляд эту операцию было бы рациональнее вынести в обработчик события Load. Однако это не самая лучшая идея, так как информация о параметрах графического устройства теряется при сбросе методом Reset. Следовательно, такое приложение перестало бы нормально функционировать после первой же потери устройства.
И, наконец, главная изюминка программы: визуализация точки на экране с использованием эффекта:
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
device.DrawUserPrimitives(PrimitiveType.PointList,
vertices, 0, vertices.Length);
pass.End() ;
}
effect.End();
Как видно, несмотря на обилие кода, приложение имеет достаточно простую структуру. Как говорится, у страха глаза велики. Теперь давайте попробуем создать это приложение в Visual Studio 2005. Для начала запустите Visual Studio 2005, создайте проект нового приложения Windows Forms и подключите сборку Microsoft.Xna.Framework.dll. В окне Solution Explorer щелкните правой кнопкой мыши на узле проекта и выберите в контекстном меню команду Add | New Folder, и создайте папку Data, в которой мы
(рис 2.6) Создание новой папкибудем хранить различные вспомогательные эффекты (рисунок 2.6,). Затем добавьте в папку Data файл эффекта SimpleEffect.fx (рисунок 2.7), например, при помощи команды контекстного меню Add | New Item... . Поместите в файл SimpleEffect.fx текст эффекта из листинга 2.6.
(рис 2.7) Файл SimpleEffect.fxПосле этих действий в каталоге проекта появится каталог Data, содержащий файл эффекта SimpleEffect.fx. Однако подобное расположение файла не совсем удобно, ведь при компиляции Debug -версии приложения Visual Studio копирует исполняемый exe -файл в подкаталог проекта bin\Debug, а при компиляции Release версии соответственно в каталог bin\Release. Соответственно, было бы логичным, если бы файл эффекта размещался вместе с исполняемым файлом приложения, что облегчило бы создание инсталлятора финальной версии приложения. К счастью, это достаточно легко организовать: просто выделите в окне Solution Explorer файл SimpleEffect.fx и в окне Properties присвойте свойству Copy to Output Directory значение Copy if newer (рисунок 2.8). После этого при каждой компиляции приложения Visual Studio будет автоматически создавать в подкаталоге bin\Debug или bin\Release подкаталог bin\Debug\Data или bin\Release\Data и копировать в него файл SimpleEffect.fx.
В заключении остается создать необходимые обработчики сообщений в соответствии с листингом 2.7. Полную версию приложения можно найти в example.zip в каталоге Ch02\Ex01.
(рис 2.8) Свойства файла SimpleEffect.fxНаше приложение, визуализирующее точку в центре экрана, всегда создает графическое устройство с использованием флага CreateOptions.SoftwareVertexProcessing, то есть вершинные шейдеры всегда выполняются средствами центрального процессора ( CPU ). Учитывая, что подавляющее большинство современных графических процессоров имеют аппаратную поддержку вершинных шейдеров, этот недочет приводит к неоптимальному использованию ресурсов . Использование флага CreateOptions.HardwareVertexProcessing тоже не является хорошей идей, так это сделает невозможной работу приложения на видеокартах без аппаратных вершинных процессоров (например, Intel GMA 900 и Intel GMA 950 ).
Так что же делать? Наиболее красивое решение проблемы - проверка возможностей текущего . Если текущий имеет аппаратные вершинные процессоры, приложение должно создать устройство с использованием флага CreateOptions.HardwareVertexProcessing, в противном случае - CreateOptions.SoftwareVertexProcessing.
Таким образом, нам необходимо научиться анализировать возможности текущего . В XNA Framework информация обо всех возможностях графического устройства инкапсулируются в классе GraphicsDeviceCapabilities, каждое свойство которого соответствует одной из характеристик графического устройства. Учитывая многообразие характеристик устройства, разработчики сгруппировали часть свойств в логические группы (структуры), то есть некоторые свойства класса GraphicsDeviceCapabilities в свою очередь тоже содержат набор свойств по некоторой тематике:
// Некоторые фрагменты определения класса
GraphicsDeviceCapabilities
public sealed class GraphicsDeviceCapabilities : IDisposable
{
// Группа свойств, описывающих возможности
графического устройства по визуализации примитивов
public GraphicsDeviceCapabilities.Primitive
Caps PrimitiveCapabilities { get; }
// Группа свойств с информацией о возможностях декларации вершин
public GraphicsDeviceCapabilities.
DeclarationTypeCaps DeclarationTypeCapabilities { ^ get; }
// Группа свойств с информацией о вершинных шейдерах
public GraphicsDeviceCapabilities.
VertexShaderCaps VertexShaderCapabilities { get; }
// Группа свойств с информацией о пиксельных шейдерах
public GraphicsDeviceCapabilities.
PixelShaderCaps PixelShaderCapabilities { get; }
// Группа свойств с информацией о драйвере устройства
public GraphicsDeviceCapabilities.DriverCaps DriverCapabilities { get; }
// Группа свойств с информацией об устройстве,
которая может пригодится при создании
// устройства
public GraphicsDeviceCapabilities.DeviceCaps
DeviceCapabilities { get; }3
...
// Свойства без подсвойств:
// Максимальная версия языка Vertex Shader,
поддерживаемая графическим устройством
public Version VertexShaderVersion { get; }
// Максимальная версия языка Pixel Shader,
поддерживаемая графическим устройством
public Version PixelShaderVersion { get; }
// Максимальный размер точки, которую способно
отображать графическое устройство
public float MaxPointSize { get; }
// Максимальное количество примитивов,
которое способно отобразить графическое устройство за
// один вызов метода DrawUserPrimitives
public int MaxPrimitiveCount { get; }
// Остальные свойства
...
}
Информация, которая может понадобиться при создании графического устройства, сосредоточена в свойствах свойства GraphicsDeviceCapabilities.DeviceCaps DeviceCapabilities:
// Некоторые фрагменты определения структуры
DeviceCaps public struct DeviceCaps
{
// Поддерживает ли графическое устройство
метод DrawUserPrimitives на аппаратном уровне
public bool SupportsDrawPrimitives2Ex { get; }
// Поддерживает ли графическое устройство
аппаратную растеризацию примитивов (при отсутствии
// подобной поддержки визуализация будет
выполняться с неприемлемо низкой
// производительностью)
public bool SupportsHardwareRasterization { get; }
// Имеет ли графическое устройство аппаратные вершинные процессоры
public bool SupportsHardwareTransformAndLight { get; }
...
}
Как видно, информация о наличии аппаратных вершинных процессоров содержится в свойстве SupportsHardwareTransformAndLight. Таким образом, нашему приложению необходимо просто проверить значение свойства GraphicsDeviceCapabilities.DeviceCapabilities.SupportsHardwareTransformAndLight. Если оно равно true, приложение может создать графическое устройство с использованием флага CreateOptions.HardwareVertexProcessing, в противном случае должен использоваться флаг CreateOptions.SoftwareVertexProcessing.
XNA Framework предоставляет разработчику два способа получения доступа к экземпляру объекта GraphicsDeviceCapabilities. Наиболее простым из них является использование свойства GraphicsDeviceCapabilities экземпляра класса графического устройства:
public GraphicsDeviceCapabilities GraphicsDeviceCapabilities { get; }
Не смотря на простоту данный способ обладает существенным недостатком: для получения доступа к свойству GraphicsDeviceCapabilities приложение должно создать графическое устройство. Получается замкнутый круг: чтобы получить информацию, необходимую для создания графического устройства, приложение должно создать это устройство. В принципе, мы можем попробовать написать что-то вроде:
// Создаем графическое устройство без
аппаратной поддержки вершинных шейдеров.
device = new GraphicsDevice(GraphicsAdapter.
DefaultAdapter, DeviceType.Hardware, this.Handle,
CreateOptions.SoftwareVertexProcessing |
CreateOptions.SingleThreaded, presentParams);
// Если GPU имеет аппаратные вершинные процессоры
if (device.GraphicsDeviceCapabilities.DeviceCapabilities.
SupportsHardwareTransformAndLight)
{
// Уничтожаем устройство device.Dispose();
// Снова создаем устройство, но уже с аппаратной
поддержкой вершинных шейдеров
device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter,
DeviceType.Hardware, this.Handle,
CreateOptions.HardwareVertexProcessing |
CreateOptions.SingleThreaded, presentParams);
}
Хотя данная технология и работает, все это напоминает поездку из Киева в Харьков через Жмеринку. Поэтому разработчики XNA Framework предусмотрели альтернативный способ получения экземпляра класса GraphicsDeviceCapabilities без создания графического устройства. Как вы знаете, конструктор класса GraphicsDevice принимает в качестве первого параметра экземпляр класса GraphicsAdapter, описывающий используемую видеокарту. Так вот, заботливые разработчики XNA Framework снабдили этот класс методом GetCapabilities, возвращающем экземпляр класса GraphicsDeviceCapabilities, соответствующий этому устройству:
public GraphicsDeviceCapabilities GetCapabilities(DeviceType deviceType);
где
deviceType - тип устройства, задаваемый с использованием перечислимого типа DeviceType.Зачем нужен параметр deviceType? Дело в том, что метод GetCapabilitie не может предугадать, какой тип устройства вы собираетесь создать ( DeviceType.Hardware, DeviceType.Reference или DeviceType.NullReference ), в то время как все эти типы устройств имеют совершенно разные характеристики. Соответственно, при помощи параметра deviceType вы указываете методу GetCapabilities, какое значение вы планируете передать параметру deviceType конструктора класса графического устройства ( GraphicsDevice/ ).
Таким образом, проверку наличия аппаратных вершинных процессоров можно организовать с использованием следующего фрагмента кода:
GraphicsDeviceCapabilities caps = GraphicsAdapter.DefaultAdapter.GetCapabilities(DeviceType.Hardware); CreateOptions options = CreateOptions.SingleThreaded; if (caps.DeviceCapabilities.SupportsHardwareTransformAndLight) options |= CreateOptions.HardwareVertexProcessing; else options |= CreateOptions.SoftwareVertexProcessing; device = new GraphicsDevice(GraphicsAdapter. DefaultAdapter, DeviceType.Hardware, this.Handle, options, presentParams)
Точка, визуализируемая нашим приложением ( 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;
(рис 2.9) Точка размером 10x10 пикселей Однако мы не можем просто так взять и присвоить свойству GraphicsDevice.RenderState.PointSize произвольное значение. Ведь никто не может гарантировать, что ваши программы будут запускаться исключительно на тех видеокартах, которые умеют работать с большими точками размером 10x10. Следовательно, необходимо предусмотреть поведение приложения в ситуации, когда видеокарта не удовлетворяет минимальным требованиям к размеру точек: наиболее логичное действие приложения в подобной ситуации – выдача соответствующего сообщение об ошибке с последующим завершением работы.
В разделе 2.4.1 упоминалось, что в XNA Framework имеется класс GraphicsDeviceCapabilities с информацией о возможностях графического устройства. В частности, свойство PointSize содержит максимальный размер точки в пикселях, поддерживаемый указанным графическим устройством:
public float MaxPointSize { get; }
Дополнительная информация
Для быстрого получения информации о возможностях текущей видеокарты я обычно пользуюсь тестовым пакетом , инсталлятор которого находится в example.zip в каталоге RightMark . Достаточно запустить , щелкнуть левой кнопкой мыши на узле (вкладка Available Tests ) и в правой части экрана появится древовидный список возможностей видеокарты. В частности на рисунке 2.10 видно, что видеокарта может визуализировать точки размером не более 256x256 пикселей.
К сожалению имеет одну нехорошую особенность – он всегда загружает процессор на 100%. Не забывайте закрывать , когда он вам больше не нужен; в противном случае вы рискуете столкнуться с резким падением производительности других приложений.
(рис 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-х пикселей. Это обстоятельство позволит нам несколько сократить код некоторых примеров без ущерба надежности.
| |
Максимальный размер точки (в пикселях) |
|---|---|
| NV20 (NVIDIA GeForce3) | 64 |
| NV25 (NVIDIA GeForce4) | 8192 |
| NV3x (NVIDIA GeForce FX) | 8192 |
| R2xx – R5xx ( |
256 |
| GMA 900 (Intel 915G) | 256 |
| GMA 950 (Intel 945G) | 256 |
В этом разделе мы доработаем нашу программу, включив в нее возможность добавления новых точек путем простых щелчков левой кнопкой мыши на поверхности формы. Для этого мы добавим в программу обработчик события 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();
}
}
}
Пройдемся по наиболее интересным особенностям приложения. Как известно, массивы .NET Framework не умеют изменять свой размер, поэтому при добавлении элемента в массив необходимо создать массив увеличенного размера, скопировать в него первоначальный массив и занести в последний элемент массива информацию о новой точке. Однако это не совсем оптимальный вариант, так как каждое добавление новой вершины сопряжено с довольно затратными операциями копирования массива, включая, возможную сборку мусора. В нашей программе используется более агрессивный подход: при каждом увеличении размера массива его размер увеличивается с неким запасом (размер массива увеличивается не на один элемент, а сразу удваивается), чтобы уменьшить вероятность повторного выделения памяти при добавлении следующих точек и уменьшить частоту запуска сборщика мусора.
Примечание
На первый взгляд, может показаться, что информацию о вершинах было бы рациональнее хранить в -коллекции list<>. Однако у подобного подхода есть один неочевидный недостаток. Дело в том, что метод DrawUserPrimitives умеет работать исключительно с классическими массивами System.Array, в результате чего нам придется постоянно преобразовывать список в массив посредством метода ToArray(), неявно создающим новый массив и копирующим в него содержимое списка. Таким образом, использование класса list<> снизит производительность приложения за счет неявного копирования информации из списка в массив, и, что еще хуже, повысит интенсивность вызовов сборщика мусора для удаления предыдущих массивов.
Другой очень полезный прием, используемый в программе - вывод всех точек одним вызовом метода GraphicsDevice.DrawUserPrimitives. Дело в том, что метод GraphicsDevice.DrawUserPrimitives тратит относительно много времени центрального процессора на подготовку CPU и .
Однако метод DrawUserPrimitives имеет ограничения на максимальное количество примитивов, которые можно визуализировать за один вызов этого метода. Количество вершин, которые можно вывести за один присест, тоже далеко не бесконечно. Информация о возможностях текущей видеокарты по визуализации примитивов хранится в двух свойствах класса GraphicsDeviceCapabilities:
// Максимальное количество примитивов, которые можно визуализировать за один присест
public int MaxPrimitiveCount { get; }
// Максимальное количество вершин, которые можно визуализировать за один присест.
public int MaxVertexIndex { get; }
В таблицах 2.7 и 2.8 приведены значения этого свойства для наиболее распространенных моделей видеокарт. Например, интегрированная видеокарта Intel GMA 900 могут визуализировать не более 65535
примитивов и не более 65534 вершины. При запуске приложения на данной видеокарте оно будет
упираться в максимальное количество вершин (65534). А вот на видеокартах корпорации наше приложение будет упираться в максимальное количество визуализируемых примитивов. Таким образом, при оценке максимального количества точек, которые приложение может вывести на экран, необходимо
учитывать как значение свойства MaxPrimitiveCount, так и MaxVertexIndex:
maxVertexCount = Math.Min(device.GraphicsDeviceCapabilities.MaxVertexIndex, device.GraphicsDeviceCapabilities.MaxPrimitiveCount)
|
Значение |
|---|---|
NVIDIA NV2x - NV3x |
1.048.575 |
|
1.048.575 |
Intel GMA 9xx |
65.535 |
Intel GMA 3000 |
65.535 – 1.048.575 |
|
Значение |
|---|---|
|
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. Иными словами, это довольно внушительное значение для нашего приложения, но вполне достижимое. Поэтому в приложение на всякий случай встроена проверка: при достижении предала на количество визуализируемых примитивов, точки просто перестают добавляться в массив. Как говорится, дешево и 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;
}
}
}
В этом, заключительно разделе, посвященном точкам, мы добавим в наше приложение возможность визуализации разноцветных пикселей. В принципе, это довольно тривиальная операция, если бы не одно но: в настоящее время наше приложение визуализирует точки исключительно фиксированного цвета морской волны ( 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
Начнем модификацию эффекта с вершинного шейдера. Теперь на вход шейдера будут подаются два параметра: координаты вершины ( ) и цвет вершины ( iColor ). Результаты выполнения шейдера -однородные координаты вершины ( ) и цвет вершины ( 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;
}
Обратите внимание на использование новых ключевых слов: 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);
}
Если не указан тип параметра функции ( in, out или inout ), HLSL делает этот параметр входящим ( in ). Соответственно, ключевое слово in указывать не обязательно. Кстати, мы активно использовали эту возможность в прошлом разделе.
Структуры
Как известно, передача в функцию большого количества параметров делает код HLSL аналогично языку C (листинг 2.12).
// Объявляем структуру входных данных шейдера.
Обратите внимание на возможность назначения
// каждому полю структуры семантики struct VertexInput
{
float3 pos : POSITION;
float4 color : COLOR; };
// Объявляем структуру выходных данных шейдера
struct VertexOutput
{
float4 pos : POSITION;
float4 color : COLOR; };
void MainVS(in VertexInput input, out VertexOutput output)
{
output.pos = float4(input.pos, 1.0f);
output.color = input.color;
}
Как видно, использование структур делает код значительно более понятным: для определения формата входных данных вершинного шейдера, достаточно лишь беглого взгляда на определение структуры VertexInput. После этой модификации наш шейдер MainVS возвращает в качестве результата лишь один параметр ( output ). Следовательно, процедуру MainVS можно заменить функцией, что сделает код программы еще более интуитивно понятным ( листинг 2.13).
VertexOutput MainVS(VertexInput input)
{
// Создаем структуру output
VertexOutput output;
output.pos = float4(input.pos, 1.0f);
output.color = input.color;
// Возвращаем результаты работы шейдера
return output;
}
Пиксельный шейдер
После обработки вершинным процессором вершины объединяются в примитивы, которые разбиваются на отдельные пиксели (то есть растеризуются). При этом параметры вершины, рассчитанные вершинным шейдером, интерполируются вдоль поверхности примитива. В нашем случае, вдоль поверхности примитива интерполируется цвет вершины. Иными словами, каждому пикселю примитива ставится в соответствие интерполированный цвет (при визуализации точек вдоль поверхности точки интерполируется константный цвет). Наш пиксельный шейдер будет просто принимать интерполированный цвет и выводить его на экран (листинг 2.14).
float4 MainPS(float4 color:COLOR):COLOR
{
return color;
}
Для привязки входных данных пиксельного шейдера к интерполированным выходным данным из вершинного шейдера используется семантика color. Хочу обратить ваше внимание на то, что семантики выходных данных вершинного шейдера и входных данных пиксельного шейдера ничего не говорят о смысле этих ps_3_0 семантики иногда все же могут оказывать незначительное влияние на работу шейдера. Эта тема подробно будет рассмотрена в разделе 4.x.color на texcoord некоим образом не повлияет на работу приложения (листинг 2.14). Главное, чтобы выходные параметры вершинного шейдера и входные параметры пиксельного шейдера использовали одинаковые семантики.
Примечание
Так как профили семейства ps_1_x не позволяют использовать четырех компонентные текстурные координаты, нам пришлось применить профиль ps_2_0. Использование текстурных координат будет рассмотрено в разделе 2.6.
struct VertexInput {
float3 pos : POSITION;
float4 color : COLOR;
};
struct VertexOutput {
float4 pos : POSITION;
// Рассчитанный цвет вершины, передается как текстурные координаты
float4 color : TEXCOORD;
};
VertexOutput MainVS(VertexInput input)
{
VertexOutput output;
output.pos = float4(input.pos, 1.0f);
output.color = input.color;
return output;
}
// Пиксельный шейдер получает входные параметры из
интерполированных текстурных координат
float4 MainPS(float4 color:TEXCOORD):COLOR
{
return color;
}
technique Fill
{
pass p0
{
VertexShader = compile vs20 MainVS(); PixelShader =
compile ps20 MainPS();
}
}
Доработка C#-приложения
С кодом эффекта мы вполне разобрались и, следовательно, можем приступать к модификации C#-кода нашего приложения: теперь при каждом щелчке левой кнопкой мыши на форму будут добавляться разноцветные точки случайного цвета. Для этого достаточно лишь немного подправить обработчик события MouseDown (листинг 2.15).
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 = newVertices;
}
XnaGraphics.Color color; double delta; do
{
// Вычисляем случайные значение компонентов R, G, B
цвета точки byte[] bytes = new byte[3]; rnd.NextBytes(bytes) ;
// Формируем цвет
color = new XnaGraphics.Color(bytes[0], bytes[1], bytes[2]);
// Вычисляем квадрат "расстояния"
между рассчитанным случайным цветом и цветом фона формы
delta = Math.Pow((color.R - XnaGraphics.Color.CornflowerBlue.R), 2) + Math.Pow((color.G - XnaGraphics.Color.CornflowerBlue.G), 2) +
Math.Pow((color.B - XnaGraphics.Color.CornflowerBlue.B), 2);
}
// Если цвет точки слабо отличается от цвета фона,
повторяем вычисления. while(delta < 1000);
// Заносим информацию о точке в массив вершин
vertices[pointCount] = new VertexPositionColor(Helper.
MouseToLogicalCoords(
e.Location, ClientSize), color);
pointCount++;
}
Invalidate();
}
При генерации случайного цвета точки приложение проверяет, не сольется ли полученный цвет с цветом фона. Так как в компьютерной графике цвет задается яркостью трех компонентов, мы можем трактовать значения этих трех компонентов как координаты цвета в некотором цветовом пространстве (рисунок 2.11). Соответственно, в качестве критерия похожести двух цветов можно использовать расстояние между этими цветами:
$$R=\sqrt{(c1_r-c2_r)^2+(c1_g-c2_g)^2+(c1_b-c2_b)^2} $$где
Однако учитывая высокую ресурсоемкость операции вычисления квадратного корня, в качестве критерия похожести цветов рациональнее использовать не само расстояние, а его квадрат. Полная версия приложения находится в example.zip в каталоге Ch02\Ex05.
(рис 2.11) Цветовое пространство Практическое упражнение №2.1
Создайте приложение, рисующее поточечный график функции y=cos(x) , где x находится в диапазоне 0°…720° (рисунок 2.12). Если у вас возникнут трудности при выполнении упражнения, посмотрите готовое приложение в example.zip ( Ch02\Ex06 ).
(рис 2.12) Поточечный график функции y=f(x), визуализированный с использованием двухсот точек
В XNA имеется два типа отрезков: независимые отрезки ( PrimitiveType.LineList ) и связанные отрезки ( PrimitiveType.LineStrip ). При указании независимого типа отрезков метод Device.DrawUserPrimitives рисует набор несвязанных между собой отрезков прямых линий. Первый отрезок рисуется между нулевой и первой вершиной набора вершин, второй отрезок – между второй и третьей, и т.д. (рисунок 2.13). Данный тип примитивов обычно применяется для рисования отдельных отрезков. Связанные отрезки ( PrimitiveType.LineStrip ) используются для построения ломаной линии, проходящей через вершины. Первый сегмент линии рисуется между нулевой и первой вершиной, второй – между первой и второй вершиной и т.д. (рисунок 2.14).

(рис 2.14) Независимые отрезки (рис 2.13) Связанные отрезки (Direct3D.PrimitiveType.LineStrip)Для демонстрации практического использования примитивов PrimitiveType.LineList мы перепишем пример Ex04. Первая точка отрезка будет задаваться нажатием левой кнопки, а вторая – при отпускании левой кнопки мыши. Таким образом, процесс рисования линии будет аналогичен редактору Paint – пользователь помещает указатель мышь в начало отрезка, зажимает левую кнопку, и ведет указатель мыши до конца отрезка, после чего отпускает левую кнопку мыши. В листинге 2.16 приведены основные фрагменты исходного кода полученного приложения (Ex07):
public partial class MainForm : Form
{
...
// Массив вершин отрезков
VertexPositionColor[] vertices = null;
// Количество отрезков
int lineCount = 0;
// Максимальное количество отрезков, которые текущая
видеокарта может визуализировать одним
// вызовом метода DrawUserPrimitives int maxLineCount;
// Флаг, показывающий, находится ли программа в
режиме добавления нового отрезка (когда
// пользователь уже указал начало отрезка, но еще
не отжал левую кнопку мыши)
bool AddingLine = false;
private void MainFormLoad(object sender, EventArgs e)
{
// Определяем максимальное количество отрезков,
которое видеокарта может визуализировать за
// один вызов метода DrawUserPrimitives
maxLineCount = Math.Min(device.GraphicsDeviceCapabilities.
MaxPrimitiveCount,
device.GraphicsDeviceCapabilities.MaxVertexIndex / 2);
// Создаем массив, рассчитанный на хранение вершин
восьми отрезков vertices = new VertexPositionColor[16];
}
private void MainFormPaint(object sender, PaintEventArgs e)
{
// Очищаем экран
device.Clear(XnaGraphics.Color.CornflowerBlue);
// Если количество отрезков больше нуля if (lineCount > 0)
{
device.VertexDeclaration = decl;
// Визуализируем отрезки
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin() ;
device.DrawUserPrimitives(PrimitiveType.LineList,
vertices, 0, lineCount);
pass.End() ;
}
effect.End();
}
device.Present();
}
private void MainFormMouseDown(object sender, MouseEventArgs e)
{
// Если нажата левая кнопка мыши
if (e.Button == MouseButtons.Left)
{
// Если количество линий достигло предельно возможной величины,
нечего не делаем if (lineCount == maxLineCount)
{
MessageBox.Show(String.Format("Количество отрезков достигло
максимального" +
"значения для данного GPU: {0}.", maxLineCount),
"Внимание", MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return; }
// Переходим в режим добавления отрезка AddingLine = true;
// Если размер массива вершин не достаточен
вставки нового отрезка, то создаем массив
// удвоенного размера и копируем в него
содержимое старого массива. if (lineCount * 2 >= vertices.Length)
{
int newLineCount = lineCount * 2;
// Размер массива не должен превышать предельно
лимит текущей видеокарты if
(newLineCount > maxLineCount) newLineCount = maxLineCount;
VertexPositionColor[] newVertices = new VertexPositionColor
[newLineCount*2];
vertices.CopyTo(newVertices, 0);
vertices = newVertices;
}
// Заносим в массив вершин координаты начала и конца
нового отрезка. Для перевода координат
// указателя мыши к диапазону [-1, +1] используется
метод MouseToLogicalCoords, созданный
// нами в разделе 2.4.3.
vertices[lineCount * 2] =
new VertexPositionColor(Helper.MouseToLogicalCoords
(e.Location, ClientSize),
XnaGraphics.Color.Aqua);
vertices[lineCount * 2 + 1] = vertices[lineCount * 2];
// Увеличиваем счетчик количества отрезков на 1
lineCount++;
// Перерисовываем форму Invalidate();
}
}
private void MainFormMouseMove(object sender, MouseEventArgs e)
{
// Если программа находится в режиме добавления нового отрезка
if AddingLine == true)
{
// Обновляем координаты конца отрезка
vertices[lineCount * 2 - 1].Position =
Helper.MouseToLogicalCoords(e.Location,
ClientSize) ; // Перерисовываем экран Invalidate() ;
}
}
private void MainFormMouseUp(object sender, MouseEventArgs e)
{
// Если была отжата левая кнопка мыши
if (e.Button==MouseButtons.Left)
// Выходим из режима добавления нового отрезка AddingLine = false;
}
}
Небольшого внимания заслуживает код, вычисляющий максимальное количество линий, которое может визуализировать видеокарта за один вызов метода DrawUserPrimitives. Как вы знаете из раздела 2.4.3, значение максимального количества примитивов, которые может визуализировать видеокарта за один присест, определяется свойствами GraphicsDeviceCapabilities.MaxPrimitiveCount и GraphicsDeviceCapabilities.MaxVertexIndex. Но так как каждый примитив типа PrimitiveType.LineList содержит две вершины, при оценке максимального количества отрезков, которые может визуализировать видеокарта за один присест, приложение должно поделить значение GraphicsDeviceCapabilities.MaxVertexIndex на 2.
Чтобы сделать работу с программой более комфортной, мы встроим в нее возможность отмены изменений при помощи комбинации клавиш Ctrl+Z, что позволит пользователю легко откатываться назад после ошибочно нарисованных отрезков и т.д. Код обработчика, выполняющего откат изменений, приведен в листинге 2.17 После такой доработки нашу программу вполне можно будет использовать как простенький графический редактор (рисунок 2.15).
private void MainForm_KeyDown(object sender, KeyEventArgs e)
{
if ((e.KeyCode==Keys.Z) (e.Control==true))
if (AddingLine == false)
{
if (lineCount > 0) lineCount--;
Invalidate();
}
}
(рис 2.15) Изображение, нарисованное при помощи нашего самодельного графического редактора (Ex07)
Перейдем к следующему типу примитивов – PrimitiveType.LineStrip. Как говорилось выше, этот тип примитивов применяется для рисования ломаных линий, которые часто используются при построении контуров различных поверхностей или графиков функций. Чтобы опробовать примитивы типа PrimitiveType.LineStrip на практике, мы напишем приложение, рисующее в центре формы окружность радиусом 0.8 единиц ( Ex08 ). Окружность будет нарисована с использованием ломаной линии, содержащей тридцать два сегмента. Каждая вершина ломанной будет иметь свой цвет, благодаря чему окружность будет переливаться различными цветами (рисунок 2.16). Для вычисления координат вершин окружности мы воспользуемся простой формулой из школьного курса аналитической геометрии:
где
y - координаты текущей вершины окружностиy0 - координаты центра окружностиНаиболее важные фрагменты приложения приведены в листинге 2.18.

(рис 2.18) Окружность, нарисованная с использованием примитивов Direct3D.PrimitiveType.LineStrip(рис 2.16) public partial class MainForm : Form { GraphicsDevice device = null; PresentParameters presentParams; VertexDeclaration decl; VertexPositionColor[] vertices = null; // Количество сегментов в ломанной линии, аппроксимирующей окружность. const int LineStripCount = 32; ... private void MainForm_Load(object sender, EventArgs e) { ... decl = new VertexDeclaration(device, VertexPositionColor.VertexElements); // Создаем графический буфер, для хранения вершин окружности vertices = new VertexPositionColor[LineStripCount + 1]; } private void MainForm_Paint(object sender, PaintEventArgs e) { ... // Очищаем экран device.Clear(XnaGraphics.Color.CornflowerBlue); device.VertexDeclaration = decl; // Перебираем все вершины for (int i = 0; i <= LineStripCount; i++) { // Вычисляем координаты текущей вершины окружности по формуле 2.2 float angle = (float)i / (float)LineStripCount * 2.0f * (float)Math.PI; // Окружность имеет радиус 0.8 единиц и расположена в начале системы координат float x = 0.8f * (float)Math.Sin(angle); float y = 0.8f * (float)Math.Cos(angle); // Вычисляем цвет вершины int red=(int) (255 * Math.Abs(Math.Sin(angle * 3))); int green = (int)(255 * Math.Abs(Math.Cos(angle * 2))); // Заносим информацию о вершине в графический буфер vertices[i] = new VertexPositionColor(new Vector3(x, y, 1.0f), new XnaGraphics.Color (red, green, 0) ) ; }; // Рисуем ломанную, аппроксимирующую окружность. Ломанная состоит из vertices.Length - 1 // сегментов. effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin() ; device.DrawUserPrimitives(PrimitiveType.LineStrip, vertices, 0, 4> vertices.Length - 1) ; pass.End() ; } effect.End(); device.Present(); } }
Приложение устроено достаточно просто: сначала в обработчике события Load по формуле 2.2 вычисляются вершины, через которые будет построена ломаная, аппроксимирующая окружность. Визуализация полученной ломанной выполняется в обработчике события Paint.
Стоит отметить, что с ростом числа сегментов ломанная все сильнее начинает походить на настоящую окружность; при количестве сегментов порядка сотни вряд ли кто сможет найти визуальные различия между окружностью, визуализированной поточено средствами GDI+, и ее аппроксимацией ломанной линией. А вот разница в производительности будет более чем заметна.
Управление видовым преобразованием
Так как мы используем логическую систему координат, в которой ширина и высота формы всегда равна двум, непропорциональное растяжение формы приводит к искажению изображения (рисунок 2.17). Существует два метода борьбы с этим явлением:
(рис 2.17) Искажения формы круга при непропорциональном изменении размеров окна У каждого из этих подходов есть недостатки: реализация масштабирования примитивов сцены неминуемо сделает код визуализации более запутанным, а ограничение на размеры окна будет сковывать действия пользователя и создаст чувство дискомфорта. Однако существует и третий вариант:
Этот метод имеет важный нюанс относительно первого варианта: в данном случае трансформируются не координаты вершин примитива, а итоговое Viewport класса GraphicsDevice: public Viewport Viewport { get; set; }.
Одноименная структура Viewport, инкапсулирующая параметры видового преобразования, определяется следующим образом:
public struct Viewport
{
// Ширина области, в которую осуществляется визуализация
public int Width { get; set; }
// Высота области, в которую осуществляется визуализация
public int Height { get; set; }
// Координата X левого верхнего угла области визуализация
public int X { get; set; }
// Координата Y левого верхнего угла области визуализация
public int Y { get; set; }
…
}
Как видно, структура Viewport определяет в клиентской области окна прямоугольную область, используемую для визуализации изображения. По умолчанию при создании и сбросе устройства XNA Framework автоматически присваивает полям X и Y нулевое значение, a Width и Height - ширину и высоту клиентской области окна. Таким образом, по умолчанию клиентская область заполняет всю клиентскую область окна.
В следующем примере демонстрируется использование пользовательской области визуализации размером 100x100, расположенной в левом верхнем углу приложения (листинг 2.19).
// Пример Examples\Ch02\Ex09
private void MainForm_Load(object sender, EventArgs e)
{
// Важно! Размер формы не может быть меньше области
визуализации, в противном случае при
// попытке визуализации в такую форму будет сгенерировано исключение
// System.InvalidOperationException
MinimumSize = SizeFromClientSize(new Size(100, 100)); ...
}
private void MainForm_Paint(object sender, PaintEventArgs e)
{
if (closing) return;
try
{
if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.Lost)
throw new DeviceLostException();
if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.NotReset)
device.Reset();
// Задание параметров области визуализации.
Viewport viewport = new Viewport();
viewport.Width = 100;
viewport.Height = 100;
// Присваиваем информацию об области визуализации структуре Viewport
device.Viewport = viewport;
// Очищаем экран
device.Clear(XnaGraphics.Color.CornflowerBlue);
// Рисуем окружность
...
}
}
Результат визуализации приведен на рисунке 2.18. Что же изменилось в работе приложения?
(рис 2.18) Использование области визуализации 100x100, расположенной в левом верхнем углу окна Примечание
Отладочная версия DirectX перед началом визуализации кадра автоматически закрашивает его случайным цветом, что позволяет разработчику легко обнаружить области окна, игнорируемые приложением.
Теперь мы уже можем определиться со стратегией борьбы с геометрическими искажениями в примере Ex08: перед визуализацией изображения приложение должно задать квадратную область визуализации максимально возможного размера, расположенную в клиентской области формы. При этом, во избежание артефактов по краям окна, во время очистке окна методом Clear должна применяться область визуализации размеров во всю клиентскую область формы. Учитывая, универсальность подобной технологии, методы рассчитывающие параметры области визуализации будет разумно поместить в наш класс Helper.cs (листинг 2.20).
// Сборник вспомогательных методов, полезных в хозяйстве
class Helper
{
...
// Принимает размеры клиентской области формы. Возвращает
квадратную область визуализации
// максимально возможного размера, расположенную в центре формы.
public static Viewport SquareViewport(System.Drawing.Size clientSize)
{
Viewport viewport = new Viewport();
viewport.Width = Math.Min(clientSize.Width, clientSize.Height);
viewport.Height = viewport.Width;
viewport.X = (clientSize.Width- viewport.Width) / 2;
viewport.Y = (clientSize.Height - viewport.Height) / 2;
return viewport;
}
// Принимает размеры клиентской области формы. Возвращает
область визуализации размером во
// всю клиентскую область формы
public static Viewport FullScreenViewport(System.Drawing.
Size clientSize)
{
Viewport viewport = new Viewport(); viewport.Width =
clientSize.Width; viewport.Height = clientSize.Height; return viewport;
}
}
Код модифицированного приложения, использующего новые методы класса Helper, приведен в листинге 2.21.
// Пример Ch02\Ex10
private void MainForm_Paint(object sender, PaintEventArgs e)
{
if (closing) return;
try
{
if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.
Lost) throw new DeviceLostException();
if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.
NotReset) device.Reset();
// Используем для визуализации всю клиентскую область окна
device.Viewport = Helper.FullScreenViewport(ClientSize);
// Очищаем экран
device.Clear(XnaGraphics.Color.CornflowerBlue);
// Используем квадратную область визуализации
device.Viewport = Helper.SquareViewport(ClientSize);
...
}
}
Результат визуализации круга в окне с неквадратной клиентской областью приведен на рисунке 2.19. Как видно, несмотря на непропорциональное изменение сторон окна, круг остался кругом.
(рис 2.19) Круг, визуализированный с использованием квадратной области визуализации, расположенной в центре формы Практическое упражнение №2.2
Доработайте приложение, визуализирующее график косинуса из практического упражнения №2.1, заменив примитивы PrimitiveType.PointList на PrimitiveType.LineStrip. Так же добавьте в приложение высотную закраску: когда функция принимает значение 1, график должен окрашиваться зеленым цветом, а при -1 – красным. Остальные точки графика принимают промежуточные цвета (рисунок 2.20). Для вычисления цвета промежуточных точек можно воспользоваться следующими формулами:
где
red - красная составляющая цветаgreen - зеленая составляющая цветаПримечание
Если же убрать операцию возведения в степень, то график функции будет заметно терять яркость в окрестностях y равного 0, что смотрится не особо красиво.
(рис 2.20) График синуса с высотной закраской
Для визуализации наборов треугольников с различной топологией в XNA Framework имеется три типа примитивов: PrimitiveType.TriangleList, PrimitiveType.TriangleFan и PrimitiveType.TriangleStrip.Начнем с самого простого примитива, PrimitiveType.TriangleList.
Этот примитив предназначен для визуализации набора несвязанных треугольников: первый треугольник строится с использованием 0-й, 1-й и 2-й вершин, второй треугольник – 3-й, 4-й и 5-й вершин, третий треугольник – 6-й, 7-й и 8-й вершин и т.д. (рисунок 2.21).
(рис 2.21) Треугольники, нарисованные с использованием примитива PrimitiveType.TriangleList По умолчанию XNA Framework отображает на экране только те треугольники, вершины которых расположены на экране по часовой стрелке. К примеру, при визуализации треугольников, изображенных на рисунке 2.21 на экране отобразятся только крайние треугольники ( v0, v1, v2 ) и ( v6, v7, v8 ). А вот средний треугольник ( v3, v4, v5 ) будет отброшен, так как его вершины перечисляются против часовой стрелки. Такое на первый взгляд странное поведение XNA Framework обусловлено особенностью отсечения невидимых треугольников в трехмерных сценах. Однако при визуализации двухмерных изображений эта функциональность оказывается не только излишней, но и вредной. Поэтому разработчики XNA Framework заботливо предусмотрели свойство GraphicsDevice.RenderState.CullMode,
public CullMode CullMode { get; set; }
Это свойство может принимать следующие значения перечислимого типа CullMode:
CullMode.None - отсечение выключеноCullMode.Clockwise - отсекаются треугольники, вершины которых расположены на экране по часовой стрелкеCullMode.CounterClockwise - отсекаются треугольники, у которых вершины расположены на экране против часовой стрелки.По умолчанию свойству GraphicsDevice.RenderState.CullMode присваивается значение CullMode.CounterClockwise, то есть видеокарта отбрасывает все треугольники, у которых вершины расположены против часовой стрелки. Для отключения этой функциональности достаточно присвоить этому свойству значения CullMode.None.
В листинге 2.22 приведен исходный код основных фрагментов программы (Ex12), рисующей в центре экрана треугольник (рисунок 2.22).

(рис 2.22) Треугольник с разноцветными вершинами (рис 2.22) public partial class MainForm : Form { const string effectFileName = "Data \\ColorFill.fx"; GraphicsDevice device = null; PresentationParameters presentParams; Effect effect = null; VertexDeclaration decl = null; VertexPositionColor[] vertices = null; FillMode fillMode=FillMode.Solid; bool closing = false; … private void MainFormLoad(object sender, EventArgs e) … { // Создаем массив для хранения трех вершин треугольника vertices = new GraphicsBuffer<TransformedColored>(3); // Задаем вершины треугольника vertices[0] = new VertexPositionColor(new Vector3(0.0f, 0.4f, 0.0f), 4> XnaGraphics.Color.Coral); vertices[1] = new VertexPositionColor(new Vector3(0.4f, -0.4f, 0.0f), 4> XnaGraphics.Color.LightGreen); vertices[2] = new VertexPositionColor(new Vector3(-0.4f, -0.4f, 0.0f), 4> XnaGraphics.Color.Yellow); } private void MainFormPaint(object sender, PaintEventArgs e) { device.Clear(XnaGraphics.Color.CornflowerBlue); // Выключаем отсечение треугольников device.RenderState.CullMode = Cull.None; device.VertexDeclaration = decl; // Рисуем треугольник 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(); }
Как видно, листинг программы мало чем отличается от предыдущих примеров. Единственное разница заключается в отключении режима отсечения треугольников и использовании примитивов типа PrimitiveType.TriangleList.
Режимы закраски
Как известно, многие приложения 3D моделирования вроде 3ds Max или Maya позволяют отображать сцену в режиме проволочного каркаса ( ). Благодаря этому разработчик может ясно видеть топологию сцены, в частности, взаимное расположение всех треугольников на сцене. XNA Framework тоже поддерживает подобную функциональность, позволяя отображать вместо закрашенных треугольников их проволочный каркас. Управление этой функциональностью осуществляется при помощи свойства RenderState.FillMode класса GraphicsDevice:
FillMode FillMode { get; set; }
Свойство может принимать следующие значения перечислимого типа FillMode:
FillMode.Point - визуализируются только точки, расположенные на вершинах треугольника. Визуализируемые точки являются полноценными точками XNA Framework: к примеру, их размер можно изменять при помощи свойства device.RenderState. PointSize.FillMode.WireFrame - визуализирует каркас треугольника, который рисуется с использование oбычных линий вроде TrianglePrimitive.LineList или TrianglePrimitive.LineStrip.FillMode.Solid - закрашивает внутреннюю область треугольника.По умолчанию свойству RenderState.FillMode присвоено значение FillMode.Solid, то есть треугольники рисуются закрашенными.
Для демонстрации практического использования свойства FillMode мы добавим в нашу программу ( Ex12 ) возможность циклической смены режимов отображения треугольников при помощи клавиши пробел (листинг 2.23).
public partial class MainForm : Form
{
// Режим отображения треугольников
FillMode fillMode=FillMode.Solid;
...
private void MainForm_Paint(object sender, PaintEventArgs e)
{
...
// Выключаем отсечение треугольников
device.RenderState.CullMode = Cull.None;
// Задаем режим отображения треугольников
device.RenderState.FillMode = fillMode;
// Зазаем размер точек
device.RenderState.PointSize = 3.0f;
...
// Рисуем треугольник
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
device.DrawUserPrimitives(PrimitiveType.TriangleList,
vertices, 0, verteces.Length / 3);
pass.End();
}
effect.End();
...
}
private void MainForm_KeyDown(object sender, KeyEventArgs e)
{
// Если нажата клавиша пробел
if (e.KeyCode == Keys.Space)
{
// Изменяем режим отображения switch (fillMode)
{
case FillMode.Point:
fillMode = FillMode.WireFrame; break; case FillMode.WireFrame:
fillMode = FillMode.Solid; break; case FillMode.Solid:
fillMode = FillMode.Point;
break;
}
// Перерисовываем экран
Invalidate();
}
}
}
Узор Серпинского
(рис 2.23) Построение узора Серпинского. Базовый треугольник Перейдем к более сложному примеру. Наше следующее приложение будет строить узор Серпинского путем рекурсивного разбиения треугольника, визуализируемого в каркасном режиме. Построение узора начинается с базового треугольника (рисунок 2.23). На первой интеграции в данный большой треугольник вписывается другой треугольник меньшего размера, вершины которого расположены в середине сторон большого треугольника (рисунок 2.24). В результате большой треугольник оказывается как бы разбит на 4 треугольника. На втором этапе данные в три треугольника, примыкающие к вершинам исходного большого треугольника, вписываются по три треугольника (рисунок 2.25). На третьем этапе в образовавшиеся девять треугольников вписываются уже девять треугольников (рисунок 2.26), на четвертом этапе вписывается уже 27 треугольников и так далее. В идеале процесс должен продолжаться до бесконечности, одна ко на практике вполне можно ограничиться десятком итераций (рисунок 2.27), так как размер треугольников, генерируемых в последующих итерациях, будет уже меньше размера пикселей экрана.

(рис 2.25) Построение узора Серпинского. Первая итерация (рис 2.24) Построение узора Серпинского. Вторая итерация 
(рис 2.27) Построение узора Серпинского. Третья итерация (рис 2.26) Узор Серпинского, 11 итераций Так как приложение будет визуализировать десятки или даже сотни тысяч треугольников, очень важно поместить их в единый массив и вывести одним вызовом метода DrawUserPrimitives. Однако для создания такого массива очень полезно заранее знать количество треугольников, которые будут визуализированы за n итераций. Это поможет нам избежать многочисленных изменений размера массива по мере генерации треугольников. Давайте попробуем найти зависимость числа визуализируемых треугольников от количества интеграций. И так, при нулевом количестве итераций мы визуализируем 1 треугольник. При одной итерации число треугольников становится 1 + 1 = 2. При двух итерациях количество треугольников будет равно 1 + 1 + 3 = 5, при трех 1 + 1 + 3 + 9 = 14. Таким образом, мы можем вывести некоторую общую закономерность для n итераций:
где
tc - количество треугольников, визуализируемых при n итераций.В принципе, это выражение вполне приемлемо, однако знак суммы смотрится не особо красиво. Однако открыв учебник высшей математики вроде можно найти весьма интересное соотношение:
$$\sum_{i-0}^n x^i=\frac{1-x^{n+1}}{1-x} $$Соответственно, выражение 2.4 можно переписать без использования n элементов:
Гораздо более наглядное выражение, не так ли? Однако так как разные видеокарты могут визуализировать разное число треугольников, не исключено, что приложению придется решать и образную задачу. Допустим, мы определим в приложении число итераций ( n ) равным 11, то есть узор Серпинского будет содержать $$\frac{3^{11}+1}2=88574$$ треугольников с общим количеством вершин 88574 ? 3 = 265722. Но ведь некоторые видеокарты могут оказаться не способными визуализировать такое количество треугольников за один присест. Как приложение должно повести себя в подобном случае? Наиболее простое решение – сократить количество интеграций до максимально приемлемого. А для этого нам придется определять максимальное количество итераций ( n ), при котором количество треугольников не превышает заданное значение tc. Для этого выражение (2.6) достаточно переписать как
после чего взять от обоих частей выражения логарифм по основанию 3:
$$n=floor(\log_3(2-tc-1)) $$где
x (то аналог метода Math.Floor из C#).К слову
Согласно выражению 2.8 на компьютере с Intel GMA 900 приложение может выполнить до 9-ти итераций, на NVIDIA NV 2x-3x до 12-ти итераций, а на до 13-ти итераций.
После такого небольшого математического экскурса можно приступать реализации нашего приложения. Визуализация треугольников будет осуществляться в два этапа:
DrawUserPrimitives.Вычисление координат треугольников мы организуем с использованием рекурсивной функции DrawTriangle принимающей в качестве параметров координаты треугольника и количество оставшихся итераций. Эта функция будет помещать в массив вершин координаты текущего треугольника, после чего выполнять деление этого треугольника на три части и вызывать саму себя для этих частей, но уже с уменьшенным количеством оставшихся итераций на 1. Процесс повторяется до тех пор, пока количество оставшихся итераций не достигнет 0.
Исходный код основных фрагментов программы с подробными комментариями приведен в листинге 2.24.
// Примем Examples\Ch02\Ex13
public partial class MainForm : Form
{
// Число итераций для визуализации треугольника Серпинского.
Если видеокарта не способна
// визуализировать такое количество треугольников,
число итераций автоматически уменьшается
// до приемлемого значения
const int n = 15;
const string effectFileName = "Data
\\ColorFill.fx";
GraphicsDevice device = null; PresentParameters presentParams;
Effect effect = null; VertexDeclaration decl = null;
// Массив вершин узора Серпинского
VertexPositionColor[] vertices = null;
// Индекс текущей вершины (глобальная переменная,
используемая при рекурсивном формировании
// узора Серпинского)
int currentVertex;
// Рекурсивная функция, заносящая в массив
vertices информацию о вершинах узора.
// a, b, c - координаты текущего треугольника
// pass - число оставшихся итераций
void DrawTriangle(Vector2 a, Vector2 b, Vector2 c, int pass)
{
// Если это последняя итерация, выходим из функции
if (pass <= 0)
return;
// Уменьшаем количество оставшихся итераций pass -= 1;
// Помещаем в массив вершины треугольника
вписанного в текущий "большой" треугольник
vertices[currentVertex] = new VertexPositionColor
(new Vector3(ab.X, ab.Y, 0.0f),
XnaGraphics.Color.Black);
vertices[currentVertex + 1] = new VertexPositionColor
(new Vector3(ac.X, ac.Y, 0.0f),
XnaGraphics.Color.Black);
vertices[currentVertex + 2] = new VertexPositionColor
(new Vector3(bc.X, bc.Y, 0.0f),
XnaGraphics.Color.Black);
// Увеличиваем индекс текущей вершины currentVertex += 3;
// Вычисляем координаты середины сторон треугольника
Vector2 ab = new Vector2((a.X + b.X) / 2.0f,(a.Y+b.Y)/2.0f);
Vector2 ac = new Vector2((a.X + c.X) / 2.0f,(a.Y+c.Y)/2.0f);
Vector2 bc = new Vector2((b.X + c.X) / 2.0f,(b.Y+c.Y)/2.0f);
// Вызываем этот рекурсивный метод для
образовавшихся трех крайних треугольников, примыкающих
// к углам текущего треугольника
DrawTriangle(a, ab, ac, pass);
DrawTriangle(b, ab, bc, pass);
DrawTriangle(c, ac, bc, pass);
}
private void MainForm_Load(object sender, EventArgs e)
{
...
// Определяем максимальное количество треугольников,
которое текущая видеокарта может
// визуализировать за один присест
int maxTriangleCount = Math.Min(device.GraphicsDevice
Capabilities.MaxPrimitiveCount,
device.GraphicsDeviceCapabilities.MaxVertexIndex / 3);
// Вычисляем по формуле 2.8 максимальное количество
итераций визуализации узора, которые
// можно выполнить на текущей видеокарте
int maxPass = (int) Math.Floor(Math.Log(2 * maxTriangleCount - 1, 3));
// При необходимости уменьшаем количество интеграций,
которое задаются константой n
int passes = Math.Min(n, maxPass);
// Вычисляем по формуле 2.6 количество треугольников,
формирующих данный узор Серпинского.
int triangleCount = ((int)Math.Pow(3, passes) + 1) / 2;
// Выделяем память для хранения информации о вершинах
треугольниках vertices = new VertexPositionColor[3 * triangleCount];
Text += " Количество итераций: " + passes.ToString();
// Вершины начального треугольника
Vector2 a = new Vector2(0.0f, 0.9f); Vector2 b = new
Vector2(-0.9f, -0.9f); Vector2 c = new Vector2(0.9f, -0.9f);
// Обнуляем индекс текущей вершины
currentVertex = 0;
// Заносим в массив вершины самого большого треугольника
vertices[currentVertex] = new
VertexPositionColor(new Vector3(a.X, a.Y, 0.0f),
XnaGraphics.Color.Black); vertices[currentVertex + 1] = new
VertexPositionColor(new Vector3(b.X, b.Y, 0.0f),
XnaGraphics.Color.Black); vertices[currentVertex + 2] = new
VertexPositionColor(new Vector3(c.X, c.Y, 0.0f),
XnaGraphics.Color.Black); currentVertex += 3;
// Выполняет рекурсивное деление треугольника в течении
pass итераций CreateTriangle(a, b, c, passes);
}
private void MainForm_Paint(object sender, PaintEventArgs e)
{
...
// Очищаем экран
device.Clear(ClearFlags.Target, Color.White, 0.0f, 0);
device.BeginScene();
// Отключаем отсечение треугольников
device.RenderState.CullMode = Cull.None;
// Используем каркасную визуализацию треугольников
device.RenderState.FillMode = FillMode.WireFrame;
device.VertexFormat = TransformedColored.Format;
// Рисуем треугольники
device.DrawUserPrimitives(PrimitiveType.TriangleList,
verteces.NumberElements/3, verteces);
device.EndScene();
device.Present();
}
}
}
Практическое упражнение №2.3
Напишите приложение, рисующее обыкновенный деревянный забор, покрашенный (рисунок 2.28). Готовое приложение находится в example.zip в каталоге Ch02\Ex14.
(рис 2.28) Покрашенный деревянный забор
Следующий тип примитивов, PrimitiveType.TriangleFan, используется для рисования вееров треугольников. Первые три вершины (0-я, 1-я и 2-я) задают первый треугольник. Второй треугольник задается 0-й, 2-й и 3-й вершинами, третий – 0-й, 3-й и 4-й вершинами и т.д. (рисунок 2.29). Данный тип примитивов идеально подходит для рисования эллипсов, окружностей, секторов окружностей и аналогичных фигур.
(рис 2.29) Веер треугольников, нарисованный с использованием примитивов PrimitiveType.TriangleFan Чтобы опробовать этот тип примитива на практике, мы модифицируем пример Ex09, заставив его рисовать на экране закрашенный круг вместо окружности (рисунки 2.31). Для этого придется внести три небольших изменения в обработчики событий Load и Paint:
LineList на TriangleFan
(рис 2.30) . Круг, нарисованный при помощи веера из 64-х треугольников (PrimitiveType.TriangleFan)Так же мы добавим в программу возможность переключения между каркасным и закрашенным режимами отображения треугольников при помощи клавиши пробел ( Space ). Эта функциональность, позволяющая просматривать топологию сцены, неоценима при отладке приложения (рисунок 2.31).
(рис 2.31) Круг (веер из 18-ти треугольников), визуализированный в каркасном режиме Основные фрагменты исходного кода полученного приложения ( Ex15 ) приведены в листинге 2.25.
// Количество сегментов в круге
const int slices = 64;
// Режим закраски круга
FillMode fillMode = FillMode.Solid;
private void MainForm_Load(object sender, EventArgs e)
{
...
// Создаем графический буфер
vertices = new VertexPositionColor[slices + 2];
// Помещаем в начало графического буфера
вершину, расположенную в центре экрана
vertices[0] = new VertexPositionColor(new Vector3(0.0f, 0.0f, 0.0f),
XnaGraphics.Color.White);
// Перебираем все вершины окружности for (int i = 0;
i <= slices; i++)
{
// Определяем координаты текущей вершины
float angle = (float)i /
(float)slices * 2.0f * (float)Math.PI; float x = 0.7f *
(float)Math.Sin(angle); float y = 0.7f * (float)Math.Cos(angle);
// Вычисляем цвет вершины
byte red = (byte)(255 * Math.Abs(Math.Sin(angle * 3))); byte green =
(byte)(255 * Math.Abs(Math.Cos(angle * 2)));
// Помещаем информацию о вершине в массив вершин
vertices[i + 1] = new VertexPositionColor(new Vector3(x, y, 0.0f), new
XnaGraphics.Color(red, green, 0)); }; ... }
private void MainForm_Paint(object sender, PaintEventArgs e)
{
...
// Задаем область визуализации размером во весь
экран. Для вычисления параметров видового
// преобразования используется метод
FullScreenViewport нашего вспомогательного класса Helper
device.Viewport = Helper.FullScreenViewport(ClientSize);
// Закрашиваем поверхность формы
device.Clear(XnaGraphics.Color.CornflowerBlue);
// Задаем квадратную область закраски максимально
возможного размера device.Viewport = Helper.SquareViewport(ClientSize);
// Выключаем отсечение треугольников (см. предыдущий раздел)
device.RenderState.CullMode = CullMode.None;
// Задаемрежим визуализации треугольников
device.RenderState.FillMode = fillMode;
device.VertexDeclaration = decl;;
// Рисуем круг
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
device.DrawUserPrimitives(PrimitiveType.
TriangleFan, vertices, 0, vertices.Length - 2);
pass.End(); } effect.End();
// Переключаем вспомогательные буферы
device.Present(); }
// Обработчик события нажатия клавиш
private void MainForm_KeyDown(object sender, KeyEventArgs e)
{
// Если нажата клавиша пробел
if (e.KeyCode == Keys.Space)
{
// Меняем режим отображения
if (fillMode == FillMode.Solid)
fillMode = FillMode.WireFrame; else
fillMode = FillMode.Solid;
// Обновляем изображение Invalidate();
}
}
Последний тип примитивов, PrimitiveType.TriangleStrip, предназначен для рисования полосы из связанных треугольников. При этом первый треугольник проходит через 0-ю, 1-ю 2-ю вершины, второй треугольник – через 3-ю, 2-ю и 1-ю вершины, третий треугольник – через 2-ю, 3-ю и 4-ю вершины, четвертый через 5-ю, 4-ю и 3-ю, и т.д (рисунок 2.32). Обратите внимание на порядок перечисления вершин в треугольниках: все треугольники в полосе имеют одинаковую ориентацию относительно часовой стрелки – например, если вершины первого треугольника располагаются по часовой стрелке, то и вершины других треугольников так же будут перечисляться по часовой стрелке. Эта особенность используется при отсечении невидимых треугольников с использованием поля RenderState.CullMode.
(рис 2.32) Визуализация полосы связанных треугольников Данный тип примитивов очень удобно использовать для визуализации ломаных линий шириной больше одного пикселя, то есть в качестве продвинутой версии примитива PrimitiveType.LineStrip. В частности, на рисунке 2.32 в качестве иллюстрации приведена ломаная линия переменной ширины, состоящая из пяти сегментов.
Визуализация графика функции y=cos(x)
Чтобы попрактиковаться в рисовании ломаных линий при помощи примитива PrimitiveType.TriangleStrip, мы напишем приложение, визуализирующее график косинуса в интервале с $$0°…720° (0.4 \pi \ в \ радианах)$$ использованием ломаной толщиной 10 пикселей. Хотя на первый взгляд эта задача не намного сложнее практический упражнений №2.1 и №2.2, она все же имеет несколько подвохов.
Для начала сформулируем задачу более четко. Нам необходимо построить полосу из связанных треугольников, аппроксимирующую график косинуса, центр которой совпадает с графиком косинуса (рисунок 2.33).
(рис 2.33) Полоса из связанных треугольников, аппроксимирующая график косинуса (тонкая линию, проходящая по центру полосы).Для построения синусоиды мы будем перебирать точки графика косинуса с определенным шагом. На рисунке 2.30 эти точки обозначены как $$p_0, p_1, p_2$$ и т.д. Отступив симметрично по обе стороны от точки p0 на некоторое расстояние, например, на 0.05 единиц, мы получим две вершины $$v_0$$ и $$v_1$$, расстояние межу которыми равно 10 пикселей. Проделав аналогичную операцию над остальными точками, мы получим пары вершин ( $$v_2–v_3, v_4–v_5, v_6–v_7, …, v_{2n}n–v_{2n+1}$$ ), расстояние между которыми равно 0.1 единиц. И, наконец, построив полосу из треугольников, опирающуюся на вершины $$v_0, v_1, v_2, v_3$$, мы получим ломаную линию толщиной 0.1 пикселей, точно аппроксимирующую график косинуса (рисунок 2.34).
(рис 2.34) Построение полосы из треугольников По ширине график косинуса будет вписан в клиентскую область окна, а по высоте наш график будет немного меньше высоты окна. Таким образом, в действительности наше приложение будет визуализировать не сам график $$y=\cos(x)$$, а несколько другую функцию, полученную путем масштабирования графика $$y=\cos(x)$$ вдоль осей X и Y:
где
[-1, +1] . Фактически это координата x графика, пробегающая с определенным шагом значения от левого до правого краев экрана, то есть от -1 до +1. Соответственно аргумент функции косинуса пробегает значения от 0 до 4 • 71 (0°...720°).[-0.7, +0.7] .Перебирая с определенным шагом значения координаты X от -1 до +1 и подставляя их в выражение (2.9), мы получим координаты набора точек $$p_0, p_1, … , p_n$$ (рисунок 2.34). Как говорилось выше, для получения координат вершин полосы треугольников $$v_0, v_1, …, v_{2n}, v_{2n+1}$$ необходимо симметрично отупить от точек $$p_0 … p_n$$ на 0.05 единиц.
Вроде бы все просто и понятно, если не считать одной мелочи: мы пока не еще определились, каким образом должны быть сориентированы отрезки $$v_0-v_1, v_2-v_3, …, v_{2n}-v_{2n+1}$$ относительно точек $$p_0, p_1, … , p_n$$. Не мудрствуя лукаво, мы сделаем эти отрезки параллельными оси Y и посмотрим, что из этого выйдет:
и т.д.
Основные фрагменты приложения (Ex16) приведены в листинге 2.26.
public partial class MainForm : Form
{
// Количество сегментов в ломаной линии,
аппроксимирующей график косинуса
const int QuadStrips = 100;
// Число треугольников в ломанной линии
const int TriStrips = QuadStrips * 2;
// Массив вершин
VertexPositionColor[] vertices = null;
// Режим закраски треугольников
FillMode fillMode = FillMode.Solid;
private void MainForm_Load(object sender, EventArgs e)
{ ...
// Создаем массив вершин для хранения вершин
полоски из треугольников vertices = new
пVertexPositionColor[TriStrips+2];
// Перебираем вершины полоски из треугольников
for (int i = 0; i <= QuadStrips; i++)
{
// Определяем текущее значение координаты x вершины
float x = -1.0f + 2.0f * (float) i / (float) QuadStrips;
// Вычисляем значение косинуса, соответствующее координате x
float angle = 2.0f * (float)Math.PI * x;
float cos = (float)Math.Cos(angle);
// Вычисляем значение координаты y вершины по формуле 2.9
float y = 0.6f * cos;
// Вычисляем красную и зеленую составляющую
цвета по формулам 2.3 (см. практическое
// упражнение 2.2)
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);
// Заносим в массив координаты вершины v[i*2] (см. выражение 2.10)
vertices[i * 2] = new VertexPositionColor(new
Vector3(x, y - 0.05f, 0.0f), new
XnaGraphics.Color(red, green, 0));
// Заносим в массив координаты вершины v[i*2+1]
vertices[i * 2 + 1] = new VertexPositionColor
(new Vector3(x, y + 0.05f, 0.0f), new
XnaGraphics.Color(red, green, 0));
};
}
private void MainForm_Paint(object sender, PaintEventArgs e) {
device.Clear(XnaGraphics.Color.DarkSlateGray);
// Выключаем отсечение невидимых треугольников
device.RenderState.CullMode = CullMode.None;
// Задаем режим показа треугольников
device.RenderState.FillMode = fillMode;
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();
}
}
Скомпилируйте и запустите приложение на выполнение. Как и ожидалось, на экране появится график функции y=cos(x) , однако толщина графика будет переменной, причем максимальная толщина графика будет достигаться в окрестностях точек, в которых функция cos(x) принимает значения -1 или 1. (рисунок 2.35).
Примечание
Следующий материал этого раздела содержит довольно много математических выкладок, поэтому если вы не в ладах с математикой, можете смело пропустить оставшуюся часть раздела 2.6.3.
(рис 2.35) График функции y=cos(x) переменной толщины График y=cos(x) постоянной толщины
И так, попытка использования отрезков $$v_0–v_1, …, v_{2n}–v_{2n+1}$$ параллельных оси Y закончилась неудачей. Ну что ж, отрицательный результат, это тоже результат. Попробуем поэкспериментировать ориентацией отрезков $$v_0–v_1, …, v_{2n}–v_{2n+1}$$, например, развернув их под углом 45° (рисунок 2.36):
// Вычисляем вектор смещения вершин v[2*i+1] относительно p[n] float nx = (float) (5.0 / Math.Sqrt(2.0)); float ny = nx; // Симметрично смещаем вершины на 5 пикселей в направлении векторов (-1, -1) и (+1, +1) 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));
(рис 2.36) График функции y=cos(x) переменной толщины. Вершины смещаются в направлении векторов (-1, -1) и (+1, +1)Проведя несколько экспериментов, мы придем к выводу, что график косинуса имеет необходимую толщину только в там, где отрезки $$v_{2i} –v_{2*i+1}$$ перпендикулярны графику косинуса. Следовательно, чтобы график функции имел постоянную толщину 0.1 единиц, все отрезки $$v_{2i} –v_{2*i+1}$$ должны быть перпендикулярны графику косинуса.
Для нахождения координат вершин отрезка $$v_{2i} –v_{2*i+1}$$ длиной 0.1 единиц, проходящего через точку $$p_i$$, перпендикулярно графику функции необходимо выполнить следующие действия:
s перпендикулярный графику функции в точке pi.pi вектора $$\ovepline n $$ и $$- \ovepline n $$.Рассмотрим эти шаги более подробно. Как вы знаете из курса аналитической геометрии, вектор, перпендикулярного графику функции, определяются по формуле:
$$\overline S=(\frac{df(x,y)}{dx}*\frac{df(x,y)}{dy}) $$где
Чтобы определить значение вектора s для нашей функции (2.9), перепишем ее в неявной форме $$f(x, y)=0$$:
$$y-0.6*\cos(2*\pi*x)=0 $$Теперь найдем частные производные, являющиеся координатами вектора $$s:$$
$$\overline S=\overline{(S_x,S_y)}// S_x=\frac {d(y-0.6*\cos(2*\pi*x))}{dx}=1.2*\pi*\sin(2*\pi*x)// S_x=\frac {d(y-0.6*\cos(2*\pi*x))}{dy}=1 $$Нахождение производных в среде MathCAD
Если вы немного подзабыли высшую математику, не огорчайтесь. Для нахождения производных можно воспользоваться, к примеру, математическим пакетом MathCAD.
Для вычисления значения производной средствами символьной математики пакета MatCAD просто наберите выражение производной, которую выходите вычислить. Затем введите специальный символ ? (Ctrl + .) и нажмите Enter, после чего справа от выражения появится вычисленное значение производной (рисунок 2.37)
(рис 2.37) Математический пакет MathCAD Зная вектор $$\overline s$$ можно легко найти вектор $$\overline n$$ заданной длины, параллельный вектору $$\overline s$$:
$$\overline n=\alpha*\frac{\overline S}{|\overline S|} $$где
После этого определить координаты точек $$v_2i$$ и $$v_{2i+1}$$ не составит труда:
$$V_{2-i}=P_i-\overline n// V_{2-i+1}=P_i+\overline n $$Имея под рукой формулы 2.13, 2.14 и 2.15 мы можем легко исправить ошибку в примере Ex16 (визуализация графика функций переменной толщины вместо постоянной) путем небольшой модификации фрагмента обработчика события Paint (листинг 2.27). Полная версия приложения находится в example.zip в каталоге Ex02\Ex17
for (int i = 0; i <= QuadStrips; i++)
{
float x = -1.0f + 2.0f * (float) i / (float) QuadStrips;
float angle = 2.0f * (float)Math.PI * x;
float cos = (float)Math.Cos(angle);
float y = 0.6f * cos;
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);
// Вычисляем вектор s
float sx = (float)(1.2 * Math.PI * Math.Sin(2.0 * Math.PI * x));
float sy = 1.0f;
// Вычисляем длину вектора s
float length = (float)Math.Sqrt(sx * sx + sy * sy);
// Вычисляем вектор nx
float nx = sx / length * 0.05f;
float ny = sy / length * 0.05f;
// Заносим в графический буфер координаты
вершин v[i*2] и v[i*2+1] (вычисляются по формуле
// 2.15)
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));
};
(рис 2.38) График функции y=cos(x) постоянной толщины Все изменения обработчика события Paint сводятся к добавлению пяти новых строк кода и косметической правке двух строк. Как говорится, дело в количестве строк, а в математических формулах, которые заложены в эти строки. Результат работы приложения приведен на рисунке 2.38.
Визуализация CD-диска
Область применения полос из треугольников не ограничивается визуализацией полосок определенной толщины, ведь примитивы PrimitiveType.TriangleStrip активно используются для визуализации самых разнообразных геометрических объектов. В листинге 2.28 приведены основные фрагменты исходного кода примера, визуализирующий на экране CD диск (рисунок 2.39).

(рис 2.28) CD-диск, нарисованный с использованием полосы из 200 треугольников (рис 2.39) public partial class MainForm : Form { // fx-файл const string effectFileName = "Data\\ColorFill.fx"; // Количество сегментов в диске const int slices = 100; // Радиус внутренней границы CD-диска const float innerRadius = 0.2f; // Радиус внешней границы CD-диска const float outerRadius = 0.7f; // Режим закраски треугольников FillMode fillMode = FillMode.Solid; ... private void MainForm_Load(object sender, EventArgs e) { ... // Выделяем память для хранения вершин диска vertices = new VertexPositionColor[slices * 2 + 2]; // Перебираем вершины CD диска for (int i = 0; i <= slices; i++) { // Вычисляем текущий угол ? float angle = (float)i / (float)slices * 2.0f * (float)Math.PI; // Вычисляем вспомогательные переменные float sin = (float)Math.Sin(angle); float cos = (float)Math.Cos(angle); // Вычисляем координаты вершины внутренней границы CD диска float x = innerRadius * sin; float y = innerRadius * cos; // Добавляем вершину внутренней границы диска в массив вершин vertices[i * 2] = new VertexPositionColor(new Vector3(x, y, 0.0f), XnaGraphics.Color.White); // Вычисляем координаты вершины внешней границы CD диска x = outerRadius * sin; y = outerRadius * cos; // Вычисляем цвет вершины byte red = (byte)(255 * Math.Abs(Math.Sin(angle * 3))); byte green = (byte)(255 * Math.Abs(Math.Cos(angle * 2))); // Добавляем вершину внешней границы диска в массив вершин vertices[i * 2 + 1] = new VertexPositionColor(new Vector3(x, y, 0.0f), new XnaGraphics.Color(red, green, 0)); }; } private void MainForm_Paint(object sender, PaintEventArgs e) { ... device.Viewport = Helper.FullScreenViewport(ClientSize); device.Clear(ClearFlags.Target, Color.CornflowerBlue, 0.0f, 0); device.Viewport = Helper.SquareViewport(ClientSize); device.RenderState.CullMode = CullMode.None; device.RenderState.FillMode = fillMode; device.VertexDeclaration = decl; // Рисуем CD диск 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(); } ... } // Обработчик событий от клавиатуры, переключающий режимы закраски при нажатии клавиши Space // (Пробел) private void MainForm_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Space) { if (fillMode == FillMode.Solid) fillMode = FillMode.WireFrame; else fillMode = FillMode.Solid; Invalidate(); } } }
Нетрудно заметить, что пример Ex18 является тривиальной модификацией примера Ex09 из раздела 2.5.2 (круг, переливающийся разнообразными цветами). CD-диск рисуется при помощи замкнутой полосы из треугольников,
(рис 2.40) CD-диск, состоящий из 18-ти сегментов (36 треугольников), визуализированный в каркасном режиме Визуализация квадрата с круглым отверстием
В следующем примере ( Ex19 ) мы нарисуем более интересное изображение: квадрат, внутри которого вырезана дырка в виде окружности (рисунок 2.41). Для упрощения задачи в качестве отправной точки будет использоваться исходный код примера Ex18.
(рис 2.41) Квадрат с круглым отверстием На первый взгляд между квадратом и кругом практически нет ничего общего: граница круга ограничивается окружностью, а граница квадрата – четырьмя отрезками равной длины. Однако вспомним аналитическую геометрию. Граница квадрата, стороны которого расположены под углом 45° к осям X и Y, может быть описана следующими формулами:
где
Math.Sign в .NET.Таким образом, для визуализации повернутого на 45° квадрата, в центре которого вырезано круглое отверстие, необходимо всего лишь немного подправить пример Ex19, подкорректировав формулу для расчета внешней границы CD диска (листинг 2.39).
for (int i = 0; i <= slices; i++)
{
float angle = (float)i / (float)slices * 2.0f * (float)Math.PI;
float sin = (float)Math.Sin(angle);
float cos = (float)Math.Cos(angle);
// Вычисляем вспомогательные переменные,
используемые при расчете координат вершин границы
// квадрата
float sin2 = sin * sin * Math.Sign(sin);
float cos2 = cos * cos * Math.Sign(cos);
// Вычисляем координаты вершин внутреннего
круглого отверстия и заносим в массив вершин
float x = innerRadius * sin;
float y = innerRadius * cos;
vertices[i * 2] = new VertexPositionColor(new
Vector3(x, y, 0.0f), XnaGraphics.Color.White);
// Вычисляем координаты внешней границы квадрата по формуле 2.16.
x = outerRadius * sin2;
y = outerRadius * cos2; // Вычисляем цвет вершины
byte red = (byte)(255 * Math.Abs(Math.Sin(angle * 3)));
byte green = (byte)(255 * Math.Abs(Math.Cos(angle * 2)));
// Заносим в информацию о вершине в массив вершин
vertices[i * 2 + 1] = new VertexPositionColor(new
Vector3(x, y, 0.0f), new
XnaGraphics.Color(red, green, 0));
};
(рис 2.42) Повернутый на 45° квадрат с отверстием, визуализированный в каркасном режиме. Фигура построена с использованием полосы из 40-ка треугольников Результат работы примера Ex21 приведен на рисунке 2.42. Теперь, нам необходимо развернуть эту фигуру на 45° относительно геометрического центра фигуры. Из курса аналитической геометрии вам должно быть известно, что при повороте изображения на угол $$\varphi$$ относительно точки с координатами x0, y0, координаты всех точек изображения трансформируются по следующим формулам:
где
$$x_0, y_0$$ - координаты точки, относительно которой выполняется поворот
$$\qquard x',\qquard y'$$ - старые координаты точки
$$\varphi$$ - угол поворота
Подставив вместо $$\qquard x'$$ и $$\qquard y'$$ выражения 2.16, а так же положив угол ( $$р$$ равным 45 $$\deg$$, мы получим следующие формулы, задающие координаты точек квадрата:
$$x=x_0+\frac{r}{\sqrt 2}*(\cos^2 \alpha*sign(\cos\alpha)+\sin^2*sign(\cos\alpha)) y=y_0+\frac{r}{\sqrt 2}*(\cos^2 \alpha*sign(\cos\alpha)-\sin^2*sign(\cos\alpha)) $$Модифицируем пример Ex19, изменив вычисление координат вершин внешней границы квадрата согласно выражению 2.18, и запустим программу на выполнение. В результате мы получим любопытную картину: в процессе поворота квадрата на 45° полоса треугольников как бы завинчивается по спирали. (рисунок 2.39) Это обусловлено тем, что мы повернули только внешнюю сторону полосы треугольников, забыв о внутренней стороне, имеющую форму круга. Для поворота круглого отверстия в квадрате на 45 $$\deg$$ мы скомбинируем выражения 2.17 и 2.2:
(рис 2.39) Квадрат, повернутый относительно внутреннего отверстия. Каркасный режим После доработки приложения согласно выражению 2.19 изображение наконец-то примет нормальный вид (рисунки 2.37 и 2.40). Исправленный код с учетом выражений 2.18 и 2.19 приведен в листинге 2.40 (Ex20):

(рис 2.40) Квадрат с круглым отверстием, визуализированный в каркасном режиме (рис 2.44) // Несколько увеличиваем размер квадрата и отверстия const float innerRadius = 0.285f; const float outerRadius = 1.0f; // Константа, используемая при расчете вершин квадрата readonly float sqrt2 = (float)Math.Sqrt(2.0); for (int i = 0; i <= slices; i++) { float angle = (float)i / (float)slices * 2.0f * (float)Math.PI; float sin = (float)Math.Sin(angle); float cos = (float)Math.Cos(angle); float sin2 = sin * sin * Math.Sign(sin); float cos2 = cos * cos * Math.Sign(cos); float x = innerRadius /sqrt2 * (cos + sin); float y = innerRadius / sqrt2 * (cos - sin) ; vertices[i * 2] = new VertexPositionColor(new Vector3(x, y, 0.0f), XnaGraphics.Color.White); x = outerRadius * outerRadius/sqrt2 * (cos2 + sin2); y = outerRadius * outerRadius / sqrt2 * (cos2 - sin2); byte red = (byte)(255 * Math.Abs(Math.Sin(angle * 3))); byte green = (byte)(255 * Math.Abs(Math.Cos(angle * 2))); vertices[i * 2 + 1] = new VertexPositionColor(new Vector3(x, y, 0.0f), new XnaGraphics.Color(red, green, 0)); };
Практическое упражнение №2.4
Выражения 2.18 и 2.19 являются частными случаями так называемой суперокружности, координаты точек которой задаются формулами:
$$x=x_0+\frac{r}{\sqrt2}*(|\cos\alpha|^n*sign(\cos\alpha)+|\sin\alpha|^n*sign(\cos\alpha)) y=y_0+\frac{r}{\sqrt2}*(|\cos\alpha|^n*sign(\cos\alpha)-|\sin\alpha|^n*sign(\cos\alpha)) $$где
$$x, y$$ - координаты текущей точки.
$$x_0, y_0$$ - координаты центра суперокружности.
$$r$$ - радиус суперокружности.
$$n$$ - степень суперокружности
$$\alpha$$ - угол, пробегающий с некоторым шагом все значения от 0° до 360°.
При n=1, суперокружность приобретает форму обычной окружности (формула 2.19), а при n=2 - квадрата (формула 2.18). При других n суперокружность приобретает различные интересные формы.
Напишите приложение, визуализирующую фигуру, имеющую форму суперокружности, степень n которой равна 2.5. При этом в центре этой фигуры должно быть вырезано отверстие в форме суперокружности степени 1.3 (рисунок 2.45).
Если быть более точным, формула 2.20 задает суперокружность, повернутую на 45 градусов
(рис 2.45) Фигура, границы которой задаются двумя суперокружностями. Внешняя суперокружность имеет степень 2.5, а внутренняя 1.3.Практическое упражнение №2.5
Создайте приложение, визуализирующее фигуру Лиссажу, координаты точек которой задаются следующей формулой:
$$x = \sin(2*\alpha) y = \cos(3*\alpha) $$где
$$x, y$$ - координаты текущей точки
$$\alpha$$ - угол, пробегающий с определенным шагом значения от 0° до 360° (0…2· $$\pi$$ )
(рис 2.46) Фигура Листажу График функции должен быть визуализирован с использованием линии толщиной 10 пикселей (рисунок 2.46).
На всякий случай, ниже приведены математические выкладки, которые могут облегчить написание программы.
Для построения фигуры Лиссажу, вписанной в форму с небольшими отступами по краям, выражение 2.21 необходимо преобразовать к следующему виду:
$$x = 0.85*\sin(2-\alpha)\\ y = 0.85*\cos(3-\alpha) $$Для нахождения вектора касательной в точке x, y достаточно найти производные по а:
где
$$k$$ - вектор касательной
$$k_x, k_y$$ - компоненты этого вектора
Зная вектор касательной, можно легко найти вектор, перпендикулярный графику функции:
$$\overline{s}=\overline{(k_y,-k_x)}\\ s_x=k_y=-2.55*\sin(3*\alpha)\\ s_y=-k_x=-1.7*\cos(2*\alpha) $$Имея перпендикуляр к графику функции ( s ), вы сможете легко найти координаты вершин полосы из треугольников (по аналогии примером построения графика синуса из начала раздела).
Имея перпендикуляр к графику функции ( s ), вы сможете легко найти координаты вершин полосы из треугольников (по аналогии примером построения графика синуса из начала раздела).
В отличие от универсальных графических библиотек вроде GDI, XNA Framework поддерживает визуализацию весьма ограниченного набора примитивов: точки, отрезки и треугольники. Любой визуализируемый объект, какой сложной формы он не был, всегда аппроксимируется набором этих примитивов. Хотя подобный подход заметно усложняет жизнь разработчика, он позволяет достичь беспрецедентного уровня производительности (порядка нескольких сотен миллионов визуализированных примитивов в секунду).
Примитивы в процессе визуализации обрабатываются специализированными вершинными и пиксельными процессорами видеокарты. Вершинные процессоры предназначены для преобразования вершин, а пиксельные – для закраски примитивов. В DirectX и XNA Framework вершинные и пиксельные процессоры программируются с использованием семейства ассемблеро-подобных языков и . Каждый из этих языков оптимизирован под определенный тип . Например, язык был специально разработан для программирования пиксельных процессоров NV25 ( GeForce4 ). При загрузке шейдера программа, написанная на ассемблеро-подобном языке автоматически компилируется в машинный код текущего ; таким образом, языки и очень похожи на промежуточный язык IL в .NET.
По мере роста функциональности вершинных и пиксельных процессоров появилась потребность в языке программирования высокого уровня. Таким языком стал HLSL ( High Level ) – C-подобный язык программирования, предназначенный для программирования вершинных и пиксельных процессоров. Программа, написанная на HLSL, компилируется в промежуточный ассемблеро-подобный язык с использованием профиля, указывающего под какой ускоритель необходимо оптимизировать генерируемый код. Так, профиль ps_2_a указывает, что шейдер будет транслироваться в язык и оптимизирован для семейства NV3x (GeForce FX). Как правило, чем выше номер профиля, тем большая функциональность доступна программисту, однако платой за эту гибкость является рост требований приложения к функциональности (см. приложение 2).
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.