8.2 Сплайны - реализация алгоритма на OpenGL.

Необходимые знания:
Вам могут понадобится следующие статьи:
Разработка программы начинается с создания оболочки. Создайте окно программы и разместите на ней элемент openglsimplecontrol, как показано на рисунке 1, после чего установите его размеры 500х500.

Переименуйте данный объект, дав ему имя AnT.
Уроки OpenGL + C#: Заготовка окна Рисунок 1. Заготовка окна.

Справа от данного элемента разместите элемент comboBox, после чего в его свойствах установите значение параметра DropDownStyle = DropDownList. После этого выпадающие элементы перестанут быть доступными для редактирования. После этого измените элементы Items, как показано на рисунке 2.
Уроки OpenGL + C#: Заполнение элемента listBox Рисунок 2. Заполнение элемента listBox.
Также не забудьте установить ссылки на используемые библиотеки Tao (рис. 3).

Инициализация окна и OpenGl происходит так же, как и в предыдущих проектах. Код приведен на всякий случай чуть ниже.
Уроки OpenGL + C#: Подключение Tao Рисунок 3. Подключение Tao.
Для обработки событий мыши выделите объект AnT, перейдите к его свойствам, после чего перейдите к настройке событий и добавьте обработку событий мыши, как показано на рисунке 4.
Уроки OpenGL + C#: События мыши Рисунок 4. События мыши.

Для реализации визуализации будет использоваться таймер. После инициализации окна он будет генерировать событие, называемое тиком таймера, раз в 30 миллисекунд. Добавьте элемент таймер, переименуйте экземпляр в RenderTimer и установите время тика 30 миллисекунд (как показано на рисунке 5), а также добавьте ему событие для обработки тика.
Уроки OpenGL + C#: Создание таймера Рисунок 5. Создание таймера.
Нам потребуется объявить ряд переменных для дальнейшей работы программы:

/*http://esate.ru, Anvi*/

// массив, в который будут заносится управляющие точки 
private float [,] DrawingArray = new float [64, 2]; // количество точек 
private int count_points = 0;
private int max_point = 62;

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

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

private float devX;
private float devY;

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

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


/*
* Состояние захвата вершины мышью (при редактировании)
*/ 

int captured = -1; // -1 означает, что нет захваченной, иначе - номер указывает на элемент массива, хранящий захваченную вершину 


Код инициализации окна, настройки визуализации в OpenGL и старт таймера после завершения начальной настройки.

Не забудьте создать непосредственно само событие загрузки формы!

/*http://esate.ru, Anvi*/


public Form1()
{ 
  InitializeComponent();
  AnT.InitializeContexts();
}

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 = 500.0;
    ScreenH = 500.0 * ( float )AnT.Height / ( float )AnT.Width;
    Glu.gluOrtho2D(0.0, ScreenW, 0.0, ScreenH);
  }
  else
  {
    ScreenW = 500.0 * ( float )AnT.Width / ( float )AnT.Height;
    ScreenH = 500.0;
    Glu.gluOrtho2D(0.0, 500.0 * ( float )AnT.Width / ( float )AnT.Height, 0.0, 500.0);
  }

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

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

}


Функция визуализации текста уже была рассмотрена нами ранее.

/*http://esate.ru, Anvi*/


// функция визуализации текста 
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_8_BY_13, char_for_draw);
  }

}

Теперь переходим непосредственно к обработке нажатий мыши, таймера и построению сплайна.

Обработка события таймера:

/*http://esate.ru, Anvi*/


private void RenderTime_Tick( object sender, EventArgs e)
{ 
  // обработка 'тика' таймера - вызов функции отрисовки 
  Draw();
}

Функция отрисовки:

/*http://esate.ru, Anvi*/


// функция отрисовки, вызываемая событием таймера 
private void Draw()
{ 

  // количество сегментов при расчете сплайна 
  int N = 30; // вспомогательные переменные для расчета сплайна 
  double X, Y;

  // n = count_points+1 означает что мы берем все созданные контрольные 
  // точки + ту, которая следует за мышью, для создания интерактивности приложения 
  int eps = 4, i, j, n = count_points+1, first;
  double xA, xB, xC, xD, yA, yB, yC, yD, t;
  double a0, a1, a2, a3, b0, b1, b2, b3;

  // очистка буфера цвета и буфера глубины 
  Gl.glClear( Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);
  Gl.glClearColor(255, 255, 255, 1);
  // очищение текущей матрицы 
  Gl.glLoadIdentity();

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

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

  Gl.glPointSize(5.0f);
  Gl.glBegin( Gl.GL_POINTS);

  Gl.glVertex2d(0, 0);

  Gl.glEnd();
  Gl.glPointSize(1.0f);

  PrintText2D(devX * Mcoord_X + 0.2f, ( float )ScreenH - devY * Mcoord_Y + 0.4f, "[ x: " + (devX * Mcoord_X).ToString() + " ; y: " + (( float )ScreenH - devY * Mcoord_Y).ToString() + "]");
  // выполняем перемещение в пространстве по осям X и Y 

  // выполняем цикл по контрольным точкам 
  for (i = 0; i < n; i++)
  { 
  
    // сохраняем координаты точки (более легкое представления кода) 
    X = DrawingArray[i, 0];
    Y = DrawingArray[i, 1];

    // если точка выделена (перетаскивается мышью) 
    if (i == captured)
    { 
      // для ее отрисовки будут использоваться более толстые линии 
      Gl.glLineWidth(3.0f);
    }

    // начинаем отрисовку точки (квадрат) 
    Gl.glBegin( Gl.GL_LINE_LOOP);

    Gl.glVertex2d(X - eps, Y - eps);
    Gl.glVertex2d(X + eps, Y - eps);
    Gl.glVertex2d(X + eps, Y + eps);
    Gl.glVertex2d(X - eps, Y + eps);

    Gl.glEnd();

    // если была захваченная точка, необходимо вернуть толщину линий 
    if (i == captured)
    { 
      // возвращаем прежнее значение 
      Gl.glLineWidth(1.0f);
    }

  }


  // дополнительный цикл по всем контрольным точкам - 
  // подписываем их координаты и номер 
  for (i = 0; i < n; i++)
  { 
    // координаты точки 
    X = DrawingArray[i, 0];
    Y = DrawingArray[i, 1];
    // выводим подпись рядом с точкой 
    PrintText2D(( float )(X - 20), ( float )(Y - 20), "P " + i.ToString() + ": " + X.ToString() + ", " + Y.ToString());
  }

  // начинает отрисовку кривой 
  Gl.glBegin( Gl.GL_LINE_STRIP);

  // используем все точки -1 (т,к. алгоритм 'зацепит' i+1 точку 
  for (i = 1; i < n-1; i++)
  { 
    
    // реализация представленного в теоретическом описании алгоритма для калькуляции сплайна 
    first = 1;
    xA = DrawingArray[i - 1, 0];
    xB = DrawingArray[i, 0];
    xC = DrawingArray[i + 1, 0];
    xD = DrawingArray[i + 2, 0];

    yA = DrawingArray[i - 1, 1];
    yB = DrawingArray[i, 1];
    yC = DrawingArray[i + 1, 1];
    yD = DrawingArray[i + 2, 1];

    a3 = (-xA + 3 * (xB - xC) + xD) / 6.0;

    a2 = (xA - 2 * xB + xC) / 2.0;

    a1 = (xC - xA) / 2.0;

    a0 = (xA + 4 * xB + xC) / 6.0;

    b3 = (-yA + 3 * (yB - yC) + yD) / 6.0;

    b2 = (yA - 2 * yB + yC) / 2.0;

    b1 = (yC - yA) / 2.0;

    b0 = (yA + 4 * yB + yC) / 6.0;

      // отрисовка сегментов 

      for (j = 0; j <= N; j++)
      {
        // параметр t на отрезке от 0 до 1 
        t = ( double )j / ( double )N;

        // генерация координат 
        X = (((a3 * t + a2) * t + a1) * t + a0);
        Y = (((b3 * t + b2) * t + b1) * t + b0);

        // и установка вершин 
        if (first == 1)
        { 
          first = 0;
          Gl.glVertex2d(X, Y);
        }
        else
          Gl.glVertex2d(X, Y);

      }

  }
  Gl.glEnd();


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

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


}

Обработка события мыши, установленная для объекта AnT, будет решать две задачи: при перемещении курсора, интерактивная (т.е. еще не зафиксированная точка) будет создавать эффект создания сплайна на ходу. Но если установлен режим редактирования сплайна, то в том случае, если какая-либо из точек захвачена, т.е. на нее наведен курсор и зажата левая клавиша мыши, то до тех пор пока пользователь не отпустит клавишу мыши, мы будем вносить изменения в координаты данной (захваченной) вершины.


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

Обработка событий мыши:

/*http://esate.ru, Anvi*/


// обработка движения мыши 
private void AnT_MouseMove( object sender, Mouse EventArgs e)
{
  // если установлен режим создания сплайна 
  if (comboBox1.SelectedIndex == 0)
  {
    // сохраняем координаты мыши 
    Mcoord_X = e.X;
    Mcoord_Y = e.Y; // вычисляем параметры для будущей дорисовки линий от указателя мыши к координатным осям. 
    lineX = devX * e.X;
    lineY = ( float )(ScreenH - devY * e.Y);

    // текущая (интерактивная точка, добавляемая к уже установленным - непрерывно изменяется от движения 
    // мыши и создает эффект интерактивности и наглядности приложения 
    DrawingArray[count_points, 0] = lineX;
    DrawingArray[count_points, 1] = lineY;

  }
  else
  {
    // обычное протоколирование координат для подсвечивания вершины в случае наведения 
    // сохраняем координаты мыши 
    Mcoord_X = e.X;
    Mcoord_Y = e.Y;

    // вычисляем параметры для будущей дорисовки линий от указателя мыши к координатным осям. 

    float _lastX = lineX;
    float _lastY = lineY;

    lineX = devX * e.X;
    lineY = ( float )(ScreenH - devY * e.Y);

    // если точка захвачена (т.е. пользователь удерживает кнопку мыши), 
    if (captured != -1)
    {
      // то мы вносим разницу с последними координатами курсора 
      // другими словами перемещаем захваченную точку 
      DrawingArray[captured, 0] -= _lastX-lineX;
      DrawingArray[captured, 1] -= _lastY-lineY;
    }

  }
}

// щелчок мыши 
private void AnT_MouseClick( object sender, Mouse EventArgs e)
{ 
  // если мы находимся в режиме создания сплайна
  if (comboBox1.SelectedIndex == 0)
  { 
    // забираем координаты мыши 
    Mcoord_X = e.X;
    Mcoord_Y = e.Y;

    // приводим к нужному нам формату в соответствии с настройками проекции 
    lineX = devX * e.X;
    lineY = ( float )(ScreenH - devY * e.Y);

    // создаем новую контрольную точку 
    DrawingArray[count_points, 0] = lineX;
    DrawingArray[count_points, 1] = lineY;

    // и увеличиваем значение счетчика контрольных точек 
    count_points++;
  }
}

// если ОПУЩЕНА клавиша мыши 
private void AnT_MouseDown( object sender, Mouse EventArgs e)
{ 
  
  if (count_points == max_point) // мы не должны выходить за границы объявленных массивов
       return;

  // если режим редактирования сплайна 
  if (comboBox1.SelectedIndex == 1)
  { 
    // получаем и преобразовываем координаты нажатия 
    Mcoord_X = e.X;
    Mcoord_Y = e.Y;

    lineX = devX * e.X;
    lineY = ( float )(ScreenH - devY * e.Y);

    // проходим циклом по всем установленным контрольным точкам 
    for ( int ax = 0; ax < count_points; ax++)
    { 
      // если точка попадает под курсор 
      if (lineX < DrawingArray[ax, 0] + 5 && lineX > DrawingArray[ax, 0] - 5 && lineY < DrawingArray[ax, 1] + 5 && lineY > DrawingArray[ax, 1] - 5)
      { 
        // отмечаем ее как захваченную (записываем ее индекс в массив captured) 
        captured = ax;
        // останавливаем цикл, мы нашли нужную точку 
        break;
      }
    }
  }
}

// если клавиша мыши поднята, пользователь отпустил клавишу, и точка должна остановиться в перемещении 
private void AnT_MouseUp( object sender, Mouse EventArgs e)
{ 
  // отмечаем, что нет захваченной точки 
  captured = -1;
}

Вот и все, теперь можно протестировать работу программы (рис. 6).
Уроки OpenGL + C#: Результат работы программы: реализация B-сплайнов на OpenGl Рисунок 6. Результат работы программы: реализация B-сплайнов на OpenGl.

Прикрепленные файлы для скачивания:
Добавить комментарий
Расширенный режим добавления комментариев доступен на форуме: загрузка изображений, цитирование, форматирование текста, и т.д.
Ваше имя:
Текст сообщения:
Комментарии (5):
mike124
mike124,  
Цитата
private int count_po int s = 0;
Цитата
private void Pr int Text2D( float x, float y, string text)
Цитата
foreach ( char char _for_draw in text)
Цитата
Glut.glutBitmapCharacter( Glut.GLUT_BITMAP_8_BY_13, char _for_draw);
Цитата
int eps = 4, i, j, n = count_po int s+1, first; // чему равно n? Шарп дает ошибку на такой синтаксис.
Цитата
DrawingArray[count_po int s, 0] = lineX;
DrawingArray[count_po int s, 1] = lineY;
Цитата
// создаем новую контрольную точку
DrawingArray[count_po int s, 0] = lineX;
DrawingArray[count_po int s, 1] = lineY;

// и увеличиваем значение счетчика контрольных точек
count_po int s++;

Ошибки.

Посмотрел исходники :) Почему-то имя переменной count_points разбило на 3 части. Причем, везде.
mike124
mike124,  
Так же забыта функция PrintText2D.
mike124
mike124,  
Точнее, имя функции также покорежило (1 сообщение, цитата 2)
Anvi
Anvi,  
Большое спасибо :good:
alita
alita,  
mike124,
Спасибо.
Цитата
mike124 пишет:
Посмотрел исходники Почему-то имя переменной count_points разбило на 3 части. Причем, везде.
Наследие старого сайта :( . Есть мелкие ошибки после переезда контента на другую CMS.

В этом уроке текст исправлен. Исходный код урока перезалит на сайт с учетом правки кода для исключения ошибки при выходе из границ массива :good: .
^