Уроки OpenGL + C#.

OpenGL. Визуализация графика функции.

Создание визуализации анимированного графика функции на С# и OpenGL.


В данной главе мы рассмотрим процесс создание программы, задачей которой будет визуализация графика заданной функции.
Особенность программы будет то, что в ней будет анимировано демонстрироваться то, как меняются значения функции на графике.

Это будет реализовано следующим образом: по графику двигается красная точка, принимающая значения y для заданного x в нашем графике (по всей видимой области).
Помимо этого, возле курсора будут визуализироваться его координаты (рисунок 1).


Рисунок 1.


Создайте основу приложения, так как это было описано в главе 4.4. Только не добавляйте кнопки «Визуализировать» и «Закрыть», ограничьтесь элементом SimpleOpenGLControl.
Окно должно иметь форму, как показано на рисунке 1.
Добавьте в проект 1 таймер – назовите его (параметр name в свойствах таймера) PointInGrap, и установите в его свойствах интервал 30 миллисекунд.
После этого щелкните по нему дважды, чтобы создалась функция PointInGrap_Tick, отвечающая за обработку события ontimer.


Теперь, когда основа приложения создана, мы перейдем к исходному коду.
Он будет основан на 7 функциях, которые мы сейчас рассмотрим, но сначала, перед кодом функции-конструктора класса добавьте инициализацию следующих переменных:

 


// размеры окна
double ScreenW, ScreenH;

// отношения сторон окна визуализации
// для корректного перевода координат мыши в координаты,
// принятые в программе

private float devX;
private float devY;

// массив, который будет хранить значения x,y точек графика
private float[,] GrapValuesArray;
// количество элементов в массиве
private int elements_count = 0;

// флаг, означающий, что массив с значениями координат графика пока еще не заполнен
private bool not_calculate = true;

// номер ячейки массива, из которой будут взяты координаты для красной точки,
// для визуализации текущего кадра
private int pointPosition = 0;

// вспомогательные переменные для построения линий от курсора мыши к координатным осям
float lineX, lineY;

// текущение координаты курсора мыши
float Mcoord_X = 0, Mcoord_Y = 0;



Перед каждой переменной закомментировано ее назначение, так что вопросов не возникнуть не должно.
Теперь вернемся к нашим 7 функциям.


Первая функция – это обработчик события загрузки формы - Form1_Load. Здесь, при загрузке приложения будет произведена инициализация OpenGL для последующей визуализации.
Инициализацию OpenGL с двухмерной проекцией мы рассмотрели в предыдущей части главы, так что здесь все для нас понятно. Едиственным отличием является то, что код стал немного более подробным. Код этой функции:

 


private void Form1_Load(object sender, EventArgs e)
{
// инициализация бибилиотеки glut
Glut.glutInit();
// инициализация режима экрана
Glut.glutInitDisplayMode(Glut.GLUT_RGB | Glut.GLUT_DOUBLE);

// установка цвета очистки экрана (RGBA)
Gl.glClearColor(255, 255, 255, 1);

// установка порта вывода
Gl.glViewport(0, 0, AnT.Width, AnT.Height);

// активация проекционной матрицы
Gl.glMatrixMode(Gl.GL_PROJECTION);
// очистка матрицы
Gl.glLoadIdentity();

// определение параметров настройки проекции, в зависимости от размеров сторон элемента AnT.
if ((float)AnT.Width <= (float)AnT.Height)
{
ScreenW = 30.0;
ScreenH = 30.0 * (float)AnT.Height / (float)AnT.Width;

Glu.gluOrtho2D(0.0, ScreenW, 0.0, ScreenH);
}
else
{
ScreenW = 30.0 * (float)AnT.Width / (float)AnT.Height;
ScreenH = 30.0;

Glu.gluOrtho2D(0.0, 30.0 * (float)AnT.Width / (float)AnT.Height, 0.0, 30.0);
}

// сохранение коэфицентов, которые нам необходимы для перевода координат указателя в оконной системе, в координаты
// принятые в нашей OpenGL сцене
devX = (float)ScreenW / (float)AnT.Width;
devY = (float)ScreenH / (float)AnT.Height;

// установка объектно-видовой матрицы
Gl.glMatrixMode(Gl.GL_MODELVIEW);

// старт щетчика, отвечающего за выхов функции визуализации сцены
PointInGrap.Start();
}



Как видите, почти ничего не изменилось.
Теперь обратимся к функции PointInGrap_Tick. Эта функция вызывается с задержкой в 30 миллисекунд.
В ней мы ведем отсчет того, из какого элемента массива с координатами графика, мы сейчас возмем координаты, которые используем для рисования красной точки.
Отсюда так же вызывается функция Draw, отвечающая за визуализацию.
Код этой функции:

 


// функция обработчик события таймера
private void PointInGrap_Tick(object sender, EventArgs e)
{
// если мы дошли до последнего элемента массива
if (pointPosition == elements_count-1)
pointPosition = 0; // переходим к начальному элементу

// функция визуализации
Draw();

// переход к следующему элементу массива
pointPosition++;
}


Теперь, перед тем как перейти к функциям, отвечающим за визуализацию, мы рассмотрим несолько небольших вспомогательных функий.
Начтем с функции AnT_MouseMove.
Эта функция добавляется созданием события MouseMove для элемента SimpleOpnGLControl (AnT). Событие создается аналогично тому, как мы его создавали в главе 2.2. Только в данном случае мы переходим к свойствам элемента AnT, и уже в них перезходим во вкладку Event и добавляем событие MouseMove.
В данной функции, мы производим сохранение текущих координат мыши, чтобы в будующем использовать их при визуализации графика, а так же производим вычисление размеров линий, которые будут по нормалям соединять координаты указателя мыши с координатными осями (две красные линии на рисунке 1).
Код этой функции выглядит следующим образом:

 


// обработка движения мыши над элементом AnT
private void AnT_MouseMove(object sender, MouseEventArgs e)
{
// созраняем координаты мыши
Mcoord_X = e.X;
Mcoord_Y = e.Y;

// вычисляем параметры для будующей дорисовке линий от указателя мыши к координатным осям.
lineX = devX * e.X;
lineY = (float)(ScreenH - devY * e.Y);
}


Теперь рассмотрим функцию, которая будет осуществлять визуализацию текстовых строк.
Эта функция устанавливает координаты вывода растровых символов, в соответствии с координатами, переданными в параметрах x и y, а затем в цикле перебирает все символы из указанной в параметре строки текста. Каждый из символов визуализируется с помощью функции . В этой функции указывается шрифт для вывода и переменная типа char для визуализации.
Код функции выглядит следующим образом:

 


// фукнция визуализации текста
private void PrintText2D(float x, float y, string text)
{
// устанавливаем позицию вывода растровых символов
// в переданных координатах x и y.
Gl.glRasterPos2f(x, y);

// в цикле foreach перебираем значения из массива text,
// который содержит значение строки для визуализации
foreach (char char_for_draw in text)
{
// визуализируем символ c, с помощью функции glutBitmapCharacter, используя шрифт GLUT_BITMAP_9_BY_15.
Glut.glutBitmapCharacter(Glut.GLUT_BITMAP_9_BY_15, char_for_draw);
}
}


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

 

// функция, производящая вычисления координат графика
// и заносящая их в массив GrapValuesArray
private void functionCalculation()
{
// определение локальных переменных X и Y
float x = 0, y = 0;

// инициализация массива, который будет хранить значение 300 точек
// из которых будет состоять график

GrapValuesArray = new float[300, 2];

// щетчик элементов массива
elements_count = 0;

// вычисления всех значений y, для x пренадлежащего промежутку от -15 до 15, с шагом в 0.01f
for (x = -15; x < 15; x += 0.1f)
{
// вычисление y для текущего x
// по формуле y = (float)Math.Sin(x)*3 + 1;
// эта строка задает формулу, описывающую график функции для нашего уравнения y = f(x).
y = (float)Math.Sin(x)*3 + 1;

// запись координаты x
GrapValuesArray[elements_count, 0] = x;
// запись координаты y
GrapValuesArray[elements_count, 1] = y;
// подсчет элементов
elements_count++;
}

// изменяем флаг, сигнализировавший о том, что координаты графика не вычисленны
not_calculate = false;
}



Функция, выполняющая визуализацию графика.
Так как визуализацию графика можно отнести к конкретной подзадаче функции визуализации сцены (и чтобы не загромождать функцию визуализации сцены), мы вынесем визуализацию графика в отдельную функцию.


В этой функции сначала будет проверен флаг, сигнализирующий о том, что координаты графика вычислены и занесены в массив (переменная not_calculate). В том случае, если флаг указывает, что просчета значений еще не было – вызывается функция , которая посчитает значения координат точек графика и заполнит ими массив.

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

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


Исходный код данной функции.


// визуализация графика
private void DrawDiagram()
{
// проверка флага, сигнализирующего о том, что координаты графика вычеслены
if (not_calculate)
{
// если нет - то вызываем функцию вычисления координат графика
functionCalculation();
}

// стартуем отрисовку в режиме визуализации точек
// объединяемых в линии (GL_LINE_STRIP)
Gl.glBegin(Gl.GL_LINE_STRIP);

// рисуем начальную точку
Gl.glVertex2d(GrapValuesArray[0, 0], GrapValuesArray[0, 1]);

// проходим по массиву с координатами вычисленных точек
for (int ax = 1; ax < elements_count; ax+=2)
{
// передаем в OpenGL информацию о вершине, участвующей в построении линий
Gl.glVertex2d(GrapValuesArray[ax, 0], GrapValuesArray[ax, 1]);
}

// завершаем режим рисования
Gl.glEnd();

// устанавливаем размер точек, равный 5 пикселям
Gl.glPointSize(5);
// устанавливаем текущим цветом - красный цвет
Gl.glColor3f(255, 0, 0);
// активируем режим вывода точек (GL_POINTS)
Gl.glBegin(Gl.GL_POINTS);
// выводим красную точку, используя ту ячеку массива, до которой мы дошли (вычисляется в функии обработчике событий таймера)
Gl.glVertex2d(GrapValuesArray[pointPosition, 0], GrapValuesArray[pointPosition, 1]);
// завершаем режим рисования
Gl.glEnd();
// устанавливаем размер точек равный еденице
Gl.glPointSize(1);
}


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

 


// функция, управляющая визуализацией сцены
private void Draw()
{
// очистка буфера цвета и буфера глубины
Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);

// очищение текущей матрицы
Gl.glLoadIdentity();

// утснаовка черного цвета
Gl.glColor3f(0, 0, 0);

// помещаем состояние матрицы в стек матриц
Gl.glPushMatrix();

// выполняем перемещение в прострастве по осям X и Y
Gl.glTranslated(15, 15, 0);

// активируем рижим рисования (Указанные далее точки будут выводиться как точки GL_POINTS)
Gl.glBegin(Gl.GL_POINTS);

// с помощью прохода вдумя циклами, создаем сетку из точек
for (int ax = -15; ax < 15; ax++)
{
for (int bx = -15; bx < 15; bx++)
{
// вывод точки
Gl.glVertex2d(ax, bx);
}
}

// завершение редима рисования примитивов
Gl.glEnd();


// активируем режим рисования, каждые 2 последовательно вызванные комманды glVertex
// объединяются в линии
Gl.glBegin(Gl.GL_LINES);

// далее мы рисуем координатные оси и стрекли на их концах
Gl.glVertex2d(0, -15);
Gl.glVertex2d(0, 15);

Gl.glVertex2d(-15, 0);
Gl.glVertex2d(15, 0);

// вертикальная стрелка
Gl.glVertex2d(0, 15);
Gl.glVertex2d(0.1, 14.5);
Gl.glVertex2d(0, 15);
Gl.glVertex2d(-0.1, 14.5);

// горизонтальная трелка
Gl.glVertex2d(15, 0);
Gl.glVertex2d(14.5, 0.1);
Gl.glVertex2d(15, 0);
Gl.glVertex2d(14.5, -0.1);

// завершаем режим рисования
Gl.glEnd();

// выводим подписи осей "x" и "y"
PrintText2D(15.5f, 0, "x");
PrintText2D(0.5f, 14.5f, "y");

// вызываем функцию рисования графика
DrawDiagram();

// возвращаем матрицу из стека
Gl.glPopMatrix();

// выводим текст со значением координат возле курсора
PrintText2D(devX * Mcoord_X + 0.2f, (float)ScreenH - devY * Mcoord_Y + 0.4f, "[ x: " + (devX * Mcoord_X - 15).ToString() + " ; y: " + ((float)ScreenH - devY * Mcoord_Y - 15).ToString() + "]");

// устанавливаем красный цвет
Gl.glColor3f(255, 0, 0);

// включаем режим рисования линий, для того чтобы нарисовать
// линии от курсора мыши к координатным осям
Gl.glBegin(Gl.GL_LINES);

Gl.glVertex2d(lineX, 15);
Gl.glVertex2d(lineX, lineY);
Gl.glVertex2d(15, lineY);
Gl.glVertex2d(lineX, lineY);

Gl.glEnd();

// дожидаемся завершения визуализации кадра
Gl.glFlush();

// сигнал для обновление элемента реализующего визуализацию.
AnT.Invalidate();
}

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


Обсуждение данного урока: Визуализация графика функции в С# и OpenGL.
Далее: 6.1 Описание задачи, создание базовой оболочки программы, базовых классов. Тестирование работы редактора.