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); }
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 는 쓸데가 많은데 읽어오지를 못하더라...
댓글 없음:
댓글 쓰기