-
[DirectX11] 애니메이션 트위닝(Animation Tweening)2024년 03월 12일
- 유니얼
-
작성자
-
2024.03.12.:06
728x90DirectX 11로 게임 엔진 아키텍처 만들기
게임 개발에서 애니메이션은 캐릭터와 환경을 생동감 있게 만드는 핵심 요소입니다. 복잡한 애니메이션 시스템 중에서도 '애니메이션 트위닝(Animation Tweening)'은 중요한 개념입니다. 이 글에서는 DirectX11을 사용한 게임 엔진 아키텍처 구축 과정에서 애니메이션 트위닝에 대해 알아보고, 예제 코드를 통해 이를 어떻게 구현할 수 있는지 살펴보겠습니다.
참고강의 링크:
[게임 프로그래머 도약반] DirectX11 입문 강의 - 인프런
게임 프로그래머 공부에 있어서 필수적인 DirectX 11 지식을 초보자들의 눈높이에 맞춰 설명하는 강의입니다., [사진][사진] [사진] 게임 개발자는 Unreal, Unity만 사용할 줄 알면 되는 거 아닌가요? 엔
www.inflearn.com
애니메이션 트위닝이란?
애니메이션 트위닝은 두 키 프레임 사이의 중간 상태를 자동으로 생성하여 부드러운 애니메이션 효과를 만드는 기법입니다. 예를 들어, 캐릭터가 한 포즈에서 다른 포즈로 이동할 때, 단순히 두 상태 사이를 직접 전환하는 것이 아니라, 중간 단계의 포즈를 계산하여 자연스러운 움직임을 생성합니다. 이는 보간(Interpolation) 기법을 사용하여 구현됩니다.
DirectX11에서의 애니메이션 트위닝 구현
DirectX11과 같은 저수준 그래픽 API를 사용할 때, 애니메이션 트위닝을 구현하기 위해서는 몇 가지 핵심 단계를 거쳐야 합니다. 아래는 애니메이션 트위닝을 구현하는 기본적인 절차입니다.
애니메이션 데이터 준비
애니메이션을 구성하는 키 프레임 데이터를 준비합니다. 각 키 프레임은 캐릭터 또는 객체의 특정 시점에서의 상태를 나타냅니다.
struct KeyframeDesc { int animIndex; // 애니메이션 인덱스 uint currFrame; // 현재 프레임 uint nextFrame; // 다음 프레임 float ratio; // 현재 프레임과 다음 프레임 사이의 보간 비율 float sumTime; // 애니메이션 진행 시간 합계 float speed; // 애니메이션 재생 속도 float2 padding; // 패딩 }; struct TweenFrameDesc { float tweenDuration; // 트윈(중간 상태) 지속 시간 float tweenRatio; // 트윈 비율 float tweenSumTime; // 트윈 진행 시간 합계 float padding; // 패딩 KeyframeDesc curr; // 현재 키프레임 정보 KeyframeDesc next; // 다음 키프레임 정보 };
보간 계산
현재 프레임과 다음 프레임 사이의 중간 상태를 계산합니다. 이 과정에서 선형 보간(Linear Interpolation) 또는 다른 보간 방법을 사용할 수 있습니다.
// 애니메이션 행렬을 계산하는 함수 matrix GetAnimationMatrix(VertexModel 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[2]; int currFrame[2]; int nextFrame[2]; float ratio[2]; // 인덱스와 가중치를 사용하여 본 변환 행렬을 계산하고, animIndex[0] = TweenFrames[input.instanceID].curr.animIndex; currFrame[0] = TweenFrames[input.instanceID].curr.currFrame; nextFrame[0] = TweenFrames[input.instanceID].curr.nextFrame; ratio[0] = TweenFrames[input.instanceID].curr.ratio; animIndex[1] = TweenFrames[input.instanceID].next.animIndex; currFrame[1] = TweenFrames[input.instanceID].next.currFrame; nextFrame[1] = TweenFrames[input.instanceID].next.nextFrame; ratio[1] = TweenFrames[input.instanceID].next.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[0], animIndex[0], 0)); c1 = TransformMap.Load(int4(indices[i] * 4 + 1, currFrame[0], animIndex[0], 0)); c2 = TransformMap.Load(int4(indices[i] * 4 + 2, currFrame[0], animIndex[0], 0)); c3 = TransformMap.Load(int4(indices[i] * 4 + 3, currFrame[0], animIndex[0], 0)); curr = matrix(c0, c1, c2, c3); n0 = TransformMap.Load(int4(indices[i] * 4 + 0, nextFrame[0], animIndex[0], 0)); n1 = TransformMap.Load(int4(indices[i] * 4 + 1, nextFrame[0], animIndex[0], 0)); n2 = TransformMap.Load(int4(indices[i] * 4 + 2, nextFrame[0], animIndex[0], 0)); n3 = TransformMap.Load(int4(indices[i] * 4 + 3, nextFrame[0], animIndex[0], 0)); next = matrix(n0, n1, n2, n3); matrix result = lerp(curr, next, ratio[0]); if (animIndex[1] >= 0) { c0 = TransformMap.Load(int4(indices[i] * 4 + 0, currFrame[1], animIndex[1], 0)); c1 = TransformMap.Load(int4(indices[i] * 4 + 1, currFrame[1], animIndex[1], 0)); c2 = TransformMap.Load(int4(indices[i] * 4 + 2, currFrame[1], animIndex[1], 0)); c3 = TransformMap.Load(int4(indices[i] * 4 + 3, currFrame[1], animIndex[1], 0)); curr = matrix(c0, c1, c2, c3); n0 = TransformMap.Load(int4(indices[i] * 4 + 0, nextFrame[1], animIndex[1], 0)); n1 = TransformMap.Load(int4(indices[i] * 4 + 1, nextFrame[1], animIndex[1], 0)); n2 = TransformMap.Load(int4(indices[i] * 4 + 2, nextFrame[1], animIndex[1], 0)); n3 = TransformMap.Load(int4(indices[i] * 4 + 3, nextFrame[1], animIndex[1], 0)); next = matrix(n0, n1, n2, n3); matrix nextResult = lerp(curr, next, ratio[1]); result = lerp(result, nextResult, TweenFrames[input.instanceID].tweenRatio); } transform += mul(weights[i], result); } return transform; }
트위닝 데이터 업데이트
계산된 보간 데이터를 기반으로, 트위닝 데이터를 업데이트합니다. 이 데이터는 애니메이션의 현재 상태를 나타내며, 렌더링 시 사용됩니다.
void ModelAnimator::Update() { if (_model == nullptr) return; if (_texture == nullptr) CreateTexture(); TweenDesc& desc = _tweenDesc; desc.curr.sumTime += DT; // 현재 애니메이션 { shared_ptr<ModelAnimation> currentAnim = _model->GetAnimationByIndex(desc.curr.animIndex); if (currentAnim) { float timePerFrame = 1 / (currentAnim->frameRate * desc.curr.speed); if (desc.curr.sumTime >= timePerFrame) { desc.curr.sumTime = 0; desc.curr.currFrame = (desc.curr.currFrame + 1) % currentAnim->frameCount; desc.curr.nextFrame = (desc.curr.currFrame + 1) % currentAnim->frameCount; } desc.curr.ratio = (desc.curr.sumTime / timePerFrame); } } // 다음 애니메이션이 예약 되어 있다면 if (desc.next.animIndex >= 0) { desc.tweenSumTime += DT; desc.tweenRatio = desc.tweenSumTime / desc.tweenDuration; if (desc.tweenRatio >= 1.f) { // 애니메이션 교체 성공 desc.curr = desc.next; desc.ClearNextAnim(); } else { // 교체중 shared_ptr<ModelAnimation> nextAnim = _model->GetAnimationByIndex(desc.next.animIndex); desc.next.sumTime += DT; float timePerFrame = 1.f / (nextAnim->frameRate * desc.next.speed); if (desc.next.ratio >= 1.f) { desc.next.sumTime = 0; desc.next.currFrame = (desc.next.currFrame + 1) % nextAnim->frameCount; desc.next.nextFrame = (desc.next.currFrame + 1) % nextAnim->frameCount; } desc.next.ratio = desc.next.sumTime / timePerFrame; } } // Anim Update ImGui::InputInt("AnimIndex", &desc.curr.animIndex); _keyframeDesc.animIndex %= _model->GetAnimationCount(); static int32 nextAnimIndex = 0; if (ImGui::InputInt("NextAnimIndex", &nextAnimIndex)) { nextAnimIndex %= _model->GetAnimationCount(); desc.ClearNextAnim(); // 기존꺼 밀어주기 desc.next.animIndex = nextAnimIndex; } if (_model->GetAnimationCount() > 0) desc.curr.animIndex %= _model->GetAnimationCount(); ImGui::InputFloat("Speed", &desc.curr.speed, 0.5f, 4.f); RENDER->PushTweenData(desc); // SRV를 통해 정보 전달 _shader->GetSRV("TransformMap")->SetResource(_srv.Get()); // Bones 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); // Transform auto world = GetTransform()->GetWorldMatrix(); RENDER->PushTransformData(TransformDesc{ world }); const auto& meshes = _model->GetMeshes(); for (auto& mesh : meshes) { if (mesh->material) mesh->material->Update(); // BoneIndex _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); } }
GPU 데이터 전송
업데이트된 애니메이션 데이터를 GPU에 전송합니다. 이 데이터는 셰이더에서 캐릭터나 객체의 변형을 계산하는 데 사용됩니다.
프로젝트 호출
TweenDemo.h
#pragma once #include "IExecute.h" class TweenDemo : 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; };
TweenDemo.cpp
#include "pch.h" #include "TweenDemo.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 TweenDemo::Init() { RESOURCES->Init(); _shader = make_shared<Shader>(L"17. TweenDemo.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 TweenDemo::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 TweenDemo::Render() { } void TweenDemo::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); } }
결론
애니메이션 트위닝은 게임 내 캐릭터와 객체들의 움직임을 자연스럽고 매끄럽게 만드는 데 필수적인 기술입니다. DirectX11 같은 저수준 API를 사용하면, 애니메이션 시스템을 더 세밀하게 제어할 수 있으며, 게임의 성능과 품질을 향상시킬 수 있습니다.
반응형다음글이전글이전 글이 없습니다.댓글