0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DirectX12でミクさんを躍らせてみよう8-VMDの読み込み

Last updated at Posted at 2024-10-01

前回

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ファイルを読み込む時と同様に、単純にファイルを読み込むコードを追加しました。

次の記事では、読み込んだデータを使ってミクさんを踊らせてみましょう。

次回

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?