2021년 1월 20일 수요일

MMD 파일 구조 - vmd

0. 배경

MMD 관련 프로그램은 많이 있었는데, 그게 어떻게 구성되고 돌아가는지에 대해서 아무리 뒤져봐도 친절히 설명히주는 자료는 찾을 수 없었다. 어딘가엔 있었겠지만 내 검색실력과 짧은 영어와 일본어론 알 수 없었다.. 아래는 참고자료이다.

https://github.com/oguna/MMDFormats

위는 c++ 에서 구현된 간단한 parser 이다.

https://github.com/benikabocha/saba/blob/master/README.md

위는 c++ 에서 mmd simulate 를 할 수 있는 프로그램이다.

나는 이걸 주로 참고해서 mmd 를 분석했다.

이 글에서 모르겠거나 잘못된거 같은 정보가 있으면 이거보면 된다. 

https://github.com/Mona04/MMD2FBX

위는 내가 만든 mmd 파일을 fbx 파일로 바꾸는 프로그램으로 이하의 코드는 여기서 들고온 것이다.


위의 parser 를 순서대로 분석하면서 진행하겠다.


주의사항으로는 local 이 일본어가 아니면 언어가 깨질 수 있다는 것이다.


1. VMD Init

bool MMD_Importer::Init_VMD(std::wstring_view path)
{
	_basePath = FileSystem::GetFileDirectoryFromPath(path);
	_basePathName = FileSystem::GetFileDirectoryFromPath(path) + FileSystem::GetIntactFileNameFromPath(path);

	_fb = new std::filebuf();
	if (!_fb->open(path.data(), std::ios::in | std::ios::binary))
	{
		LOG_WARNING("Can't open the " + FileSystem::ToString(path) + ", please check");
		return false;
	}

	_stream = new std::istream(_fb);
	if (!_stream)
	{
		LOG_WARNING("Failed to create IStream");
		return false;
	}

	auto magic = ReadPmxString(_stream, 30);
	auto name = ReadPmxString(_stream, 20);
	if (strncmp(magic.c_str(), "Vocaloid Motion Data", 20))
	{
		std::cerr << "invalid vmd file." << std::endl;
		return false;
	}

	int version = std::atoi(magic.data() + 20);
}

pmx 처럼 매직이나 파일명, 버전을 확인하는 단계이다.


2. Bone Frame

bool MMD_Importer::LoadBoneFrame(std::shared_ptr<Animation> animation, std::shared_ptr<Skeleton> skeleton)
{
	int bone_frame_num;
	_stream->read((char*)&bone_frame_num, sizeof(int));
	animation->Init_Channels(skeleton->GetBoneMap().size());
	animation->Set_MsPerTic(1000.f / 30.0f);  // 30 fps 

	std::wstring name;
	int frame = 0;
	for (int i = 0; i < bone_frame_num; i++)
	{
		Vector3 pos;
		Quaternion rot;
		uint8_t interpolation[4][16];  // x,y,z,rot bezier. 1byte 만 유효한 값임

		name = ReadPmxWString(_stream, 15);
		_stream->read((char*)&frame, sizeof(int));
		_stream->read((char*)&pos, sizeof(float) * 3); PreProcess_MMD_Vector3(pos, true);
		_stream->read((char*)&rot, sizeof(float) * 4); rot.Normalize();
		_stream->read((char*)interpolation, 4 * 4 * 4); 

		auto& bone = skeleton->GetBone(name);
		if (bone.index >= 0)
		{
			auto& channel = animation->Get_Channel(bone.index);
			auto& key = channel.Add_Key();
			key.pos = pos;
			key.rot = rot; // Vector3(rot.x, rot.y, rot.z).ToQuaternion();
			key.frame = frame;
			CalcBezier(key.bezier_x,   interpolation[0]);
			CalcBezier(key.bezier_y,   interpolation[1]);
			CalcBezier(key.bezier_z,   interpolation[2]);
			CalcBezier(key.bezier_rot, interpolation[3]);

			animation->Set_Duration(frame);
		}
	}

	for (auto& channel : animation->Get_Channels())
		std::sort(channel.keys.begin(), channel.keys.end(),
			[](Bone_Key& ls, Bone_Key& rs) { return ls.frame < rs.frame; });

	return true;
}

각 Bone 에 대한 애니메이션 key 가 한번에 들어있으니 주의하자.

Frame 은 하나의 단위로, 내가 알기론 1초에 30 frame 이 진행된다.


여기서 생소한게 Bezier 인데 검색하면 쉽게 나온다. 

보간용 변수로 (0,0), (1,1) 사이의 두 점을 저장하고 있다.

자료형인 만큼 0~255 의 값이 저장되어 있으며 255로 나누면 제대로 쓸 수 있다.


void MMD_Importer::CalcBezier(UInt8Vector2* desc, const uint8_t* src)
{
	int x0 = src[0]; int y0 = src[4]; 
	int x1 = src[8]; int y1 = src[12];
	desc[0] = UInt8Vector2(x0, y0);
	desc[1] = UInt8Vector2(x1, y1);
}

void MMD_Importer::CalcBezier(UInt8Vector2* desc, const char* src)
{
	int x0 = src[0]; int x1 = src[1]; int y0 = src[2]; int y1 = src[3];
	desc[0] = UInt8Vector2(x0, y0);
	desc[1] = UInt8Vector2(x1, y1);
}

희안한 것은 저장된 자료형마다 값을 뽑아오는 방법이 다르다는 것이다.

char 의 경우 카메라 프레임에서 쓰인다.


또한 가끔 frame 순서가 정렬이 안되어 있는 경우가 있어서 정렬을 꼭 해줘야한다.


3. Face Frame

bool MMD_Importer::LoadFaceFrame(std::shared_ptr<class Animation> animation)
{
	// face frames
	int face_frame_num;
	_stream->read((char*)&face_frame_num, sizeof(int));
	
	auto& morph_channels = animation->Get_Morph_Channels();

	std::wstring name;
	int frame = 0;
	float weight = 0;
	for (int i = 0; i < face_frame_num; i++)
	{
		name = ReadPmxWString(_stream, 15);  // bone 에 없으니까 알아두셈
		_stream->read((char*)&frame, sizeof(int));
		_stream->read((char*)&weight, sizeof(float));
		
		auto& key = morph_channels[name].Add_Key();
		key.frame = frame;
		key.weight = weight;
	}

	for (auto& pair : morph_channels)
		std::sort(pair.second.keys.begin(), pair.second.keys.end(),
			[](Morph_Key& ls, Morph_Key& rs) { return ls.frame < rs.frame; });
	
	return true;
}

앞에서 말했듯 morph name 이 identifier 이다.

saba repository 는 스레드 여러개 써서 매 프레임마다 cpu 로 morph offset 을 계산했다.

morph vertex 수가 많아봐야 1000개 정도라서 gpu 에서 처리하기도 애매하다고 생각한다.



4. Ect

외로 Camera Frame, Light Frame, IK Frame 이 있다.

내가본 것들은 Camera 는 쓸데가 많은데 읽어오지를 못하더라...


List