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. PMX Init
bool MMD_Importer::Init_PMX(std::wstring_view 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; } char magic[4]; _stream->read((char*)magic, sizeof(char) * 4); if (magic[0] != 0x50 || magic[1] != 0x4d || magic[2] != 0x58 || magic[3] != 0x20) { std::cerr << "invalid magic number." << std::endl; throw; } _stream->read((char*)&_version, sizeof(float)); if (_version != 2.0f && _version != 2.1f) { std::cerr << "this is not ver2.0 or ver2.1 but " << _version << "." << std::endl; throw; } _setting.Read(_stream); this->_model_name = std::move(ReadString(_stream, _setting.encoding)); this->_model_english_name = std::move(ReadString(_stream, _setting.encoding)); this->_model_comment = std::move(ReadString(_stream, _setting.encoding)); this->_model_english_comment = std::move(ReadString(_stream, _setting.encoding)); return true; }
기본적인 설정, 매직, 버전 등을 우선 들고온다.
기존의 파일은 대부분 2.0 버전이 많다.
이후로 계속 std::istream 에서 파일을 읽어올 것이다.
std::wstring FileSystem::ToWString(std::string_view str, bool isJapanese) { wchar_t buffer[1024]; DWORD minSize; minSize = MultiByteToWideChar(isJapanese ? 932 : CP_ACP, 0, str.data(), -1, NULL, 0); if (1024 < minSize) return L"Size over"; // Convert string from multi-byte to Unicode. MultiByteToWideChar(isJapanese ? 932 : CP_ACP, 0, str.data(), -1, buffer, minSize); return std::wstring(buffer); }
번외로 일본어는 멀티바이트로 데이터가 들어오는데 유니코드형식으로 들고오는 코드이다.
유감스럽게도 로케일이 다르면 제대로 작동을 하지 않는다.
2. Mesh
1. Vertices
bool MMD_Importer::LoadVertices(std::shared_ptr<class Mesh> mesh) { auto& vertices = mesh->GetVertices(); int vertex_count = 0; _stream->read((char*)&vertex_count, sizeof(int)); vertices.reserve(vertex_count); vertices.resize(vertex_count); for (int i = 0; i < vertex_count; ++i) { Vertex_MASTER& v = vertices[i]; _stream->read((char*)(&v.pos), sizeof(float) * 3); _stream->read((char*)(&v.normal), sizeof(float) * 3); _stream->read((char*)(&v.uv), sizeof(float) * 2); assert(v.uv.x <= 1.0f); for (int set_uv = 0; set_uv < _setting.uv; ++set_uv) { _stream->read((char*)(&v.uva[set_uv]), sizeof(float) * 4); } PmxVertexSkinningType skinningType; _stream->read((char*)(&skinningType), sizeof(PmxVertexSkinningType)); switch (skinningType) { case PmxVertexSkinningType::BDEF1: v.bone_index[0] = ReadIndex(_stream, _setting.bone_index_size); v.bone_weight[0] = 1.0f; break; case PmxVertexSkinningType::BDEF2: v.bone_index[0] = ReadIndex(_stream, _setting.bone_index_size); v.bone_index[1] = ReadIndex(_stream, _setting.bone_index_size); _stream->read((char*)&(v.bone_weight), sizeof(float)); v.bone_weight[1] = 1 - v.bone_weight[0]; break; case PmxVertexSkinningType::BDEF4: // mostly used v.bone_index[0] = ReadIndex(_stream, _setting.bone_index_size); v.bone_index[1] = ReadIndex(_stream, _setting.bone_index_size); v.bone_index[2] = ReadIndex(_stream, _setting.bone_index_size); v.bone_index[3] = ReadIndex(_stream, _setting.bone_index_size); _stream->read((char*)&(v.bone_weight), sizeof(float) * 4); break; case PmxVertexSkinningType::SDEF: LOG_WARNING("I haven't learn SDEF. So I cannot handle it"); v.bone_index[0] = ReadIndex(_stream, _setting.bone_index_size); v.bone_index[1] = ReadIndex(_stream, _setting.bone_index_size); // 뭔말인지 모르겠어서 제대로 구현 안함. 원래 코드 파서를 참고하자. 일단 캬루는 이거 안쓴다. _stream->read((char*)&(v.bone_weight), sizeof(float) * 3); _stream->read((char*)&(v.bone_weight), sizeof(float) * 3); _stream->read((char*)&(v.bone_weight), sizeof(float) * 3); _stream->read((char*)&(v.bone_weight), sizeof(float) * 1); break; case PmxVertexSkinningType::QDEF: v.bone_index[0] = ReadIndex(_stream, _setting.bone_index_size); v.bone_index[1] = ReadIndex(_stream, _setting.bone_index_size); v.bone_index[2] = ReadIndex(_stream, _setting.bone_index_size); v.bone_index[3] = ReadIndex(_stream, _setting.bone_index_size); _stream->read((char*)&(v.bone_weight), sizeof(float) * 4); break; default: LOG_WARNING("invalid skinning type"); } _stream->read((char*)&(v.edge), sizeof(float)); } return true; }
자주 봐왔던 vertex 로 이중에 낯선 것은 edge 와 uva 정도이다.
edge 는 내가본 mmd 파일은 모두 1 을 가지고 있고 별 의미가 없다.
어차피 material 의 데이터로 외각선을 그리기 때문이다.
uva 는 나는 쓴 모습을 못봐서 잘 모르겠다.
유의사항은 pmx 는 mesh 가 하나라는 것이다.
Material 마다 index 가 지정되어있어 하나의 mesh 를 끊어가면서 렌더링한다.
2. Indices
bool MMD_Importer::LoadIndices(std::shared_ptr<class SkeletalMesh> mesh) { auto& indices = mesh->GetIndices(); int index_count = 0; _stream->read((char*)&index_count, sizeof(int)); indices.reserve(index_count); indices.resize(index_count); for (int i = 0; i < index_count; i += 3) { indices[i + 0] = ReadIndex(_stream, _setting.vertex_index_size); indices[i + 1] = ReadIndex(_stream, _setting.vertex_index_size); indices[i + 2] = ReadIndex(_stream, _setting.vertex_index_size); } return true; }
특별한건 없다.
indices 순서가 앞면이 시계방향이니 DirectX 의 default 값으론 잘 작동한다.
3. Material
1. Texture
bool MMD_Importer::LoadTexturePath(std::vector<std::wstring>& texturesPath) { int texture_count = 0; _stream->read((char*)&texture_count, sizeof(int)); texturesPath.reserve(texture_count); texturesPath.resize(texture_count); for (int i = 0; i < texture_count; i++) texturesPath[i] = FileSystem::Find_Replace_All(ReadString(_stream, _setting.encoding), L"\\", L"/"); return true; }
여기서 중요한건 텍스쳐의 인덱스이다.
Material 은 텍스쳐 인덱스로 텍스쳐를 들고오기 때문에 꼭 저대로 넣어야한다.
2. Material
bool MMD_Importer::LoadMaterial(std::shared_ptr<Material> material, const std::vector<std::wstring>& texturesPath) { material->Set_MaterialName(ReadString(_stream, _setting.encoding)); material->Set_MaterialEnglishName(ReadString(_stream, _setting.encoding)); auto& material_common = material->Get_Material_Common(); auto& material_mmd = material->Get_Material_MMD(); _stream->read((char*)&material_common._diffuse, sizeof(float) * 4); _stream->read((char*)&material_common._specular, sizeof(float) * 3); _stream->read((char*)&material_common._specularlity, sizeof(float)); _stream->read((char*)&material_common._ambient, sizeof(float) * 3); _stream->read((char*)&material_mmd._draw_mode, sizeof(uint8_t)); _stream->read((char*)&material_mmd._edge_color, sizeof(float) * 4); _stream->read((char*)&material_mmd._edge_size, sizeof(float)); int diffuse_texture_index = ReadIndex(_stream, _setting.texture_index_size); int sphere_texture_index = ReadIndex(_stream, _setting.texture_index_size); int toon_texture_index = -1; _stream->read((char*)&material_mmd._sphere_op_mode, sizeof(uint8_t)); _stream->read((char*)&material_mmd._toon_mode, sizeof(uint8_t)); if (material_mmd._toon_mode == PMXToonMode::Common) { uint8_t tmp; _stream->read((char*)&tmp, sizeof(uint8_t)); toon_texture_index = tmp; } else { toon_texture_index = ReadIndex(_stream, _setting.texture_index_size); } auto memo = std::move(ReadString(_stream, _setting.encoding)); // 이건 무시했음 _stream->read((char*)&material->Get_IndexCount(), sizeof(int)); if (diffuse_texture_index >= 0) material->Set_Texture(_basePath + texturesPath[diffuse_texture_index], Material::Type_Texture::Diffuse); if (sphere_texture_index >= 0) material->Set_Texture(_basePath + texturesPath[sphere_texture_index], Material::Type_Texture::Sphere); if (toon_texture_index >= 0) { if (material_mmd._toon_mode == PMXToonMode::Separate) material->Set_Texture(_basePath + texturesPath[toon_texture_index], Material::Type_Texture::Toon); else if (material_mmd._toon_mode == PMXToonMode::Common) { std::wstringstream ss; ss << MMD_COMMON_DIRECTORYW + L"toon" << std::setfill(L'0') << std::setw(2) << (toon_texture_index + 1) << L".bmp"; material->Set_Texture(ss.str(), Material::Type_Texture::Toon); } } return true; }
mmd 는 자기만의 shader 를 사용하기 때문에 material 에 담긴 정보는 대부분 새로울 것이다.
1. Diffuse, Specular, Ambient
diffuse, specular, ambient 는 쉐이더에서 쓰는 우리가 친숙한 값이다.
mmd 에서 크게 중요한 값은 아닌게 대부분 diffuseTexture 로 덮이기 때문이다.
specularity 는 우리가 아는 제곱 몇번할지에 대한 그 값이다.
자세한건 아래에 적을 shader 를 읽어보자.
2. DrawMode
drawMode 는 여러 플래그가 들어있다.
/* 0x01:양면묘화 0x02:지면그림자 0x04:셀프 쉐도우맵에 묘화 0x08:셀프 쉐도우 묘화 0x10:엣지 묘화 0x20:정점 색상 ( 2.1 확장 ) 0x40:포인트 묘화 ( 2.1 확장 ) 0x80:라인 묘화 ( 2.1 확장) */ enum PMXDrawMode : uint8_t { BothFace = 0x01, GroundShadow = 0x02, CastSelfShadow = 0x04, RecieveSelfShadow = 0x08, DrawEdge = 0x10, VertexColor = 0x20, DrawPoint = 0x40, DrawLine = 0x80, };
쉐도우맵은 내가 따로 구현하지 않아서 잘 모르겠다.
여기서 꼭 챙겨야할 건 CullMode 를 말하는 BothFace 이다.
가끔 Face 하나만 가지고 양쪽을 표현해서 CallBack 등을 쓰면 투명한 경우가 있다.
DrawEdge 는 말 그대로 외각선그리기이다.
카툰렌더링에서 많이 쓰는 기법인 vertex 뒷부분을 조금 크게 그려서 렌더링된 모델과 합치는 기법을 사용한다.
edgeColor, edgeSize 가 여기서 사용된다.
자세한건 포프 아저씨한테 가자.
3. Texture
텍스쳐는 Diffuse, Sphere, Toon 을 사용한다.
앞에서 말했듯이 texturePath 을 읽은 순서에 따른 인덱스가 저장되어있다.
이때 뒤의 두가지는 fbx 등에서 사용하는 텍스쳐와 사용법이 매우 다르다.
자세한건 아래의 shader 를 살펴보자.
4. Sphere/Toon Mode
enum class PMXSphereMode: uint8_t { None = 0, // 무효 Mul, // 곱 Add, SubTexture, // 추가 uv1 의 x,y 를 참조해서 통상 텍스쳐 그리기 진행 }; enum class PMXToonMode : uint8_t { Separate, //!< 0:개별 툰 Common, //!< 1:공유 툰 Toon[0-9] toon01.bmp~toon10.bmp };
SphereMode 는 shader 내부에서 사용하는 값이다.
텍스쳐에서 빼낸 값을 더하거나 곱하거나를 결정한다.
ToonMode 는 shader 에서 쓰는게 아니라 toon texture 가 기본 어셋을 쓸지 따로 제공될지를 말한다.
mmd 파일이나 위의 mmd simulate 프로그램에서 toon01.bmp 를 찾아보면 있는데 이게 바로 Common 이 말하는 기본 어셋이다.
유의할 것은 toon_texture_index 가 0부터 시작하고 toon.bmp 는 1부터 시작하므로 1을 더해줘야한다는 것이다.
5. IndexCount
제일 중요한 값으로 적용시 material 의 순서를 고려해야한다.
읽어온 material 순서대로 (이전 누적합 IndexCount ~ 이전 누적합 IndexCount + 현재 IndexCount) 만큼의 index 범위를 말하기 때문이다.
3. Shader
float4 PS(Pixel_MASTER input) : SV_TARGET { float4 result = float4(1, 1, 1, 1); float3 normalDirection = input.normal; float4 albedo = float4(1, 1, 0, 1); float3 sphere = float3(0, 0, 0); float3 toon = float3(1, 1, 1); float3 diffuse = _diffuse; float3 specular = _specular; float3 ambient = _ambient; float3 cameraDir = normalize((float3) input.pos-_camera_pos); float3 lightDir = normalize(-_lightDirection); float3 viewNormal = normalize(mul(input.normal, (float3x3) _view)); #if DIFFUSE albedo = _diffuseMap.Sample(_sampler_, input.uv); result = albedo; #endif #if SPHERE float2 sphereUv = viewNormal.xy * 0.5f + 0.5f; sphere = _sphereMap.Sample(_sampler_, sphereUv); sphere = ComputeTexMulFactor(sphere, _sphere_mul_factor); sphere = ComputeTexAddFactor(sphere, _sphere_add_factor); switch (_sphere_op_mode) { case 1: result *= float4(sphere, 1); break; case 2: result += float4(sphere, 1); break; } #endif #if TOON float ln = dot(normalDirection, lightDir); ln = clamp(0.5 - ln, 0, 1); toon = _toonMap.Sample(_sampler_, float2(0.0, ln)); // _sampler 로 보간 안당하게 주의 toon = ComputeTexMulFactor(toon, _toon_mul_factor); toon = ComputeTexAddFactor(toon, _toon_add_factor); result *= float4(toon, 1); #endif if(_specularlity > 0) { float3 halfVec = normalize(cameraDir + lightDir); specular = _specular * _lightColor; specular *= pow(saturate(dot(normalDirection, halfVec)), _specularlity); } return result; }
위는 mmd 에서 사용하는 픽셀 쉐이더를 내가 조금 바꾼 것이다.
위 saba 레포지터리에 쉐도우까지 구현된게 들어있다.
이때 pos 는 world space 의 값이다.
sphere, toon 의 texture 를 어떻게 가져오는지 눈여겨보자.
// factor 가 0 일 수록 1,1,1,1 이 됨 float3 ComputeTexMulFactor(float3 texColor, float4 factor) { return lerp(float3(1.0, 1.0, 1.0), texColor * factor.rgb, factor.a); } // factor 가 0 일 수록 원래 값, 1일 수록 뭐가 더해지는데 ... float3 ComputeTexAddFactor(float3 texColor, float4 factor) { float3 ret = texColor + (texColor - float3(1, 1, 1)) * factor.a; ret = clamp(ret, float3(0, 0, 0), float3(1, 1, 1)) + factor.rgb; return ret; }
Compute~~Factor 는 위의 함수를 쓴다.
Pixel_MASTER VS(Vertex_MASTER input) { Pixel_MASTER output; float4x4 world = _world; float4x4 wvp_cur = _wvp_cur; float4x4 wvp_pref = _wvp_prev; #if BONE { world = bone_worlds[input.bone_index[0]] * input.bone_weight[0]; wvp_cur = bone_wvp_cur[input.bone_index[0]] * input.bone_weight[0]; wvp_pref = bone_wvp_before[input.bone_index[0]] * input.bone_weight[0]; for (int i = 1; i < 4; i++) { world += bone_worlds[input.bone_index[i]] * input.bone_weight[i]; wvp_cur += bone_wvp_cur[input.bone_index[i]] * input.bone_weight[i]; wvp_pref += bone_wvp_before[input.bone_index[i]] * input.bone_weight[i]; } } #endif output.position = mul(input.position, wvp_cur); // edge if (_draw_mode & 16) { float3 n = mul(input.normal, (float3x3) (_view * world)); float2 screenNor = normalize((float2) n); float2 offset = screenNor * _edge_size * output.position.w; offset.x /= _screenX * 0.5; offset.y /= _screenY * 0.5; output.position.xy += offset; output.color = _edge_color; } return output; } float4 PS(Pixel_MASTER input) : SV_TARGET { return float4(input.color.xyz, 1); }
위는 내가 edge 그릴 때 쓴 vs shader 이다.
offset 더할 때 view space 의 값을 사용한다.
이때 offset 을 온전한 project space 의 좌표로 만들기 위해 w 로 나눈다.
이 w 는 proj matrix 에서 z 값을 w 에 넣어주는걸 말한다.
screenX, screenY 를 나누는 것은 역시 project matrix 를 고려한 값이다.
orthonormal Project Matrix 에서 _11, _22 이 하는 일과 똑같다.
이 과정을 거치면 offset 은 온전한 project space 의 좌표가 되며 온전히 더할 수 있게 된다.
4. Skeleton
mmd 는 IK(Inverse Kinetic) 를 사용해 다리의 움직임을 표현한다.
또한 어떤 bone 은 다른 bone 에 자신의 일정 비율만큼의 변환행렬을 제공한다.
그래서 구현이 빡시다.
bool MMD_Importer::LoadSkeleton(std::shared_ptr<Skeleton> skeleton, std::vector<std::pair<int, int>>& bone_links_data) { int bone_count = 0; _stream->read((char*)&bone_count, sizeof(int)); std::wstring bone_name; std::wstring bone_english_name; for (int i = 0; i < bone_count; i++) { bone_name = std::move(ReadString(_stream, _setting.encoding)); bone_english_name = std::move(ReadString(_stream, _setting.encoding)); PMXBoneFlags bone_flag = PMXBoneFlags::AllowControl; int parent_index = -1; Vector3 pos = 0; Vector3 offset = 0; Matrix rotate = Matrix::identity; auto& bone = skeleton->AddBone(i, Matrix::identity, bone_name); _stream->read((char*)&pos, sizeof(float) * 3); bone.offset = Matrix::PositionToMatrix(pos).Inverse_RT(); // offset 은 T 기준이라서 얘가 맞음 bone.parent_index = ReadIndex(_stream, _setting.bone_index_size); int target_index = -1; int level = 0; int key = 0; int ik_target_bone_index = 0; int ik_loop = 0; float ik_loop_angle_limit = 0; Vector3 lock_axis_orientation; Vector3 local_x_orientation; Vector3 local_y_orientation; _stream->read((char*)&level, sizeof(int)); _stream->read((char*)&bone_flag, sizeof(uint16_t)); if (bone_flag & PMXBoneFlags::TargetShowMode) { // 해결 target_index = ReadIndex(_stream, _setting.bone_index_size); } else { // 잘 모르겠음 _stream->read((char*)&offset, sizeof(float) * 3); } if (bone_flag & (PMXBoneFlags::AppendRotate | PMXBoneFlags::AppendTranslate)) { bone.append_index = ReadIndex(_stream, _setting.bone_index_size); _stream->read((char*)&bone.append_weight, sizeof(float)); } if (bone_flag & PMXBoneFlags::FixedAxis) { // 허리 같은 거나 해당되고 값이 없음. 안쓰이는 듯. _stream->read((char*)&lock_axis_orientation, sizeof(float) * 3); } if (bone_flag & PMXBoneFlags::LocalAxis) { // axis orientation. 손가락만 해당되고 쓰면 이상해짐. 안쓰이는 듯 _stream->read((char*)&local_x_orientation, sizeof(float) * 3); _stream->read((char*)&local_y_orientation, sizeof(float) * 3); } if (bone_flag & PMXBoneFlags::DeformOuterParent) { // kyaru 에선 값이 없음. 안쓰이는 듯 _stream->read((char*)&key, sizeof(int)); } if (bone_flag & PMXBoneFlags::IK) { bone.ikTargetBone_index = ReadIndex(_stream, _setting.bone_index_size); _stream->read((char*)&bone.ikIterationCount, sizeof(int)); _stream->read((char*)&bone.ikItertationAngleLimit, sizeof(float)); int ik_link_count = 0; _stream->read((char*)&ik_link_count, sizeof(int)); for (int i = 0; i < ik_link_count; i++) { auto& iklink = bone.AddIKLink(); iklink.ikBoneIndex = ReadIndex(_stream, _setting.bone_index_size); _stream->read((char*)&iklink.enableAxisLimit, sizeof(uint8_t)); if (iklink.enableAxisLimit == 1) { _stream->read((char*)&iklink.limitMin, sizeof(float) * 3); _stream->read((char*)&iklink.limitMax, sizeof(float) * 3); } } } bone_links_data.push_back({ bone.parent_index, bone.index }); } return true; }
1. bone identifier
주의할 것이 bone name 이 animation 에서 identifier 로 사용되고, vertex 에선 bone index 가 사용된다는 것이다.
둘다 저장하자.
2. offset, parent
제일 중요한 값은 pos 와 parent index 이다.
pos 는 global space 의 좌표로 나는 이걸 행렬로 바꾸고 역행렬로 해서 저장했다.
그러면 mesh 들이 local space 로 들어가게 할 행렬이 되기 때문이다.
parent index 는 말 그대로이다.
3. bone 종류
level 은 나는 사용하지 않았고, bone_flag 가 나머지 bone 들의 특성을 정한다.
이 중에서 반절은 쓰지 않아도 모델을 구현하는데 크게 영향이 없다.
append index / weight, IK Bone 관련 값만 있어도 충분하다.
이때 Bone 은 다음과 같은 순서로 변환된다는 것을 숙지하자.
Local -> Animation -> IK Rotation -> Append Bone's Animation * weight
4. Appended Bone
이때 append index / weight 가 사용된다.
animation 은 pos, rot 만 쓰이기 때문에 그것들만 가져와서 추가로 변환해주면 된다.
이건 보간이 아니므로 주의.
5. IK Bone
IK Bone 은 MMD 에선 휴리스틱 방식으로 구현되었다.
그 종특으로 다리가 가끔 이상해지는데 Jacobian 을 써서 수정해도 된다.
나는 3DF 용 야매 Jacobian 을 써서 정확도가 좀 떨어지는 알고리즘을 사용했다.
여기서는 변수만 설명해두겠다.
ikTargetIndex 는 우리가 변화시킬 끝 점을 의미한다.
이때 현재 Bone 의 인덱스는 주어진 끝 점을 말한다.
IK 는 변화시킬 끝 점을 주어진 끝 점으로 옮기는 프로세스라는 것을 상기하자.
IterateCount 은 휴리스틱 알고리즘에서 최대 몇번 점근적 반복을 할지 말한다.
IterateAngleLimit 은 ikChain 을 기준으로 주어진 끝점과 변화시킬 끝점의 최대 각도를 말한다. 휴리스틱 알고리즘은 이 각도를 매 반복마다 점점 줄이는 것을 목표로 하는데, 이 값이 너무 크면 알고리즘이 작동을 안할 수 있다. 그래서 위의 값으로 Clamp 를 거쳐서 그 각도를 사용한다.
ikBoneIndex 는 ikChain 에 사용될 bone index 이다. 예를들어 손이 ikTargetIndex 고 사탕이 현재 bone 의 index 라고 하면, ikChain 은 팔꿈치와 어깨의 관절이며, 이 관절에 해당되는 bone 을 말하는데 ikBoneIndex 인 것이다.
enableAxisLimit 은 각 관절의 축이 제한되었는지를 말한다. 1 이 아니면 어깨같은 3dof 가 된다.
limitMin/Max 는 축을 결정지으며 동시에 회전 한계를 말한다. MinMax 둘다 0의 경우 축이 닫혀있음을 말한다.
IKBone 은 구현이 빡세기 때문에 내 코드나 saba repository 를 참고하자.
mmd 를 분석하면서 이에 대해서 내가 따로 정리한 글도 있다.
https://moksha1905.blogspot.com/2020/11/inverse-kinematicsik-iterate-method.html
5. Morph
대개 얼굴 표정에 사용되며 vertex pos 나 uv 의 offset 을 주로 사용한다.
material offset, bone offset 도 있긴 하다.
bool MMD_Importer::LoadMorph(Renderable* renderable) { auto mgr = _context->GetSubsystem<ResourceManager>(); auto morphs = std::make_shared<Morphs>(_context); int morph_count = 0; _stream->read((char*)&morph_count, sizeof(int)); for (int i = 0; i < morph_count; i++) { auto morph = morphs->AddMorph(); std::wstring morph_name; std::wstring morph_english_name; pmx::MorphCategory category; pmx::MorphType morph_type; int offset_count; morph_name = ReadString(_stream, _setting.encoding); morph_english_name = ReadString(_stream, _setting.encoding); _stream->read((char*)&category, sizeof(pmx::MorphCategory)); _stream->read((char*)&morph_type, sizeof(pmx::MorphType)); _stream->read((char*)&offset_count, sizeof(int)); morph->Set_MorphName(morph_name); morph->Set_MorphCategory(static_cast<Morph::MorphCategory>(category)); switch (morph_type) { case pmx::MorphType::Group: { PmxMorphGroupOffset offset; auto& dst_offsets = morph->Get_GroupOffsets(); for (int i = 0; i < offset_count; i++) { offset.Read(_stream, &_setting); dst_offsets.push_back({ offset.morph_index, offset.morph_weight }); } morph->Set_MorphType(Morph::MorphType::Group); } break; case pmx::MorphType::Vertex: { PmxMorphVertexOffset offset; auto& dst_offsets = morph->Get_VertexOffsets(); for (int i = 0; i < offset_count; i++) { offset.Read(_stream, &_setting); Vector3 tmp = Vector3(offset.position_offset[0], offset.position_offset[1], offset.position_offset[2]); dst_offsets.push_back({ offset.vertex_index, tmp}); } morph->Set_MorphType(Morph::MorphType::Vertex); // 1:1 대응으로 안해놔서 따로따로 해야함 break; } case pmx::MorphType::Bone: { auto bone_offsets = std::make_unique<PmxMorphBoneOffset[]>(offset_count); for (int i = 0; i < offset_count; i++) { bone_offsets[i].Read(_stream, &_setting); } morph->Set_MorphType(Morph::MorphType::Bone); break; } case pmx::MorphType::Matrial: { PmxMorphMaterialOffset offset; auto& dst_offsets = morph->Get_MaterialOffsets(); for (int i = 0; i < offset_count; i++) { offset.Read(_stream, &_setting); auto& dst = dst_offsets.emplace_back(); { dst.material_index = offset.material_index; dst.offset_operation = offset.offset_operation; dst.diffuse = Color4(offset.diffuse[0], offset.diffuse[1], offset.diffuse[2], offset.diffuse[3]); dst.specular = Vector3(offset.specular[0], offset.specular[1], offset.specular[2]); dst.specularity = offset.specularity; dst.ambient = Vector3(offset.ambient[0], offset.ambient[1], offset.ambient[2]); dst.edge_color = Color4(offset.edge_color[0], offset.edge_color[1], offset.edge_color[2], offset.edge_color[3]); dst.edge_size = offset.edge_size; dst.texture_rgba = Color4(offset.texture_argb[1], offset.texture_argb[2], offset.texture_argb[3], offset.texture_argb[0]); dst.sphere_texture_rgba = Color4(offset.sphere_texture_argb[1], offset.sphere_texture_argb[2], offset.sphere_texture_argb[3], offset.sphere_texture_argb[0]); dst.toon_texture_rgba = Color4(offset.toon_texture_argb[1], offset.toon_texture_argb[2], offset.toon_texture_argb[3], offset.toon_texture_argb[0]); } } morph->Set_MorphType(Morph::MorphType::Matrial); break; } case pmx::MorphType::UV: case pmx::MorphType::AdditionalUV1: case pmx::MorphType::AdditionalUV2: case pmx::MorphType::AdditionalUV3: case pmx::MorphType::AdditionalUV4: { PmxMorphUVOffset offset; auto dst_offsets = morph->Get_UVOffsets(); for (int i = 0; i < offset_count; i++) { offset.Read(_stream, &_setting); dst_offsets.push_back({ offset.vertex_index, Vector2(offset.uv_offset[0], offset.uv_offset[1]) }); } morph->Set_MorphType(Morph::MorphType::UV); break; } default: throw; } } mgr->RegisterResource(morphs, _basePathName + Extension_MorphW); renderable->SetMorphs(morphs->GetPath()); }
Animation 에서 Morph Offset 을 이름을 Identifier 로 사용하므로 주의하자.
나머지는 평범한 Morph Animation 이다.