82
Глава 2. Визуализация примитивов. Процесс визуализации изображений в XNA Framework ощутимо отличаются от подхода, используемого в классическихдвухмерных библиотеках вроде GDI/GDI+. В XNA Framework все графические построения осуществляются с использованием простых фигур, называемых графическими примитивами. XNA Framework поддерживает три вида графических примитивов: точки, отрезки и треугольники. Эти примитивы очень просты в отображении, поэтому все современные графические ускорители могут рисовать их аппаратно с очень высокой скоростью. Например, видеокарта NVIDIA GeForce 8800 GTX может визуализировать порядка 400 миллионов треугольников в секунду 1 [С.7]. Каждый примитив задаётся набором вершин: точка одной в центре точки, отрезок двумя вершинами на концах отрезка, а треугольник тремя вершинами в углах треугольника. В XNA Framework координаты вершин обычно задаются тремя координатами x, y и z. Центр используемой системы координат расположен в центре клиентской области формы, ось положительное направление оси X направлено вправо, ось Y – вверх, а ось Z – вглубь экрана монитора (рисунок 2.1). Левый нижний угол формы имеет координаты (-1, -1, 0), верхний правый (+1, +1, 0). Рисунок 2.1. Система координат клиентской области формы при использовании XNA Framework. 2.1. Работа с вершинами примитивов. В пространстве имен Microsoft.Xna.Framework.Graphics имеется ряд структур для хранения информации о вершинах примитива. В настоявшее время для нас наиболее интересна структура VertexPositionColor, инкапсулирующая информацию о координатах и цвете вершины. Начнем рассмотрение этой структуры с конструктора: public VertexPositionColor(Vector3 position, Color color); где position координаты вершины; color цвет вершины. В процессе создания новой вершины ширина и высота автоматически заносятся конструктором в поля Position и Color структуры VertexPositionColor: public Color Color; public Vector3 Position; 1 Приведена производительность в реальных задачах. Вообще производители обожают указывать в описании видеокарты теоретическую пиковую производительность, практически не достижимую в реальных приложениях. В целом, пиковая производительность видеокарты аналогична максимальной скорости автомобилей или локомотивов. К примеру, электропоезд TGV-A теоретически можно разогнать до 513 км/ч [15], однако на практике его средняя скорость не превышает 350 км/ч. 1

Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Embed Size (px)

Citation preview

Page 1: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Глава 2. Визуализация примитивов. Процесс визуализации изображений в XNA Framework ощутимо отличаются от подхода, используемого в “классических” двухмерных библиотеках вроде GDI/GDI+. В XNA Framework все графические построения осуществляются с использованием простых фигур, называемых графическими примитивами. XNA Framework поддерживает три вида графических примитивов: точки, отрезки и треугольники. Эти примитивы очень просты в отображении, поэтому все современные графические ускорители могут рисовать их аппаратно с очень высокой скоростью. Например, видеокарта NVIDIA GeForce 8800 GTX может визуализировать порядка 400 миллионов треугольников в секунду1 [С.7]. Каждый примитив задаётся набором вершин: точка – одной в центре точки, отрезок двумя вершинами на концах отрезка, а треугольник – тремя вершинами в углах треугольника. В XNA Framework координаты вершин обычно задаются тремя координатами x, y и z. Центр используемой системы координат расположен в центре клиентской области формы, ось положительное направление оси X направлено вправо, ось Y – вверх, а ось Z – вглубь экрана монитора (рисунок 2.1). Левый нижний угол формы имеет координаты (-1, -1, 0), верхний правый (+1, +1, 0).

Рисунок 2.1. Система координат клиентской области формы при использовании XNA Framework.

2.1. Работа с вершинами примитивов. В пространстве имен Microsoft.Xna.Framework.Graphics имеется ряд структур для хранения информации о вершинах примитива. В настоявшее время для нас наиболее интересна структура VertexPositionColor, инкапсулирующая информацию о координатах и цвете вершины. Начнем рассмотрение этой структуры с конструктора: public VertexPositionColor(Vector3 position, Color color);

где position – координаты вершины; color – цвет вершины. В процессе создания новой вершины ширина и высота автоматически заносятся конструктором в поля Position и Color структуры VertexPositionColor: public Color Color;

public Vector3 Position;

1 Приведена производительность в реальных задачах. Вообще производители обожают указывать в описании видеокарты теоретическую пиковую производительность, практически не достижимую в реальных приложениях. В целом, пиковая производительность видеокарты аналогична максимальной скорости автомобилей или локомотивов. К примеру, электропоезд TGV-A теоретически можно разогнать до 513 км/ч [15], однако на практике его средняя скорость не превышает 350 км/ч.

1

Page 2: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Здесь мы впервые встречаемся с новой для нас структурой Microsoft.XNA.Framework.Vector3, инкапсулирующей трехмерный вектор. Наряду с Vector3 в XNA Framework определены структуры Vector2 и Vector4, которые, как нетрудно догадаться, предназначены для работы с двухмерными и четырехмерными векторами. Структуры Vector2, Vector3, Vector4 широко используются в XNA Framework для хранения координат вершин, а так же выполнения различных математических векторных операций. Так как большая часть функциональности данных структур нам пока не нужна, мы отложим их подробное изучение до шестой главы. В конец концов, с точки зрения логики работы наших первых приложений структуры Vector2, Vector3 и Vector4 представляют собой всего лишь расширенную версию структуры System.Drawing.PointF. Информация обо всех вершинах примитива хранится в массиве. Например: // Вершин примитива VertexPositionColor[] vertices;

Казалось бы, всё должно быть очень просто, если бы не один нюанс. Дело в том, что при визуализации примитивов информация о вершинах напрямую передается в графический процессор видеокарты (GPU – Graphics Processor Unit), который не имеет ни малейшего понятия об управляемом коде и, соответственно, формате структуры. Для разъяснения графическому процессору формата отдельных полей структуры применяются декларации формата вершины. В XNA Framework декларация вершины инкапсулируется классом VertexDeclaration, конструктор которого приведен ниже: public VertexDeclaration(GraphicsDevice graphicsDevice, VertexElement[] elements);

где graphicsDevice – графическое устройство, используемое для работы с вершинами elements – массив элементов (структур VertexElement) c описанием формата структуры. Описание формата структуры задается массивом elements, каждый элемент которого описывает одно поле структуры. Соответственно, количество элементов в массиве elements всегда равно количеству полей структуры. Какая информация содержится в каждом элементе массива elements? Это: 1. Адрес описываемого поля структуры (смещение от начала структуры). 2. Тип поля структуры (скаляр, вектор, упакованный вектор1). 3. Информация, содержащаяся в данном поле (координаты вершины, цвет вершины, текстурные

координаты2 и т.п.). 4. Некоторая другая служебная информация. Создание массива, описывающего структуру, является довольно монотонной и утомительной операцией. К счастью, разработчики XNA Framework встроили в структуру VertexPositionColor (а так же во все аналогичные структуры) статическое поле только для чтения, содержащее массив с описанием этой структуры: public static readonly VertexElement[] VertexElements;

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

2.2. Основы визуализации примитивов. Рассмотрим основные этапы визуализации примитивов, информация о вершинах которых хранится в массиве структур VertexPositionColor. Сначала приложение должно создать декларацию вершины на основе описания, содержащегося в нашей структуре VertexTransformedPositionColor: VertexDeclaration decl; … decl = new VertexDeclaration(device, VertexTransformedPositionColor.vertexElements);

Эту операцию достаточно выполнять один при запуске приложения, например, где-нибудь в обработчике события Load формы. Код визуализации примитива следует поместить в обработчик события Paint. Перед тем, как приступить к визуализации примитивов, необходимо задать формат вершин примитива, присвоив свойству VertexDeclaration класса Device декларацию формата вершины, созданную в обработчике события Load.

1 Примером упакованного вектора является 32-х битное число, содержащее информацию о яркости красного, синего и зеленого компонентов цвета. 2 Текстурные координаты будут рассмотрены в разделе 5.X.

2

Page 3: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Собственно визуализация примитивов выполняется методом 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 – количество примитивов, которые будут визуализированы. Резюмируем всё вышесказанное. Для визуализации примитива приложение должно выполнить следующие шаги:

1. Создать массив с информацией о вершинах примитива. 2. Создать декларацию формата вершины. В большинстве случаев первые два шага логичнее всего

выполнять один раз при запуске приложения, например, в обработчике события Load. 3. В обработчике события Paint первым делом необходимо очисть экран методом

GraphicsDevice.Clear. 4. Поместить в массив координаты и цвет вершин (если координаты вершин не меняются в процессе

выполнения приложения, эту операцию разумно будет вынести в метод Load). 5. Указать декларацию формата вершины, присвоив свойству GraphicsDevice. VertexDeclaration

декларацию, созданную на втором этапе. 6. Нарисовать набор примитивов вызовом метода GraphicsDevice.DrawUserPrimitives. 7. Показать полученное изображение на экране, переключив буферы методом Device.Present.

Однако, это ещё не всё. Дело в том, что все современные GPU содержан специализированные векторные процессоры, используемые для трансформации вершин и закраске примитивов. Так как эти процессоры принимают участие при визуализации при любых примитивов, приложение должно запрограммировать их на выполнения требуемых преобразований. Если этого не сделать, результат вызова метода DrawUserPrimitives будет не предсказуемым1. Программирование вершинных и пиксельных процессоров будет подробно рассмотрено в следующем разделе.

2.3. Введение в HLSL В этой разделе мы познакомимся с языком High Level Shader Language (язык высокого уровня для программирования шейдеров), или сокращённо HLSL. HLSL используется для программирования вершинных и пиксельных процессоров графического ускорителя. Программа для вершинного процессора называется вершинным шейдером, а для пиксельного процессора – пиксельным шейдером. Поддержка шейдеров впервые появилась в 8-й версии DirectX. Правда шейдеры DirectX 8 имели множество ограничений и программировались на низкоуровневом ассемблеро-подобном языке, однако в 9-й версии DirectX возможности шейдеров значительно возросли, что привело появлению к подробности в языках высокого уровня. Было создано несколько языков высокого уровня для написания шейдеров DirectX2, однако стандартом де-факто стал язык HLSL, входящий в состав DirectX 9. В XNA Framework шейдеры так же пишутся языке HLSL, а сам XNA Framework при работе с шейдерами на платформе Windows в значительной степени опирается на функциональность DirectX.

1 Вызов метода DrawUserPrimitives без явного задания вершинных и пиксельных шейдеров привет к генерации исключения System.InvalidOperationException с сообщением Both a valid vertex shader and pixel shader (or valid effect) must be set on the device before draw operations may be performed. 2 Например, NVIDIA Cg [К.16], [С.3]

3

Page 4: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Так язык HLSL тесно связан с архитектурой графического процессора, мы начнём этот раздел с знакомства с основами архитектуры современного графического процессора.

2.3.1. Графический конвейер В разделе 2.2 вы получили представлении о визуализации примитивов средствами XNA Framework. При этом собственно процесс визуализации изображения (метод GraphicsDevice.DrawUserPrimitives) оставался для нас чёрным ящиком. Настало время наверстать упущенное. И так, при вызове метода GraphicsDevice.DrawUserPrimitives вершины из графического буфера поступают на обработку в графический конвейер XNA Framework, представляющий собой последовательность ступней (простых операций), выполняемых над вершинами в определённом порядке (рисунок 2.2). Рассмотрим эти ступени в порядке выполнения:

Рисунок 2.2. Упрощенная схема графического конвейера

1. Вначале вершины обрабатываются вершинным процессором по программе, называемой вершинным шейдером. На выходе из вершинного процессора получаются так называемые трансформированные (преобразованные) вершины. К вершинам могут быть “привязаны” различные параметры: цвет вершины, текстурные координаты1 и так далее. Координаты трансформированных вершин задаются в логической системе однородных координат, называемой clip space. Однородные координаты вершины определяются четырьмя числами: (x, y, z, w). Перевод однородных координат в обычные геометрические

осуществляется путём деления первых трех компонентов на четвертый компонент w: ),,(wz

wy

wx

.

Например, вершине с однородными координатами (1, 2, 3, 4) в трёхмерном пространстве соответствует

точка с координатами )75.0,5.0,25.0()43,

42,

41( = Использование четвертого компонента обусловлено

рядом особенностей алгоритмов визуализации трехмерных изображений, используемых в 3D графике. При визуализации двухмерных изображений компонент w обычно полагают равным 1. В этом случае

1 Текстурные координаты будут рассмотрены в разделе 5.x.

4

Page 5: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

нижнему левому углу клиентской области формы соответствует точка с координатами (-1, -1, 0, 1), правому верхнему углу клиентской области – (1, 1, 0, 1), а центру клиентской области – соответственно (0, 0, 0, 1).

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

Примечание DirectX позволяет программисту задавать координаты вершин в оконных координатах. В этом случае, при вызове метода Device.DrawUserPrimitives вершины сразу поступают на третью стадию графического конвейера, минуя первую и вторую стадии. Managed DirectX и XNA Framework Beta 1 позволяют задавать координаты в оконной системе координат, однако начиная с XNA Framework Beta 2 эта функциональность почему-то пропала. По видимости, это обусловлено стремлением сделать XNA Framework как можно более платформо-независимый.

3. Далее идёт сборка примитивов. На этой стадии вершины объединяются в примитивы. Тип примитивов определяется первым параметром метода GraphicsDevice.DrawUserPrimitives. Так при использовании параметра PrimitiveType.TriangleStrip вершины трактуются, как опорные точки (вершины) полосы треугольников. При этом каждый треугольник из полосы является независимым примитивов и обрабатывается независимо от других треугольников этой полосы. Полосы треугольников подробно будут рассмотрены в разделе 2.6.3.

4. Затем происходит растеризация примитивов – преобразование каждого примитива в набор пикселей экрана. Параметры внутренних пикселей примитива (например, цвет) определяются путём интерполяции соответствующих параметров вершин вдоль поверхности примитива. Как мы увидим в следующих разделах, благодаря этой интерполяции при закраске треугольника с разноцветными вершинами образуются красивые цветовые переходы.

5. Следующий этап – обработка пикселей пиксельным процессором с использованием программы, называемой пиксельным шейдером. На вход пиксельному процессору подаются параметры пикселя (цвет, текстурные координаты и т.д.), полученные путём интерполяции соответствующих вершинных параметров вдоль поверхности примитива. После обработки входных параметров, пиксельный процессор возвращает цвет пикселя.

Технические подробности В современных графических процессорах имеется массив вершинных и пиксельных процессоров, что позволяет им одновременно обрабатывать несколько вершин и пикселей. Так графический процессор R580 корпорации ATI, используемый в видеокартах семейства ATI Radeon X800, имеет 6 вершинных и 16 пиксельных процессоров, соответственно, он может параллельно обрабатывать до 6-ми вершин и 16-ми пикселей. [С.8]

6. Полученные цвета пикселей заносятся в кадровый буфер. При этом возможно выполнение некоторой простой постобработки изображения вроде смешивания цветов при эффекте полупрозрачности.

В заключении стоит отметить, что этот логический конвейер DirectX не обязательно соответствует физической организации видеокарты. К примеру, видеокарта NVIDIA GeForce 8800 GTX, основанная на GPU G80, содержит 8 универсальных блоков, которые могут выполняться как вершинные, так и пиксельные шейдеры [С.7]. После прочтения этого раздела у вас, возможно, сложились несколько сумбурные представления графическом конвейере. Ничего страшного – в следующем разделе вы познакомитесь с языком HLSL и напишете несколько шейдеров, после чего всё встанет на свои места.

Дополнительная информация Все современные графические подсистемы построены по принципу конвейера. Идея конвейера, впервые реализованная Генри Фордом, заключается в следующем: если сложный процесс разбить на последовательность простых операций (конвейер), то на выходе конвейера мы получим производительность равную производительности самой медленной операции в этой цепочке. В качестве примера конвейера рассмотрим процесс производства популярных процессоров AMD Athlon . Создание одного процессора занимает около двух месяцев. Соответственно, при классической организации производственного процесса одно подразделение может выпускать около шести процессоров в год. Однако на современных фабриках AMD производственный процесс разбивается на 400 стадий. В процессе производства каждый будущий процессор проходит через все эти 400 стадий. Как только процессор проходит текущую стадию производства, на его место приходит следующий. В итоге, на различных стадиях производства одновременно может находиться до 400 процессоров. В итоге, на выходе конвейера получается

5

Page 6: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

производительность труда порядка 400 процессоров в два месяца или 2400 процессоров в год. Иными словами производительность труда вырастает примерно в 400 раз. Стороннему наблюдателю может показаться, что за день один конвейер производит около 7-ми процессоров (400/60). Но в реальности, между поступлением заготовки процессора и выходом готового процессора по-прежнему проходит два месяца. Это явление получило название латентность конвейера. При нормальном функционировании конвейера на это обстоятельство можно не обращать внимания; однако в случае неполадок латентность конвейера не замедлит проявиться. Предположим, что была обнаружена и исправлена очень опасная ошибка в архитектуре процессора, после чего исправленная версия процессора немедленно поступила в производство. Но, не смотря на всю оперативность исправления ошибки, первые исправленные образцы процессоров выйдут с конвейера лишь через два месяца. А ведь подобная задержка может принести фирме заметные убытки… Другое следствие латентности – низкая эффективность конвейера при производстве небольших партий процессоров. К примеру, при производстве одного процессора темп производства будет равен 0.017 процессоров в день (один процессор за 60 дней), при производстве 28 процессоров – 0.44 процессора в день, при 100 процессорах - уже 1.33 процессоров в день и т.д. Более-менее, нормальный темп будет достигнут только при производстве партии из нескольких тысяч процессоров (рисунок 2.3). К слову, графический конвейер не является исключением из правил. Он также малоэффективен при визуализации небольшого количества примитивов. Поэтому для эффективного использования графического конвейера программист должен стараться минимизировать количество вызов метода GraphicsDevice.DrawUserPrimitives, визуализируя за один присест как можно больше примитивов.

Рисунок 2.3. Зависимость производительности конвейера от количества выпускаемых процессоров. Производительность оценивается по числу процессоров, выпускаемых в среднем за сутки.

2.3.2. Язык HLSL В начале XXI века корпорация 3dfx работала над революционным GPU Rampage, имеющим на борту массив вершинных и пиксельных процессоров. Для программирования этих процессоров Microsoft в тесном сотрудничестве с 3dfx разработала два похожих ассемблеро-подобных языка, которые были включены в DirectX 8. Язык для программирования вершинных процессоров получил название Vertex Shader 1.0 (VS 1.0), а язык для программирования пиксельных процессоров – Pixel Shader 1.0 (PS 1.0) [С.4]. Соответственно, программы, написанные на этих языках, стали называться вершинными и пиксельными шейдерами1. К сожалению, графический процессор Rampage так и не поступил в массовое производство по финансовым причинам: компания 3dfx была объявлена банкротом и вскоре куплена NVIDIA, а проект Rampage закрыт.

1 Название шейдер (shader) обусловлено применением первых вершинных и пиксельных процессоров преимущественно для более точной передачи игры света и тени (shade) на поверхности объектов.

6

Page 7: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Примечание Если быть более точным, зачатки пиксельных шейдеров1 впервые появились в GPU NV10 (1999 год). Однако по ряду причин Microsoft не захотела включить поддержку этих шейдеров в DirectX. В результате, с точки зрения DirectX-программиста, в NV10 отсутствует какая-либо поддержка шейдеров. Единственная возможность задействовать шейдеры NV10 – воспользоваться API OpenGL [К.17], [К.18].

Первым действительно массовым GPU с вершинными и пиксельными процессорами стал NV20 (NVIDIA GeForce3), появившийся в 2001 году. Для программирования вершинных и пиксельных процессоров NV20 корпорация Microsoft совместно с NVIDIA разработали языки Vertex Shader 1.1 и Pixel Shader 1.1, являющиеся расширенными версиями Vertex Shader 1.0 и Pixel Shader 1.0. Вскоре после NV20 вышел NV25 (GeForce4), функциональность пиксельных процессоров которого была несколько расширена. Соответственно язык Pixel Shader 1.1 был обновлён до версии 1.32. Потом появился процессор GPU R200 (Radeon 8500) корпорации ATI и язык Pixel Shader 1.4, затем R300 (Radeon 9700 Pro) с Vertex Shader 2.0 и Pixel Shader 2.0 и так далее (см. приложение 1). В итоге к началу 2002-го года на рынке творилась полная неразбериха среди языков программирования шейдеров. К счастью Microsoft предвидела подобный поворот, и поэтому заранее сделала языки Vertex Shader и Pixel Shader независимыми от системы команд графического процессора. Фактически каждая версия языка Vertex/Pixel Shader является языком программирования для некоторого виртуального процессора, приближенного к некоторому реальному прототипу. Компиляция шейдера в систему команд физического процессора происходит непосредственно перед загрузкой шейдера в GPU. Таким образом, языки Vertex Shader и Pixel Shader являются аналогами языка IL в .NET. Независимость языков Vertex Shader и Pixel Shader от системы команд физического процессора теоретически позволяет GPU выполнять любой ассемблерный код, независимо о версии шейдера. Например, GPU R200 корпорации ATI наряду с родными Pixel Shader 1.4 может выполнять Pixel Shader 1.0, Pixel Shader 1.1, Pixel Shader 1.2 и Pixel Shader 1.3. Это достигается путём перекомпиляции чужеродных шейдеров в родной код. К сожалению, обратное преобразование не всегда возможно. Например, R200 не может выполнять Pixel Shader 2.0, так как программа, использующая продвинутые возможности этой версии шейдеров не может быть втиснута в прокрустово ложе архитектуры R200. По мере роста возможностей GPU программы для вершинных и пиксельных процессоров становились всё сложение и сложнее. Например, если в Pixel Shader 1.1 длина программы не могла превышать 16 ассемблерных команд, то в Pixel Shader 2.0 максимально возможное число ассемблерных инструкций превысило сотню. Соответственно возрастала трудоёмкость разработки и поддержки шейдеров с использованием ассемблера-подобного языка. Таким образом, возникла реальная потребность в переходе на языки программирования шейдеров высокого уровня. В 2002 году Microsoft выпустила высокоуровневый язык программирования шейдеров High Level Shader Language (HLSL). HLSL – это язык программирования высокого уровня, предназначенный для написания программ (шейдеров) для вершинных и пиксельных процессоров. HLSL является C-подобным языком программирования с многочисленными заимствованиями из C++ и C#. В тоже время в HLSL имеется ряд важных расширений, полезных при программировании графического процессора. Программа, написанная на HLSL, компилируется в один из ассемблеро-подобных языков DirectX. Таким образом, процесс компиляции HLSL программы очень напоминает компиляцию C#-программы сначала на промежуточный язык (IL), а затем в машинный для конкретного центрального процессора (рисунок 2.4).

Программа на HLSL

Ассемблеро-подобный язык

(Vertex/Pixel Shader)

Машинный код для графического

процессора

Рисунок 2.4. Компиляция HLSL-программы.

Самой крупной логической единицей HLSL является эффект (Effect), хранящийся в отдельном текстовом файле с расширением .fx. В принципе, эффект можно считать аналогом материала в 3DS MAX. Каждый эффект состоит из одной или нескольких техник (technique). Техника – это способ визуализации материала. Например, эффект визуализации мраморного материала может содержать три техники для различных графических процессоров: технику High для ускорителей класса High End, Medium для ускорителей

1 Эти шейдеры получили неофициальное обозначение Pixel Shader 0.5 2 Для программирования пиксельных процессоров NV25 (GeForce4) планировалось использовать язык Pixel Shader 1.2. Однако после выхода NV25 оказалась, что его функциональность несколько шире, чем предполагалось. Соответственно язык Pixel Shader 1.2 оказался не удел, и вскоре был обновлён до версии 1.3.

7

Page 8: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

среднего класса, и Low – максимальная производительность при низком качестве изображения1. Каждой технике сопоставлен пиксельный и вершинный шейдер, при этом несколько техник могут использовать общий шейдер.

Типы данных Как известно, лучший способ изучить новый язык программирования – написать на нём несколько программ. Так мы и поступим. Для начала мы создадим простейший эффект, закрашивающий примитив цветом морской волны (aqua). Эффект будет содержать одну технику, которую мы назовём Fill. И так, приступим. Мы начнём с написания программы для вершинного процессора: вершинного шейдера. Наш шейдер будет принимать в качестве параметра координаты вершины в обычных декартовых координатах, а возвращать координаты вершины уже в однородных координатах. Всё преобразование будет сводиться к добавлению к координатам вершины четвёртого компонента (w), равного 1 (листинг 2.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 (таблица 1.1).

Таблица 1.1. Скалярные типы

Тип Описание

bool Логический тип, который может принимать значения true или false

int 32-х битное целое число

half 16-ти битное число с плавающей точкой

float 32-х битное число с плавающей точкой

double 64-х битное число с плавающей точкой

Задавая тип переменной, вы просто указываете компилятору, что вы хотели бы использовать переменную этого типа. Если текущий ускоритель не поддерживает некоторые типы данных, используемые в программе, то при компиляции шейдера в машинный код они будут заменены ближайшими аналогами2. Например, тип double может быть заменён на тип float, half или какой-нибудь иной внутренний тип3. Поэтому программист должен стараться избегать жёсткой привязки к точности и допустимому диапазону значений используемого типа данных. Особенно это актуально для типа int, так как подавляющее большинство современных ускорителей не поддерживают тип int, в результате чего он эмулируется посредством одного из вещественных типов. Допустим, у нас имеется следующий код: // a присваивается значение 5 int a = 5; // b должно быть присвоено значение 1 int b = a / 3; // c должно стать равно 2 int c = b * 2; 1 Количество техник и их названия могут быть произвольными. 2 Это лучше, чем прерывать работу программы с сообщением об ошибке. 3 Например, GPU семейства NV3x (GeForce FX) в дополнение к half и float поддерживают 12-битный вещественный формат с диапазоном значений от -2 до 2. А GPU ATI R3xx/R4xx поддерживают 24-х битный формат с плавающей запятой. Подробную информацию о типах данных, поддерживаемых GPU корпораций ATI, NVIDIA и Intel, можно найти в приложении 5.

8

Page 9: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Какой код будет сгенерирован компилятором? Трудно дать однозначный ответ. В большинстве случаев компилятор просто заменяет типы 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. Единственным надежным решением является внимательный анализ кода ассемблерного кода шейдера1. Поэтому рекомендуется, по возможности, избегать использования типа int в коде шейдера за исключением переменных счетчиков цикла и индексов массивов.

Векторные типы. Большинство данных, используемых в трёхмерной графике, является многомерными векторами, размерность которых редко превышает 4. Так, координаты точки в трёхмерном пространстве задаются трёхмерным вектором, цвет пикселя – четырёхмерным вектором (три цвета и альфа-канал) и так далее. Соответственно, все современные GPU являются векторными процессорами, способными одновременно выполнять одну операцию сразу над набором из четырёх чисел (четырёхмерным вектором). В 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}

где

1 Основы ассемблероподобных языков Vertex Shader и Pixel Shader будут рассмотрены в четвертой главе.

9

Page 10: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

type – имя базового типа N – размерность вектора. Таким образом, вышеприведённое определение переменной v можно переписать следующим образом: float4 v;

Язык HLSL позволяет инициализировать вектор двумя способами. Первый способ – перечислить значения вектора в фигурных скобках на манер инициализации массивов в языке C. Ниже приведён пример, присвоения четырёхмерному вектору v начального значения 0.8) 0.6, 0.4, (0.2, .

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); // Создаём трёхмерный вектор. Конструктору в качестве параметра передаётся вектор “a” и число // 1.0. Соответственно вектору c будет присвоено значение (0.3, 0.4, 1.0) float3 c=float3(b, 1.0); // Создаём четырёхмерный вектор на основе скалярного типа и трёхмерного вектора. Итоговое // значение вектора d будет равно (0.7, 0.3. 0.2, 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.

Таблица 2.2. Некоторые семантики входных данных вершинного шейдера

Семантика Описание

POSITION Координаты вершины

COLOR Цвет вершины

PSIZE Размер точки (при визуализации набора точек)

Для связи параметра функции с входными данными шейдера, после объявления параметра укажите знак двоеточия и название соответствующей семантики. Таким образом, для связи параметра pos функции MainVS с координатами вершины необходимо использовать семантику POSITION (листинг 2.2).

Листинг 2.2.

float4 MainVS(float3 pos:POSITION) { return float4(pos, 1.0); }

10

Page 11: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Теперь нам надо указать, что функция MainVS возвращает трансформированные координаты вершины. Для этого в HLSL используются семантики выходных данных вершинного шейдера. В частности, для указания того факта, что шейдер возвращает трансформированные координаты вершины используется семантика POSITION (листинг 2.3).

Листинг 2.3.

float4 MainVS(float3 pos:POSITION):POSITION { return float4(pos, 1.0); }

Вот теперь мы наконец-то получили полноценный вершинный шейдер. Следующий этап – написание пиксельного шейдера. Наш первый пиксельный шейдер будет просто закрашивать все пиксели цветом морской волны (aqua) (листинг 2.4).

Листинг 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).

Листинг 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 {используемый профиль} {пиксельный шейдер};

11

Page 12: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Профиль шейдера (shader profile) задаёт промежуточный ассемблеро-подобный язык, на который будет скомпилирован шейдер. Кроме того, профиль задаёт некоторые архитектурные особенности целевого графического процессора, которые будут учтены компилятором при генерации промежуточного ассемблерного кода. В большинстве случаев каждой версии шейдеров соответствует один профиль. Например, языку Vertex Shader 1.1 соответствует профиль vs_1_1; Pixel Shader 1.4 – профиль ps_1_4, Pixel Shader 2.0 – профиль ps_2_0 и так далее. Однако некоторым языкам вроде Pixel Shader 2.x соответствует два профиля: в данном случае это ps_2_a и ps_2_b, при этом первый профиль генерирует код Pixel Shader 2.x, оптимизированный под архитектуру NV3x, а второй – для R4xx. В таблицах 2.3 и 2.4 приведено соответствие между профилями и соответствующими версиями шейдеров.

Таблица 2.3. Профили вершинных шейдеров

Профиль Версия вершинных шейдеров

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

Таблица 2.4. Профили пиксельных шейдеров

Профиль Версия пиксельных шейдеров

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

Большинство видеокарт поддерживает несколько профилей вершинных и пиксельных шейдеров (см. приложение 2). В результате каждый разработчик сталкивается с проблемой выбора используемого профиля. В большинстве случаев выбор версии шейдеров определяется минимальными требованиями к приложению. Допустим, необходимо, чтобы наша программа могла работать на видеокартах класса ATI Radeon 9500 (R3xx) и выше, 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.

Примечание GPU семейства NV3x демонстрируют очень низкую производительность при использовании профилей пиксельных шейдеров ps_2_0 и ps_2_a ([С.5], [С.6]). Если для вас актуальна производительность вашего приложения на этих GPU, то имеет смысл стараться по возможности использовать профиль ps_1_4 вместо ps_2_0. Другой вариант – предусмотреть отдельные упрощённые техники для NV3x, использующие профили ps_1_4.

В примерах этой книги я буду стараться использовать минимальную версию профилей, необходимую для нормальной компиляции шейдеров. В частности, именно по этой причине, наш эффект Fill использует профили vs_1_1 и ps_1_1: это позволит работать нашему эффекту даже на стареньких видеокартах семейства GeForce3 (NV20).

12

Page 13: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

И так, у нас есть вершинный и пиксельный шейдеры, а так же техника Fill, использующая эти шейдеры. Для получения готового эффекта осталось только помесить их в файл с расширением *.fx, например, в SimpleEffect.fx (листинг 2.6).

Листинг 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#-приложениях.

2.3.3. Использование эффектов в XNA Framework Одним из основных классов XNA Framework, предназначенным для работы с эффектами, является класс Effect. Класс Effect является довольно сложным классом, содержащим ряд коллекций, отражающих структуру файла эффекта (рисунок 2.5). Как говорилось в прошлом разделе, в каждом эффекте HLSL имеется несколько техник, которые в свою очередь содержат несколько проходов. При этом минимально возможный эффект включает хотя бы одну технику и один проход. Соответственно, класс Effect содержит коллекцию Techniques с экземплярами классов EffectTechnique, инкапсулирующих техники. В свою очередь, каждая техника содержит коллекцию Passes экземпляров класса EffectPass с информацией об эффекте.

Рисунок 2.5. Коллекции класса Effect.

Загрузка и компиляция файла эффекта. Загрузка эффекта из файла *.fx с последующей компиляцией осуществляется при помощи статического метода Effect.CompiledEffect: public static CompiledEffect CompileEffectFromFile(string effectFile, CompilerMacro[] preprocessorDefines, CompilerIncludeHandler includeHandler, CompilerOptions options, TargetPlatform platform);

13

Page 14: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

где effectFile – имя файла с эффектом. preprocessorDefines – массив макроопределений (аналогов директивы #define в C#), используемых при компиляции эффекта. Мы будем использовать значение null.

includeHandler – объект, используемый для обработки директив #include в fx-файле. Так как наш файл не содержит директив #include, мы будем использовать значение null.

options – опции компилятора HLSL, которые задаваемые с использованием перечислимого типа CompilerOptions (таблица 2.5.). Члены типа CompilerOptions являются битовыми флагами, что позволяет комбинировать их с использованием оператора |. В качестве этого параметра, как правило, передаётся значение CompilerOptions.None.

platform – значение перечислимого типа TargetPlatform, указывающее платформу, для которой компилируется эффект. В XNA Framework 1.0 поддерживаются две платформы: TargetPlatform.Windows и TargetPlatform.Xbox360, названия которых говорят за себя. Все примеры этой книги будут использовать значение TargetPlatform.Windows.

Таблица 2.5. Некоторые члены перечислимого типа CompilerOptions.

Член перечисления Значение

None Нет никаких опций

Debug Вставляет в ассемблерный код отладочную информацию

NotCloneable(*) Запрещает клонирование (создании копии) эффекта при помощи метод Clone. Эта опция уменьшает объём используемой памяти, так как в оперативной памяти не хранится информация, необходимая для клонирования эффекта. При этом экономия оперативной памяти достигает 50%.

ForceVertexShaderSoftwareNoOptimizations Форсирует компиляцию вершинного шейдера с использованием максимально возможной версии Pixel Shader (на момент написания книги это 3.0), не взирая на возможности текущего графического устройства.

ForcePixelShaderSoftwareNoOptimizations Форсирует компиляцию пиксельного шейдера с использованием максимально возможной версии Pixel Shader (на момент написания книги это 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

14

Page 15: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

{

// Сообщения о проблемах, возникших при компиляции эффекта

public string ErrorsAndWarnings { get; }

// Бил ли эффект откомпилирован удачно

public bool Success { get; }

// Если свойство Success равное true, содержит откомпилированный код эффекта

public byte[] GetEffectCode();

...

}

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

Примечание При желании приложение может сохранить откомпилированный байт-код в каком-нибудь файле, и при следующих запусках считывать из файла уже готовый откомпилированный байт-код. Кстати, Visual C# 2005 Express1 при компиляции проектов, использующих Content Pipeline, автоматически выполняет компиляцию fx-файлов проекта и сохраняет полученный промежуточный код в файлах с расширением nvb. Таким образом, приложениям, использующим Content Pipeline, нет нужды самостоятельно компилировать fx-файлы.

Следующий этап – компиляция байт-кода промежуточного языка в машинный код вершинных и пиксельных процессоров текущей видеокарты. Эта операция автоматически осуществляется конструктором класса Effect: public Effect(GraphicsDevice graphicsDevice, byte[] effectCode, CompilerOptions options, EffectPool pool);

где graphicsDevice – устройство Direct3D, которое будет использоваться для работы с эффектом effectFile – имя файла с эффектом. 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; } // Выполняет валидацию техники

1 В Visual Studio 2005 Pro эта функциональность в настоящее время не доступна.

15

Page 16: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

public bool Validate(); ... }

И так каждый эффект может содержать несколько техник. При этом некоторые техники эффекта могут нормально работать на текущем GPU, а некоторые (наиболее продвинутые) нет. Если требования техники (technique) превышают возможности текущего GPU (например, пользователь пытается запустить эффект использующий профиль ps_1_4 на NV2x), XNA Framework проигнорирует технику. В результате примитивы, использующие эту технику, будут отображаться некорректно1. Во избежание подобных неприятностей необходимо заранее проверить возможность выполнения данной техники средствами текущего графического устройства. Для этой цели в классе EffectTechnique предусмотрен метод Validate. Если техника может быть выполнена на текущем устройстве, метод Validate возвращает значение true, иначе – false. Во втором случае, приложение может попытаться подобрать альтернативную технику с меньшими системными требованиями или завершить приложение с сообщением о недостаточной “мощности” текущей видеокарты. Резюмируя всё вышесказанное можно предположить, что код для загрузки эффекта и выбора техники, как правило, имеет следующую структуру: GraphicsDevice device; ...

// Этот код обычно размещается в обработчике события 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;

Application.Idle += new EventHandler(Application_Idle);

return;

}

// Если эффект был скомпилирован с ошибками

if (!compiledEffect.Success)

{

// Выдаем сообщение об ошибке

MessageBox.Show(String.Format("Ошибка при компиляции эффекта: \r\n{0}",

compiledEffect.ErrorsAndWarnings), "Критическая ошибка", MessageBoxButtons.OK,

MessageBoxIcon.Error);

closing = true;

Application.Idle += new EventHandler(Application_Idle);

1 Как правило, такие примитивы просто закрашиваются чёрным цветом.

16

Page 17: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

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(Application_Idle);

return;

}

Визуализация объекта, использующего эффект. Визуализация примитивов, использующих эффект, начинается с вызова метода Effect.Begin: public void Begin();

Далее приложение должно перебрать все проходы (коллекция passes) текущей техники (CurrentTechnique) и для каждой техники: 1. Вызвать метод Pass текущего эффекта. 2. Визуализировать примитивы с использованием метода GraphicsDevice.DrawUserPrimitives. 3. Вызывать метод End текущего эффекта. По окончанию визуализации эффекта приложение должно вызвать метод Effect.End. В итоге код визуализации примитива выглядит следующим образом: Effect effect; ... // Фрагмент типового обработчика события Paint ... // Начинаем визуализацию примитивов с использованием эффекта effect. int passes = effect.Begin(); // Перебираем все проходы визуализации текущей техники foreach (EffectPass pass in effect.CurrentTechnique.Passes) { // Начинаем визуализацию текущего прохода effect.Begin(); // Визуализируем примитивы device.DrawUserPrimitives(...); ... device.DrawUserPrimitives(...); // Завершаем проход effect.End(); } // Заканчиваем визуализацию эффекта effect.End();

Ну что ж, этих знаний вполне достаточно, для того, чтобы попробовать свои силы в визуализации простых примитивов.

17

Page 18: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Примечание Как известно, оператор foreach, используемый нами для перебора коллекции проходов (effect.CurrentTechnique.Passes), обладает примерно на 3% более низкой производительностью по сравнению с классическим оператором for [С.9]. Однако при небольшом количестве итераций эта особенность не является сколь либо заметным недостатком.

2.4. Точки (PrimitiveType.PointList). Как известно, иногда лучше один раз увидеть, чем сто раз услышать. Эта простая истина как никогда подходит к XNA Framework с весьма запутанной технологией визуализации примитивов. Поэтому мы начнём изучение материала с разбора приложения, рисующего в центре экрана одну точку цвета морской волны (листинг 2.7).

Листинг 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 MainForm_Load(object sender, EventArgs e)

18

Page 19: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

{ // Стандартная процедура настройки параметров формы и создание графического устройства 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(Application_Idle); return; } // Если эффект не был удачно откомпилирован if (!compiledEffect.Success) { // Выводим сообщение об ошибках и предупреждениях из свойства ErrorsAndWarnings и завершаем // работу приложения MessageBox.Show(String.Format("Ошибка при компиляции эффекта: \r\n{0}", compiledEffect.ErrorsAndWarnings), "Критическая ошибка", MessageBoxButtons.OK, MessageBoxIcon.Error);

closing = true; Application.Idle += new EventHandler(Application_Idle); return; }

19

Page 20: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

// Создаем эффект на базе скомпилированного байт-кода. Обратите на использование флага // 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(Application_Idle); return; } } private void MainForm_Paint(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(); }

20

Page 21: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

// Оканчиваем визуализацию эффекта 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);

21

Page 22: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

// Описание формата вершины берется из поля VertexPositionColor decl = new VertexDeclaration(device, VertexPositionColor.VertexElements);

Далее обработчик события Load выполняет компиляцию fx-файла, после использует полученный байт-код для создания объекта эффекта: // Для сокращения объёма кода из него исключена обработка исключительных ситуаций. В реальных // приложениях так поступать категорически не рекомендуется, так как это значительно снизит // “дуракоустойчивость” вашего приложения. Поэтому настоятельно рекомендую ознакомится с // полной версией кода из листинга 2.7. CompiledEffect compiledEffect; // Компилируем fx-файл в байт код compiledEffect = Effect.CompileEffectFromFile(effectFileName, null, null, CompilerOptions.None, TargetPlatform.Windows);

// Используем полученный байт-код для создания объекта эффекта. effect = new Effect(device, compiledEffect.GetEffectCode(), CompilerOptions.NotCloneable, null);

При возникновении ошибок при загрузке или компиляции эффекта обработчик не завершает работу приложения путем вызова метода Close формы, так как, если верить MSDN, это может вызвать утечку ресурсов. Вместо этого он регистрирует собственный обработчик события Idle, автоматически вызывающий метод Close. Но здесь есть один подводный камень: метод Idle будет вызван по завершении обработки всех событий, в том числе Paint. Таким образом, если не принять особых мер, не исключен вызов метода Idle с не полностью сформированным эффектом, что с большой долей вероятности приведет к краху приложения. Для борьбы с этим недоразумением в начале обработчика события Paint осуществляется проверка, не готовится ли приложение к завершению работы: если это так, то обработчик события Paint не выполняет визуализацию сцены. Переходим к обработчику события Paint, выполняющего визуализацию изображения. Первым делом данный обработчик выполняет стандартные проверки потери устройства, после чего очищает экран. Далее он присваивает свойству VertexDeclaration графического устройства декларацию вершины, созданную в обработчике события Load: device.VertexDeclaration = decl;

На первый взгляд эту операцию было бы рациональнее вынести в обработчик события Load. Однако это не самая лучшая идея, так как информация о параметрах графического устройства теряется при сбросе методом Reset. Следовательно, такое приложение перестало бы нормально функционировать после первой же потери устройства. И, наконец, главная изюминка программы: визуализация точки на экране с использованием эффекта: effect.Begin();

foreach (EffectPass pass in effect.CurrentTechnique.Passes)

{

pass.Begin();

device.DrawUserPrimitives(PrimitiveType.PointList, vertices, 0, vertices.Length);

pass.End();

}

effect.End();

Как видно, смотря на обилие кода, приложение имеет достаточно простую структуру. Как говорится, у страха глаза велики. Теперь давайте попробуем создать это приложение в Visual Studio 2005. Для начала запустите Visual Studio 2005, создайте проект нового приложения Windows Forms и подключите сборку Microsoft.Xna.Framework.dll. В окне Solution Explorer щелкните правой кнопкой мыши на узле проекта и выберите в контекстном меню команду Add | New Folder, и создайте папку Data, в которой мы будем хранить различные вспомогательные эффекты (рисунок 2.6). Затем добавьте в папку Data файл эффекта SimpleEffect.fx (рисунок 2.7), например, при помощи команды контекстного меню Add | New Item... . Поместите в файл SimpleEffect.fx текст эффекта из листинга 2.6.

22

Page 23: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Рисунок 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. Полную версию приложения можно найти на CD диске с книгой в каталоге Ch02\Ex01.

23

Page 24: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Рисунок 2.8. Свойства файла SimpleEffect.fx.

2.4.1. Проверка аппаратной поддержки вершинных шейдеров. Наше приложение, визуализирующее точку в центре экрана, всегда создает графическое устройство с использованием флага CreateOptions.SoftwareVertexProcessing, то есть вершинные шейдеры всегда выполняются средствами центрального процессора (CPU). Учитывая, что подавляющее большинство современных графических процессоров имеют аппаратную поддержку вершинных шейдеров, этот недочет приводит к неоптимальному использованию ресурсов GPU. Использование флага CreateOptions.HardwareVertexProcessing тоже не является хорошей идей, так это сделает невозможной работу приложения на видеокартах без аппаратных вершинных процессоров (например, Intel GMA 900 и Intel GMA 950). Так что же делать? Наиболее красивое решение проблемы – проверка возможностей текущего GPU. Если текущий GPU имеет аппаратные вершинные процессоры, приложение должно создать устройство с использованием флага CreateOptions.HardwareVertexProcessing, в противном случае – CreateOptions.SoftwareVertexProcessing. Таким образом, нам необходимо научиться анализировать возможности текущего GPU. В XNA Framework информация обо всех возможностях графического устройства инкапсулируются в классе GraphicsDeviceCapabilities, каждое свойство которого соответствует одной из характеристик графического устройства. Учитывая многообразие характеристик устройства, разработчики сгруппировали часть свойств в логические группы (структуры), то есть некоторые свойства класса GraphicsDeviceCapabilities в свою очередь тоже содержат набор свойств по некоторой тематике: // Некоторые фрагменты определения класса GraphicsDeviceCapabilities public sealed class GraphicsDeviceCapabilities : IDisposable { // Группа свойств, описывающих возможности графического устройства по визуализации примитивов

public GraphicsDeviceCapabilities.PrimitiveCaps 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 ...

24

Page 25: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

// Свойства без подсвойств: rtex Shader, поддерживаемая графическим устройством

аемая графическим устройством

// Макс ражать графическое устройство

рое способно отобразить графическое устройство за

; } // Оста

формация, которая может понадобиться при создании графического устройства, сосредоточена в

Поддерживает ли графическое устройство метод DrawUserPrimitives на аппаратном уровне

// Подд стеризацию примитивов (при отсутствии

tsHardwareRasterization { get; } Имее ессоры

...

видно, информация о наличии аппаратных вершинных процессоров содержится в свойстве o и

eviceCapabilities.DeviceCapabil portsHardwareTransformAndLight.

способа получения доступа к экземпляру объекта

к ч рet; }

лучения доступа к

his.Handle,

Если GPU имеет аппаратные вершинные процессоры ities.SupportsHardwareTransformAndLight)

Уничтожаем устройство

Снова создаем устройство, но уже с аппаратной поддержкой вершинных шейдеров e,

Options.HardwareVertexProcessing | CreateOptions.SingleThreaded,

// Максимальная версия языка Ve 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 { //

public bool SupportsDrawPrimitives2Ex { get; } ерживает ли графическое устройство аппаратную ра

// подобной поддержки визуализация будет выполняться с неприемлемо низкой // производительностью) public bool Suppor// т ли графическое устройство аппаратные вершинные проц

public bool SupportsHardwareTransformAndLight { get; }

}

КакSupportsHardwareTransf rmAndLight. Таким образом, нашему приложению необход мо просто проверить значение свойства GraphicsD ities.Sup Если оно равно true, приложение может создать графическое устройство с использованием флага CreateOptions.HardwareVertexProcessing, в противном случае должен использоваться флаг CreateOptions.SoftwareVertexProcessing. XNA Framework предоставляет разработчику дваGraphicsDeviceCapabilities. Наиболее простым из них является использование свойства GraphicsDeviceCapabilities э земпляра класса графи еского уст ойства: public GraphicsDeviceCapabilities GraphicsDeviceCapabilities { g

Не смотря на простоту данный способ обладает существенным недостатком: для посвойству GraphicsDeviceCapabilities приложение должно создать графическое устройство. Получается замкнутый круг: чтобы получить информацию, необходимую для создания графического устройства, приложение должно создать это устройство. В принципе, мы можем попробовать написать что-то вроде: // Создаем графическое устройство без аппаратной поддержки вершинных шейдеров. device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware, t CreateOptions.SoftwareVertexProcessing | CreateOptions.SingleThreaded, presentParams); //if (device.GraphicsDeviceCapabilities.DeviceCapabil{ // device.Dispose(); // device = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardwarthis.Handle, CreatepresentParams); }

25

Page 26: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Хотя данная технология и работает, всё это напоминает поездку из Киева в Харьков через Жмеринку.

es GetCapabilities(DeviceType deviceType);

eviceType – тип устройства, задаваемый с использованием перечислимого типа DeviceType (таблица

Зач ен параметр deviceType? Дело в том, что метод GetCapabilities не может предугадать, какой

з

s п а а

тных вершинных процессоров можно организовать с ф

pabilities(DeviceType.Hardware);

dLight)

tions |= CreateOptions.SoftwareVertexProcessing;

vice = new GraphicsDevice(GraphicsAdapter.DefaultAdapter, DeviceType.Hardware, this.Handle,

тся на CD с книгой в каталоге Examples\Ch02\Ex02.

2.4.2. Управление размером точек. имеет достаточно небольшой размер, в результате чего

}

RenderState, содержащий множество свойств, влияющих на

e значение 10, мы увеличите размер визуализируемых точек до 10x10

ка события Paint формы

Поэтому разработчики XNA Framework предусмотрели альтернативный способ получения экземпляра класса GraphicsDeviceCapabilities без создания графического устройства. Как вы знаете, конструктор класса GraphicsDevice принимает в качестве первого параметра экземпляр класса GraphicsAdapter, описывающий используемую видеокарту. Так вот, заботливые разработчики XNA Framework снабдили этот класс методом GetCapabilities, возвращающем экземпляр класса GraphicsDeviceCapabilities, соответствующий этому устройству: public GraphicsDeviceCapabiliti

где d

1.4). ем нуж

тип устройства вы собираетесь создать (DeviceType.Hardware, DeviceType.Reference или DeviceType.NullReference), в то время как все эти типы устройств имеют совершенно ра ные характеристики. Соответственно, при помощи параметра deviceType вы указываете методу GetCapabilitie , какое значение вы планируете ередать пар метру deviceType конструктор класса графического устройства (GraphicsDevice). Таким образом, проверку наличия аппараиспользованием следующего рагмента кода: GraphicsDeviceCapabilities caps = GraphicsAdapter.DefaultAdapter.GetCaCreateOptions options = CreateOptions.SingleThreaded; if (caps.DeviceCapabilities.SupportsHardwareTransformAn options |= CreateOptions.HardwareVertexProcessing; else op de options, presentParams);

Полная версия приложения находи

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

Это свойство возвращает экземпляр класса процесс визуализации. В частности, свойство RenderState.PointSize отвечает за размер точек: // По умолчанию значение этого свойства равно 1.0f float PointSize { get; set; }

Так, присвоив свойству PointSizпикселей (рисунок 2.9): // Фрагмент обработчиdevice.RenderState.PointSize = 10.0f;

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

26

Page 27: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Однако мы не можем просто так взять и присвоить свойству GraphicsDevice.RenderState.PointSize произвольное значение. Ведь никто не может гарантировать, что ваши программы будут запускаться исключительно на тех видеокартах, которые умеют работать с большими точками размером 10x10. Следовательно, необходимо предусмотреть поведение приложения в ситуации, когда видеокарта не удовлетворяет минимальным требованиям к размеру точек: наиболее логичное действие приложения в подобной ситуации – выдача соответствующего сообщение об ошибке с последующим завершением работы. В разделе 2.4.1 упоминалось, что в XNA Framework имеется класс GraphicsDeviceCapabilities с информацией о возможностях графического устройства. В частности, свойство PointSize содержит максимальный размер точки в пикселях, поддерживаемый указанным графическим устройством: public float MaxPointSize { get; }

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

Рисунок 2.10. Тестовый пакет D3D RightMark

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

27

Page 28: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

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

Таблица 2.6. Максимальные размеры точек для некоторых GPU.

GPU Максимальный размер точки (в пикселях)

NV20 (NVIDIA GeForce3) 64

NV25 (NVIDIA GeForce4) 8192

NV3x (NVIDIA GeForce FX) 8192

R2xx – R5xx (ATI Radeon) 256

GMA 900 (Intel 915G) 256

GMA 950 (Intel 945G) 256

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

Листинг 2.8.

public partial class MainForm : Form { ... // Массив вершин VertexPositionColor[] vertices = null; // Количество вершин int pointCount = 0; ... private void MainForm_Load(object sender, EventArgs e) { ... // Вычисляем максимальное количество вершин, которые видеокарта может визуализировать за один // вызов метода DrawUserPrimitives maxVertexCount = Math.Min(device.GraphicsDeviceCapabilities.MaxPrimitiveCount, device.GraphicsDeviceCapabilities.MaxVertexIndex);

// Создаем массив вершин, рассчитанный на хранение 16-ти вершин vertices = new VertexTransformedPositionColor[16]; } private void MainForm_Paint(object sender, PaintEventArgs e) { ... // Очищаем экран device.Clear(XnaGraphics.Color.CornflowerBlue); // Если количество точек больше нуля (метод DrawUserPrimitives некорректно работает с // массивами нулевого размера) if (pointCount > 0) { device.VertexDeclaration = decl;

28

Page 29: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

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 MainForm_MouseDown(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++;

29

Page 30: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

// Перерисовываем экран Invalidate(); } } }

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

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

Другой очень полезный приём, используемый в программе – вывод всех точек одним вызовом метода GraphicsDevice.DrawUserPrimitives. Дело в том, что метод GraphicsDevice.DrawUserPrimitives тратит относительно много времени центрального процессора на подготовку графического ускорителя к визуализации примитивов, при этом собственно процесс визуализации выполняется графическим ускорителем и практически не требует вмешательства со стороны центрального процессора. Таким образом, рисуя все точки за один присест, мы значительно снижаем нагрузку на центральный процессор, распараллеливая работу между CPU и GPU. Однако метод DrawUserPrimitives имеет ограничения на максимальное количество примитивов, которые можно визуализировать за один вызов этого метода. Количество вершин, которые можно вывести за один присест, тоже далеко не бесконечно. Информация о возможностях текущей видеокарты по визуализации примитивов хранится в двух свойствах класса GraphicsDeviceCapabilities: // Максимальное количество примитивов, которые можно визуализировать за один присест public int MaxPrimitiveCount { get; } // Максимальное количество вершин, которые можно визуализировать за один присест. public int MaxVertexIndex { get; }

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

Таблица 2.7. Значение свойства MaxPrimitiveCount для некоторых GPU

GPU Значение

NVIDIA NV2x - NV3x 1048575

ATI R2xx - R5xx 1048575

Intel GMA 9xx 65535

Таблица 2.8. Значение свойства MaxVertexIndex для некоторых GPU

30

Page 31: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

GPU Значение

ATI R2xx - R5xx 16,777,215

NVIDIA NV2x - NV3x 1,048,575

Intel GMA 9xx 65534

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

И так, теоретически приложение вполне может столкнуться с видеокартой, способной выводить не более 65534 примитивов за один присест. Много это и ли мало? Например, если пользователь будет каждую секунду добавлять на экран по точке, то через 18 часов он достигнет лимита для Intel GMA 900. Иными словами, это довольно внушительное значение для нашего приложения, но вполне достижимое. Поэтому в приложение на всякий случай встроена проверка: при достижении предала на количество визуализируемых примитивов, точки просто перестают добавляться в массив. Как говорится, дешево и сердито1. Так же стоит обратить внимание на проверку размера массива на неравенство нулю перед тем, как вывести его на экран. Дело в том, что метод DrawUserPrimitives при попытке визуализации массива генерирует исключение System.IndexOutOfRangeException. Хотя подобное поведение метода нельзя назвать безупречным, эту особенность приходится учитывать. В заключении следует обратить внимание на преобразование координат указателя мыши из системы координат клиентской области окна в логическую систему XNA Framework, в которой координаты компонентов вершин лежат в диапазоне от -1 .. +1. Кроме того, следует учитывать, что в Windows положительное направление оси Y направленно вниз, а в XNA Framework – вверх. Так подобные преобразования будут довольно часто применяться в наших приложениях, они были вынесены в отдельный класс Helper, расположенный в файле Helper.cs (листинг 2.9). В дальнейшем мы продолжим размещать в этом классе различные вспомогательные методы, облегчающие работу с XNA Framework.

Листинг 2.9.

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;

1 Более практичным подходом является автоматическая генерация дополнительных массивов по мере достижения размера предыдущего массива значения MaxPrimitiveCount. Однако эта функциональность заметно усложнит приложение, а её полезность в данном случае весьма сомнительна.

31

Page 32: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

return v;

}

}

}

2.4.4. Управление цветом точек средствами HLSL. В этом, заключительно разделе, посвященном точкам, мы добавим в наше приложение возможность визуализации разноцветных пикселей. В принципе, это довольно тривиальная операция, если бы не одно но: в настоящее время наше приложение визуализирует точки исключительно фиксированного цвета морской волны (aqua), который жестко задан в файле эффекта (в нашем случае это SimpleEffect.fx) и не может быть изменен C#-приложением. К примеру, если вы исправите код vertices[pointCount] = new VertexPositionColor( Helper.MouseToLogicalCoords(e.Location, ClientSize), XnaGraphics.Color.Aqua);

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

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

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

Листинг 2.10.

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

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

Листинг 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 указывать не обязательно. Кстати, мы активно использовали эту возможность в прошлом разделе.

32

Page 33: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Структуры Как известно, передача в функцию большого количества параметров делает код трудночитаемым1, поэтому параметры шейдера обычно группируют в структуры входных и выходных параметров. Объявление структуры в HLSL аналогично языку C (листинг 2.12).

Листинг 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).

Листинг 2.13.

VertexOutput MainVS(VertexInput input) { // Создаём структуру output VertexOutput output; output.pos = float4(input.pos, 1.0f); output.color = input.color; // Возвращаем результаты работы шейдера return output; }

Пиксельный шейдер. После обработки вершинным процессором вершины объединяются в примитивы, которые разбиваются на отдельные пиксели (то есть растеризуются). При этом параметры вершины, рассчитанные вершинным шейдером, интерполируются вдоль поверхности примитива. В нашем случае, вдоль поверхности примитива интерполируется цвет вершины. Иными словами, каждому пикселю примитива ставится в соответствие интерполированный цвет (при визуализации точек вдоль поверхности точки интерполируется константный цвет). Наш пиксельный шейдер будет просто принимать интерполированный цвет и выводить его на экран (листинг 2.14).

Листинг 2.14.

float4 MainPS(float4 color:COLOR):COLOR

1 Сложный шейдер может принимать до нескольких десятков различных параметров.

33

Page 34: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

{ return color; }

Для привязки входных данных пиксельного шейдера к интерполированным выходным данным из вершинного шейдера используется семантика COLOR. Хочу обратить ваше внимание на то, что семантики выходных данных вершинного шейдера и входных данных пиксельного шейдера нечего не говорят о смысле этих данных1. Главное предназначение этих семантик – связь между выходными параметрами вершинного шейдера и входными параметрами пиксельным шейдера. Например, замена семантики COLOR на TEXCOORD некоим образом не повлияет на работу приложения (листинг 2.14). Главное, чтобы выходные параметры вершинного шейдера и входные параметры пиксельного шейдера использовали одинаковые семантики.

Примечание Так как профили семейства ps_1_x не позволяют использовать четырех компонентные текстурные координаты, нам пришлось применить профиль ps_2_0. Использование текстурных координат будет рассмотрено в разделе 2.6.

Листинг 2.14.

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 vs_2_0 MainVS(); PixelShader = compile ps_2_0 MainPS(); } }

1 В профилях до ps_3_0 семантики иногда всё же могут оказывать незначительное влияние на работу шейдера. Эта тема подробно будет рассмотрена в разделе 4.x.

34

Page 35: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Доработка C#-приложения С кодом эффекта мы вполне разобрались и, следовательно, можем приступать к модификации C#-кода нашего приложения: теперь при каждом щелчке левой кнопкой мыши на форму будут добавляться разноцветные точки случайного цвета. Для этого достаточно лишь немного подправить обработчик события MouseDown (листинг 2.15).

Листинг 2.15.

private void MainForm_MouseDown(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();

35

Page 36: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

}

При генерации случайного цвета точки приложение проверяет, не сольётся ли полученный цвет с цветом фона. Так как в компьютерной графике цвет задаётся яркостью трех компонентов, мы можем трактовать значения этих трех компонентов как координаты цвета в некотором цветовом пространстве (рисунок 2.11). Соответственно, в качестве критерия похожести двух цветов можно использовать расстояние между этими цветами:

222 )21()21()21( bbggrr ccccccr −+−+−= (2.1)

где r – расстояние между цветами в цветовом пространстве.

, , – яркости красного, зеленого и синего компонента первого цвета; rc1 gc1 bc1

, , – яркости красного, зеленого и синего компонента второго цвета. rc2 gc2 bc2

Однако учитывая высокую ресурсоёмкость операции вычисления квадратного корня, в качестве критерия похожести цветов рациональнее использовать не само расстояние, а его квадрат. Полная версия приложения находится на CD с книгой в каталоге Ch02\Ex05.

Рисунок 2.11. Цветовое пространство.

Практическое упражнение №2.1 Создайте приложение, рисующее поточечный график функции y=cos(x), где x находится в диапазоне 0°…720° (рисунок 2.12). Если у вас возникнут трудности при выполнении упражнения, посмотрите готовое приложение на CD с книгой (Ch02\Ex06).

36

Page 37: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

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

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

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

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

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

Листинг 2.16

public partial class MainForm : Form { ...

37

Page 38: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

// Массив вершин отрезков VertexPositionColor[] vertices = null; // Количество отрезков int lineCount = 0; // Максимальное количество отрезков, которые текущая видеокарта может визуализировать одним // вызовом метода DrawUserPrimitives int maxLineCount; // Флаг, показывающий, находится ли программа в режиме добавления нового отрезка (когда // пользователь уже указал начало отрезка, но ещё не отжал левую кнопку мыши) bool AddingLine = false; ... private void MainForm_Load(object sender, EventArgs e) { ... // Определяем максимальное количество отрезков, которое видеокарта может визуализировать за // один вызов метода DrawUserPrimitives maxLineCount = Math.Min(device.GraphicsDeviceCapabilities.MaxPrimitiveCount, device.GraphicsDeviceCapabilities.MaxVertexIndex / 2);

// Создаём массив, рассчитанный на хранение вершин восьми отрезков vertices = new VertexPositionColor[16]; } private void MainForm_Paint(object sender, PaintEventArgs e) { ... // Очищаем экран device.Clear(XnaGraphics.Color.CornflowerBlue); // Если количество отрезков больше нуля if (lineCount > 0) { device.VertexDeclaration = decl; // Визуализируем отрезки effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); device.DrawUserPrimitives(PrimitiveType.LineList, vertices, 0, lineCount); pass.End(); } effect.End(); } device.Present(); ... } ... private void MainForm_MouseDown(object sender, MouseEventArgs e) { // Если нажата левая кнопка мыши if (e.Button == MouseButtons.Left) { // Если количество линий достигло предельно возможной величины, нечего не делаем if (lineCount == maxLineCount)

38

Page 39: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

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

return; } // Переходим в режим добавления отрезка AddingLine = true; // Если размер массива вершин не достаточен вставки нового отрезка, то создаем массив // удвоенного размера и копируем в него содержимое старого массива. if (lineCount * 2 >= vertices.Length) { int newLineCount = lineCount * 2; // Размер массива не должен превышать предельно лимит текущей видеокарты if (newLineCount > maxLineCount) newLineCount = maxLineCount; VertexPositionColor[] newVertices = new VertexPositionColor[newLineCount*2]; vertices.CopyTo(newVertices, 0); vertices = newVertices; } // Заносим в массив вершин координаты начала и конца нового отрезка. Для перевода координат // указателя мыши к диапазону [-1, +1] используется метод MouseToLogicalCoords, созданный // нами в разделе 2.4.3. vertices[lineCount * 2] = new VertexPositionColor(Helper.MouseToLogicalCoords(e.Location, ClientSize), XnaGraphics.Color.Aqua);

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

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

39

Page 40: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

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

Листинг 2.17.

private void MainForm_KeyDown(object sender, KeyEventArgs e)

{

if ((e.KeyCode==Keys.Z) && (e.Control==true))

if (AddingLine == false)

{

if (lineCount > 0)

lineCount--;

Invalidate();

}

}

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

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

40

Page 41: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

)cos()sin(

360..0

0

0

αα

α

⋅+=⋅+=

=

ryyrxx

oo

(2.2)

где x и – координаты текущей вершины окружности y

и – координаты центра окружности 0x 0y α – угол, пробегающий с некоторым шагом значения от 0° до 360°. r – радиус окружности Наиболее важные фрагменты приложения приведёны в листинге 2.18.

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

Листинг 2.18.

public partial class MainForm : Form { GraphicsDevice device = null; PresentParameters presentParams; VertexDeclaration decl; VertexPositionColor[] vertices = null; // Количество сегментов в ломанной линии, аппроксимирующей окружность. const int LineStripCount = 32; ... private void MainForm_Load(object sender, EventArgs e) { ... decl = new VertexDeclaration(device, VertexPositionColor.VertexElements); // Создаём графический буфер, для хранения вершин окружности vertices = new VertexPositionColor[LineStripCount + 1]; } private void MainForm_Paint(object sender, PaintEventArgs e) { ... // Очищаем экран device.Clear(XnaGraphics.Color.CornflowerBlue); device.VertexDeclaration = decl;

41

Page 42: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

// Перебираем все вершины for (int i = 0; i <= LineStripCount; i++) { // Вычисляем координаты текущей вершины окружности по формуле 2.2 float angle=Geometry.DegreeToRadian((float) i/(float) LineStripCount *360.0f); // Окружность имеет радиус 0.8 единиц и расположена в начале системы координат float x = 0.8f * (float)Math.Sin(angle); float y = 0.8f * (float)Math.Cos(angle); // Вычисляем цвет вершины int red=(int) (255 * Math.Abs(Math.Sin(angle * 3))); int green = (int)(255 * Math.Abs(Math.Cos(angle * 2))); // Заносим информацию о вершине в графический буфер vertices[i] = new VertexPositionColor(new Vector3(x, y, 1.0f), new XnaGraphics.Color(red, green, 0)); }; // Рисуем ломанную, аппроксимирующую окружность. Ломанная состоит из vertices.Length - 1 // сегментов. effect.Begin(); foreach (EffectPass pass in effect.CurrentTechnique.Passes) { pass.Begin(); device.DrawUserPrimitives(PrimitiveType.LineStrip, vertices, 0, vertices.Length - 1);

pass.End(); } effect.End(); device.Present(); } ... }

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

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

независимо от размеров формы. 2. Запретить пользователю делать окно неквадратным, то есть действовать по принципу “нет неквадратной

формы – нет и проблемы”.

42

Page 43: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

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

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

непропорционального размера формы. Этот метод имеет важный нюанс относительно первого варианта: в данном случае трансформируются не координаты вершин примитива, а итоговое изображение1. В разделе 2.3.1 на рисунке 2.2 была приведена схема графического конвейера. Как вы знаете, на выходе из вершинного шейдера получаются вершины в логической системе координат, в которой координаты вершин лежат в диапазоне [-1, +1]. Затем эти логические координаты трансформируются в систему координат окна посредством видового преобразование. Параметры этого преобразования задаются свойством Viewport класса GraphicsDevice: public Viewport Viewport { get; set; }

Одноименная структура Viewport, инкапсулирующая параметры видового преобразования, определяется следующим образом: public struct Viewport { // Ширина области, в которую осуществляется визуализация public int Width { get; set; } // Высота области, в которую осуществляется визуализация public int Height { get; set; } // Координата X левого верхнего угла области визуализация public int X { get; set; } // Координата Y левого верхнего угла области визуализация public int Y { get; set; } ... }

Как видно, структура Viewport определяет в клиентской области окна прямоугольную область, используемую для визуализации изображения. По умолчанию при создании и сбросе устройства XNA Framework автоматически присваивает полям X и Y нулевое значение, а Width и Height – ширину и высоту клиентской области окна. Таким образом, по умолчанию клиентская область заполняет всю клиентскую область окна. В следующем примере демонстрируется использование пользовательской области визуализации размером 100×100, расположенной в левом верхнем углу приложения (листинг 2.19).

Листинг 2.19.

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

43

Page 44: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

// Пример 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. Что же изменилось в работе приложения? 1. Размер визуализируемого изображения теперь не зависит от размера окна. Соответственно,

непропорциональный размер формы больше не искажает изображение. 2. Клиентская область формы за пределами области визуализации заполнена “мусором”. В этом не ничего

удивительного, ведь наше приложение оставляет содержимое формы за пределами области визуализации “как есть”.

44

Page 45: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

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

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

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

Листинг 2.20.

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

45

Page 46: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

} }

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

Листинг 2.21.

// Пример Ch02\Ex10 private void MainForm_Paint(object sender, PaintEventArgs e) { if (closing) return; try { if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.Lost) throw new DeviceLostException(); if (device.GraphicsDeviceStatus == GraphicsDeviceStatus.NotReset) device.Reset(); // Используем для визуализации всю клиентскую область окна device.Viewport = Helper.FullScreenViewport(ClientSize); // Очищаем экран device.Clear(XnaGraphics.Color.CornflowerBlue); // Используем квадратную область визуализации device.Viewport = Helper.SquareViewport(ClientSize); ... } }

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

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

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

46

Page 47: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

255)cos5.05.0( 3.0 ⋅⋅−= αred (2.3)

255)cos5.05.0( 3.0 ⋅⋅+= αgreen

где

– красная составляющая цвета red – зелёная составляющая цвета green

Примечание Если же убрать операцию возведения в степень, то график функции будет заметно терять яркость в окрестностях y равного 0, что смотрится не особо красиво.

Рисунок 2.20. График синуса с высотной закраской

Готовое приложение можно найти на CD с книгой в каталоге Examples\Ch02\Ex11.

2.6. Треугольники Для визуализации наборов треугольников с различной топологией в XNA Framework имеется три типа примитивов: PrimitiveType.TriangleList, PrimitiveType.TriangleFan и PrimitiveType.TriangleStrip. Начнём с самого простого примитива, PrimitiveType.TriangleList.

2.6.1. Несвязанные треугольники (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 обусловлено особенностью отсечения невидимых треугольников в трехмерных сценах. Однако при визуализации двухмерных изображений эта

47

Page 48: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

функциональность оказывается не только излишней, но и вредной. Поэтому разработчики 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;

48

Page 49: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

FillMode fillMode=FillMode.Solid; bool closing = false; ... private void MainForm_Load(object sender, EventArgs e) { ... // Создаём массив для хранения трёх вершин треугольника vertices = new GraphicsBuffer<TransformedColored>(3); // Задаём вершины треугольника vertices[0] = new VertexPositionColor(new Vector3(0.0f, 0.4f, 0.0f), XnaGraphics.Color.Coral);

vertices[1] = new VertexPositionColor(new Vector3(0.4f, -0.4f, 0.0f), XnaGraphics.Color.LightGreen);

vertices[2] = new VertexPositionColor(new Vector3(-0.4f, -0.4f, 0.0f), XnaGraphics.Color.Yellow);

} private void MainForm_Paint(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 позволяют отображать сцену в режиме проволочного каркаса (Wireframe). Благодаря этому разработчик может ясно видеть топологию сцены, в частности, взаимное расположение всех треугольников на сцене. XNA Framework тоже поддерживает подобную функциональность, позволяя отображать вместо закрашенных треугольников их проволочный каркас. Управление этой функциональностью осуществляется при помощи свойства RenderState.FillMode класса GraphicsDevice: FillMode FillMode { get; set; }

Свойство может принимать следующие значения перечислимого типа FillMode:

49

Page 50: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

FillMode.Point – визуализируются только точки, расположенные вершинах треугольника. Визуализируемые точки являются полноценными точками XNA Framework: к примеру, их размер можно изменять при помощи свойства device.RenderState.PointSize.

FillMode.WireFrame – визуализирует каркас треугольника, который рисуется с использованием обычных линий вроде TrianglePrimitive.LineList или TrianglePrimitive.LineStrip.

FillMode.Solid – закрашивает внутреннюю область треугольника. По умолчанию свойству RenderState.FillMode присвоено значение FillMode.Solid, то есть треугольники рисуются закрашенными. Для демонстрации практического использования свойства FillMode мы добавим в нашу программу (Ex12) возможность циклической смены режимов отображения треугольников при помощи клавиши пробел (листинг 2.23).

Листинг 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;

50

Page 51: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

break; } // Перерисовываем экран Invalidate(); } } }

Узор Серпинского. Перейдём к более сложному примеру. Наше следующее приложение будет строить узор Серпинского1 путём рекурсивного разбиения треугольника, визуализируемого в каркасном режиме. Построение узора начинается с базового треугольника (рисунок 2.23). На первой интеграции в данный большой треугольник вписывается другой треугольник меньшего размера, вершины которого расположены в середине сторон большого треугольника (рисунок 2.24). В результате большой треугольник оказывается как бы разбит на 4 треугольника. На втором этапе данные в три треугольника, примыкающие к вершинам исходного большого треугольника, вписываются по три треугольника (рисунок 2.25). На третьем этапе в образовавшиеся девять треугольников вписываются уже девять треугольников (рисунок 2.26), на четвертом этапе вписывается уже 27 треугольников и так далее. В идеале процесс должен продолжаться до бесконечности, однако на практике вполне можно ограничиться десятком итераций (рисунок 2.27), так как размер треугольников, генерируемых в последующих итерациях, будет уже меньше размера пикселей экрана.

Рисунок 2.23. Построение узора Серпинского. Базовый треугольник.

1 Узор Серпинского, являющийся фракталом, подробно описан [16]

51

Page 52: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Рисунок 2.24. Построение узора Серпинского. Первая итерация.

Рисунок 2.25. Построение узора Серпинского. Вторая итерация.

Рисунок 2.26. Построение узора Серпинского. Третья итерация.

52

Page 53: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Рисунок 2.27. Узор Серпинского, 11 итераций.

Так как приложение будет визуализировать десятки или даже сотни тысяч треугольников, очень важно поместить их в единый массив и вывести одним вызовом метода DrawUserPrimitives. Однако для создания такого массива очень полезно заранее знать количество треугольников, которые будут визуализированы за n итераций. Это поможет нам избежать многочисленных изменений размера массива по мере генерации треугольников. Давайте попробуем найти зависимость числа визуализируемых треугольников от количества интеграций. И так, при нулевом количестве итераций мы визуализируем 1 треугольник. При одной итерации число треугольников становится 1 + 1 = 2. При двух итерациях количество треугольников будет равно 1 + 1 + 3 = 5, при трех 1 + 1 + 3 + 9 = 14. Таким образом, мы можем вывести некоторую общую закономерность для n итераций:

∑−

=

− +=+++++=1

0

1 313...9311n

i

intc (2.4)

где tc – количество треугольников, визуализируемых при n итераций. В принципе, это выражение вполне приемлемо, однако знак суммы смотрится не особо красиво. Однако открыв учебник высшей математики вроде [К.20] можно найти весьма интересное соотношение:

∑=

+

−−

=n

i

ni

xxx

0

1

11

(2.5)

Соответственно, выражение 2.4 можно переписать без использования n элементов:

213

3131131

1)1(1

0

+=

−−

+=+=+−−

=∑

nnn

i

itc (2.6)

Гораздо более наглядное выражение, не так ли? Однако так как разные видеокарты могут визуализировать разное число треугольников, не исключено, что приложению придется решать и образную задачу. Допустим, мы определим в приложении число итераций (n) равным 11, то есть узор Серпинского будет

содержать 885742

1311

=+

треугольников с общим количеством вершин 88574 · 3 = 265722. Но ведь

некоторые видеокарты могут оказаться не способными визуализировать такое количество треугольников за один присест. Как приложение должно повести себя в подобном случае? Наиболее простое решение – сократить количество интеграций до максимально приемлемого. А для этого нам придется определять максимальное количество итераций (n), при котором количество треугольников не превышает заданное значение tc. Для этого выражение (2.6) достаточно переписать как

123 −⋅= tcn (2.7) после чего взять от обоих частей выражения логарифм по основанию 3:

))12((log3 −⋅= tcfloorn (2.8)

53

Page 54: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

где floor(x) – функция, возвращающая целое число, не превышающее x (то аналог метода Math.Floor из C#).

К слову Согласно выражению 2.8 на компьютере с Intel GMA 900 приложение может выполнить до 9-ти итераций, на NVIDIA NV2x-3x до 12-ти итераций, а на ATI R2xx–R5xx до 13-ти итераций.

После такого небольшого математического экскурса можно приступать реализации нашего приложения. Визуализация треугольников будет осуществляться в два этапа:

1. Заполнение графического буфера информацией о треугольниках. 2. Визуализация треугольников одним вызовом метода DrawUserPrimitives.

Вычисление координат треугольников мы организуем с использованием рекурсивной функции DrawTriangle принимающей в качестве параметров координаты треугольника и количество оставшихся итераций. Эта функция будет помещать в массив вершин координаты текущего треугольника, после чего выполнять деление этого треугольника на три части и вызывать саму себя для этих частей, но уже с уменьшенным количеством оставшихся итераций на 1. Процесс повторяется до тех пор, пока количество оставшихся итераций не достигнет 0. Исходный код основных фрагментов программы с подробными комментариями приведён в листинге 2.24.

Листинг 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);

54

Page 55: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

// Увеличиваем индекс текущей вершины 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.GraphicsDeviceCapabilities.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) { ...

55

Page 56: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

// Очищаем экран 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). Готовое приложение находится на CD с книгой в каталоге Ch02\Ex14.

Рисунок 2.28. Покрашенный деревянный забор

56

Page 57: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

2.6.2. Веер треугольников (PrimitiveType.TriangleFan) Следующий тип примитивов, PrimitiveType.TriangleFan, используется для рисования вееров треугольников. Первые три вершины (0-я, 1-я и 2-я) задают первый треугольник. Второй треугольник задаётся 0-й, 2-й и 3-й вершинами, третий – 0-й, 3-й и 4-й вершинами и т.д. (рисунок 2.29). Данный тип примитивов идеально подходит для рисования эллипсов, окружностей, секторов окружностей и аналогичных фигур.

Рисунок 2.29. Веер треугольников, нарисованный с использованием примитивов PrimitiveType.TriangleFan

Чтобы опробовать этот тип примитива на практике, мы модифицируем пример Ex09, заставив его рисовать на экране закрашенный круг вместо окружности (рисунки 1.31). Для этого придётся внести три небольших изменения в обработчики событий Load и Paint:

• Увеличить размер массива вершин на одну вершину. 3. Вставить в начало массива вершину с координатами центра окружности 4. Изменить тип примитива с LineList на TriangleFan Так же мы добавим в программу возможность переключения между каркасным и закрашенным режимами отображения треугольников при помощи клавиши пробел (Space). Эта функциональность, позволяющая просматривать топологию сцены, неоценима при отладке приложения (рисунок 2.31).

Рисунок 2.30. Круг, нарисованный при помощи веера из 64-х треугольников (PrimitiveType.TriangleFan)

57

Page 58: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Рисунок 2.31. Круг (веер из 18-ти треугольников), визуализированный в каркасном режиме.

Основные фрагменты исходного кода полученного приложения (Ex15) приведены в листинге 2.25.

Листинг 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) { ...

58

Page 59: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

// Задаем область визуализации размером во весь экран. Для вычисления параметров видового // преобразования используется метод 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(); } }

2.6.3.Полоса из связанных треугольников (PrimitiveType.TriangleStrip) Последний тип примитивов, PrimitiveType.TriangleStrip, предназначен для рисования полосы из связанных треугольников. При этом первый треугольник проходит через 0-ю, 1-ю 2-ю вершины, второй треугольник – через 3-ю, 2-ю и 1-ю вершины, третий треугольник – через 2-ю, 3-ю и 4-ю вершины, четвёртый через 5-ю, 4-ю и 3-ю, и т.д (рисунок 2.32). Обратите внимание на порядок перечисления вершин в треугольниках: все треугольники в полосе имеют одинаковую ориентацию относительно часовой стрелки – например, если вершины первого треугольника располагаются по часовой стрелке, то и вершины других треугольников так же будут перечисляться по часовой стрелке. Эта особенность используется при отсечении невидимых треугольников с использованием поля RenderState.CullMode (см. раздел x.x).

59

Page 60: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Рисунок 2.32. Визуализация полосы связанных треугольников

Данный тип примитивов очень удобно использовать для визуализации ломаных линий шириной больше одного пикселя, то есть в качестве продвинутой версии примитива PrimitiveType.LineStrip. В частности, на рисунке 2.32 в качестве иллюстрации приведена ломаная линия переменной ширины, состоящая из пяти сегментов.

Визуализация графика функции y=cos(x) Чтобы попрактиковаться в рисовании ломаных линий при помощи примитива PrimitiveType.TriangleStrip, мы напишем приложение, визуализирующее график косинуса в интервале с 0º…720º (0…4·π в радианах) использованием ломаной толщиной 10 пикселей. Хотя на первый взгляд эта задача не намного сложнее практический упражнений №2.1 и №2.2, она всё же имеет несколько подвохов. Для начала сформулируем задачу более чётко. Нам необходимо построить полосу из связанных треугольников, аппроксимирующую график косинуса, центр которой совпадает с графиком косинуса (рисунок 2.33).

Рисунок 2.33. Полоса из связанных треугольников, аппроксимирующая график косинуса (тонкая линию, проходящая по центру полосы).

Для построения синусоиды мы будем перебирать точки графика косинуса с определённым шагом. На рисунке 2.30 эти точки обозначены как p0, p1, p2 и т.д. Отступив симметрично по обе стороны от точки p0 на некоторое расстояние, например, на 0.05 единиц, мы получим две вершины v0 и v1, расстояние межу которыми равно 10 пикселей. Проделав аналогичную операцию над остальными точками, мы получим пары вершин (v2–v3, v4–v5, v6–v7, …, v2·n–v2·n+1), расстояние между которыми равно 0.1 единиц. И, наконец, построив полосу из треугольников, опирающуюся на вершины v0, v1, v2, v3, мы получим ломаную линию толщиной 0.1 пикселей, точно аппроксимирующую график косинуса (рисунок 2.34).

60

Page 61: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Рисунок 2.34. Построение полосы из треугольников

По ширине график косинуса будет вписан в клиентскую область окна, а по высоте наш график будет немного меньше высоты окна. Таким образом, в действительности наше приложение будет визуализировать не сам график y=cos(x), а несколько другую функцию, полученную путем масштабирования графика y=cos(x) вдоль осей X и Y:

)2cos(6.0 xy ⋅⋅⋅= π (2.9)

где cos – функция вычисляющая значение косинуса в радианах x – аргумент функции, лежащий в диапазоне [-1, +1]. Фактически это координата x графика, пробегающая с определённым шагом значения от левого до правого краёв экрана, то есть от -1 до +1. Соответственно аргумент функции косинуса пробегает значения от 0 до π⋅4 (0º...720º).

– координата графика функции, лежащая в диапазоне от [-0.7, +0.7]. y yПеребирая с определённым шагом значения координаты x от -1 до +1 и подставляя их в выражение (2.9), мы получим координаты набора точек p0, p1, …, pn (рисунок 2.34). Как говорилось выше, для получения координат вершин полосы треугольников v0, v1, …, v2·n, v2·n+1 необходимо симметрично отупить от точек p0 … pn на 0.05 единиц. Вроде бы всё просто и понятно, если не считать одной мелочи: мы пока не ещё определились, каким образом должны быть сориентированы отрезки v0–v1, v2–v3, …, v2·n–v2·n+1 относительно точек p0, p1, …, pn. Не мудрствуя лукаво, мы сделаем эти отрезки параллельными оси Y и посмотрим, что из этого выйдет: v0 = p0 - 0.05 (2.10) v1 = p0 + 0.05 и т.д. Основные фрагменты приложения (Ex16) приведены в листинге 2.26.

Листинг 2.26.

public partial class MainForm : Form { // Количество сегментов в ломаной линии, аппроксимирующей график косинуса const int QuadStrips = 100; // Число треугольников в ломанной линии const int TriStrips = QuadStrips * 2; ... // Массив вершин VertexPositionColor[] vertices = null; // Режим закраски треугольников FillMode fillMode = FillMode.Solid; ...

61

Page 62: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

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(); } }

62

Page 63: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Скомпилируйте и запустите приложение на выполнение. Как и ожидалось, на экране появится график функции y=cos(x), однако толщина графика будет переменной, причем максимальная толщина графика будет достигаться в окрестностях точек, в которых функция cos(x) принимает значения -1 или 1. (рисунок 2.35).

Примечание Следующий материал этого раздела содержит довольно много математических выкладок, поэтому если вы не в ладах с математикой, можете смело пропустить оставшуюся часть раздела 2.6.3.

Рисунок 2.35. График функции y=cos(x) переменной толщины

График y=cos(x) постоянной толщины И так, попытка использования отрезков v0–v1, …, v2·n–v2·n+1 параллельных оси Y закончилась неудачей. Ну что ж, отрицательный результат, это тоже результат. Попробуем поэкспериментировать ориентацией отрезков v0–v1, …, v2·n–v2·n+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));

63

Page 64: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Рисунок 2.36. График функции y=cos(x) переменной толщины. Вершины смещаются в направлении векторов (-1, -1) и (+1, +1).

Проведя несколько экспериментов, мы придём к выводу, что график косинуса имеет необходимую толщину только в там, где отрезки v2·i–v2·i+1 перпендикулярны графику косинуса. Следовательно, чтобы график функции имел постоянную толщину 0.1 единиц, все отрезки v2·i–v2·i+1 должны быть перпендикулярны графику косинуса. Для нахождения координат вершин отрезка v2·i–v2·i+1 длиной 0.1 единиц, проходящего через точку pi, перпендикулярно графику функции необходимо выполнить следующие действия: 1. Найти вектор, s перпендикулярный графику функции в точке pi.

2. Найти вектор, параллельный вектору n длиной пять единиц, параллельный вектору s .

3. Для нахождения координат вершин v2·i и v2·i+1 необходимо отложить от точки pi вектора n и n− . Рассмотрим эти шаги более подробно. Как вы знаете из курса аналитической геометрии, вектор, перпендикулярного графику функции, определяются по формуле:

)),(,),((dy

yxdfdx

yxdfs = (2.11)

где s – вектор, перпендикулярный графику функции

– функция, заданная в неявной форме (f(x, y)=0) ),( yxf

dy

yxdfdx

yxdf ),(,),( – частные производные по x и y

Чтобы определить значение вектора s для нашей функции (2.9), перепишем её в неявной форме f(x, y)=0:

0)2cos(6.0 =⋅⋅⋅− xy π (2.12)

Теперь найдём частные производные, являющиеся координатами вектора s :

),( yx sss =

)x2sin(2.1))2cos(6.0(⋅⋅⋅⋅=

⋅⋅⋅−= πππ

dxxydsx (2.13)

64

Page 65: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

1))2cos(6.0(=

⋅⋅⋅−=

dyxydsy

π

Нахождение производных в среде MathCAD Если вы немного подзабыли высшую математику, не огорчайтесь. Для нахождения производных можно воспользоваться, к примеру, математическим пакетом MathCAD. Для вычисления значения производной средствами символьной математики пакета MatCAD просто наберите выражение производной, которую выходите вычислить. Затем введите специальный символ → (Ctrl + .) и нажмите Enter, после чего справа от выражения появится вычисленное значение производной (рисунок 2.37). Подробную информацию о среде MathCAD можно найти, к примеру, в [К.22].

Рисунок 2.37. Математический пакет MathCAD.

Зная вектор s можно легко найти вектор n заданной длины, параллельный вектору s :

ssan ⋅= (2.14)

где n – вектор, параллельный вектору s и имеющий длину a – необходимая длина вектора (в нашем случае 0.05) a

s – длина вектора s

После этого определить координаты точек v2·i и v2·i+1 не составит труда:

npv ii −=⋅2 (2.15)

npv ii +=+⋅ 12

65

Page 66: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Имея под рукой формулы 2.13, 2.14 и 2.15 мы можем легко исправить ошибку в примере Ex16 (визуализация графика функций переменной толщины вместо постоянной) путем небольшой модификации фрагмента обработчика события Paint (листинг 2.27).

Листинг 2.27.

// Полная версия приложения находится на CD диске книги в каталоге 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)); };

Все изменения обработчика события Paint сводятся к добавлению пяти новых строк кода и косметической правке двух строк. Как говорится, дело в количестве строк, а в математических формулах, которые заложены в эти строки. Результат работы приложения приведен на рисунке 2.38.

Рисунок 2.38. График функции y=cos(x) постоянной толщины

66

Page 67: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Визуализация CD-диска Область применения полос из треугольников не ограничивается визуализацией полосок определённой толщины, ведь примитивы PrimitiveType.TriangleStrip активно используются для визуализации самых разнообразных геометрических объектов. В листинге 2.28 приведёны основные фрагменты исходного кода примера, визуализирующий на экране CD диск (рисунок 2.39).

Рисунок 2.39. CD-диск, нарисованный с использованием полосы из 200 треугольников

Листинг 2.28

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);

67

Page 68: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

// Вычисляем координаты вершины внутренней границы 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;

68

Page 69: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Invalidate(); } } }

Нетрудно заметить, что пример Ex18 является тривиальной модификацией примера Ex09 из раздела 2.5.2 (круг, переливающийся разнообразными цветами). CD-диск рисуется при помощи замкнутой полосы из треугольников, внутренние вершины которой расположены на окружность радиусом 0.1 единицы, а внешние – на окружности радиусом 0.7 (рисунок 2.40) Координаты точек, лежащих на окружностях вычисляются по формуле 2.2 (раздел 2.5.2).

Рисунок 2.40. CD-диск, состоящий из 18-ти сегментов (36 треугольников), визуализированный в каркасном режиме.

Визуализация квадрата с круглым отверстием В следующем примере (Ex19) мы нарисуем более интересное изображение: квадрат, внутри которого вырезана дырка в виде окружности (рисунок 2.41). Для упрощения задачи в качестве отправной точки будет использоваться исходный код примера Ex18.

Рисунок 2.41. Квадрат с круглым отверстием.

На первый взгляд между квадратом и кругом практически нет ничего общего: граница круга ограничивается окружностью, а граница квадрата – четырьмя отрезками равной длины. Однако вспомним аналитическую геометрию. Граница квадрата, стороны которого расположены под углом 45º к осям X и Y, может быть описана следующими формулами:

69

Page 70: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

)(sinsin 20 αα signrxx ⋅⋅+= (2.16)

)(coscos20 αα signryy ⋅⋅+=

где x и – координаты текущей вершины квадрата. y

и – координаты центра квадрата. 0x 0y α – угол, пробегающий с некоторым шагом значения от 0° до 360° (0…2·π). r – половина диагонали квадрата. – аналог метода Math.Sign в .NET. signТаким образом, для визуализации повёрнутого на 45º квадрата, в центре которого вырезано круглое отверстие, необходимо всего лишь немного подправить пример Ex19, подкорректировав формулу для расчета внешней границы CD диска (листинг 2.39).

Листинг 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)); };

70

Page 71: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Рисунок 2.42. Повёрнутый на 45° квадрат с отверстием, визуализированный в каркасном режиме. Фигура построена с использованием полосы из 40-ка треугольников.

Результат работы примера Ex21 приведён на рисунке 2.42. Теперь, нам необходимо развернуть эту фигуру на 45 градусов относительно геометрического центра фигуры. Из курса аналитической геометрии вам должно быть известно, что при повороте изображения на угол φ относительно точки с координатами x0, y0, координаты всех точек изображения трансформируются по следующим формулам:

ϕϕ sin)(cos)( 0.00 ⋅−′+⋅−′+= yyxxxx (2.17)

ϕϕ cos)(sin)( 0.00 ⋅−′+⋅−′−= yyxxyy

где

0x , – координаты точки, относительно которой выполняется поворот 0y

x′ , – старые координаты точки y′ϕ – угол поворота

Подставив вместо и выражения 2.16, а так же положив угол x′ y′ ϕ равным 45º, мы получим следующие формулы, задающие координаты точек квадрата:

))(cossin)(cos(cos2

220 αααα signsignrxx ⋅+⋅⋅+= (2.18)

))(cossin)(cos(cos2

220 αααα signsignryy ⋅−⋅⋅+=

Модифицируем пример Ex19, изменив вычисление координат вершин внешней границы квадрата согласно выражению 2.18, и запустим программу на выполнение. В результате мы получим любопытную картину: в процессе поворота квадрата на 45º полоса треугольников как бы завинчивается по спирали. (рисунок 2.39) Это обусловлено тем, что мы повернули только внешнюю сторону полосы треугольников, забыв о внутренней стороне, имеющую форму круга. Для поворота круглого отверстия в квадрате на 45º мы скомбинируем выражения 2.17 и 2.2:

)sin(cos20 αα +⋅+=

rxx (2.19)

)sin(cos20 αα −⋅−=

ryy

71

Page 72: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Рисунок 2.39. Квадрат, повёрнутый относительно внутреннего отверстия. Каркасный режим.

После доработки приложения согласно выражению 2.19 изображение наконец-то примет нормальный вид (рисунки 2.37 и 2.40). Исправленный код с учётом выражений 2.18 и 2.19 приведен в листинге 2.40 (Ex20):

Рисунок 2.44. Квадрат с круглым отверстием, визуализированный в каркасном режиме.

Листинг 2.40.

// Несколько увеличиваем размер квадрата и отверстия 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);

72

Page 73: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

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 являются частными случаями так называемой суперокружности, координаты точек которой задаются формулами1:

))(cossin)(coscos(20 αααα signsignrxx nn ⋅+⋅⋅+= (2.20)

))(cossin)(coscos(20 αααα signsignryy nn ⋅−⋅⋅+=

где x , – координаты текущей точки. y

, – координаты центра суперокружности. 0x 0y r – радиус суперокружности. – степень суперокружности n α – угол, пробегающий с некоторым шагом все значения от 0º до 360º. При n=1, суперокружность приобретает форму обычной окружности (формула 2.19), а при n=2 – квадрата (формула 2.18). При других n суперокружность приобретает различные интересные формы. Более подробную информацию о суперокружностях можно найти в [К.23]. Напишите приложение, визуализирующую фигуру, имеющую форму суперокружности, степень n которой равна 2.5. При этом в центре этой фигуры должно быть вырезано отверстие в форме суперокружности степени 1.3 (рисунок 2.45).

1 Если быть более точным, формула 2.20 задаёт суперокружность, повёрнутую на 45 градусов

73

Page 74: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Рисунок 2.45. Фигура, границы которой задаются двумя суперокружностями. Внешняя суперокружность имеет степень 2.5, а внутренняя 1.3.

Готовое приложение можно найти на CD диске в каталоге \Examples\Ch02\Ex21.

Практическое упражнение №2.5 Создайте приложение, визуализирующее фигуру Лиссажу, координаты точек которой задаются следующей формулой:

)2sin( α⋅=x (2.21)

)3cos( α⋅=y

где x , – координаты текущей точки y α – угол, пробегающий с определённым шагом значения от 0º до 360º (0…2·π) График функции должен быть визуализирован с использованием линии толщиной 10 пикселей (рисунок 2.46).

Рисунок 2.46. Фигура Листажу

74

Page 75: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

На всякий случай, ниже приведены математические выкладки, которые могут облегчить написание программы. Для построения фигуры Лиссажу, вписанной в форму с небольшими отступами по краям, выражение 2.21 необходимо преобразовать к следующему виду:

)2sin(85.0 α⋅⋅=x (2.22)

)3cos(85.0 α⋅⋅=y

Для нахождения вектора касательной в точке x, y достаточно найти производные по α :

),( yx kkk =

)2cos(7.1))2sin(85.0( αα

α⋅⋅=

⋅⋅=

ddkx (2.23)

)3sin(55.2))3cos(85.0( αα

α⋅⋅−=

⋅⋅=

ddky

где

k – вектор касательной

xk , – компоненты этого вектора yk

Зная вектор касательной, можно легко найти вектор, перпендикулярный графику функции:

),( xy kks −=

)3sin(55.2 α⋅⋅−== yx ks (2.24)

)2cos(7.1 α⋅⋅−=−= xy ks

Имея перпендикуляр к графику функции ( s ), вы сможете легко найти координаты вершин полосы из треугольников (по аналогии примером построения графика синуса из начала раздела). Готовое приложение можно найти на CD с книгой в каталоге \Examples\Ch02\Ex22.

Заключение В отличие от универсальных графических библиотек вроде GDI, XNA Framework поддерживает визуализацию весьма ограниченного набора примитивов: точки, отрезки и треугольники. Любой визуализируемый объект, какой сложной формы он не был, всегда аппроксимируется набором этих примитивов. Хотя подобный подход заметно усложняет жизнь разработчика, он позволяет достичь беспрецедентного уровня производительности (порядка нескольких сотен миллионов визуализированных примитивов в секунду). Примитивы в процессе визуализации обрабатываются специализированными вершинными и пиксельными процессорами видеокарты. Вершинные процессоры предназначены для преобразования вершин, а пиксельные – для закраски примитивов. В DirectX и XNA Framework вершинные и пиксельные процессоры программируются с использованием семейства ассемблеро-подобных языков Vertex Shader и Pixel Shader. Каждый из этих языков оптимизирован под определённый тип GPU. Например, язык Pixel Shader 1.3 был специально разработан для программирования пиксельных процессоров NV25 (GeForce4). При загрузке шейдера программа, написанная на ассемблеро-подобном языке автоматически компилируется в машинный код текущего GPU; таким образом, языки Vertex Shader и Pixel Shader очень похожи на промежуточный язык IL в .NET. По мере роста функциональности вершинных и пиксельных процессоров появилась потребность в языке программирования высокого уровня. Таким языком стал HLSL (High Level Shader Language) – C-подобный язык программирования, предназначенный для программирования вершинных и пиксельных процессоров. Программа, написанная на HLSL, компилируется в промежуточный ассемблеро-подобный язык с использованием профиля, указывающего под какой ускоритель необходимо оптимизировать генерируемый код. Так, профиль ps_2_a указывает, что шейдер будет транслироваться в язык Pixel Shader 2.x и оптимизирован для GPU семейства NV3x (GeForce FX). Как правило, чем выше номер профиля, тем большая функциональность доступна программисту, однако платой за эту гибкость является рост требований приложения к функциональности GPU (см. приложение 2).

75

Page 76: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Автор выражает благодарность: Корпорации ATI, которая предоставила видеокарты ATI Radeon 9700 Pro и ATI Radeon 9800 XT для тестирования примеров от книги.

Корпорации NVIDIA, предоставившей видеокарту NVIDIA GeForce 5900 Ultra. Корпорации Microsoft, предоставившей Microsoft Visual Studio .NET 2003 Professional и Microsoft Visual

Studio 2005, а так же Microsoft Windows 2003 Server. Издательству BHV, которое предоставило англоязычную литературу по 3D графике.

Так же автор благодарит Алексея Кряжева (Codemasters Software), Игоря Рыбинского (BHV), Филиппа Герасимова (NVIDIA), Андрея Крючкова (Microsoft), Александра Ложечкина (Microsoft), Олега Михайлика, Евгения Маркова (NVIDIA), Кирилла Дмитриева (NVIDIA), Виктории Жислиной (Intel) и Николаю Семенову (стажер Intel, СПбГУАП) за консультации и полезные советы. Отдельная благодарность выражается Геннадию Ригеру (AMD) и Юрию Уральскому (NVIDIA), оказавшими неоценимую помощь при написании книги. В заключение хочу поблагодарить сотрудников ВЦ ПГУ Дмитрия Ларина, Дениса Белолапоткова и Павла Маркина, предоставивших мне внутренние резервы Интернет-трафика для скачивания новых версий Microsoft DirectX SDK, XNA Framework, драйверов, патчей, презентаций и т.д.

76

Page 77: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Приложение 1. Вершинные и пиксельные шейдеры, поддерживаемые видеокартами ATI, NVIDIA и Intel

Таблица 1. Версии вершинных шейдеров

GPU 1.0 1.1 2.0 3.0

R2xx X X

R3xx X X X

R4xx X X X

R5xx X X X X

NV2x X X

NV3x X X X

NV4x X X X X

GMA 9xx1 X X X

Таблица 2. Версии пиксельных шейдеров

GPU 1.0 1.1 1.2 1.3 1.4 2.0 2.x 3.0

R2xx X X X X X

R3xx X X X X X X

R4xx X X X X X X

R5xx X X X X X X X X

NV20 X X

NV25 X X X X

NV3x X X X X X X X

NV4x X X X X X X X X

1 GMA 9xx эмулирует вершинные шейдеры посредством центрального процессора.

77

Page 78: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Приложение 2. Профили вершинных и пиксельных шейдеров, поддерживаемые видеокартами ATI, NVIDIA и Intel.

Таблица 1. Профили вершинных шейдеров

GPU vs_1_0 vs_1_1 vs_2_0 vs_2_a vs_3_0

R2xx X X

R3xx X X X

R4xx X X X

R5xx X X X X X

NV2x X X

NV3x X X X X

NV4x X X X X X

GMA 9xx1 X X X X X

Таблица 2. Профили пиксельных шейдеров

GPU ps_1_0 ps_1_1 ps_1_2 ps_1_3 ps_1_4 ps_2_0 ps_2_a ps_2_b ps_3_0

R2xx X X X X X

R3xx X X X X X X

R4xx X X X X X X X

R5xx X X X X X X X2 X X

NV20 X X

NV25 X X X X

NV3x X X X X X X X X3

NV4x X X X X X X X X X

GMA 9xx X X X X X X

1 GMA 9xx эмулирует вершинные шейдеры посредством центрального процессора. 2 Рекомендуется использовать профиль ps_2_b. 3 Рекомендуется использовать профиль ps_2_a.

78

Page 79: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Приложение 3. Семантики языка HLSL

Таблица 1. Семантики входных данных вершинного шейдера

Семантика Описание

POSITION Координаты вершины

COLOR Цвет вершины

PSIZE Размер точки (при визуализации набора точек)

TEXCOORD0 – TEXCOORDn1 Текстурные координаты

Таблица 2. Семантики выходных данных вершинного шейдера

Семантика Описание

POSITION Трансформированные координаты вершины в однородных координатах

COLOR Цвет вершины

TEXCOORD0 – TEXCOORD7 Текстурные координаты

Таблица 3. Семантики входных данных пиксельного шейдера

Семантика Описание

POSITION Оконные координаты текущего пикселя (x, y)

COLOR Интерполированный цвет пикселя

TEXCOORD0 – TEXCOORD7 Текстурные координаты

Таблица 4. Семантики входных данных пиксельного шейдера

Семантика Описание

COLOR Цвет пикселя

Таблица 5. Стандартные семантики входных параметров эффекта

Семантика Описание

World Матрица мира

WorldInverse Обратная матрица мира ( ) 1−WorldWorldInverseTranspose Обратная транспонированная матрица мира TWorld )( 1−

View Матрица вида

ViewInverse Обратная матрица вида

ViewInverseTranspose Обратная транспонированная матрица вида

Projection Матрица проекции

ProjectionInverse Обратная матрица проекции

оличества 1 Максимальное количество наборов текстурных координат зависит от используемого профиля и к

входных параметров вершинного шейдера.

79

Page 80: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

ProjectionInverseTranspose Обратная транспонированная матрица проекции

WorldView Матрица мира-вида ( ViewWorld ⋅ )

WorldViewInverse Обратная матрица мира-вида

WorldViewInverseTranspose Обратная транспонированная матрица мира-вида

ViewProjection Матрица вида-проекции ( ) ojectionView Pr⋅

ViewProjectionInverse Обратная матрица вида-проекции

ViewProjectionInverseTranspose Обратная транспонированная матрица вида-проекции

WorldViewProjection Матрица мира-вида-проекции ( ) ojectionViewWorld Pr⋅⋅

WorldViewProjectionInverse Обратная матрица мира-вида-проекции

WorldViewProjectionInverseTranspose Обратная транспонированная матрица мира-вида-проекции

80

Page 81: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Приложение 5. Вещественные форматы переменных, поддерживаемые графическими процессорами ATI, NVIDIA и Intel.

Таблица 1. Характеристики вещественных форматов

Формат1 Число бит

Число бит2

(мантисса/порядок) Число бит3

(целая/дробная часть)

Число значащих десятичных разрядов после запятой

Примерный диапазон значений

fixed4 10 - 1/9 2 -1 .. +1

fixed12 12 - 2/10 3 -2 .. +2

fixed16 16 - 4/12 3 -8 .. +8

half 16 11/5 - 3 -6.5·104 .. +6.5·104

float24 24 16/8 - 4 -3.4·1038 .. +3.4·1038

float 32 24/8 - 7 -3.4·1038 .. +3.4·1038

Таблица 2. Поддержка вещественных форматов в пиксельном шейдере в зависимости от профиля и GPU

GPU ps_1_0 ps_1_1 ps_1_2 ps_1_3 ps_1_4 ps_2_0 ps_2_a ps_2_b ps_3_0

R2xx fixed16 fixed16 fixed16 fixed16 fixed16

R3xx float24 float24 float24 float24 float24 float24

R4xx float24 float24 float24 float24 float24 float24 float24 float24

R5xx float float float float float float float float float

NV20 fixed fixed

NV25 fixed fixed fixed fixed

NV3x fixed12 fixe12 fixed12 fixed12 fixed12 half

half float

half float

half float

NV4x half half half half half half float

half float

half float

half float

GMA 9xx float float float float float float

В вершинных шейдерах все GPU используют тип float.

1 Условные обозначения внутренних форматов в данных GPU. 2 С учётом знаковых битов 3 С учётом знакового бита 4 Типы fixed предназначены для работы с вещественными значениями с фиксированной точкой. В C# тоже имеется подобный встроенный тип: decimal.

81

Page 82: Глава 2. Визуализация примитивовxnadev.ru/_Content/GSAF/Ch02.pdf · à graphicsDevice – графическое ... 4 3, 4 2 , 4 1 ( = ... чтобы растянуть

Список использованной литературы

Книги 1. Фень Юань. Программирование графики для Windows 2. Том Миллер. Managed DirectX. Программирование графики и игр 3. Станислав Горнаков. DirectX 9. Уроки программирования на C++ 4. Н. Секунов. Обработка звука на PC 5. В.М. Михальчук, А.А. Ровдо, С.В. Рыжков. Микропроцессоры 80x86, Pentium. Архитектура,

функционирование, оптимизация кода. 6. Ровдо А.А. Микропроцессоры от 8086 до Pentium III Xeon и AMD-К6-3. 7. Джон Роббинс. Отладка приложений для Microsoft .NET и Microsoft Windows 8. Брайан Джонсон, Крейг Скибо, Марк Янг. Основы Microsoft Visual Studio .NET 2003 9. Симон Робинсон и др. C# для профессионалов. 10. Ксавье Пачеко. Delphi for .NET. Руководство разработчика. 11. Михаил Краснов. DirectX. Графика в проектах Delphi 12. Крис Касперски. Техника оптимизации программ. Эффективное использование памяти. 13. Guennadi Riguer. Performance Optimization Techniques for ATI Graphics Hardware with DirectX® 9. 14. Guennadi Riguer. The Radeon X1000 Series Programming Guide. 15. И.И. Кантор. Высокоскоростные железнодорожные магистрали. 16. Cg Toolkit. User’s manual. 17. Алексей Боресков. Расширения OpenGL. 18. Алексей Боресков. Разработка и отладка шейдеров. 19. C# Language Specification. 20. Рональд Грэхем, Дональд Кнут, Орен Паташник. Конкретная математика. Основание информатики. 21. М.Я. Выгодский. Справочник по высшей математике для ВУЗов и ВТУЗов. 22. А.И. Плис, Н.А. Сливина. MathCAD. Математический практикум. 23. Френсис Хилл. OpenGL. Программирование трёхмерной графики.

Статьи 1. AlexandreS T. DirectX от WinG до Fahrenheit. www.3dnews.ru 2. Константин Максимов. Совершенство графики в GDI+. Программист №2, 2002 3. Петр Попов. Cg. (www.gamedev.ru) 4. Александр Медведев. 3dfx Tribute - Rampage, Sage, Fear, Mojo. (www.ixbt.com) 5. Андрей Воробьев. Александр Медведев. NVIDIA GeForce FX 5800 Ultra 128MB. (www.ixbt.com) 6. Андрей Воробьев, Алексей Барковой. NVIDIA GeForce FX 5900 Ultra 256MB. (www.ixbt.com) 7. Андрей Воробьев. Александр Медведев. DirectX 10 на пороге или 128 калифорнийских стрелков в

действии: NVIDIA GeForce 8800 GTX (G80) 8. Александр Медведев. Кирилл Буданков. NVIDIA GeForce 6800 Ultra (NV40). 9. Э. Гюннерсон. У границ безопасного кода. Алгоритм №4.

Презентации 1. Mitch Walker. Creating Games with the XNA Framework. Gamefest’2006

82