-
[DirectX11] 애니메이션 이해(Animation)2024년 03월 11일
- 유니얼
-
작성자
-
2024.03.11.:06
728x90DirectX 11로 게임 엔진 아키텍처 만들기
애니메이션 가져오기는 게임 엔진에서 캐릭터나 오브젝트에 생명을 불어넣는 중요한 과정입니다. 이번 포스트에서는 애니메이션 데이터를 읽어오고, 이를 게임 엔진에서 사용할 수 있도록 변환하는 방법에 대해 자세히 살펴보겠습니다.
캐릭터 애니메이션 이해
캐릭터 모델이 움직임을 구현하는 과정은 복잡하면서도 세밀한 여러 단계를 포함합니다. 이 과정은 주로 본(bones), 스키닝(skinning), 애니메이션 클립(animation clips)을 통해 이루어집니다. 각 단계별로 구체적으로 설명하면 다음과 같습니다.
1, 본(Bones)의 설정
본은 캐릭터의 뼈대와 같은 역할을 합니다. 각 본은 캐릭터의 특정 부위를 대표하며, 전체 본 구조는 캐릭터의 골격을 형성합니다. 본들은 계층적 구조로 배열되어 있으며, 상위 본의 움직임은 하위 본에 영향을 미칩니다. 예를 들어, 어깨 본을 움직이면 연결된 팔과 손의 본도 같이 움직이게 됩니다.
2, 스키닝(Skinning)
스키닝 과정에서 각 본은 메쉬의 버텍스(모델을 이루는 가장 작은 단위의 점)에 연결됩니다. 각 버텍스는 하나 이상의 본에 "가중치(Weight)"를 가지며, 이 가중치는 해당 본의 움직임이 버텍스에 얼마나 영향을 미치는지를 결정합니다. 예를 들어, 팔 부위의 버텍스는 팔 본에 높은 가중치를, 몸통 본에는 낮은 가중치를 가질 수 있습니다. 이를 통해 본의 움직임에 따라 캐릭터의 메쉬가 자연스럽게 변형되어 움직임을 구현합니다.
3, 애니메이션 클립(Animation Clips)
애니메이션 클립은 특정 동작이나 움직임을 구현하기 위한 본들의 움직임 데이터를 포함합니다. 각 클립은 시간에 따른 본의 위치, 회전, 스케일 변화를 정의합니다. 애니메이션 시스템은 이 클립을 재생함으로써 캐릭터에 동적인 움직임을 부여합니다.
본과 메쉬 모델의 움직임 과정
- 본 구조 정의: 모델링 단계에서 본의 구조가 정의되며, 각 본은 메쉬의 특정 부분을 제어합니다.
- 스킨닝: 각 메쉬 버텍스는 하나 이상의 본에 가중치(Weight)와 함께 연결됩니다. 이 가중치는 해당 본의 움직임이 버텍스에 미치는 영향력을 결정합니다.
- 본 변형: 애니메이션에서 본의 위치와 회전이 변하면, 연결된 버텍스들도 본의 움직임에 따라 변형됩니다. 이 변형은 본의 가중치와 연결된 버텍스의 원래 위치를 기반으로 계산됩니다.
- 메쉬 업데이트: 본의 움직임에 따른 버텍스의 변형 정보를 바탕으로 메쉬 모델이 실시간으로 업데이트됩니다. 이 과정을 통해 캐릭터나 오브젝트에 자연스러운 움직임이 구현됩니다.
// 모델 애니메이터의 업데이트 함수 void ModelAnimator::Update() { // 모델이 설정되지 않았다면 업데이트를 진행하지 않음 if (_model == nullptr) return; // 텍스처가 없으면 생성 if (_texture == nullptr) CreateTexture(); // 애니메이션 재생을 위한 시간 누적 _keyframeDesc.sumTime += DT; // 현재 재생 중인 애니메이션 가져오기 shared_ptr<ModelAnimation> current = _model->GetAnimationByIndex(_keyframeDesc.animIndex); if (current) { // 프레임 당 시간 계산 (프레임레이트와 재생 속도 고려) float timePerFrame = 1 / (current->frameRate * _keyframeDesc.speed); // 누적 시간이 프레임 당 시간을 초과하면 다음 프레임으로 if (_keyframeDesc.sumTime >= timePerFrame) { _keyframeDesc.sumTime = 0.f; _keyframeDesc.currFrame = (_keyframeDesc.currFrame + 1) % current->frameCount; _keyframeDesc.nextFrame = (_keyframeDesc.currFrame + 1) % current->frameCount; } // 다음 프레임으로 넘어가는 비율 계산 _keyframeDesc.ratio = (_keyframeDesc.sumTime / timePerFrame); } // ImGui를 통해 애니메이션 인덱스와 속도 조절 ImGui::InputInt("AnimIndex", &_keyframeDesc.animIndex); _keyframeDesc.animIndex %= _model->GetAnimationCount(); ImGui::InputFloat("Speed", &_keyframeDesc.speed, 0.5f, 4.f); // 현재 프레임 정보를 렌더러에 전달 RENDER->PushKeyframeData(_keyframeDesc); // 셰이더 리소스 뷰(SRV)를 통해 변형 맵 정보 전달 _shader->GetSRV("TransformMap")->SetResource(_srv.Get()); // 본 데이터 준비 BoneDesc boneDesc; // 모델의 본 갯수 가져오기 const uint32 boneCount = _model->GetBoneCount(); for (uint32 i = 0; i < boneCount; i++) { shared_ptr<ModelBone> bone = _model->GetBoneByIndex(i); // 각 본의 변환 정보를 본 설명자에 저장 boneDesc.transforms[i] = bone->transform; } // 렌더러에 본 데이터 전달 RENDER->PushBoneData(boneDesc); // 월드 변환 행렬 전달 auto world = GetTransform()->GetWorldMatrix(); RENDER->PushTransformData(TransformDesc{ world }); // 모델의 메시들을 순회하며 렌더링 준비 const auto& meshes = _model->GetMeshes(); for (auto& mesh : meshes) { // 메시의 재질이 있으면 업데이트 if (mesh->material) mesh->material->Update(); // 셰이더에 본 인덱스 설정 _shader->GetScalar("BoneIndex")->SetInt(mesh->boneIndex); // 메시의 정점 버퍼 설정 uint32 stride = mesh->vertexBuffer->GetStride(); uint32 offset = mesh->vertexBuffer->GetOffset(); // 드로우 콜 DC->IASetVertexBuffers(0, 1, mesh->vertexBuffer->GetComPtr().GetAddressOf(), &stride, &offset); DC->IASetIndexBuffer(mesh->indexBuffer->GetComPtr().Get(), DXGI_FORMAT_R32_UINT, 0); // 셰이더를 사용하여 인덱스 버퍼를 기반으로 메시를 렌더링 _shader->DrawIndexed(0, _pass, mesh->indexBuffer->GetCount(), 0, 0); } }
본 기반 애니메이션의 장점
- 효율성: 하나의 본 구조로 다양한 애니메이션을 생성할 수 있어, 모델링과 애니메이션 작업이 효율적으로 이루어집니다.
- 리얼리즘: 본을 사용하여 복잡한 움직임을 정교하게 제어할 수 있어, 모델에 보다 생동감을 부여할 수 있습니다.
- 재사용성: 본 구조는 다양한 모델에 재사용이 가능하여, 작업 시간과 비용을 절감할 수 있습니다.
애니메이션 데이터의 이해
애니메이션 데이터는 주로 키 프레임(Keyframe) 기반으로 저장됩니다. 각 키 프레임은 특정 시점에서의 객체나 캐릭터의 상태를 나타내며, 시간에 따라 이러한 키 프레임들 사이를 보간(Interpolation)하여 부드러운 움직임을 생성합니다.
struct KeyframeDesc { int animIndex; // 애니메이션 인덱스 uint currFrame; // 현재 프레임 uint nextFrame; // 다음 프레임 float ratio; // 현재 프레임과 다음 프레임 사이의 보간 비율 float sumTime; // 애니메이션 진행 시간 합계 float speed; // 애니메이션 재생 속도 float2 padding; // 패딩 };
애니메이션 데이터 읽기
애니메이션 데이터를 읽는 첫 단계는 애니메이션 파일의 형식을 이해하고, 필요한 데이터를 추출하는 것입니다. 이 과정에서 Assimp와 같은 라이브러리를 사용하여 다양한 3D 모델링 파일 형식에서 애니메이션 데이터를 쉽게 읽을 수 있습니다.
예제: 애니메이션 데이터 읽기
다음은 Assimp 라이브러리를 사용하여 애니메이션 데이터를 읽는 예제 코드입니다.
// Assimp 애니메이션 데이터를 읽어 사용자 정의 애니메이션 객체로 변환하는 함수 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; // 애니메이션 노드를 캐싱하기 위한 맵을 선언합니다. map<string, shared_ptr<asAnimationNode>> cacheAnimNodes; // Assimp 애니메이션 채널을 순회하며 노드별 애니메이션 데이터를 처리합니다. for (uint32 i = 0; i < srcAnimation->mNumChannels; i++) { aiNodeAnim* srcNode = srcAnimation->mChannels[i]; // 애니메이션 노드를 파싱합니다. 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; } // 애니메이션 노드를 파싱하는 함수 shared_ptr<asAnimationNode> Converter::ParseAnimationNode(shared_ptr<asAnimation> animation, aiNodeAnim* srcNode) { // 새로운 애니메이션 노드 객체를 생성합니다. std::shared_ptr<asAnimationNode> node = make_shared<asAnimationNode>(); // 노드 이름을 설정합니다. node->name = srcNode->mNodeName.C_Str(); // 위치, 회전, 스케일 중 가장 많은 키프레임을 가진 것을 기준으로 총 키프레임 수를 결정합니다. 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(); // 현재 키프레임의 인덱스 // 위치, 회전, 스케일 키프레임을 처리합니다.각 키프레임의 시간과 데이터를 추출하여 frameData에 저장합니다. // Position 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); // 처리된 키프레임을 노드에 추가합니다. } // 애니메이션의 키프레임 수보다 노드의 키프레임 수가 적은 경우, 마지막 키프레임을 복제하여 채웁니다. 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; } // 애니메이션 데이터에서 특정 노드의 키프레임 데이터를 읽어 내부 데이터 구조에 저장하는 함수 void Converter::ReadKeyframeData(shared_ptr<asAnimation> animation, aiNode* node, map<string, shared_ptr<asAnimationNode>>& cache) { // 새로운 키프레임 객체를 생성합니다. shared_ptr<asKeyframe> keyframe = make_shared<asKeyframe>(); // 현재 노드(본)의 이름을 키프레임의 본 이름으로 설정합니다. keyframe->boneName = node->mName.C_Str(); // 현재 노드에 해당하는 애니메이션 노드를 찾습니다. shared_ptr<asAnimationNode> findNode = cache[node->mName.C_Str()]; // 애니메이션의 모든 프레임에 대해 반복합니다. for (uint32 i = 0; i < animation->frameCount; i++) { asKeyframeData frameData; // 키프레임 데이터 객체를 생성합니다. // 만약 현재 노드에 대한 애니메이션 노드가 캐시에서 찾아지지 않는 경우 if (findNode == nullptr) { // 노드의 변환 행렬을 가져와 전치한 뒤, 이를 기반으로 위치, 회전, 스케일 데이터를 추출합니다. Matrix transform(node->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); } // 처리된 키프레임 객체를 애니메이션의 키프레임 목록에 추가합니다. animation->keyframes.push_back(keyframe); // 현재 노드의 모든 자식 노드에 대해 재귀적으로 동일한 처리를 수행합니다. for (uint32 i = 0; i < node->mNumChildren; i++) ReadKeyframeData(animation, node->mChildren[i], cache); }
이 코드는 특정 애니메이션 파일에서 애니메이션 데이터를 읽어, 게임 엔진에서 사용할 수 있는 형식으로 내보내는 과정을 담고 있습니다.
애니메이션 데이터 내보내기
읽어들인 애니메이션 데이터를 게임 엔진에서 사용할 수 있는 형식으로 변환하고 저장하는 과정입니다. 변환 과정에서는 애니메이션 클립(Animation Clip)의 생성, 본(Bone)과의 연결, 키 프레임 데이터의 처리 등이 포함됩니다.
예제: 애니메이션 데이터 내보내기
애니메이션 데이터를 .clip 형식으로 내보내는 과정은 다음과 같습니다.
// 애니메이션 데이터를 내보내는 함수 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); // 애니메이션 파일 쓰기 } // 애니메이션 데이터를 파일로 저장하는 함수 void Converter::WriteAnimationData(shared_ptr<asAnimation> animation, wstring finalPath) { // 최종 파일 경로를 생성하고, 해당 경로의 부모 디렉토리를 만듭니다. auto path = filesystem::path(finalPath); filesystem::create_directory(path.parent_path()); // 파일 작성을 위한 FileUtils 객체를 생성하고 파일을 엽니다. 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()); // 변환 데이터 } }
이 코드는 변환된 애니메이션 데이터를 파일로 저장하며, 게임 내에서 해당 애니메이션을 재생할 수 있도록 준비합니다.
프로젝트 호출
animationShader
#include "00. Global.fx" // 전역 설정과 공유 변수 포함 #include "00. Light.fx" // 조명 처리를 위한 설정과 함수 포함 #define MAX_MODEL_TRANSFORMS 250 // 최대 본(뼈) 변환 수 정의 #define MAX_MODEL_KEYFRAMES 500 // 최대 키프레임 수 정의 // 키프레임 정보를 담는 구조체 정의 struct KeyframeDesc { int animIndex; // 애니메이션 인덱스 uint currFrame; // 현재 프레임 번호 uint nextFrame; // 다음 프레임 번호 float ratio; // 현재와 다음 프레임 사이의 보간 비율 float sumTime; // 총 경과 시간 float speed; // 애니메이션 재생 속도 float2 padding; // 패딩 }; // 키프레임 정보를 저장하는 상수 버퍼 cbuffer KeyframeBuffer { KeyframeDesc Keyframes; }; // 본 변환 정보를 저장하는 상수 버퍼 cbuffer BoneBuffer { matrix BoneTransforms[MAX_MODEL_TRANSFORMS]; }; uint BoneIndex; // 현재 처리 중인 본의 인덱스 Texture2DArray TransformMap; // 본 변환을 저장한 텍스처 배열 // 정점 데이터에 애니메이션 행렬을 적용하여 변환을 계산하는 함수 matrix GetAnimationMatrix(VertexTextureNormalTangentBlend input) { // 입력된 정점의 블렌드 인덱스와 가중치 float indices[4] = { input.blendIndices.x, input.blendIndices.y, input.blendIndices.z, input.blendIndices.w }; float weights[4] = { input.blendWeights.x, input.blendWeights.y, input.blendWeights.z, input.blendWeights.w }; // 현재 애니메이션 상태에 대한 정보 int animIndex = Keyframes.animIndex; int currFrame = Keyframes.currFrame; int nextFrame = Keyframes.nextFrame; float ratio = Keyframes.ratio; float4 c0, c1, c2, c3; // 현재 프레임의 변환 컴포넌트 float4 n0, n1, n2, n3; // 다음 프레임의 변환 컴포넌트 matrix curr = 0; // 현재 프레임의 변환 행렬 matrix next = 0; // 다음 프레임의 변환 행렬 matrix transform = 0; // 최종 계산된 변환 행렬 // 각 본에 대해 변환 행렬을 계산 for (int i = 0; i < 4; i++) { // 현재 프레임의 변환 컴포넌트 로드 c0 = TransformMap.Load(int4(indices[i] * 4 + 0, currFrame, animIndex, 0)); c1 = TransformMap.Load(int4(indices[i] * 4 + 1, currFrame, animIndex, 0)); c2 = TransformMap.Load(int4(indices[i] * 4 + 2, currFrame, animIndex, 0)); c3 = TransformMap.Load(int4(indices[i] * 4 + 3, currFrame, animIndex, 0)); curr = matrix(c0, c1, c2, c3); // 현재 프레임의 변환 행렬 생성 // 다음 프레임의 변환 컴포넌트 로드 n0 = TransformMap.Load(int4(indices[i] * 4 + 0, nextFrame, animIndex, 0)); n1 = TransformMap.Load(int4(indices[i] * 4 + 1, nextFrame, animIndex, 0)); n2 = TransformMap.Load(int4(indices[i] * 4 + 2, nextFrame, animIndex, 0)); n3 = TransformMap.Load(int4(indices[i] * 4 + 3, nextFrame, animIndex, 0)); next = matrix(n0, n1, n2, n3); // 다음 프레임의 변환 행렬 생성 matrix result = lerp(curr, next, ratio); // 현재와 다음 프레임 사이 보간 transform += mul(weights[i], result); // 가중치 적용하여 변환 행렬 누적 } return transform; // 최종 변환 행렬 반환 } // 버텍스 셰이더: 정점 데이터를 애니메이션 변환 행렬로 변환 MeshOutput VS(VertexTextureNormalTangentBlend input) { MeshOutput output; matrix m = GetAnimationMatrix(input); // 애니메이션 변환 행렬 계산 output.position = mul(input.position, m); // 정점 위치 변환 output.position = mul(output.position, W); // 월드 변환 적용 output.worldPosition = output.position.xyz; // 월드 좌표 설정 output.position = mul(output.position, VP); // 뷰-프로젝션 변환 적용 output.uv = input.uv; // UV 좌표는 변경 없음 output.normal = mul(input.normal, (float3x3)W); // 법선 벡터 변환 output.tangent = mul(input.tangent, (float3x3)W); // 탄젠트 벡터 변환 return output; // 변환된 정점 데이터 반환 } // 기본 픽셀 셰이더: 텍스처 샘플링 결과 반환 float4 PS(MeshOutput input) : SV_TARGET { float4 color = DiffuseMap.Sample(LinearSampler, input.uv); // DiffuseMap에서 색상 샘플링 return color; } // 레드 컬러 픽셀 셰이더: 단일 색상(빨간색) 반환 float4 PS_RED(MeshOutput input) : SV_TARGET { return float4(1,0,0,1); // 빨간색 반환 } // 렌더링 기술 정의 technique11 T0 { PASS_VP(P0, VS, PS) // 기본 렌더링 패스 PASS_RS_VP(P1, FillModeWireFrame, VS, PS_RED) // 와이어프레임 모드 렌더링 패스 };
AnimationDemo.h
#pragma once #include "IExecute.h" class AnimationDemo : public IExecute { public: void Init() override; void Update() override; void Render() override; void CreateKachujin(); private: shared_ptr<Shader> _shader; shared_ptr<GameObject> _obj; shared_ptr<GameObject> _camera; };
AnimationDemo.cpp
#include "pch.h" #include "AnimationDemo.h" #include "GeometryHelper.h" #include "Camera.h" #include "GameObject.h" #include "CameraScript.h" #include "MeshRenderer.h" #include "Mesh.h" #include "Material.h" #include "Model.h" #include "ModelRenderer.h" #include "ModelAnimator.h" void AnimationDemo::Init() { RESOURCES->Init(); _shader = make_shared<Shader>(L"16. AnimationDemo.fx"); // Camera _camera = make_shared<GameObject>(); _camera->GetOrAddTransform()->SetWorldPosition(Vec3{ 0.f, 0.f, -5.f }); _camera->AddComponent(make_shared<Camera>()); _camera->AddComponent(make_shared<CameraScript>()); CreateKachujin(); //RENDER->Init(_shader); } void AnimationDemo::Update() { _camera->Update(); //RENDER->Update(); { LightDesc lightDesc; lightDesc.ambient = Vec4(0.4f); lightDesc.diffuse = Vec4(1.f); lightDesc.specular = Vec4(0.f); lightDesc.direction = Vec3(1.f, 0.f, 1.f); //RENDER->PushLightData(lightDesc); } { _obj->Update(); } } void AnimationDemo::Render() { } void AnimationDemo::CreateKachujin() { shared_ptr<class Model> m1 = make_shared<Model>(); m1->ReadModel(L"Kachujin/Kachujin"); m1->ReadMaterial(L"Kachujin/Kachujin"); m1->ReadAnimation(L"Kachujin/Idle"); m1->ReadAnimation(L"Kachujin/Run"); m1->ReadAnimation(L"Kachujin/Slash"); _obj = make_shared<GameObject>(); _obj->GetOrAddTransform()->SetWorldPosition(Vec3(0, 0, 1)); _obj->GetOrAddTransform()->SetWorldScale(Vec3(0.01f)); _obj->AddComponent(make_shared<ModelAnimator>(_shader)); { _obj->GetModelAnimator()->SetModel(m1); //_obj->GetModelAnimator()->SetPass(1); } }
결론
애니메이션 가져오기는 3D 게임 개발에서 캐릭터와 오브젝트에 다양한 움직임을 부여하는 데 필수적인 과정입니다. 애니메이션 데이터를 효율적으로 읽고, 게임 엔진에서 사용할 수 있도록 변환하는 방법을 이해하고 적용함으로써, 보다 생동감 있는 게임 세계를 구현할 수 있습니다.
반응형다음글이전글이전 글이 없습니다.댓글