2021년 1월 20일 수요일

MMD 파일 구조 - pmx

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 가 여기서 사용된다.

https://gamedevforever.com/18 

자세한건 포프 아저씨한테 가자.


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 이다.


List