애니메이션 관련된 정보를 추출하는 작업을 해볼 것이다.
기본적으로 원리는 단순하다.
파일이 있는 걸 추출하면 된다.
1. Converter.h에 ExportAnimationData 선언하기
Converter.h에
void ExportAnimationData(wstring savePath, uint32 index=0 ); // 애니메이션이 여러개 있을 수 있으니 index를 받아준다.
선언한다.
fbx가 어렵다고 한 게 mesh만 있는 경우가 있고, mesh에 애니메이션이 같은 파일에 있는 경우도 있고, mesh는 없지만 애니메이션 정보만 있는 경우가 있고, 굉장히 다양하다. 대부분은 경우는 애니메이션이 따로 빠져있는 경우가 많다.
핵심적인 부분은 스키닝과 실질적으로 애니메이션의 동작 원리를 이해하는 게 중요하다.
나중에 이걸 조금씩 다듬으면서 전반적으로 실제로 사용할 에셋들에 대해서도 완전하게 사용할 수 있게끔 조금씩 수정하긴 해야 한다.
2. AssimpTool::Init에서 fbx 애니메이션 파일의 경로를 ReadAssetFile에 넣어 호출하고, 지정한 주소를 ExportAnimationData에 넣어 호출하기
ExportAnimationData를 사용할 때에는 어떻게 해야 할 것이냐
AssimpTool::Init에서 먼저 메시를 로드했었다.
근데 여기서 초기화를 해도 되지만 새로 파서 만드는게 조금 더 간단하다.
void AssimpTool::Init()
{
{
shared_ptr<Converter> converter = make_shared<Converter>();
converter->ReadAssetFile(L"Kachujin/Mesh.fbx");
converter->ExportMaterialData(L"Kachujin/Kachujin");
converter->ExportModelData(L"Kachujin/Kachujin");
}
{
shared_ptr<Converter> converter = make_shared<Converter>();
converter->ReadAssetFile(L"Kachujin/Idle.fbx");
converter->ExportAnimationData(L"Kachujin/Idle");
}
{
shared_ptr<Converter> converter = make_shared<Converter>();
converter->ReadAssetFile(L"Kachujin/Run.fbx");
converter->ExportAnimationData(L"Kachujin/Run");
}
{
shared_ptr<Converter> converter = make_shared<Converter>();
converter->ReadAssetFile(L"Kachujin/Slash.fbx");
converter->ExportAnimationData(L"Kachujin/Slash");
}
}
이렇게 사용하게 될 것이다.
ExpertAnimationData에 코드를 추가해서 실질적으로 애니메이션을 읽고 추출하는 부분을 넣어줄 것이다.
항상 하던 것처럼 Read를 하고 Write를 하는 방식으로 만들어 줄 것이다.
애니메이션을 읽어 올 건데 읽어 온 다음에 그걸 반환해서 우리가 작업하는 파일 형태로 만들어 주는 것이 목적이기 때문에 그 부분을 넣어줄 것이다.
3. AsType.h에 asKeyframeData, asKeyframe, asAnimation, asAnimationNode 구조체를 정의하기
AsType.h에 지난 시간에 넣어준 것처럼 새로운 애들이 생길 필요가 있다. 지난 시간에 넣어 준 것은 스키닝과 관련된 부분이었다. 정점에다가 연관성이 있는 뼈대에 대한 정보를 기입하는 부분이었다.
실질적으로 애니메이션 클립 파일과 관련된 부분은 이제 작업을 해준다.
AsTypes.h에
struct asKeyframeData
{
float time;
Vec3 scale;
Quaternion rotaion;
Vec3 translation;
};
// 애니메이션의 하나의 프레임을 얘기 해준다.
struct asKeyframe
{
string boneName;
vector<asKeyFrameData> transforms;
};
struct asAnimation
{
string name;
uint32 frameCount; // 몇 프레임 짜리 인지
float frameRate; // 30 이라면 1/30초마다 다음 그림으로 넘어간다는 얘기
float duration; // 얼마만큼 지속이 되는지
vector<asKeyFrame> keyframes; // 매 프레임마다 어떠한 정보로 틀어주면 되는지
// 관절마다 어떻게 프레임마다 변화를 하는지에 대한 정보를 담고 있다.
};
frame이 지나감에 따라 모든 관절들이 어떻게 움직일지가 기입이 되어있고,
그것에 따라 SRT가 적용된 최종변환 행렬이 있다고보면 된다.
만약에 1프레임을 실행하고 있다고 하면 모든 bone들을 각각 변환행렬 FinalMat을 이용해 가지고 변화를 해주는 식으로 작업을 해주면 된다.
이 그림을 보면 왜 2차원 배열이 되어야 하는지, 애니메이션이란게 이런 식으로 Bone별로 프레임마다 관련된 SRT 최종 변환 행렬이 이렇게 있다고 일단 생각하면 된다.
struct asKeyframeData
{
float time;
Vec3 scale;
Quaternion rotaion;
Vec3 translation;
};
이 STR을 행렬로 만들어 주는 부분은 나중에 넣어야 한다.
그리고 중요하진 않지만 하는 과정에서 필요한 ai원본을 잠시 들고 있다가 그걸 이용해서 뭔가 찾아오고 그런 작업을 할거라 Cache용도로 임시적으로 사용하는 구조체를 만든다.
// Cache
struct asAnimationNode
{
aiString name;
vector<asKeyFrameData> keyframe;
};
4. ExportAnimationData를 할 때 필요한 ReadAnimationData 정의하기
1) ReadAnimationData 정의하기
준비는 끝났으니 ExportAnimationData를 할 때 필요한 ReadAnimationData를 선언한다.
shared_ptr<asAnimation> ReadAnimationData(aiAnimation* srcAnimation);
우리만의 버전으로 aiAnimation을 변환 시키는 것이다.
shared_ptr<asAnimation> Converter::ReadAnimationData(aiAnimation* srcAnimation)
{
shared_ptr<asAnimation> animation = make_shared<asAnimation>();
animation->name = srcAnimation->mName.C_Str();
animation->frameRate = (float)srcAnimation->mTicksPerSecond;
animation->frameCount = (uint32)srcAnimation->mDuration + 1; // 10초 짜리면 framedl 0부터 시작하니까 0~10까지 11개가 된다.
for (uint32 i = 0; i < srcAnimation->mNumChannels; i++)
{
aiNodeAnim* srcNode = srcAnimation->mChannels[i]; // Each channel affects a single node
// 애니메이션 노드 데이터 파싱
shared_ptr<asAnimationNode> node = ParseAnimationNode(animation, srcNode);
}
}
ReadAnimationData에서 aiNodeAnim이란 것을 원하는 다른 버전으로 들고 있도록 Animation노드를 파싱 하는 작업을 해볼 것인데 그 작업을 하기 위한 함수를 만들어 준다.
2) mChannel[i] 즉 각 뼈대의 keyframeData를 추출하는 헬퍼함수 ParseAnimationNode함수 만들를 만들어 ReadAnimationData에서 호출하기
Converter.h에
shared_ptr<asAnimationNode> ParseAnimationNode(shared_ptr<asAnimation> animation, aiNodeAnim* srcNode);
이렇게 만들어 준다.
animation이 있고, 추가적으로 srcNode를 받아 줘서 asAnimationNode를 만들어 주는 헬퍼 함수를 만들어 줄 것이다. 그냥 하나씩 파싱을 하면 된다. SRT와 관련된 부분을 파싱 하는 부분이라고 볼 수 있다.
shared_ptr<asAnimationNode> Converter::ParseAnimationNode(shared_ptr<asAnimation> animation, aiNodeAnim* srcNode)
{
std::shared_ptr<asAnimationNode> node = make_shared<asAnimationNode>();
node->name = srcNode->mNodeName;
// srcNode->mNumPositionKeys, mNumRotationKeys, mNumScalingKeys 이렇게 있는데 따로 하지 않고,
// 3개를 동시에 관리하도록 3개 중에 가장 큰 값으로 keyCount를 고정할 것이다.
// 최적화 보다 실행되게 하는게 목적이니 비효율적이라 하더라도 편리성을 생각해서 작업을 한다.
// 유니티에서 애니메이션 파일을 넣었을 때 어떻게 될지 나중에 살펴 본다.
// 그래서 셋 중에서 가장 큰 것을 골라주기 위해서
uint32 keyCount = max(max(srcNode->mNumPositionKeys, srcNode->mNumScalingKeys), srcNode->mNumRotationKeys);
for (uint32 k = 0; k < keyCount; k++)
{
asKeyframeData frameData;
bool found = false;
uint32 t = node->keyframe.size(); // 최근 반목문 째에 몇 개까지 채웠는지
// Position
// 애니메이션 파일을 보면 0,1,2,3,4,5초 마다 무엇인가가 들어가 있을 것이다. 각 초에 뭐가 있는지 추출을 해가지고 그것에 값이 있으면 key에 값을 넣어주는 것이다.
if (::fabsf((float)srcNode->mPositionKeys[k].mTime - (float)t) <= 0.0001f) // 빼기를 해서 차이가 얼마 이내면 같게 비교하고 있다.
{
aiVectorKey key = srcNode->mPositionKeys[k];
frameData.time = (float)key.mTime;
::memcpy_s(&frameData.translation, sizeof(Vec3), &key.mValue, sizeof(aiVector3D));
found = true;
}
// Rotation
if (::fabsf((float)srcNode->mRotationKeys[k].mTime - (float)t) <= 0.0001f)
{
aiQuatKey key = srcNode->mRotationKeys[k];
frameData.time = (float)key.mTime;
frameData.rotation.x = key.mValue.x;
frameData.rotation.y = key.mValue.y;
frameData.rotation.z = key.mValue.z;
frameData.rotation.w = key.mValue.w;
found = true;
}
// Scale
if (::fabsf((float)srcNode->mScalingKeys[k].mTime - (float)t) <= 0.0001f)
{
aiVectorKey key = srcNode->mScalingKeys[k];
frameData.time = (float)key.mTime;
::memcpy_s(&frameData.scale, sizeof(Vec3), &key.mValue, sizeof(aiVector3D));
found = true;
}
if (found == true)
node->keyframe.push_back(frameData);
}
// Keyframe 늘려주기
if (node->keyframe.size() < animation->frameCount)
{
uint32 count = animation->frameCount - node->keyframe.size();
asKeyframeData keyFrame = node->keyframe.back();
for (uint32 n = 0; n < count; n++)
node->keyframe.push_back(keyFrame);
}
return node;
}
SRT 정보를 추출할 수 있다.
S, R, T중 하나라도 걸리면 keyframe에다가 넣어주고 나머지는 초기값으로 들어가게 될 것이다.
중요한 건 인자로 넣어준 aiNodeAnim* srcNode에 따라가지고 SRT를 추출하는 함수구나라는 것만 일단 이해하고 넘어가도 된다. 외부 라이브러리를 쓸 때는 모든 부분을 신경 써서 볼 이유가 없다.
3) ReadAnimationData에서 각 노드의 애니메이션 데이터를 map으로 캐싱하기
다시 ReadAnimationData로 가서
shared_ptr<asAnimation> Converter::ReadAnimationData(aiAnimation* srcAnimation)
{
shared_ptr<asAnimation> animation = make_shared<asAnimation>();
animation->name = srcAnimation->mName.C_Str();
animation->frameRate = (float)srcAnimation->mTicksPerSecond;
animation->frameCount = (uint32)srcAnimation->mDuration + 1; // 10초 짜리면 framedl 0부터 시작하니까 0~10까지 11개가 된다.
for (uint32 i = 0; i < srcAnimation->mNumChannels; i++)
{
aiNodeAnim* srcNode = srcAnimation->mChannels[i]; // Each channel affects a single node
// 애니메이션 노드 데이터 파싱
shared_ptr<asAnimationNode> node = ParseAnimationNode(animation, srcNode);
}
}
shared_ptr<asAnimationNode> node에 노드가 파싱이 되었다.
shared_ptr<asAnimation> Converter::ReadAnimationData(aiAnimation* srcAnimation)
{
shared_ptr<asAnimation> animation = make_shared<asAnimation>();
animation->name = srcAnimation->mName.C_Str();
animation->frameRate = (float)srcAnimation->mTicksPerSecond;
animation->frameCount = (uint32)srcAnimation->mDuration + 1; // 10초 짜리면 framedl 0부터 시작하니까 0~10까지 11개가 된다.
map<string, shared_ptr<asAnimationNode>> cacheAnimNodes;
for (uint32 i = 0; i < srcAnimation->mNumChannels; i++)
{
aiNodeAnim* srcNode = srcAnimation->mChannels[i]; // Each channel affects a single node
// 애니메이션 노드 데이터 파싱
shared_ptr<asAnimationNode> node = ParseAnimationNode(animation, srcNode);
// 현재 찾은 노드 중에 제일 긴 시간으로 애니메이션 시간 갱신
animation->duration = max(animation->duration, node->keyframe.back().time);
cacheAnimNodes[srcNode->mNodeName.C_Str()] = node; // 이름에다가 만들어준 노드를 기입해서 임시적으로 들고 있는다.
}
}
map 변수에다가 노드의 이름과 shared_ptr<asAnimationNode> 데이터를 짝을 맞춰서 넣어 들고 있는다.
애니메이션은 거의 다 추출한 것이다.
이제 처음에 설계했던 방식대로 정보를 연결해 주는 부분이 필요하다.
4) ReadAnimationData에서 호출되어 루트 노드부터 재귀적으로 호출되며 타고 내려가서 asAnimation의 keyfreames를 채워주는 ReadKeyframeData를 만들기
Conveter.h에
void ReadKeyframeData(shared_ptr<asAnimation> animation, aiNode* node, map<string, shared_ptr<asAnimationNode>>& cache);
void WriteAnimationData(shared_ptr<asAnimation> animation, wstring finalPath);
ReadKeyframeData는 지금 작업하고 있는 캐시값을 이용해서 animation에다가 원하는 값들을 연결해 주고 채워주는 부분을 만들어 주게 된다.
WriteAnimationData로 원하는 애니메이션을 원하는 경로에다가 원하는 포멧으로 만들어 주면 된다.
ReadKeyframeData 구현이 완료가 되면
shared_ptr<asAnimation> Converter::ReadAnimationData(aiAnimation* srcAnimation)
{
shared_ptr<asAnimation> animation = make_shared<asAnimation>();
animation->name = srcAnimation->mName.C_Str();
animation->frameRate = (float)srcAnimation->mTicksPerSecond;
animation->frameCount = (uint32)srcAnimation->mDuration + 1; // 10초 짜리면 framedl 0부터 시작하니까 0~10까지 11개가 된다.
map<string, shared_ptr<asAnimationNode>> cacheAnimNodes;
for (uint32 i = 0; i < srcAnimation->mNumChannels; i++)
{
aiNodeAnim* srcNode = srcAnimation->mChannels[i]; // Each channel affects a single node
// 애니메이션 노드 데이터 파싱
shared_ptr<asAnimationNode> node = ParseAnimationNode(animation, srcNode);
// 현재 찾은 노드 중에 제일 긴 시간으로 애니메이션 시간 갱신
animation->duration = max(animation->duration, node->keyframe.back().time);
cacheAnimNodes[srcNode->mNodeName.C_Str()] = node; // 이름에다가 만들어준 노드를 기입해서 임시적으로 들고 있는다.
}
ReadKeyframeData(animation, _scene->mRootNode, cacheAnimNodes);
return animation;
}
ReadAnimationData에서 채워준 정보를 animation에 넣어주기 위해 이렇게 최종적으로 ReadKeyframeData를 호출하면 된다.
중요한 건 ReadKeyframeData 이 아이가
void Converter::ReadKeyframeData(shared_ptr<asAnimation> animation, aiNode* node, map<string, shared_ptr<asAnimationNode>>& cache)
animation과 node를 받아주는데
assimp라이브러리 자체가 노드들이라는 게 계층구조로 되어 있어 가지고,
계층구조가 트리구조이기 때문에 걔를 파싱 할 때 항상 루트노트부터 시작해 가지고, 자식들로 이어지는 방식으로 작업을 했었다. 재귀적으로 호출하는 것이다.
ReadKyframeData에서 처음에는 _scene->mRootNode를 기반으로 하지만 나중에는 이 노드의 child를 대상으로 또 ReadKeyframeData를 하면서 이걸 이어서 호출해 주게 될 것이다. 그럼 처음에 작업한 노드 순서대로 호출이 될 것이기 때문에 그거에 따라서 하나씩 채워주면 된다고 볼 수 있다.
결국 하고 싶은 건 animation에 아직 안 채워준 게 ainmation→keyframes에 관한 정보를 안 채워줬다. 이 부분을 채워주려고 노력하는 것이다.
vector<shared_ptr<asKeyframe>> keyframes;
이게 asKeyframe안에 vector<asKeyframeData> transforms가 있는 2차 배열이기 때문에 어려울 수 있다.
헤딩을 해야 될 부분과 하면 안 될 부분이 있는데 이렇게 라이브러리 사용하는 거 이런 거는 어지간해서 인터넷 검색해서 잘되는 샘플 하나 찾은 다음에 그걸 조금 가공해서 작업하는 것을 권장한다.
이런 거 헤딩하는 건 의미가 없다.
복사를 줄이고 싶어서 keyframes의 vector type은 shared_ptr<asKeyframe>로 만들어 놨다.
void Converter::ReadKeyframeData(shared_ptr<asAnimation> animation, aiNode* srcNode, map<string, shared_ptr<asAnimationNode>>& cache)
{
shared_ptr<asKeyframe> keyframe = make_shared<asKeyframe>();
keyframe->boneName = srcNode->mName.C_Str();
shared_ptr<asAnimationNode> findNode = cache[srcNode->mName.C_Str()];
for (uint32 i = 0; i < animation->frameCount; i++)
{
asKeyframeData frameData;
if (findNode == nullptr)
{
Matrix transform(srcNode->mTransformation[0]);
transform = transform.Transpose();
frameData.time = (float)i;
transform.Decompose(OUT frameData.scale, OUT frameData.rotation, OUT frameData.translation);
}
else
{
frameData = findNode->keyframe[i];
}
keyframe->transforms.push_back(frameData);
}
// 애니메이션 키프레임 채우기, 한 노드의 전 프레임 정보를 담은 asKeyfame을 재귀 호출마다 넣어준다.
animation->keyframes.push_back(keyframe);
for (uint32 i = 0; i < srcNode->mNumChildren; i++)
ReadKeyframeData(animation, srcNode->mChildren[i], cache);
}
중요한 건 Converter::ReadAnimationData에서
ReadKeyframeData(animation, _scene->mRootNode, cacheAnimNodes);
이렇게 루트부터 시작을 해가지고,
하나씩 재귀적으로 호출하는 부분을 계속하고 노드를 순회를 하면서 노드의 이름에 따라가지고 그게 이미 ReadAnimationData에서 파싱 한 애면 이름으로 찾아 미리 채워놓은 것을 asKeyframeData frameData에다가 넣어주고,
이름으로 찾아서 없는 애면 직접 sKeyframeData frameData를 채워서 vector<asKeyframeData> transforms;에 넣어주고, 그게 채워지면 vector<shared_ptr<asKeyframe>> keyframes;에 넣어줘서, 모든 정보를 채워주고 있다가 결론이다.
여기서 애니메이션을 로딩하는 부분까지 끝났다고 하면 디버깅을 해서 보면 된다.
Converter::ReadAnimationData의
ReadKeyframeData(animation, _scene->mRootNode, cacheAnimNodes);
여기에 중단점을 찍고,
애니메이션을 다 읽어서 keyframe까지 다 들어가게 되면
Bone에 따라가지고 SRT와 Frame으로 이루어진 것들이 완성이 된다.
5. ExportAnimationData를 할 때 필요한 WriteAnimationData 정의하기
ExportAnimationData에서
ReadAnimationData를 해줘서 shared_ptr<asAnimation> animation을 만들어 주고, 그거에 따라 WriteAnimationData을 해주면 된다.
void Converter::ExportAnimationData(wstring savePath, uint32 index)
{
wstring finalPath = _modelPath + savePath + L".clip";
assert(index < _scene->mNumAnimations);
shared_ptr<asAnimation> animation = ReadAnimationData(_scene->mAnimations[index]);
WriteAnimationData(animation, finalPath);
}
메모리 상에 들고 있던 거를 우리만의 다른 형식으로 일단은 바꾸는 부분을 먼저 하고,
그걸 WriteAnimationData에서 우리가 원하는 다른 파일 포맷으로 적어주면 되는 그런 느낌으로 동작을 하게 된다.
shared_ptr<asAnimation> animation = ReadAnimationData(_scene->mAnimations[index]);
에 중단점을 찍고 실행을 해본다.
코드가 이해가 안 될 때 디버깅을 해서 어떤 값이 들어가는지 보면 이해가 수월한 경우가 있다.
프레임별로 본 이름에 따라가지고 성공적으로 채워지면,
return animation에서
모든 정보가 들어가 있는 것을 확인할 수 있다.
얘를 다시 파일로 들고 있어서 파일 입출력을 통해서 언제든 편리하게 쓸 수 있게 만들어 주는 게 중요하다. fbx파일은 너무 느리다 보니까 그 부분을 조금 더 편리하게 작업할 수 있는 코드를 WriteAnimationData에 만들어주도록 할 것이다.
애니메이션을 텍스트로 봐서 찾아보기에는 너무 양이 많다 보니까 그냥 바이너리 파일로 만들어서 한 번에 저장하는 게 효율적일 것이다.
Model 파일도 이런 느낌으로 만들었으니까 애니메이션도 동일하게 채워주면 된다.
void Converter::WriteAnimationData(shared_ptr<asAnimation> animation, wstring finalPath)
{
auto path = filesystem::path(finalPath);
// 폴더가 없으면 만든다.
filesystem::create_directory(path.parent_path());
shared_ptr<FileUtils> file = make_shared<FileUtils>();
file->Open(finalPath, FileMode::Write);
file->Write<string>(animation->name);
file->Write<float>(animation->duration);
file->Write<float>(animation->frameRate);
file->Write<uint32>(animation->frameCount);
file->Write<uint32>(animation->keyframes.size());
for (shared_ptr<asKeyframe> keyframe : animation->keyframes)
{
file->Write<string>(keyframe->boneName);
file->Write<uint32>(keyframe->transforms.size());
file->Write(&keyframe->transforms[0], sizeof(asKeyframeData) * keyframe->transforms.size());
}
}
이 코드가 읽을 때도 동일하게 읽히면 성공하는 것이다.
6. 실행해서 지정된 폴더에 파일이 생성되었는지 확인하기
void Converter::ExportAnimationData(wstring savePath, uint32 index)
{
wstring finalPath = _modelPath + savePath + L".clip";
assert(index < _scene->mNumAnimations);
shared_ptr<asAnimation> animation = ReadAnimationData(_scene->mAnimations[index]);
WriteAnimationData(animation, finalPath);
}
여기서 실제로 만들어지는지를 보면 된다.
실행을 하고 폴더에 가서 살펴보면
여기에 clip이 새로 생성되었다는 걸 볼 수 있다.
파싱 했던 애니메이션 파일 정보가 여기 들어있다고 볼 수 있다.
궁금하면 헥스 에디터로 열어서 보면 된다.
이걸 이용해서 나중에 읽어서 이 애니메이션 코드를 조립만 하면 되는 것이다.
Assimp에서 꺼내고 이런 작업은 지루하고 재미없지만
Model과 관련된 부분에서 애니메이션 관련된 파일을 추가해서 방금 읽었던 애니메이션 파일에 따라서 뭔가 움직이게끔 작업을 시작할 준비가 끝났다고 볼 수 있다.
7. 맺음말
Textrues에도 들어가 있나 보면
뭔가 만들어지긴 했는데 Kachujin 폴더에 들어가야 하는데 이름이 중복 입력 되어 있는 걸 보니 경로가 잘 못 된 거 같다.
Converter::WriteTexture 이 부분에서
string pathStr = saveFolder + fileName;
를
string pathStr = (filesystem::path(saveFolder) / fileName).string();
이렇게 해서 실행해서 다시 살펴보면
원하던 경로에 잘 생긴 것을 볼 수 있다.
Material에서도 Kachujin.xml을 보
<?xml version="1.0" encoding="UTF-8"?>
<Materials>
<Material>
<Name>kachujin_MAT</Name>
<DiffuseFile>Kachujin_diffuse.png</DiffuseFile>
<SpecularFile>Kachujin_specular.png</SpecularFile>
<NormalFile>Kachujin_normal.png</NormalFile>
<Ambient R="0" G="0" B="0" A="1"/>
<Diffuse R="0.80000001" G="0.80000001" B="0.80000001" A="1"/>
<Specular R="0.5" G="0.5" B="0.5" A="20"/>
<Emissive R="0" G="0" B="0" A="1"/>
</Material>
</Materials>
이렇게 defuse, specular, normal이라고 했으니까
그 아이들을 긁어서 복사한 것까지 한 것이다.
알아서 fbx파일을 추출해지고
내장되어 있는 리소스를 꺼내서 그 아트 리소스를 복사하는 부분이 이렇게 실행이 된다고 보면 된다.
사실 이것도 중요한 건 아니다.
툴에서 읽고 그런 건 빨리 넘어가는 걸 권장한다.
지금까지 한 것에서 중요한 것은 두 가지인데
- 스키닝과 관련되어 버텍스에 가중치랑 뼈대 정보가 들어가 있다.
- 애니메이션이라는 게 결국 어떻게 생겼는지 이해하는 게 중요한다.
나머지는 Assimp에 의존적인 거니까 구글에서 검색해서 하면 된다고 했지만 기본적으로 말하고 싶은 내용들은
이것이다.
프레임에 따라 모든 Bone에 대해서 어떻게 움직이는지 SRT가 들어가 있다가 핵심이다
직금은 SRT 원본을 넣어준 것이다. 가공을 안 한 상태이다. 그렇다는 건 SRT 변환 행렬을 이용해 만든 행렬이 어떤 것일까?
루트까지 가서 탈출하는 것일까, 직속상관인 부모로 넘어가는 것일까
여기까지는 글로벌로 빠져나가는 거 까진 아니고 기본적으로 애니메이션 정보에 기입되어 있는 SRT만 저장해 둔 상태이다.
이걸 감안해서 나중에 가공해서 글로벌로 보내는 작업을 해주긴 해야 한다.
글로벌에서 월드, 뷰, 프로젝션을 곱하는 것이기 때문이다.
파싱이 완료가 되었고,
다음 시간에는 Model로 돌아가서 작업을 할 수 있게 되었다.
Model과 관련해서 애니메이션 관련된 부분을 추가해 가지고, 방금 읽었던 애니메이션 파일에 따라서 뭔가 움직이게끔 작업을 시작할 수 있게 되었다.
지금은 애니메이션 파일을 추출하고 나머지 부분들에 대해 익히는 것에 만족을 할 수 있다.
애니메이션 관련된 부분을 추출을 해가지고 원하는 포맷으로 집어넣는 데까지 일단은 진행했다.
언리얼, 유니티는 어떻게 처리했는지 보면 의미가 있다.
애니메이션 파일을 유니티에 넣어 보면
다 같은 fbx 파일인데 까보면 삼각형 모양의 아이콘이 있는 거 볼 수 있다. 이게 유니티에서 말하는 애니메이션이다.
애니메이션을 따로 추출한 상태이기 때문에 사용할 수 있다.
Idle 애니를 Mesh에 드래그 앤 드롭을 하면 애니메이션 컨트롤러라는 새로운 파일이 만들어진다.
FSM 방식으로 어떻게 규칙을 정할지에 대한 그림을 그려준다.
DX나 OpenGL을 공부하면 유니티, 언리얼에서 해준 작업들이 얼마나 대단한고 고마운 작업인지를 알게 된다.
물론 유니티나 언리얼 회사에는 전담하는 전문가가 있다.
요즘은 자체 엔진보다는 언리얼이나 유니티를 깊이 이용해서 사용하는 게 낫다고 본다.
3D 공부를 처음 하는 입장이면 파일의 양이 압도가 된다. 온갖 것들이 다 있으니까 헷갈리는데 이제는 좀 정리가 될 것이다. FBX파일에 애니메이션만 있는 경우가 있구나라는 것도 겸사 알게 된다.
애니메이션을 더블클릭해서 보면 매시간마다 뭘 하기보다는 띄엄띄엄 되어 있다. 우리는 무식하게 모든 프레임의 모든 변환을 넣어서 작업하고 있지만 원래는 더 복잡하게 되어 있다.
후반부에 해볼게 애니를 섞어서 해볼 건데 Transition도 유니티에 잘 되어 있다. 굉장히 기능이 많다는 걸 볼 수 있다.
'DirectX' 카테고리의 다른 글
62. 애니메이션_애니메이션#2_CreateAnimationTransform, CreateTexture (0) | 2024.03.06 |
---|---|
61. 애니메이션_애니메이션#1_ReadAnimation, ModelAnimator (0) | 2024.03.05 |
59. 애니메이션_스키닝 (0) | 2024.03.03 |
58. 애니메이션_애니메이션 이론 (0) | 2024.03.02 |
57. 모델_ImGUI (0) | 2024.03.01 |
댓글