javascript - 有没有办法从 4x4 矩阵计算 X 和 Y 轴上的 3D 旋转

标签 javascript math matrix rotation euler-angles

首先,我根本不是数学专家。请容忍我的数学错误并在必要时纠正我,我很想学习。

我有一个立方体,它使用带有变换的 css 动画旋转:matrix3d(4x4)。我还可以手动旋转立方体,将用户操作转换为相同的 matrix3d 转换。

当用户停止从用户离开它的位置开始交互时,我想要的是一个带有 css 的旋转立方体。这是我通过获取多维数据集的变换 matrix3d 值并使用乘法动态设置 css 的关键帧而成功完成的事情。

但是,当用户开始与立方体交互时,立方体会跳到其最后一个已知的手动旋转点并从那里继续,因为我无法弄清楚如何从 4x4 矩阵中获得 X 和 Y 轴上的旋转。

我目前正在使用以下库,Rematrix ,这有助于我从手动旋转到 css 旋转,如上所述。

我一直在研究有关欧拉的文章,以及如何从欧拉到矩阵,反之亦然,但正如我之前提到的,我认为这就是我缺乏数学知识阻碍我前进的地方。我似乎无法弄清楚。

作为引用,这里有一些我读过的文章来尝试解决我的问题。

  • https://medium.com/@behreajj/3d-rotations-in-processing-vectors-matrices-quaternions-10e2fed5f0a3
  • http://www.gregslabaugh.net/publications/euler.pdf
  • https://www.learnopencv.com/rotation-matrix-to-euler-angles/
  • https://css-tricks.com/get-value-of-css-rotation-through-javascript/

  • 最后一个来源对我来说最有意义,但如果我是正确的,在这种情况下没有用,因为它是关于 2D 转换,而不是 3D。

    我通过以下方式获得当前的 matrix3d:
    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])};
          }
        }
      `;
    }
    

    请提供答案或评论任何有用的信息。尽管它会很受欢迎,但我不希望您提供完整的代码示例。非常感谢任何形式的任何有用信息。

    最佳答案

    初读:

  • Understanding 4x4 homogenous transform matrices

  • 因为我从那里使用术语。

    好吧,我懒得将整个环境等同于我的环境,但基于此:
  • Computing Euler angles from a rotation matrix

  • 得到的 m 的 3D 旋转子矩阵对于任何轮换订单将始终具有这些热量:
    (+/-)sin(a)
    (+/-)sin(b)cos(a)
    (+/-)cos(b)cos(a)
    (+/-)sin(c)cos(a)
    (+/-)cos(c)cos(a)
    

    只有它们的符号和位置会随着转换顺序和约定而改变。因此,要识别它们,请执行以下操作:
  • 让我们先设置一些非平凡的欧拉 Angular

    他们的|sin| , |cos|值必须不同,因此这 6 个值都不相同,否则这将不起作用!!!

    我选择了这些:
    ex = 10 [deg]
    ey = 20 [deg]
    ez = 30 [deg]
    
  • 计算旋转矩阵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和产地O用矩阵的线表示,矩阵是直接的。
  • 识别 (+/-)sin(a)
    a可以是任何欧拉 Angular ,所以打印 sin他们所有人中:

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

    现在见 m[8] = sin(ey)所以我们找到了我们的热水器......现在我们知道了:
    ey = a = asin(m[8]);
    
  • 识别 (+/-)???(?)*cos(a)热敏

    只需为未使用的 Angular 打印 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]
    

    从中我们可以计算 Angular ......
    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 中的欧拉 Angular 命令。它呈现 3 个坐标系轴。左边是m用作模型 View 矩阵,中间是m的基向量使用恒等模型 View ,右侧是由计算的欧拉 Angular 构造的模型 View ...

    所有 3 应该匹配。如果左侧和中间不匹配,那么您将获得不同的矩阵或布局约定。

    这里预览(10,20,30) [deg]测试用例:

    preview

    即使经过多次旋转(箭头键)它也匹配......
    gl_simple.h可以在这里找到:
  • complete GL+GLSL+VAO/VBO C++ example

  • 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 编写自己的优化版本结果为您的环境。

    关于javascript - 有没有办法从 4x4 矩阵计算 X 和 Y 轴上的 3D 旋转,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56900173/

    相关文章:

    javascript - 如何在reactJs中显示实时摄像头

    javascript - 在angularjs中为每个 View 创建一个 Controller

    java - 为什么将 float 除以整数返回 0.0?

    javascript - 如何使用Math.random()将三个图片分配到三个img元素中?

    c - 定义和打印矩阵的所有元素的功能

    Java循环超过数组的一半

    c - 将2D数组C代码转换为malloc

    javascript - Redux saga debounce 而不仅仅是延迟/取消

    javascript - 在asp.net中控件的可见性?使用JavaScript?

    java - 我应该依靠 Android 放弃屏幕外绘制吗?