Есть ли способ рассчитать трехмерное вращение по осям X и Y из матрицы 4х4 - PullRequest
2 голосов
/ 05 июля 2019

Прежде всего, я вообще не математический специалист. Пожалуйста, будьте терпимы к моим математическим ошибкам и исправьте меня в случае необходимости, я бы с удовольствием учился.

У меня есть куб, который вращается с использованием CSS анимации с transform: matrix3d ​​(4x4). Я также могу вручную вращать куб, преобразовывая действия пользователя в те же преобразования matrix3d.

Что мне нужно, так это вращающийся куб с css, когда пользователь перестает взаимодействовать, начиная с того места, где он его оставил. Это то, что я успешно делаю, получая значение transform matrix3d ​​куба и используя умножение для динамической установки ключевых кадров css.

Однако, когда пользователь начинает взаимодействовать с кубом, куб переходит к своей последней известной точке вращения вручную и продолжает оттуда, поскольку я не могу понять, как получить вращение по осям X и Y из матрицы 4x4.

В настоящее время я использую следующую библиотеку Rematrix , которая помогает мне перейти от ручного вращения к вращению CSS, как описано выше.

Я изучал статьи об Эйлере и о том, как перейти от Эйлера к матрицам и наоборот, но, как я уже упоминал ранее, это то, что мое отсутствие математических знаний сдерживает меня, я думаю. Я не могу понять это.

В качестве ссылки, вот несколько статей, которые я прочитал, чтобы попытаться решить мою проблему.

Последний источник имеет для меня наибольшее значение, но, если я прав, в данном случае бесполезен, поскольку речь идет о 2D-преобразованиях, а не 3D.

Я получаю текущую матрицу 3d следующим образом:

const style = getComputedStyle(this.element).transform
const matrix = Rematrix.parse(style)

Для ручного вращения я использую матричное умножение, основанное на позициях мыши пользователя (positionY, positionX).

const r1 = Rematrix.rotateX(this.positionY)
const r2 = Rematrix.rotateY(this.positionX)

const transform = [r1, r2].reduce(Rematrix.multiply)

this.element.style[userPrefix.js + 'Transform'] = Rematrix.toString(transform)

Переходя от ручного к вращению css, я использую следующую функцию:

const setCssAnimationKeyframes = (lastTransform, animationData) => {
  const rotationIncrement = 90

  let matrixes = []

  for (let i = 0; i < 5; i++) {
    const rX = Rematrix.rotateX(rotationIncrement * i)
    const rY = Rematrix.rotateY(rotationIncrement * i)

    const matrix = [lastTransform, rX, rY].reduce(Rematrix.multiply);

    matrixes.push(matrix)
  }

  animationData.innerHTML = `
    @keyframes rotateCube {
      0% {
        transform: ${Rematrix.toString(matrixes[0])};
      }
      25% {
        transform: ${Rematrix.toString(matrixes[1])};
      }
      50% {
        transform: ${Rematrix.toString(matrixes[2])};
      }
      75% {
        transform: ${Rematrix.toString(matrixes[3])}};
      }
      100% {
        transform: ${Rematrix.toString(matrixes[4])};
      }
    }
  `;
}

Пожалуйста, предоставьте ответы или комментарии с любой полезной информацией. Хотя это очень приветствуется, я не ожидаю, что вы предоставите полностью рабочий пример кода. Мы ценим любую полезную информацию в любой форме.

1 Ответ

1 голос
/ 09 июля 2019

Первое чтение:

как я использую терминологию оттуда.

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

Полученное 3Dподматрица вращения m для любого порядка вращения всегда будет иметь следующие термины:

(+/-)sin(a)
(+/-)sin(b)cos(a)
(+/-)cos(b)cos(a)
(+/-)sin(c)cos(a)
(+/-)cos(c)cos(a)

Только их знак и местоположение будут меняться в зависимости от порядка преобразования и соглашений.Чтобы их идентифицировать, сделайте следующее:

  1. позвольте сначала установить несколько нетривиальных углов Эйлера

    их | sin |, | cos |значения должны быть разными, поэтому ни одно из 6 значений не будет одинаковым, иначе это не сработает !!!

    Я выбрал следующее:

    ex = 10 [deg]
    ey = 20 [deg]
    ez = 30 [deg]
    
  2. вычислите матрицу вращения m

    , поэтому примените 3 поворота Эйлера к единичной матрице в их порядке.В моей установке результирующая матрица выглядит следующим образом:

    double m[16] = 
     { 
      0.813797652721405, 0.543838143348694,-0.204874128103256, 0, // Xx,Xy,Xz,0.0
     -0.469846308231354, 0.823172926902771, 0.318795770406723, 0, // Yx,Yy,Yz,0.0
      0.342020153999329,-0.163175910711288, 0.925416529178619, 0, // Zx,Zy,Zz,0.0
      0                , 0                , 0                , 1  // Ox,Oy,Oz,1.0
     };
    

    обратите внимание, что я использую соглашения OpenGL, базисные векторы X,Y,Z и origin O представлены строками матрицы, а матрица является прямой.

  3. определить (+/-)sin(a) термо

    a может быть любым из углов эйлера, поэтому выведите sin их всех:

    sin(ex) = 0.17364817766693034885171662676931
    sin(ey) = 0.34202014332566873304409961468226
    sin(ez) = 0.5
    

    теперь посмотрим m[8] = sin(ey), поэтому мы нашли наш терм ... Теперь мы знаем:

    ey = a = asin(m[8]);
    
  4. идентифицируем (+/-)???(?)*cos(a) термины

    просто выведите cos (?) * Cos (ey) для еще не использованных углов.поэтому, если ey - это 20 градусов, я печатаю 10 и 30 градусов ...

    sin(10 deg)*cos(20 deg) = 0.16317591116653482557414168661534
    cos(10 deg)*cos(20 deg) = 0.92541657839832335306523309767123
    sin(30 deg)*cos(20 deg) = 0.46984631039295419202705463866237
    cos(30 deg)*cos(20 deg) = 0.81379768134937369284469321724839
    

    , когда мы снова смотрим на m, мы можем сопоставить:

    sin(ex)*cos(ey) = 0.16317591116653482557414168661534 = -m[9]
    cos(ex)*cos(ey) = 0.92541657839832335306523309767123 = +m[10]
    sin(ez)*cos(ey) = 0.46984631039295419202705463866237 = -m[4]
    cos(ez)*cos(ey) = 0.81379768134937369284469321724839 = +m[0]
    

    из этого мы можем вычислить углы ...

    sin(ex)*cos(ey) = -m[ 9]
    cos(ex)*cos(ey) = +m[10]
    sin(ez)*cos(ey) = -m[ 4]
    cos(ez)*cos(ey) = +m[ 0]
    ------------------------
    sin(ex) = -m[ 9]/cos(ey)
    cos(ex) = +m[10]/cos(ey)
    sin(ez) = -m[ 4]/cos(ey)
    cos(ez) = +m[ 0]/cos(ey)
    

    так, наконец:

    ---------------------------------------------
    ey = asin(m[8]);
    ex = atan2( -m[ 9]/cos(ey) , +m[10]/cos(ey) )
    ez = atan2( -m[ 4]/cos(ey) , +m[ 0]/cos(ey) )
    ---------------------------------------------
    

И это все.Если вы получили другой порядок расположения / соглашений / преобразования, этот подход все еще должен работать ... Изменяются только индексы и знаки.Вот небольшой пример C ++ / VCL OpenGL , который я проверяю на (X,Y,Z порядке):

//---------------------------------------------------------------------------
#include <vcl.h>
#include <math.h>
#pragma hdrstop
#include "Unit1.h"
#include "gl_simple.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
bool _redraw=true;                  // need repaint?

//---------------------------------------------------------------------------
double m[16]=               // uniform 4x4 matrix
    {
    1.0,0.0,0.0,0.0,        // Xx,Xy,Xz,0.0
    0.0,1.0,0.0,0.0,        // Yx,Yy,Yz,0.0
    0.0,0.0,1.0,0.0,        // Zx,Zy,Zz,0.0
    0.0,0.0,0.0,1.0         // Ox,Oy,Oz,1.0
    };
double e[3]={0.0,0.0,0.0};  // euler angles x,y,z order
//---------------------------------------------------------------------------
const double deg=M_PI/180.0;
const double rad=180.0/M_PI;
void matrix2euler(double *e,double *m)
    {
    double c;
    e[1]=asin(+m[ 8]);
    c=cos(e[1]); if (fabs(c>1e-20)) c=1.0/c; else c=0.0;
    e[0]=atan2(-m[ 9]*c,m[10]*c);
    e[2]=atan2(-m[ 4]*c,m[ 0]*c);
    }
//---------------------------------------------------------------------------
void gl_draw()
    {
    _redraw=false;
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    glMatrixMode(GL_PROJECTION);
//  glLoadIdentity();
    glMatrixMode(GL_TEXTURE);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glTranslated(0.0,0.0,-10.0);    // some distance from camera ...

    glDisable(GL_DEPTH_TEST);
    glDisable(GL_TEXTURE_2D);

    int i;
    // draw source matrix:
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glTranslated(-1.0,0.0,0.0); // source matrix on the left
    glMultMatrixd(m);
    glBegin(GL_LINES);
    glColor3f(1.0,0.0,0.0); glVertex3d(0.0,0.0,0.0); glVertex3d(1.0,0.0,0.0);
    glColor3f(0.0,1.0,0.0); glVertex3d(0.0,0.0,0.0); glVertex3d(0.0,1.0,0.0);
    glColor3f(0.0,0.0,1.0); glVertex3d(0.0,0.0,0.0); glVertex3d(0.0,0.0,1.0);
    glEnd();
    glPopMatrix();

    // draw source matrix:
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glTranslated(m[12],m[13],m[14]);    // source matrix in the middle
    glBegin(GL_LINES);
    glColor3f(1.0,0.0,0.0); glVertex3d(0.0,0.0,0.0); glVertex3dv(m+0);
    glColor3f(0.0,1.0,0.0); glVertex3d(0.0,0.0,0.0); glVertex3dv(m+4);
    glColor3f(0.0,0.0,1.0); glVertex3d(0.0,0.0,0.0); glVertex3dv(m+8);
    glEnd();
    glPopMatrix();

    // draw euler angles
    matrix2euler(e,m);
    glMatrixMode(GL_MODELVIEW);
    glPushMatrix();
    glTranslated(+1.0,0.0,0.0); // euler angles on the right
    glRotated(e[0]*rad,1.0,0.0,0.0);
    glRotated(e[1]*rad,0.0,1.0,0.0);
    glRotated(e[2]*rad,0.0,0.0,1.0);
    glBegin(GL_LINES);
    glColor3f(1.0,0.0,0.0); glVertex3d(0.0,0.0,0.0); glVertex3d(1.0,0.0,0.0);
    glColor3f(0.0,1.0,0.0); glVertex3d(0.0,0.0,0.0); glVertex3d(0.0,1.0,0.0);
    glColor3f(0.0,0.0,1.0); glVertex3d(0.0,0.0,0.0); glVertex3d(0.0,0.0,1.0);
    glEnd();
    glPopMatrix();

//  glFlush();
    glFinish();
    SwapBuffers(hdc);
    }
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner):TForm(Owner)
    {
    gl_init(Handle);
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glRotated(10.0,1.0,0.0,0.0);
    glRotated(20.0,0.0,1.0,0.0);
    glRotated(30.0,0.0,0.0,1.0);
    glGetDoublev(GL_MODELVIEW_MATRIX,m);
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormDestroy(TObject *Sender)
    {
    gl_exit();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormPaint(TObject *Sender)
    {
    gl_draw();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::Timer1Timer(TObject *Sender)
    {
    if (_redraw) gl_draw();
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormResize(TObject *Sender)
    {
    gl_resize(ClientWidth,ClientHeight);
    _redraw=true;
    }
//---------------------------------------------------------------------------
void __fastcall TForm1::FormKeyDown(TObject *Sender, WORD &Key, TShiftState Shift)
    {
//  Caption=Key;
    const double da=5.0;
    if (Key==37){ _redraw=true; glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadMatrixd(m); glRotated(+da,0.0,1.0,0.0); glGetDoublev(GL_MODELVIEW_MATRIX,m); glPopMatrix(); }
    if (Key==39){ _redraw=true; glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadMatrixd(m); glRotated(-da,0.0,1.0,0.0); glGetDoublev(GL_MODELVIEW_MATRIX,m); glPopMatrix(); }
    if (Key==38){ _redraw=true; glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadMatrixd(m); glRotated(+da,1.0,0.0,0.0); glGetDoublev(GL_MODELVIEW_MATRIX,m); glPopMatrix(); }
    if (Key==40){ _redraw=true; glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadMatrixd(m); glRotated(-da,1.0,0.0,0.0); glGetDoublev(GL_MODELVIEW_MATRIX,m); glPopMatrix(); }
    }
//---------------------------------------------------------------------------

Единственное, что важно из этого - это matrix2euler матрица преобразования функций m вуглы Эйлера в порядке x,y,z.Это делает 3 оси системы координат.Слева m используется в качестве матрицы вида модели, в середине - базисные векторы m с использованием идентичного представления модели, а справа - вид модели, построенный по вычисленным углам Эйлера ...

Все 3 должны совпадать,Если левые и средние значения не совпадают, вы получаете другое соглашение о матрице или макете.

Вот предварительный просмотр для (10,20,30) [deg] теста:

preview

Соответствует даже после многих поворотов (клавиши со стрелками) ...

Здесь можно найти gl_simple.h:

PS. В зависимости от платформы / среды для вычислений может потребоваться некоторая обработка краевого случая, например округленная величина для asin больше, чем 1, деление на ноль и т. Д. Также atan2 имеет свои причуды ...

[Edit1] Вот последний пример C ++, который делает все это автоматически:

//---------------------------------------------------------------------------
enum _euler_cfg_enum
    {
    _euler_cfg_a=0,
    _euler_cfg_b,
    _euler_cfg_c,
    _euler_cfg__sina,
    _euler_cfg_ssina,
    _euler_cfg__sinb_cosa,
    _euler_cfg_ssinb_cosa,
    _euler_cfg__cosb_cosa,
    _euler_cfg_scosb_cosa,
    _euler_cfg__sinc_cosa,
    _euler_cfg_ssinc_cosa,
    _euler_cfg__cosc_cosa,
    _euler_cfg_scosc_cosa,
    _euler_cfgs
    };
//---------------------------------------------------------------------------
void matrix2euler_init(double *e,double *m,int *cfg)    // cross match euler angles e[3] and resulting m[16] transform matrix into cfg[_euler_cfgs]
    {
    int i,j;
    double a,tab[4];
    const double _zero=1e-6;
    for (i=0;i<_euler_cfgs;i++) cfg[i]=-1;      // clear cfg
    // find (+/-)sin(a)
    for (i=0;i<3;i++)                           // test all angles in e[]
        {
        a=sin(e[i]);
        for (j=0;j<16;j++)                      // test all elements in m[]
         if (fabs(fabs(a)-fabs(m[j]))<=_zero)   // find match in |m[j]| = |sin(e[i])|
            {                                   // store configuration
            cfg[_euler_cfg_a]=i;            
            cfg[_euler_cfg__sina]=j;
            cfg[_euler_cfg_ssina]=(a*m[j]<0.0);
            j=-1; break;
            }
        if (j<0){ i=-1; break; }                // stop on match found
        }
    if (i>=0){ cfg[0]=-1; return;   }           // no match !!!
    // find (+/-)???(?)*cos(a)
    a=cos(e[cfg[_euler_cfg_a]]);
    i=0; if (i==cfg[_euler_cfg_a]) i++; tab[0]=sin(e[i])*a; tab[1]=cos(e[i])*a; cfg[_euler_cfg_b]=i;
    i++; if (i==cfg[_euler_cfg_a]) i++; tab[2]=sin(e[i])*a; tab[3]=cos(e[i])*a; cfg[_euler_cfg_c]=i;

    for (i=0;i<4;i++)
        {
        a=tab[i];
        for (j=0;j<16;j++)                      // test all elements in m[]
         if (fabs(fabs(a)-fabs(m[j]))<=_zero)   // find match in |m[j]| = |tab[i]|
            {                                   // store configuration
            cfg[_euler_cfg__sinb_cosa+i+i]=j;
            cfg[_euler_cfg_ssinb_cosa+i+i]=(a*m[j]<0.0);
            j=-1; break;
            }
        if (j>=0){ cfg[0]=-1; return;   }       // no match !!!
        }
    }
//---------------------------------------------------------------------------
void matrix2euler(double *e,double *m,int *cfg) // compute euler angles e[3] from transform matrix m[16] using confing cfg[_euler_cfgs]
    {
    double c;
    //-----angle------         --------------sign--------------     ----------index----------
    e[cfg[_euler_cfg_a]]=asin ((cfg[_euler_cfg_ssina]?-1.0:+1.0) *m[cfg[_euler_cfg__sina     ]]);
    c=cos(e[cfg[_euler_cfg_a]]); if (fabs(c>1e-20)) c=1.0/c; else c=0.0;
    e[cfg[_euler_cfg_b]]=atan2((cfg[_euler_cfg_ssinb_cosa]?-c:+c)*m[cfg[_euler_cfg__sinb_cosa]],
                               (cfg[_euler_cfg_scosb_cosa]?-c:+c)*m[cfg[_euler_cfg__cosb_cosa]]);
    e[cfg[_euler_cfg_c]]=atan2((cfg[_euler_cfg_ssinc_cosa]?-c:+c)*m[cfg[_euler_cfg__sinc_cosa]],
                               (cfg[_euler_cfg_scosc_cosa]?-c:+c)*m[cfg[_euler_cfg__cosc_cosa]]);
    }
//---------------------------------------------------------------------------

Использование:

const double deg=M_PI/180.0;
const double rad=180.0/M_PI;
// variables
double e[3],m[16];
int euler_cfg[_euler_cfgs];
// init angles
e[0]=10.0*deg;
e[1]=20.0*deg;
e[2]=30.0*deg;
// compute coresponding rotation matrix
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glRotated(e[0]*rad,1.0,0.0,0.0);
glRotated(e[1]*rad,0.0,1.0,0.0);
glRotated(e[2]*rad,0.0,0.0,1.0);
glGetDoublev(GL_MODELVIEW_MATRIX,m);
// cross match e,m -> euler_cfg
matrix2euler_init(e,m,euler_cfg);

// now we can use
matrix2euler(e,m,euler_cfg);

Это работает для любого порядка преобразования и / или соглашения / макета.init вызывается только один раз, и затем вы можете использовать преобразование для любой матрицы преобразования ... Вы также можете написать свою собственную оптимизированную версию на основе результатов euler_cfg для вашей среды.

...