теория, алгоритмы, примеры на С++ и OpenGL  

Мы vkontakte.ru


Rambler's Top100 Rambler's Top100
Каталог@Mail.ru - каталог ресурсов интернет

Друзья

Словарь синонимов русского языка

Поворот плоскости с помощью мыши

Перед тем, как перейти к довольно сложной теме, а именно, заданию аффинного преобразования в матричной форме, рассмотрим следующий вопрос. Как поворачивать изображение (плоскость) с помощью мыши?

Имеется пара точек 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’):

\begin{pmatrix}x \\ y \\ 0\end{pmatrix} X\begin{pmatrix}x^{'} \\ y^{'} \\ 0\end{pmatrix} = \begin{pmatrix}\vec{i} & \vec{j} & \vec{k} \\x & y & 0 \\x^{'} & y^{'} & 0 \\\end{pmatrix} = (xy^{'}-yx^{'})\vec{k}

Функция от векторов f(a, b) = (xy’ – yx) будет равна синусу направленного угла от вектора а к вектору b, умноженному на длины векторов a и b (|a||b|sin(α)).

Прим. Можете проверить, что (f(a, b))2 + (ab)2 = |a||b|.

Учитывая тот факт, что наши вектора единичные (M0’ и M' лежат на единичной сфере) , с помощью скалярного произведения и функции f(a, b) получаем косинус и синус нужного угла с учетом его направления. Остается подставить полученные значения в матрицу поворота и требуемый эффект будет получен.

Прим. Есть замечательное тригонометрическое тождество:

\sin^{2}{\alpha} + \cos^{2}{\alpha} = 1

Но в данном случае им воспользоваться не удается именно из тех соображений, что нужно учитывать знак тригонометрической функции:

\cos{\alpha} = \pm\sqrt{1 - \sin^{2}{\alpha}}

Программная реализация

Теория понятна. Теперь перейдем к реализации, которая оказывается достаточно громоздкой. Понадобится модуль для работы с векторами:

#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).  

Скачать исходный текст демонстрационной программы