https://www.inflearn.com/course/lecture?courseSlug=directx11-%EA%B2%8C%EC%9E%84%EA%B0%9C%EB%B0%9C-%EB%8F%84%EC%95%BD%EB%B0%98&unitId=161145

 

학습 페이지

 

www.inflearn.com

 

 

이제 불러온 모델을 통해 애니메이션을 구현해보자 

1.이론

2D 애니메이션이었다면 우리가 여러 스프라이트를 틀어주는 방식으로 구현할 수 있을 것이다. 하지만 3D에서 이런방식

으로 구현하기에는 효율적이지 않기 때문에 각 노드, 뼈를 움직이는 것으 애니메이션을 구현한다.

이때 한 뼈가 움직인다면 다른 뼈를 어떻게 처리해줄 것인지가 중요하다. 여기서 우리가 전에 사용했던 Transform을 계층관계를 통해 다시 사용하게 된다. 

여기서 우리가 처음에 불러온 정점을 사용하게 된다. 이 정점은 초기 자세에서의 글로벌 좌표이다. 이것을 역행렬을 통해 다시 상대좌표로 바꾼다음 이  상대좌표를 바꾸고 다시 글로벌 좌표로 바꾸면 되는 것이다.

하지만 이 정점을 모두 움직이기에는 용량이 너무 크기 때문에 정점을 뼈대에 따라 움직이게 해주면 된다. 한 정점이 여러 뼈대에 영향을 받을 수 있는데 이럴 때는 비율을 정해두고 영향을 받도록 한다. 이것을 스키닝이라고 한다.

 

2.스키닝

일단 애니메이션에 들어가기에 앞서 스키닝에 대한 실습을 진행해보자.

스킨은 자신이 어떠한 뼈에 영향을 받아서 움직일 것인지에 대한 정보를 말한다.

이때 스킨 정보에 관련된 변수는 만들어두었던 VertexData의 구조체에서 변수 중에 blendIndicies는 뼈대의 번호 blendWeights는 그 뼈대에 얼마나 영향을 받을 것인가를 저장해준다. 

우선 스킨에 대한 정보를 추출해야한다 매쉬를 순회하면서 뼈대가 있다면 본을 순회하면서 연관된 정점과 가중치 정보를 전달해주어야하는데 Assimp에서는 이 정보가 정점이 아닌 뼈대에 들어있기 때문에 이 정보를 추출해서 정점에 Parsing하는 작업이 필요하다.

이를 위해 정점마다 뼈번호와, 가중치를 저장할 구조체를 선언해주고 이를 파싱해줄 구조체도 만들어주자.

AsTypes.h

//Animation

struct asBlendWeight
{
	//뼈 목록
	Vec4 indices = Vec4(0, 0, 0, 0);
	Vec4 weights = Vec4(0, 0, 0, 0);

	void Set(uint32 index, uint32 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;
		}
	}
};

//영향을 주는 각 뼈들 - 정점마다 - (뼈번호, 가중치)
struct asBoneWeights
{
	void AddWeights(uint32 boneIndex, float weight)
	{
		if (weight <= 0.0f)
			return;
		
		//weight 값이 나보다 작은애가 있는지 찾는다.-> 위치찾기
		auto findIt = std::find_if(boneWeights.begin(), boneWeights.end(),
			[weight](const Pair& p) {return weight > p.second; });

		//그 위치앞에 추가
		boneWeights.insert(findIt, Pair(boneIndex, weight));
	}

	asBlendWeight GetBlendWeight()
	{
		asBlendWeight blendWeights;

		for (uint32 i = 0; i < boneWeights.size(); i++)
		{
			if (i >= 4)
				break;

			//가중치설정
			blendWeights.Set(i, boneWeights[i].first, boneWeights[i].second);
		}

		return blendWeights;
	}

	// 정규화 합 1로 맞춰주기 
	void Normalize()
	{
		if (boneWeights.size() >= 4)
			boneWeights.resize(4);

		float totalWeight = 0.f;
		for (const auto& item : boneWeights)
			totalWeight += item.second;

		float scale = 1.f / totalWeight;
		for (const auto& item : boneWeights)
			item.second *= scale;
	}

	using Pair = pair<int32, float>;
	vector<Pair> boneWeights;

};

 

이 구조체를 사용하여 정점마다 뼈대 가중치 정보를 먼저 가져오고이를 정점마다 4개의 뼈/ 가중치 목록으로 채워주는 함수를 완성해주자.

Converter.cpp

void Converter::ReadSkinData()
{
	for (uint32 i = 0; i < _scene->mNumMeshes; i++)
	{
		aiMesh* srcMesh = _scene->mMeshes[i];
		if (srcMesh->HasBones() == false)
			continue; 

		shared_ptr<asMesh> mesh = _meshes[i];

		vector<asBoneWeights> tempVertexBoneWeights;
		tempVertexBoneWeights.resize(mesh->vertices.size());

		//Bone 순회 -> 연관된 VertexId, Weight 구해서 기록
		for (uint32 b = 0; b < srcMesh->mNumBones; b++)
		{
			aiBone* srcMeshBone = srcMesh->mBones[b];
			uint32 boneIndex = GetBoneIndex(srcMeshBone->mName.C_Str());

			for (uint32 w = 0; w < srcMeshBone->mNumWeights; w++)
			{
				uint32 index = srcMeshBone->mWeights[w].mVertexId;
				float weight = srcMeshBone->mWeights[w].mWeight;

				//정점마다 가중치 넣어주기
				tempVertexBoneWeights[index].AddWeights(boneIndex, weight);
			}
		}

		//최종 결과 계산 - 정점에 있는 뼈의 번호로 정리
		for (uint32 v = 0; v < tempVertexBoneWeights.size(); v++)
		{
			tempVertexBoneWeights[v].Normalize();

			asBlendWeight blendWeight = tempVertexBoneWeights[v].GetBlendWeight();
			mesh->vertices[v].blendIndices = blendWeight.indices;
			mesh->vertices[v].blendWeights = blendWeight.weights;
		}
	}
}

 

이렇게 해주면 정점마다 가중치정보가 저장되는데 이를 확인하기 위해 이 정보를 모두 엑셀파일에 저장하고 이 파일을 확인해보자.

Converter.cpp

void Converter::ExportModelData(wstring savePath)
{
	wstring finalPath = _modelPath + savePath + L".mesh";
	ReadModelData(_scene->mRootNode, -1, -1);		//메모리 -> CustomData
	ReadSkinData();

	//Write CSV File
	{
		FILE* file;
		::fopen_s(&file, "../Vertices.csv", "w");

		for (shared_ptr<asBone>& bone : _bones)
		{
			string name = bone->name;
			::fprintf(file, "%d,%s\n", bone->index, bone->name.c_str());
		}

		::fprintf(file, "\n");

		for (shared_ptr<asMesh>& mesh : _meshes)
		{
			string name = mesh->name;
			::printf("%s\n", name.c_str());

			for (UINT i = 0; i < mesh->vertices.size(); i++)
			{
				Vec3 p = mesh->vertices[i].position;
				Vec4 indices = mesh->vertices[i].blendIndices;
				Vec4 weights = mesh->vertices[i].blendWeights;

				::fprintf(file, "%f,%f,%f,", p.x, p.y, p.z);
				::fprintf(file, "%f,%f,%f,%f,", indices.x, indices.y, indices.z, indices.w);
				::fprintf(file, "%f,%f,%f,%f\n", weights.x, weights.y, weights.z, weights.w);
			}
		}

		::fclose(file);
	}

	WriteModelFile(finalPath);		//메모리 -> 파일
}

이렇게 해주면 정보가 담긴 엑셀파일이 생겨있는것을 볼 수 있다.

 

 

이제 뼈대를 움직일 때 이 정보를 바탕으로 정점을 움직여주면 된다. 이제 관절의 움직임과 전체적인 애니메이션 데이터를 가지고 와서 사용해주면 될것같다.

+ Recent posts