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

ArcBall Rotation

 

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

 

Моя реализация класса ArcBall базируется на коде Бреттона Вада, который позаимствовал код Кена Шоемака в одной из книг "Графические драгоценности" (Graphic Gems). Однако я исправил ошибки в коде, и оптимизировал его.

 

Класс ArcBall предназначен для того, чтобы преобразовать координаты курсора мыши в сферические координаты ArcBall, так как, если бы это было непосредственно перед Вами.

 

Чтобы выполнить это, вначале масштабируем, координаты мыши из диапазона [0…ширина], [0...высота] в диапазон [-1...1], [1...-1] (запомните, что мы меняем знак координаты Y, чтобы получить корректный результат в OpenGL). Фактически, это делается так:

 

MousePt.X  =  ((MousePt.X / ((Width  – 1) / 2)) – 1);

MousePt.Y  = -((MousePt.Y / ((Height – 1) / 2)) – 1);

 

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

 

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

 

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

 

Конструктор класса ArcBall:

 

ArcBall_t::ArcBall_t(GLfloat NewWidth, GLfloat NewHeight)

 

Где NewWidth и NewHeight – ширина и высота окна.

 

Когда пользователь щелкает мышью, начальный вектор рассчитывается исходя из точки, где он произошел:

 

void    ArcBall_t::click(const Point2fT* NewPt)

 

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

 

void    ArcBall_t::drag(const Point2fT* NewPt, Quat4fT* NewRot)

 

И если кватернион задан, то он обновляется.

 

Если меняется размер окна, мы просто передаём классу ArcBall следующую информацию:

 

void    ArcBall_t::setBounds(GLfloat NewWidth, GLfloat NewHeight)

 

При использовании этого класса в нашем проекте, нам будет необходимо изменить некоторые переменные:

 

// Завершающая трансформация

Matrix4fT  Transform = {  1.0f,  0.0f,  0.0f,  0.0f,

             0.0f,  1.0f,  0.0f,  0.0f,

             0.0f,  0.0f,  1.0f,  0.0f,

             0.0f,  0.0f,  0.0f,  1.0f };

 

Matrix3fT  LastRot   = {  1.0f,  0.0f,  0.0f,          // Последнее вращение

                          0.0f,  1.0f,  0.0f,

                          0.0f,  0.0f,  1.0f };

 

Matrix3fT  ThisRot   = {  1.0f,  0.0f,  0.0f,          // Это вращение

                          0.0f,  1.0f,  0.0f,

                          0.0f,  0.0f,  1.0f };

 

ArcBallT  ArcBall(640.0f, 480.0f); // экземпляр класса ArcBall

Point2fT  MousePt;                 // Текущие координаты мыши

bool    isClicked  = false;        // Кликнули по мыши?

bool    isRClicked = false;        // Кликнули по правой клавиши мыши?

bool    isDragging = false;        // Потащили мышь?

 

Переменная Transform - это матрица с помощью, которой вращаются все объекты. Переменная LastRot - матрица, в которой сохраняется вращение объекта, когда пользователь отпускает кнопку мышки. Переменная ThisRot - матрица, в которой накапливается вращение объекта во время перетаскивания мышью. Эти переменные вначале инициализируются единичной матрицей.

 

Когда мы кликаем, мы начинаем крутить объекты с начальной позиции. Когда мы перетаскиваем, мы вычисляем угол поворота от начальной точки до точки, в которой находится указатель. Необходимо заметить, что класс ArcBall не вращается. Чтобы получить одинаковое значение в этих переменных (кумулятивное вращение) мы должны сами об этом позаботиться.

 

Для этого и нужны LastRot и ThisRot. LastRot можно представить как переменную, показывающую то, что происходило до этого момента. Тогда ThisRot - переменная показывающая то, что происходит с объектами сейчас. Каждый раз, когда мы начинаем перетаскивать мышь, ThisRot изменяется вместе с координатами курсора. Одновременно с этим изменяется Transform. Как только перетаскивание заканчивается, значение ThisRot передаётся в переменную LastRot.

 

Если мы не будем накапливать значения сами, то модель будет возвращаться к положению до перетаскивания. Например: если мы вращаем модель по оси X на 90 градусов, а затем на 45, мы должны будем увидеть поворот на 135 град, вместо последних 45.

 

Остальным переменным (кроме isDragged) необходимо вовремя передавать правильные значения. Классу ArcBall необходимо сбрасывать значения каждый раз, когда меняется размер окна. MousePt обновляется тогда, когда мышь движется, или когда нажата кнопка. Переменные isClicked/isRClicked отвечают за нажатие левой/правой кнопки мыши, соответственно. Переменная isClicked используется для распознавания нажатия/перетаскивания. Переменную isRClicked  мы будем использовать для возврата в начальное положение всех объектов.

 

Всё выше описанное выглядит примерно так :

 

void ReshapeGL (int width, int height)

{

  . . .

  ArcBall.setBounds((GLfloat)width, (GLfloat)height); // Обновить границы для ArcBall

}

 

// Обслуживание сообщений

LRESULT CALLBACK WindowProc (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

{

  . . .

  // Сообщения от мыши для ArcBall

  case WM_MOUSEMOVE:

    MousePt.s.X = (GLfloat)LOWORD(lParam);

    MousePt.s.Y = (GLfloat)HIWORD(lParam);

    isClicked   = (LOWORD(wParam) & MK_LBUTTON) ? true : false;

    isRClicked  = (LOWORD(wParam) & MK_RBUTTON) ? true : false;

    break;

 

  case WM_LBUTTONUP:   isClicked  = false; break;

  case WM_RBUTTONUP:   isRClicked = false; break;

  case WM_LBUTTONDOWN: isClicked  = true;  break;

  case WM_RBUTTONDOWN: isRClicked = true;  break;

  . . .

}

 

Теперь самое время для того, чтобы вставить код клика. Всё это очевидно, если понять написанное выше.

 

if (isRClicked) // Если правая клавиша мыши нажата, сбросить все вращения

{

  // Сброс вращения

  Matrix3fSetIdentity(&LastRot);

 

  // Сброс вращения

  Matrix3fSetIdentity(&ThisRot);

 

  // Сброс вращения

  Matrix4fSetRotationFromMatrix3f(&Transform, &ThisRot);

}

 

if (!isDragging)             // Нет перетаскивания

{

  if (isClicked)             // Первый клик

  {

    isDragging = true;       // Начать перетаскивание

    LastRot = ThisRot;       // Присвоить последнему статическому вращению

                             // значение последнего динамического вращения

    ArcBall.click(&MousePt); // Обновить начальный вектор и выполнить перетаскивание

  }

}

else

{

  if (isClicked)  // Все еще клик, по этому и перетаскивание

  {

    Quat4fT     ThisQuat;

 

    ArcBall.drag(&MousePt, &ThisQuat); // Обновить окончательный вектор

                                       // и получить вращение как кватернион

    Matrix3fSetRotationFromQuat4f(&ThisRot, &ThisQuat); // Конвертировать кватернион в Matrix3fT

    Matrix3fMulMatrix3f(&ThisRot, &LastRot); // Добавить текущее вращение в последнее вращение

    Matrix4fSetRotationFromMatrix3f(&Transform, &ThisRot); // Вычислить нашу финальную трансформацию

  }

  else // Больше нет перетаскивания

    isDragging = false;

}

 

Это всё что нам необходимо. Осталось только сделать так, чтобы положение моделей постоянно обновлялось. Это достаточно просто:

 

  glPushMatrix();             // Выполнить динамическую трансформацию

  glMultMatrixf(Transform.M); // Применить динамическую трансформацию

 

  glBegin(GL_TRIANGLES);      // Начать рисовать модели

  . . .

  glEnd();                    // Завершить рисование моделей

 

  glPopMatrix();              // Сбросить динамическую трансформацию

 

 

Я сделал пример, демонстрирующий всё вышеописанное. Вы можете использовать не только мой код, но и встраивать туда какие-то свои конструкции. Программы созданные с помощью этой библиотеки могут работать сами по себе и не требуют ничего дополнительного.

 

Теперь, когда вы видите насколько это всё просто, вы можете спокойно добавлять класс ArcBall в свои приложения!

 

© Terence J. Grant

PMG  27 апреля 2004 (c)  snegovick