Компьютерная графика c примерами на С++ Поворот плоскости с помощью мыши
Перед тем, как перейти к довольно сложной теме, а именно, заданию аффинного
преобразования в матричной форме рассмотрим следующий вопрос. Как поворачивать
изображение с помощью мыши?
Имеется пара точек M0(x0, y0), M(x, y) – начальное и конечное положения мыши. По
этим точкам нужно определить угол поворота плоскости. Как это сделать? Можно
попытаться экспериментировать с формулами, но вскоре станет понятно, что надо
учитывать не только относительные изменения координат delta_x, delta_y, но и их абсолютные значения. Так,
например, на приведенном ниже изображении изменения по xдля передвижений мыши M0M и M0’M’ совпадают, но интуитивно понятно, что
одно из этих передвижений вращает треугольник по часовой стрелке, а другое -
против.
Не буду мучить вас попытками угадать формулу, а лучше покажу, как получить
конечный результат. Суть приема заключается в том, что строится воображаемая единичная
окружность с центром в начале координат и на ней отмеряются начальный и конечный
углы. Потом производится поворот на разницу углов.
В данном случае, чтобы получить точки M0’ и M’ надо просто нормализовать векторы OM0 и OM. Теперь повернем плоскость, чтобы векторOM0 перешелв OM. Оказывается это не так просто, как может
показаться на первый взгляд.
Угол между векторами
Немного отвлечемся от настоящей темы и посмотрим, как подсчитать угол между
векторами в декартовой системе координат. Первое, что приходит в голову, это
воспользоваться скалярным произведением.
Проблема заключается в том, что для скалярного произведения вектора OM'' и OM' будут неразличимы. Можно получить косинус угла
между ними. Но если вращать плоскость от OM к OM', то вращение идет по часовой стрелке. В случаеOM'' - против часовой. Т.о. образом кроме значения
угла необходимо знать его знак. Тут может помочь “векторное
произведение”.
Прим. Т.к. мы работаем в двухмерном
пространстве, то строго говорить о векторном произведении нельзя. Но можно
использовать тот результат, что эта операция различает порядок векторов.
Рассмотрим трехмерное пространство, причем наше двухмерное сечение это плоскость
z =
0. Это означает, что все
вектора в этой плоскости имеют третью координату равной 0. Векторно перемножим
вектора OM(x, y) и OM'(x’, y’):
[
x ][ x’ ][ ijk
] [
y ]x[ y’ ]=[ xy0 ]=(x *
y’ – y * x’)k [
0 ][ 0 ][
x’y’0 ]
Функция от векторовf(a, b) = (x * y’ – y * x’) будет равна синусу направленного угла от
вектора ак вектору b, умноженному на длины векторовa
и b (|a||b|sin(alpha)).
Прим. Можете проверить, что (f(a, b))2 +
(ab)2 =
|a||b|.
Учитывая тот факт, что наши вектора единичные (M0’ иM' лежат на единичной сфере) , с помощью скалярного произведения и функции f(a, b) получаем косинус и синус нужного угла
с учетом его направления. Остается подставить полученные значения в матрицу
поворота и требуемый эффект будет получен.
Прим. Есть замечательное тригонометрическое
тождество: sin2(alpha) + cos2(alpha) = 1. Но в данном случае им воспользоваться не удается
именно из тех соображений, что нужно учитывать знак тригонометрической функции
(cos(alpha) = ±sqrt(1
- sin2(alpha))).
Теория понятна. Теперь перейдем к реализации, которая оказывается
достаточно громоздкой. Понадобится модуль для работы с векторами: #include"math.h" typedefdouble vec_float; class vec { public: vec_float x, y; vec() {} vec(vec_float xx, vec_float yy) { x = xx; y = yy; } vec(const
vec& vector){ x = vector.x; y = vector.y; } inlinevoid set(vec_float xx, vec_float yy) { x = xx; y = yy; } inline vec operator + (vec t) //
сложение { return
vec(x + t.x, y + t.y); } inline vec operator - (vec t) //
вычитание { return
vec(x - t.x, y - t.y); } inline vec operator * (vec_float t) //
произведение на число { return
vec(x * t, y * t); } inline
vec_float operator * (vec t) // скалярное произведение { return
x * t.x + y * t.y; } inlinevec_floatoperator ^ (vect) //
длина результата векторного произведения с учетом направления { return
x * t.y - y * t.x; } inline
vec_float length() // длина вектора { return
sqrt(x * x + y * y); } inlinevecunit() // нормализация вектора { vec_float l
= length(); if
(l == 0.0) return vec(0.0, 0.0); return
vec(x / l, y / l); } inlineboolzero() // определяет нулевой ли
вектор { returnx == 0 && y == 0; } inlineboolequals(vect) // проверяет вектора на точное
совпадение { return
x == t.x && y == t.y; } }; Думаю код не нуждается в комментариях. Если понадобятся дополнительные
разъяснения о векторных операциях, пишите на cgraphics@mail.ru, я сделаю спецвыпуск. Теперь создадим класс, который отвечает за поворот: Rotation::Rotation(){ CurrentMatrix[0] = 1; CurrentMatrix[1] = 0; CurrentMatrix[2] = 0; CurrentMatrix[3] = 1; } void Rotation::InitRotation(int x, int y){ old_mouse.set(x, y); old_mouse = old_mouse.unit(); } void
Rotation::Rotate(int x, int y){ vec new_mouse(x, y); vec_float sina, cosa; new_mouse = new_mouse.unit(); sina = new_mouse ^ old_mouse; cosa = new_mouse * old_mouse; Matrix Rot; SetRotationMatrixbySinCos(sina, cosa,
Rot); MultiplyMatrices(CurrentMatrix,
CurrentMatrix, Rot); old_mouse = new_mouse; } Суть этого класса в том, что он запоминает действия мыши. В каждый
момент времени есть матрица, которая отвечает за текущее состояние системы
(поворот).
конструктор Rotation()
инициализирует матрицу как единичную
voidInitRotation(intx, inty): при нажатии мыши мы запоминаем текущий вектор, указывающий направление
на точку нажатия. Этот вектор нормализован (unit()).
voidRotate(intx, inty): при движении получаем вектор, указывающий на текущее положение мыши.
С помощью векторных операций получаем тригонометрические функции угла
между этим вектором и вектором, отвечающим за предыдущее положение мыши.
Умножаем текущую матрицуCurrentMatrix на матрицу поворота, определяемую
полученными синусом и косинусом.
Заметим, что в модуль matrix.cppдобавляется
новая функция void
SetRotationMatrixbySinCos(double sinalpha, double cosalpha, Matrix &matrix){ matrix[0] = cosalpha; matrix[1] = -sinalpha; matrix[2] = sinalpha; matrix[3] = cosalpha; } Посмотрим на изменения вфайле draw.cpp:
появляется новая переменная, отвечающая за
классRotation:
Rotation *Ball;
С помощью функции SetBall устанавливается текущий обработчик вращений:
void SetBall(Rotation *_Ball) { Ball = _Ball; }
Ко всем точкам
применяется преобразование, но теперь оно берется из класса Ball:
И наконец, добавляется обработчик мыши в основной файл main.cpp: case
WM_LBUTTONDOWN: Ball->InitRotation(LOWORD(lParam)
- Rect.right / 2, HIWORD(lParam) - Rect.bottom / 2); InvalidateRect(hWnd, NULL, FALSE); break; case
WM_MOUSEMOVE: if
(UINT(wParam) & MK_LBUTTON) { Ball->Rotate(LOWORD(lParam)
- Rect.right / 2, HIWORD(lParam) - Rect.bottom / 2); InvalidateRect(hWnd, NULL,
FALSE); } break; Функции, вызываемые в обработчике, были описаны ранее. Прим. Хочу обратить ваше внимание, что передаются не координаты
текущего положения мыши, а вектор-направление. В данном случае он отсчитывается
от центра экрана. Но это не совсем корректно. Ведь центр выбранной
системы координат может быть вовсе не в центре экрана. Строго, надо применить
функцию, которая переводит логические координаты обратно в оконные, к точке (0, 0). Прим. Если растянуть окно так, чтобы его размеры по горизонтали и
вертикали различались, то при повороте видно, что пропорции треугольника не
сохраняются. Это связано с различием масштабов по осям. В OpenGL мы встретимся
с этим, когда неожиданно получим вместо сферы эллипсоид.