前回
VMDの読み込み
VMDはMMDのアニメーションファイル形式です。
正確には、アニメーション情報以外の情報も含んでいますが、読み込むだけで、実際に使用するのはアニメーションのみです。
今回も使用するVMDファイルは、自分でお探しください。
これもPMXファイルを読み込む時と大きく変わりません。同じように順番にバイト数だけ読み込めば大丈夫です。
VMDは以下の情報を含んでいます。
- ヘッダー
- アニメーション
- モーフ
- カメラ
- ライト
- シャドウ
- IK
それでは始めましょう。
VMDファイルの読み込み
struct VMDFileData
{
VMDHeader header;
std::vector<VMDMotion> motions;
std::vector<VMDMorph> morphs;
std::vector<VMDCamera> cameras;
std::vector<VMDLight> lights;
std::vector<VMDShadow> shadows;
std::vector<VMDIK> iks;
};
VMDはVMDFileDataという構造体に保存するようにしました。
bool LoadVMDFile(const std::wstring& filePath, VMDFileData& fileData)
PMXファイルを読み込む際と同じです。
ファイルパスを指定し、保存先の構造体を渡します。
if (filePath.empty() == true)
{
return false;
}
std::ifstream vmdFile{ filePath, (std::ios::binary | std::ios::in) };
if (vmdFile.fail() == true)
{
vmdFile.close();
return false;
}
ファイルをバイナリ、読み取りモードで開きます。
ヘッダー
struct VMDHeader
{
char header[30];
char modelName[20];
};
ヘッダーの構造体は上記の通りです。
bool ReadHeader(VMDFileData& data, std::ifstream& file)
{
file.read(reinterpret_cast<char*>(&data.header.header), 30);
std::string headerStr = data.header.header;
if (headerStr != "Vocaloid Motion Data 0002" &&
headerStr != "Vocaloid Motion Data")
{
return false;
}
file.read(reinterpret_cast<char*>(&data.header.modelName), 20);
return true;
}
最初の30バイトはPMXのマジックナンバーに似ています。VMDにも決まった文字列が含まれています。上記の2つの文字列のいずれでもない場合、VMDファイルではないとしてメソッドを終了します。
次の20バイトはモデル名です。
アニメーション
このデータが最も重要なデータです。
struct VMDMotion
{
std::wstring boneName;
unsigned int frame;
DirectX::XMFLOAT3 translate;
DirectX::XMFLOAT4 quaternion;
unsigned char interpolation[64];
};
各パラメータがどのようなものかは、実際にアニメーションを実装する際に見ていきましょう。
bool ReadMotion(VMDFileData& data, std::ifstream& file)
{
unsigned int count = 0;
file.read(reinterpret_cast<char*>(&count), 4);
data.motions.resize(count);
for (auto& motion : data.motions)
{
UnicodeUtil::ReadJISToWString(file, motion.boneName, 15);
file.read(reinterpret_cast<char*>(&motion.frame), 4);
file.read(reinterpret_cast<char*>(&motion.translate), 12);
file.read(reinterpret_cast<char*>(&motion.quaternion), 16);
file.read(reinterpret_cast<char*>(motion.interpolation), 64);
}
return true;
}
アニメーションデータを読み込むコードは上記の通りです。
ボーンの名前を読み込む部分を見てみましょうか。
ここでも std::wstring にボーン名を保存していますが、PMX の時とは異なるメソッドを使用しています。
なぜ異なるものを使用しているのでしょうか?
void ReadJISToWString(std::ifstream& _file, std::wstring& output, size_t length)
{
char* charStr = new char[length];
_file.read(reinterpret_cast<char*>(charStr), length);
std::string jisString = charStr;
output = multi_to_wide_capi(jisString);
delete[] charStr;
}
メソッドの内容は以下のようになっています。ボーンの名前も日本語であるため、コードデバッガーで内容を確認したり、後でUIに表示したりするためにwstringとして保存します。
しかし、VMDのアニメーションデータの文字列はPMXとは異なり、バイト数が固定されています。
PMXでは文字列を読み込む前に4バイトを先に読み込んで文字列の長さを取得していましたね。
VMDではこの文字列の長さが別途なく固定されているため、別のメソッドを作成しました。
モーフ
struct VMDMorph
{
std::wstring blendShapeName;
unsigned int frame;
float weight;
};
今回はモーフデータです。主に表情アニメーションなどのためのデータです。
bool ReadMorph(VMDFileData& data, std::ifstream& file)
{
unsigned int count = 0;
file.read(reinterpret_cast<char*>(&count), 4);
data.morphs.resize(count);
for (auto& morph : data.morphs)
{
UnicodeUtil::ReadJISToWString(file, morph.blendShapeName, 15);
file.read(reinterpret_cast<char*>(&morph.frame), 4);
file.read(reinterpret_cast<char*>(&morph.weight), 4);
}
return true;
}
カメラ
VMDはカメラのデータも持っています。
おそらくMMDプログラムのカメラの位置や移動のためのデータでしょうが、ここでは使用せず、読み込むだけにします。
struct VMDCamera
{
unsigned int frame;
float distance;
DirectX::XMFLOAT3 interest;
DirectX::XMFLOAT3 rotate;
unsigned char interpolation[24];
unsigned int fov;
unsigned char isPerspective;
};
bool ReadCamera(VMDFileData& data, std::ifstream& file)
{
unsigned int count = 0;
file.read(reinterpret_cast<char*>(&count), 4);
data.cameras.resize(count);
for (auto& camera : data.cameras)
{
file.read(reinterpret_cast<char*>(&camera.frame), 4);
file.read(reinterpret_cast<char*>(&camera.distance), 4);
file.read(reinterpret_cast<char*>(&camera.interest), 12);
file.read(reinterpret_cast<char*>(&camera.rotate), 12);
file.read(reinterpret_cast<char*>(camera.interpolation), 24);
file.read(reinterpret_cast<char*>(&camera.fov), 4);
file.read(reinterpret_cast<char*>(&camera.isPerspective), 1);
}
return true;
}
ライト
照明に関するデータも含まれています。カメラと同様に、読み込むだけにします。
struct VMDLight
{
unsigned int frame;
DirectX::XMFLOAT3 color;
DirectX::XMFLOAT3 position;
};
bool ReadLight(VMDFileData& data, std::ifstream& file)
{
unsigned int count = 0;
file.read(reinterpret_cast<char*>(&count), 4);
data.lights.resize(count);
for (auto& light : data.lights)
{
file.read(reinterpret_cast<char*>(&light.frame), 4);
file.read(reinterpret_cast<char*>(&light.color), 12);
file.read(reinterpret_cast<char*>(&light.position), 12);
}
return true;
}
影
影についても同様に読み込むだけです。
struct VMDShadow
{
unsigned int frame;
unsigned char shadowType;
float distance;
};
bool ReadShadow(VMDFileData& data, std::ifstream& file)
{
unsigned int count = 0;
file.read(reinterpret_cast<char*>(&count), 4);
data.shadows.resize(count);
for (auto& shadow : data.shadows)
{
file.read(reinterpret_cast<char*>(&shadow.frame), 4);
file.read(reinterpret_cast<char*>(&shadow.shadowType), 1);
file.read(reinterpret_cast<char*>(&shadow.distance), 4);
}
return true;
}
IK
これはIKに関するデータです。
IKとは何か、そしてデータ構造については、実際に適用する際にまた話すことにします。
struct VMDIKInfo
{
std::wstring name;
unsigned char enable;
};
struct VMDIK
{
unsigned int frame;
unsigned char show;
std::vector<VMDIKInfo> ikInfos;
};
bool ReadIK(VMDFileData& data, std::ifstream& file)
{
unsigned int count = 0;
file.read(reinterpret_cast<char*>(&count), 4);
data.iks.resize(count);
for (auto& ik : data.iks)
{
file.read(reinterpret_cast<char*>(&ik.frame), 4);
file.read(reinterpret_cast<char*>(&ik.show), 1);
unsigned int ikInfoCount = 0;
file.read(reinterpret_cast<char*>(&ikInfoCount), 4);
ik.ikInfos.resize(ikInfoCount);
for (auto& ikInfo : ik.ikInfos)
{
UnicodeUtil::ReadJISToWString(file, ikInfo.name, 20);
file.read(reinterpret_cast<char*>(&ikInfo.enable), 1);
}
}
return true;
}
まとめ
今回もPMXファイルを読み込む時と同様に、単純にファイルを読み込むコードを追加しました。
次の記事では、読み込んだデータを使ってミクさんを踊らせてみましょう。
次回