Отправляет email-рассылки с помощью сервиса Sendsay
  Все выпуски  

Компьютерная графика 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’ ]     [ i   j   k ]
[ y ]  x  [ y’ ]  =  [ x   y   0 ]  =  (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 + (a b)2 = |a||b|.

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

Прим. Есть замечательное тригонометрическое тождество: sin2(alpha) + cos2(alpha) = 1. Но в данном случае им воспользоваться не удается именно из тех соображений, что нужно учитывать знак тригонометрической функции (cos(alpha) = ±sqrt(1 - sin2(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.0) return vec(0.0, 0.0);
            return vec(x / l, y / l);
      }
    inline bool zero() // определяет нулевой ли вектор
    {
            return x == 0 && y == 0;
      }
    inline bool equals(vec t) // проверяет вектора на точное совпадение
    {
            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() инициализирует матрицу как единичную
  • 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).
 
Прим. Если растянуть окно так, чтобы его размеры по горизонтали и вертикали различались, то при повороте видно, что пропорции треугольника не сохраняются. Это связано с различием масштабов по осям. В OpenGL мы встретимся с этим, когда неожиданно получим вместо сферы эллипсоид.

Можно найти этот выпуск и исходные коды на сайте: Поворот плоскости с помощью мыши

Задавайте свои вопросы, я буду стараться на них отвечать. Моя электронная почта: cgraphics@mail.ru

Ведущий рассылки, Денис


В избранное