TOTAL:1989, TODAY:102

四元数(クォータニオン)でトーラスを回転

なかなかGLSLのサンプルに到達できないのですが、GLSLの前に、モデルをグリグリ動かすことはやっておいた方がいいと思い、四元数(クォータニオン)でモデルを回転するサンプルを作成しました。クォータニオンについては、ネット上にいろいろ解説がでており、私はそれよりもきちんと説明できる自信はないのですが、簡単に紹介します。

クォータニオン

クォータニオンは、一つの実数部と三つの虚数部(みたいなもの)i, j, kから成る数で、次のように表記します。

ここで、i2 = j2 = k2 = ijk = -1 が成り立ちます。3次元の座標(x, y, z)は、クォータニオンを使用して次のように表現します。

また、任意軸 (ax, ay, az) 回りのθ回転は、次のように表すことができます。

この時、3次元座標pの任意軸回りの回転qは、次のような計算が成り立ちます。

ここで、q(___)は、qの複素共役で、qの虚数部の符号を反転したものです。ややこしいことを書きましたが、結局、「任意軸回りの回転はクォータニオンqで表現できる」ということになります。そして、このような任意軸回りの回転を複数回施した変換は、「クォータニオンの積」と同じになります。即ち、3次元のすべての回転はクォータニオンで表現できるということになります。3次元の回転は、x軸回転, y軸回転、z軸回転の3種類のパラメータで表すこともできます。この場合、いわゆるジンバルロック(モデルを包む球があるとして、北極や南極に相当する部分でモデルがクルリンと回ってしまう現象)が発生します。一方、クォータニオンを使用すれば、それを防ぐことができます。

クォータニオン関数

クォータニオンをOpenGLの座標変換として使用するには、クォータニオンを行列に変換する必要があります。これは、プログラムで次のようになります。これは、上で示した数式 p'=qpq(___) から導くことができます。

/* クォータニオン */
typedef struct {
    float w;        /* 実数部 */
    float x;        /* 虚数部i */
    float y;        /* 虚数部j */
    float z;        /* 虚数部k */
} FQuat;

/*
 * クォータニオンを行列に変換
 * lpM <= lpQ
 */
void FUTL_QuatToMatrix(FMatrix *lpM, FQuat *lpQ)
{
    float qw, qx, qy, qz;
    float x2, y2, z2;
    float xy, yz, zx;
    float wx, wy, wz;

    qw = lpQ->w; qx = lpQ->x; qy = lpQ->y; qz = lpQ->z;

    x2 = 2.0f * qx * qx;
    y2 = 2.0f * qy * qy;
    z2 = 2.0f * qz * qz;

    xy = 2.0f * qx * qy;
    yz = 2.0f * qy * qz;
    zx = 2.0f * qz * qx;
        
    wx = 2.0f * qw * qx;
    wy = 2.0f * qw * qy;
    wz = 2.0f * qw * qz;

    lpM->m00 = 1.0f - y2 - z2;
    lpM->m01 = xy - wz;
    lpM->m02 = zx + wy;
    lpM->m03 = 0.0f;

    lpM->m10 = xy + wz;
    lpM->m11 = 1.0f - z2 - x2;
    lpM->m12 = yz - wx;
    lpM->m13 = 0.0f;

    lpM->m20 = zx - wy;
    lpM->m21 = yz + wx;
    lpM->m22 = 1.0f - x2 - y2;
    lpM->m23 = 0.0f;

    lpM->m30 = lpM->m31 = lpM->m32 = 0.0f;
    lpM->m33 = 1.0f;
}

また、クォータニオンの積 r = pq は、プログラムでは次のようになります。これも、
pq = (pw+pxi+pyj+pzk) (qw+qxi+qyj+qzk) を普通に計算すれば、導くことができます。少し見にくいですが、三つのクォータニオンが例え同じものであっても、大丈夫なようにしています。

/*
 * クォータニオンの掛け算
 * lpR = lpP * lpQ
 */
void FUTL_QuatMult(FQuat *lpR, FQuat *lpP, FQuat *lpQ)
{
    float pw, px, py, pz;
    float qw, qx, qy, qz;

    pw = lpP->w; px = lpP->x; py = lpP->y; pz = lpP->z;
    qw = lpQ->w; qx = lpQ->x; qy = lpQ->y; qz = lpQ->z;

    lpR->w = pw * qw - px * qx - py * qy - pz * qz;
    lpR->x = pw * qx + px * qw + py * qz - pz * qy;
    lpR->y = pw * qy - px * qz + py * qw + pz * qx;
    lpR->z = pw * qz + px * qy - py * qx + pz * qw;
}

最後に、任意軸 (ax, ay, az) 回りのθ回転をクォータニオンに変換するプログラムは、次のようになります。

/*
 * 回転クォータニオンの作成
 * lpQ <= 回転軸:(ax, ay, az), 回転角度:rad 
 */
void FUTL_QuatRotation(FQuat *lpQ, float rad, float ax, float ay, float az)
{
    float hrad;
    float s;

    hrad = 0.5f * rad;
    s = FUTL_Sin(hrad);

    lpQ->w = FUTL_Cos(hrad);
    lpQ->x = s * ax;
    lpQ->y = s * ay;
    lpQ->z = s * az;
}

後は、これを使ってマウスによるモデルの回転を実現します。

マウスドラッグによる回転

マウスドラッグによる回転は、床井先生のプログラムを参考に作成しました。床井先生のプログラム同様、マウスボタンが押された時、押されたマウス位置からの変位により回転の軸と回転角度を決めるようにします。例として、下の図を見てください。緑の矢印で示すように右方向にマウスを動かした場合、y軸で右回転。黄色の矢印のように左方向にマウスを動かした場合、y軸で左回転になるようにします。

図にはありませんが、マウスを縦方向に動かす場合はx軸回転になります。結局、マウスで動かす方向とは垂直な軸で回転するようにします。プログラムでは、左マウスボタンが押された時の位置とクォータニオンを記録します。

/*
 * マウスボタン操作時のコールバック関数
 * マウスボタンを押したり、離したりする時にコールされる
 */
static void mouse(int button, int state, int x, int y)
{
    switch (button)
    {
    case GLUT_LEFT_BUTTON:
        if (state == GLUT_DOWN && drag == 0)
        {
            /* ドラッグによるモデルの回転開始 */
            drag = 1;

            /* 開始位置と開始時のクォータニオンの記録 */
            startX = x;
            startY = y;
            FUTL_QuatCopy(&startQtn, &mdlQtn);
        }
        else if (state == GLUT_UP && drag == 1)
        {
            /* ドラッグによるモデルの回転停止 */
            drag = 0;
        }
        break;

    default:
        break;
    }
}

そして、ドラッグ中は、ドラッグ開始位置からの変位により回転軸と回転角度を求めます。ここでは、横方向ならばウィンドウ幅で1回転、縦方向ならばウィンドウ高さで1回転するようにします。気をつけてほしいのは、glutのコールバック関数はマウス位置を左上原点で取得します。そのため、下方向のドラッグは変位 dy が正になるため、下方向ならば、+x軸回転(右ねじ回転、右手則)になります。

/*
 * マウス移動時のコールバック関数
 * マウスが押されている時だけ、コールされる
 */
static void motion(int x, int y)
{
    if (drag != 0)  /* マウスで回転中 */
    {
        int dx, dy;
        FVector3D axis;
        FQuat dqtn;
        float rot;

        /* マウス位置は左上原点 */
        dx = x - startX;
        dy = y - startY;

        /* ドラッグ開始位置からの変位(ラジアン換算) */
        /* 横方向はウィンドウ幅が一周分、縦方向はウィンドウ高さが一周分 */
        /* 下方向のマウス移動は+X軸回転、右方向のマウス移動は+Y軸回転 */
        axis.x = 2.0f * FUTL_PI * dy / (float)screenHeight;
        axis.y = 2.0f * FUTL_PI * dx / (float)screenWidth;
        axis.z = 0.0f;

        /* ドラッグ開始位置からの回転角度、同時に回転軸を正規化 */
        rot = FUTL_VecNormalize(&axis);

        /* ドラッグ開始位置からの差分を元に回転クォータニオンを生成 */
        FUTL_QuatRotation(&dqtn, rot, axis.x, axis.y, axis.z);

        /* 回転クォータニオンを合成 */
        FUTL_QuatMult(&mdlQtn, &dqtn, &startQtn);

        /* クォータニオンをマトリックスに変換 */
        FUTL_QuatToMatrix(&mdlRot, &mdlQtn);
    }
}

後は、FUTL_VecNormalize関数により、変位の大きさ(つまり、回転角度)をラジアン換算で求めると同時に、回転軸を正規化しています。これで回転軸と回転角度を求めることができたので、クォータニオンdqtnを生成し、ドラッグ開始位置でのクォータニオンstartQtnに掛けることで、新しい回転を求めています。最後に、OpenGLの行列スタックに渡すために、新しい回転(クォータニオン)を行列に変換しています。

サンプルプログラムを動かせば、次のような絵が描画されるので、左マウスボタンで動かしてみてください。

ドーナツのテクスチャが変わったのは特に意味がありません。毎回同じだと、更新していないように見えるので、大理石風のものに変更しました。絵心があれば、チョコでものったおいしそうなドーナツにしてみたいのですが。。。。

最新の7件

OpenGL

電子工作

玄箱HG

ホームページ

日記

Copyright (C) 2007 Arakin , All rights reserved.