1
0

More than 3 years have passed since last update.

MMDモデルのボーンをMV1SetFrameUserLocalMatrixで回転させる話

Last updated at Posted at 2020-03-06

初めに

DXライブラリという素晴らしいライブラリについてはこの記事を読んでいる皆さまはもうすでにご存じかと思います。DirectXのラッパーで3D描写が簡単に出来る上にMMDモデルの読み込み・描写も可能です。

そしてそのMMDモデルを動かす時にはモーションファイルを一緒に読み込ませるのが一般的なようです。しかしプログラムの中で動的に動かしたい場合もあると思います。その場合にはMV1SetFrameUserLocalMatrixを使うことでフレーム(ボーン)単位で動かすことが可能となっています。

そのまま回してみる

それでは実際に回転させてみましょう。今回はポンプ長さんの春雨改をお借りしました。

「じゃぁ春雨ちゃん、右を向いてください」

右を向いて
    int mHandle = MV1LoadModel("C:\\ぽんぷ長式春雨改_テスト.pmx");
    MV1SetFrameUserLocalMatrix(mHandle, 35, MGetRotY(0.2 * 2 * M_PI));
    MV1DrawModel(mHandle);

「はい、司令官」
春雨首埋没.PNG

……埋まってるー!?
はい、こうなります。

挙動の説明

このMV1SetFrameUserLocalMatrix、公式のドキュメントでフレームの座標変換行列を設定するという説明がなされているのですが、その座標の原点は1つ上の親ボーンの原点となっているようです。そのためただ単に回転させる行列をセットしただけでは本来合ってほしい座標(この場合では首の場所)では回転せず、親ボーンの座標(この場合は胸のあたり)に移動した状態で回転してしまいます。

そこが分かれば当たり前の挙動ではあります。
座標1.png
MV1SetFrameUserLocalMatrixは親ボーンの座標を基にそこからの変換行列を設定するので
回転行列をセットしただけでは
座標3.png
親ボーンを基準に回転するだけです。
このため春雨ちゃんは首を親ボーンの上半身2に埋めた状態で右を向いてしまったのです。

すなわち理想とする回転
座標5.png

を得るためには回転だけではなく
座標4.png
回転させてから平行移動をする必要があることが分かります。

修正版の実装

そこが分かれば実装も簡単です。

回転行列を得る
MATRIX MV1GetFrameRotateMatrix(int MHandle, int FrameIndex, double Xaxis, double Yaxis, double Zaxis) {
    //親フレームの取得
    int ParentFrame = MV1GetFrameParent(MHandle, FrameIndex);

    //親フレームが存在する(=すなわちParentFrameが-2ではない)ならば相対座標分だけ平行移動する行列を取得
    //親フレームが存在しないならば単位行列を得る(=すなわち平行移動しない)
    MATRIX MTranslate;
    if (-2 != ParentFrame)
    {
        //親子フレームの座標の取得
        VECTOR vecParent = MV1GetFramePosition(MHandle, ParentFrame);
        VECTOR vecChild = MV1GetFramePosition(MHandle, FrameIndex);

        //親を基準にした子の相対座標を取得
        VECTOR vecRerativPar2Chi = VSub(vecChild, vecParent);

        //平行移動分の行列を取得
        MTranslate = MGetTranslate(vecRerativPar2Chi);
    }
    else
    {
        MTranslate = MGetIdent();
    }

    //それぞれの軸に沿って回転する行列を取得
    MATRIX MXaxis = MGetRotX(Xaxis);
    MATRIX MYaxis = MGetRotY(Yaxis);
    MATRIX MZaxis = MGetRotZ(Zaxis);

    //軸毎に回転させてから平行移動を実行する
    MATRIX MReturn = MMult(MMult(MMult(MXaxis, MYaxis), MZaxis), MTranslate);

    return MReturn;
}

このメソッドを用いてみましょう。

「春雨ちゃん、右向いて」

右を向いて改
int mHandle = MV1LoadModel("C:\\ぽんぷ長式春雨改_テスト.pmx");
MATRIX Mrotate = MV1GetFrameRotateMatrix(mHandle, 35, 0, 0.2 * 2 * M_PI, 0);
MV1SetFrameUserLocalMatrix(mHandle, 35, Mrotate);
MV1DrawModel(mHandle);

「はい、司令官」
春雨首長.PNG

春雨!首が!
安いもんだ、首の一本ぐらい…  駄目です、ゴムの少年の方になってます。

挙動の更なる説明

はい、こちら、3Dモデルを拡大していることに起因しています。

モデルの拡大
//モデルのサイズ調整
int scale = 3;
MV1SetScale(mHandle, VGet(scale, scale, scale));
for (int i = 0; i < nMaterial; i++)
{
    double dotwidth = MV1GetMaterialOutLineDotWidth(mHandle, i);
    MV1SetMaterialOutLineDotWidth(mHandle, i, dotwidth / scale);
    dotwidth = MV1GetMaterialOutLineWidth(mHandle, i);
    MV1SetMaterialOutLineWidth(mHandle, i, dotwidth / scale);
}

実はこのモデル、丁度いい大きさに表示するために拡大しています。(今回は3倍)
ちなみにforループ内のように輪郭線を細くしてあげないと太くなった輪郭線でモデルが埋まります。公式の掲示板でも説明されています。極端な例をあげるとこんな感じです。(モデルの大きさを80倍にしています)
春雨ボーダー太い.PNG

さて、このようにモデルが3倍されていたことで算出された子ボーン-親ボーンの距離も3倍になったようです。
(そして恐らく元のサイズのモデルに変形の行列を適応してから縮小・拡大が適応されていると思われる。これはモデルの拡大のタイミングを変えてみても適切に描写されないことからの想像ですが…)
そのためモデルが何倍されているかという情報もメソッドに与えて、その分平行移動の距離を小さくしてあげる必要があります。
(修正が不要な場合もあり??)

更なる修正を行う

更なる修正版がこちら

回転行列を得るの改善版
MATRIX MV1GetFrameRotateMatrix(int MHandle, int FrameIndex, double Xaxis, double Yaxis, double Zaxis, double modelScale) {
    //親フレームの取得
    int ParentFrame = MV1GetFrameParent(MHandle, FrameIndex);

    //親フレームが存在する(=すなわちParentFrameが-2ではない)ならば相対座標分だけ平行移動する行列を取得
    //親フレームが存在しないならば単位行列を得る(=すなわち平行移動しない)   
    MATRIX MTranslate;
    if (-2 != ParentFrame)
    {
        //親子フレームの座標の取得
        VECTOR vecParent = MV1GetFramePosition(MHandle, ParentFrame);
        VECTOR vecChild = MV1GetFramePosition(MHandle, FrameIndex);

        //親を基準にした子の相対座標を取得
        VECTOR vecRerativPar2Chi = VSub(vecChild, vecParent);

        //モデルが何倍に大きくなっているかに従って相対座標を補正
        if (0 != modelScale)
        {
            modelScale = 1 / modelScale;
        }
        vecRerativPar2Chi = VScale(vecRerativPar2Chi, modelScale);
        MTranslate = MGetTranslate(vecRerativPar2Chi);
    }
    else
    {
        MTranslate = MGetIdent();
    }

    //それぞれの軸に沿って回転する行列を取得
    MATRIX MXaxis = MGetRotX(Xaxis);
    MATRIX MYaxis = MGetRotY(Yaxis);
    MATRIX MZaxis = MGetRotZ(Zaxis);

    //軸毎に回転させてから平行移動を実行する

    MATRIX MReturn = MMult(MMult(MMult(MXaxis, MYaxis), MZaxis), MTranslate);

    return MReturn;
}

このように修正してもう一度右を向いて貰いましょう
「右を向いて」

右を向いて改二
int mHandle = MV1LoadModel("C:\\ぽんぷ長式春雨改_テスト.pmx");
MATRIX Mrotate = MV1GetFrameRotateMatrix(mHandle, 35, 0, 0.2 * 2 * M_PI, 0, scale);
MV1SetFrameUserLocalMatrix(mHandle, 35, Mrotate);
MV1DrawModel(mHandle);

「はい、司令官」
春雨正常.PNG

見事に首が正常です。可愛い

締め

不思議な挙動はプログラミングをやっていれば良くあることですが、ビジュアルに訴えてくるのは中々に面白いことになる衝撃的ですね。
そういうところも楽しんでプログラミングをしていきたいものです。
何はともあれ、これでモデルを動かすことが出来るようになりました。
最終的にはすでにあるけど素敵なデスクトップマスコットを作りたいですね。

それでは最後まで読んでいただきありがとうございました。

締まらない(2020/3/8追記)

実はこれには問題があります。まずはこちらをご覧ください。
春雨荒ぶる足.PNG

明らかに足が荒ぶっていることが分かります。
こちらは左足をZ方向に、左ひざをY軸方向に、左足首をX軸方向に、それぞれ90°回転させたときの様子です。
一体どうして…

またしても説明

このように親子関係にある複数ボーンに、中枢(今回では股関節→膝→足首)から変形をさせたのが原因です。

場所の補正の位置ベクトルはワールド座標系で取得される一方で、
ボーンの変形を設定するときには親ボーン座標で設定するのでした。
そのため前の首だけの設定、と言うように他に座標変換行列が設定されていない時にはワールド座標と親ボーン座標の向きが一致していましたが、
今回のように親ボーンに座標変換行列を設定していた場合はワールド座標と親ボーン座標の向きが不一致となり
この例のように位置ベクトルの向きが望まない方向になってしまったのです。

解決方法は二つです。
より末梢のボーンから座標変換行列を設定するか、
親ボーンの座標変換行列を用いて補正するかです。

補正に成功したのでソースを載せます。

回転行列取得メソッド更なる改善版
MATRIX MV1GetFrameRotateMatrix(int MHandle, int FrameIndex, double Xaxis, double Yaxis, double Zaxis, double modelScale) {
    //親フレームの取得
    int ParentFrame = MV1GetFrameParent(MHandle, FrameIndex);

    //モデルの拡大率に従って移動距離を補正する準備
    if (0 != modelScale)
    {
        modelScale = 1 / modelScale;
    }
    else
    {
        return MGetIdent();
    }

    //相対座標分の平行移動行列を取得
    MATRIX MTranslate;
    if (-2 != ParentFrame)
    {
        //親子フレームの座標の取得
        VECTOR vecParent = MV1GetFramePosition(MHandle, ParentFrame);
        VECTOR vecChild = MV1GetFramePosition(MHandle, FrameIndex);

        //親を基準にした子の相対座標を取得
        VECTOR vecRerativPar2Chi = VSub(vecChild, vecParent);
        //モデルの拡大率によって相対距離を補正
        vecRerativPar2Chi = VScale(vecRerativPar2Chi, modelScale);
        MTranslate = MGetTranslate(vecRerativPar2Chi);
    }
    else
    {
        MTranslate = MGetIdent();
    }

    //それぞれの軸に沿って回転する行列を取得
    MATRIX MXaxis = MGetRotX(Xaxis);
    MATRIX MYaxis = MGetRotY(Yaxis);
    MATRIX MZaxis = MGetRotZ(Zaxis);

    //遡って親フレームの回転要素の取得
    std::vector<MATRIX> MParentsRotates;
    while (-2 != ParentFrame && -1 !=ParentFrame)
    {
        //親フレーム座標を取得し、そこから回転要素を抽出
        MATRIX MParentFrame = MV1GetFrameLocalMatrix(MHandle, ParentFrame);
        MATRIX MParentRotate = MGetRotElem(MParentFrame);
        //回転行列の逆行列=回転の方向を逆にする
        MATRIX MInvParentRotate = MInverse(MParentRotate);
        //順に追加
        MParentsRotates.push_back(MInvParentRotate);

        //更に親のフレームを取得
        ParentFrame = MV1GetFrameParent(MHandle, ParentFrame);
    }
    //取得した祖先たちの回転行列をより中枢の方からかけて、平行移動のベクトルを補正する
    for (int i = MParentsRotates.size()-1; i >=0; i--)
    {
        MTranslate = MMult(MTranslate, MParentsRotates[i]);
    }
    //平行移動ベクトルに生じうる回転成分を消す
    MTranslate.m[0][0] = 1; MTranslate.m[0][1] = 0; MTranslate.m[0][2] = 0;
    MTranslate.m[1][0] = 0; MTranslate.m[1][1] = 1; MTranslate.m[1][2] = 0;
    MTranslate.m[2][0] = 0; MTranslate.m[2][1] = 0; MTranslate.m[2][2] = 1;

    //軸毎に回転させてから平行移動を実行する
    MATRIX MReturn = MMult(MMult(MMult(MXaxis, MYaxis), MZaxis), MTranslate);

    return MReturn;
}

これでもう一度先ほどのポーズをしてもらいます。
春雨荒ぶらない.PNG
はい、股関節がZ方向(横に開く)、膝がy方向(内側)、足首がx方向(脛につく向き)にちゃんと90°だけ回っています。

締め

Dxlibの回転行列・平行移動行列は少し癖があるように感じましたので注意が必要ですね。
ともあれこれでMMDモデルに動的にポーズを取らせることが出来るようになりました。
動的にポーズを取らせる必要があるのはゲーム作りでは稀かもしれませんが可能性が広がるのはいいものです。
二度目になりますが最後までご覧いただきありがとうございました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0