9.2 Алгоритмы геометрических преобразований - реализация на OpenGl.

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

Переименуйте данный объект, дав ему имя AnT.

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

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

Инициализация окна и OpenGl происходит так же, как и в предыдущих проектах:

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

public Form1()
{
  InitializeComponent();
  // инициализация для работы с openGL 
  AnT.InitializeContexts();
}

// массив вершин создаваемого геометрического объекта 
private float[,] GeomObject = new float[32, 3];
// счетчик его вершин 
private int count_elements = 0;

// событие загрузки формы окна 
private void Form1_Load( object sender, EventArgs e)
{ 
  // инициализация OpenGL, много раз комментированная ранее 
  // инициализация библиотеки 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();

  Glu.gluPerspective(45, (float)AnT.Width / (float)AnT.Height, 0.1, 200);

  Gl.glMatrixMode( Gl.GL_MODELVIEW);
  Gl.glLoadIdentity();

  Gl.glEnable( Gl.GL_DEPTH_TEST);

  // пирамида для визуализации (4 точки) 

  GeomObject[0, 0] = -0.7f;
  GeomObject[0, 1] = 0;
  GeomObject[0, 2] = 0;

  GeomObject[1, 0] = 0.7f;
  GeomObject[1, 1] = 0;
  GeomObject[1, 2] = 0;

  GeomObject[2, 0] = 0.0f;
  GeomObject[2, 1] = 0;
  GeomObject[2, 2] = 1.0f;

  GeomObject[3, 0] = 0;
  GeomObject[3, 1] = 0.7f;
  GeomObject[3, 2] = 0.3f;

  // количество вершин рассматриваемого геометрического объекта 
  count_elements = 4;
  // устанавливаем ось X по умолчанию 
  comboBox1.SelectedIndex = 0;

  // начало визуализации (активируем таймер) 
  RenderTimer.Start();

}

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

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

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

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

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


// функция отрисовки 
private void Draw()
{ 
    // очистка буфера цвета и буфера глубины 
    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.glTranslated(0, 0, -7);
    // поворачиваем ее на 15 градусов 
    Gl.glRotated(15, 1, 1, 0);
    
    // помещаем состояние матрицы в стек матриц 
    Gl.glPushMatrix();
    
    // начинаем отрисовку объекта 
    Gl.glBegin( Gl.GL_LINE_LOOP);
    
    // геометрические данные мы берем из массива GeomObject 
    // рисуем основание с помощью зацикленной линии 
    Gl.glVertex3d(GeomObject[0, 0], GeomObject[0, 1], GeomObject[0, 2]);
    Gl.glVertex3d(GeomObject[1, 0], GeomObject[1, 1], GeomObject[1, 2]);
    Gl.glVertex3d(GeomObject[2, 0], GeomObject[2, 1], GeomObject[2, 2]);
    
    // завершаем отрисовку примитивов 
    Gl.glEnd();
    
    // рисуем линии от вершин основания к вершине пирамиды 
    Gl.glBegin( Gl.GL_LINES);
    
    Gl.glVertex3d(GeomObject[0, 0], GeomObject[0, 1], GeomObject[0, 2]);
    Gl.glVertex3d(GeomObject[3, 0], GeomObject[3, 1], GeomObject[3, 2]);
    
    Gl.glVertex3d(GeomObject[1, 0], GeomObject[1, 1], GeomObject[1, 2]);
    Gl.glVertex3d(GeomObject[3, 0], GeomObject[3, 1], GeomObject[3, 2]);
    
    Gl.glVertex3d(GeomObject[2, 0], GeomObject[2, 1], GeomObject[2, 2]);
    Gl.glVertex3d(GeomObject[3, 0], GeomObject[3, 1], GeomObject[3, 2]);
    
    // завершаем отрисовку примитивов 
    Gl.glEnd();
    
    // возвращаем состояние матрицы 
    Gl.glPopMatrix();
    
    // возвращаем состояние матрицы 
    Gl.glPopMatrix();
    
    // отрисовываем геометрию 
    Gl.glFlush();
    
    // обновляем состояние элемента 
    AnT.Invalidate();
}

Что же касается самих преобразований, то они реализованы с помощью 3-х функций, которые будут активированы при обработке соответствующих клавиш.

W / S реализуют перемещение по указанной оси.

A / D – вращение вокруг указанной оси.

Z / X – масштабирование.

Обработка данных клавиш клавиатуры и обработка события элемента comboBox при изменении его значения для исключения перехвата им нажатия клавиш:

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

// обработка событий от клавиатуры - нажатие (клавиша зажата!) 
private void AnT_KeyDown( object sender, Key EventArgs e)
{

  // Z и X отвечают за масштабирование 
  if(e.KeyCode == Keys.Z)
  { 
    // вызов функции, в которой мы реализуем масштабирование - передаем коэффициент масштабирования и выбранную ось в окне программы 
    CreateZoom(1.05f, comboBox1.SelectedIndex);
  }
  if(e.KeyCode == Keys.X)
  {
    // вызов функции, в которой мы реализуем масштабирование - передаем коэффициент масштабирования и выбранную ось в окне программы 
    CreateZoom(0.95f, comboBox1.SelectedIndex);
  }

  // W и S отвечают за перенос 
  if(e.KeyCode == Keys.W)
  { 
    // вызов функции, в которой мы реализуем перенос - передаем значение перемещения и выбранную ось в окне программы 
    CreateTranslate(0.05f, comboBox1.SelectedIndex);
  }
  if(e.KeyCode == Keys.S)
  { 
    // вызов функции, в которой мы реализуем перенос - передаем значение перемещения и выбранную ось в окне программы 
    CreateTranslate(-0.05f, comboBox1.SelectedIndex);

  } // A и D отвечают за поворот 
  if(e.KeyCode == Keys.A)
  { 
    // вызов функции, в которой мы реализуем поворот - передаем значение для поворота и выбранную ось 
    CreateRotate(0.05f, comboBox1.SelectedIndex);
  }
  if(e.KeyCode == Keys.D)
  { 
    CreateRotate(-0.05f, comboBox1.SelectedIndex);
  }
}

// дополнительное событие для элемента comboBox - если произошло 
// изменение его значения - установить фокус в элемент AnT 
// чтобы избежать перехват события нажатия клавиши данным элементом 
private void comboBox1_SelectedIndexChanged( object sender, EventArgs e)
{ 
  // устанавливаем фокус в AnT 
  AnT.Focus();
}

Ну и самое главное – собственно реализация поворота, переноса и масштабирования:

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

// функция масштабирования 
private void CreateZoom( float coef, int os)
{
  // создаем матрицу 
  float[,] Zoom3D = new float[3, 3]; Zoom3D[0, 0] = 1;
  Zoom3D[1, 0] = 0;
  Zoom3D[2, 0] = 0;

  Zoom3D[0, 1] = 0;
  Zoom3D[1, 1] = 1;
  Zoom3D[2, 1] = 0;

  Zoom3D[0, 2] = 0;
  Zoom3D[1, 2] = 0;
  Zoom3D[2, 2] = 1;

  // устанавливаем коэффициент масштабирования для необходимой (выбранной и переданной в качестве параметра) оси 
  Zoom3D[os, os] = coef;

  // вызываем функцию для выполнения умножения матриц, представляющих собой координаты вершин геометрического объекта 
  // на созданную в данной функции матрицу 
  multiply(GeomObject, Zoom3D);
}

// перенос 
private void CreateTranslate( float translate, int os)
{ 
  // в виду простоты данного алгоритма, мы упростили его обработку - 
  // достаточно прибавить изменение (перенос) в координатах объекта по выбранной и переданной оси 
  for ( int ax = 0; ax < count_elements; ax++)
  { 
    // обновление координат (для выбранной оси) 
    GeomObject[ax, os] += translate;
  }
}

// реализация поворота 
private void CreateRotate( float angle, int os)
{ 
  // массив, который будет содержать матрицу 
  float[,] Rotate3D = new float[3, 3];
  // в зависимости от оси, матрицы будут кардинально различаться, 
  // поэтому создаем необходимую матрицу в зависимости от оси, используя switch 
  switch (os)
  { 
    case 0: // вокруг оси Х 
    { 
      Rotate3D[0, 0] = 1;
      Rotate3D[1, 0] = 0;
      Rotate3D[2, 0] = 0;

      Rotate3D[0, 1] = 0;
      Rotate3D[1, 1] = (float)Math.Cos(angle);
      Rotate3D[2, 1] = (float)-Math.Sin(angle);

      Rotate3D[0, 2] = 0;
      Rotate3D[1, 2] = (float)Math.Sin(angle);
      Rotate3D[2, 2] = (float)Math.Cos(angle);
      break;
    }
    case 1: // вокруг оси Y 
    { 
      Rotate3D[0, 0] = (float)Math.Cos(angle);
      Rotate3D[1, 0] = 0;
      Rotate3D[2, 0] = (float)Math.Sin(angle);
      Rotate3D[0, 1] = 0;
      Rotate3D[1, 1] = 1;
      Rotate3D[2, 1] = 0;
      Rotate3D[0, 2] = (float)-Math.Sin(angle);
      Rotate3D[1, 2] = 0;
      Rotate3D[2, 2] = (float)Math.Cos(angle);
      break;

    }
    case 2: // вокруг оси Z 
    { 
      Rotate3D[0, 0] = (float)Math.Cos(angle);
      Rotate3D[1, 0] = (float)-Math.Sin(angle);
      Rotate3D[2, 0] = 0;

      Rotate3D[0, 1] = (float)Math.Sin(angle);
      Rotate3D[1, 1] = (float)Math.Cos(angle);
      Rotate3D[2, 1] = 0;

      Rotate3D[0, 2] = 0;
      Rotate3D[1, 2] = 0;
      Rotate3D[2, 2] = 1;
      break;
    }

  }

  // вызываем функцию для выполнения умножения матриц, представляющих собой координаты вершин геометрического объекта 
  // на созданную в данной функции матрицу 
  multiply(GeomObject, Rotate3D);
}

// функция умножения матриц 
private void multiply(float[,] obj, float[,] matrix)
{ 
  // временные переменные 
  float res_1, res_2, res_3;

  // проходим циклом по всем координатам (представляющие собой матрицу A [x,y,z]) 
  // и умножаем каждую матрицу на матрицу B (переданную) 
  // результат сразу заносим в массив геометрии 

  for ( int ax = 0; ax < count_elements; ax++)
  { 
    res_1 = (obj[ax, 0] * matrix[0, 0] + obj[ax, 1] * matrix[0, 1] + obj[ax, 2] * matrix[0, 2]);
    res_2 = (obj[ax, 0] * matrix[1, 0] + obj[ax, 1] * matrix[1, 1] + obj[ax, 2] * matrix[1, 2]);
    res_3 = (obj[ax, 0] * matrix[2, 0] + obj[ax, 1] * matrix[2, 1] + obj[ax, 2] * matrix[2, 2]);

    obj[ax, 0] = res_1;
    obj[ax, 1] = res_2;
    obj[ax, 2] = res_3;
  }
}

Результат работы программы вы можете видеть на рисунке 6. Псевдо-трехмерный объект, управляемый нажатиями кнопок W,S,A,D,Z,X и установкой оси в окне формы.
Уроки OpenGL + C#: Результат работы программы Рисунок 6. Результат работы программы.
Данный метод отлично демонстрирует математические основы геометрических преобразований. Его недостатком является преобразование геометрии самого объекта.

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

Для реализации самого перемещения, поворота и масштабирования нам будет достаточно использовать функции glTranslate**, glRotate**, glScale**. Мы рассмотрим это в следующих главах.

Прикрепленные файлы для скачивания:
Добавить комментарий
Расширенный режим добавления комментариев доступен на форуме: загрузка изображений, цитирование, форматирование текста, и т.д.
Ваше имя:
Текст сообщения:
Комментарии (10):
ONE_GOG
ONE_GOG,  
Здравствуйте.


Скопировал код. Работает. Большое спасибо.
Но не работает, если пытаться запускать на дочерних формах. Не подскажете в чем может быть причина? При загрузке дочерней формы приложение завершается с кодом (0*1).

Могу приложить код программы. Пользуюсь Visual Studio 2013.

Заранее спасибо!
927 forever
927 forever,  
Привет, возможно вот это поможет
http://esate.ru/forum/?PAGE_NAME=message&fid=2&tid=36&mid=136#message136
ONE_GOG
ONE_GOG,  
Цитата
927 forever написал:
Привет, возможно вот это поможет
http://esate.ru/forum/?PAGE_NAME=message&fid=2&tid=36&mid=136#message136

Приветствую!

Спасибо большое за помощь! Я уже сам разобрался. Оказывается не надо инициализировать glut несколько раз. Только один. Тогда все работает и на дочерних формах.
ONE_GOG
ONE_GOG,  
Здравствуйте.

Хочу сделать управление премещения, вращения и поворота через мышь. Фактически требуется чтобы фигура вращалась за указателем мыши при нажатии кнопки "A".

На данный момент не знаю как связать два события "нажатие на кнопку А" и "движение мышкой" с функцией вращения фигуры. Можете подсказать как это сделать?

Георгий
ONE_GOG
ONE_GOG,  
Здравствуйте. Скажите, а зачем мы инициализируем glut и указываем, что будем работать с двойным буфером глубины, если мы его нигде не используем?
noname
noname,  
Цитата
ONE_GOG написал:
Скажите, а зачем мы инициализируем glut и указываем, что будем работать с двойным буфером глубины, если мы его нигде не используем?
мне кажется просто копипастили куски настроек инициализации, так оно сюда и попало.
но , если не ошибаюсь, можно вызывать glutPostRedisplay и все будет работать корректно
ONE_GOG
ONE_GOG,  
Цитата
noname написал:
Цитата
ONE_GOG написал:
Скажите, а зачем мы инициализируем glut и указываем, что будем работать с двойным буфером глубины, если мы его нигде не используем?
мне кажется просто копипастили куски настроек инициализации, так оно сюда и попало.
но , если не ошибаюсь, можно вызывать glutPostRedisplay и все будет работать корректно
А можете более подробно про работу функции glutPostRedisplay рассказать? Просто у меня назрела проблема. Я хочу использовать двойную буферизацию, но не могу этого сделать. Пробовал использовать glutswapbuffer - приложение перестает запускаться.
noname
noname,  
Ее используются чтобы отдать команду о перерисовке сцены.
Чтобы не ждать запуска idle функции, а сразу при каких-либо пользовательских действиях отправить на перерисовку.
ONE_GOG
ONE_GOG,  
Цитата
noname написал:
Ее используются чтобы отдать команду о перерисовке сцены.
Чтобы не ждать запуска idle функции, а сразу при каких-либо пользовательских действиях отправить на перерисовку.
А есть книга, где это полностью описано и желательно с примерами. Идеально, если рассматривается работа с двойным буфером на windows form c#.
Кстати, а я правильно понимаю, что двойной буфер нужен, когда речь идет об анимации? То есть двигается или камера или сам объект. Ну а если, статическая картинка, он не нужен?
constX7
constX7,  
Здравствуйте! С математикой не все так хорошо как хотелось бы, поэтому вынужден обратиться за помощью. Реализовал поворот фигуры как описано в "Алгоритмы геометрических преобразований - реализация на OpenGl.", но там сказано: "Предположим, что центр вращения совпадает с началом координат." А как реализовать вращение фигуры со смещение относительно центра (0, 0), например, на (10, 10)? Спасибо.
^