본문 바로가기

DirectX11 3D/기본 문법

<Directx11 3D> 42 - Animation Clip Read & Write

 

필요한 개념


애니메이션을 읽으려면 애니메이션 데이터가 있어야 한다.
Mixamo.com에 많다.

3D 인간형 모델의 기본 디자인은 T-Pose로 되어 있습니다.(엔진에서 거의 그렇게 사용됨)
그래서 모델 다운시에 기본은 T-Pose로 다운하자

애니메이션 리타케팅이란?
같은 모델의 애니메이션이 아니어도 쓸 수 있도록 해주는 기술

애니메이션은 본들이 움직이는 것이다. 본 이름이 애니메이션과 Mesh쪽이 일치해야 한다. 그런데 Mixamo는 자동으로
세팅되게 처리되어있다.

 

더보기

*Tip

<Mixamo 페이지 애니메이션 속성 정리>

 

Override -> 속도 정리
space -> 간격

다운로드 시
With Skin -> 피부를 가지고 있다는 의미, 여기서 skin은 mesh를 의미
Without Skin
우리는 mesh를 미리 받아놓아서 withoutskin(메쉬없는 모델)을 씀

Frame Per Second -> 30프레임을 많이씀(게임 프레임 의미)
KeyFrame Reduction : 키 프레임 최적화 의미 -> 키 프레임의 길이 비율을 의미(너무 긴 애니메이션 프레임을 짤라냄,
정확하지는 않아서 none 해서 원래 프레임으로 세팅)


Mixamo에서 Run 애니메이션보면 Root 본이 움직인다. 그런데 이렇게 프로그래밍은 어렵다. 그래서 InPlace로 세팅



이제까지 세팅한걸 보면 Bone개수는 250로 세팅했고, 프레임은 최대 500으로 설정할 예정임
나중에 더 늘리고 싶다면 늘리면 됨

* 주의
Clip : 애니메이션 한동작을 의미, Assimp에서는 Animation
1) Mixamo에서 받는 파일들은 한 파일당 하나로 분리되어있다.
2) 근데 어떤 파일은 fbx파일에 여러개의 Clip으로 몰아넣은 것도 있다.

 

 

<Read Clip Data>

 

AnimationClip 설명


ModelMesh가 ModelBone을 참조해서 ModelBone의 위치로 그리고 있다.
ModelBone은 시간에 따라 움직일 것이다. 시간에 따라 움직이는 것을 우리가 불러올 AnimationClip에 저장되어 있다.
AnimationClip에는 본별로 이름이 있고 20프레임이라면 20프레임에 대한 SRT를 저장할 것이다.
또다른 본도 있다면 마찬가지로 들고 있는다.

이때 ModelBone과 AnimationClip의 Bone 이름을 일치시켜야 한다.
해당 ModelBone이 그려질 때 시간에 따라 해당 프레임의 정보로 ModelBone이 움직인다.
이렇게 키프레임으로 저장한 애니메이션을 키프레임 애니메이션이라함

키프레임 애니메이션이란?
키프레임 별로 저장한 애니메이션, 실무에서는 애니메이션이 변하는 시점의 키프레임만 저장하지만,
강의에서는 모든 키프레임을 저장한다.

예를 들어 사람이 있다면 10프레임에 팔만 움직였다면 실무에서는 10프레임에 팔만 움직였다는 것만 저장하지만,
우리는 전체부분을 저장한다.(교육이어서)

TIP!
정석 코드와 현재 코드는 약간 차이가 있다. 이 코드는 애니메이션을 이해하기 위한 코드, 애니메이션을 이해하기 위한
코드이다.

struct aiVectorKey
{
    /** The time of this key */
    double mTime; -> 키가 변화된 시간

    /** The value of this key */
    C_STRUCT aiVector3D mValue; -> 해당 시간에 얼마만큼 변했는지, 이동했는지(x, y, z)
}

요약
ClipData 읽기
Animation->mChannel 읽어 asClipNode에 저장

<Read Keyframe Data>
aiNode와 aniNodeInfo의 본 이름을 일치시켜 데이터 저장하기
Clip 정보를 파일로 쓰기
Model 클래스에서 .clip 파일 읽기

 

 

 

Converter 함수 흐름 정리


Converter 함수&nbsp;흐름 정리

 

 

 

 

 

Converter.h 추가된 부분


더보기
public:
	// 파일로 부터 ClipList를 얻어옴
	void ClipList(vector<wstring>* list);
	void ExportAnimClip(UINT index, wstring savePath);

private:
	// 들어오는 클립들 하나씩 써주기 위해 리턴함(따로 맴버로 저장 x)
	struct asClip* ReadClipData(aiAnimation* animation);
	void ReadKeyFrameData(struct asClip* clip, aiNode* node, vector<struct asClipNode>& aiNodeInfos);
	void WriteClipData(struct asClip* clip, wstring savePath);

 

 

 

 

 

 

 

Converter.Cpp 추가된 부분


 

더보기
// Clip이름을 얻어옴
// Fbx에 클립이 몇개 있는지 알 수 없다. 그래서 클립의 개수를 반환해주도록 만듬
void Converter::ClipList(vector<wstring>* list)
{
	// Animations -> clip의 갯수 Assimp는 clip을 Animation라고 부름
	// scene에 animation(clip) 정보가 있다.
	for (UINT i = 0; i < scene->mNumAnimations; i++)
	{
		aiAnimation* anim = scene->mAnimations[i];

		list->push_back(String::ToWString(anim->mName.C_Str()));
	}
}

void Converter::ExportAnimClip(UINT index, wstring savePath)
{
	savePath = L"../../_Models/" + savePath + L".clip";

	asClip* clip = ReadClipData(scene->mAnimations[index]);
	WriteClipData(clip, savePath);
}

asClip* Converter::ReadClipData(aiAnimation* animation)
{
	asClip* clip = new asClip();
	clip->Name = animation->mName.C_Str();
	// 프레임 비율
	// 우리가 mixamo에서 30프레임으로 다운 받아서 30프레임이 리턴됨
	clip->FrameRate = (float)animation->mTicksPerSecond;
	// animation->mDuration -> 애니메이션의 프레임 길이
	// +1을 하는 이유는? double->uint로 하기 때문에 30.1 프레임일수도 있어서
	// 31로 한다.
	clip->FrameCount = (UINT)animation->mDuration + 1;

	vector<asClipNode> aniNodeInfos;
	// Channel은 각 노드를 가지고 있다. 채널이 본정보라고 생각하면 된다.
	for (UINT i = 0; i < animation->mNumChannels; i++)
	{
		// 애니메이션에 있는 노드들을 가져옴
		aiNodeAnim* aniNode = animation->mChannels[i];

		asClipNode aniNodeInfo;
		aniNodeInfo.Name = aniNode->mNodeName;

		// 키 갯수
		// 변하든 변하지 않든 다 저장할 것이라고 함
		// 어느 프레임에서 값이 변했다면, 그 이후로 안변했다면
		// 뒤의 프레임 정보가 없다. 그래서 없는 곳의 값을 채워넣을 것이다.
		
		// 따로 저장되어 있지 않기 때문에 값이 변하기 전까지의 값을 계산해서
		// 저장
		// srt중에 큰 키 값을 찾을려고 함
		UINT keyCount = max(aniNode->mNumPositionKeys, aniNode->mNumScalingKeys);
		keyCount = max(keyCount, aniNode->mNumRotationKeys);

		for (UINT k = 0; k < keyCount; k++)
		{
			asKeyframeData frameData;

			// 키를 찾았는지
			bool bFound = false;

			// t에 현재 프레임을 저장
			UINT t = aniNodeInfo.Keyframe.size();

			// t = 0, 1, 2, 3
			// 키 값들을 찾아줌
			// fabsf() : float형 실수 절대값 반환
			// 나온 절대 값이 D3DX_16F_EPSILON(2바이트 float에 대한 오차값)보다 작거나 같다면 0이라고 간주(그 프레임이라고 간주)
			// ex) 자동차 프로그래밍 한다면 가속하면 *로 올라가고 감속하면 *로 -로 줄인다.
			// 근데 그게 0이 될까? 0이 될 확률은 거의 없고, 0에 가깝게 된다.(오차 범위를 줘서 0.1보다 작다면 0이라고 해라.. 등)
			// D3DX_16F_EPSILON의 10의 -6승을 사용함(0.000001) 정도
			// 물론 1e-6 해도 되지만, 둘다 차이는 없는데
			// Assimp 예제에서는 D3DX_16F_EPSILON로 많이 사용
            
            		// 현재 프레임(t)와 Position의 프레임을 빼서 EPSILON보다 작다면 같은 프레임으로 간주
			if (fabsf((float)aniNode->mPositionKeys[k].mTime - (float)t) <= D3DX_16F_EPSILON)
			{
				// 키 값을 가져옴
				aiVectorKey key = aniNode->mPositionKeys[k];


				frameData.Time = (float)key.mTime;
				// 근처의 키를 찾았다면 키 프레임에 써줌
				memcpy_s(&frameData.Translation, sizeof(Vector3), &key.mValue, sizeof(aiVector3D));
			
				bFound = true;
			}

			// 애니메이션에서는 회전은 쿼터니언을 사용
			// 회전행렬은 Vector값으로 저장하면 약간 오차를 발생, 짐벌락 발생가능성
			// 행렬은 연산량이 많지만 얘는 x,y,z,w 4가지만 가짐
			// x,y,z은 회전에 대한 값, w는 회전값에 대한 비율
			if (fabsf((float)aniNode->mRotationKeys[k].mTime - (float)t) <= D3DX_16F_EPSILON)
			{
				// 쿼터니언은 aiQuatKey 타입을 가짐
				aiQuatKey key = aniNode->mRotationKeys[k];
				frameData.Time = (float)key.mTime;
				
				// aiQuatKey의 쿼터니언은
				// TReal w, x, y, z; 이다.
				// 뒤집혀 있어서 memcpy 사용 안함
				// DX와 Assimp의 메모리 순서가 다르기 때문에 직접 대입해서
				// 사용함
				frameData.Rotation.x = key.mValue.x;
				frameData.Rotation.y = key.mValue.y;
				frameData.Rotation.z = key.mValue.z;
				frameData.Rotation.w = key.mValue.w;

				bFound = true;
			}

			if (fabsf((float)aniNode->mScalingKeys[k].mTime - (float)t) <= D3DX_16F_EPSILON)
			{
				aiVectorKey key = aniNode->mScalingKeys[k];
				frameData.Time = (float)key.mTime;
				memcpy_s(&frameData.Scale, sizeof(Vector3), &key.mValue, sizeof(aiVector3D));

				bFound = true;
			}

			if (bFound == true)
				aniNodeInfo.Keyframe.push_back(frameData);
		}//for(k)


		// 키가 없는 부분(전체에 길이 보다 짧은 경우)
		// 키 프레임 사이즈가 clip이 가지고 있는 실제 프레임 수보다 작다면
		// 나머지는 마지막 값으로 저장
		if (aniNodeInfo.Keyframe.size() < clip->FrameCount)
		{
			UINT count = clip->FrameCount - aniNodeInfo.Keyframe.size();

			// 마지막거 가져옴
			asKeyframeData keyFrame = aniNodeInfo.Keyframe.back();

			// 마지막거로 나머지 채워줌
			for (UINT n = 0; n < count; n++)
				aniNodeInfo.Keyframe.push_back(keyFrame);
		}

		// 길이도 조정되서, 마지막 프레임의 시간과 비교
		clip->Duration = max(clip->Duration, aniNodeInfo.Keyframe.back().Time);

		// 본 별로 키 프레임이 들어감
		aniNodeInfos.push_back(aniNodeInfo);
	}

	// 재귀처리
	ReadKeyFrameData(clip, scene->mRootNode, aniNodeInfos);


	return clip;
}

void Converter::ReadKeyFrameData(asClip* clip, aiNode* node, vector<struct asClipNode>& aiNodeInfos)
{
	asKeyframe* keyFrame = new asKeyframe();
	// 본을 부를때 썼던 aiBone과 애니메이션 정보를 저장해놓은 본과 일치시켜서
	// 해당 애니메이션의 본 정보를 가져온다.
	// 여기는 실제 Mesh 본
	keyFrame->BoneName = node->mName.C_Str();

	// Mixamo는 이름을 자동으로 맞춰주지만, 다른 데는 Animation bone 네임과 메쉬 본 네임이 다르다.
	// 이름을 일일히 맞춰주는 것을 리타겟팅이라 함
	// 애니메이션의 본 이름이 다를 경우 자동으로 리타켓팅해주는 프로그램이 있다.
	// 인간형 구조는 모두 비슷하므로 가능하며
	// 두개의 이름이 다를 경우 맞춰주는 과정을 이 함수에서 진행하면 된다.

	// 우리 수업에서는 이름이 동일하다 가정하고 진행,
	// 이름이 없다면 그냥 없다라고 가정하고 Identity가정하고 자신의 행렬을 사용
	for (UINT i = 0; i < clip->FrameCount; i++)
	{
		asClipNode* asClipNode = nullptr;

		// 이름이 같은지 찾는다.
		for (UINT n = 0; n < aiNodeInfos.size(); n++)
		{
			if (aiNodeInfos[n].Name == node->mName)
			{
				asClipNode = &aiNodeInfos[n];

				break;
			}
		}//for(n)


		// 본의 이름이 있을 경우 해당 데이터를 처리하며 없으면
		// 노드의 본 정보를 그대로 사용한다.
		asKeyframeData frameData;
		if (asClipNode == nullptr)
		{
			// 부모를 따라 움직이게 됨

			Matrix transform(node->mTransformation[0]);
			D3DXMatrixTranspose(&transform, &transform);

			// asKeyframeData의 맴버가 srt형태로 분할 되서 변수로 되어있다.
			// 그래서 분할해서 저장

			// 행렬의 정보를 분리하기 위해 Decompose를 사용하며,
			// D3DXMatrixDecompose함수는 회전이 쿼터니온으로 리턴
			// i는 FrameCount;
			frameData.Time = (float)i;
			D3DXMatrixDecompose(&frameData.Scale, &frameData.Rotation, &frameData.Translation, &transform);
		}
		else
		{
			frameData = asClipNode->Keyframe[i];
		}

		keyFrame->Transforms.push_back(frameData);
	}

	clip->Keyframes.push_back(keyFrame);

	for (UINT i = 0; i < node->mNumChildren; i++)
		ReadKeyFrameData(clip, node->mChildren[i], aiNodeInfos);
}

void Converter::WriteClipData(asClip* clip, wstring savePath)
{
	Path::CreateFolders(Path::GetDirectoryName(savePath));

	BinaryWriter* w = new BinaryWriter();
	w->Open(savePath);

	w->String(clip->Name);
	w->Float(clip->Duration);
	w->Float(clip->FrameRate);
	w->UInt(clip->FrameCount);

	w->UInt(clip->Keyframes.size());
	for (asKeyframe* keyframe : clip->Keyframes)
	{
		w->String(keyframe->BoneName);

		w->UInt(keyframe->Transforms.size());
		w->Byte(&keyframe->Transforms[0], sizeof(asKeyframeData) * keyframe->Transforms.size());

		SafeDelete(keyframe);
	}

	w->Close();
	SafeDelete(w);
}

 

 

 

 

 

 

 

 

Types.h 추가된 부분


더보기
// 각 키프레임의 SRT 정보
struct asKeyframeData
{
	float Time; // 프레임 수...

	// 프레임에 해당하는 SRT값
	Vector3 Scale;
	Quaternion Rotation;
	Vector3 Translation;
};

// 각각의 키프레임 정보
// 프레임 별로 전체 트랜스폼 저장
struct asKeyframe
{
	string BoneName;
	vector<asKeyframeData> Transforms;
};

// Animation Clip
struct asClip
{
	string Name;

	UINT FrameCount; // 프레임 수
	float FrameRate; // 프레임 속도(비율)
	float Duration; // 클립(애니메이션)의 길이 

	vector<asKeyframe*> Keyframes; // 키 프레임 정보
};

//aniNode의 원본 키프레임 저장 -> 실제 파일로 저장될 데이터
struct asClipNode
{
	aiString Name; // 본의 이름
	vector<asKeyframeData> Keyframe; // 해당 본의 키프레임에 해당하는 SRT 저장됨. 
};

 

Model.h 추가된 부분


더보기
void ReadClip(wstring file);

 

 

 

 

 

 

 

Model.Cpp 추가된 부분


더보기
void Model::ReadClip(wstring file)
{
	file = L"../../_Models/" + file + L".clip";

	BinaryReader* r = new BinaryReader();
	r->Open(file);


	ModelClip* clip = new ModelClip();

	clip->name = String::ToWString(r->String());
	clip->duration = r->Float();
	clip->frameRate = r->Float();
	clip->frameCount = r->UInt();

	UINT keyframesCount = r->UInt();
	for (UINT i = 0; i < keyframesCount; i++)
	{
		ModelKeyframe* keyframe = new ModelKeyframe();
		keyframe->BoneName = String::ToWString(r->String());


		UINT size = r->UInt();
		if (size > 0)
		{
			keyframe->Transforms.assign(size, ModelKeyframeData());

			void* ptr = (void*)&keyframe->Transforms[0];
			r->Byte(&ptr, sizeof(ModelKeyframeData) * size);
		}

		clip->keyframeMap[keyframe->BoneName] = keyframe;
	}

	r->Close();
	SafeDelete(r);

	clips.push_back(clip);
}