• 티스토리 홈
  • 프로필사진
    유니얼
  • 방명록
  • 공지사항
  • 태그
  • 블로그 관리
  • 글 작성
유니얼
  • 프로필사진
    유니얼
    • 분류 전체보기 (295)
      • Unity (17)
        • 게임 개발 (5)
      • Unreal (24)
        • 게임 개발 (20)
      • DirectX (36)
      • 코딩테스트 (91)
        • 프로그래머스 (25)
        • 백준 (66)
      • Google Workspace (1)
      • Programing (102)
        • C# (68)
        • C++ (24)
        • JavaScript (10)
      • 게임 서버 프로그래밍 (17)
      • Web (6)
        • 슈퍼코딩 (6)
  • 방문자 수
    • 전체:
    • 오늘:
    • 어제:
  • 최근 댓글
    등록된 댓글이 없습니다.
  • 최근 공지
    등록된 공지가 없습니다.
# Home
# 공지사항
#
# 태그
# 검색결과
# 방명록
  • [DirectX11] 애니메이션 트위닝(Animation Tweening)
    2024년 03월 12일
    • 유니얼
    • 작성자
    • 2024.03.12.:06
    728x90

    DirectX 11로 게임 엔진 아키텍처 만들기

    게임 개발에서 애니메이션은 캐릭터와 환경을 생동감 있게 만드는 핵심 요소입니다. 복잡한 애니메이션 시스템 중에서도 '애니메이션 트위닝(Animation Tweening)'은 중요한 개념입니다. 이 글에서는 DirectX11을 사용한 게임 엔진 아키텍처 구축 과정에서 애니메이션 트위닝에 대해 알아보고, 예제 코드를 통해 이를 어떻게 구현할 수 있는지 살펴보겠습니다.

     

    Result

    참고강의 링크:

    https://www.inflearn.com/course/directx11-%EA%B2%8C%EC%9E%84%EA%B0%9C%EB%B0%9C-%EB%8F%84%EC%95%BD%EB%B0%98/dashboard

     

    [게임 프로그래머 도약반] 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를 사용하면, 애니메이션 시스템을 더 세밀하게 제어할 수 있으며, 게임의 성능과 품질을 향상시킬 수 있습니다.

    반응형
    다음글
    다음 글이 없습니다.
    이전글
    이전 글이 없습니다.
    댓글
조회된 결과가 없습니다.
스킨 업데이트 안내
현재 이용하고 계신 스킨의 버전보다 더 높은 최신 버전이 감지 되었습니다. 최신버전 스킨 파일을 다운로드 받을 수 있는 페이지로 이동하시겠습니까?
("아니오" 를 선택할 시 30일 동안 최신 버전이 감지되어도 모달 창이 표시되지 않습니다.)
목차
표시할 목차가 없습니다.
    • 안녕하세요
    • 감사해요
    • 잘있어요

    티스토리툴바