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

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

Нахождение производных в среде MathCAD

Если вы немного подзабыли высшую математику, не огорчайтесь. Для нахождения производных можно воспользоваться, к примеру, математическим пакетом MathCAD.

Для вычисления значения производной средствами символьной математики пакета MatCAD просто наберите выражение производной, которую выходите вычислить. Затем введите специальный символ ? (Ctrl + .) и нажмите Enter, после чего справа от выражения появится вычисленное значение производной (рисунок 2.37)

 Математический пакет MathCAD

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

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

\overline n=\alpha*\frac{\overline S}{|\overline S|}
( 2.14)

где

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

После этого определить координаты точек v_2i и v_{2i+1} не составит труда:

V_{2-i}=P_i-\overline n//
V_{2-i+1}=P_i+\overline n
( 2.15)

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

for (int i = 0; i <= QuadStrips; i++)
{
float x = -1.0f + 2.0f * (float) i / (float) QuadStrips;
float angle = 2.0f * (float)Math.PI * x;
float cos = (float)Math.Cos(angle);
float y = 0.6f * cos;
byte green = (byte)(Math.Pow(0.5f + cos * 0.5f, 0.3f) * 255.0f);
byte red = (byte)(Math.Pow(0.5f - cos * 0.5f, 0.3f) * 255.0f);
// Вычисляем	вектор s
float sx	= (float)(1.2 * Math.PI * Math.Sin(2.0 * Math.PI * x));
float sy	= 1.0f;
// Вычисляем	длину вектора s
float length = (float)Math.Sqrt(sx * sx + sy * sy);
// Вычисляем	вектор nx
float nx	= sx / length * 0.05f;
float ny	= sy / length * 0.05f;
// Заносим в графический буфер координаты 
вершин v[i*2] и v[i*2+1] (вычисляются по формуле 
// 2.15)
vertices[i * 2] = new VertexPositionColor(new
 Vector3(x - nx, y - ny, 0.0f), new
XnaGraphics.Color(red, green, 0));
vertices[i * 2 + 1] = new VertexPositionColor(new 
Vector3(x + nx, y + ny, 0.0f), new
XnaGraphics.Color(red, green, 0)); 
};
Листинг 2.27.
 График функции y=cos(x) постоянной толщины

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

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

Визуализация CD-диска

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

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

Рис. 2.39. CD-диск, нарисованный с использованием полосы из 200 треугольников
public partial class MainForm : Form
{
// fx-файл
const string effectFileName = "Data\\ColorFill.fx"; 
// Количество сегментов в диске
const int slices = 100; 
// Радиус внутренней границы CD-диска
const float innerRadius = 0.2f; 
// Радиус внешней границы CD-диска
const float outerRadius = 0.7f;
// Режим закраски треугольников
FillMode fillMode = FillMode.Solid;
...
private void MainForm_Load(object sender, EventArgs e)
{ 
... 
// Выделяем память для хранения вершин диска
vertices = new VertexPositionColor[slices * 2 + 2];
// Перебираем вершины CD диска
for (int i = 0; i <= slices; i++)
{ 
// Вычисляем текущий угол ?
float angle = (float)i / (float)slices * 2.0f * (float)Math.PI;
// Вычисляем вспомогательные переменные
float sin = (float)Math.Sin(angle); float cos = (float)Math.Cos(angle);
// Вычисляем координаты вершины внутренней границы CD диска
float x = innerRadius * sin;
float y = innerRadius * cos; 
// Добавляем вершину внутренней границы диска в массив вершин
vertices[i * 2] = new VertexPositionColor(new 
Vector3(x, y, 0.0f), XnaGraphics.Color.White);
// Вычисляем координаты вершины внешней границы 
CD диска x = outerRadius * sin; y = outerRadius * cos; 
// Вычисляем цвет вершины
byte red = (byte)(255 * Math.Abs(Math.Sin(angle * 3))); 
byte green = (byte)(255 * Math.Abs(Math.Cos(angle * 2))); 
// Добавляем вершину внешней границы диска в массив вершин
vertices[i * 2 + 1] = new VertexPositionColor(new 
Vector3(x, y, 0.0f), new XnaGraphics.Color(red, green, 0)); 
};
 }
private void MainForm_Paint(object sender, PaintEventArgs e)
 { ...
device.Viewport = Helper.FullScreenViewport(ClientSize);
device.Clear(ClearFlags.Target, Color.CornflowerBlue, 0.0f, 0);
device.Viewport = Helper.SquareViewport(ClientSize);
device.RenderState.CullMode = CullMode.None; 
device.RenderState.FillMode = fillMode;
device.VertexDeclaration = decl;
// Рисуем CD диск
effect.Begin();
foreach (EffectPass pass in effect.CurrentTechnique.Passes)
{
pass.Begin();
device.DrawUserPrimitives(PrimitiveType.TriangleStrip,
 vertices, 0, vertices.Length - 2);
pass.End(); } effect.End();
device.Present(); } 
...
 }
// Обработчик событий от клавиатуры, переключающий 
режимы закраски при нажатии клавиши Space 
// (Пробел)
private void MainForm_KeyDown(object sender, KeyEventArgs e)
 {
if (e.KeyCode == Keys.Space)
 {
if (fillMode == FillMode.Solid)
fillMode = FillMode.WireFrame; else
fillMode = FillMode.Solid;
Invalidate();
}
}
}
Листинг 2.28.

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

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

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

Визуализация квадрата с круглым отверстием

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

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

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

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

X=x_0+r*\sin^2\alpha*sign(\sin\alpha)
Y=y_0+r*\cos^\alpha*sign(\cos\alpha)
( 2.16)

где

  • x и y - координаты текущей вершины квадрата.
  • x_0 и y_0 - координаты центра квадрата.
  • \alpha - угол, пробегающий с некоторым шагом значения от 0° до 360° (0…2 ·?).
  • r - половина диагонали квадрата.
  • sign - аналог метода Math.Sign в .NET.

Таким образом, для визуализации повернутого на 45° квадрата, в центре которого вырезано круглое отверстие, необходимо всего лишь немного подправить пример Ex19, подкорректировав формулу для расчета внешней границы CD диска (листинг 2.39).

for (int i = 0; i <= slices; i++) 
{
float angle = (float)i / (float)slices * 2.0f * (float)Math.PI;
float sin = (float)Math.Sin(angle);
float cos = (float)Math.Cos(angle); 
// Вычисляем вспомогательные переменные,
 используемые при расчете координат вершин границы 
// квадрата
float sin2 = sin * sin * Math.Sign(sin);
float cos2 = cos * cos * Math.Sign(cos);
// Вычисляем координаты вершин внутреннего 
круглого отверстия и заносим в массив вершин
float x = innerRadius * sin;
float y = innerRadius * cos;
vertices[i * 2] = new VertexPositionColor(new 
Vector3(x, y, 0.0f), XnaGraphics.Color.White);
// Вычисляем координаты внешней границы квадрата по формуле 2.16.
x = outerRadius * sin2;
y = outerRadius * cos2; // Вычисляем цвет вершины
byte red = (byte)(255 * Math.Abs(Math.Sin(angle * 3)));
byte green = (byte)(255 * Math.Abs(Math.Cos(angle * 2))); 
// Заносим в информацию о вершине в массив вершин
vertices[i * 2 + 1] = new VertexPositionColor(new 
Vector3(x, y, 0.0f), new
XnaGraphics.Color(red, green, 0)); 
};
Листинг 2.39.
 Повернутый на 45° квадрат с отверстием, визуализированный в каркасном режиме. Фигура построена с использованием полосы из 40-ка треугольников

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

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

x=x_0+(\qquard x'-x_0)*\cos\varphi+(\qquard y'-y_0)*\sin\varphi
y=y_0+(\qquard x'-x_0)*\sin\varphi+(\qquard y'-y_0)*\cos\varphi
( 2.17)

где

x_0, y_0 - координаты точки, относительно которой выполняется поворот

\qquard x',\qquard y' - старые координаты точки

\varphi - угол поворота

Подставив вместо \qquard x' и \qquard y' выражения 2.16, а так же положив угол ( р равным 45 \deg, мы получим следующие формулы, задающие координаты точек квадрата:

x=x_0+\frac{r}{\sqrt 2}*(\cos^2 \alpha*sign(\cos\alpha)+\sin^2*sign(\cos\alpha))
y=y_0+\frac{r}{\sqrt 2}*(\cos^2 \alpha*sign(\cos\alpha)-\sin^2*sign(\cos\alpha))
( 2.18)

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

x=x_0+\frac{r}{\sqrt2}*(\cos\alpha+\sin\alpha)
y=y_0+\frac{r}{\sqrt2}*(\cos\alpha-\sin\alpha)
( 2.19)
 Квадрат, повернутый относительно внутреннего отверстия. Каркасный режим

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

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

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

Рис. 2.44. Квадрат с круглым отверстием, визуализированный в каркасном режиме
// Несколько увеличиваем размер квадрата и отверстия
const float innerRadius = 0.285f;
const float outerRadius = 1.0f;
// Константа, используемая при расчете вершин квадрата
readonly float sqrt2 = (float)Math.Sqrt(2.0);

for (int i = 0; i <= slices; i++) 
{
float angle = (float)i / (float)slices * 2.0f * (float)Math.PI;
float sin = (float)Math.Sin(angle);
float cos = (float)Math.Cos(angle);
 float sin2 = sin * sin * Math.Sign(sin); float cos2 = 
 cos * cos * Math.Sign(cos);
float x = innerRadius /sqrt2 * (cos + sin); float y = 
innerRadius / sqrt2 * (cos - sin) ;
vertices[i * 2] = new VertexPositionColor(new 
Vector3(x, y, 0.0f), XnaGraphics.Color.White);
x = outerRadius * outerRadius/sqrt2 * (cos2 + sin2); y = outerRadius *
outerRadius / sqrt2 * (cos2 - sin2); byte red = (byte)(255 *
Math.Abs(Math.Sin(angle * 3))); byte green = (byte)(255 *
Math.Abs(Math.Cos(angle * 2))); vertices[i * 2 + 1] = new
VertexPositionColor(new Vector3(x, y, 0.0f), new XnaGraphics.Color(red,
green, 0)); 
};
Листинг 2.40.

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

Выражения 2.18 и 2.19 являются частными случаями так называемой суперокружности, координаты точек которой задаются формулами:

x=x_0+\frac{r}{\sqrt2}*(|\cos\alpha|^n*sign(\cos\alpha)+|\sin\alpha|^n*sign(\cos\alpha))
y=y_0+\frac{r}{\sqrt2}*(|\cos\alpha|^n*sign(\cos\alpha)-|\sin\alpha|^n*sign(\cos\alpha))
( 2.20)

где

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

x_0, y_0 - координаты центра суперокружности.

r - радиус суперокружности.

n - степень суперокружности

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

При n=1, суперокружность приобретает форму обычной окружности (формула 2.19), а при n=2 - квадрата (формула 2.18). При других n суперокружность приобретает различные интересные формы.

Напишите приложение, визуализирующую фигуру, имеющую форму суперокружности, степень n которой равна 2.5. При этом в центре этой фигуры должно быть вырезано отверстие в форме суперокружности степени 1.3 (рисунок 2.45).

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

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

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

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

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

x = \sin(2*\alpha)
y = \cos(3*\alpha)
( 2.21)

где

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

\alpha - угол, пробегающий с определенным шагом значения от 0° до 360° (0…2· \pi )

 Фигура Листажу

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

График функции должен быть визуализирован с использованием линии толщиной 10 пикселей (рисунок 2.46).

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

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

x = 0.85*\sin(2-\alpha)\\
y = 0.85*\cos(3-\alpha)
( 2.22)

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

\overline k=\overline{(k_x,k_y)}\\
k_x=\frac{d(0.85*\sin(2*\alpha))}{d \alpha}=1.7*\cos(2*\alpha)\\
k_y=\frac{d(0.85*\cos(3*\alpha))}{d \alpha}=-2.55*\sin(3*\alpha)

где

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

k_x, k_y - компоненты этого вектора

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

\overline{s}=\overline{(k_y,-k_x)}\\
s_x=k_y=-2.55*\sin(3*\alpha)\\
s_y=-k_x=-1.7*\cos(2*\alpha)
( 2.24)

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

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

Заключение

В отличие от универсальных графических библиотек вроде 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).

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

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