LoginSignup
5
5

More than 5 years have passed since last update.

MMDモデルを使用したデスクトップユーティリティの開発 その9 スキンメッシュアニメーション(FKのみ)

Posted at

その8
次 その10

完全に理解できた訳ではないので、間違っている部分もあるかもしれません。

今回やること

FK・モーションの実装
MSAAの設定
カリングモードの切り替え

ソース(GitHub): ModelViewer, MmdFileLoader

ボーン

MMDでモデルを動かそうとするときに表示される、これ。
cirno_bone.png

ボーン(を可視化したもの)です。
このボーンを回転させるなどすることで動きを表現します。

PMDファイルでは、ボーンを以下のように定義しています。(今回使用するパラメータ・情報のみ抜粋)

Bone
public class PmdBone {
    public string Name;

    //親ボーンのインデックス なしの場合は-1
    public short ParentIndex;

    //子ボーンのインデックス なしの場合は0
    public short TailIndex;

    //ボーンの根本の位置
    public Vector3 HeadPosition;
}

public class PmxBone {
    public string Name;

    //ボーンの根本の位置
    public Vector3 Position;

    //親ボーンのインデックス なしの場合は-1
    public int ParentIndex;

    //子ボーンのインデックス なしの場合は-1
    public int TailIndex;

    //ボーンの変形階層
    public int Rank;

    //ボーンの変形フラグ
    public BoneFlagEnum BoneFlag;
}

public enum BoneFlagEnum {
    TransformAfterPhysic = 0x1000
}

注意しなければいけない点として、
PMDでは子ボーンのインデックスは「なしの場合は0」になっています。PMXのデータと共通化することを考え、-1に修正しています。
PMXではボーンの向きを決定するために、子ボーンのインデックスで指定 or ボーンの向きを表すベクトルのどちらかで指定します。Offsetで指定されている場合は、子ボーンのインデックスを-2にしています。
さらに、PMXではボーンを変形させる順番が物理演算前後と階層により決まります。

FK(フォワードキネマティクス)

ボーンを変形させる方法の一つ、FK(フォワードキネマティクス)を実装します。
FKについてはこちらが詳しいです。

シェーダの変更

ボーンの計算をさせる前に、ボーンに従って頂点を動かすためにシェーダに情報を送る準備をします。

effect.fx
matrix World;
matrix View;
matrix Projection;
matrix BoneMatrix[512];
(省略)

struct Vertexes {
    float4 position : SV_Position;
    float4 normal : NORMAL;
    float2 uv : TEXCOORD;
    int4 idx : BLENDINDICES;
    float4 weight : BLENDWEIGHT;
};

(省略)

OutVert myVertexShader(Vertexes input) {
    OutVert output = (OutVert)0;
    matrix Comb = (matrix)0;
    for(int i = 0;i < 4;i++) {
        if(input.idx[i] == -1) continue;
        Comb += BoneMatrix[input.idx[i]] * input.weight[i];
    }
    (省略)
}

(省略)

BoneMatrixにはボーンの変形行列を格納します。ボーンの数は多めに512に設定しています。
頂点データには、頂点に影響を与えるボーンのインデックスとその重みを表す配列を渡しています。

Bone.cs
public void SetBoneMatrix(Matrix[] bones) {
    effect.GetVariableByName("BoneMatrix").AsMatrix().SetMatrixArray(bones);
}

ボーン変形行列の作成

こちらを参考にしました。

ボーンの親子関係を表現するSkinBoneクラスを作成します。

Bone.cs
public class SkinBone {
    public int Id { get; private set; }
    public string Name { get; private set; }

    //親ボーン
    public SkinBone Parent { get; set; }

    //子ボーンのリスト
    public List<SkinBone> Children { get; set; }

    //階層
    public int Rank { get; private set; }

    //物理演算前に変形させるか
    public bool IsBeforePhysic { get; private set; }

    //初期姿勢行列
    public Matrix Init { get; private set; }

    //オフセット行列
    public Matrix Offset { get; private set; }
    public Matrix Bone { get; set; }

    //回転量
    public Quaternion Rotate { get; set; }
    //移動量
    public Vector3 Translate { get; set; }

    //モーション用の回転・移動量
    public Quaternion MotionRotate { get; set; }
    public Vector3 MotionTranslate { get; set; }
}

モデルから読み込んだボーンの情報をSkinBoneクラスに流しこみます。
初期姿勢行列はボーンの根本へ移動する行列を用意し、オフセット行列を親から掛けていくことで得られます。
オフセット行列はボーンを原点に移動させる行列です。

Bone.cs
private void CreateMatrix(MmdBone[] bones, int index) {
    Vector3 pos = bones[index].Position;

    Init = Matrix.Translation(pos);
    Offset = Matrix.Invert(Init);
}

public static void CalcRelative(SkinBone me, Matrix parent) {
    foreach(var c in me.Children) {
        CalcRelative(c, me.Offset);
    }
    me.Init *= parent;
}

ボーンの回転量と移動量をRotateTranslateに設定し、変換行列を更新させます。
シェーダに変換行列を渡すことで、モデルを動かすことができます。

Bone.cs
//ボーン行列の更新
private void UpdateBone() {
    foreach(var b in Bones) {
        b.Bone = CalcTranspose(b.Rotate, b.Translate) * b.Init;
        if(b.Parent != null) b.Bone *= b.Parent.Bone;
    }
}

//回転と移動から変換行列の作成
public Matrix CalcTranspose(Quaternion Rotation, Vector3 Translation) {
    return Matrix.RotationQuaternion(Rotation) * Matrix.Translation(Translation);
}

//変換行列配列
public Matrix[] Results {
    get {
        return Bones.Select(x => x.Offset * x.Bone).ToArray();
    }
}

モーション

VMD

モーションデータとしてVMDを使用します。
フォーマットはこちら

VmdMotion
public class VmdMotion {
    public string BoneName;
    public int FrameCount;
    public Vector3 Position;
    public Quaternion Rotation;
}

MMDにおいてモーションとモーションの間は、三次ベジエ曲線を用いて補完されますが、とりあえず線形補完にします。
FrameCountは、相対的なフレーム数です。FrameCount[0, 10, 20, 30]とあった場合、0・10・20・30フレーム目ではなく、0・10・30・60フレーム目です。

モーションの再生・適用を管理するMotionManagerクラスを作ります。
現在のフレームを算出するため、System.Diagnostics.Stopwatchを用いて再生時間を計測します。
再生されるモーションデータのリストをボーンごとに追加し、それぞれのボーンのモーションが始まるフレームと終了するフレーム番号を記録します。

Motion.cs
public class MotionManager {
    private MotionBone[] motionList;
    private Stopwatch stopWatch;
    private readonly int FPS = 30;
    public float NowFrame { get { return FPS * stopWatch.ElapsedMilliseconds / 1000.0f; } }
}

public class MotionBone {
    public string BoneName;
    public List<MotionData> MotionList;
    public int StartFrame;
    public int EndFrame;
}

public class MotionData {
    public int FrameCount;
    public Quaternion Rotate;
    public Vector3 Translate;
}

モーションデータを格納し、モーションの再生が始まったら、その時点のフレームをもとに、各ボーンの回転・移動量を計算します。

Motion.cs
public ApplyedMotion[] GetMotion() {
    var tmp = new ApplyedMotion[motionList.Length];
    var nowFrame = NowFrame;

    for(int i = 0;i < tmp.Length; i++) {
        tmp[i] = new ApplyedMotion();
        var nowList = motionList[i].MotionList;

        //このボーンのモーションがない場合
        if(nowList.Count == 0) continue;
        int startFrm = motionList[i].StartFrame;
        int endFrm = motionList[i].EndFrame;

        //最後のモーションが終わっている場合
        if(endFrm <= nowFrame) {
            var nowAt = nowList.Last();
            tmp[i].Rotate = nowAt.Rotate;
            tmp[i].Translate = nowAt.Translate;
        //最初のモーションが始まっていない場合
        } else if(nowFrame < startFrm) {
            int nowIdx = 0;
            var t = (nowFrame) / (nowList[nowIdx].FrameCount);
            tmp[i].Translate = Vector3.Lerp(Vector3.Zero, nowList[nowIdx].Translate, t);
            tmp[i].Rotate = Quaternion.Lerp(Quaternion.Identity, nowList[nowIdx].Rotate, t);
        } else {
            int nowIdx = 0;
            while(nowList[nowIdx].FrameCount <= nowFrame) nowIdx++;
            if(nowIdx > 0) nowIdx--;
            var t = (nowFrame - nowList[nowIdx].FrameCount) / (nowList[nowIdx + 1].FrameCount - nowList[nowIdx].FrameCount);
            tmp[i].Translate = Vector3.Lerp(nowList[nowIdx].Translate, nowList[nowIdx + 1].Translate, t);
            tmp[i].Rotate = Quaternion.Lerp(nowList[nowIdx].Rotate, nowList[nowIdx + 1].Rotate, t);
        }
    }

    return tmp;
}

public class ApplyedMotion {
    public Quaternion Rotate;
    public Vector3 Translate;
}

各ボーンの回転・移動量を計算したあと、ボーンにそれを適用します。

DrawMmdModel.cs
//現在のフレームのボーンの動きを取得し、セットする
boneMng.SetPose(motMng.GetMotion());
//変換行列を計算する。
boneMng.Update();
//シェーダにセットする
effect.SetBoneMatrix(boneMng.Results);
//描画
effect.DrawAll(camera);

やっと踊らせられました。
dancing.png

今回実装したのはFKのみなので、足がぶんぶん振れています。

アンチエイリアス

前回までの画像を見ると分かるとおり、ちょっとギザギザしてました。
SlimDX.Direct3D11.Deviceを作成したあとにDevice.CheckMultisampleQualityLevelsQualityCountを調べ、SwapChainDescriptionTexture2DDescriptionSampleDescriptionに設定することで、アンチエイリアスが働き、綺麗に描画してくれます。

Lat式ミクの描画

Lat式ミクを描画しようとすると下図のようになってしまいます。
Lat式ミクは若干特殊で、輪郭などを表現するために透明な裏返しのポリゴンを顔に貼り付けているそうです。

lat.png

マテリアルのアルファ値が0.999のときのみにカリングさせることで正しく描画されます。
こちらを参考にしました。

Effect.cs
private void SetMaterial(int nowCount) {
    texture.SetTexture(nowCount);
    SetLight(nowCount);
    SetCull(!(materials[nowCount].DrawFlag.HasFlag(DrawFlagEnumes.DrawBoth)
            || materials[nowCount].Alpha == 0.999f));
}

latok.png

さいごに

次回はIKの実装とTGA読み込み対応をするつもりです。

5
5
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
5
5