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

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

Структуры

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

// Объявляем структуру входных данных шейдера. 
Обратите внимание на возможность назначения 
// каждому полю структуры семантики struct VertexInput 
{
float3 pos : POSITION;
float4 color : COLOR; };
// Объявляем структуру выходных данных шейдера
struct VertexOutput
{
float4 pos : POSITION;
float4 color : COLOR; };
void MainVS(in VertexInput input, out VertexOutput output) 
{
output.pos = float4(input.pos, 1.0f);
output.color = input.color; 
}
Листинг 2.12.

Как видно, использование структур делает код значительно более понятным: для определения формата входных данных вершинного шейдера, достаточно лишь беглого взгляда на определение структуры VertexInput. После этой модификации наш шейдер MainVS возвращает в качестве результата лишь один параметр ( output ). Следовательно, процедуру MainVS можно заменить функцией, что сделает код программы еще более интуитивно понятным ( листинг 2.13).

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

Пиксельный шейдер

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

float4 MainPS(float4 color:COLOR):COLOR 
{
return color; 
}
Листинг 2.14.

Для привязки входных данных пиксельного шейдера к интерполированным выходным данным из вершинного шейдера используется семантика color. Хочу обратить ваше внимание на то, что семантики выходных данных вершинного шейдера и входных данных пиксельного шейдера ничего не говорят о смысле этих данных12В профилях до ps_3_0 семантики иногда все же могут оказывать незначительное влияние на работу шейдера. Эта тема подробно будет рассмотрена в разделе 4.x.. Главное предназначение этих семантик - связь между выходными параметрами вершинного шейдера и входными параметрами пиксельным шейдера. Например, замена семантики color на texcoord некоим образом не повлияет на работу приложения (листинг 2.14). Главное, чтобы выходные параметры вершинного шейдера и входные параметры пиксельного шейдера использовали одинаковые семантики.

Примечание

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

struct VertexInput {
float3 pos : POSITION;
float4 color : COLOR; 
};
struct VertexOutput {
float4 pos : POSITION; 
// Рассчитанный цвет вершины, передается как текстурные координаты
float4 color : TEXCOORD; 
};
VertexOutput MainVS(VertexInput input) 
{
VertexOutput output;
output.pos = float4(input.pos, 1.0f);
output.color = input.color;
return output; 
}
// Пиксельный шейдер получает входные параметры из
 интерполированных текстурных координат
float4 MainPS(float4 color:TEXCOORD):COLOR
{
return color; 
}
technique Fill 
{
pass p0
{
VertexShader = compile vs20 MainVS(); PixelShader =
 compile ps20 MainPS(); 
 }
}
Листинг 2.14.

Доработка C#-приложения

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

private void MainFormMouseDown(object sender, MouseEventArgs e) 
{
if (e.Button == MouseButtons.Left) 
{ 
// Если достигли предельного количества точек, выходим if 
(pointCount == maxVertexCount) 
{
MessageBox.Show(String.Format("Количество точек достигло 
максимального значения"+ 
 " для данного GPU: {0}.", maxVertexCount), 
"Внимание", MessageBoxButtons.OK, 
 MessageBoxIcon.Warning); return; 
}
// При необходимости удваиваем размер массива. if 
(pointCount == vertices.Length) 
{
int newSize = vertices.Length * 2; if (newSize > maxVertexCount) 
newSize = maxVertexCount;
VertexPositionColor[] newVertices = new VertexPositionColor
[newSize]; vertices.CopyTo(newVertices, 0); vertices = newVertices; 
}
XnaGraphics.Color color; double delta; do 
{ 
// Вычисляем случайные значение компонентов R, G, B 
цвета точки byte[] bytes = new byte[3]; rnd.NextBytes(bytes) ; 
// Формируем цвет
color = new XnaGraphics.Color(bytes[0], bytes[1], bytes[2]); 
// Вычисляем квадрат "расстояния" 
между рассчитанным случайным цветом и цветом фона формы 
delta = Math.Pow((color.R - XnaGraphics.Color.CornflowerBlue.R), 2) + Math.Pow((color.G - XnaGraphics.Color.CornflowerBlue.G), 2) + 
Math.Pow((color.B - XnaGraphics.Color.CornflowerBlue.B), 2);
}
// Если цвет точки слабо отличается от цвета фона, 
повторяем вычисления. while(delta < 1000);
// Заносим информацию о точке в массив вершин
vertices[pointCount] = new VertexPositionColor(Helper.
MouseToLogicalCoords(
 e.Location, ClientSize), color);
pointCount++;
}
Invalidate();
}
Листинг 2.15.

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

R=\sqrt{(c1_r-c2_r)^2+(c1_g-c2_g)^2+(c1_b-c2_b)^2}
( 2.1)

где

  • r - расстояние между цветами в цветовом пространстве.
  • c 1_r, c 1 , c 1_b - яркости красного, зеленого и синего компонента первого цвета;
  • c2_r , c2_g , c2_b – яркости красного, зеленого и синего компонента второго цвета.

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

 Цветовое пространство

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

Практическое упражнение №2.1

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

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

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

2.5. Отрезки

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

 Независимые отрезки

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

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

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

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

Небольшого внимания заслуживает код, вычисляющий максимальное количество линий, которое может визуализировать видеокарта за один вызов метода DrawUserPrimitives. Как вы знаете из раздела 2.4.3, значение максимального количества примитивов, которые может визуализировать видеокарта за один присест, определяется свойствами GraphicsDeviceCapabilities.MaxPrimitiveCount и GraphicsDeviceCapabilities.MaxVertexIndex. Но так как каждый примитив типа PrimitiveType.LineList содержит две вершины, при оценке максимального количества отрезков, которые может визуализировать видеокарта за один присест, приложение должно поделить значение GraphicsDeviceCapabilities.MaxVertexIndex на 2.

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

private void MainForm_KeyDown(object sender, KeyEventArgs e) 
{
if ((e.KeyCode==Keys.Z) && (e.Control==true))
 if (AddingLine == false) 
{
if (lineCount > 0) lineCount--;
Invalidate(); 
} 
}
Листинг 2.17.
 Изображение, нарисованное при помощи нашего самодельного графического редактора (Ex07)

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

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

\alpha=0\circ\dots360\circ//
x=x_o+r*\sin(\alpha)//
y=y_o+r*\cos(\alpha) ( 2.2)

где

  • x и y - координаты текущей вершины окружности
  • x_0 и y0 - координаты центра окружности
  • \alpha - угол, пробегающий с некоторым шагом значения от 0° до 360°.
  • r - радиус окружности

Наиболее важные фрагменты приложения приведены в листинге 2.18.

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

Рис. 2.16. Окружность, нарисованная с использованием примитивов Direct3D.PrimitiveType.LineStrip
public partial class MainForm : Form 
{
GraphicsDevice device = null;
PresentParameters presentParams;
VertexDeclaration decl;
VertexPositionColor[] vertices = null; 
// Количество сегментов в ломанной линии,
 аппроксимирующей окружность.
const int LineStripCount = 32;
...
private void MainForm_Load(object sender, EventArgs e) 
{ 
...
decl = new VertexDeclaration(device, 
VertexPositionColor.VertexElements); 
// Создаем графический буфер, для хранения вершин окружности
vertices = new VertexPositionColor[LineStripCount + 1]; 
}
private void MainForm_Paint(object sender, PaintEventArgs e)
{ 
... 
// Очищаем экран
device.Clear(XnaGraphics.Color.CornflowerBlue);
device.VertexDeclaration = decl;
// Перебираем все вершины
for (int i = 0; i <= LineStripCount; i++) 
{
// Вычисляем координаты текущей вершины окружности по формуле 2.2
float angle = (float)i / (float)LineStripCount * 2.0f * (float)Math.PI; 
// Окружность имеет радиус 0.8 единиц и расположена
 в начале системы координат 
float x = 0.8f * (float)Math.Sin(angle); 
float y = 0.8f * (float)Math.Cos(angle); 
// Вычисляем цвет вершины
int red=(int) (255 * Math.Abs(Math.Sin(angle * 3))); 
int green = (int)(255 * Math.Abs(Math.Cos(angle * 2))); 
// Заносим информацию о вершине в графический буфер
vertices[i] = new VertexPositionColor(new Vector3(x, y, 1.0f), 
new XnaGraphics.Color (red, green, 0) ) ; 
};
// Рисуем ломанную, аппроксимирующую окружность. 
Ломанная состоит из vertices.Length - 1 
// сегментов.
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin() ;
device.DrawUserPrimitives(PrimitiveType.LineStrip,
 vertices, 0, 4> vertices.Length - 1) ;
pass.End() ; 
} 
effect.End();
device.Present(); 
}
}
Листинг 2.18.

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

Стоит отметить, что с ростом числа сегментов ломанная все сильнее начинает походить на настоящую окружность; при количестве сегментов порядка сотни вряд ли кто сможет найти визуальные различия между окружностью, визуализированной поточено средствами GDI+, и ее аппроксимацией ломанной линией. А вот разница в производительности будет более чем заметна.

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

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