Как Вы думаете, здорово было бы вращать объект, пользуясь только мышью? Используя функциональность класса 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 в свои приложения!