NeHe Tutorials Народный учебник по OpenGL
Урок 44. OpenGL

Quaternion Camera Class

Hi! Меня зовут Вик Холлис. Я натолкнулся на уроки NeHe пару лет назад, и я благодарен ему за эти уроки, и вот я решил тоже внести свой вклад. В последнее время я изучал кватернионы, которые использовал, чтобы вращать объекты, и если честно, я их все еще не очень хорошо понимаю, как хотелось бы. Поскольку я начал их применять, я могу сказать, что работа с кватернионами может вызвать множество проблем. Конечно, всегда можно обойтись матрицами и векторной математикой, кроме этого используя кватернионы, все равно придется иметь дело с ними. В этом уроке представлен класс камеры, которая вращается, используя кватернионы. Кто захочет, может использовать исходники в своем проекте. Я не буду в этом уроке останавливаться на математике кватернионов, и без этого полно информации о них. Как и большинству программистов, мне важен результат. В этом уроке будет создан ландшафт, основанный на карте высот. Для полетов над этим ландшафтом будет использован класс камеры, который использует кватернионы. Способ управления полетом похож на тот, который используется в игре Wing Commander. Другие способы полета вы можете сделать сами, базируясь на этом уроке.

 

Кватернион – это странная штука. После нескольких месяцев попыток визуально представить их себе, я понял, что это невозможно, и решил принять их такими, какие они есть. Эта вера помогает мне манипулировать ими, и я бы рекомендовал вам сделать тоже самое. Я думаю, вы слышали о блокировке вращения (gimbal lock – дословно шарнирная блокировка, когда объект отказывается поворачиваться в определенную сторону или внезапно переходит в другое местоположение). Блокировка происходит, когда вы суммируете несколько вращений вместе. Кватернионы решаю эту проблему, и поэтому столь полезны. Я думаю, хватит, ходить “вокруг да около”, вот одна из функций класса:

 

void glQuaternion::CreateFromAxisAngle(GLfloat x, GLfloat y, GLfloat z, GLfloat degrees)

{

  // Вначале конвертируем углы в радианы

  // поскольку углы в радианах

  GLfloat angle = GLfloat((degrees / 180.0f) * PI);

 

  // Вычислим sin( theta / 2) для оптимизации

  GLfloat result = (GLfloat)sin( angle / 2.0f );

 

  // Вычисляем значение w как cos( theta / 2 )

  m_w = (GLfloat)cos( angle / 2.0f );

 

  // Вычислим x, y и z кватерниона

  m_x = GLfloat(x * result);

  m_y = GLfloat(y * result);

  m_z = GLfloat(z * result);

}

 

Думайте об этой функции, как о glRotatef(). Те же параметры только в другом порядке. Далее вы увидите, что можно их использовать с перестановкой и в OpenGL. Как вы могли заметить кватернион представлен четверкой чисел: x, y, z, w, где x, y, z – ось вращения, w – угол вращения вокруг оси. Ниже еще одна функция класса:

 

void glQuaternion::CreateMatrix(GLfloat *pMatrix)

{

  // Проверим, выделено ли место под матрицу

  if(!pMatrix) return;

 

  // Первая строка

  pMatrix[ 0] = 1.0f - 2.0f * ( m_y * m_y + m_z * m_z );

  pMatrix[ 1] = 2.0f * (m_x * m_y + m_z * m_w);

  pMatrix[ 2] = 2.0f * (m_x * m_z - m_y * m_w);

  pMatrix[ 3] = 0.0f;

 

  // Вторая сторока

  pMatrix[ 4] = 2.0f * ( m_x * m_y - m_z * m_w );

  pMatrix[ 5] = 1.0f - 2.0f * ( m_x * m_x + m_z * m_z );

  pMatrix[ 6] = 2.0f * (m_z * m_y + m_x * m_w );

  pMatrix[ 7] = 0.0f;

 

  // Третья строка

  pMatrix[ 8] = 2.0f * ( m_x * m_z + m_y * m_w );

  pMatrix[ 9] = 2.0f * ( m_y * m_z - m_x * m_w );

  pMatrix[10] = 1.0f - 2.0f * ( m_x * m_x + m_y * m_y );

  pMatrix[11] = 0.0f;

 

  // Четвертая строка

  pMatrix[12] = 0;

  pMatrix[13] = 0;

  pMatrix[14] = 0;

  pMatrix[15] = 1.0f;

 

  // Сейчас pMatrix[] – это гомогенная матрица, которая может быть использована в OpenGL

}

 

Как следует из названия функции, она создает гомогенную матрицу 4 на 4, которую можно использовать в glMultMatrixf(). Это значит, что нет необходимости использовать glRotatef(), при использовании этого класса для задания вращения, т.е. вначале вызываете CreateFromAxisAngle(), а потом извлекаете матрицу, чтобы использовать ее в glMultMatrixf(). Следующая функция – это оператор умножения кватерниона, она позволяет комбинировать вращения.

 

glQuaternion glQuaternion::operator *(glQuaternion q)

{

  glQuaternion r;

 

  r.m_w = m_w*q.m_w - m_x*q.m_x - m_y*q.m_y - m_z*q.m_z;

  r.m_x = m_w*q.m_x + m_x*q.m_w + m_y*q.m_z - m_z*q.m_y;

  r.m_y = m_w*q.m_y + m_y*q.m_w + m_z*q.m_x - m_x*q.m_z;

  r.m_z = m_w*q.m_z + m_z*q.m_w + m_x*q.m_y - m_y*q.m_x;

 

  return(r);

}

 

Обратите свое внимание, что результат умножения кватернионов не коммутативный, т.е. Qa*Qb не равно Qb*Qa. Так же как и для матриц. Это сыграет свою роль позже, когда мы будем искать наши мировые координаты для переноса. Вот и все что нам надо от класса кватерниона. Далее рассмотрим простой класс камеры.

 

void glCamera::SetPrespective()

{

  GLfloat Matrix[16];

  glQuaternion q;

 

  // Два кватерниона для представления наших вращений

  m_qPitch.CreateFromAxisAngle(1.0f, 0.0f, 0.0f, m_PitchDegrees);

  m_qHeading.CreateFromAxisAngle(0.0f, 1.0f, 0.0f, m_HeadingDegrees);

 

  // Комбинируем наклон и направление

  q = m_qPitch * m_qHeading;

  q.CreateMatrix(Matrix);

 

  // Настройка перспективы!

  glMultMatrixf(Matrix);

 

  // Создать матрицу из наклона и взять j для вектора направления.

  m_qPitch.CreateMatrix(Matrix);

  m_DirectionVector.j = Matrix[9];

 

  // Комбинируем наклон и направление, из полученной матрицы берем i и k для вектора.

  q = m_qHeading * m_qPitch;

  q.CreateMatrix(Matrix);

  m_DirectionVector.i = Matrix[8];

  m_DirectionVector.k = Matrix[10];

 

  // Масштабируем направление на нашу скорость.

  m_DirectionVector *= m_ForwardVelocity;

 

  // Смещаем позицию на вектор

  m_Position.x += m_DirectionVector.i;

  m_Position.y += m_DirectionVector.j;

  m_Position.z += m_DirectionVector.k;

 

  // Перенос на новую позицию

  glTranslatef(-m_Position.x, -m_Position.y, m_Position.z);

}

 

Этот код необходимо разъяснить. Вначале мы объявим массив из 16 чисел с плавающей запятой, т.к. в OpenGL матрица задается в таком виде. Далее нам необходим временный кватернион q для сохранения результата умножения кватернионов. Далее мы вычисляем два кватерниона, которые задают вращения по осям X и Y. Потом мы умножаем эти два кватерниона, чтобы получить ориентацию на нашей сцене (направление вперед). Далее нам необходимо вектор направления, полученный их нашего кватерниона, который мы используем для сдвига нашей позиции на сцене. Из матрицы, полученной из m_Pitch, можно извлечь координату ‘j’ нашего вектора направления. Обратите внимание на интересную особенность, дело в том, что в третьей строке матрицы (элементы 8, 9, 10) всегда находятся координаты сдвига. Поэтому не надо использовать дополнительные вычисления, чтобы получить их. Помните, что я говорил, что умножение кватернионов не коммуникативное? Здесь мы используем это, чтобы получить i и k координаты нашего вектора. Мы умножим m_Heading на m_Pitch, чтобы получить другую матрицу. В этой матрицы находятся i и k координаты нашего вектора, и поскольку мы используем нормированные кватернионы для представления наших вращений, то и значения в третьей строке матрицы могут быть использованы как нормированный вектор. После того как мы получим этот вектор, мы масштабируем его на нашу скорость, и добавляем обратно позицию. Теперь осталось сдвинуть на полученную позицию. Имейте ввиду, что этот стиль полета используется в игре Wing Commander, а не в MS Flight Simulator. Далее функция Init GL:

 

  // загрузить карту высот

  if(!hMap.LoadRawFile("Art/Terrain1.raw", MAP_SIZE * MAP_SIZE))

  {

    MessageBox(NULL, "Failed to load Terrain1.raw.", "Error", MB_OK);

  }

 

  // Загрузить текстуру

  if(!hMap.LoadTexture("Art/Dirt1.bmp"))

  {

    MessageBox(NULL, "Failed to load terrain texture.", "Error", MB_OK);

  }

 

  // Настроить ограничения нашей камеры

  Cam.m_MaxForwardVelocity = 5.0f;

  Cam.m_MaxPitchRate = 5.0f;

  Cam.m_MaxHeadingRate = 5.0f;

  Cam.m_PitchDegrees = 0.0f;

  Cam.m_HeadingDegrees = 0.0f;

 

Единственное различие между нашей функцией init и той, что приведена в коде NeHe, то, что я добавил в нее код выше. Вначале загружается raw-файл со сгенерированной картой высот, и затем загружается текстура для ландшафта. После этого производятся настройки движения камеры, такие как максимум скорости, изменения высоты и направления движения. Так же посмотрим на обработчик клавиатуры:

 

void CheckKeys(void)

{

  if(keys[VK_UP])

  {

    Cam.ChangePitch(5.0f);

  }

 

  if(keys[VK_DOWN])

  {

    Cam.ChangePitch(-5.0f);

  }

 

  if(keys[VK_LEFT])

  {

    Cam.ChangeHeading(-5.0f);

  }

 

  if(keys[VK_RIGHT])

  {

    Cam.ChangeHeading(5.0f);

  }

 

  if(keys['W'] == TRUE)

  {

    Cam.ChangeVelocity(0.1f);

  }

 

  if(keys['S'] == TRUE)

  {

    Cam.ChangeVelocity(-0.1f);

  }

}

 

Здесь мы проверяем нажатия клавиш и затем соответственно меняем положение камеры. В классе камеры есть соответствующие функции для этого: ChangeVelocity (), ChangeHeading (), и ChangePitch (). В этих функциях так же проверяются некоторые случаи, например, если камера вверх тормашками или не вращается. Я не буду приводить этот код, так как он довольно прост. Далее обработчик мыши:

 

void CheckMouse(void)

{

  GLfloat DeltaMouse;

  POINT pt;

  GetCursorPos(&pt);

 

  MouseX = pt.x;

  MouseY = pt.y;

 

  if(MouseX < CenterX)

  {

    DeltaMouse = GLfloat(CenterX - MouseX);

    Cam.ChangeHeading(-0.2f * DeltaMouse);

  }

  else if(MouseX > CenterX)

  {

    DeltaMouse = GLfloat(MouseX - CenterX);

    Cam.ChangeHeading(0.2f * DeltaMouse);

  }

 

  if(MouseY < CenterY)

  {

    DeltaMouse = GLfloat(CenterY - MouseY);

    Cam.ChangePitch(-0.2f * DeltaMouse);

  }

  else if(MouseY > CenterY)

  {

    DeltaMouse = GLfloat(MouseY - CenterY);

    Cam.ChangePitch(0.2f * DeltaMouse);

  }

 

  MouseX = CenterX;

  MouseY = CenterY;

 

  SetCursorPos(CenterX, CenterY);

}

 

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

И, наконец, осталась одна функция DrawGLScene():

 

int DrawGLScene(GLvoid) // Здесь мы все рисуем

{

  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  // Очистка экрана и буфера глубины

  glLoadIdentity();          // Сброс текущей матрицы вида и модели

 

  Cam.SetPrespective();

 

  // Сделать карту высот большой, так как мы быстро двигаемся.

  glScalef(hMap.m_ScaleValue, hMap.m_ScaleValue * HEIGHT_RATIO, hMap.m_ScaleValue);

  hMap.DrawHeightMap();

 

  CheckKeys();

  CheckMouse();

 

  return TRUE;            // Все отлично

}

 

Вначале мы очищаем экран, сбрасываем матрицы вида и модели, потом позиционируем камеру. Затем масштабируем и отрисовываем карту. Проверяем изменение клавиатуры и мыши. Вот и все.

 

Примечание:

В этом уроке можно было обойтись и без кватернионов!!! Я мог написать этот урок и, не используя кватернионы, а используя glRotatef() для вращения и glGetFloatv() для получения матрицы модели и вида для моего вектора направления. Но почему я тогда заострил ваше внимание на кватернионах? Дело в том, что Вы действительно не нуждаетесь в этих кватернионах для подобного рода задач. Для иллюстрации случая, когда кватернионы не нужны я и написал этот урок. Я думаю, многие думают, что необходима сложная математика, чтобы реализовать движение камерой, как при симуляции полета. OpenGL имеет достаточно информации для реализации подобного рода задач, и причем без затрат процессорного времени. Я видел много примеров кода, в котором используется сложная векторная математика для реализации того, что могут сделать матрицы. Одно время я думал, что кватернионы будут делать тоже самое, если только я их пойму. И после того, как я понял их математику, я понял, что они мне не нужны для этого. Поймите меня правильно, я не говорю, что кватернионы бесполезны. Я более чем уверен, что кватернионы нужны и полезны в некоторых случаях. Я только хотел проиллюстрировать, что их не нужно использовать, для облета вашей сцены. Так как старый добрый код на OpenGL подойдет для этого лучше. Если вы измените содержимое функции SetPrespective в классе glCamera, на код, приведенный ниже, то вы получите тот же самый эффект, как если бы вы использовали в SetPrespective кватернионы. Но вы можете вспомнить о блокировке вращения, о которой я упоминал выше. Уверяю вас, этого не произойдет в этом коде! Так как матрица вида и модели сбрасывается в SetPrespective, т.е. накопления вращений не происходит! Блокировка вращений проявляется тогда, когда комбинируется несколько вращений и не используется загрузка единичной матрицы, всякий раз при вызове SetPrespective.

 

void glCamera::SetPrespective()

{

  GLfloat Matrix[16];

 

  glRotatef(m_HeadingDegrees, 0.0f, 1.0f, 0.0f);

  glRotatef(m_PitchDegrees, 1.0f, 0.0f, 0.0f);

 

  glGetFloatv(GL_MODELVIEW_MATRIX, Matrix);

 

  m_DirectionVector.i = Matrix[8];

  m_DirectionVector.k = Matrix[10];

 

  glLoadIdentity();

 

  glRotatef(m_PitchDegrees, 1.0f, 0.0f, 0.0f);

 

  glGetFloatv(GL_MODELVIEW_MATRIX, Matrix);

  m_DirectionVector.j = Matrix[9];

 

  glRotatef(m_HeadingDegrees, 0.0f, 1.0f, 0.0f);

 

  // Масштабировать направление на скорость.

  m_DirectionVector *= m_ForwardVelocity;

 

  // Сместить позицию на наш вектор

  m_Position.x += m_DirectionVector.i;

  m_Position.y += m_DirectionVector.j;

  m_Position.z += m_DirectionVector.k;

 

  // Переместить на новую координату.

  glTranslatef(-m_Position.x, -m_Position.y, m_Position.z);

}

 

Я надеюсь, что вы сможете это использовать, и этот урок поможет вам при реализации ваших проектов. По благодарите Джефа за его прекрасный сайт, где есть столько полезной и простой информации. Так же я должен поблагодарить DigiBen (http://www.gametutorials.com) за его программу по кватернионам, на основе которой и получился этот урок.

© Vic Hollis

PMG  22 октября 2006 (c)  Сергей Анисимов