본문 바로가기

DirectX11 3D/기본 문법

<DirectX11 3D> 41 - Animation (Mesh Skinning)

필요한 개념


- Skinning(스키닝) : 정점을 땡겨서 비어있는 피부를 매꿔주는 기법

 

Skinning 설명

 

자기 주변에 있는 Bone(*인간형 기준 2 ~ 3개) 영향을 줌

ex) 허벅지가 영향을 주는 본은 위, 아래 본(0, 1)번을 준다.

 

개념이해 예시 1)
1번 본에게 영향을 0.5만큼 받는다고 치면

BlendIndices.x -> 1(본의 번호)
BlendWeights.x -> 0.5(가중치 값)

Matrix
1 0 0 0    x 0.5   0.5 0 0 0
0 1 0 0     ->     0 0.5 0 0
0 0 1 0             0 0 0.5 0
2 0 0 1             1 0 0 0.5(전체 Matrix에 대한 비율)

_44는 전체 Matrix에 대한 비율이어서 전체 Matrix가 다시 x 0.5만큼 더 줄어든다.

그 나온 값을 이동시키겠다는 것이다.

1번 본에 영향을 받으니까, 1번 본쪽으로 나온 값만큼 이동한다.(즉, 1번본쪽 방향으로 조금 이동하게 된다.)

개념이해 예시 2)
BlendIndices.x -> 0(본의 번호)
BlendWeights.x -> 0.5(가중치 값)


BlendIndices.y -> 1(본의 번호)
BlendWeights.y -> 0.5(가중치 값)
이라 한다면

0번 본쪽으로 가중치 0.5만큼 이동하고, 그 후 1번 본쪽으로 0.5 만큼 이동한다.

이 값을 가져올 것이다.

왜 4개만(x,y,z,w) 사용하나? 애니메이션을 할때에는 특별한 몬스터를 고려하지 않고, 사람모양을 쓸 것이다. 사람모양은
생긴것이 관절이 한 접점이 4개가 된 곳은 없다. bone이 영향을 받을 곳이 많아야 2개 ~ 3개이다.(일반적으로 2개, 디자인에 따라 3개가 될수도)
보통 최대해봐야 4개(그리고 shader로 넘길때 float4로 해야 편하다.)
8개 정도면, 문어나 오징어는 나와야함
만약 8개로 하고 싶다면, float4 Indices0, float4 Indices1, float4 Weights0, float4 Weights1 이런식으로 넘기면 된다.


*중요
BlendWeights의 x,y,z,w의 총합은 반드시 1이어야 한다. 1의 비율로 다루기 때문에(4, 8개 여도 총합은 1이다.)


이 정보들을 가져와서, 정점 정보에 세팅해줄 것이다. 영향을 받을 bone, mesh를 일치시켜서 가져와야 한다

* Converter.h, cpp 수정해서 정점 정보 넣을 때 넣을 수 있도록 수정
Types.h 수정, ExportFile.cpp 수정

요약
정점을 이동시켜 피부를 메꿔줄 가중치 정보 읽기
정점과 매칭시켜 저장하기
가중치 정보를 csv파일로 저장

 

 

- Model에서 Skinning 데이터를 불러와서 다음에 할 애니메이션 데이터를 적용시키는 것이다. 

 

 

구조 분석


 

 

 

Converter.h 추가된 부분


더보기
void ReadSkinData();

 

 

 

 

 

 

 

Converter.Cpp 추가된 부분


더보기
// Bone 데이터 읽고 나서 바로 skin 데이터 읽기

void Converter::ReadSkinData()
{
	// Mesh 기준으로 일치 시킬 것이다.
	for (UINT i = 0; i < scene->mNumMeshes; i++)
	{
		aiMesh* aiMesh = scene->mMeshes[i];
		// 해당 매쉬가 본을 가지고 있지 않다면 안써준다.
		if (aiMesh->HasBones() == false) continue;

		asMesh* mesh = meshes[i];

		vector<asBoneWeights> boneWeights;
		// 미리 크기를 잡아놈
		boneWeights.assign(mesh->Vertices.size(), asBoneWeights());
		// stl의 array 사용해도됨(vector 말고)

		for (UINT b = 0; b < aiMesh->mNumBones; b++)
		{
			// Mesh가 가지고 있는 Bone을 가져옴
			aiBone* aiMeshBone = aiMesh->mBones[b];

			UINT boneIndex = 0;
			// 우리가 가지고 있는 본의 이름과 메쉬 본의 일치되는 것을 찾아옴
			for (asBone* bone : bones)
			{
				// 이름이 같다면 인덱스를 가져옴
				if (bone->Name == (string)aiMeshBone->mName.C_Str())
				{
					boneIndex = bone->Index;
					break;
				}
			}//for(bone)

			// 가중치를 가져옴
			for (UINT w = 0; w < aiMeshBone->mNumWeights; w++)
			{
				// 영향을 받을 정점 id이다.
				UINT index = aiMeshBone->mWeights[w].mVertexId;

				float weight = aiMeshBone->mWeights[w].mWeight;

				// 위에서 assign으로 잡아놔서, 정점의 번호랑 index일치함
				// 예를 들어 0번 정점에다가 영향을 받을 본 번호, 가중치를 넣어줌
				boneWeights[index].AddWeights(boneIndex, weight);

			}
		
		}//for(b)


		// 가중치를 직접 정점에 넣어줌
		for (UINT w = 0; w < boneWeights.size(); w++)
		{
			// 크기를 나눠서 1로 만듬
			boneWeights[w].Normalize();

			asBlendWeight blendWeight;
			// blendWeight 꺼내옴
			boneWeights[w].GetBlendWeights(blendWeight);

			// 정점의 번호와 일치한다.
			mesh->Vertices[w].BlendIndices = blendWeight.Indices;
			mesh->Vertices[w].BlendWeights = blendWeight.Weights;
		}

	}
}

 

 

 

 

 

 

 

 

Types.h


더보기
// 최종적으로 저장할 때 사용
struct asBlendWeight
{
	Vector4 Indices = Vector4(0, 0, 0, 0);
	Vector4 Weights = Vector4(0, 0, 0, 0);

	void Set(UINT index, UINT boneIndex, float weight)
	{
		float i = (float)boneIndex;
		float w = weight;

		switch (index)
		{
		case 0: Indices.x = i; Weights.x = w; break;
		case 1: Indices.y = i; Weights.y = w; break;
		case 2: Indices.z = i; Weights.z = w; break;
		case 3: Indices.w = i; Weights.w = w; break;
		}
	}
};

// 우리가 읽어온 것을 여기다 넣어서 정리(int 4개, float 4개) 총 4개 Pair로 넣어주고
// asBlendWeight에다가 넣어주는 역할 이다.
struct asBoneWeights
{
private:
	typedef pair<int, float> Pair;
	vector<Pair> BoneWeights;

public:
	void AddWeights(UINT boneIndex, float boneWeights)
	{
		/*
			즉, 가중치가 큰 값이 앞에 나오도록 세팅
			-> 3D Model 규칙이다.
		*/

		// 번호 비교해가면서 넣도록
		if (boneWeights <= 0.0f) return;

		bool bInsert = false;
		vector<Pair>::iterator it = BoneWeights.begin();
		while (it != BoneWeights.end())
		{
			// 이전에 있던 것 보다 작을 때만 insert해줌
			if (boneWeights > it->second)
			{
				// 중간에 insert해줌, 나머지 뒤로 밀림
				BoneWeights.insert(it, Pair(boneIndex, boneWeights));
				bInsert = true;

				break;
			}

			it++;
		} // while(it)

		// 큰게 하나도 없었다면 Insert가 아니라면 맨 뒤에 새로 추가
		if (bInsert == false)
			BoneWeights.push_back(Pair(boneIndex, boneWeights));
	}

	// 우리가 해논 데이터를 정점에다가 넣어줌
	void GetBlendWeights(asBlendWeight& blendWeights)
	{
		for (UINT i = 0; i < BoneWeights.size(); i++)
		{
			// 4개 까지만
			if (i >= 4) return;

			blendWeights.Set(i, BoneWeights[i].first, BoneWeights[i].second);
		}
	}

	// 크기를 1로 만들어줌(총합은 반드시 1이어야 함)
	void Normalize()
	{
		float totalWeight = 0.0f;

		int i = 0;
		vector<Pair>::iterator it = BoneWeights.begin();
		while (it != BoneWeights.end())
		{
			// 4개까지만
			if (i < 4)
			{
				totalWeight += it->second;
				i++; it++;
			}
			else
				it = BoneWeights.erase(it);
		}

		float scale = 1.0f / totalWeight;

		it = BoneWeights.begin();
		while (it != BoneWeights.end())
		{
        	// 크기만큼 줄여줌(정규화)
			it->second *= scale;
			it++;
		}
	}
};