Перед тем, как перейти к довольно сложной теме, а именно, заданию аффинного
преобразования в матричной форме, рассмотрим следующий вопрос. Как поворачивать
изображение (плоскость) с помощью мыши?
Имеется пара точек M0(x0, y0), M(x, y) – начальное и конечное положения мыши. По
этим точкам нужно определить угол поворота плоскости. Как это сделать. Можно
попытаться экспериментировать с формулами, но вскоре станет понятно, что надо
учитывать не только относительные изменения координат Δx, Δ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’):
Функция от векторов f(a, b) = (xy’ – yx’) будет равна синусу направленного угла от
вектора а к вектору b, умноженному на длины векторов a и b (|a||b|sin(α)).
Прим. Можете проверить, что (f(a, b))2 +
(a • b)2 =
|a||b|.
Учитывая тот факт, что наши вектора единичные (M0’ и M' лежат на единичной сфере) , с помощью скалярного произведения и функции f(a, b) получаем косинус и синус нужного угла
с учетом его направления. Остается подставить полученные значения в матрицу
поворота и требуемый эффект будет получен.
Прим. Есть замечательное тригонометрическое
тождество:
Но в данном случае им воспользоваться не удается
именно из тех соображений, что нужно учитывать знак тригонометрической функции:
Программная реализация
Теория понятна. Теперь перейдем к реализации, которая оказывается
достаточно громоздкой. Понадобится модуль для работы с векторами:
#include "math.h"
typedef double 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;
}
inline void 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;
}
inline vec_float operator ^ (vec t) //
длина результата векторного произведения с учетом направления
{
return
x * t.y - y * t.x;
}
inline
vec_float length() // длина вектора
{
return
sqrt(x * x + y * y);
}
inline vec unit() // нормализация вектора
{
vec_float l
= length();
if
(l == 0.0f) return vec(0.0f, 0.0f);
return
vec(x / l, y / l);
}
inline bool zero() // определяет нулевой ли
вектор
{
return (x == 0.0f) && (y == 0.0f);
}
inline bool equals(vec t) // проверяет вектора на точное
совпадение
{
return (x == t.x) && (y == t.y);
}
};
Теперь создадим класс, который отвечает за поворот:
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()
инициализирует матрицу как единичную
- void InitRotation(int x, int y): при нажатии мыши мы запоминаем текущий вектор, указывающий направление
на точку нажатия. Этот вектор нормализован (unit()).
- void Rotate(int x, int y): при движении получаем вектор, указывающий на текущее положение мыши.
С помощью векторных операций получаем тригонометрические функции угла
между этим вектором и вектором, отвечающим за предыдущее положение мыши.
Умножаем текущую матрицу 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:
ApplyMatrixtoPoint(Ball->CurrentMatrix,
triangle[i]);
И наконец, добавляется обработчик мыши в основной файл 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).
Скачать исходный текст демонстрационной программы